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 相关文章推荐
JS event使用方法详解
Apr 28 Javascript
使用javascript获取flash加载的百分比的实现代码
May 25 Javascript
基于mootools插件实现遮罩层新手引导
May 24 Javascript
将HTML的左右尖括号等转义成实体形式的两种实现方式
May 04 Javascript
jquery通过visible来判断标签是否显示或隐藏
May 08 Javascript
jQuery实现textarea自动增长宽高的方法
Dec 18 Javascript
angular双向绑定模拟探索
Dec 26 Javascript
vue项目打包后打开页面空白解决办法
Jun 29 Javascript
AngularJS实现的自定义过滤器简单示例
Feb 02 Javascript
AjaxFileUpload.js实现异步上传文件功能
Apr 19 Javascript
小程序实现横向滑动日历效果
Oct 21 Javascript
JavaScript 中的六种循环方法
Jan 06 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
PHP网站安装程序制作的原理、步骤、注意事项和示例代码
2010/08/01 PHP
html静态页面调用php文件的方法
2014/11/13 PHP
php利用cookies实现购物车的方法
2014/12/10 PHP
PDO::quote讲解
2019/01/29 PHP
PHP微信发送推送消息乱码的解决方法
2019/02/28 PHP
jquery 得到当前页面高度和宽度的两个函数
2010/02/21 Javascript
多个datatable共存造成多个表格的checkbox都被选中
2013/07/11 Javascript
JS中prototype关键字的功能介绍及使用示例
2013/07/21 Javascript
jQuery垂直多级导航菜单代码分享
2015/08/18 Javascript
返回函数的JavaScript函数
2016/06/14 Javascript
原生js更改css样式的两种方式
2017/03/15 Javascript
Vue实现动态响应数据变化
2017/04/28 Javascript
js插件实现图片滑动验证码
2020/09/29 Javascript
ES10 特性的完整指南小结
2019/03/04 Javascript
微信小程序中网络请求缓存的解决方法
2019/12/29 Javascript
原生JS实现汇率转换功能代码实例
2020/05/13 Javascript
Nuxt.js的路由跳转操作(页面跳转nuxt-link)
2020/11/06 Javascript
[01:55]2014DOTA2国际邀请赛 BBC正赛第一天总结
2014/07/10 DOTA
Python map和reduce函数用法示例
2015/02/26 Python
用Python制作简单的钢琴程序的教程
2015/04/01 Python
python类装饰器用法实例
2015/06/04 Python
详细介绍Python的鸭子类型
2016/09/12 Python
python中异常报错处理方法汇总
2016/11/20 Python
Python实现句子翻译功能
2017/11/14 Python
python3学习笔记之多进程分布式小例子
2018/02/13 Python
详解通过API管理或定制开发ECS实例
2018/09/30 Python
Python Numpy数组扩展repeat和tile使用实例解析
2019/12/09 Python
Python实现不规则图形填充的思路
2020/02/02 Python
详解anaconda安装步骤
2020/11/23 Python
python 30行代码实现蚂蚁森林自动偷能量
2021/02/08 Python
New Balance天猫官方旗舰店:始于1906年,百年慢跑品牌
2017/11/15 全球购物
现金会计岗位职责
2013/12/05 职场文书
收银员岗位职责
2015/02/03 职场文书
迎新生晚会主持词
2015/06/30 职场文书
关于远足的感想
2015/08/10 职场文书
导游词之青岛崂山
2019/12/27 职场文书