Skip to content

Commit

Permalink
Rework docs to clarify action vs executable commands (#990)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shadowspawn authored Jul 10, 2019
1 parent 270cdf3 commit fe7850a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 162 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
==================
Expand Down
126 changes: 72 additions & 54 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<required>` 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 <dir>')
.option('-r, --recursive', 'Remove recursively')
.action(function (dir, cmd) {
console.log('remove ' + dir + (cmd.recursive ? ' recursively' : ''))
})
.command('clone <source> [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 <service>', '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. `<required>`) 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('<cmd> [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
Expand All @@ -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('<cmd> [env]')
.action(function (cmd, env) {
cmdValue = cmd;
envValue = env;
});
program.parse(process.argv);
.command('rm <dir>')
.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. `<cmd>`) 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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
68 changes: 18 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>', 'change the working directory')
* .option('-c, --config <path>', '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 <cmd>')
* .description('run the given remote command')
* .action(function(cmd) {
* console.log('exec "%s"', cmd);
* });
*
* program
* .command('teardown <dir> [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 <source> [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 <service>', '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 `<required>` 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) {
Expand Down
7 changes: 6 additions & 1 deletion typings/commander-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ program.on('--help', () => {

program
.command('allow-unknown-option')
.description("description")
.allowUnknownOption()
.action(() => {
console.log('unknown option is allowed');
Expand All @@ -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');
console.log('stuff');
Loading

0 comments on commit fe7850a

Please sign in to comment.