Skip to content

Commit

Permalink
Parse rework for nested commands (#1149)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shadowspawn committed Jan 31, 2020
1 parent 1691344 commit 1345f98
Show file tree
Hide file tree
Showing 27 changed files with 763 additions and 338 deletions.
85 changes: 52 additions & 33 deletions Readme.md
Expand Up @@ -11,7 +11,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)

- [Commander.js](#commanderjs)
- [Installation](#installation)
- [Declaring program variable](#declaring-program-variable)
- [Declaring _program_ variable](#declaring-program-variable)
- [Options](#options)
- [Common option types, boolean and value](#common-option-types-boolean-and-value)
- [Default option value](#default-option-value)
Expand All @@ -23,17 +23,18 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [Specify the argument syntax](#specify-the-argument-syntax)
- [Action handler (sub)commands](#action-handler-subcommands)
- [Git-style executable (sub)commands](#git-style-executable-subcommands)
- [Automated --help](#automated---help)
- [Automated help](#automated-help)
- [Custom help](#custom-help)
- [.usage and .name](#usage-and-name)
- [.outputHelp(cb)](#outputhelpcb)
- [.helpOption(flags, description)](#helpoptionflags-description)
- [.addHelpCommand()](#addhelpcommand)
- [.help(cb)](#helpcb)
- [Custom event listeners](#custom-event-listeners)
- [Bits and pieces](#bits-and-pieces)
- [Avoiding option name clashes](#avoiding-option-name-clashes)
- [TypeScript](#typescript)
- [Node options such as --harmony](#node-options-such-as---harmony)
- [Node options such as `--harmony`](#node-options-such-as---harmony)
- [Node debugging](#node-debugging)
- [Override exit handling](#override-exit-handling)
- [Examples](#examples)
Expand Down Expand Up @@ -269,7 +270,7 @@ program
program.parse(process.argv);
```
```
```bash
$ pizza
error: required option '-c, --cheese <type>' not specified
```
Expand All @@ -296,7 +297,11 @@ program.version('0.0.1', '-v, --vers', 'output the current version');
## Commands
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...`.
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)).
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...`.
You can use `.addCommand()` to add an already configured subcommand to the program.
For example:
Expand All @@ -315,8 +320,15 @@ program
program
.command('start <service>', 'start named service')
.command('stop [service]', 'stop named service, or all if no name supplied');
// Command prepared separately.
// Returns top-level command for adding more commands.
program
.addCommand(build.makeBuildCommand());
```
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)).
### Specify the argument syntax
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.
Expand Down Expand Up @@ -397,13 +409,11 @@ async function main() {
}
```
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.
A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error.
### 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.
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.
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 can specify a custom name with the `executableFile` configuration option.
Expand All @@ -422,14 +432,12 @@ program
.parse(process.argv);
```
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.
Specifying a name with `executableFile` will override the default constructed name.
If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.
## Automated --help
## 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:
The help information is auto-generated based on the information commander already knows about your program. The default
help option is `-h,--help`.
```bash
$ ./examples/pizza --help
Expand All @@ -444,17 +452,25 @@ Options:
-b, --bbq Add bbq sauce
-c, --cheese <type> Add the specified type of cheese (default: "marble")
-C, --no-cheese You do not want any cheese
-h, --help output usage information
-h, --help display help for command
```
A `help` command is added by default if your command has subcommands. It can be used alone, or with a subcommand name to show
further help for the subcommand. These are effectively the same if the `shell` program has implicit help:
```bash
shell help
shell --help
shell help spawn
shell spawn --help
```
### Custom help
You can display arbitrary `-h, --help` information
You can display extra `-h, --help` information
by listening for "--help". Commander will automatically
exit once you are done so that the remainder of your program
does not execute causing undesired behaviors, for example
in the following executable "stuff" will not output when
`--help` is used.
exit after displaying the help.
```js
#!/usr/bin/env node
Expand All @@ -467,9 +483,7 @@ program
.option('-b, --bar', 'enable some bar')
.option('-B, --baz', 'enable some baz');
// must be before .parse() since
// node's emit() is immediate
// must be before .parse()
program.on('--help', function(){
console.log('')
console.log('Examples:');
Expand All @@ -488,11 +502,11 @@ Yields the following help output when `node script-name.js -h` or `node script-n
Usage: custom-help [options]
Options:
-h, --help output usage information
-V, --version output the version number
-f, --foo enable some foo
-b, --bar enable some bar
-B, --baz enable some baz
-h, --help display help for command
Examples:
$ custom-help --help
Expand Down Expand Up @@ -550,6 +564,16 @@ program
.helpOption('-e, --HELP', 'read more information');
```
### .addHelpCommand()
You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`.
You can both turn on and customise the help command by supplying the name and description:
```js
program.addHelpCommand('assist [command]', 'show assistance');
```
### .help(cb)
Output help information and exit immediately.
Expand All @@ -564,9 +588,10 @@ program.on('option:verbose', function () {
process.env.VERBOSE = this.verbose;
});
// error on unknown commands
program.on('command:*', function () {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]]);
// custom error on unknown command
program.on('command:*', function (operands) {
console.error(`Invalid command '${operands[0]}'. Did you mean:`);
console.error(mySuggestions(operands[0]));
process.exit(1);
});
```
Expand Down Expand Up @@ -686,12 +711,6 @@ program
console.log(' $ deploy exec async');
});
program
.command('*')
.action(function(env){
console.log('deploying "%s"', env);
});
program.parse(process.argv);
```
Expand Down
20 changes: 6 additions & 14 deletions Readme_zh-CN.md
Expand Up @@ -31,15 +31,15 @@
- [.help(cb)](#helpcb)
- [自定义事件监听](#%e8%87%aa%e5%ae%9a%e4%b9%89%e4%ba%8b%e4%bb%b6%e7%9b%91%e5%90%ac)
- [零碎知识](#%e9%9b%b6%e7%a2%8e%e7%9f%a5%e8%af%86)
- [避免选项命名冲突](#避免选项命名冲突)
- [避免选项命名冲突](#%e9%81%bf%e5%85%8d%e9%80%89%e9%a1%b9%e5%91%bd%e5%90%8d%e5%86%b2%e7%aa%81)
- [TypeScript](#typescript)
- [Node 选项例如 --harmony](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony)
- [Node 选项例如 `--harmony`](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony)
- [Node 调试](#node-%e8%b0%83%e8%af%95)
- [重载退出(exit)处理](#%e9%87%8d%e8%bd%bd%e9%80%80%e5%87%baexit%e5%a4%84%e7%90%86)
- [例子](#%e4%be%8b%e5%ad%90)
- [许可证](#%e8%ae%b8%e5%8f%af%e8%af%81)
- [支持](#%e6%94%af%e6%8c%81)
- [企业使用Commander](#企业使用Commander)
- [企业使用Commander](#%e4%bc%81%e4%b8%9a%e4%bd%bf%e7%94%a8commander)

## 安装

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

// must be before .parse() since
// node's emit() is immediate
// must be before .parse()
program.on('--help', function(){
console.log('');
console.log('Examples:');
Expand All @@ -474,7 +472,7 @@ console.log('stuff');
Usage: custom-help [options]

Options:
-h, --help output usage information
-h, --help display help for command
-V, --version output the version number
-f, --foo enable some foo
-b, --bar enable some bar
Expand Down Expand Up @@ -670,12 +668,6 @@ program
console.log(' $ deploy exec async');
});
program
.command('*')
.action(function(env){
console.log('deploying "%s"', env);
});
program.parse(process.argv);
```
Expand Down
4 changes: 1 addition & 3 deletions examples/custom-help
Expand Up @@ -9,9 +9,7 @@ program
.option('-b, --bar', 'enable some bar')
.option('-B, --baz', 'enable some baz');

// must be before .parse() since
// node's emit() is immediate

// must be before .parse()
program.on('--help', function() {
console.log('');
console.log('Examples:');
Expand Down
36 changes: 36 additions & 0 deletions examples/defaultCommand.js
@@ -0,0 +1,36 @@
// const commander = require('commander'); // (normal include)
const commander = require('../'); // include commander in git clone of commander repo
const program = new commander.Command();

// Example program using the command configuration option isDefault to specify the default command.
//
// $ node defaultCommand.js build
// build
// $ node defaultCommand.js serve -p 8080
// server on port 8080
// $ node defaultCommand.js -p 443
// server on port 443

program
.command('build')
.description('build web site for deployment')
.action(() => {
console.log('build');
});

program
.command('deploy')
.description('deploy web site to production')
.action(() => {
console.log('deploy');
});

program
.command('serve', { isDefault: true })
.description('launch web server')
.option('-p,--port <port_number>', 'web port')
.action((opts) => {
console.log(`server on port ${opts.port}`);
});

program.parse(process.argv);
6 changes: 0 additions & 6 deletions examples/deploy
Expand Up @@ -34,10 +34,4 @@ program
console.log();
});

program
.command('*')
.action(function(env) {
console.log('deploying "%s"', env);
});

program.parse(process.argv);
47 changes: 47 additions & 0 deletions examples/nestedCommands.js
@@ -0,0 +1,47 @@
// const commander = require('commander'); // (normal include)
const commander = require('../'); // include commander in git clone of commander repo
const program = new commander.Command();

// Commander supports nested subcommands.
// .command() can add a subcommand with an action handler or an executable.
// .addCommand() adds a prepared command with an actiomn handler.

// Example output:
//
// $ node nestedCommands.js brew tea
// brew tea
// $ node nestedCommands.js heat jug
// heat jug

// Add nested commands using `.command()`.
const brew = program.command('brew');
brew
.command('tea')
.action(() => {
console.log('brew tea');
});
brew
.command('tea')
.action(() => {
console.log('brew tea');
});

// Add nested commands using `.addCommand().
// The command could be created separately in another module.
function makeHeatCommand() {
const heat = new commander.Command('heat');
heat
.command('jug')
.action(() => {
console.log('heat jug');
});
heat
.command('pot')
.action(() => {
console.log('heat pot');
});
return heat;
}
program.addCommand(makeHeatCommand());

program.parse(process.argv);

0 comments on commit 1345f98

Please sign in to comment.