Repository: yargs/yargs
Pull request #461 (2016-03-31 04:17:51)

Avoid repeating options in completion
This makes sure that an option won't be suggested for completion if the option, or any of its aliases, is already in the provided arguments.

Pretty much the same thing that was done for command completion in https://github.com/yargs/yargs/pull/183

I figure this is a good start, but it only checks options in **long form**. Checking for short form depends on whether 'short-option-groups' is enabled or not, and would need to duplicate parts of the parser logic, so it is a bit tricky.

I also had to modify slightly the integration test that checks that the completion for '--help' works. Now it won't complete if the current word is '--help', so I made it try to complete '--h'


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14b52e6..b1fb493 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,12 @@
-## Change Log
+### v4.4.0 (2016/04/03 21:10 +07:00)
 
-### v4.3.3 (2016/4/20 22:00 +07:00)
-
-- [446](https://github.com/yargs/yargs/pull/446) update yargs-parser, y18n, nyc, cliui (@bcoe)
+- [#454](https://github.com/yargs/yargs/pull/454) fix demand() when second argument is an array (@elas7)
+- [#452](https://github.com/yargs/yargs/pull/452) fix code example for `.help()` docs (@maxrimue)
+- [#450](https://github.com/yargs/yargs/pull/450) fix for bash completion trailing space edge-case (@elas7)
+- [#448](https://github.com/yargs/yargs/pull/448) allow a method to be passed to `showHelp`, rather than a log-level (@osher)
+- [#446](https://github.com/yargs/yargs/pull/446) update yargs-parser, y18n, nyc, cliui, pkg-conf (@bcoe)
+- [#436](https://github.com/yargs/yargs/pull/436) the rebase method is only used by tests, do not export it in two places (@elas7)
+- [#428](https://github.com/yargs/yargs/pull/428) initial support for subcommands (@nexdrew)
 
 ### v4.3.2 (2016/3/20 15:07 +07:00)
 
@@ -10,7 +14,6 @@
 - [#443](https://github.com/yargs/yargs/pull/443) adds Italian translation \o/ (@madrisan)
 - [#441](https://github.com/yargs/yargs/pull/441) remove duplicate keys from array options configuration (@elas7)
 - [#437](https://github.com/yargs/yargs/pull/437) standardize tests for .command() (@lrlna)
-- [#436](https://github.com/yargs/yargs/pull/436) the rebase method is only used by tests, do not export it in two places (@elas7)
 
 ### v4.3.0 (2016/3/12 14:19 +07:00)
 
diff --git a/README.md b/README.md
index fd90f0e..cf8fdce 100644
--- a/README.md
+++ b/README.md
@@ -454,9 +454,11 @@ var argv = require('yargs')
 ```
 
 .command(cmd, desc, [builder], [handler])
--------------------
+-----------------------------------------
 .command(cmd, desc, [module])
--------------------
+-----------------------------
+.command(module)
+----------------
 
 Document the commands exposed by your application.
 
@@ -528,16 +530,33 @@ yargs.command('get <source> [proxy]', 'make a get HTTP request')
   .argv
 ```
 
+#### Variadic Positional Arguments
+
+The last positional argument can optionally accept an array of
+values, by using the `..` operator:
+
+```js
+yargs.command('download <url> [files..]', 'download several files')
+  .help()
+  .argv
+```
+
 ### Providing a Command Module
 
 For complicated commands you can pull the logic into a module. A module
 simply needs to export:
 
-* `exports.builder`: which describes the options that a command accepts.
+* `exports.command`: string that executes this command when given on the command line, may contain positional args
+* `exports.describe`: string used as the description for the command in help text, use `false` for a hidden command
+* `exports.builder`: object declaring the options the command accepts, or a function accepting and returning a yargs instance
 * `exports.handler`: a function which will be passed the parsed argv.
 
 ```js
 // my-module.js
+exports.command = 'get <source> [proxy]'
+
+exports.describe = 'make a get HTTP request'
+
 exports.builder = {
   banana: {
     default: 'cool'
@@ -554,6 +573,14 @@ exports.handler = function (argv) {
 
 You then register the module like so:
 
+```js
+yargs.command(require('my-module'))
+  .help()
+  .argv
+```
+
+Or if the module does not export `command` and `describe` (or if you just want to override them):
+
 ```js
 yargs.command('get <source> [proxy]', 'make a get HTTP request', require('my-module'))
   .help()
@@ -622,13 +649,15 @@ var argv = require('yargs')
   .argv;
 ```
 
-<a name="config"></a>.config(key, [description], [parseFn])
+<a name="config"></a>.config([key], [description], [parseFn])
 ------------
 
 Tells the parser that if the option specified by `key` is passed in, it
 should be interpreted as a path to a JSON config file. The file is loaded
 and parsed, and its properties are set as arguments.
 
+If invoked without parameters, `.config()` will make `--config` the option to pass the JSON config file.
+
 An optional `description` can be provided to customize the config (`key`) option
 in the usage string.
 
@@ -816,7 +845,19 @@ yargs have been validated.
 
 Method to execute when a failure occurs, rather than printing the failure message.
 
-`fn` is called with the failure message that would have been printed.
+`fn` is called with the failure message that would have been printed and the
+`Error` instance originally thrown, if any.
+
+```js
+var argv = require('yargs')
+  .fail(function (msg, err) {
+    if (err) throw err // preserve stack
+    console.error('You broke it!')
+    console.error(msg)
+    process.exit(1)
+  })
+  .argv
+```
 
 <a name="global"></a>.global(globals)
 ------------
@@ -957,10 +998,12 @@ Locales currently supported:
 * **es:** Spanish.
 * **fr:** French.
 * **id:** Indonesian.
+* **it:** Italian.
 * **ja:** Japanese.
 * **ko:** Korean.
 * **nb:** Norwegian Bokmål.
 * **pirate:** American Pirate.
+* **pl:** Polish.
 * **pt:** Portuguese.
 * **pt_BR:** Brazilian Portuguese.
 * **tr:** Turkish.
diff --git a/lib/command.js b/lib/command.js
index 6e38b48..7665edc 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -45,15 +45,26 @@ module.exports = function (yargs, usage, validation) {
 
   function parseCommand (cmd) {
     var splitCommand = cmd.split(/\s/)
-    var bregex = /[\][<>]/g
+    var bregex = /\.*[\][<>]/g
     var parsedCommand = {
       cmd: (splitCommand.shift()).replace(bregex, ''),
       demanded: [],
       optional: []
     }
-    splitCommand.forEach(function (cmd) {
-      if (/^\[/.test(cmd)) parsedCommand.optional.push(cmd.replace(bregex, ''))
-      else parsedCommand.demanded.push(cmd.replace(bregex, ''))
+    splitCommand.forEach(function (cmd, i) {
+      var variadic = false
+      if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true
+      if (/^\[/.test(cmd)) {
+        parsedCommand.optional.push({
+          cmd: cmd.replace(bregex, ''),
+          variadic: variadic
+        })
+      } else {
+        parsedCommand.demanded.push({
+          cmd: cmd.replace(bregex, ''),
+          variadic: variadic
+        })
+      }
     })
     return parsedCommand
   }
@@ -113,15 +124,19 @@ module.exports = function (yargs, usage, validation) {
     validation.positionalCount(demanded.length, argv._.length)
 
     while (demanded.length) {
-      if (!argv._.length) break
       var demand = demanded.shift()
-      argv[demand] = argv._.shift()
+      if (demand.variadic) argv[demand.cmd] = []
+      if (!argv._.length) break
+      if (demand.variadic) argv[demand.cmd] = argv._.splice(0)
+      else argv[demand.cmd] = argv._.shift()
     }
 
     while (optional.length) {
-      if (!argv._.length) break
       var maybe = optional.shift()
-      argv[maybe] = argv._.shift()
+      if (maybe.variadic) argv[maybe.cmd] = []
+      if (!argv._.length) break
+      if (maybe.variadic) argv[maybe.cmd] = argv._.splice(0)
+      else argv[maybe.cmd] = argv._.shift()
     }
 
     argv._ = context.commands.concat(argv._)
diff --git a/lib/completion.js b/lib/completion.js
index 8f917c4..4ba189a 100644
--- a/lib/completion.js
+++ b/lib/completion.js
@@ -14,6 +14,7 @@ module.exports = function (yargs, usage, command) {
     var current = process.argv[process.argv.length - 1]
     var previous = process.argv.slice(process.argv.indexOf('--' + self.completionKey) + 1)
     var argv = yargs.parse(previous)
+    var aliases = yargs.parsed.aliases
 
     // a custom completion function can be provided
     // to completion().
@@ -57,7 +58,14 @@ module.exports = function (yargs, usage, command) {
 
     if (current.match(/^-/)) {
       Object.keys(yargs.getOptions().key).forEach(function (key) {
-        completions.push('--' + key)
+        // If the key and its aliases aren't in 'previous', add the key to 'completions'
+        var keyAndAliases = [key].concat(aliases[key] || [])
+        var notInPrevious = keyAndAliases.every(function (val) {
+          return previous.indexOf('--' + val) === -1
+        })
+        if (notInPrevious) {
+          completions.push('--' + key)
+        }
       })
     }
 
diff --git a/locales/en.json b/locales/en.json
index ce19199..0fab5cc 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -5,6 +5,7 @@
   "boolean": "boolean",
   "count": "count",
   "string": "string",
+  "number": "number",
   "array": "array",
   "required": "required",
   "default:": "default:",
diff --git a/locales/it.json b/locales/it.json
index 85a5cd2..bd9573a 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -5,6 +5,7 @@
   "boolean": "booleano",
   "count": "contatore",
   "string": "stringa",
+  "number": "numero",
   "array": "vettore",
   "required": "richiesto",
   "default:": "predefinito:",
diff --git a/package.json b/package.json
index 76ee28f..e92946a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "yargs",
-  "version": "4.3.2",
+  "version": "4.4.0",
   "description": "Light-weight option parsing with an argv hash. No optstrings attached.",
   "main": "./index.js",
   "files": [
@@ -13,22 +13,23 @@
   ],
   "dependencies": {
     "camelcase": "^2.0.1",
-    "cliui": "^3.1.1",
+    "cliui": "^3.1.2",
     "decamelize": "^1.1.1",
     "lodash.assign": "^4.0.3",
     "os-locale": "^1.4.0",
-    "pkg-conf": "^1.1.1",
+    "pkg-conf": "^1.1.2",
     "read-pkg-up": "^1.0.1",
     "require-main-filename": "^1.0.1",
     "string-width": "^1.0.1",
     "window-size": "^0.2.0",
     "y18n": "^3.2.1",
-    "yargs-parser": "^2.1.2"
+    "yargs-parser": "^2.2.0"
   },
   "devDependencies": {
     "chai": "^3.4.1",
-    "chalk": "^1.1.1",
-    "coveralls": "^2.11.4",
+    "chalk": "^1.1.3",
+    "conventional-recommended-workflow": "^1.0.0",
+    "coveralls": "^2.11.9",
     "cpr": "^1.0.0",
     "es6-promise": "^3.0.2",
     "hashish": "0.0.4",
@@ -42,7 +43,8 @@
   "scripts": {
     "pretest": "standard",
     "test": "nyc --cache mocha --require ./test/before.js --timeout=4000 --check-leaks",
-    "coverage": "nyc report --reporter=text-lcov | coveralls"
+    "coverage": "nyc report --reporter=text-lcov | coveralls",
+    "release": "conventional-recommended-workflow"
   },
   "repository": {
     "type": "git",
diff --git a/yargs.js b/yargs.js
index c9e86d4..03ff310 100644
--- a/yargs.js
+++ b/yargs.js
@@ -35,9 +35,9 @@ function Yargs (processArgs, cwd, parentRequire) {
     .map(function (x, i) {
       // ignore the node bin, specify this in your
       // bin file with #!/usr/bin/env node
-      if (i === 0 && /\b(node|iojs)$/.test(x)) return
+      if (i === 0 && /\b(node|iojs)(\.exe)?$/.test(x)) return
       var b = rebase(cwd, x)
-      return x.match(/^\//) && b.length < x.length ? b : x
+      return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x
     })
     .join(' ').trim()
 
@@ -179,6 +179,8 @@ function Yargs (processArgs, cwd, parentRequire) {
       parseFn = msg
       msg = null
     }
+
+    key = key || 'config'
     self.describe(key, msg || usage.deferY18nLookup('Path to JSON config file'))
     ;(Array.isArray(key) ? key : [key]).forEach(function (k) {
       options.config[k] = parseFn || true


Test patch:
diff --git a/test/command.js b/test/command.js
index ec08c32..5f5b696 100644
--- a/test/command.js
+++ b/test/command.js
@@ -17,8 +17,14 @@ describe('Command', function () {
         })
       var command = y.getCommandInstance()
       var handlers = command.getCommandHandlers()
-      handlers.foo.demanded.should.include('bar')
-      handlers.foo.optional.should.include('awesome')
+      handlers.foo.demanded.should.include({
+        cmd: 'bar',
+        variadic: false
+      })
+      handlers.foo.optional.should.include({
+        cmd: 'awesome',
+        variadic: false
+      })
     })
 
     it('populates inner argv with positional arguments', function (done) {
@@ -60,6 +66,55 @@ describe('Command', function () {
     })
   })
 
+  describe('variadic', function () {
+    it('allows required arguments to be variadic', function () {
+      var argv = yargs('foo /root file1 file2 file3')
+        .command('foo <root> <files..>')
+        .argv
+
+      argv.root.should.equal('/root')
+      argv.files.should.deep.equal(['file1', 'file2', 'file3'])
+    })
+
+    it('allows optional arguments to be variadic', function () {
+      var argv = yargs('foo /root file1 file2 file3')
+        .command('foo <root> [files..]')
+        .argv
+
+      argv.root.should.equal('/root')
+      argv.files.should.deep.equal(['file1', 'file2', 'file3'])
+    })
+
+    it('fails if required arguments are missing', function (done) {
+      yargs('foo /root')
+        .command('foo <root> <files..>')
+        .fail(function (err) {
+          err.should.match(/Not enough non-option arguments/)
+          return done()
+        })
+        .argv
+    })
+
+    it('does not fail if zero optional arguments are provided', function () {
+      var argv = yargs('foo /root')
+        .command('foo <root> [files...]')
+        .argv
+
+      argv.root.should.equal('/root')
+      argv.files.should.deep.equal([])
+    })
+
+    it('only allows the last argument to be variadic', function () {
+      var argv = yargs('foo /root file1 file2')
+        .command('foo <root..> <file>')
+        .argv
+
+      argv.root.should.equal('/root')
+      argv.file.should.equal('file1')
+      argv._.should.include('file2')
+    })
+  })
+
   describe('missing positional arguments', function () {
     it('fails if a required argument is missing', function (done) {
       var argv = yargs('foo hello')
diff --git a/test/completion.js b/test/completion.js
index 017e8c7..45de21b 100644
--- a/test/completion.js
+++ b/test/completion.js
@@ -38,6 +38,36 @@ describe('Completion', function () {
       r.logs.should.not.include('apple')
     })
 
+    it('avoids repeating already included options', function () {
+      var r = checkUsage(function () {
+        return yargs(['--get-yargs-completions'])
+          .options({
+            foo: {describe: 'foo option'},
+            bar: {describe: 'bar option'}
+          })
+          .completion()
+          .argv
+      }, ['./completion', '--get-yargs-completions', './completion', '--foo', '--'])
+
+      r.logs.should.include('--bar')
+      r.logs.should.not.include('--foo')
+    })
+
+    it('avoids repeating options whose aliases are already included', function () {
+      var r = checkUsage(function () {
+        return yargs(['--get-yargs-completions'])
+            .options({
+              foo: {describe: 'foo option', alias: 'f'},
+              bar: {describe: 'bar option'}
+            })
+            .completion()
+            .argv
+      }, ['./completion', '--get-yargs-completions', './completion', '--f', '--'])
+
+      r.logs.should.include('--bar')
+      r.logs.should.not.include('--foo')
+    })
+
     it('completes options for a command', function () {
       var r = checkUsage(function () {
         return yargs(['--get-yargs-completions'])
diff --git a/test/integration.js b/test/integration.js
index 2978c2b..7062174 100644
--- a/test/integration.js
+++ b/test/integration.js
@@ -44,7 +44,7 @@ describe('integration tests', function () {
 
   // see #177
   it('allows --help to be completed without returning help message', function (done) {
-    testCmd('./bin.js', [ '--get-yargs-completions', '--help' ], function (code, stdout) {
+    testCmd('./bin.js', [ '--get-yargs-completions', '--h' ], function (code, stdout) {
       if (code) {
         done(new Error('cmd exited with code ' + code))
         return
diff --git a/test/usage.js b/test/usage.js
index 6bcbd4e..05bbcd9 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -2070,7 +2070,7 @@ describe('usage tests', function () {
       ])
     })
 
-    it('when passed a string - should call the correct console.log method', function () {
+    it('should call the correct console.log method', function () {
       var r = checkUsage(function () {
         var y = yargs(['--foo'])
           .options('foo', {
@@ -2089,30 +2089,23 @@ describe('usage tests', function () {
         ''
       ])
     })
-    it('when passed a handler - should provide the help string to the handler', function () {
-      var testlogs = []
-      var r = checkUsage(function () {
-        var y = yargs(['--foo'])
-          .options('foo', {
-            alias: 'f',
-            describe: 'foo option'
-          })
-          .wrap(null)
-
-        y.showHelp(testHandler)
-      })
-
-      r.errors.length.should.eql(0)
-      r.logs.length.should.eql(0)
 
-      testlogs.join('\n').split(/\n+/).should.deep.equal([
-        'Options:',
-        '  --foo, -f  foo option',
-        ''
-      ])
+    it('should provide a help string to handler when provided', function (done) {
+      var y = yargs(['--foo'])
+        .options('foo', {
+          alias: 'f',
+          describe: 'foo option'
+        })
+        .wrap(null)
 
+      y.showHelp(testHandler)
       function testHandler (msg) {
-        testlogs.push(msg)
+        msg.split(/\n+/).should.deep.equal([
+          'Options:',
+          '  --foo, -f  foo option',
+          ''
+        ])
+        return done()
       }
     })
   })
@@ -2172,6 +2165,46 @@ describe('usage tests', function () {
         yargs([]).$0.should.equal('/code/iojs/script.js')
       })
     })
+
+    it('is detected correctly when argv contains "node.exe"', function () {
+      mockProcessArgv(['node.exe', 'script.js'], function () {
+        yargs([]).$0.should.equal('script.js')
+      })
+    })
+
+    it('is detected correctly when argv contains "iojs.exe"', function () {
+      mockProcessArgv(['iojs.exe', 'script.js'], function () {
+        yargs([]).$0.should.equal('script.js')
+      })
+    })
+
+    if (process.platform !== 'win32') {
+      it('is resolved to the relative path if it is shorter', function () {
+        mockProcessArgv(['node', '/code/node/script.js'], function () {
+          yargs([], '/code/python/').$0.should.equal('../node/script.js')
+        })
+      })
+
+      it('is not resolved to the relative path if it is larger', function () {
+        mockProcessArgv(['node', '/script.js'], function () {
+          yargs([], '/very/long/current/directory/').$0.should.equal('/script.js')
+        })
+      })
+    }
+
+    if (process.platform === 'win32') {
+      it('is resolved to the relative path if it is shorter, using Windows paths', function () {
+        mockProcessArgv(['node.exe', 'C:\\code\\node\\script.js'], function () {
+          yargs([], 'C:\\code\\python\\').$0.should.equal('..\\node\\script.js')
+        })
+      })
+
+      it('is not resolved to the relative path if it is larger, using Windows paths', function () {
+        mockProcessArgv(['node', 'C:\\script.js'], function () {
+          yargs([], 'C:\\very\\long\\current\\directory\\').$0.should.equal('C:\\script.js')
+        })
+      })
+    }
   })
 
   describe('choices', function () {
diff --git a/test/validation.js b/test/validation.js
index 70ba7fc..ac0b48a 100644
--- a/test/validation.js
+++ b/test/validation.js
@@ -240,6 +240,24 @@ describe('validation tests', function () {
       ])
     })
 
+    it('should be displayed in the help message with its default name', function () {
+      var checkUsage = require('./helpers/utils').checkOutput
+      var r = checkUsage(function () {
+        return yargs(['--help'])
+            .config()
+            .help('help')
+            .wrap(null)
+            .argv
+      })
+      r.should.have.property('logs').with.length(1)
+      r.logs.join('\n').split(/\n+/).should.deep.equal([
+        'Options:',
+        '  --config  Path to JSON config file',
+        '  --help    Show help  [boolean]',
+        ''
+      ])
+    })
+
     it('should allow help message to be overridden', function () {
       var checkUsage = require('./helpers/utils').checkOutput
       var r = checkUsage(function () {