Repository: yargs/yargs
Pull request #727 (2016-12-06 06:36:01)

feat: support for positional argument aliases
When migrating one of npm, Inc's internal chat-bots to yargs, I ran into some help strings that were documented like so:

`delete <user|org>    delete a user or organization`

Given that it's not a huge amount of work, I thought it would be worth adding this syntax to yargs' command API, in the form of positional argument aliases.

CC: @ceejbot, @nexdrew 

iarna: I will immediately make use of this when you land it. =D
nexdrew: I have one or two suggestions, will submit them as a review here soon.

Patch:
diff --git a/README.md b/README.md
index 2f4cacc..ea6aea9 100644
--- a/README.md
+++ b/README.md
@@ -609,6 +609,21 @@ yargs.command('get <source> [proxy]', 'make a get HTTP request')
   .argv
 ```
 
+#### Positional Argument Aliases
+
+Aliases can be provided for positional arguments using the `|` character.
+As an example, suppose our application allows either a username _or_
+an email as the first argument:
+
+```js
+yargs.command('get <username|email> [password]', 'fetch a user by username or email.')
+  .help()
+  .argv
+```
+
+In this way, both `argv.username` and `argv.email` would be populated with the
+same value when the command is executed.
+
 #### Variadic Positional Arguments
 
 The last positional argument can optionally accept an array of
@@ -1152,7 +1167,7 @@ Method to execute when a failure occurs, rather than printing the failure messag
 
 `fn` is called with the failure message that would have been printed, the
 `Error` instance originally thrown and yargs state when the failure
-occured. 
+occured.
 
 ```js
 var argv = require('yargs')
