本文详解 Node.js 中命令行参数的收集与解析机制。通过 process.argv 获取原始参数数组后,文章深入解析 Unix、GNU 和 BSD 三种风格,并展示如何手动实现参数名与值的提取、别名映射及多次赋值处理,为开发高效 CLI 工具提供底层实现参考。
引言
构建命令行界面(CLI)应用时,获取并处理用户输入的参数是核心环节。
下文将阐述实现参数收集与解析的具体方法。
获取命令行参数
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, 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,值为 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 install --save-dev webpack
亦可采用以下简写形式:
npm install -D webpack
对用户而言,-D 与 --save-dev 是两种输入方式;对开发者而言,最终逻辑需统一以单一参数名为准。因此,解析库结果应涵盖所有别名情况:
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 解析命令行参数内容请搜索本站历史文章或继续浏览以下相关文章,希望大家以后多多支持本站!