玩轉 Commander-js —— 你也是命令行大師
導語 | 最近做團隊底層構建工具架構升級和命令行交互打了不少交道,再加上在研究 Vue-CLI 的源碼,覺得 Commander.js 作爲 Node.js 下這麼優秀的命令行交互工具,值得總結一下,文章主體內容搬運了 Commander.js 的官方文檔,對一些晦澀的翻譯部分進行了註解和必要的代碼註釋,適合躺在收藏夾,需要時拿出來查一查。
1. 安裝
npm install commander
2. 聲明 program 變量
直接引入對象(本文使用的方式):
const { program } = require('commander');
program.version('0.0.1');
創建實例方法:
const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');
TypeScript 用法:
// index.ts
import { Command } from 'commander';
const program = new Command();
3. 選項
Commander 使用.option()
方法來定義選項,同時可以附加選項的簡介。每個選項可以定義一個短選項名稱(- 後面接單個字符)和一個長選項名稱(-- 後面接一個或多個單詞),使用逗號、空格或|
分隔。
program
.option('-j | --join','Join IMWeb now!')
解析後的選項可以通過Command
對象上的.opts()
方法獲取,同時會被傳遞給命令處理函數。可以使用.getOptionValue()
和.setOptionValue()
操作單個選項的值。
program
.option('-j | --join','Join IMWeb now!');
program.parse()
console.log(program.opts()) // {join: true}
console.log(program.getOptionValue('join')) // true
$ imweb-options -j
{ join:true }
{ true }
以上兩個方法是我們最常用的選項聲明和取值方法,下面也羅列了一些小 Tips:
-
對於多個單詞的長選項,選項名會轉爲駝峯命名法(camel-case),例如
**--template-engine**
選項可通過**program.opts().templateEngine**
獲取 -
多個短選項可以合併簡寫,其中最後一個選項可以附加參數。例如,
**-a -b -p 80**
也可以寫爲**-ab -p80**
,甚至進一步簡化爲**-abp80**
。 -
**--**
可以標記選項的結束,後續的參數均不會被命令解釋,可以正常使用。 -
默認情況下,選項在命令行中的順序不固定,一個選項可以在其他參數之前或之後指定。
3.1. 常用選項類型,boolean 型選項和帶參數選項
有兩種最常用的選項,一類是 boolean 型選項,選項無需配置參數;另一類選項則可以設置參數(使用尖括號聲明在該選項後,如--expect <value>
)。如果在命令行中不指定具體的選項及參數,則會被定義爲undefined
。
program
.option('-d, --debug', 'output extra debugging')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza');
program.parse(process.argv);
const options = program.opts();
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);
$ pizza-options -p
error: option '-p, --pizza-type <type>' argument missing
$ pizza-options -d -s -p vegetarian
{ debug: true, small: true, pizzaType: 'vegetarian' }
pizza details:
- small pizza size
- vegetarian
$ pizza-options --pizza-type=cheese
pizza details:
- cheese
通過 program.parse(arguments)
方法處理參數,沒有被使用的選項會存放在 program.args
數組中。該方法的參數是可選的,默認值爲 process.argv
,注意在一個大項目中,如果涉及到一些指令的註冊,請注意在所有指令註冊結束後再執行 program.parse()
,因爲這一操作會終止參數處理,導致指令不能正確註冊,Commander.js 提供了單獨處理選項的函數 program.parseOptions(arguments)
參數同 program.parse(arguments)
。
3.2. 選項的默認值
選項可以設置一個默認值。
program
.option('-c, --cheese <type>', 'add the specified type of cheese', 'blue');
program.parse();
console.log(`cheese: ${program.opts().cheese}`);
$ pizza-options
cheese: blue
$ pizza-options --cheese stilton
cheese: stilton
3.3. 其他的選項類型,取反選項,以及可選參數的選項
可以定義一個以 no-
開頭的 boolean 型長選項。在命令行中使用該選項時,會將對應選項的值置爲 false
。當只定義了帶 no-
的選項,未定義對應不帶 no-
的選項時,該選項的默認值會被置爲 true
。
如果已經定義了 --foo
,那麼再定義 --no-foo
並不會改變它本來的默認值。可以爲一個 boolean 類型的選項指定一個默認的布爾值,在命令行裏可以重寫它的值。
program
.option('--no-sauce', 'Remove sauce')
.option('--cheese <flavour>', 'cheese flavour', 'mozzarella')
.option('--no-cheese', 'plain with no cheese')
.parse();
const options = program.opts();
const sauceStr = options.sauce ? 'sauce' : 'no sauce';
const cheeseStr = (options.cheese === false) ? 'no cheese' : `${options.cheese} cheese`;
console.log(`You ordered a pizza with ${sauceStr} and ${cheeseStr}`);
$ pizza-options
You ordered a pizza with sauce and mozzarella cheese
$ pizza-options --sauce
error: unknown option '--sauce'
$ pizza-options --cheese=blue
You ordered a pizza with sauce and blue cheese
$ pizza-options --no-sauce --no-cheese
You ordered a pizza with no sauce and no cheese
$ pizza-options --cheese=blue --no-cheese
You ordered a pizza with sauce and no cheese
注意取反選項在命令行同時出現,後使用的會覆蓋之前的選項,如上代碼塊的最後一行。
選項的參數使用方括號聲明表示參數是可選參數(如--optional [value]
)。該選項在不帶參數時可用作 boolean 選項,在帶有參數時則從參數中得到值。
program
.option('-c, --cheese [type]', 'Add cheese with optional type');
program.parse(process.argv);
const options = program.opts();
if (options.cheese === undefined) console.log('no cheese');
else if (options.cheese === true) console.log('add cheese');
else console.log(`add cheese type ${options.cheese}`);
$ pizza-options
no cheese
$ pizza-options --cheese
add cheese
$ pizza-options --cheese mozzarella
add cheese type mozzarella
3.4. 必填選項
通過.requiredOption()
方法可以設置選項爲必填。必填選項要麼設有默認值,要麼必須在命令行中輸入,對應的屬性字段在解析時必定會有賦值。該方法其餘參數與.option()
一致。
program
.requiredOption('-c, --cheese <type>', 'pizza must have cheese');
program.parse();
$ pizza
error: required option '-c, --cheese <type>' not specified
3.5. 變長參數選項
定義選項時,可以通過使用...
來設置參數爲可變長參數。在命令行中,用戶可以輸入多個參數,解析後會以數組形式存儲在對應屬性字段中。在輸入下一個選項前(-
或--
開頭),用戶輸入的指令均會被視作變長參數。與普通參數一樣的是,可以通過--
標記當前命令的結束。
program
.option('-n, --number <numbers...>', 'specify numbers')
.option('-l, --letter [letters...]', 'specify letters');
program.parse();
console.log('Options: ', program.opts());
console.log('Remaining arguments: ', program.args);
$ collect -n 1 2 3 --letter a b c
Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
Remaining arguments: []
$ collect --letter=A -n80 operand
Options: { number: [ '80' ], letter: [ 'A' ] }
Remaining arguments: [ 'operand' ]
$ collect --letter -n 1 -n 2 3 -- operand
Options: { number: [ '1', '2', '3' ], letter: true }
Remaining arguments: [ 'operand' ]
3.6. 版本選項
.version()
方法可以設置版本,其默認選項爲-V
和--version
,設置了版本後,命令行會輸出當前的版本號。
program.version('0.0.1');
$ ./examples/pizza -V
0.0.1
版本選項也支持自定義設置選項名稱,可以在.version()
方法裏再傳遞一些參數(長選項名稱、描述信息),用法與.option()
方法類似。
program.version('0.0.1', '-v, --vers', 'output the current version');
3.7. 其他選項配置
大多數情況下,選項均可通過.option()
方法添加。但對某些不常見的用例,也可以直接構造Option
對象,對選項進行更詳盡的配置。
program
.addOption(new Option('-s, --secret').hideHelp())
.addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
.addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']));
$ extra --help
Usage: help [options]
Options:
-t, --timeout <delay> timeout in seconds (default: one minute)
-d, --drink <size> drink cup size (choices: "small", "medium", "large")
-h, --help display help for command
$ extra --drink huge
error: option '-d, --drink <size>' argument 'huge' is invalid. Allowed choices are small, medium, large.
3.8. 自定義選項處理
選項的參數可以通過自定義函數來處理,該函數接收兩個參數,即用戶新輸入的參數值和當前已有的參數值(即上一次調用自定義處理函數後的返回值),返回新的選項參數值。
自定義函數適用場景包括參數類型轉換,參數暫存,或者其他自定義處理的場景。
可以在自定義函數的後面設置選項參數的默認值或初始值(例如參數用列表暫存時需要設置一個初始空列表)。
function myParseInt(value, dummyPrevious) {
// parseInt 參數爲字符串和進制數
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new commander.InvalidArgumentError('Not a number.');
}
return parsedValue;
}
function increaseVerbosity(dummyValue, previous) {
return previous + 1;
}
function collect(value, previous) {
return previous.concat([value]);
}
function commaSeparatedList(value, dummyPrevious) {
return value.split(',');
}
program
.option('-f, --float <number>', 'float argument', parseFloat)
.option('-i, --integer <number>', 'integer argument', myParseInt)
.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0)
.option('-c, --collect <value>', 'repeatable value', collect, [])
.option('-l, --list <items>', 'comma separated list', commaSeparatedList)
;
program.parse();
const options = program.opts();
if (options.float !== undefined) console.log(`float: ${options.float}`);
if (options.integer !== undefined) console.log(`integer: ${options.integer}`);
if (options.verbose > 0) console.log(`verbosity: ${options.verbose}`);
if (options.collect.length > 0) console.log(options.collect);
if (options.list !== undefined) console.log(options.list);
$ custom -f 1e2
float: 100
$ custom --integer 2
integer: 2
$ custom -v -v -v
verbose: 3
$ custom -c a -c b -c c
[ 'a', 'b', 'c' ]
$ custom --list x,y,z
[ 'x', 'y', 'z' ]
4. 命令
通過.command()
或.addCommand()
可以配置命令,有兩種實現方式:爲命令綁定處理函數,或者將命令單獨寫成一個可執行文件(詳述見後文)。子命令支持嵌套。
.command()
的第一個參數爲命令名稱。命令參數可以跟在名稱後面,也可以用.argument()
單獨指定。參數可爲必選的(尖括號表示)、可選的(方括號表示)或變長參數(點號表示,如果使用,只能是最後一個參數)。
使用.addCommand()
向program
增加配置好的子命令。
例如:
// 通過綁定處理函數實現命令(這裏的指令描述爲放在`.command`中)
// 返回新生成的命令(即該子命令)以供繼續配置
program
.command('clone <source> [destination]')
.description('clone a repository into a newly created directory')
.action((source, destination) => {
console.log('clone command called');
});
// 通過獨立的的可執行文件實現命令 (注意這裏指令描述是作爲`.command`的第二個參數)
// 返回最頂層的命令以供繼續添加子命令
program
.command('start <service>', 'start named service')
.command('stop [service]', 'stop named service, or all if no name supplied');
// 分別裝配命令
// 返回最頂層的命令以供繼續添加子命令
program
.addCommand(build.makeBuildCommand());
使用.command()
和addCommand()
來指定選項的相關設置。當設置hidden: true
時,該命令不會打印在幫助信息裏。當設置isDefault: true
時,若沒有指定其他子命令,則會默認執行這個命令,。
4.1. 命令參數
如上所述,字命令的參數可以通過.command()
指定。對於有獨立可執行文件的子命令來說,參數只能以這種方法指定。而對其他子命令,參數也可用以下方法。
在Command
對象上使用.argument()
來按次序指定命令參數。該方法接受參數名稱和參數描述。參數可爲必選的(尖括號表示,例如<required>
)或可選的(方括號表示,例如[optional]
)。
program
.version('0.1.0')
.argument('<username>', 'user to login')
.argument('[password]', 'password for user, if required', 'no password given')
.action((username, password) => {
console.log('username:', username);
console.log('password:', password);
});
在參數名後加上...
來聲明可變參數,且只有最後一個參數支持這種用法。可變參數會以數組的形式傳遞給處理函數。例如:
program
.version('0.1.0')
.command('rmdir')
.argument('<dirs...>')
.action(function (dirs) {
dirs.forEach((dir) => {
console.log('rmdir %s', dir);
});
});
有一種便捷方式可以一次性指定多個參數,但不包含參數描述:
program
.arguments('<username> <password>');
4.1.1. 其他參數配置
有少數附加功能可以直接構造Argument
對象,對參數進行更詳盡的配置。
program
.addArgument(new commander.Argument('<drink-size>', 'drink cup size').choices(['small', 'medium', 'large']))
.addArgument(new commander.Argument('[timeout]', 'timeout in seconds').default(60, 'one minute'))
4.1.2. 自定義參數處理
選項的參數可以通過自定義函數來處理(與處理選項參數時類似),該函數接收兩個參數:用戶新輸入的參數值和當前已有的參數值(即上一次調用自定義處理函數後的返回值),返回新的命令參數值。
處理後的參數值會傳遞給命令處理函數,同時可通過.processedArgs
獲取。
可以在自定義函數的後面設置命令參數的默認值或初始值。
program
.command('add')
.argument('<first>', 'integer argument', myParseInt)
.argument('[second]', 'integer argument', myParseInt, 1000)
.action((first, second) => {
console.log(`${first} + ${second} = ${first + second}`);
})
;
4.2. 處理函數
命令處理函數的參數,爲該命令聲明的所有參數,除此之外還會附加兩個額外參數:一個是解析出的選項,另一個則是該命令對象自身。
program
.argument('<name>')
.option('-t, --title <honorific>', 'title to use before name')
.option('-d, --debug', 'display some debugging')
.action((name, options, command) => {
if (options.debug) {
console.error('Called %s with options %o', command.name(), options);
}
const title = options.title ? `${options.title} ` : '';
console.log(`Thank-you ${title}${name}`);
});
處理函數支持async
,相應的,需要使用.parseAsync
代替.parse
。
async function run() { /* 在這裏編寫代碼 */ }
async function main() {
program
.command('run')
.action(run);
await program.parseAsync(process.argv);
}
使用命令時,所給的選項和命令參數會被驗證是否有效。凡是有未知的選項,或缺少所需的命令參數,都會報錯。如要允許使用未知的選項,可以調用.allowUnknownOption()
。默認情況下,傳入過多的參數並不報錯,但也可以通過調用.allowExcessArguments(false)
來啓用過多參數的報錯。
4.3. 獨立的可執行(子)命令
當.command()
帶有描述參數時,就意味着使用獨立的可執行文件作爲子命令。Commander 將會嘗試在入口腳本(例如./examples/pm
)的目錄中搜索program-command
形式的可執行文件,例如pm-install
、pm-search
。通過配置選項executableFile
可以自定義名字。
你可以在可執行文件裏處理(子)命令的選項,而不必在頂層聲明它們。
program
.version('0.1.0')
.command('install [name]', 'install one or more packages')
.command('search [query]', 'search with optional query')
.command('update', 'update installed packages', { executableFile: 'myUpdateSubCommand' })
.command('list', 'list packages installed', { isDefault: true });
program.parse(process.argv);
如果該命令需要支持全局安裝,請確保有對應的權限,例如755
。
4.4. 生命週期鉤子
可以在命令的生命週期事件上設置回調函數。
program
.option('-t, --trace', 'display trace statements for commands')
.hook('preAction', (thisCommand, actionCommand) => {
if (thisCommand.opts().trace) {
console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
console.log('arguments: %O', actionCommand.args);
console.log('options: %o', actionCommand.opts());
}
});
鉤子函數支持async
,相應的,需要使用.parseAsync
代替.parse
。一個事件上可以添加多個鉤子。
支持的事件有:
-
**preAction**
:在本命令或其子命令的處理函數執行前 -
**postAction**
:在本命令或其子命令的處理函數執行後
鉤子函數的參數爲添加上鉤子的命令,及實際執行的命令。
5. 自動化幫助信息
幫助信息是 Commander 基於你的程序自動生成的,默認的幫助選項是-h,--help
。
$ node ./examples/pizza --help
Usage: pizza [options]
An application for pizza ordering
Options:
-p, --peppers Add peppers
-c, --cheese <type> Add the specified type of cheese (default: "marble")
-C, --no-cheese You do not want any cheese
-h, --help display help for command
如果你的命令中包含了子命令,會默認添加help
命令,它可以單獨使用,也可以與子命令一起使用來提示更多幫助信息。用法與shell
程序類似,注意這一指令不能被註銷。
shell help
shell --help
shell help spawn
shell spawn --help
5.1. 自定義幫助
可以添加額外的幫助信息,與內建的幫助一同展示。
program
.option('-f, --foo', 'enable some foo');
program.addHelpText('after', `
Example call:
$ custom-help --help`);
將會輸出以下的幫助信息:
Usage: custom-help [options]
Options:
-f, --foo enable some foo
-h, --help display help for command
Example call:
$ custom-help --help
位置參數對應的展示方式如下:
-
**beforeAll**
:作爲全局標頭欄展示 -
**before**
:在內建幫助信息之前展示 -
**after**
:在內建幫助信息之後展示 -
**afterAll**
:作爲全局末尾欄展示
beforeAll
和afterAll
兩個參數作用於命令及其所有的子命令,注意 Commander.js 不支持重載所有的幫助信息,只能實現和內建的幫助一同展示,如下圖是 Commander.js 配合 CommandLineUsage 的效果,可以看到 Commander.js 內置的幫助信息在 CommandLineUsgae 之後輸出:
第二個參數可以是一個字符串,也可以是一個返回字符串的函數。對後者而言,爲便於使用,該函數可以接受一個上下文對象,它有如下屬性:
-
**error**
:boolean 值,代表該幫助信息是否由於不當使用而展示 -
**command**
:代表展示該幫助信息的**Command**
對象
5.2. 在出錯後展示幫助信息
默認情況下,出現命令用法錯誤時只會顯示錯誤信息。可以選擇在出錯後展示完整的幫助或自定義的幫助信息。
program.showHelpAfterError();
// 或者
program.showHelpAfterError('(add --help for additional information)');
$ pizza --unknown
error: unknown option '--unknown'
(add --help for additional information)
5.3. 使用代碼展示幫助信息
.help()
:展示幫助信息並退出。可以通過傳入{ error: true }
來讓幫助信息從 stderr 輸出,並以代表錯誤的狀態碼退出程序。
.outputHelp()
:只展示幫助信息,不退出程序。傳入{ error: true }
可以讓幫助信息從 stderr 輸出。
.helpInformation()
:得到字符串形式的內建的幫助信息,以便用於自定義的處理及展示。
5.4. .usage 和 .name
通過這兩個選項可以修改幫助信息的首行提示,name
屬性也可以從參數中推導出來。例如:
program
.name("my-command")
.usage("[global options] command")
幫助信息開頭如下:
Usage: my-command [global options] command
5.5. .helpOption(flags, description)
每一個命令都帶有一個默認的幫助選項。可以重寫flags
和description
參數。傳入false
則會禁用內建的幫助信息。
program
.helpOption('-e, --HELP', 'read more information');
5.6. .addHelpCommand()
如果一個命令擁有子命令,它也將有一個默認的幫助子命令。使用.addHelpCommand()
和.addHelpCommand(false)
可以打開或關閉默認的幫助子命令。
也可以自定義名字和描述:
program.addHelpCommand('assist [command]', 'show assistance');
5.7. 其他幫助配置
內建幫助信息通過Help
類進行格式化。如有需要,可以使用.configureHelp()
來更改其數據屬性和方法,或使用.createHelp()
來創建子類,從而配置Help
類的行爲。
數據屬性包括:
-
**helpWidth**
:指明幫助信息的寬度。可在單元測試中使用。 -
**sortSubcommands**
:以字母序排列子命令 -
**sortOptions**
:以字母序排列選項
可以得到可視化的參數列表,選項列表,以及子命令列表。列表的每個元素都具有_term_
和_description_
屬性,並可以對其進行格式化。關於其使用方式,請參考.formatHelp()
。
program.configureHelp({
sortSubcommands: true,
subcommandTerm: (cmd) => cmd.name() // 顯示名稱,而非用法
});
6. 自定義事件監聽
監聽命令和選項可以執行自定義函數。
program.on('option:verbose', function () {
process.env.VERBOSE = this.opts().verbose;
});
program.on('command:*', function (operands) {
console.error(`error: unknown command '${operands[0]}'`);
const availableCommands = program.commands.map(cmd => cmd.name());
mySuggestBestMatch(operands[0], availableCommands);
process.exitCode = 1;
});
7. 零碎知識
7.1. .parse() 和 .parseAsync()
.parse
的第一個參數是要解析的字符串數組,也可以省略參數而使用process.argv
。
如果參數遵循與 node 不同的約定,可以在第二個參數中傳遞from
選項:
-
**node**
:默認值,**argv[0]**
是應用,**argv[1]**
是要跑的腳本,後續爲用戶參數; -
**electron**
:**argv[1]**
根據 electron 應用是否打包而變化; -
**user**
:來自用戶的所有參數。
例如:
program.parse(process.argv); // 指明,按 node 約定
program.parse(); // 默認,自動識別 electron
program.parse(['-f', 'filename'], { from: 'user' });
7.2. 解析配置
當默認的解析方式無法滿足需要,Commander 也提供了其他的解析行爲。
默認情況下,程序的選項在子命令前後均可被識別。如要只允許選項出現在子命令之前,可以使用.enablePositionalOptions()
。這樣可以在命令和子命令中使用意義不同的同名選項。
當啓用了帶順序的選項解析,以下程序中,-b
選項在第一行中將被解析爲程序頂層的選項,而在第二行中則被解析爲子命令的選項:
program -b subcommand
program subcommand -b
默認情況下,選項在命令參數前後均可被識別。如要使選項僅在命令參數前被識別,可以使用.passThroughOptions()
。這樣可以把參數和跟隨的選項傳遞給另一程序,而無需使用--
來終止選項解析。如要在子命令中使用此功能,必須首先啓用帶順序的選項解析。
當啓用此功能時,以下程序中,--port=80
在第一行中會被解析爲程序的選項,而在第二行中則會被解析爲一個命令參數:
program --port=80 arg
program arg --port=80
默認情況下,使用未知選項會提示錯誤。如要將未知選項視作普通命令參數,並繼續處理其他部分,可以使用.allowUnknownOption()
。這樣可以混用已知和未知的選項。
默認情況下,傳入過多的命令參數並不會報錯。可以使用.allowExcessArguments(false)
來啓用這一檢查。
7.3. 指令重複註冊的問題
在設計腳手架或者底層構建工具的時候,設計者往往會考慮到,如果開放插件系統給各個插件註冊指令,難免會出現指令重複的問題,這裏很遺憾 Commander.js 沒有對這類問題做特殊處理,也沒有提供相關 API 或者 HOOK 支持。重複註冊時,Commander.js 會先後註冊兩個指令,在執行相關指令的時候也會依次執行,輸入 -h
的時候也會出現兩個指令,即使他們的命令規範是不同的,比如一個是 create <name>
,另一個只有 create
,這裏需要開發者人爲封裝一層 registerCommand
,按照自己的設定的規則註冊相應的指令。
// 封裝指令註冊
public registerCommand(command: string): Commander {
// command是一個如 create <dir> [type] 的字符串,從中獲取到命令 create 賦值給subCommand
const subCommand = command.split(" ")[0];
this.cli.on(`command:${subCommand}`, msg => {
logger.debug(`執行指令${subCommand}`, msg);
// TODO 上報
});
logger.debug(`正在註冊指令${command}`);
if (this.commandMap.has(subCommand)) {
// 重複指令因爲插件中的 commander 鏈式調用,直接返回其它類型會報錯,所以再實例化一個 commander 作爲子程序分離出去
logger.warn(
`${
this.activePlugin
} 正在註冊的指令 ${subCommand} 已經被 ${this.commandMap.get(
subCommand
)} 註冊過,跳過`
);
const tempProgram = new Commander();
return tempProgram.command(command);
}
this.commandMap.set(subCommand, this.activePlugin);
return this.cli.command(command);
}
如上圖是 IMFLOW 中的部分脫敏代碼,這裏要注意,因爲編寫插件的開發者在調用 registerCommand
方法的時候並不知道這一指令有沒有註冊過,所以大概率會按照 Commander.js 一往的鏈式調用方法書寫下去,爲了防止插件報錯,當我們確定要棄用該指令的註冊的時候,可以再實例化一個 Commander,將廢棄的命令註冊到該 Commander 身上而非主程序中的 Commander,這樣既能夠讓該指令註冊 “流產”,也不會讓插件在註冊指令時報錯。
7.4. 作爲屬性的遺留選項
在 Commander 7 以前,選項的值是作爲屬性存儲在命令對象上的。這種處理方式便於實現,但缺點在於,選項可能會與Command
的已有屬性相沖突。通過使用.storeOptionsAsProperties()
,可以恢復到這種舊的處理方式,並可以不加改動地繼續運行遺留代碼。
program
.storeOptionsAsProperties()
.option('-d, --debug')
.action((commandAndOptions) => {
if (commandAndOptions.debug) {
console.error(`Called ${commandAndOptions.name()}`);
}
});
7.5. TypeScript
如果你使用 ts-node,並有.ts
文件作爲獨立可執行文件,那麼需要用 node 運行你的程序以使子命令能正確調用,例如:
node -r ts-node/register pm.ts
7.6. createCommand()
使用這個工廠方法可以創建新命令,此時不需要使用new
,如
const { createCommand } = require('commander');
const program = createCommand();
createCommand
同時也是Command
對象的一個方法,可以創建一個新的命令(而非子命令),使用.command()
創建子命令時內部會調用該方法。
7.7. Node 選項,如 --harmony
要使用--harmony
等選項有以下兩種方式:
-
在子命令腳本中加上
**#!/usr/bin/env node --harmony**
。注:Windows 系統不支持; -
調用時加上
**--harmony**
參數,例如**node --harmony examples/pm publish**
。**--harmony**
選項在開啓子進程時仍會保留。
7.8. 調試子命令
一個可執行的子命令會作爲單獨的子進程執行。
如果使用 node inspector 的node -inspect
等命令來調試可執行命令,對於生成的子命令,inspector 端口會遞增 1。
如果想使用 VSCode 調試,則需要在launch.json
配置文件裏設置"autoAttachChildProcesses": true
。
7.9. 重寫退出和輸出
默認情況下,在檢測到錯誤、打印幫助信息或版本信息時 Commander 會調用process.exit
方法。其默認實現會拋出一個CommanderError
,可以重寫該方法並提供一個回調函數(可選)。
回調函數的參數爲CommanderError
,屬性包括 Number 型的exitCode
、String 型的code
和message
。子命令完成調用後會開始異步處理。正常情況下,打印錯誤信息、幫助信息或版本信息不會被重寫影響,因爲重寫會發生在打印之後。
program.exitOverride();
try {
program.parse(process.argv);
} catch (err) {
// 自定義處理...
}
Commander 默認用作命令行應用,其輸出寫入 stdout 和 stderr。對於其他應用類型,這一行爲可以修改。並且可以修改錯誤信息的展示方式。
function errorColor(str) {
// 添加 ANSI 轉義字符,以將文本輸出爲紅色
return `\x1b[31m${str}\x1b[0m`;
}
program
.configureOutput({
// 此處使輸出變得容易區分
writeOut: (str) => process.stdout.write(`[OUT] ${str}`),
writeErr: (str) => process.stdout.write(`[ERR] ${str}`),
// 將錯誤高亮顯示
outputError: (str, write) => write(errorColor(str))
});
8. 例子
在只包含一個命令的程序中,無需定義處理函數。
const { program } = require('commander');
program
.description('Wanna Join TENCENT IMWEB?')
.option('-s, --social', 'I\'m a candidate with work experience')
.option('-g, --graduate', 'I\'m a graduate')
.options('-p, --phone','Your phone number')
program.parse();
const options = program.opts();
console.log('Welcome to IMWEB');
if (options.social) console.log('-- has work experience');
const phone = !options.phone ? 'NA' : options.phone;
console.log(' - %s Phone Number', phone);
在包含多個命令的程序中,應爲每個命令指定處理函數,或獨立的可執行程序,下文的鏈式調用也是 IMFLOW 中使用的,開發者可以封裝頂層的 .command
方法,加入上報和命令 Set 等操作,返回一個 command 實例以繼續鏈式調用。
const { Command } = require('commander');
const program = new Command();
program
.version('0.0.1')
.option('-c, --config <path>', 'set config path', './deploy.conf');
program
.command('setup [env]')
.description('run setup commands for all envs')
.option('-s, --setup_mode <mode>', 'Which setup mode to use', 'normal')
.action((env, options) => {
env = env || 'all';
console.log('read config from %s', program.opts().config);
console.log('setup for %s env(s) with %s mode', env, options.setup_mode);
});
program
.command('exec <script>')
.alias('ex')
.description('execute the given remote cmd')
.option('-e, --exec_mode <mode>', 'Which exec mode to use', 'fast')
.action((script, options) => {
console.log('read config from %s', program.opts().config);
console.log('exec "%s" using %s mode and config %s', script, options.exec_mode, program.opts().config);
}).addHelpText('after', `
Examples:
$ deploy exec sequential
$ deploy exec async`
);
program.parse(process.argv);
9. 支持
當前版本的 Commander 在 LTS 版本的 Node 上完全支持。Node 版本應不低於 v12,使用更低版本 Node 的用戶建議安裝更低版本的 Commander。Commander 2.x 具有最廣泛的支持,但注意 2.x 版本的 API 和本文存在較多出入,請以官方文檔爲準。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Mw2f1En29WtRPLQ5CyXEHg