From fe7850a591fdfe6f0f2e889b405055502173d81f Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 10 Jul 2019 22:19:51 +1200 Subject: [PATCH] Rework docs to clarify action vs executable commands (#990) * TSDoc: defined .command as an overload, with reworked documentstion * Add TSDoc remarks about command description * description is the key difference between the overloads, so be explicit about how it is suppliied. * Rework .command JSDoc to clarify two styles * Minor changes in comments * Rework (sub)command coverage in README * Update CHANGELOG with #938 * Minor improvements to command and argument coverage * Fix indentation in code blocks in README --- CHANGELOG.md | 1 + Readme.md | 126 +++++++++++++++++++++---------------- index.js | 68 ++++++-------------- typings/commander-tests.ts | 7 ++- typings/index.d.ts | 94 +++++++++++---------------- 5 files changed, 134 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d929c4b1..c3b11a6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * can now define both `--foo` and `--no-foo` * custom event listeners: `--no-foo` on cli now emits `option:no-foo` (previously `option:foo`) * default value: defining `--no-foo` after defining `--foo` leaves the default value unchanged (previously set it to false) + * Change docs for `.command` to contrast action handler vs git-style executable. TypeScript now uses overloaded function. (#938) 2.20.0 / 2019-04-02 ================== diff --git a/Readme.md b/Readme.md index 826a3a6aa..05bee0fed 100644 --- a/Readme.md +++ b/Readme.md @@ -216,12 +216,12 @@ $ custom --list x,y,z The optional `version` method adds handling for displaying the command version. The default option flags are `-V` and `--version`, and when present the command prints the version number and exits. ```js - program.version('0.0.1'); +program.version('0.0.1'); ``` ```bash - $ ./examples/pizza -V - 0.0.1 +$ ./examples/pizza -V +0.0.1 ``` You may change the flags and description by passing additional parameters to the `version` method, using @@ -231,39 +231,59 @@ the same syntax for flags as the `option` method. The version flags can be named program.version('0.0.1', '-v, --vers', 'output the current version'); ``` -## Command-specific options - -You can attach options to a command. +## Commands -```js -#!/usr/bin/env node +You can specify (sub)commands for your top-level command using `.command`. There are two ways these can be implemented: using an action handler attached to the command, or as a separate executable file (described in more detail later). In the first parameter to `.command` you specify the command name and any command arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. -var program = require('commander'); +For example: +```js +// Command implemented using action handler (description is supplied separately to `.command`) +// Returns new command for configuring. program - .command('rm ') - .option('-r, --recursive', 'Remove recursively') - .action(function (dir, cmd) { - console.log('remove ' + dir + (cmd.recursive ? ' recursively' : '')) - }) + .command('clone [destination]') + .description('clone a repository into a newly created directory') + .action((source, destination) => { + console.log('clone command called'); + }); -program.parse(process.argv) +// Command implemented using separate executable file (description is second parameter to `.command`) +// Returns top-level command for adding more commands. +program + .command('start ', 'start named service') + .command('stop [service]', 'stop named service, or all if no name supplied'); ``` -A command's options are validated when the command is used. Any unknown options will be reported as an error. However, if an action-based command does not define an action, then the options are not validated. +### Specify the argument syntax -## Variadic arguments +You use `.arguments` to specify the arguments for the top-level command, and for subcommands they are included in the `.command` call. Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[optional]`) indicate optional input. - The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to - append `...` to the argument name. Here is an example: ```js -#!/usr/bin/env node +var program = require('commander'); -/** - * Module dependencies. - */ +program + .version('0.1.0') + .arguments(' [env]') + .action(function (cmd, env) { + cmdValue = cmd; + envValue = env; + }); + +program.parse(process.argv); + +if (typeof cmdValue === 'undefined') { + console.error('no command given!'); + process.exit(1); +} +console.log('command:', cmdValue); +console.log('environment:', envValue || "no environment given"); +``` + + The last argument of a command can be variadic, and only the last argument. To make an argument variadic you + append `...` to the argument name. For example: +```js var program = require('commander'); program @@ -281,36 +301,37 @@ program program.parse(process.argv); ``` - An `Array` is used for the value of a variadic argument. This applies to `program.args` as well as the argument passed - to your action as demonstrated above. +The variadic argument is passed to the action handler as an array. (And this also applies to `program.args`.) -## Specify the argument syntax +### Action handler (sub)commands -```js -#!/usr/bin/env node +You can add options to a command that uses an action handler. +The action handler gets passed a parameter for each argument you declared, and one additional argument which is the +command object itself. This command argument has the values for the command-specific options added as properties. +```js var program = require('commander'); program - .version('0.1.0') - .arguments(' [env]') - .action(function (cmd, env) { - cmdValue = cmd; - envValue = env; - }); - -program.parse(process.argv); + .command('rm ') + .option('-r, --recursive', 'Remove recursively') + .action(function (dir, cmdObj) { + console.log('remove ' + dir + (cmdObj.recursive ? ' recursively' : '')) + }) -if (typeof cmdValue === 'undefined') { - console.error('no command given!'); - process.exit(1); -} -console.log('command:', cmdValue); -console.log('environment:', envValue || "no environment given"); +program.parse(process.argv) ``` -Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. -## Git-style sub-commands +A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error. However, if an action-based command does not define an action, then the options are not validated. + +Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. + +### Git-style executable (sub)commands + +When `.command()` is invoked with a description argument, this tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools. +Commander will search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-subcommand`, like `pm-install`, `pm-search`. + +You handle the options for an executable (sub)command in the executable, and don't declare them at the top-level. ```js // file: ./examples/pm @@ -324,19 +345,10 @@ program .parse(process.argv); ``` -When `.command()` is invoked with a description argument, no `.action(callback)` should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools. -The commander will try to search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-command`, like `pm-install`, `pm-search`. - -Options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the subcommand from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified. +Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified. If the program is designed to be installed globally, make sure the executables have proper modes, like `755`. -### `--harmony` - -You can enable `--harmony` option in two ways: -* Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. Note some os version don’t support this pattern. -* Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process. - ## Automated --help The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free: @@ -485,6 +497,12 @@ If you use `ts-node` and git-style sub-commands written as `.ts` files, you nee node -r ts-node/register pm.ts ``` +### Node options such as `--harmony` + +You can enable `--harmony` option in two ways: +* Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. (Note Windows does not support this pattern.) +* Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process. + ## Examples ```js diff --git a/index.js b/index.js index 73570914c..486178ec9 100644 --- a/index.js +++ b/index.js @@ -109,73 +109,41 @@ function Command(name) { } /** - * Add command `name`. + * Define a command. * - * The `.action()` callback is invoked when the - * command `name` is specified via __ARGV__, - * and the remaining arguments are applied to the - * function for access. - * - * When the `name` is "*" an un-matched command - * will be passed as the first arg, followed by - * the rest of __ARGV__ remaining. + * There are two styles of command: pay attention to where to put the description. * * Examples: * + * // Command implemented using action handler (description is supplied separately to `.command`) * program - * .version('0.0.1') - * .option('-C, --chdir ', 'change the working directory') - * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') - * .option('-T, --no-tests', 'ignore test hook') - * - * program - * .command('setup') - * .description('run remote setup commands') - * .action(function() { - * console.log('setup'); - * }); - * - * program - * .command('exec ') - * .description('run the given remote command') - * .action(function(cmd) { - * console.log('exec "%s"', cmd); - * }); - * - * program - * .command('teardown [otherDirs...]') - * .description('run teardown commands') - * .action(function(dir, otherDirs) { - * console.log('dir "%s"', dir); - * if (otherDirs) { - * otherDirs.forEach(function (oDir) { - * console.log('dir "%s"', oDir); - * }); - * } + * .command('clone [destination]') + * .description('clone a repository into a newly created directory') + * .action((source, destination) => { + * console.log('clone command called'); * }); * + * // Command implemented using separate executable file (description is second parameter to `.command`) * program - * .command('*') - * .description('deploy the given env') - * .action(function(env) { - * console.log('deploying "%s"', env); - * }); + * .command('start ', 'start named service') + * .command('stop [service]', 'stop named serice, or all if no name supplied'); * - * program.parse(process.argv); - * - * @param {String} name - * @param {String} [desc] for git-style sub-commands - * @return {Command} the new command + * @param {string} nameAndArgs - command name and arguments, args are `` or `[optional]` and last may also be `variadic...` + * @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable) + * @param {Object} [execOpts] - configuration options (for executable) + * @return {Command} returns new command for action handler, or top-level command for executable command * @api public */ -Command.prototype.command = function(name, desc, opts) { +Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts) { + var desc = actionOptsOrExecDesc; + var opts = execOpts; if (typeof desc === 'object' && desc !== null) { opts = desc; desc = null; } opts = opts || {}; - var args = name.split(/ +/); + var args = nameAndArgs.split(/ +/); var cmd = new Command(args.shift()); if (desc) { diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 474db4e87..94353c40e 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -82,6 +82,7 @@ program.on('--help', () => { program .command('allow-unknown-option') + .description("description") .allowUnknownOption() .action(() => { console.log('unknown option is allowed'); @@ -94,6 +95,10 @@ program console.log(cmd, env); }); +program + .command("name1", "description") + .command("name2", "description", { isDefault:true }) + program.parse(process.argv); -console.log('stuff'); \ No newline at end of file +console.log('stuff'); diff --git a/typings/index.d.ts b/typings/index.d.ts index 4637acdaa..9fafd0101 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -49,65 +49,45 @@ declare namespace local { version(str: string, flags?: string, description?: string): Command; /** - * Add command `name`. - * - * The `.action()` callback is invoked when the - * command `name` is specified via __ARGV__, - * and the remaining arguments are applied to the - * function for access. - * - * When the `name` is "*" an un-matched command - * will be passed as the first arg, followed by - * the rest of __ARGV__ remaining. - * + * Define a command, implemented using an action handler. + * + * @remarks + * The command description is supplied using `.description`, not as a parameter to `.command`. + * * @example - * program - * .version('0.0.1') - * .option('-C, --chdir ', 'change the working directory') - * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') - * .option('-T, --no-tests', 'ignore test hook') - * - * program - * .command('setup') - * .description('run remote setup commands') - * .action(function() { - * console.log('setup'); - * }); - * - * program - * .command('exec ') - * .description('run the given remote command') - * .action(function(cmd) { - * console.log('exec "%s"', cmd); - * }); - * - * program - * .command('teardown [otherDirs...]') - * .description('run teardown commands') - * .action(function(dir, otherDirs) { - * console.log('dir "%s"', dir); - * if (otherDirs) { - * otherDirs.forEach(function (oDir) { - * console.log('dir "%s"', oDir); - * }); - * } - * }); - * - * program - * .command('*') - * .description('deploy the given env') - * .action(function(env) { - * console.log('deploying "%s"', env); - * }); - * - * program.parse(process.argv); - * - * @param {string} name - * @param {string} [desc] for git-style sub-commands - * @param {CommandOptions} [opts] command options - * @returns {Command} the new command + * ```ts + * program + * .command('clone [destination]') + * .description('clone a repository into a newly created directory') + * .action((source, destination) => { + * console.log('clone command called'); + * }); + * ``` + * + * @param nameAndArgs - command name and arguments, args are `` or `[optional]` and last may also be `variadic...` + * @param opts - configuration options + * @returns new command + */ + command(nameAndArgs: string, opts?: commander.CommandOptions): Command; + /** + * Define a command, implemented in a separate executable file. + * + * @remarks + * The command description is supplied as the second parameter to `.command`. + * + * @example + * ```ts + * program + * .command('start ', 'start named service') + * .command('stop [service]', 'stop named serice, or all if no name supplied'); + * ``` + * + * @param nameAndArgs - command name and arguments, args are `` or `[optional]` and last may also be `variadic...` + * @param description - description of executable command + * @param opts - configuration options + * @returns top level command for chaining more command definitions */ - command(name: string, desc?: string, opts?: commander.CommandOptions): Command; + command(nameAndArgs: string, description: string, opts?: commander.CommandOptions): Command; /** * Define argument syntax for the top-level command.