commander.js
commander is a lightweight nodejs module that provides powerful functions for user command line input and parameter parsing.
Characteristics of commander:
- Self recording code
- Auto generate help
- Merge short parameters
- Default options
- Mandatory options
- Command parsing
- Prompt
install
npm install commander
use
Introducing a commander into a file can be used by directly introducing a program object or creating an instance.
const { program } = require('commander'); program.version("v1.0.0");
const { Command } = require('commander'); const program = new Command(); program.version("v1.0.0");
By looking at the source code, we can know that program is actually a newly created instance, which can more clearly access the global commands.
Source code fragment:
exports = module.exports = new Command(); exports.program = exports; exports.Command = Command;
Option option
The Commander uses the. option() method to define options, and can attach an introduction to the options. Each option can define a short option name (- followed by a single character) and a long option name (- followed by one or more words), separated by commas, spaces, or |.
Syntax:
options(flags, description, fn, defaultValue)
commander.option( "-f, --filename [filename]", "The filename to use when reading from stdin. This will be used in source-maps, errors etc." );
Source code analysis:
lib\command.js option()
Using the Corey writing method, the actual call is_ optionsEx() method
options(flags, description, fn, defaultValue) { return this._optionEx({}, flags, description, fn, defaultValue); }
_ optionsEx(), create an options instance. fn can be in the form of function, regular, etc. note that the regular form should not be used as far as possible. Since Commander v7, this function is no longer recommended.
_optionEx(config, flags, description, fn, defaultValue) { // Create an option instance const option = this.createOption(flags, description); if (typeof fn === "function") { option.default(defaultValue).argParser(fn); } else if (fn instanceof RegExp) { // deprecated ... } else { option.default(fn); } return this.addOption(option); }
In the Option constructor, a splitOptionFlags() method will be called to resolve long and broken identifiers, such as - m, - mixed < value >. attributeName() will return a character of camelcase, for example -- file name will be resolved to fileName.
Cut out different characters through spaces, | and.
function splitOptionFlags(flags) { let shortFlag; let longFlag; const flagParts = flags.split(/[ |,]+/); if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift(); longFlag = flagParts.shift(); if (!shortFlag && /^-[^-]$/.test(longFlag)) { shortFlag = longFlag; longFlag = undefined; } return { shortFlag, longFlag }; }
At the same time, judge whether the reception parameters are set according to the characters in flags< Value > means that a parameter must be passed in when executing the command, < value... > means that multiple parameters can be accepted, and [value] means configurable parameters.
this.required = flags.includes("<"); this.optional = flags.includes("["); this.variadic = /\w\.\.\.[>\]]$/.test(flags)
After creating an option object, put it into addOption() for processing. This method will register and listen for options.
addOption(option) { const oname = option.name(); const name = option.attributeName(); // register this.options.push(option); const handleOptionValue = (val, invalidValueMessage, valueSource) => { // ... }; this.on("option:" + oname, (val) => { const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`; handleOptionValue(val, invalidValueMessage, "cli"); }); if (option.envVar) { this.on("optionEnv:" + oname, (val) => { const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`; handleOptionValue(val, invalidValueMessage, "env"); }); } return this; }
Command inherits the event module EventEmitter in node to monitor and trigger option s.
After writing the option configuration, you need to call the program.parse(process.argv) method to parse the user's input.
parse(argv, parseOptions)
parse(argv, parseOptions) { const userArgs = this._prepareUserArgs(argv, parseOptions); this._parseCommand([], userArgs); return this; }
In_ Parse and read the user's input in the prepareUserArgvs method. The first two elements of the array obtained by process.argv are the node installation address and the running script path respectively, followed by the user's input. Therefore, these two elements need to be filtered out first. Multiple argv conventions are also supported.
_prepareUserArgs(argv, parseOptions) { parseOptions = parseOptions || {}; this.rawArgs = argv.slice(); let userArgs; switch (parseOptions.from) { case undefined: case 'node': this._scriptPath = argv[1]; userArgs = argv.slice(2); break; case 'electron': if (process.defaultApp) { this._scriptPath = argv[1]; userArgs = argv.slice(2); } else { userArgs = argv.slice(1); } break; case 'user': userArgs = argv.slice(0); break; default: throw new Error( `unexpected parse option { from: '${parseOptions.from}' }` ); } if (!this._scriptPath && require.main) { this._scriptPath = require.main.filename; } this._name = this._name || (this._scriptPath && path.basename(this._scriptPath, path.extname(this._scriptPath))); return userArgs; }
After the user input is obtained, these parameters are parsed.
_parseCommand()
_parseCommand(operands, unknown) { const parsed = this.parseOptions(unknown); // The command section continues below }
parseOptions reads the configuration of the input
parseOptions(argv) { const operands = []; const unknown = []; let dest = operands; const args = argv.slice(); // To judge whether it is an option configuration, it must start with - function maybeOption(arg) { return arg.length > 1 && arg[0] === '-'; } let activeVariadicOption = null; // Step by step read configuration while (args.length) { const arg = args.shift(); // --Stop reading, that is -- the subsequent configuration will not take effect // For example, node Src / index.js -- number 1 2 3 -- C a B // -c it doesn't work if (arg === '--') { if (dest === unknown) dest.push(arg); dest.push(...args); break; } // Handle multi parameter situations if (activeVariadicOption && !maybeOption(arg)) { // When this listening is triggered, handleOptionValue will be executed, in which the value of variadic will be judged, and all parameters will be put into an array this.emit(`option:${activeVariadicOption.name()}`, arg); continue; } activeVariadicOption = null; if (maybeOption(arg)) { // Check to see if the command is already configured const option = this._findOption(arg); if (option) { // As mentioned earlier, when configuring an option, if there is a required parameter to configure < value >, the required value of the option is true if (option.required) { // Read value const value = args.shift(); if (value === undefined) this.optionMissingArgument(option); // Trigger monitoring method this.emit(`option:${option.name()}`, value); // Optional parameters configured } else if (option.optional) { let value = null; if (args.length > 0 && !maybeOption(args[0])) { value = args.shift(); } this.emit(`option:${option.name()}`, value); } else { // boolean flag this.emit(`option:${option.name()}`); } // As mentioned earlier, when... > such as < value... > exists in the configured option, the variadic value is true activeVariadicOption = option.variadic ? option : null; continue; } } // Combining flag s, such as - abc, will traverse to determine whether - a -b -c is a configured option if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { // Disassembly and assembly const option = this._findOption(`-${arg[1]}`); if (option) { if ( option.required || (option.optional && this._combineFlagAndOptionalValue) ) { this.emit(`option:${option.name()}`, arg.slice(2)); } else { this.emit(`option:${option.name()}`); args.unshift(`-${arg.slice(2)}`); } continue; } } // Parsing -- foo=bar parameter transfer format if (/^--[^=]+=/.test(arg)) { const index = arg.indexOf('='); const option = this._findOption(arg.slice(0, index)); if (option && (option.required || option.optional)) { this.emit(`option:${option.name()}`, arg.slice(index + 1)); continue; } } if (maybeOption(arg)) { dest = unknown; } // Parsing command is explained below dest.push(arg); } return { operands, unknown }; }
From the above source code analysis, it can be summarized as follows:
- Multiple short options can be combined and abbreviated, and the last option can be attached with parameters. For example, - a -b -c 1 can be written as - abc 1.
- --You can mark the end of the option. Subsequent parameters will not be interpreted by the command and can be used normally.
- Parameters can be passed through
- Multiple parameters can be passed
Common option types
- The boolean option does not require configuration parameters. We usually use this type.
- Set parameters (after the option is declared with angle brackets, such as -- expect < value >), which will be defined as undefined if no specific options and parameters are specified on the command line.
const { Command } = require('commander'); const program = new Command(); program.option('-e --example', 'this is a boolean type option'); program.option('-t --type <type>', 'must set an param or an error will occur'); program.parse(process.argv); console.log(program.opts());
eg.
node index.js -e { example: true } node index.js -t error: option '-t --type <type>' argument missing node index.js -t a { type: 'a' }
- Negative, you can define a boolean type long option starting with no -. When you use this option on the command line, the value of the corresponding option is set to false. When only the option with no - is defined and the corresponding option without no - is not defined, the default value of this option will be set to true.
program.option('--no-example', 'no example');
node index.js --no-example { example: false }
Source code analysis:
After the option is configured, the command will be monitored. Executing the flag will trigger the handleOptionValue method to set the value of the option according to whether the user has set negative or default value
handleOptionValue
const handleOptionValue = (val, invalidValueMessage, valueSource) => { const oldValue = this.getOptionValue(name); // Parameter processing is explained in the user defined options section // .. if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') { if (val == null) { this.setOptionValueWithSource( name, // Whether to take the negative value. If the negative value is taken, the value is false. If not, judge whether there is a value. If not, assign the value to true option.negate ? false : defaultValue || true, valueSource ); } else { this.setOptionValueWithSource(name, val, valueSource); } } else if (val !== null) { this.setOptionValueWithSource( name, option.negate ? false : val, valueSource ); } };
- Optional parameter (- - optional [value]). This option can be used as a boolean option without parameters, and get the value from the parameters with parameters.
program.option('-f [filename]', 'optional');
node index.js -f { f: true } node index.js -f index.js { f: 'index.js' }
- Required options
You can set the option to be mandatory through the. requiredOption() method. Required options are either set with default values or must be entered on the command line. The corresponding attribute fields must be assigned during parsing. The remaining parameters of this method are consistent with. option().
program.requiredOption('-r --required <type>', 'must');
node index.js error: required option '-r --required <type>' not specified
Of course, you can set a default value
program.requiredOption('-r --required <type>', 'must', 'a');
node index.js { required: 'a' }
- Variable length parameter options
When defining options, you can set parameters to variable length parameters by using. In the command line, you can enter multiple parameters, which will be stored in the corresponding attribute field in the form of array after parsing. Instructions entered by the user are treated as variable length parameters until the next option is entered (- or --). As with normal parameters, you can mark the end of the current command through -- to mark the end of the current command.
program.option('-n <numbers...>', 'set numbers'); program.option('-c <chars...>', 'set chars');
node index.js -n 1 2 3 4 -c a b c { n: [ '1', '2', '3', '4' ], c: [ 'a', 'b', 'c' ] }
- edition
The. version() method can set the version. Its default options are - V and -- version. After setting the version, the command line will output the current version number.
program.version('v1.0.0')
Version options also support custom setting option names. You can pass some parameters (long option names and descriptions) in the. version() method. The usage is similar to that of the. option() method.
program.version('v1.0.0', '-a --aversion', 'current version');
Source code analysis:
The version method is actually very simple, which is to judge whether the user has customized information when configuring version information, such as starting commands. Then create an option to listen after registration.
version(str, flags, description) { if (str === undefined) return this._version; this._version = str; flags = flags || '-V, --version'; description = description || 'output the version number'; const versionOption = this.createOption(flags, description); this._versionOptionName = versionOption.attributeName(); this.options.push(versionOption); this.on('option:' + versionOption.name(), () => { this._outputConfiguration.writeOut(`${str}\n`); this._exit(0, 'commander.version', str); }); return this; }
- Using the addOption method
In most cases, options can be added through the. option() method. However, for some uncommon use cases, you can also directly construct option objects to configure options in more detail.
program .addOption( new Option('-t, --timeout <delay>', 'timeout in seconds').default( 60, 'one minute' ) ) .addOption( new Option('-s, --size <type>', 'size').choices([ 'small', 'medium', 'large', ]) );
node index.js -s small { timeout: 60, size: 'small' } node index.js -s mini error: option '-s, --size <type>' argument 'mini' is invalid. Allowed choices are small, medium, large.
Source code analysis: just add a fn and judge the value when passing in the value.
function choices(values) { this.argChoices = values; this.parseArg = (arg, previous) => { if (!values.includes(arg)) { throw new InvalidArgumentError( `Allowed choices are ${values.join(', ')}.` ); } if (this.variadic) { return this._concatValue(arg, previous); } return arg; }; return this; }
- Custom options
The parameters of the option can be processed through the user-defined function, which receives two parameters, namely, the parameter value newly entered by the user and the current existing parameter value (that is, the return value after the user-defined processing function was called last time), and returns the new option parameter value. That is, the third parameter in option. The fourth parameter is the initial value
User defined functions are applicable to scenarios, including parameter type conversion, parameter temporary storage, or other user-defined processing scenarios.
program.option( '-f --float <number>', 'process float argument', (value, previous) => { return Number(value) + previous; }, 2 );
node index.js -f 3 { float: 5 }
Source code analysis:
const handleOptionValue = (val, invalidValueMessage, valueSource) => { const oldValue = this.getOptionValue(name); // When the third parameter of option is configured if (val !== null && option.parseArg) { try { val = option.parseArg( val, // defaultValue is the fourth parameter oldValue === undefined ? defaultValue : oldValue ); } catch (err) { if (err.code === "commander.invalidArgument") { const message = `${invalidValueMessage} ${err.message}`; this._displayError(err.exitCode, err.code, message); } throw err; } // Handle multi parameter situations } else if (val !== null && option.variadic) { val = option._concatValue(val, oldValue); } // ... // View the explanation of inversion }
Configuration command
Commands can be configured through. command() or. addCommand(), which can be implemented in two ways: binding processing functions for commands, or writing commands as an executable file separately. Subcommands support nesting.
The first argument to. command() is the command name. Command parameters can follow the name or be specified separately with. argument(). The parameter can be required (indicated by angle brackets), optional (indicated by square brackets) or variable length parameter (indicated by a dot. If used, it can only be the last parameter).
program .command('clone <source> [destination]') .description('clone a repository into a newly created directory') .action((source, destination) => { console.log('clone command called'); });
Use. argument() on the Command object to specify Command parameters in order. The method accepts parameter names and parameter descriptions. Parameters can be required (indicated by angle brackets, such as < required >) or optional (indicated by square brackets, such as [optional]).
program .command('login') .description('login') .argument('<username>', 'user') .argument('[password]', 'password', 'no password') .action((username, password) => { console.log(`login, ${username} - ${password}`); });
Variable parameters are declared by adding... After the parameter name, and only the last parameter supports this usage. Variable parameters are passed to the handler as an array.
program .command('readFile') .description('read multiple file') .argument('<username>', 'user') .argument('[password]', 'password', 'no password') .argument('<filepath...>') .action((username, password, args) => { args.forEach((dir) => { console.log('rmdir %s', dir); }); console.log(`username: ${username}, pass: ${password}, args: ${args}`); });
Source code analysis: create a new command
command(nameAndArgs, actionOptsOrExecDesc, execOpts) { let desc = actionOptsOrExecDesc; let opts = execOpts; if (typeof desc === 'object' && desc !== null) { opts = desc; desc = null; } opts = opts || {}; // Parsing and obtaining input commands and parameters const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/); // Create a command const cmd = this.createCommand(name); if (desc) { cmd.description(desc); cmd._executableHandler = true; } if (opts.isDefault) this._defaultCommandName = cmd._name; cmd._hidden = !!(opts.noHelp || opts.hidden); cmd._executableFile = opts.executableFile || null; // Add parameter if (args) cmd.arguments(args); this.commands.push(cmd); cmd.parent = this; // Inheritance properties cmd.copyInheritedSettings(this); if (desc) return this; return cmd; }
action is used to register command callbacks.
action(fn) { const listener = (args) => { const expectedArgsCount = this._args.length; const actionArgs = args.slice(0, expectedArgsCount); if (this._storeOptionsAsProperties) { actionArgs[expectedArgsCount] = this; } else { actionArgs[expectedArgsCount] = this.opts(); } actionArgs.push(this); return fn.apply(this, actionArgs); }; this._actionHandler = listener; return this; }
The next step is how to parse the execution command. Remember the process when we parsed the option before
parse() -> _ Parsecommand(), in the parseOptions method, we still have some code left to process the command. Now let's take a look.
Source code analysis:
parseOptions(argv) { const operands = []; const unknown = []; let dest = operands; const args = argv.slice(); while (args.length) { // Parsing the option section // ... // When enablePositionalOptions is enabled, all the parameters behind the command will be defined as unknown parameters instead of command options. At the same time, if you want to start enablePositionalOptions in the sub command, you need to start enablePositionalOptions in the parent command first. if ( (this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0 ) { if (this._findCommand(arg)) { operands.push(arg); if (args.length > 0) unknown.push(...args); break; } else if ( arg === this._helpCommandName && this._hasImplicitHelpCommand() ) { operands.push(arg); if (args.length > 0) operands.push(...args); break; } else if (this._defaultCommandName) { unknown.push(arg); if (args.length > 0) unknown.push(...args); break; } } if (this._passThroughOptions) { dest.push(arg); if (args.length > 0) dest.push(...args); break; } dest.push(arg); } return { operands, unknown }; }
If the enablePositionalOptions configuration is not enabled, the command and subsequent parameters will be saved in the operands array. When enabled, the parameters behind the command will become unknown.
After parsing the parameters entered by the user, continue to execute_ parseCommand method
_parseCommand(operands, unknown) { // Resolution configuration const parsed = this.parseOptions(unknown); this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env operands = operands.concat(parsed.operands); unknown = parsed.unknown; this.args = operands.concat(unknown); // Find the matching command and run the command using the child process if (operands && this._findCommand(operands[0])) { return this._dispatchSubcommand(operands[0], operands.slice(1), unknown); } // If the command is not found, judge whether there is a help command, and the first parameter entered by the user is help if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { // There are and only one command if (operands.length === 1) { this.help(); } // Execute the second command return this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]); } // The help command was not found, or the first parameter entered by the user is unknown, but there is a default name if (this._defaultCommandName) { outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command return this._dispatchSubcommand( this._defaultCommandName, operands, unknown ); } // ... const commandEvent = `command:${this.name()}`; // command processor present if (this._actionHandler) { checkForUnknownOptions(); // Processing parameters this._processArguments(); let actionResult; actionResult = this._chainOrCallHooks(actionResult, 'preAction'); actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs) ); // Trigger parent command if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy actionResult = this._chainOrCallHooks(actionResult, 'postAction'); return actionResult; } // Trigger parent listener if (this.parent && this.parent.listenerCount(commandEvent)) { checkForUnknownOptions(); this._processArguments(); this.parent.emit(commandEvent, operands, unknown); // Processing parameters } else if (operands.length) { if (this._findCommand('*')) { return this._dispatchSubcommand('*', operands, unknown); } if (this.listenerCount('command:*')) { this.emit('command:*', operands, unknown); } else if (this.commands.length) { this.unknownCommand(); } else { checkForUnknownOptions(); this._processArguments(); } // No command exists } else if (this.commands.length) { checkForUnknownOptions(); this.help({ error: true }); } else { checkForUnknownOptions(); this._processArguments(); } }
At this point, the whole parsing process is completed.
Declare uniform parameters
The parameters of the command processing function. In addition to all the parameters declared for the command, two additional parameters will be attached: one is the parsed option, and the other is the command object itself.
program .argument('<name>') .option('-t, --title <title>', 'title to use before name') .option('-d, --de') .action((name, options, command) => { console.log(name); console.log(options); console.log(command.name()); });
Help information
The help information is automatically generated by the Commander based on your program. The default help options are - H, - help.
node index.js -h Usage: index [options] Options: -h, --help display help for command
Source code analysis:
commander.js provides a help class, but its methods are static and can be overridden. There is a createHelp method in the command class, which overrides the original method of help through the Object.assign() method.
createHelp() { return Object.assign(new Help(), this.configureHelp()); }
formatHelp generates help text, parses and processes the set command, option, argument, etc
formatHelp(cmd, helper) { const termWidth = helper.padWidth(cmd, helper); const helpWidth = helper.helpWidth || 80; const itemIndentWidth = 2; const itemSeparatorWidth = 2; // format function formatItem(term, description) { if (description) { const fullText = `${term.padEnd( termWidth + itemSeparatorWidth )}${description}`; return helper.wrap( fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth ); } return term; } function formatList(textArray) { return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth)); } let output = [`Usage: ${helper.commandUsage(cmd)}`, '']; // descriptor const commandDescription = helper.commandDescription(cmd); if (commandDescription.length > 0) { output = output.concat([commandDescription, '']); } // parameter const argumentList = helper.visibleArguments(cmd).map((argument) => { return formatItem( helper.argumentTerm(argument), helper.argumentDescription(argument) ); }); if (argumentList.length > 0) { output = output.concat(['Arguments:', formatList(argumentList), '']); } // option const optionList = helper.visibleOptions(cmd).map((option) => { return formatItem( helper.optionTerm(option), helper.optionDescription(option) ); }); if (optionList.length > 0) { output = output.concat(['Options:', formatList(optionList), '']); } // command const commandList = helper.visibleCommands(cmd).map((cmd) => { return formatItem( helper.subcommandTerm(cmd), helper.subcommandDescription(cmd) ); }); if (commandList.length > 0) { output = output.concat(['Commands:', formatList(commandList), '']); } return output.join('\n'); }
custom
Use the addHelpText method to add additional help information.
program.addHelpText('after', `call help`);
node index.js -h Usage: index [options] Options: -h, --help display help for command call help
The first parameter of the addHelpText method is the location where the added help information is displayed,
It includes the following:
-
Before all: displayed as a global header bar
-
Before: show before built-in help information
-
After: display after built-in help information
-
After all: displayed as the global end column
Source code analysis:
After the addHelpText method is executed, the event will be monitored. When the input help command is executed, all events will be executed in order to display user-defined information.
addHelpText
addHelpText(position, text) { const allowedValues = ['beforeAll', 'before', 'after', 'afterAll']; // filter if (!allowedValues.includes(position)) { throw new Error(`Unexpected value for position to addHelpText. Expecting one of '${allowedValues.join("', '")}'`); } const helpEvent = `${position}Help`; // Listening events this.on(helpEvent, (context) => { let helpStr; if (typeof text === 'function') { helpStr = text({ error: context.error, command: context.command }); } else { helpStr = text; } // Ignore falsy value when nothing to output. if (helpStr) { context.write(`${helpStr}\n`); } }); return this; }
outputHelp
outputHelp(contextOptions) { let deprecatedCallback; if (typeof contextOptions === 'function') { deprecatedCallback = contextOptions; contextOptions = undefined; } const context = this._getHelpContext(contextOptions); // Traverse the beforeAllHelp event that triggers all ancestor commands getCommandAndParents(this) .reverse() // Trigger beforeAllHelp .forEach((command) => command.emit('beforeAllHelp', context)); // Trigger beforeHelp this.emit('beforeHelp', context); let helpInformation = this.helpInformation(context); if (deprecatedCallback) { helpInformation = deprecatedCallback(helpInformation); if ( typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation) ) { throw new Error('outputHelp callback must return a string or a Buffer'); } } context.write(helpInformation); // Trigger help instruction this.emit(this._helpLongFlag); // deprecated // Trigger afterHelp this.emit('afterHelp', context); // Traverse the afterAllHelp event that triggers all ancestor commands getCommandAndParents(this).forEach((command) => // Trigger afterAllHelp command.emit('afterAllHelp', context) ); }
showHelpAfterError displays help information
program.showHelpAfterError(); // perhaps program.showHelpAfterError('(add --help for additional information)');
node index.js -asd error: unknown option '-asd' (add --help for additional information)