From 7899f0887bca0caea8c3aa2c62935e752cdc52c8 Mon Sep 17 00:00:00 2001 From: nexdrew Date: Mon, 3 Oct 2016 22:47:43 -0400 Subject: [PATCH 1/7] feat: initial support for command aliases --- lib/command.js | 30 ++++++++++++++++++++---------- lib/usage.js | 11 ++++++++--- locales/en.json | 1 + 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/command.js b/lib/command.js index 3d6615dff..a96b6de12 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) + var aliases = [] + if (Array.isArray(cmd)) { + aliases = cmd.slice(1) + cmd = cmd[0] + } else if (typeof cmd === 'object') { + const commandString = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd) self.addHandler(commandString, 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) + var alias + 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/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", From ed82c47b3e836316accb2603828924eda2f888f7 Mon Sep 17 00:00:00 2001 From: nexdrew Date: Mon, 3 Oct 2016 23:09:27 -0400 Subject: [PATCH 2/7] fix standard nits and failing tests --- lib/command.js | 1 - test/command.js | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/command.js b/lib/command.js index a96b6de12..1b2b7ec56 100644 --- a/lib/command.js +++ b/lib/command.js @@ -27,7 +27,6 @@ module.exports = function (yargs, usage, validation) { } var parsedCommand = parseCommand(cmd) - var alias aliases = aliases.map(function (alias) { alias = parseCommand(alias).cmd // remove positional args aliasMap[alias] = parsedCommand.cmd diff --git a/test/command.js b/test/command.js index 6c405b587..d36e53184 100644 --- a/test/command.js +++ b/test/command.js @@ -143,10 +143,11 @@ 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 string, boolean as first 2 arguments', function () { @@ -221,6 +222,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 +230,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 +240,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 +248,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 +258,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 +266,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 +313,7 @@ describe('Command', function () { }, handler: function (argv) {} } + var aliases = [] var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -316,7 +321,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 +334,7 @@ describe('Command', function () { } } } + var aliases = [] var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -336,7 +342,7 @@ 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]) }) }) From 99ff561d6a9463660ac95aee6def1d160cef672d Mon Sep 17 00:00:00 2001 From: nexdrew Date: Wed, 5 Oct 2016 19:29:46 -0400 Subject: [PATCH 3/7] tests for command aliases --- test/command.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ test/usage.js | 36 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/test/command.js b/test/command.js index d36e53184..b918641aa 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 () { @@ -150,6 +164,18 @@ describe('Command', function () { 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 () { var cmd = 'foo' var desc = false @@ -159,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' @@ -344,6 +382,25 @@ describe('Command', function () { var commands = y.getUsageInstance().getCommands() 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']) + }) }) describe('commandDir', function () { @@ -642,4 +699,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..1fb1234a2 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 (default wrap)', function () { + var r = checkUsage(function () { + return yargs('help') + .command(['copy [dest]', 'cp', 'dupe'], 'Copy something') + .help() + .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 () { From 3311af031ff05d8ed0288567840c977f7bdae0a9 Mon Sep 17 00:00:00 2001 From: nexdrew Date: Wed, 5 Oct 2016 19:37:36 -0400 Subject: [PATCH 4/7] define the wrap value for consistency wih all terminals --- test/usage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/usage.js b/test/usage.js index 1fb1234a2..e5e83ed36 100644 --- a/test/usage.js +++ b/test/usage.js @@ -1791,11 +1791,11 @@ describe('usage tests', function () { ]) }) - it('displays aliases for commands that have them (default wrap)', function () { + 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() + .help().wrap(80) .argv }) From 56be558da91395051dd4494127b67e63dd7b4acf Mon Sep 17 00:00:00 2001 From: nexdrew Date: Thu, 6 Oct 2016 11:22:05 -0400 Subject: [PATCH 5/7] allow command module to use `aliases` property --- lib/command.js | 5 +++-- test/command.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/lib/command.js b/lib/command.js index 1b2b7ec56..35b307791 100644 --- a/lib/command.js +++ b/lib/command.js @@ -15,8 +15,9 @@ module.exports = function (yargs, usage, validation) { aliases = cmd.slice(1) cmd = cmd[0] } else if (typeof cmd === 'object') { - const commandString = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd) - self.addHandler(commandString, extractDesc(cmd), cmd.builder, cmd.handler) + 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 } diff --git a/test/command.js b/test/command.js index b918641aa..b344e50be 100644 --- a/test/command.js +++ b/test/command.js @@ -401,6 +401,66 @@ describe('Command', function () { 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']) + }) }) describe('commandDir', function () { From 9f4286b84e9f7df98a16c8834a07feb7dfa447cd Mon Sep 17 00:00:00 2001 From: nexdrew Date: Thu, 6 Oct 2016 11:28:23 -0400 Subject: [PATCH 6/7] docs: document command aliases and command execution --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 4 deletions(-) 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. From 20d4b43062d3d24b8ae9f0d4c80392dbe962f0dd Mon Sep 17 00:00:00 2001 From: nexdrew Date: Sun, 9 Oct 2016 22:56:53 -0400 Subject: [PATCH 7/7] add German translation for "aliases:" thanks to @maxrimue! --- locales/de.json | 1 + 1 file changed, 1 insertion(+) 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",