利用Node.js 怎么对命令行参数进行解析
创始人
2024-12-28 07:02:59
0次

收集命令行参数

在 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 安装开发环境依赖时,你可以选择这种完整的写法:

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();

利用Node.js 怎么对命令行参数进行解析

相关内容

热门资讯

空调连续工作多久需要停机休息 空调连续工作的时间取决于多个因素,包括空调的型号、功率、使用环境和维护情况等。一般来说,空调可以连续...
空调哪个符号是除湿的 空调除湿功能通常用一个水滴形状的符号表示。不同品牌和型号的空调遥控器可能会有所差异,但一般来说,除湿...
格力空调多长时间加一次氟利昂 1、一般情况下,空调对氟利昂的使用量是非常小的,所以在空调使用了四年左右的时候再考虑加也没问题。2、...
空调用多长时间需要加氟一次 一般情况下是五年。当出现以下这几种情况时,空调才需要加氟。1、空调的室内机和室外机各有两个接口,当接...
格调生活·C标思享荟:卡萨帝在... 9月12日的郑州龙湖金融岛,夜风轻拂,灯光璀璨。“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑...
品牌价值破千亿、高端份额持续领... 9月12日,“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑州站”在郑州龙湖金融岛举办。2026...
空调主板坏了修800还是换新的 空调主板坏了是修还是换新,需要考虑多个因素,以下是一些建议:1. 维修成本:如果维修费用较低,比如 ...
空调开多久需要休息多长时间 问题:空调开多久需要休息多长时间回答:1、在使用空调的过程中,一般空调开7~8个小时左右需要休息2~...
中央空调多长时间清洗一次 1、一般空调开机前清洗一次即可,空调开机中间时段再清洗一次,空调关机时清洗一次,这样比较合理。2、空...
中央空调氟利昂多久加一次 看空调制热和制冷程度,很多人的观点认为氟利昂必须一年放一次,其实这个观点是错误的,理解上面有点偏差,...