Repository: yargs/yargs
Pull request #647 (2016-10-04 03:02:14)

feat: initial support for command aliases
Allows a command to have aliases if given as an array, e.g.

```js
.command(['start', 'run', 'go'], 'Start the app', builder, handler)
```

The first element in the array is treated as the canonical command, which supports positional args; the remaining elements are considered aliases. Aliases inherit their positional args from the canonical command and, thus, any positional args defined for an alias are ignored.

For example, all of the following are equivalent and only the `<name>` required positional arg will be parsed/validated:

```js
.command(['hi <name>', 'hello'], 'Say hi', builder, handler)
.command(['hi <name>', 'hello <name>'], 'Say hi', builder, handler)
.command(['hi <name>', 'hello <someone> [others..]'], 'Say hi', builder, handler)
```

For help text, command aliases are displayed in a new right-aligned column after the canonical command and description, e.g.

```console
$ node test.js help
Commands:
  start      Start the app                                    [aliases: go, run]
  hi <name>  Say hi                                             [aliases: hello]

Options:
  --help  Show help                                                    [boolean]
```

TODO:

- [x] Tests
- [x] ~~Add `"aliases:"` to all locales~~ Will create a new issue for this
- [x] Docs

bcoe: @nexdrew for the locale files; feel free to open an issue and not let this block landing this feature.
nexdrew: This should be ready for final review. Added the ability for command modules to export an `aliases` property so they don't _have_ to use an array for the `command` property, because why not.

@bcoe @maxrimue Let me know what you think.
maxrimue: LGTM, nice work 👍 

We should probably open a new issue to call for translations of `aliases` once this PR is merged. Btw, the German translation would be **Aliase**, maybe you can implement this right into this PR @nexdrew?
I think we also still need to call for translations for `Did you mean?`, could do this in the same issue.

nexdrew: Oh good point! Thanks @maxrimue, I'll do that.

Patch:
diff --git a/README.md b/README.md
index 319be65..660661c 100644
--- a/README.md
+++ b/README.md
@@ -525,7 +525,11 @@ var argv = require('yargs')
 .command(module)
 ----------------
 
-Document the commands exposed by your application.
+Define the commands exposed by your application.
+
+`cmd` should be a string representing the command or an array of strings
+representing the command and its aliases. Read more about command aliases in the
+subsection below.
 
 Use `desc` to provide a description for each command your application accepts (the
 values stored in `argv._`).  Set `desc` to `false` to create a hidden command.
@@ -536,7 +540,8 @@ Optionally, you can provide a `builder` object to give hints about the
 options that your command accepts:
 
 ```js
-yargs.command('get', 'make a get HTTP request', {
+yargs
+  .command('get', 'make a get HTTP request', {
     url: {
       alias: 'u',
       default: 'http://yargs.js.org/'
@@ -558,7 +563,8 @@ options (if used) **always** apply globally, just like the
 with a `yargs` instance, and can be used to provide _advanced_ command specific help:
 
 ```js
