Node.js 中如何收集和解析命令行参数


Posted in Javascript onJanuary 08, 2021

前言

在开发 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, 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 中如何收集和解析命令行参数

本文的内容主要参考解析效率最高的 mri 库的源码,感兴趣的同学可以学习其源码实现。(顺便吐槽一下:嵌套三元操作符可读性真的很差。。)

虽然上述基准测试中 minimist 效率并不很好,但是其覆盖了比较全的参数输入场景。(以上测试用例覆盖的场景有限)

到此这篇关于Node.js 中如何收集和解析命令行参数的文章就介绍到这了,更多相关Node.js 解析命令行参数内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript 拖拉缩放效果
Dec 10 Javascript
js 数组实现一个类似ruby的迭代器
Oct 27 Javascript
js展开闭合效果演示代码
Jul 24 Javascript
JQuery触发事件例如click
Sep 11 Javascript
js中top的作用深入剖析
Mar 04 Javascript
JavaScript判断数组是否存在key的简单实例
Aug 03 Javascript
JS实现列表页面隔行变色效果
Mar 25 Javascript
javascript 取小数点后几位几种方法总结
Aug 02 Javascript
详解Vue-cli代理解决跨域问题
Sep 27 Javascript
js定时器+简单的动画效果实例
Nov 10 Javascript
Vue中的vue-resource示例详解
Nov 02 Javascript
JavaScript实现简单验证码
Aug 24 Javascript
vue编写简单的购物车功能
Jan 08 #Vue.js
three.js中多线程的使用及性能测试详解
Jan 07 #Javascript
解决vue使用vant轮播组件swipe + flex时文字抖动问题
Jan 07 #Vue.js
vuex的使用和简易实现
Jan 07 #Vue.js
vue watch监控对象的简单方法示例
Jan 07 #Vue.js
vue.js watch经常失效的场景与解决方案
Jan 07 #Vue.js
Node快速切换版本、版本回退(降级)、版本更新(升级)
Jan 07 #Javascript
You might like
Phpbean路由转发的php代码
2008/01/10 PHP
使用php验证复选框有效性的示例
2013/11/13 PHP
基于thinkphp6.0的success、error实现方法
2019/11/05 PHP
JavaScript CSS修改学习第五章 给“上传”添加样式
2010/02/19 Javascript
jquery multiSelect 多选下拉框
2010/07/09 Javascript
javascript的数据类型、字面量、变量介绍
2012/05/23 Javascript
jquery实现输入框动态增减的实例代码
2013/07/14 Javascript
jquery $(&quot;#variable&quot;) 循环改变variable的值示例
2014/02/23 Javascript
bootstrap-wysiwyg结合ajax实现图片上传实时刷新功能
2016/05/27 Javascript
Ionic默认的Tabs模板使用实例
2016/08/29 Javascript
D3.js实现雷达图的方法详解
2016/09/22 Javascript
JQuery选中select组件被选中的值方法
2018/03/08 jQuery
详解Vue基于 Nuxt.js 实现服务端渲染(SSR)
2018/04/05 Javascript
详解Express笔记之动态渲染HTML(新手入坑)
2018/12/13 Javascript
vue点击按钮实现简单页面的切换
2020/09/08 Javascript
[01:54]TI珍贵瞬间系列(五):压力
2020/08/29 DOTA
Python的MongoDB模块PyMongo操作方法集锦
2016/01/05 Python
Python实现提取XML内容并保存到Excel中的方法
2018/09/01 Python
纯用NumPy实现神经网络的示例代码
2018/10/24 Python
python获取中文字符串长度的方法
2018/11/14 Python
python读取图片的几种方式及图像宽和高的存储顺序
2020/02/11 Python
python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例
2020/02/27 Python
python游戏开发的五个案例分享
2020/03/09 Python
python中threading开启关闭线程操作
2020/05/02 Python
Python基于codecs模块实现文件读写案例解析
2020/05/11 Python
pytorch加载自己的图像数据集实例
2020/07/07 Python
大学三年的自我评价
2013/12/25 职场文书
总经理秘书的岗位职责
2013/12/27 职场文书
小学运动会广播稿200字(十二篇)
2014/01/14 职场文书
初中生自我评价
2014/02/01 职场文书
小学教师培训感言
2014/02/11 职场文书
数控专业自荐书范文
2014/03/16 职场文书
心理学专业求职信
2014/06/16 职场文书
缓刑期间思想汇报范文
2014/10/10 职场文书
python爬取豆瓣电影TOP250数据
2021/05/23 Python
Go本地测试解耦任务拆解及沟通详解Go本地测试的思路沟通的重要性总结
2022/06/21 Golang