在開發 CLI(Command Line Interface)工具的業務場景下,離不開命令行參數的收集和解析。
接下來,本文介紹如何收集和解析命令行參數。
收集命令行參數在 Node.js 中,可以通過 process.argv 屬性收集進程被啟動時傳入的命令行參數:
// ./example/demo.js
process.argv.slice(2);
// 命令行執行如下命令
node ./example/demo.js --name=xiaoming --age=20 man
// 得到的結果
[ '--name=xiaoming', '--age=20', 'man' ]由上述示例可以發現,Node.js 在處理命令行參數時,只是簡單地通過空格來分割字符串。
對於這樣的參數數組,無法很方便地獲取到每個參數對應的值,所以需要再進行一次解析操作。
命令行參數風格在解析命令行參數之前,需要了解一些常見的命令行參數風格:
Unix 參數風格有一個特殊的注意事項:「「-」後面緊鄰的每一個字母都表示一個參數名」。
ls -al上述命令用來顯示當前目錄下所有的文件、文件夾並且顯示它們的詳細信息,等同於:
ls -a -lGNU 風格的參數以 「--」開頭,一般後面會跟上一個單詞或者短語,例如熟悉的 npm 安裝依賴的命令:
npm install --save koa對於兩個單詞的情況,在 GNU 參數風格中,會通過「-」來連接,例如 npm 安裝僅用於開發環境的依賴:
npm install --save-dev webpackBSD 是加州大學伯克利分校開發的一個 Unix 版本。其與 Unix 的區別主要在於參數前面沒有 「-」,個人感覺這樣很難區別參數和參數值。
❝注意事項:-- 後面緊鄰空格時,表示後面的字符串不需要解析。
❞解析命令行參數function parse(args = []) {
// _ 屬性用來保留不需要處理的參數字符串
const output = { _: [] };
for (let index = 0; index < args.length; 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));接收到命令行參數數組之後,需要遍歷數組,處理每一個參數字符串。
isIgnoreFollowingParameters 方法主要用來判斷單個「--」的場景,後續的參數字符串不再需要處理:
function isIgnoreFollowingParameters(output, args, index, arg) {
if (arg !== '--') {
return false;
}
output._ = output._.concat(args.slice(++index));
return true;
}接下來,如果參數字符串不以「-」開頭,同樣也不需要處理,參數的形式以 Unix 和 GNU 風格為主:
function isParameter(arg) {
return arg.startsWith('-');
}參數的表現形式主要分為以下幾種:
"--name=xiaoming": 參數名為 name,參數值為 xiaoming"-abc=10": 參數名為 a,參數值為 true;參數名為 b,參數值為 true;參數名為 c,參數值為 10"--save-dev": 參數名為 save-dev,參數值為 true"--age 20":參數名為 age,參數值為 20let hyphensIndex;
for (hyphensIndex = 0; hyphensIndex < arg.length; hyphensIndex++) {
if (arg.charCodeAt(hyphensIndex) !== 45) {
break;
}
}
let assignmentIndex;
for (assignmentIndex = hyphensIndex + 1; assignmentIndex < arg.length; assignmentIndex++) {
if (arg[assignmentIndex].charCodeAt(0) === 61) {
break;
}
}利用 Unicode 碼點值找出連字符和等號的下標值,從而根據下標分割出參數名和參數值:
const name = arg.substring(hyphensIndex, assignmentIndex);
let value;
const assignmentValue = arg.substring(++assignmentIndex);處理參數值時,需要考慮參數賦值的四種場景:
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 風格中每一個字母都代表一個參數,並且「手動傳遞的參數值應該賦值給最後一個參數」,所以還需針對該場景進行適配:
// 「-」or「--」
const arr = hyphensIndex === 2 ? [name] : name;
for (let keyIndex = 0; keyIndex < arr.length; keyIndex++) {
const _key = arr[keyIndex];
const _value = keyIndex + 1 < arr.length || value;
handleKeyValue(output, _key, _value);
}最後針對參數的賦值操作,需要考慮到「多次賦值」的情況:
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;
}到此,命令行參數的解析功能就完成了,上述方法執行的效果如下:
# 命令行執行
node ./example/step1.js --name=xiaoming --age 20 --save-dev -abc=10 -c=20 -- --ignore
# 解析結果
{
_: [ '--ignore' ],
name: 'xiaoming',
age: '20',
'save-dev': true,
a: true,
b: true,
c: [ '10', '20' ]
}
別名機制比較優秀的 CLI 工具在參數的解析上都支持參數的別名設置,例如使用 npm 安裝開發環境依賴時,你可以選擇這種完整的寫法:
npm install --save-dev webpack你也可以使用下面這種別名方式:
npm install -D webpack從使用上來說 -D 和 --save-dev 是兩種方式,但是從 CLI 工具的開發者來說,最終處理邏輯時只能以一個參數名為標準,所以對於一個命令行參數解析庫來說,其結果需要包含所有的情況:
npm install --save-dev webpack
# 解析的結果
{ 'save-dev': true, 'D': true }以上文的解析方法為例,需要添加額外的選項參數,加入 alias 屬性來聲明別名屬性的對應關係:
parse(process.argv.slice(2), {
alias: {
'save-dev': 'S'
}
})上述方式符合正常的理解:設置參數對應的別名。但這是一個「單向查找關係」,需要轉化為:
"alias": {
"save-dev": ["s"],
"s": ["save-dev"]
}因為對於使用者來說,只會選擇一種方式傳遞參數。對於開發者的話需要根據任意一個別名找到其相關聯的別名:
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;
}除了別名之外,還可以在參數解析之後做如下優化:
成熟的解析庫針對一些成熟的命令行參數解析庫可以採用基準測試查看它們的解析效率:
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();本文的內容主要參考解析效率最高的 mri 庫的源碼,感興趣的同學可以學習其源碼實現。(順便吐槽一下:嵌套三元操作符可讀性真的很差。。)
雖然上述基準測試中 minimist 效率並不很好,但是其覆蓋了比較全的參數輸入場景。(以上測試用例覆蓋的場景有限)
1.看到這裡了就點個在看支持下吧,你的「點讚,在看」是我創作的動力。
2.關注公眾號
程式設計師成長指北,回復「1」加入高級前端交流群!「在這裡有好多 前端 開發者,會討論 前端 Node 知識,互相學習」!
3.也可添加微信【ikoala520】,一起成長。
「在看轉發」是最大的支持