-yargs.command('get', 'make a get HTTP request', function (yargs) {
+yargs
+  .command('get', 'make a get HTTP request', function (yargs) {
     return yargs.option('url', {
       alias: 'u',
       default: 'http://yargs.js.org/'
@@ -614,12 +620,78 @@ yargs.command('download <url> [files..]', 'download several files')
   .argv
 ```
 
+### Command Execution
+
+When a command is given on the command line, yargs will execute the following:
+
+1. push the command into the current context
+2. reset non-global configuration
+3. apply command configuration via the `builder`, if given
+4. parse and validate args from the command line, including positional args
+5. if validation succeeds, run the `handler` function, if given
+6. pop the command from the current context
+
+### Command Aliases
+
+You can define aliases for a command by putting the command and all of its
+aliases into an array.
+
+Alternatively, a command module may specify an `aliases` property, which may be
+a string or an array of strings. All aliases defined via the `command` property
+and the `aliases` property will be concatenated together.
+
+The first element in the array is considered the canonical command, which may
+define positional arguments, and the remaining elements in the array are
+considered aliases. Aliases inherit positional args from the canonical command,
+and thus any positional args defined in the aliases themselves are ignored.
+
+If either the canonical command or any of its aliases are given on the command
+line, the command will be executed.
+
+```js
+#!/usr/bin/env node
+require('yargs')
+  .command(['start [app]', 'run', 'up'], 'Start up an app', {}, (argv) => {
+    console.log('starting up the', argv.app || 'default', 'app')
+  })
+  .command({
+    command: 'configure <key> [value]',
+    aliases: ['config', 'cfg'],
+    desc: 'Set a config variable',
+    builder: (yargs) => yargs.default('value', 'true'),
+    handler: (argv) => {
+      console.log(`setting ${argv.key} to ${argv.value}`)
+    }
+  })
+  .demand(1)
+  .help()
+  .wrap(72)
+  .argv
+```
+
+```
+$ ./svc.js help
+Commands:
+  start [app]              Start up an app            [aliases: run, up]
+  configure <key> [value]  Set a config variable  [aliases: config, cfg]
+
+Options:
+  --help  Show help                                            [boolean]
+
+$ ./svc.js cfg concurrency 4
+setting concurrency to 4
+
+$ ./svc.js run web
+starting up the web app
+```
+
 ### Providing a Command Module
 
 For complicated commands you can pull the logic into a module. A module
 simply needs to export:
 
-* `exports.command`: string that executes this command when given on the command line, may contain positional args
+* `exports.command`: string (or array of strings) that executes this command when given on the command line, first string may contain positional args
+* `exports.aliases`: array of strings (or a single string) representing aliases of `exports.command`, positional args defined in an alias are ignored
 * `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.
diff --git a/lib/command.js b/lib/command.js
index 3d6615d..35b3077 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -8,27 +8,36 @@ module.exports = function (yargs, usage, validation) {
   const self = {}
 
   var handlers = {}
+  var aliasMap = {}
   self.addHandler = function (cmd, description, builder, handler) {
-    if (typeof cmd === 'object') {
-      const commandString = typeof cmd.command === 'string' ? cmd.command : moduleName(cmd)
-      self.addHandler(commandString, extractDesc(cmd), cmd.builder, cmd.handler)
+    var aliases = []
+    if (Array.isArray(cmd)) {
+      aliases = cmd.slice(1)
+      cmd = cmd[0]
+    } else if (typeof cmd === 'object') {
+      var command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd)
+      if (cmd.aliases) command = [].concat(command).concat(cmd.aliases)
+      self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler)
       return
     }
 
     // allow a module to be provided instead of separate builder and handler
     if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
-      self.addHandler(cmd, description, builder.builder, builder.handler)
+      self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler)
       return
     }
 
+    var parsedCommand = parseCommand(cmd)
+    aliases = aliases.map(function (alias) {
+      alias = parseCommand(alias).cmd // remove positional args
+      aliasMap[alias] = parsedCommand.cmd
+      return alias
+    })
+
     if (description !== false) {
-      usage.command(cmd, description)
+      usage.command(cmd, description, aliases)
     }
 
-    // we should not register a handler if no
-    // builder is provided, e.g., user will
-    // handle command themselves with '_'.
-    var parsedCommand = parseCommand(cmd)
     handlers[parsedCommand.cmd] = {
       original: cmd,
       handler: handler,
@@ -112,7 +121,7 @@ module.exports = function (yargs, usage, validation) {
   }
 
   self.getCommands = function () {
-    return Object.keys(handlers)
+    return Object.keys(handlers).concat(Object.keys(aliasMap))
   }
 
   self.getCommandHandlers = function () {
@@ -121,7 +130,7 @@ module.exports = function (yargs, usage, validation) {
 
   self.runCommand = function (command, yargs, parsed) {
     var argv = parsed.argv
-    var commandHandler = handlers[command]
+    var commandHandler = handlers[command] || handlers[aliasMap[command]]
     var innerArgv = argv
     var currentContext = yargs.getContext()
     var parentCommands = currentContext.commands.slice()
@@ -204,6 +213,7 @@ module.exports = function (yargs, usage, validation) {
 
   self.reset = function () {
     handlers = {}
+    aliasMap = {}
     return self
   }
 
diff --git a/lib/usage.js b/lib/usage.js
index f362fb7..6d989ef 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -70,8 +70,8 @@ module.exports = function (yargs, y18n) {
   }
 
   var commands = []
-  self.command = function (cmd, description) {
-    commands.push([cmd, description || ''])
+  self.command = function (cmd, description, aliases) {
+    commands.push([cmd, description || '', aliases])
   }
   self.getCommands = function () {
     return commands
@@ -152,10 +152,15 @@ module.exports = function (yargs, y18n) {
       ui.div(__('Commands:'))
 
       commands.forEach(function (command) {
-        ui.div(
+        ui.span(
           {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands, theWrap) + 4},
           {text: command[1]}
         )
+        if (command[2] && command[2].length) {
+          ui.div({text: '[' + __('aliases:') + ' ' + command[2].join(', ') + ']', padding: [0, 0, 0, 2], align: 'right'})
+        } else {
+          ui.div()
+        }
       })
 
       ui.div()
diff --git a/locales/de.json b/locales/de.json
index 86c874c..d805710 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -10,6 +10,7 @@
   "required": "erforderlich",
   "default:": "Standard:",
   "choices:": "Möglichkeiten:",
+  "aliases:": "Aliase:",
   "generated-value": "Generierter-Wert",
   "Not enough non-option arguments: got %s, need at least %s": "Nicht genügend Argumente ohne Optionen: %s vorhanden, mindestens %s benötigt",
   "Too many non-option arguments: got %s, maximum of %s": "Zu viele Argumente ohne Optionen: %s vorhanden, maximal %s erlaubt",
diff --git a/locales/en.json b/locales/en.json
index 59d4eb0..4917664 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -10,6 +10,7 @@
   "required": "required",
   "default:": "default:",
   "choices:": "choices:",
+  "aliases:": "aliases:",
   "generated-value": "generated-value",
   "Not enough non-option arguments: got %s, need at least %s": "Not enough non-option arguments: got %s, need at least %s",
   "Too many non-option arguments: got %s, maximum of %s": "Too many non-option arguments: got %s, maximum of %s",


Test patch:
diff --git a/test/command.js b/test/command.js
index 6c405b5..b344e50 100644
--- a/test/command.js
+++ b/test/command.js
@@ -65,6 +65,20 @@ describe('Command', function () {
         })
         .argv
     })
+
+    it('ignores positional args for aliases', function () {
+      var y = yargs([])
+        .command(['foo [awesome]', 'wat <yo>'], 'my awesome command')
+      var command = y.getCommandInstance()
+      var handlers = command.getCommandHandlers()
+      handlers.foo.optional.should.include({
+        cmd: 'awesome',
+        variadic: false
+      })
+      handlers.foo.demanded.should.deep.equal([])
+      expect(handlers.wat).to.not.exist
+      command.getCommands().should.deep.equal(['foo', 'wat'])
+    })
   })
 
   describe('variadic', function () {
@@ -143,10 +157,23 @@ describe('Command', function () {
     it('accepts string, string as first 2 arguments', function () {
       var cmd = 'foo'
       var desc = 'i\'m not feeling very creative at the moment'
+      var aliases = []
 
       var y = yargs([]).command(cmd, desc)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([cmd, desc])
+      commands[0].should.deep.equal([cmd, desc, aliases])
+    })
+
+    it('accepts array, string as first 2 arguments', function () {
+      var aliases = ['bar', 'baz']
+      var cmd = 'foo <qux>'
+      var desc = 'i\'m not feeling very creative at the moment'
+
+      var y = yargs([]).command([cmd].concat(aliases), desc)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands[0].should.deep.equal([cmd, desc, aliases])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar', 'baz'])
     })
 
     it('accepts string, boolean as first 2 arguments', function () {
@@ -158,6 +185,18 @@ describe('Command', function () {
       commands.should.deep.equal([])
     })
 
+    it('accepts array, boolean as first 2 arguments', function () {
+      var aliases = ['bar', 'baz']
+      var cmd = 'foo <qux>'
+      var desc = false
+
+      var y = yargs([]).command([cmd].concat(aliases), desc)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands.should.deep.equal([])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar', 'baz'])
+    })
+
     it('accepts function as 3rd argument', function () {
       var cmd = 'foo'
       var desc = 'i\'m not feeling very creative at the moment'
@@ -221,6 +260,7 @@ describe('Command', function () {
         builder: function (yargs) { return yargs },
         handler: function (argv) {}
       }
+      var aliases = []
 
       var y = yargs([]).command(module)
       var handlers = y.getCommandInstance().getCommandHandlers()
@@ -228,7 +268,7 @@ describe('Command', function () {
       handlers.foo.builder.should.equal(module.builder)
       handlers.foo.handler.should.equal(module.handler)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([module.command, module.describe])
+      commands[0].should.deep.equal([module.command, module.describe, aliases])
     })
 
     it('accepts module (description key, builder function) as 1st argument', function () {
@@ -238,6 +278,7 @@ describe('Command', function () {
         builder: function (yargs) { return yargs },
         handler: function (argv) {}
       }
+      var aliases = []
 
       var y = yargs([]).command(module)
       var handlers = y.getCommandInstance().getCommandHandlers()
@@ -245,7 +286,7 @@ describe('Command', function () {
       handlers.foo.builder.should.equal(module.builder)
       handlers.foo.handler.should.equal(module.handler)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([module.command, module.description])
+      commands[0].should.deep.equal([module.command, module.description, aliases])
     })
 
     it('accepts module (desc key, builder function) as 1st argument', function () {
@@ -255,6 +296,7 @@ describe('Command', function () {
         builder: function (yargs) { return yargs },
         handler: function (argv) {}
       }
+      var aliases = []
 
       var y = yargs([]).command(module)
       var handlers = y.getCommandInstance().getCommandHandlers()
@@ -262,7 +304,7 @@ describe('Command', function () {
       handlers.foo.builder.should.equal(module.builder)
       handlers.foo.handler.should.equal(module.handler)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([module.command, module.desc])
+      commands[0].should.deep.equal([module.command, module.desc, aliases])
     })
 
     it('accepts module (false describe, builder function) as 1st argument', function () {
@@ -309,6 +351,7 @@ describe('Command', function () {
         },
         handler: function (argv) {}
       }
+      var aliases = []
 
       var y = yargs([]).command(module)
       var handlers = y.getCommandInstance().getCommandHandlers()
@@ -316,7 +359,7 @@ describe('Command', function () {
       handlers.foo.builder.should.equal(module.builder)
       handlers.foo.handler.should.equal(module.handler)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([module.command, module.describe])
+      commands[0].should.deep.equal([module.command, module.describe, aliases])
     })
 
     it('accepts module (missing handler function) as 1st argument', function () {
@@ -329,6 +372,7 @@ describe('Command', function () {
           }
         }
       }
+      var aliases = []
 
       var y = yargs([]).command(module)
       var handlers = y.getCommandInstance().getCommandHandlers()
@@ -336,7 +380,86 @@ describe('Command', function () {
       handlers.foo.builder.should.equal(module.builder)
       expect(handlers.foo.handler).to.equal(undefined)
       var commands = y.getUsageInstance().getCommands()
-      commands[0].should.deep.equal([module.command, module.describe])
+      commands[0].should.deep.equal([module.command, module.describe, aliases])
+    })
+
+    it('accepts module (with command array) as 1st argument', function () {
+      var module = {
+        command: ['foo <qux>', 'bar', 'baz'],
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command[0])
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands[0].should.deep.equal([module.command[0], module.describe, ['bar', 'baz']])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar', 'baz'])
+    })
+
+    it('accepts module (with command string and aliases array) as 1st argument', function () {
+      var module = {
+        command: 'foo <qux>',
+        aliases: ['bar', 'baz'],
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands[0].should.deep.equal([module.command, module.describe, module.aliases])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar', 'baz'])
+    })
+
+    it('accepts module (with command array and aliases array) as 1st argument', function () {
+      var module = {
+        command: ['foo <qux>', 'bar'],
+        aliases: ['baz', 'nat'],
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command[0])
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands[0].should.deep.equal([module.command[0], module.describe, ['bar', 'baz', 'nat']])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar', 'baz', 'nat'])
+    })
+
+    it('accepts module (with command string and aliases string) as 1st argument', function () {
+      var module = {
+        command: 'foo <qux>',
+        aliases: 'bar',
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var usageCommands = y.getUsageInstance().getCommands()
+      usageCommands[0].should.deep.equal([module.command, module.describe, ['bar']])
+      var cmdCommands = y.getCommandInstance().getCommands()
+      cmdCommands.should.deep.equal(['foo', 'bar'])
     })
   })
 
@@ -636,4 +759,16 @@ describe('Command', function () {
     })
     handlers.foo.demanded.should.have.lengthOf(0)
   })
+
+  it('executes a command via alias', function () {
+    var commandCalled = false
+    var argv = yargs('hi world')
+      .command(['hello <someone>', 'hi'], 'Say hello', {}, function (argv) {
+        commandCalled = true
+        argv.should.have.property('someone').and.equal('world')
+      })
+      .argv
+    argv.should.have.property('someone').and.equal('world')
+    commandCalled.should.be.true
+  })
 })
diff --git a/test/usage.js b/test/usage.js
index bc4fa7e..e5e83ed 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -1772,6 +1772,42 @@ describe('usage tests', function () {
         ''
       ])
     })
+
+    it('displays aliases for commands that have them (no wrap)', function () {
+      var r = checkUsage(function () {
+        return yargs('help')
+          .command(['copy <src> [dest]', 'cp', 'dupe'], 'Copy something')
+          .help().wrap(null)
+          .argv
+      })
+
+      r.logs[0].split('\n').should.deep.equal([
+        'Commands:',
+        '  copy <src> [dest]  Copy something  [aliases: cp, dupe]',
+        '',
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ])
+    })
+
+    it('displays aliases for commands that have them (with wrap)', function () {
+      var r = checkUsage(function () {
+        return yargs('help')
+          .command(['copy <src> [dest]', 'cp', 'dupe'], 'Copy something')
+          .help().wrap(80)
+          .argv
+      })
+
+      r.logs[0].split('\n').should.deep.equal([
+        'Commands:',
+        '  copy <src> [dest]  Copy something                          [aliases: cp, dupe]',
+        '',
+        'Options:',
+        '  --help  Show help                                                    [boolean]',
+        ''
+      ])
+    })
   })
 
   describe('epilogue', function () {