Skip to content

Commit 1345f98

Browse files
authored
Parse rework for nested commands (#1149)
* First cut at parse rework - skip asterisk tests - other tests runnning - nested commands untested - lots of details to check * Add check for requiredOption when calling executable subcommand * Set program name using supported approach * Add .addCommand, easy after previous work * Add support for default command using action handler - and remove stale _execs * Add implicitHelpCommand and change help flags description * Add implicit help command to help * Turn off implicit help command for most help tests * .addHelpCommand * Remove addHelpCommand from tests and make match more narrow * Use test of complete default help output * Add tests for whether implicit help appears in help * Add tests that help command dispatched to correct command * Add simple nested subcommand test * Add default command tests for action based subcommand * Remove mainModule, out of scope for current PR * Add legacy asterisk handling and tests * Add more initialisation so object in known state * Tests for addCommand * Add first cut at enhanced default error detection * Add test that addCommand requires name * Add block on automatic name generation for deeply nested executables * Add block on automatic name generation for deeply nested executables * Fix describe name for tests * Refine unknownCommand handling and add tests * Add suggestion to try help, when appropriate * Fix typo * Move common command configuration options in README, and add isDefault example program * Add isDefault and example to README * Add nested commands * Document .addHelpCommand, and tweaks * Remove old default command, and rework command:* example * Document .addCommand * Remove comment referring to removed code. * Revert the error tip "try --help", not happy with the wording * Say "unknown command", like "unknown option" * Set properties to null rather than undefined in constructor
1 parent 1691344 commit 1345f98

27 files changed

+763
-338
lines changed

Readme.md

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
1111

1212
- [Commander.js](#commanderjs)
1313
- [Installation](#installation)
14-
- [Declaring program variable](#declaring-program-variable)
14+
- [Declaring _program_ variable](#declaring-program-variable)
1515
- [Options](#options)
1616
- [Common option types, boolean and value](#common-option-types-boolean-and-value)
1717
- [Default option value](#default-option-value)
@@ -23,17 +23,18 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
2323
- [Specify the argument syntax](#specify-the-argument-syntax)
2424
- [Action handler (sub)commands](#action-handler-subcommands)
2525
- [Git-style executable (sub)commands](#git-style-executable-subcommands)
26-
- [Automated --help](#automated---help)
26+
- [Automated help](#automated-help)
2727
- [Custom help](#custom-help)
2828
- [.usage and .name](#usage-and-name)
2929
- [.outputHelp(cb)](#outputhelpcb)
3030
- [.helpOption(flags, description)](#helpoptionflags-description)
31+
- [.addHelpCommand()](#addhelpcommand)
3132
- [.help(cb)](#helpcb)
3233
- [Custom event listeners](#custom-event-listeners)
3334
- [Bits and pieces](#bits-and-pieces)
3435
- [Avoiding option name clashes](#avoiding-option-name-clashes)
3536
- [TypeScript](#typescript)
36-
- [Node options such as --harmony](#node-options-such-as---harmony)
37+
- [Node options such as `--harmony`](#node-options-such-as---harmony)
3738
- [Node debugging](#node-debugging)
3839
- [Override exit handling](#override-exit-handling)
3940
- [Examples](#examples)
@@ -269,7 +270,7 @@ program
269270
program.parse(process.argv);
270271
```
271272
272-
```
273+
```bash
273274
$ pizza
274275
error: required option '-c, --cheese <type>' not specified
275276
```
@@ -296,7 +297,11 @@ program.version('0.0.1', '-v, --vers', 'output the current version');
296297
297298
## Commands
298299
299-
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...`.
300+
You can specify (sub)commands for your top-level command using `.command()` or `.addCommand()`. 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). The subcommands may be nested ([example](./examples/nestedCommands.js)).
301+
302+
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...`.
303+
304+
You can use `.addCommand()` to add an already configured subcommand to the program.
300305
301306
For example:
302307
@@ -315,8 +320,15 @@ program
315320
program
316321
.command('start <service>', 'start named service')
317322
.command('stop [service]', 'stop named service, or all if no name supplied');
323+
324+
// Command prepared separately.
325+
// Returns top-level command for adding more commands.
326+
program
327+
.addCommand(build.makeBuildCommand());
318328
```
319329
330+
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 ([example](./examples/defaultCommand.js)).
331+
320332
### Specify the argument syntax
321333
322334
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.
@@ -397,13 +409,11 @@ async function main() {
397409
}
398410
```
399411
400-
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.
401-
402-
Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output.
412+
A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error.
403413
404414
### Git-style executable (sub)commands
405415
406-
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.
416+
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` and other popular tools.
407417
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`.
408418
You can specify a custom name with the `executableFile` configuration option.
409419
@@ -422,14 +432,12 @@ program
422432
.parse(process.argv);
423433
```
424434
425-
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.
426-
Specifying a name with `executableFile` will override the default constructed name.
427-
428435
If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.
429436
430-
## Automated --help
437+
## Automated help
431438
432-
The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free:
439+
The help information is auto-generated based on the information commander already knows about your program. The default
440+
help option is `-h,--help`.
433441
434442
```bash
435443
$ ./examples/pizza --help
@@ -444,17 +452,25 @@ Options:
444452
-b, --bbq Add bbq sauce
445453
-c, --cheese <type> Add the specified type of cheese (default: "marble")
446454
-C, --no-cheese You do not want any cheese
447-
-h, --help output usage information
455+
-h, --help display help for command
456+
```
457+
458+
A `help` command is added by default if your command has subcommands. It can be used alone, or with a subcommand name to show
459+
further help for the subcommand. These are effectively the same if the `shell` program has implicit help:
460+
461+
```bash
462+
shell help
463+
shell --help
464+
465+
shell help spawn
466+
shell spawn --help
448467
```
449468
450469
### Custom help
451470
452-
You can display arbitrary `-h, --help` information
471+
You can display extra `-h, --help` information
453472
by listening for "--help". Commander will automatically
454-
exit once you are done so that the remainder of your program
455-
does not execute causing undesired behaviors, for example
456-
in the following executable "stuff" will not output when
457-
`--help` is used.
473+
exit after displaying the help.
458474
459475
```js
460476
#!/usr/bin/env node
@@ -467,9 +483,7 @@ program
467483
.option('-b, --bar', 'enable some bar')
468484
.option('-B, --baz', 'enable some baz');
469485
470-
// must be before .parse() since
471-
// node's emit() is immediate
472-
486+
// must be before .parse()
473487
program.on('--help', function(){
474488
console.log('')
475489
console.log('Examples:');
@@ -488,11 +502,11 @@ Yields the following help output when `node script-name.js -h` or `node script-n
488502
Usage: custom-help [options]
489503
490504
Options:
491-
-h, --help output usage information
492505
-V, --version output the version number
493506
-f, --foo enable some foo
494507
-b, --bar enable some bar
495508
-B, --baz enable some baz
509+
-h, --help display help for command
496510
497511
Examples:
498512
$ custom-help --help
@@ -550,6 +564,16 @@ program
550564
.helpOption('-e, --HELP', 'read more information');
551565
```
552566
567+
### .addHelpCommand()
568+
569+
You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`.
570+
571+
You can both turn on and customise the help command by supplying the name and description:
572+
573+
```js
574+
program.addHelpCommand('assist [command]', 'show assistance');
575+
```
576+
553577
### .help(cb)
554578
555579
Output help information and exit immediately.
@@ -564,9 +588,10 @@ program.on('option:verbose', function () {
564588
process.env.VERBOSE = this.verbose;
565589
});
566590
567-
// error on unknown commands
568-
program.on('command:*', function () {
569-
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]]);
591+
// custom error on unknown command
592+
program.on('command:*', function (operands) {
593+
console.error(`Invalid command '${operands[0]}'. Did you mean:`);
594+
console.error(mySuggestions(operands[0]));
570595
process.exit(1);
571596
});
572597
```
@@ -686,12 +711,6 @@ program
686711
console.log(' $ deploy exec async');
687712
});
688713
689-
program
690-
.command('*')
691-
.action(function(env){
692-
console.log('deploying "%s"', env);
693-
});
694-
695714
program.parse(process.argv);
696715
```
697716

Readme_zh-CN.md

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
- [.help(cb)](#helpcb)
3232
- [自定义事件监听](#%e8%87%aa%e5%ae%9a%e4%b9%89%e4%ba%8b%e4%bb%b6%e7%9b%91%e5%90%ac)
3333
- [零碎知识](#%e9%9b%b6%e7%a2%8e%e7%9f%a5%e8%af%86)
34-
- [避免选项命名冲突](#避免选项命名冲突)
34+
- [避免选项命名冲突](#%e9%81%bf%e5%85%8d%e9%80%89%e9%a1%b9%e5%91%bd%e5%90%8d%e5%86%b2%e7%aa%81)
3535
- [TypeScript](#typescript)
36-
- [Node 选项例如 --harmony](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony)
36+
- [Node 选项例如 `--harmony`](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony)
3737
- [Node 调试](#node-%e8%b0%83%e8%af%95)
3838
- [重载退出(exit)处理](#%e9%87%8d%e8%bd%bd%e9%80%80%e5%87%baexit%e5%a4%84%e7%90%86)
3939
- [例子](#%e4%be%8b%e5%ad%90)
4040
- [许可证](#%e8%ae%b8%e5%8f%af%e8%af%81)
4141
- [支持](#%e6%94%af%e6%8c%81)
42-
- [企业使用Commander](#企业使用Commander)
42+
- [企业使用Commander](#%e4%bc%81%e4%b8%9a%e4%bd%bf%e7%94%a8commander)
4343

4444
## 安装
4545

@@ -435,7 +435,7 @@ Options:
435435
-b, --bbq Add bbq sauce
436436
-c, --cheese <type> Add the specified type of cheese (default: "marble")
437437
-C, --no-cheese You do not want any cheese
438-
-h, --help output usage information
438+
-h, --help display help for command
439439
```
440440
441441
### 自定义帮助
@@ -453,9 +453,7 @@ program
453453
.option('-b, --bar', 'enable some bar')
454454
.option('-B, --baz', 'enable some baz');
455455

456-
// must be before .parse() since
457-
// node's emit() is immediate
458-
456+
// must be before .parse()
459457
program.on('--help', function(){
460458
console.log('');
461459
console.log('Examples:');
@@ -474,7 +472,7 @@ console.log('stuff');
474472
Usage: custom-help [options]
475473

476474
Options:
477-
-h, --help output usage information
475+
-h, --help display help for command
478476
-V, --version output the version number
479477
-f, --foo enable some foo
480478
-b, --bar enable some bar
@@ -670,12 +668,6 @@ program
670668
console.log(' $ deploy exec async');
671669
});
672670
673-
program
674-
.command('*')
675-
.action(function(env){
676-
console.log('deploying "%s"', env);
677-
});
678-
679671
program.parse(process.argv);
680672
```
681673

examples/custom-help

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ program
99
.option('-b, --bar', 'enable some bar')
1010
.option('-B, --baz', 'enable some baz');
1111

12-
// must be before .parse() since
13-
// node's emit() is immediate
14-
12+
// must be before .parse()
1513
program.on('--help', function() {
1614
console.log('');
1715
console.log('Examples:');

examples/defaultCommand.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// const commander = require('commander'); // (normal include)
2+
const commander = require('../'); // include commander in git clone of commander repo
3+
const program = new commander.Command();
4+
5+
// Example program using the command configuration option isDefault to specify the default command.
6+
//
7+
// $ node defaultCommand.js build
8+
// build
9+
// $ node defaultCommand.js serve -p 8080
10+
// server on port 8080
11+
// $ node defaultCommand.js -p 443
12+
// server on port 443
13+
14+
program
15+
.command('build')
16+
.description('build web site for deployment')
17+
.action(() => {
18+
console.log('build');
19+
});
20+
21+
program
22+
.command('deploy')
23+
.description('deploy web site to production')
24+
.action(() => {
25+
console.log('deploy');
26+
});
27+
28+
program
29+
.command('serve', { isDefault: true })
30+
.description('launch web server')
31+
.option('-p,--port <port_number>', 'web port')
32+
.action((opts) => {
33+
console.log(`server on port ${opts.port}`);
34+
});
35+
36+
program.parse(process.argv);

examples/deploy

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,4 @@ program
3434
console.log();
3535
});
3636

37-
program
38-
.command('*')
39-
.action(function(env) {
40-
console.log('deploying "%s"', env);
41-
});
42-
4337
program.parse(process.argv);

examples/nestedCommands.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// const commander = require('commander'); // (normal include)
2+
const commander = require('../'); // include commander in git clone of commander repo
3+
const program = new commander.Command();
4+
5+
// Commander supports nested subcommands.
6+
// .command() can add a subcommand with an action handler or an executable.
7+
// .addCommand() adds a prepared command with an actiomn handler.
8+
9+
// Example output:
10+
//
11+
// $ node nestedCommands.js brew tea
12+
// brew tea
13+
// $ node nestedCommands.js heat jug
14+
// heat jug
15+
16+
// Add nested commands using `.command()`.
17+
const brew = program.command('brew');
18+
brew
19+
.command('tea')
20+
.action(() => {
21+
console.log('brew tea');
22+
});
23+
brew
24+
.command('tea')
25+
.action(() => {
26+
console.log('brew tea');
27+
});
28+
29+
// Add nested commands using `.addCommand().
30+
// The command could be created separately in another module.
31+
function makeHeatCommand() {
32+
const heat = new commander.Command('heat');
33+
heat
34+
.command('jug')
35+
.action(() => {
36+
console.log('heat jug');
37+
});
38+
heat
39+
.command('pot')
40+
.action(() => {
41+
console.log('heat pot');
42+
});
43+
return heat;
44+
}
45+
program.addCommand(makeHeatCommand());
46+
47+
program.parse(process.argv);

0 commit comments

Comments
 (0)