解析命令行参数第三方包之minimist

2,152 阅读7分钟

一、前言

本篇文章记录了 解析命令行参数包: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

image.png

可知,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; 指定参数的默认值;
  • ……

minimist 参数详细介绍:

image.png

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 正则条件的限制范围是由小变大, 个人觉得这块的逻辑处理的十分细腻, 是解析命令行参数的精髓所在~

图示: image.png

(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
}