關(guān)于commander.js使用及源碼分析
commander.js
commander
是一個(gè)輕巧的nodejs
模塊,提供了用戶命令行輸入和參數(shù)解析強(qiáng)大功能。
commander
的特性:
- 自記錄代碼
- 自動(dòng)生成幫助
- 合并短參數(shù)
- 默認(rèn)選項(xiàng)
- 強(qiáng)制選項(xiàng)??
- 命令解析
- 提示符
安裝
npm install commander
使用
在文件中引入commander
,可以通過直接引入一個(gè)program
對(duì)象或者創(chuàng)建一個(gè)實(shí)例的方式進(jìn)行使用。
const { program } = require('commander'); program.version("v1.0.0");
const { Command } = require('commander'); const program = new Command(); program.version("v1.0.0");
通過查看源碼可以得知program
其實(shí)就是一個(gè)剛創(chuàng)建的實(shí)例,可以更明確地訪問全局命令。
源碼片段:
exports = module.exports = new Command(); exports.program = exports; exports.Command = Command;
option選項(xiàng)
Commander
使用.option()
方法來定義選項(xiàng),同時(shí)可以附加選項(xiàng)的簡介。
每個(gè)選項(xiàng)可以定義一個(gè)短選項(xiàng)名稱(-
后面接單個(gè)字符)和一個(gè)長選項(xiàng)名稱(--
后面接一個(gè)或多個(gè)單詞),使用逗號(hào)、空格或|
分隔。
語法:
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." );
源碼解析:
lib\command.js
option()
采用柯里化寫法, 實(shí)際調(diào)用的是_optionsEx()
方法
options(flags, description, fn, defaultValue) { return this._optionEx({}, flags, description, fn, defaultValue); }
_optionsEx()
, 創(chuàng)建一個(gè)options
實(shí)例,fn可以是函數(shù)、正則等形式,需要注意的是盡量不要使用正則形式,自 Commander v7 起,該功能不再推薦使用。
_optionEx(config, flags, description, fn, defaultValue) { // 創(chuàng)建一個(gè)option實(shí)例 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); }
在Option
構(gòu)造函數(shù)中,會(huì)調(diào)用一個(gè)splitOptionFlags()
方法,用于解析長、斷標(biāo)識(shí),比如說-m,--mixed <value>
。
attributeName()
會(huì)返回一個(gè)camelcase
的字符,例如--file-name
會(huì)被解析成fileName
。
通過空格、|
和,
來切割出不同的字符。
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 }; }
同時(shí),根據(jù)flags
中的字符進(jìn)行判斷是否有設(shè)置接收參數(shù)。
<value>
表示執(zhí)行該命令時(shí)必須傳入一個(gè)參數(shù),<value...>
表示可接受多個(gè)參數(shù),[value]
表示可配置參數(shù)。
this.required = flags.includes("<"); this.optional = flags.includes("["); this.variadic = /\w\.\.\.[>\]]$/.test(flags)
在創(chuàng)建了一個(gè)option
對(duì)象后,將其放入addOption()
中進(jìn)行處理,該方法會(huì)對(duì)option
進(jìn)行注冊、監(jiān)聽等操作。
addOption(option) { const oname = option.name(); const name = option.attributeName(); // 注冊 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
繼承了node
中的事件模塊EventEmitter
實(shí)現(xiàn)對(duì)option
的監(jiān)聽和觸發(fā)。
在寫好option
配置后,需要調(diào)用program.parse(process.argv)
方法對(duì)用戶的輸入進(jìn)行解析。
parse(argv, parseOptions)
parse(argv, parseOptions) { const userArgs = this._prepareUserArgs(argv, parseOptions); this._parseCommand([], userArgs); return this; }
在_prepareUserArgvs
方法中解析讀取用戶的輸入。
process.argv
獲取的數(shù)組前兩個(gè)元素分別是node
安裝地址跟運(yùn)行的script
路徑,后邊的才是用戶的輸入,因此需要先過濾掉這兩個(gè)。
同時(shí)還支持多個(gè)argv
約定。
_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; }
在獲取了用戶輸入后,就是對(duì)這些參數(shù)進(jìn)行解析。
_parseCommand()
_parseCommand(operands, unknown) { const parsed = this.parseOptions(unknown); // command部分下邊繼續(xù)講解 }
parseOptions
讀取輸入的配置
parseOptions(argv) { const operands = []; const unknown = []; let dest = operands; const args = argv.slice(); // 判斷是否是option配置, 必須要有一個(gè)-開頭 function maybeOption(arg) { return arg.length > 1 && arg[0] === '-'; } let activeVariadicOption = null; // 逐步讀取配置 while (args.length) { const arg = args.shift(); // --終止讀取 也就是說 -- 后邊的配置并不會(huì)生效 // 例如 node src/index.js - --number 1 2 3 -- -c a b // -c其實(shí)沒有用 if (arg === '--') { if (dest === unknown) dest.push(arg); dest.push(...args); break; } // 處理多參數(shù)情況 if (activeVariadicOption && !maybeOption(arg)) { // 當(dāng)觸發(fā)這個(gè)監(jiān)聽時(shí)會(huì)執(zhí)行handleOptionValue,里邊會(huì)判斷variadic的值將所有的參數(shù)放進(jìn)一個(gè)數(shù)組中 this.emit(`option:${activeVariadicOption.name()}`, arg); continue; } activeVariadicOption = null; if (maybeOption(arg)) { // 查看是否已經(jīng)配置有該命令 const option = this._findOption(arg); if (option) { // 前邊提到的當(dāng)配置option時(shí)如果有配置 <value> 必填參數(shù),那么該option的required值為true if (option.required) { // 讀取value const value = args.shift(); if (value === undefined) this.optionMissingArgument(option); // 觸發(fā)監(jiān)聽方法 this.emit(`option:${option.name()}`, value); // 配置了可選參數(shù) } 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()}`); } // 前面提到當(dāng)配置的option中存在 ...> 如<value...>時(shí),variadic值為true activeVariadicOption = option.variadic ? option : null; continue; } } // 組合flag, 比如像 -abc , 會(huì)遍歷判斷 -a -b -c是否是配置過的option if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { // 拆解組合 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; } } // 解析--foo=bar傳參格式 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; } // 解析command 下面在講解 dest.push(arg); } return { operands, unknown }; }
從上面的源碼解析中可以總結(jié)如下:
- 多個(gè)短選項(xiàng)可以合并簡寫,最后一個(gè)選項(xiàng)可以附加參數(shù), 比如
-a -b -c 1
可以寫成-abc 1
。 --
可以標(biāo)記選項(xiàng)的結(jié)束,后續(xù)的參數(shù)均不會(huì)被命令解釋,可以正常使用。- 可以通過
=
傳參 - 可以傳遞多個(gè)參數(shù)
常用選項(xiàng)類型
boolean
型, 選項(xiàng)無需配置參數(shù), 通常我們使用的都是此類型。- 設(shè)置參數(shù)(使用尖括號(hào)聲明在該選項(xiàng)后,如
--expect <value>
),如果在命令行中不指定具體的選項(xiàng)及參數(shù),則會(huì)被定義為undefined
。
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' }
取反, 可以定義一個(gè)以 no-
開頭的 boolean
型長選項(xiàng)。
在命令行中使用該選項(xiàng)時(shí),會(huì)將對(duì)應(yīng)選項(xiàng)的值置為 false
。
當(dāng)只定義了帶 no-
的選項(xiàng),未定義對(duì)應(yīng)不帶 no-
的選項(xiàng)時(shí),該選項(xiàng)的默認(rèn)值會(huì)被置為 true
值。
program.option('--no-example', 'no example');
node index.js --no-example { example: false }
源碼解析:
在配置了option
之后,會(huì)對(duì)該命令進(jìn)行監(jiān)聽,執(zhí)行該flag
會(huì)觸發(fā)handleOptionValue
方法,根據(jù)用戶是否有設(shè)置取反或者有默認(rèn)值,來設(shè)置該option
的值
handleOptionValue
const handleOptionValue = (val, invalidValueMessage, valueSource) => { const oldValue = this.getOptionValue(name); // 參數(shù)處理這部分放在自定義選項(xiàng)部分講解 // .. if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') { if (val == null) { this.setOptionValueWithSource( name, // 是否取反,取反的話值為false, 不取反就判斷是否有值,沒有就賦值為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 ); } };
可選參數(shù) (--optional [value]
), 該選項(xiàng)在不帶參數(shù)時(shí)可用作 boolean
選項(xiàng),在帶有參數(shù)時(shí)則從參數(shù)中得到值。
program.option('-f [filename]', 'optional');
node index.js -f { f: true } node index.js -f index.js { f: 'index.js' }
必填選項(xiàng)
通過.requiredOption()
方法可以設(shè)置選項(xiàng)為必填。
必填選項(xiàng)要么設(shè)有默認(rèn)值,要么必須在命令行中輸入,對(duì)應(yīng)的屬性字段在解析時(shí)必定會(huì)有賦值。該方法其余參數(shù)與.option()
一致。
program.requiredOption('-r --required <type>', 'must');
node index.js error: required option '-r --required <type>' not specified
當(dāng)然可以設(shè)置一個(gè)默認(rèn)值
program.requiredOption('-r --required <type>', 'must', 'a');
node index.js { required: 'a' }
變長參數(shù)選項(xiàng)
定義選項(xiàng)時(shí),可以通過使用...
來設(shè)置參數(shù)為可變長參數(shù)。在命令行中,用戶可以輸入多個(gè)參數(shù),解析后會(huì)以數(shù)組形式存儲(chǔ)在對(duì)應(yīng)屬性字段中。在輸入下一個(gè)選項(xiàng)前(-
或--
開頭),用戶輸入的指令均會(huì)被視作變長參數(shù)。與普通參數(shù)一樣的是,可以通過--
標(biāo)記當(dāng)前命令的結(jié)束。
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' ] }
版本
.version()
方法可以設(shè)置版本,其默認(rèn)選項(xiàng)為-V
和--version
,設(shè)置了版本后,命令行會(huì)輸出當(dāng)前的版本號(hào)。
program.version('v1.0.0')
版本選項(xiàng)也支持自定義設(shè)置選項(xiàng)名稱,可以在.version()
方法里再傳遞一些參數(shù)(長選項(xiàng)名稱、描述信息),用法與.option()
方法類似。
program.version('v1.0.0', '-a --aversion', 'current version');
源碼解析:
version
方法其實(shí)很簡單,就是判斷用戶配置version
信息時(shí)有沒有自定義信息,比如說啟動(dòng)命令這些。之后就是創(chuàng)建一個(gè)option
注冊后監(jiān)聽。
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; }
使用addOption
方法
大多數(shù)情況下,選項(xiàng)均可通過.option()
方法添加。但對(duì)某些不常見的用例,也可以直接構(gòu)造Option
對(duì)象,對(duì)選項(xiàng)進(jìn)行更詳盡的配置。
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.
源碼解析:
就是添加一個(gè)fn
在傳入值的時(shí)候判斷一下值就可以。
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; }
自定義選項(xiàng)
選項(xiàng)的參數(shù)可以通過自定義函數(shù)來處理,該函數(shù)接收兩個(gè)參數(shù),即用戶新輸入的參數(shù)值和當(dāng)前已有的參數(shù)值(即上一次調(diào)用自定義處理函數(shù)后的返回值),返回新的選項(xiàng)參數(shù)值。 也就是option
中的第三個(gè)參數(shù)。第四個(gè)參數(shù)就是初始值
自定義函數(shù)適用場景包括參數(shù)類型轉(zhuǎn)換,參數(shù)暫存,或者其他自定義處理的場景。
program.option( '-f --float <number>', 'process float argument', (value, previous) => { return Number(value) + previous; }, 2 );
node index.js -f 3 { float: 5 }
源碼解析:
const handleOptionValue = (val, invalidValueMessage, valueSource) => { const oldValue = this.getOptionValue(name); // 當(dāng)配置了option的第三個(gè)參數(shù) if (val !== null && option.parseArg) { try { val = option.parseArg( val, // defaultValue為第四個(gè)參數(shù) 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; } // 處理多參數(shù)情況 } else if (val !== null && option.variadic) { val = option._concatValue(val, oldValue); } // ... // 查看取反部分講解 }
配置命令
通過.command()
或.addCommand()
可以配置命令,有兩種實(shí)現(xiàn)方式:為命令綁定處理函數(shù),或者將命令單獨(dú)寫成一個(gè)可執(zhí)行文件。子命令支持嵌套。
.command()
的第一個(gè)參數(shù)為命令名稱。命令參數(shù)可以跟在名稱后面,也可以用.argument()
單獨(dú)指定。參數(shù)可為必選的(尖括號(hào)表示)、可選的(方括號(hào)表示)或變長參數(shù)(點(diǎn)號(hào)表示,如果使用,只能是最后一個(gè)參數(shù))。
program .command('clone <source> [destination]') .description('clone a repository into a newly created directory') .action((source, destination) => { console.log('clone command called'); });
在Command
對(duì)象上使用.argument()
來按次序指定命令參數(shù)。該方法接受參數(shù)名稱和參數(shù)描述。參數(shù)可為必選的(尖括號(hào)表示,例如<required>
)或可選的(方括號(hào)表示,例如[optional]
)。
program .command('login') .description('login') .argument('<username>', 'user') .argument('[password]', 'password', 'no password') .action((username, password) => { console.log(`login, ${username} - ${password}`); });
在參數(shù)名后加上...
來聲明可變參數(shù),且只有最后一個(gè)參數(shù)支持這種用法??勺儏?shù)會(huì)以數(shù)組的形式傳遞給處理函數(shù)。
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}`); });
源碼解析:
新建一個(gè)command
command(nameAndArgs, actionOptsOrExecDesc, execOpts) { let desc = actionOptsOrExecDesc; let opts = execOpts; if (typeof desc === 'object' && desc !== null) { opts = desc; desc = null; } opts = opts || {}; // 解析獲取輸入的命令及參數(shù) const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/); // 創(chuàng)建一個(gè)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; // 添加參數(shù) if (args) cmd.arguments(args); this.commands.push(cmd); cmd.parent = this; // 繼承屬性 cmd.copyInheritedSettings(this); if (desc) return this; return cmd; }
action
用于注冊命令回調(diào)。
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; }
接下來就是如何解析執(zhí)行命令部分了。還記得我們之前解析option
時(shí)的流程嘛
parse()
-> _parseCommand()
, 在parseOptions
方法中,我們還剩余了處理命令部分代碼,現(xiàn)在來看一下。
源碼解析:
parseOptions(argv) { const operands = []; const unknown = []; let dest = operands; const args = argv.slice(); while (args.length) { // 解析option部分 // ... // 當(dāng)開啟了enablePositionalOptions后, 會(huì)將命令后邊的參數(shù)全部定義為未知參數(shù),而不是命令選項(xiàng),同時(shí)如果想要在子命令中啟動(dòng)enablePositionalOptions,需要在父命令中先開啟。 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 }; }
沒有開啟enablePositionalOptions
配置,命令及后邊的參數(shù)都會(huì)保存在operands
數(shù)組中,當(dāng)開啟后,命令后邊的參數(shù)都會(huì)變成unknown
。
在解析了用戶輸入的參數(shù)后繼續(xù)往下執(zhí)行_parseCommand
方法
_parseCommand(operands, unknown) { // 解析配置 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); // 匹配找到命令, 使用子進(jìn)程運(yùn)行命令 if (operands && this._findCommand(operands[0])) { return this._dispatchSubcommand(operands[0], operands.slice(1), unknown); } // 沒找到命令就判斷一下是否有幫助命令, 而且用戶輸入的第一個(gè)參數(shù)是help if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { // 有且只有個(gè)命令 if (operands.length === 1) { this.help(); } // 執(zhí)行第二條命令 return this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]); } // 幫助命令也沒有找到或者用戶輸入的第一個(gè)參數(shù)未知但是存在一個(gè)默認(rèn)名字 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處理器 if (this._actionHandler) { checkForUnknownOptions(); // 處理參數(shù) this._processArguments(); let actionResult; actionResult = this._chainOrCallHooks(actionResult, 'preAction'); actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs) ); // 觸發(fā)父命令 if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy actionResult = this._chainOrCallHooks(actionResult, 'postAction'); return actionResult; } // 觸發(fā)父監(jiān)聽器 if (this.parent && this.parent.listenerCount(commandEvent)) { checkForUnknownOptions(); this._processArguments(); this.parent.emit(commandEvent, operands, unknown); // 處理參數(shù) } 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(); } // 不存在命令 } else if (this.commands.length) { checkForUnknownOptions(); this.help({ error: true }); } else { checkForUnknownOptions(); this._processArguments(); } }
到此,整個(gè)解析過程就完成了。
聲明統(tǒng)一參數(shù)
命令處理函數(shù)的參數(shù),為該命令聲明的所有參數(shù),除此之外還會(huì)附加兩個(gè)額外參數(shù):一個(gè)是解析出的選項(xiàng),另一個(gè)則是該命令對(duì)象自身。
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()); });
幫助信息
幫助信息是 Commander
基于你的程序自動(dòng)生成的,默認(rèn)的幫助選項(xiàng)是-h,--help
。
node index.js -h Usage: index [options] Options: -h, --help display help for command
源碼解析:
commander.js
提供了一個(gè)help
類,但是它的方法都是靜態(tài)的,允許被覆蓋。
在command
類中有一個(gè)createHelp
方法,通過Object.assign()
方法覆蓋help
的原始方法。
createHelp() { return Object.assign(new Help(), this.configureHelp()); }
formatHelp
生成幫助文字,解析處理設(shè)置的command
、option
、argument
等
formatHelp(cmd, helper) { const termWidth = helper.padWidth(cmd, helper); const helpWidth = helper.helpWidth || 80; const itemIndentWidth = 2; const itemSeparatorWidth = 2; // 格式化 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)}`, '']; // 描述符 const commandDescription = helper.commandDescription(cmd); if (commandDescription.length > 0) { output = output.concat([commandDescription, '']); } // 參數(shù) 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), '']); } // 選項(xiàng) 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), '']); } // 命令 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'); }
自定義
使用addHelpText
方法添加額外的幫助信息。
program.addHelpText('after', `call help`);
node index.js -h Usage: index [options] Options: -h, --help display help for command call help
addHelpText
方法的第一個(gè)參數(shù)是添加的幫助信息展示的位置,
包括如下:
beforeAll
:作為全局標(biāo)頭欄展示before
:在內(nèi)建幫助信息之前展示after
:在內(nèi)建幫助信息之后展示afterAll
:作為全局末尾欄展示
源碼解析:
在執(zhí)行addHelpText
方法后,會(huì)對(duì)該事件進(jìn)行監(jiān)聽,同時(shí)執(zhí)行輸入幫助命令時(shí)會(huì)按順序執(zhí)行所有事件,從而實(shí)現(xiàn)自定義信息的展示。
addHelpText
addHelpText(position, text) { const allowedValues = ['beforeAll', 'before', 'after', 'afterAll']; // 過濾 if (!allowedValues.includes(position)) { throw new Error(`Unexpected value for position to addHelpText. Expecting one of '${allowedValues.join("', '")}'`); } const helpEvent = `${position}Help`; // 監(jiān)聽事件 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); // 遍歷觸發(fā)所有祖先命令的 beforeAllHelp 事件 getCommandAndParents(this) .reverse() // 觸發(fā) beforeAllHelp .forEach((command) => command.emit('beforeAllHelp', context)); // 觸發(fā) 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); // 觸發(fā) help 指令 this.emit(this._helpLongFlag); // deprecated // 觸發(fā) afterHelp this.emit('afterHelp', context); // 遍歷觸發(fā)所有祖先命令的 afterAllHelp 事件 getCommandAndParents(this).forEach((command) => // 觸發(fā) afterAllHelp command.emit('afterAllHelp', context) ); }
showHelpAfterError
展示幫助信息
program.showHelpAfterError(); // 或者 program.showHelpAfterError('(add --help for additional information)');
node index.js -asd error: unknown option '-asd' (add --help for additional information)
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js巧妙實(shí)現(xiàn)Web應(yīng)用代碼熱更新
本文給大家講解的是Node.js的代碼熱更新的問題,其主要實(shí)現(xiàn)原理 是怎么對(duì) module 對(duì)象做處理,也就是手工監(jiān)聽文件修改, 然后清楚模塊緩存, 重新掛載模塊,思路清晰考慮細(xì)致, 雖然有點(diǎn)冗余代碼,但還是推薦給大家2015-10-10防止Node.js中錯(cuò)誤導(dǎo)致進(jìn)程阻塞的辦法
Node.js我們用到非常的多了,如果我們開發(fā)不當(dāng)可能因錯(cuò)誤導(dǎo)致進(jìn)程阻塞問題,對(duì)于進(jìn)程阻塞問題一直是個(gè)頭痛的事情,今天我們一起來看一篇關(guān)于Node.js防止錯(cuò)誤導(dǎo)致的進(jìn)程阻塞示例,下面一起來看看。2016-08-08nodeJS服務(wù)器的創(chuàng)建和重新啟動(dòng)的實(shí)現(xiàn)方法
今天小編就為大家分享一篇nodeJS服務(wù)器的創(chuàng)建和重新啟動(dòng)的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-05-05使用nodejs?spider爬取圖片及數(shù)據(jù)實(shí)現(xiàn)
這篇文章主要為大家介紹了使用nodejs?spider爬取圖片及數(shù)據(jù)實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07nodejs之koa2請(qǐng)求示例(GET,POST)
本篇文章主要介紹了nodejs之koa2請(qǐng)求示例(GET,POST),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08