diff --git a/lib/command.js b/lib/command.js
index e73340d..c1afc1c 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -96,7 +96,7 @@ module.exports = function (yargs, usage, validation) {
 
   function parseCommand (cmd) {
     var extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ')
-    var splitCommand = extraSpacesStrippedCommand.split(/\s/)
+    var splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/)
     var bregex = /\.*[\][<>]/g
     var parsedCommand = {
       cmd: (splitCommand.shift()).replace(bregex, ''),
@@ -105,15 +105,16 @@ module.exports = function (yargs, usage, validation) {
     }
     splitCommand.forEach(function (cmd, i) {
       var variadic = false
+      cmd = cmd.replace(/\s/g, '')
       if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) variadic = true
       if (/^\[/.test(cmd)) {
         parsedCommand.optional.push({
-          cmd: cmd.replace(bregex, ''),
+          cmd: cmd.replace(bregex, '').split('|'),
           variadic: variadic
         })
       } else {
         parsedCommand.demanded.push({
-          cmd: cmd.replace(bregex, ''),
+          cmd: cmd.replace(bregex, '').split('|'),
           variadic: variadic
         })
       }
@@ -163,7 +164,7 @@ module.exports = function (yargs, usage, validation) {
       })
       innerArgv = innerArgv.argv
     }
-    if (!yargs._hasOutput()) populatePositional(commandHandler, innerArgv, currentContext, yargs)
+    if (!yargs._hasOutput()) populatePositionals(commandHandler, innerArgv, currentContext, yargs)
 
     if (commandHandler.handler && !yargs._hasOutput()) {
       commandHandler.handler(innerArgv)
@@ -174,7 +175,9 @@ module.exports = function (yargs, usage, validation) {
     return innerArgv
   }
 
-  function populatePositional (commandHandler, argv, context, yargs) {
+  // transcribe all positional arguments "command <foo> <bar> [apple]"
+  // onto argv.
+  function populatePositionals (commandHandler, argv, context, yargs) {
     argv._ = argv._.slice(context.commands.length) // nuke the current commands
     var demanded = commandHandler.demanded.slice(0)
     var optional = commandHandler.optional.slice(0)
@@ -183,27 +186,39 @@ module.exports = function (yargs, usage, validation) {
 
     while (demanded.length) {
       var demand = demanded.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()
-      postProcessPositional(yargs, argv, demand.cmd)
-      addCamelCaseExpansions(argv, demand.cmd)
+      populatePositional(demand, argv, yargs)
     }
 
     while (optional.length) {
       var maybe = optional.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()
-      postProcessPositional(yargs, argv, maybe.cmd)
-      addCamelCaseExpansions(argv, maybe.cmd)
+      populatePositional(maybe, argv, yargs)
     }
 
     argv._ = context.commands.concat(argv._)
   }
 
+  // populate a single positional argument and its
+  // aliases onto argv.
+  function populatePositional (positional, argv, yargs) {
+    // "positional" consists of the positional.cmd, an array representing
+    // the positional's name and aliases, and positional.variadic
+    // indicating whether or not it is a variadic array.
+    var variadics = null
+    var value = null
+    for (var i = 0, cmd; (cmd = positional.cmd[i]) !== undefined; i++) {
+      if (positional.variadic) {
+        if (variadics) argv[cmd] = variadics.slice(0)
+        else argv[cmd] = variadics = argv._.splice(0)
+      } else {
+        if (!value && !argv._.length) continue
+        if (value) argv[cmd] = value
+        else argv[cmd] = value = argv._.shift()
+      }
+      postProcessPositional(yargs, argv, cmd)
+      addCamelCaseExpansions(argv, cmd)
+    }
+  }
+
   // TODO move positional arg logic to yargs-parser and remove this duplication
   function postProcessPositional (yargs, argv, key) {
     var coerce = yargs.getOptions().coerce[key]


Test patch:
diff --git a/test/command.js b/test/command.js
index fb92002..be4f0dc 100644
--- a/test/command.js
+++ b/test/command.js
@@ -19,11 +19,11 @@ describe('Command', function () {
       var command = y.getCommandInstance()
       var handlers = command.getCommandHandlers()
       handlers.foo.demanded.should.include({
-        cmd: 'bar',
+        cmd: ['bar'],
         variadic: false
       })
       handlers.foo.optional.should.include({
-        cmd: 'awesome',
+        cmd: ['awesome'],
         variadic: false
       })
     })
@@ -96,7 +96,7 @@ describe('Command', function () {
       var command = y.getCommandInstance()
       var handlers = command.getCommandHandlers()
       handlers.foo.optional.should.include({
-        cmd: 'awesome',
+        cmd: ['awesome'],
         variadic: false
       })
       handlers.foo.demanded.should.deep.equal([])
@@ -807,4 +807,36 @@ describe('Command', function () {
     argv.should.have.property('someone').and.equal('world')
     commandCalled.should.be.true
   })
+
+  describe('positional aliases', function () {
+    it('allows an alias to be defined for a required positional argument', function () {
+      var argv = yargs('yo bcoe 113993')
+        .command('yo <user | email> [ssn]', 'Send someone a yo')
+        .argv
+      argv.user.should.equal('bcoe')
+      argv.email.should.equal('bcoe')
+      argv.ssn.should.equal(113993)
+    })
+
+    it('allows an alias to be defined for an optional positional argument', function () {
+      var argv
+      yargs('yo 113993')
+        .command('yo [ssn|sin]', 'Send someone a yo', {}, function (_argv) {
+          argv = _argv
+        })
+        .argv
+      argv.ssn.should.equal(113993)
+      argv.sin.should.equal(113993)
+    })
+
+    it('allows variadic and positional arguments to be combined', function () {
+      var parser = yargs
+        .command('yo <user|email> [ ssns | sins... ]', 'Send someone a yo')
+      var argv = parser.parse('yo bcoe 113993 112888')
+      argv.user.should.equal('bcoe')
+      argv.email.should.equal('bcoe')
+      argv.ssns.should.deep.equal([113993, 112888])
+      argv.sins.should.deep.equal([113993, 112888])
+    })
+  })
 })