一、前言
本篇文章记录了 解析命令行参数包:minimist 的学习过程。首先命令行参数指的是在终端输入的命令,例如:打开终端,输入命令 create-vite-lsx my-project --template vue
(实际上这里命令是create-vite-lsx
,参数是 my-project --template vue
),该命令旨在创建一项目:
(1)模板是 vue,即 template-vue;
(2)项目名称是 my-project,
于是在执行这行命令时,先对其解析,node 接收命令行参数是通过 process.argv
:
可知,process.argv
是以 字符串数组 的形式接收的命令行参数,其中:
(1)第一个参数是执行 node 的地址;
(2)第二个参数是当前执行命令所在的地址;
(3)往后的参数便是命令后的参数...
观察输入的命令,出现两种不同的参数:
(1)my-project:命令 create-vite-lsx
的第一个参数;
(2)--template vue:原意 是第二个参数,由用户指定参数 template 值为 vue;
而 process.argv
是以空格为参数分割符的字符串数组的形式接收命令行参数的,process.argv.slice(2)
得到的数据:
['my-project', '--template', 'vue']
这对于拿到 template 值就不好拿了,需要进行处理~
由结果导向,minimist(process.argv.slice(2))
执行结果如下:
import minimist from 'minimist';
const argv = process.argv.slice(2)
const argv_minimist = minimist(argv)
console.log("解析命令行参数:", argv, argv_minimist)
// 终端打印:
// 解析命令行参数: [ 'my-project', '--template', 'vue' ] { _: [ 'my-project' ], template: 'vue' }
通过 minimist 处理过的命令行参数,得到一个对象,非用户指定参数(即没有指定名称)的参数以数组的形式存储在 _,而明确指定参数名称的参数以键值对的形式存储,这样便很方便的拿到 template 的值。
下面开始了解 minimist 对字符串数组的处理过程~
二、准备工作
(1)先将 源码 下载到本地; (2)并安装依赖。
从 package.json 找到其入口文件 index.js, 该文件共有 263 行代码(短小精悍)。该项目没有使用打包或压缩等工具处理源代码, 直接使用即可。
三、调试/学习方式
增加脚本命令(package.json 文件 scripts):"test:parse": "node example/parse.js"
,修改 example/parse.js 的入口文件为 indexlearn.js(需要新建一个):
'use strict';
// var argv = require('../')(process.argv.slice(2));
var argv = require('../indexlearn.js')(process.argv.slice(2));
console.log(argv);
四、目录结构
目录结构:
└── example/ // 使用示例目录
├── index.js // 入口文件
└── indexlearn.js // 学习
└── test/ // 单元测试
├── package.json
├── README.md
整个项目的目录结构比较简单, 虽然只是一个功能模块(解析命令行参数),但整个项目 非常精致、短小精悍(还做了代码风格的控制,使用了 eslint)。重点关注:
- index.js : 入口文件,具体实现;
- example 目录 : 提供了一个使用示例;
- test 目录 : 单元测试,使用了 tape 工具;
写一个工具,自动生成想要的目录结构:
// scripts/generateDirStructure.js
var fs = require("fs")
var path = require("path")
var root = process.cwd();
var targetFile = "scripts/dir.md" // 输出文件
// 过滤掉的文件
var filterFiles = [
".git",".github",".gitignore","node_modules","yarn.lock",".eslintrc",".npmrc",".nycrc","CHANGELOG.md","LICENSE"
]
var content = (fs.readdirSync(path.resolve(root)) || [])
.filter(function (file) {
return file && !filterFiles.includes(file)
})
.map(function (file) {
var temp = "", hasDir = true;
try {
var fileList = fs.readdirSync(path.resolve(root, file))
// console.log("是目录: ", fileList)
} catch(e) {
// console.log("不是目录")
hasDir = false
}
temp = (hasDir ? "└── " : "└── ") + file + (hasDir ? "/" : "")
return temp
})
.filter(Boolean)
.join("\r\n")
fs.writeFileSync(path.resolve(root, targetFile), content, "utf-8")
console.log()
console.log("目录结构数据已写入 " + targetFile + " !")
五、源码解析
观源码 index.js,该包只是提供了一个方法,接收两个参数:
(1)args:命令行参数 process.argv, 类型是字符串数组;
(2)opts:配置数据,类型是对象:
- unknownFn : 函数, 针对未知参数的处理;
- boolean : Boolean(布尔类型, true 表示所有的参数均是布尔类型) | Array(字符串数组, 对应的字符串的值均为布尔类型);
- alias : Object; 声明别名, 键值对组成的数组, 其值类型:(String)字符串或Array(字符串数组);
- string : String 或 Array, 声明哪些参数是字符串, 为了将数字字符串Number化;
- default : Object; 指定参数的默认值;
- ……
1. 整体结构
// indexlearn.js
module.exports = function (args, opts) {
// ...处理...
// 1. 解析配置参数 opts
// 1.1 opts.unknownFn
// 1.2 opts.boolean 指明布尔类型参数
// 例如: 若opts.boolean为true, 则将所有不带等号的双连字符参数视为布尔值(--name)
// 1.3 opts.alias 指明参数的别名, 【处理后】存储在变量 aliases
// 例如: {name:["n", "na", "nam"]} -> {name:["n", "na", "nam"], n:["name", "na", "nam"], na:["name", "n", "nam"], nam:["name", "n", "na"] }
// 1.4 opts.strings 指明字符串类型参数,联动别名
// ...
var argv = {_:[]}
// 2. 解析 args, 整合参数
// 2.1 处理 args 参数, 得到新的 args 和 notFlags
// 2.2 遍历新的 args 并处理, 整合到 argv
// - 这里涉及了一些正则判断和处理, 是对不规则参数的处理
// 2.3 处理 opts.defaults(提供参数的默认值), 整合到 argv
// 2.4 如果 opts["--"] 为 true, 处理 notFlags 参数, 整合到 argv
// ...
return argv
}
2. 解析配置参数
(1)opts.unknownFn
if(!opts) opts = {}
var flags = {
bools: {}, // 指明哪些参数是布尔类型
strings: {}, // 字符串类型参数
unknownFn: null,
}
if (typeof opts.unknown === 'function') {
flags.unknownFn = opts.unknown;
}
(2)opts.boolean:指明布尔类型的参数
if (typeof opts.boolean === 'boolean' && opts.boolean) { // Boolean
flags.allBools = true;
} else {
// Array.concat() 链接多个数组或追加数组项
[].concat(opts.boolean).filter(Boolean).forEach(function (key) { // Array
flags.bools[key] = true; // 指明布尔类型的参数
});
}
(3)opts.alias:别名配置
var aliases = {};
// 处理示例:{"name": ["n", "na"]} -> {"name": ["n", "na"], "n": ["name", "na"], "na": ["name", "n"]}
Object.keys(opts.alias || {}).forEach(function (key) {
aliases[key] = [].concat(opts.alias[key]);
aliases[key].forEach(function (x) {
aliases[x] = [key].concat(aliases[key].filter(function (y) {
return x !== y;
}));
});
});
(4)opts.string:指明字符串类型的参数(数字字符串是否转为数字类型,例如:"2.5000" -> 2.5)
[].concat(opts.string).filter(Boolean).forEach(function (key) {
flags.strings[key] = true; // 明确指出字符串类型的参数
if (aliases[key]) {
[].concat(aliases[key]).forEach(function (k) {
flags.strings[k] = true;
});
}
});
(5)opts.default:提供了默认参数及对应值
var defaults = opts.default || {};
3. 抽离出的功能点
(1)外部功能点:
- hasKey 指定的key是否存在给定的obj
function hasKey(obj, keys) { // 实际上, 入参 keys 更多的是类似命名空间的一个概念, 通过.连接, 由大到小 var o = obj; // keys.slice(0, -1) 切片数组, [0,length-1] keys.slice(0, -1).forEach(function (key) { o = o[key] || {}; }); var key = keys[keys.length - 1]; // 最后一个数组项 return key in o; // 对象 o 是否含有键 key }
- isNumber 是否是数字(数字类型/数字字符串)
function isNumber(x) { if (typeof x === 'number') { return true; } // 数字类型 if ((/^0x[0-9a-f]+$/i).test(x)) { return true; } // 十六进制数字字符串 return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x); // 其他数字字符串,这里还匹配形如 10.2e3 的数字(10.2e3 = 10.2*1000=10200) }
- isConstructorOrProto 是否是原型
(2)内部功能点:
-
aliasIsBoolean:判断别名里是否有布尔类型的
function aliasIsBoolean(key) { return aliases[key].some(function (x) { return flags.bools[x]; }); }
-
argDefined:是否已被定义
function argDefined(key, arg) { return (flags.allBools && (/^--[^=]+$/).test(arg)) || flags.strings[key] || flags.bools[key] || aliases[key]; }
-
setKey
function setKey(obj, keys, value) { var o = obj; for (var i = 0; i < keys.length - 1; i++) { ... } var lastKey = keys[keys.length - 1]; if (isConstructorOrProto(o, lastKey)) { return; } if ( o === Object.prototype || o === Number.prototype || o === String.prototype ) { o = {}; } if (o === Array.prototype) { o = []; } if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') { o[lastKey] = value; } else if (Array.isArray(o[lastKey])) { o[lastKey].push(value); } else { o[lastKey] = [o[lastKey], value]; // 如果同key的参数有多个,则用数组存储 } }
-
setArg:将 key 存入 argv
function setArg(key, val, arg) { if (arg && flags.unknownFn && !argDefined(key, arg)) { if (flags.unknownFn(arg) === false) { return; } } var value = !flags.strings[key] && isNumber(val) // 如果不是字符串类型且是数字 ? Number(val) : val; setKey(argv, key.split('.'), value); (aliases[key] || []).forEach(function (x) { // 同步到别名参数 setKey(argv, x.split('.'), value); }); }
4. 解析参数列表 args
(1)以 -- 为分割符将参数数组 args 拆分成新的 args (分割符前数组) 和 notFlags (分割符后的数组):
var notFlags = [];
if (args.indexOf('--') !== -1) {
notFlags = args.slice(args.indexOf('--') + 1);
args = args.slice(0, args.indexOf('--'));
}
(2)遍历新的 args , 针对五种参数类型做解析,其结果存储在 argv(使用方法 setArg ):
for (var i = 0; i < args.length; i++) {
var arg = args[i], key, next;
if ((/^--.+=/).test(arg)) {
// 参数一
...
} else if ((/^--no-.+/).test(arg)) {
// 参数二
...
} else if ((/^--.+/).test(arg)) {
// 参数三
...
} else if ((/^-[^-]+/).test(arg)) {
// 参数四: 连续多个简写参数解析
...
} else {
// 参数五
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
// 若是不想让数字字符串参数转为数字类型(即 2.5000 -> 2.5), 可将 opts.string 添加 "_" 值
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg)); // 如果 strings._ 为 true, 可以直接用 arg, 而不用考虑如果是数字字符串则转为数字类型
}
// opts.stopEarly ??
// 若为true, 则在遇到一个正常的参数时, 将其后面的所有参数都存储在arv._里, 并结束for循环
if (opts.stopEarly) {
argv._.push.apply(argv._, args.slice(i + 1));
break;
}
}
}
for 循环里的 if 正则条件的限制范围是由小变大, 个人觉得这块的逻辑处理的十分细腻, 是解析命令行参数的精髓所在~
图示:
(3)处理指定的默认参数值 opts.default,将不在 argv 里明确指出的参数值存入 argv:
Object.keys(defaults).forEach(function (k) {
if (!hasKey(argv, k.split('.'))) { // 只有不在 **argv** 里的 key 方可设置默认值
setKey(argv, k.split('.'), defaults[k]);
(aliases[k] || []).forEach(function (x) { // 若是有别名,则一同处理(如果同一个key,有多个值,则会以数组形式存储)
setKey(argv, x.split('.'), defaults[k]);
});
}
});
(4)处理 notFlags ,如果 opts["--"] 为 true,则将 notFlags 存入 argv["--"]; 否则依次插入 argv["_"]
if (opts['--']) { // opts['--']
argv['--'] = notFlags.slice();
} else {
notFlags.forEach(function (k) { // 依次存入 argv._
argv._.push(k);
});
}
六、示例
1. 示例一
// example/parse.js
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "-f"
]);
console.log(argv)
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16 ],
template: 'vue',
name: true,
f: true
}
2. 示例二: boolean
// example/parse.js
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "-f"
], {
boolean: true,
});
console.log(argv)
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16, 'vue' ],
template: true, // 走到for循环的正则 /^--.+/ 的第一个条件
name: true, // 走到for循环的正则 /^--.+/ 的 else
age: 16, // 走到for循环的正则 /^--.+=/
f: true // 走到for循环的正则 /^-[^-]+/
}
3. 示例三: alias
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "-f"
], {
boolean: true,
alias: {"template": ["t", "temp"]}
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16, 'vue' ],
template: true, // 因为 opts.boolean 为 true, 则flags.allBools为true, 于是该值为true
t: true, // template 别名
temp: true, // template 别名
name: true,
f: true
}
4. 示例四: string
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "-f", "--price", "2.5120"
], {
boolean: true,
alias: {"template": ["t", "temp"]},
string: "price"
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16, 'vue', 2.512 ],
template: true,
t: true,
temp: true,
name: true,
age: 16,
f: true,
price: '' // 从args看, 定义的price值是2.5120, 但因为opts.strings.price为true即price参数是字符串
}
5. 示例五: default
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "-f", "--price", "2.5120",
], {
boolean: true,
alias: {"template": ["t", "temp"]},
string: "price",
default: {price:"1.0000", code:"123456"}
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16, 'vue', 2.512 ],
template: true,
t: true,
temp: true,
name: true,
age: 16,
f: true,
price: '',
code: '123456' // default 提供了参数的默认值, 注意default.price和default.code的区别
}
6. 示例六: --no-, --age=16, --template vue
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "--sex=女", "--no-use"
], {
boolean: ["sex"],
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16 ],
sex: true,
template: 'vue',
name: true,
age: 16,
use: false // --no-key 直接将key值赋为false,而不用明确指出
}
7. 示例七: --
var argv = require('../indexlearn-2.js')([
"lushuixi", "16", "--template", "vue", "--name", "--age=16", "--sex=女"
], {
boolean: ["sex"],
"--": true, //
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi', 16 ],
sex: false,
template: 'vue',
'--': [ '--name', '--age=16', '--sex=女' ]
}
8. 示例八: -t
var argv = require('../indexlearn-2.js')([
"lushuixi",
"-t-",
"-n10.2e3",
"-w-3",
"-abc[]",
"-efg",
"2.1234",
"-r1t2a3b[]789",
], {
});
console.log(argv);
执行命令:
# yarn test:parse
结果:
{
_: [ 'lushuixi' ],
t: '-',
n: 10200,
w: -3,
a: true,
b: true,
c: '[]',
e: true,
f: true,
g: 2.1234,
r: '1t2a3b[]789'
}
七、收获与总结
1. 收获
(1)接触了一些正则表达式,例如:
/^--.+=/
: 前两个字符是--, 第三个字符是任意字符且连续出现多次, 接着是一个=;匹配字符串形如 --template=vue、/^--no-.+/
: 前五个字符是--no-, 第六个字符是任意字符且连续出现多次;匹配字符串形如 --no-use、/^--.+/
: 前两个字符是--, 第三个字符是任意字符且连续出现多次;匹配字符串形如 --template、/^-[^-]+/
: 第一个字符是-, 第二个字符是除-以外的字符且连续出现多次;匹配字符串形如 -abc、/^--([^=]+)=([\s\S]*)$/
/-?\d+(\.\d*)?(e-?\d+)?$/
(2)了解了minimist 的具体实现方式和功能;
(3)了解了单元测试及单元测试工具 tap 的使用;
2. 小结
整体学下来,发现一个点, minimist 对参数的格式限制不是很具体, 例如:
- 正则
/^--.+=/
, 既可匹配 --template=vue, 也可匹配到 ---template=== - 正则
/^--no-.+/
, 既可匹配 --no-template, 也可匹配到 --no---template=
即: minimist 没有明确指出参数的格式(正确和错误的格式), 只要满足这个正则的字符串参数皆可。
八、源码注解
'use strict'
// 判断 keys 的最后一个 key 是否存在 obj
function hasKey(obj, keys) {
var o = obj
// Array.slice(start,end) 如果start|end是负数, 则是从尾部开始算起的位置
// keys.slice(0, -1) 除了最后一个字符
keys.slice(0, -1).forEach(function (key) {
o = o[key] || {}
});
// 处理最后一个字符
// 这种将数组的处理分两步:
// - 先处理除了最后一个字符串的数组: 决定了o
// - 再处理最后一个字符串,
var key = keys[keys.length - 1]
// key in o 判断对象 o 是否还有键为key的
return key in o
}
// 判断是否是数字
function isNumber(x) {
if (typeof x === 'number') { return true; } // 十进制数字类型
if ((/^0x[0-9a-f]+$/i).test(x)) { return true; } // 十六进制
return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x); // 字符串数字
}
// 是否是原型
function isConstructorOrProto(obj, key) {
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
}
// CommonJS 模块
module.exports = function (args, opts) {
if(!opts) opts = {}
console.log("minimist: ", args, opts)
var flags = {
bools: {}, // 指明哪些参数是布尔类型
strings: {},
unknownFn: null,
}
// 1. 处理接收得参数
// 1.1 opts.unknownFn 函数类型
if(typeof opts.unknownFn === 'function') {
flags.unknownFn = opts.unknownFn
}
// 1.2 opts.boolean
// 由2.1知, opts.boolean若是数组则是说明了数组项是布尔类型
if(typeof opts.boolean === 'boolean' && opts.boolean) {
// 如果 opts.boolean 是 Boolean 类型, 且是 true
flags.allBools = true
} else {
// flags.bools[key] 表示了什么?
// [].concat(opts.boolean).filter(Boolean) 过滤掉空和false值
// boolean: ["1", "2", "", false] -> bools: {"1":true, "2":true}
// 作用:将opts.boolean指定的值对应的bool设置为true
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
flags.bools[key] = true
})
}
// console.log("处理参数opts: ", flags, [].concat(opts.boolean).filter(Boolean))
// 1.3别名 opts.alias
var aliases = {}; // 其键值对的值是数组
// 但是为什么要用数组处理呢?
// Array.concat() 链接多个数组或追加数组项
// 那别名的键值对的值可能是字符串也可能是字符串数组
// 例如: alias: {string: ["1","2"]} -> aliases: {string: ["1","2"], "1": ["string","2"], "2": ["string","1"]}
Object.keys(opts.alias || {}).forEach(function (key) {
aliases[key] = [].concat(opts.alias[key])
aliases[key].forEach(function (x) {
// console.log("111: ", x, [key].concat(aliases[key]))
// 这里的 x 便是 key 对应的值, 将键和值都存储在 aliases 里, 且是两对, 互为键值
// [key].concat(aliases[key]) 由原键值对的键和值组成的数组, 再过滤掉与x, 组成新的键值对(键是x, 值是除了x的值)
aliases[x] = [key].concat(aliases[key]).filter(function (y) {
return x !== y
})
})
});
// console.log("别名:", opts.alias, aliases)
// 判断别名里是否有布尔类型(由flags.bools明确指定)
function aliasIsBoolean(key) {
// 只要别名里有一个的 bools[key] 为 true, 则表示该 key 对应的值是布尔类型
return aliases[key].some(function (x) {
return flags.bools[x]
})
}
// 1.4 opts.strings 作用??? 还根 aliases 有关
[].concat(opts.string).filter(Boolean).forEach(function (key) {
flags.strings[key] = true
if(aliases[key]) {
[].concat(aliases[key]).forEach(function (k) {
flags.strings[k] = true
})
}
})
// opts.defaults ?? 默认参数 ?
var defaults = opts.default || {}
var argv = {_:[]} // 存储解析结果
// 这又是为何
// if (args.indexOf('--') !== -1) {
// notFlags = args.slice(args.indexOf('--') + 1);
// args = args.slice(0, args.indexOf('--'));
// }
// 是否有默认的值
// 权重:
function argDefined(key, arg) {
return (flags.allBools && (/^--[^=]+$/).test(arg)) // 所有参数皆是布尔类型且--use
|| flags.strings[key] // strings[key] 为 true, 字符串
|| flags.bools[key] // bools[key] 为 true
|| aliases[key]; // 有别名
}
// 具体的键值对设置
function setKey(obj, keys, value) {
// console.log("keys", keys, value)
var o = obj
// 处理 [0, keys.length - 2]
for (var i = 0; i < keys.length - 1; i++) {
var key = keys[i]
if (isConstructorOrProto(o, key)) return
if (o[key] === undefined) {
o[key] = {}
}
if(
o[key] === Object.prototype
|| o[key] === Number.prototype
|| o[key] === String.prototype
) {
o[key] = {}
}
if (o[key] === Array.prototype) {
o[key] = []
}
o = o[key]
}
// 处理最后一个key
// 这种处理方式的作用何在??
var lastKey = keys[keys.length - 1]
if (isConstructorOrProto((o, lastKey))) return
if (
o === Object.prototype
|| o === Number.prototype
|| o === String.prototype
) {
o = {}
}
if (o === Array.prototype) {
o = []
}
if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === "boolean") {
o[lastKey] = value // 暂且先认为每次走到这里
} else if (Array.isArray(o[lastKey])) {
o[lastKey].push(value) // 数组继续存储同名key的值
} else {
o[lastKey] = [o[lastKey], value] // 如果是同名的key, 则将值存储在数组里
}
}
// 设置参数
function setArg(key, val, arg) {
// (1) 首先确认是否是未知参数
if(arg && flags.unknownFn && !argDefined(key, arg)) {
// unkownFn 针对未知参数处理的函数, 若处理后的未知参数是 false, 则退出
// !argDefined(key, arg) 表示没有任何外助, 即是未知参数
if(flags.unknownFn(arg) === false) return
}
// (2) 非字符串情况下, 如果是数字, 则转换为数字类型(因为命令行如参是字符串)
// 这里 key 可能有别名,所以前面 flags.strings 与 aliases 合到了 flags.strings
// 例如: key值为"2.0000", isNumber(val) 值便是数字类型的 2 了
var value = !flags.strings[key] && isNumber(val)
? Number(val)
: val;
setKey(argv, key.split('.'), value); // 为什么要让 key.split('.) ??
// (3) 别名参数设置
(aliases[key] || []).forEach(function (x) {
// console.log("setArg: ", key, x.split("."))
setKey(argv, x.split('.'), value)
});
}
// flags.bools 存储了是布尔类型的参数, 如果在 defaults 声明了默认值, 则使用默认值, 否则是 false
// 如果是布尔类型的话, 指定的参数的值就不需要明确指出来?? 好像是这样的哎~
Object.keys(flags.bools).forEach(function (key) {
setArg(key, defaults[key] === undefined ? false : defaults[key])
})
var notFlags = []
var notFlagsIndex = args.indexOf("--")
if(notFlagsIndex !== -1) {
// 如果的命令行参数数组里找到一个参数值为--
// 例如: ["false", "123", "--", "serve"] -> args.indexOf("--") 值为 2
// 即命令行参数含有两个-字符的参数
// 作用: 将参数和notFlags参数区分开
notFlags = args.slice(notFlagsIndex + 1)
args = args.slice(0, notFlagsIndex)
}
// console.log("notFlagsIndex:", notFlagsIndex, notFlags)
// 2. 处理参数, 最后整合成 {_:[], 键值对}
// 2.1 处理正规参数
for(var i = 0; i < args.length; i++) {
// 依次处理接受的字符串参数数组
// 对于参数的类型
// (1) 正常的参数
// (2) 不正常的参数, 例如: ---template -> i+1 是 template 得具体值
// (3) 反参数
var arg = args[i]
var key
var next
if ((/^--.+=/).test(arg)) {
// 2.1 匹配前两个字符是--, 第三个字符是任意字符且连续出现多次, 接着是一个=号
// 例如: --template=vue({template: 'vue'}), ---temmplate==vue({'-temmplate': '=vue'})
// 测试命令: # yarn test:parse 2 false --template=vue ---temmplate==vue
var m = arg.match(/^--([^=]+)=([\s\S]*)$/) // -> ['--template=vue', 'template', 'vue']
key = m[1] // 键
var value = m[2] // 值
if(flags.bools[key]) { // 如果该key对应得bools为true -> 说明该key是Boolean类型
value = value !== 'false' // 若value为'false',则为false;若为'true',则为true;若为'template',则为true
}
setArg(key, value, arg)
} else if ((/^--no-.+/).test(arg)) {
// 2.2 匹配前五个字符是--no-, 第六个字符是任意字符且连续出现多次
// 例如: --no-use
key = arg.match(/^--no-(.+)/)[1] // ['--no-use', 'use'] -> {'use':false}
setArg(key, false, arg)
} else if ((/^--.+/).test(arg)) {
// 2.3 匹配前两个字符是--, 第三个字符是任意字符且连续出现多次
// 从 2.1到 2.3的顺序, 实际上限制范围是从小到大的
key = arg.match(/^--(.+)/)[1]
next = args[i + 1]
if (
next !== undefined // 下一个值是存在的
&& !(/^-/).test(next) // 且不是以-开头的字符串
&& !flags.bools[key] // 若 flags.bools[key] 为 true, 则表示该key对应值是布尔类型, 且为 true
&& !flags.allBools // flags.allBools 为 true 表示参数设置全是布尔类型, 指明是布尔类型, 默认是false?
&& (aliases[key] ? !aliasIsBoolean(key) : true) // aliasIsBoolean 若返回 true,则为false, 若返回false,则是true?
) {
setArg(key, next, arg);
i += 1; // 手动不经过下一个
} else if ((/^(true|false)/).test(next)) {
// 字符串true|false, 布尔类型
// 如果next值是"true"|"false", 因为命令行参数接收的是字符串数组
setArg(key, next === 'true', arg)
i += 1;
} else {
// next 不存在 | next 是以-开头的 | flags.bools[key] 为true | flags.allBools 为true | aliasIsBoolean(key) 为 true
// 所以要区分是否是字符串还是布尔类型
// console.log("setArg", key, flags.strings[key], flags.strings[key] ? '' : true)
// 字符串
// flags.strings[key] 若为 true, 表示是字符串, 给一个空字符
setArg(key, flags.strings[key] ? '' : true, arg)
}
} else if ((/^-[^-]+/).test(arg)) {
// 2.4 第一个字符是-, 第二个字符是非-且连续多个
// 这里做的处理也着实奇怪
// 匹配的主要是连续单个字符的默认值(可以连续设置多个), 而前面的都是一个参数值
// 例如: 可以匹配到 arg: -n1t2a3 -> {n:"1",t:"2",a:"3"}
var letters = arg.slice(1, -1).split('') // 除了首尾两个字符, 类型还是字符串
var broken = false
for(var j = 0; j < letters.length; j++) {
next = arg.slice(j + 2); // 从第j+3的位置开始截取直到结束, 取原arg参数的后面的所有字符
// -abc
if (next === "-") {
// arg:-t-, -> {t:"-"}
setArg(letters[j], next, arg)
continue // 为什么不是 break?程序走到这里说明这个for循环已经结束了呀,匹配的是arg:-t- -> 一次便结束了for循环, 故这里的continue和break是一样的效果
}
if ((/[A-Za-z]/).test(letters[j]) && (/[=]/).test(next)) {
// arg:-A=123 -> {'A': '123'}
// arg:-a==123 -> next:==123, next.split('='):['','','123'] -> {'a':''}
setArg(letters[j], next.split('=')[1], arg);
broken = true; // 结束了匹配
break
}
if (
(/[A-Za-z]/).test(letters[j])
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
// 问题: 竟然能匹配 next值为 "1t2a3b[]789" ??? ----- 这里并没有说明开头字符,而是限制了结束字符,故匹配到了
// reg.test(str) 检测一个字符串是否匹配某个模式
// 也能匹配到 -3, arg: -w-3 -> {w:-3}
// 当前遍历的字符是字母, 后面的所有字符组成的字符串
// /-?\d+(\.\d*)?(e-?\d+)?$/ 匹配的是数字 在JS里 10e1=10*10=100;10e3=10*1000=10000
// ?-> {0,1} 零次或1次
// +-> {1,n} 1次+
// *-> {0,n} 零次+
// -n5 -> {n:5}
// 匹配 10.2e3 -> 表示 10.2 乘以 10 的三次方即 10.2*1000=10200
// 匹配 10.2e2 -> 表示 10.2*100=1020
// 但若是 10.2e 就匹配不进来了, 因为后面的 e 一旦出来就要跟着一个数字
setArg(letters[j], next, arg);
broken = true; // 结束了匹配
// console.log("777", j, next)
break;
}
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
// letters[j + 1] 表示下一个字符
// \W 匹配的是符号(除了下划线和.字符)
setArg(letters[j], next, arg);
broken = true; // 结束了匹配
break;
} else {
// -abc -> a,b -> {a:true,b:true}
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
}
}
key = arg.slice(-1)[0]; // 拿到最后一个字符;为什么不直接使用 key = arg[arg.length-1] ? -- 作用都是一样的
if (!broken && key !== '-') {
// 结合整个参数 args 的下一个参数当作这个key的值
if (
args[i + 1]
&& !(/^(-|--)[^-]/).test(args[i + 1]) // 指明key
&& !flags.bools[key] // 布尔值
&& (aliases[key] ? !aliasIsBoolean(key) : true) // 这里也挺奇怪的:如果有了别名,但是没有给这别名任何一个参数值,则确实这个表达式值为true;但是这别名其中有一个是布尔值,但是并没有用到就会跑到 else ?
) {
// 下一个参数是一个正常的参数
setArg(key, args[i + 1], arg);
i += 1;
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
// 下一个参数是布尔值的字符串
setArg(key, args[i + 1] === 'true', arg);
i += 1;
} else {
// -abc -> c
// 这里挺奇怪的:如果给这个key设置了默认值default呢?为什么要自动赋默认值呢?
// 先 strings, 后 boolean, 且程序给的默认值是布尔值 true
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
} else {
// 2.5 其他情况
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg)); // 如果 strings._ 为 true, 可以直接用 arg, 而不用考虑如果是数字字符串则转为数字类型
}
// opts.stopEarly ??
if (opts.stopEarly) {
argv._.push.apply(argv._, args.slice(i + 1));
break;
}
}
}
// 2.2 处理 defaults 提供默认值的参数
// defaults 是 opts.defaults, 是对象类型 提供了默认的值
// 如果没有指定值则是取自提供的默认值
Object.keys(defaults).forEach(function (k) {
console.log("defautls:", k, hasKey(argv, k.split(".")))
// hasKey 来判断键 k 对应的是否有值(在已经生成的参数结果argv寻找)
if (!hasKey(argv, k.split("."))) {
// 例如: k.split(".")-> ['lsx', 'name']
// argv: {'lsx':{}}
// defaults[k] 取出键 k 对应的
setKey(argv, k.split("."), defaults[k]);
// 给别名配置参数
(aliases[k] || []).forEach(function (x) {
setKey(argv, x.split('.'), defaults[k]);
});
}
})
// 2.3 处理 opts["--"]
// opts: {"--": "1212"}
if (opts["--"]) {
argv["--"] = notFlags.slice(); // 如果允许存储--后的参数
} else {
// notFlags 好像是参数的一个分割, 后面的参数都推到_数组里
notFlags.forEach(function (k) {
argv._.push(k);
});
}
return argv
}