diff --git a/README.md b/README.md index 319be657d..660661c56 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 [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 [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 [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 3d6615dff..35b307791 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 f362fb713..6d989eff1 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 86c874c1a..d805710b0 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 59d4eb08d..491766422 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", diff --git a/test/command.js b/test/command.js index 6c405b587..b344e50be 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 '], '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 ' + 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 ' + 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 ', '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 ', + 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 ', '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 ', + 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 ', '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 bc4fa7e8a..e5e83ed36 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 [dest]', 'cp', 'dupe'], 'Copy something') + .help().wrap(null) + .argv + }) + + r.logs[0].split('\n').should.deep.equal([ + 'Commands:', + ' copy [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 [dest]', 'cp', 'dupe'], 'Copy something') + .help().wrap(80) + .argv + }) + + r.logs[0].split('\n').should.deep.equal([ + 'Commands:', + ' copy [dest] Copy something [aliases: cp, dupe]', + '', + 'Options:', + ' --help Show help [boolean]', + '' + ]) + }) }) describe('epilogue', function () {