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ù)組,無法很方便地獲取到每個(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è)單詞或者短語,例如熟悉的 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 < 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));
接收到命令行參數(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 < 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 碼點(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 < arr.length; keyIndex++) { const _key = arr[keyIndex]; const _value = keyIndex + 1 < arr.length || value; 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ù)解析庫來說,其結(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è)定
成熟的解析庫
針對(duì)一些成熟的命令行參數(shù)解析庫可以采用基準(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 庫的源碼,感興趣的同學(xué)可以學(xué)習(xí)其源碼實(shí)現(xiàn)。(順便吐槽一下:嵌套三元操作符可讀性真的很差。。)
雖然上述基準(zhǔn)測(cè)試中 minimist 效率并不很好,但是其覆蓋了比較全的參數(shù)輸入場(chǎng)景。(以上測(cè)試用例覆蓋的場(chǎng)景有限)
到此這篇關(guān)于Node.js 中如何收集和解析命令行參數(shù)的文章就介紹到這了,更多相關(guān)Node.js 解析命令行參數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Ubuntu中搭建Nodejs開發(fā)環(huán)境過程分享
這篇文章主要介紹了Ubuntu中搭建Nodejs開發(fā)環(huán)境過程,比較郁悶的是apt-get安裝失敗了,如果有遇到一樣問題的朋友,可以參考一下本文2014-06-06Nodejs進(jìn)階:核心模塊net入門學(xué)習(xí)與實(shí)例講解
本篇文章主要是介紹了Nodejs之NET模塊,net模塊是同樣是nodejs的核心模塊,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-11-11node.js版本降級(jí)/升級(jí)的實(shí)現(xiàn)
在項(xiàng)目開發(fā)過程中,不同項(xiàng)目使用的nodejs版本不同,有時(shí)會(huì)因?yàn)閚ode版本過高或太低,導(dǎo)致報(bào)錯(cuò),本文主要介紹了node.js版本降級(jí)/升級(jí)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05NodeJs+MySQL實(shí)現(xiàn)注冊(cè)登錄功能
這篇文章主要為大家詳細(xì)介紹了NodeJs+MySQL實(shí)現(xiàn)注冊(cè)登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04基于nodejs+express4.X實(shí)現(xiàn)文件下載的實(shí)例代碼
本篇文章主要介紹了詳解nodejs+express4.X的文件下載的實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07關(guān)于Node.js的events.EventEmitter用法介紹
本篇文章主要介紹了關(guān)于Node.js的events.EventEmitter用法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04詳解nodejs微信公眾號(hào)開發(fā)——6.自定義菜單
這篇文章主要介紹了詳解nodejs微信公眾號(hào)開發(fā)——6.自定義菜單,自定義菜單能夠幫助公眾號(hào)豐富界面,讓用戶更好更快地理解公眾號(hào)的功能。2017-04-04node.js?中的require用法和import的區(qū)別解析
在Node.js中,require是一個(gè)內(nèi)置的函數(shù),用于在當(dāng)前模塊中加載和緩存其他模塊,這篇文章給大家介紹node.js?中的require用法和import的區(qū)別,感興趣的朋友跟隨小編一起看看吧2024-04-04