Node.js 中如何收集和解析命令行參數(shù)
前言
??在開發(fā) CLI(Command Line Interface)工具的業(yè)務(wù)場(chǎng)景下,離不開命令行參數(shù)的收集和解析。
??接下來,本文介紹如何收集和解析命令行參數(shù)。
收集命令行參數(shù)
??在 Node.js 中,可以通過 process.argv 屬性收集進(jìn)程被啟動(dòng)時(shí)傳入的命令行參數(shù):
??//?./example/demo.js
??process.argv.slice(2);
??//?命令行執(zhí)行如下命令
??node?./example/demo.js?--name=xiaoming?--age=20?man
??//?得到的結(jié)果
??[?'--name=xiaoming',?'--age=20',?'man'?]
??由上述示例可以發(fā)現(xiàn),Node.js 在處理命令行參數(shù)時(shí),只是簡(jiǎn)單地通過空格來分割字符串。
??對(duì)于這樣的參數(shù)數(shù)組,無(wú)法很方便地獲取到每個(gè)參數(shù)對(duì)應(yīng)的值,所以需要再進(jìn)行一次解析操作。
命令行參數(shù)風(fēng)格
??在解析命令行參數(shù)之前,需要了解一些常見的命令行參數(shù)風(fēng)格:
Unix 風(fēng)格:參數(shù)以「-」(連字符)開頭 GNU 風(fēng)格:參數(shù)以「--」(雙連字符)開頭 BSD 風(fēng)格:參數(shù)以空格分割
??Unix 參數(shù)風(fēng)格有一個(gè)特殊的注意事項(xiàng):「「-」后面緊鄰的每一個(gè)字母都表示一個(gè)參數(shù)名」。
ls -al
??上述命令用來顯示當(dāng)前目錄下所有的文件、文件夾并且顯示它們的詳細(xì)信息,等同于:
ls -a -l
??GNU 風(fēng)格的參數(shù)以 「--」開頭,一般后面會(huì)跟上一個(gè)單詞或者短語(yǔ),例如熟悉的 npm 安裝依賴的命令:
npm install --save koa
對(duì)于兩個(gè)單詞的情況,在 GNU 參數(shù)風(fēng)格中,會(huì)通過「-」來連接,例如 npm 安裝僅用于開發(fā)環(huán)境的依賴:
npm install --save-dev webpack
??BSD 是加州大學(xué)伯克利分校開發(fā)的一個(gè) Unix 版本。其與 Unix 的區(qū)別主要在于參數(shù)前面沒有 「-」,個(gè)人感覺這樣很難區(qū)別參數(shù)和參數(shù)值。
?注意事項(xiàng):-- 后面緊鄰空格時(shí),表示后面的字符串不需要解析。
?
解析命令行參數(shù)
function?parse(args?=?[])?{
??//?_?屬性用來保留不需要處理的參數(shù)字符串
??const?output?=?{?_:?[]?};
??for?(let?index?=?0;?index?????const?arg?=?args[index];
????
????if?(isIgnoreFollowingParameters(output,?args,?index,?arg))?{
??????break;
????}
????
????if?(!isParameter(arg))?{
??????output._.push(arg);
??????continue;
????}
????...
??}
??return?output;
}
parse(process.argv.slice(2));
??接收到命令行參數(shù)數(shù)組之后,需要遍歷數(shù)組,處理每一個(gè)參數(shù)字符串。
??isIgnoreFollowingParameters 方法主要用來判斷單個(gè)「--」的場(chǎng)景,后續(xù)的參數(shù)字符串不再需要處理:
function?isIgnoreFollowingParameters(output,?args,?index,?arg)?{
??if?(arg?!==?'--')?{
????return?false;
??}
??output._?=?output._.concat(args.slice(++index));
??return?true;
}
??接下來,如果參數(shù)字符串不以「-」開頭,同樣也不需要處理,參數(shù)的形式以 Unix 和 GNU 風(fēng)格為主:
function?isParameter(arg)?{
??return?arg.startsWith('-');
}
??參數(shù)的表現(xiàn)形式主要分為以下幾種:
"--name=xiaoming": 參數(shù)名為 name,參數(shù)值為 xiaoming "-abc=10": 參數(shù)名為 a,參數(shù)值為 true;參數(shù)名為 b,參數(shù)值為 true;參數(shù)名為 c,參數(shù)值為 10 "--save-dev": 參數(shù)名為 save-dev,參數(shù)值為 true "--age 20":參數(shù)名為 age,參數(shù)值為 20
??let?hyphensIndex;
??for?(hyphensIndex?=?0;?hyphensIndex?????if?(arg.charCodeAt(hyphensIndex)?!==?45)?{
??????break;
????}
??}
??let?assignmentIndex;
??for?(assignmentIndex?=?hyphensIndex?+?1;?assignmentIndex?????if?(arg[assignmentIndex].charCodeAt(0)?===?61)?{
??????break;
????}
??}
??利用 Unicode 碼點(diǎn)值找出連字符和等號(hào)的下標(biāo)值,從而根據(jù)下標(biāo)分割出參數(shù)名和參數(shù)值:
??const?name?=?arg.substring(hyphensIndex,?assignmentIndex);
??let?value;
??const?assignmentValue?=?arg.substring(++assignmentIndex);
??處理參數(shù)值時(shí),需要考慮參數(shù)賦值的四種場(chǎng)景:
??if?(assignmentValue)?{
????value?=?assignmentValue;?//?--name=xiaoming?or?-abc=10
??}?else?if?(index?+?1?===?args.length)?{
????value?=?true;?//?--save-dev
??}?else?if?((''?+?args[index?+?1]).charCodeAt(0)?!==?45)?{
????value?=?args[++index];?//?--age?20
??}?else?{
????value?=?true;?//?缺省情況
??}
??由于 Unix 風(fēng)格中每一個(gè)字母都代表一個(gè)參數(shù),并且「手動(dòng)傳遞的參數(shù)值應(yīng)該賦值給最后一個(gè)參數(shù)」,所以還需針對(duì)該場(chǎng)景進(jìn)行適配:
??//?「-」or「--」
??const?arr?=?hyphensIndex?===?2???[name]?:?name;
??for?(let?keyIndex?=?0;?keyIndex?????const?_key?=?arr[keyIndex];
????const?_value?=?keyIndex?+?1?????handleKeyValue(output,?_key,?_value);
??}
??最后針對(duì)參數(shù)的賦值操作,需要考慮到「多次賦值」的情況:
function?handleKeyValue(output,?key,?value)?{
??const?oldValue?=?output[key];
??if?(Array.isArray(oldValue))?{
????output[key]?=?oldValue.concat(value);
????return;
??}
??if?(oldValue)?{
????output[key]?=?[oldValue,?value];
????return;
??}
??output[key]?=?value;
}
??到此,命令行參數(shù)的解析功能就完成了,上述方法執(zhí)行的效果如下:
??#?命令行執(zhí)行
??node?./example/step1.js?--name=xiaoming?--age?20?--save-dev?-abc=10?-c=20??--?--ignore
??#?解析結(jié)果
??{
????_:?[?'--ignore'?],
????name:?'xiaoming',
????age:?'20',
????'save-dev':?true,
????a:?true,
????b:?true,
????c:?[?'10',?'20'?]
??}
別名機(jī)制
??比較優(yōu)秀的 CLI 工具在參數(shù)的解析上都支持參數(shù)的別名設(shè)置,例如使用 npm 安裝開發(fā)環(huán)境依賴時(shí),你可以選擇這種完整的寫法:
npm install --save-dev webpack
??你也可以使用下面這種別名方式:
npm install -D webpack
??從使用上來說 -D 和 --save-dev 是兩種方式,但是從 CLI 工具的開發(fā)者來說,最終處理邏輯時(shí)只能以一個(gè)參數(shù)名為標(biāo)準(zhǔn),所以對(duì)于一個(gè)命令行參數(shù)解析庫(kù)來說,其結(jié)果需要包含所有的情況:
??npm?install?--save-dev?webpack
??#?解析的結(jié)果
??{?'save-dev':?true,?'D':?true?}
??以上文的解析方法為例,需要添加額外的選項(xiàng)參數(shù),加入 alias 屬性來聲明別名屬性的對(duì)應(yīng)關(guān)系:
??parse(process.argv.slice(2),?{
????alias:?{
??????'save-dev':?'S'
????}
??})
??上述方式符合正常的理解:設(shè)置參數(shù)對(duì)應(yīng)的別名。但這是一個(gè)「單向查找關(guān)系」,需要轉(zhuǎn)化為:
??"alias":?{
????"save-dev":?["s"],
????"s":?["save-dev"]
??}
??因?yàn)閷?duì)于使用者來說,只會(huì)選擇一種方式傳遞參數(shù)。對(duì)于開發(fā)者的話需要根據(jù)任意一個(gè)別名找到其相關(guān)聯(lián)的別名:
function?parse(args?=?[],?options?=?{})?{
??const?output?=?{?_:?[]?};
??const?{?alias?}?=?options;
??const?hasAlias?=?alias?!==?void?666;
??if?(hasAlias)?{
????Object.keys(alias).forEach(key?=>?{
??????alias[key]?=?toArr(alias[key]);
??????alias[key].forEach((item,?index)?=>?{
????????(alias[item]?=?alias[key].concat(key)).splice(index,?1);
??????})
????})
??}
??//?省略解析代碼
??...
?if?(hasAlias)?{
????Object.keys(output).forEach(key?=>?{
??????const?arr?=?alias[key]?||?[];
??????arr.forEach(sub?=>?output[sub]?=?output[key])
????})
?}
??return?output;
}
??除了別名之外,還可以在參數(shù)解析之后做如下優(yōu)化:
參數(shù)值的類型約束 參數(shù)的默認(rèn)值設(shè)定
成熟的解析庫(kù)
??針對(duì)一些成熟的命令行參數(shù)解析庫(kù)可以采用基準(zhǔn)測(cè)試查看它們的解析效率:
const?nopt?=?require('nopt');
const?mri?=?require('mri');
const?yargs?=?require('yargs-parser');
const?minimist?=?require('minimist');
const?{?Suite?}?=?require('benchmark');
const?bench?=?new?Suite();
const?args?=?['--name=xiaoming',?'-abc',?'10',?'--save-dev',?'--age',?'20'];
bench
?.add('minimist?????',?()?=>?minimist(args))
?.add('mri??????????',?()?=>?mri(args))
?.add('nopt?????????',?()?=>?nopt(args))
?.add('yargs-parser?',?()?=>?yargs(args))
?.on('cycle',?e?=>?console.log(String(e.target)))
?.run();

??本文的內(nèi)容主要參考解析效率最高的 mri 庫(kù)的源碼,感興趣的同學(xué)可以學(xué)習(xí)其源碼實(shí)現(xiàn)。(順便吐槽一下:嵌套三元操作符可讀性真的很差。。)
??雖然上述基準(zhǔn)測(cè)試中 minimist 效率并不很好,但是其覆蓋了比較全的參數(shù)輸入場(chǎng)景。(以上測(cè)試用例覆蓋的場(chǎng)景有限)
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長(zhǎng)指北,回復(fù)「1」加入高級(jí)前端交流群!「在這里有好多 前端?開發(fā)者,會(huì)討論?前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長(zhǎng)。
“在看轉(zhuǎn)發(fā)”是最大的支持
