Skip to content

Commit

Permalink
feat: initial support for command aliases (#647)
Browse files Browse the repository at this point in the history
* feat: initial support for command aliases

* fix standard nits and failing tests

* tests for command aliases

* define the wrap value for consistency wih all terminals

* allow command module to use `aliases` property

* docs: document command aliases and command execution

* add German translation for "aliases:"

thanks to @maxrimue!
  • Loading branch information
nexdrew authored Oct 10, 2016
1 parent 23cf836 commit 127a040
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 24 deletions.
80 changes: 76 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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/'
Expand All @@ -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/'
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 21 additions & 11 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () {
Expand All @@ -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()
Expand Down Expand Up @@ -204,6 +213,7 @@ module.exports = function (yargs, usage, validation) {

self.reset = function () {
handlers = {}
aliasMap = {}
return self
}

Expand Down
11 changes: 8 additions & 3 deletions lib/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 127a040

Please sign in to comment.