Node.js 中如何收集和解析命令列引數
前言
在開發 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 風格:引數以「-」(連字元)開頭
- GNU 風格:引數以「--」(雙連字元)開頭
- BSD 風格:引數以空格分割
Unix 引數風格有一個特殊的注意事項:「「-」後面緊鄰的每一個字母都表示一個引數名」。
ls -al
上述命令用來顯示當前目錄下所有的檔案、資料夾並且顯示它們的詳細資訊,等同於:
ls -a -l
GNU 風格的引數以 「--」開頭,一般後面會跟上一個單詞或者短語,例如熟悉的 npm 安裝依賴的命令:
npm install --save koa
對於兩個單詞的情況,在 GNU 引數風格中,會通過「-」來連線,例如 npm 安裝僅用於開發環境的依賴:
npm install --save-dev webpack
BSD 是加州大學伯克利分校開發的一個 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,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,引數值為 20
let 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 效率並不很好,但是其覆蓋了比較全的引數輸入場景。(以上測試用例覆蓋的場景有限)
到此這篇關於Node.js 中如何收集和解析命令列引數的文章就介紹到這了,更多相關Node.js 解析命令列引數內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!