一、minimist的主要功能和用途
minimist 是一个专为 Node.js 设计的轻量级命令行参数解析库,作用是将原始的命令行输入(如 process.argv 数组)转化为结构化的 JavaScript 对象,大幅简化参数处理的复杂度。
主要功能
-
参数结构化与类型推断
- 自动解析:将命令行参数转换为对象
- 特殊字段 _:存储所有未被解析的命令行参数
- 智能类型转换:
- 布尔值:–debug → debug: true(无值默认 true)
- 数值:–port=8080 → port: 8080(自动转为数字)
- 字符串:通过配置强制保留类型
例1: node .\minimist.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz --debug -b [1,2,3]
-
多平台兼容性
- 统一处理不同操作系统的命令行格式差异(如 Windows 与 Unix 的短选项语法) -
类型强控:通过配置,可以强制要求某些参数为特定类型(如数字或布尔值),避免类型错误
-
参数别名:可以为命令行参数设置别名,使得用户可以通过不同的缩写调用同一参数
-
安全屏障:可以设置安全屏障(unknown函数),拦截未知的命令行参数,防止潜在的安全风险
用途
- CLI 工具开发
- 快速构建脚手架(如生成项目模板),解析用户输入的配置(如 --template=react)
- 被 gulp-cli、webpack-cli 等工具用作底层引擎
- 自动化脚本
- 处理构建参数(如 --watch 启用监听模式,–env=production 指定环境)
- 控制脚本行为,减少手动解析 process.argv 的繁琐代码
- 配置参数管理
- 可以将一些配置参数通过命令行传入,然后使用 minimist 进行解析,从而实现对程序或脚本的灵活配置,而无需修改代码中的硬编码配置。
二、minimist配置项
配置选项(最新配置参考minimist)
- 字符串(string): 指定哪些参数应该被当作字符串处理
- 示例: x 参数被指定为字符串前后输出对比
- 示例: x 参数被指定为字符串前后输出对比
- 布尔(boolean): 指定哪些参数应该被当作布尔处理
- 示例: x 参数被指定为布尔前后输出对比
- 示例: x 参数被指定为布尔前后输出对比
- 别名(Alias): 可以使不同名称指向同一参数
- 示例:设置 -h 的别名为 --help
- 示例:设置 -h 的别名为 --help
- 默认值(Default):为缺失参数提供兜底值
- 示例: 设置env默认值为dev
- 未知参数处理(unknwon): 遇到未知参数时调用,并且函数返回false,参数不会添加到argv中
- 示例: 所有输入参数都过滤掉
- 示例: 所有输入参数都过滤掉
- stopEarly:遇到第一个非选项参数(不以 - 或 – 开头的参数)后,会停止解析剩余的所有参数,将它们全部放入 argv._ 数组中
- 示例:node .\index.js --watch --verbose file1 file2 file3
- - - : 当为 true 时,将双连字符 – 后面的所有参数收集到一个专门的 argv[‘–’] 数组中,而不是默认的 argv._ 数组中
- 示例:node .\index.js --watch – file1 file2 file3,配置{‘–’: true}
- 示例: 设置env默认值为dev
三、源码分析
1. 代码初始化
module.exports = function (args, opts) {
if (!opts) { opts = {}; } // 没传入opts,则默认空对象
// 初始化标志(flags)对象
var flags = {
bools: {},// 用于存储布尔类型的选项
strings: {},// 用于存储字符串类型的选项
unknownFn: null, // 用于存储处理未知选项的函数
};
// 处理未知选项函数
if (typeof opts.unknown === 'function') {
flags.unknownFn = opts.unknown; // 把传入的unknown赋值给unknownFn
}
// 处理布尔选项
if (typeof opts.boolean === 'boolean' && opts.boolean) {
flags.allBools = true;
} else {
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
flags.bools[key] = true;
});
}
// 处理默认值
var defaults = opts.default || {};
var argv = { _: [] };
var notFlags = [];
...
...
}
处理布尔选项
- opts.boolean 类型为布尔且为 true
- 当用户配置中boolean选项设为true时,allBools 选项设置为true。并且在后续argDefined(功能:判断给定的值是否被定义)函数中会使用到。
- 默认行为
- 使用filter(Boolean) 过滤掉数组中所有非真值的元素,然后对每个元素(选项名)作为键,值设置为 true,并存到bools中
2. 别名映射构建
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; // 排除当前别名自身
}));
});
});
什么是双向别名映射?
双向别名映射(Bidirectional Alias Mapping)是一种数据结构设计模式,它在键和值之间建立双向关联关系,允许通过任意一方高效地查找另一方。在命令行参数解析等场景中,它用于处理主参数名和其别名之间的复杂关系。
基本概念
- 主键(Primary Key):参数的规范名称(如 --help)
- 别名(Alias):参数的简写或替代名称(如 -h)
- 映射关系:主键 ↔ 别名(双向可逆)
双向别名映射特点
- 双向可逆性
- 正向查找:通过主键查找所有别名
- 反向查找:通过别名查找主键
- 关联完整性
- 修改主键的别名时,所有相关别名的映射自动更新
- 添加/删除别名时,所有相关条目保持同步
- 多级关联
- 别名不仅关联主键,还关联同一主键下的其他别名
- 高效查询
- 时间复杂度:O(1) 的直接访问
- 空间换时间策略:预计算所有关系,避免运行时计算
示例:对 help和version设置别名
alias: {
help: 'h',
version: [ 'v', 'V' ]
}
经过双向别名映射,结果如下
3. 处理标记被解析为字符串类型的参数及其别名
[].concat(opts.string).filter(Boolean).forEach(function (key) {
flags.strings[key] = true; // 向flags中strings对象中推入key并标记为true
if (aliases[key]) {// 检查别名映射
// 获取双向别名系统中key对应的别名,并循环推入strings对象中
[].concat(aliases[key]).forEach(function (k) {
flags.strings[k] = true;
});
}
});
示例:配置string选项,string选项中的值同时在别名中出现
console.log(minimist(process.argv.slice(2), {
string: ['x','help'],
alias: {
help: 'h',
version: [ 'v', 'V' ]
}
}))
别名中help和h被放入strings对象中,表示它们被解析为字符串,而不是boolean
4. 为所有布尔参数设置默认值
Object.keys(flags.bools).forEach(function (key) {
setArg(key, defaults[key] === undefined ? false : defaults[key]);
});
5. setArg 函数及其相关函数分析
setArg 负责将命令行参数解析并存储到结果对象(argv)中,其中setArg 函数中会涉及到其他函数,在下面分析。
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;
// 向argv中设置值
setKey(argv, key.split('.'), value);
// 循环别名,为所有别名设置相同的值
(aliases[key] || []).forEach(function (x) {
setKey(argv, x.split('.'), value);
});
}
argDefined函数
函数用来判断参数是否一下四方面有定义(全局布尔、字符串类型、布尔类型和别名)
function argDefined(key, arg) {
return (flags.allBools && (/^--[^=]+$/).test(arg))
|| flags.strings[key]
|| flags.bools[key]
|| aliases[key];
}
类型转换
var value = !flags.strings[key] && isNumber(val)
? Number(val)
: val;
首先会检查参数是否未标记为字符串类型,也就是说字符串的优先级很高,如果在其他地方的配置和字符串中的配置有冲突还是以字符串为准
示例: x参数配置在boolean和string中
x参数配置在boolean和string中,虽然x在两个配置中,但是最后返回的还是字符串类型
在满足值不在strings中后,检查值是否为数字格式(使用 isNumber 函数,分析如下)。是就返回数值或者按照原值返回
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);
}
十进制数字检测正则表达式分析
- 符号部分:
- [-+]?:可选的正负号(+ 或 -)
- 数字主体:
- (?:\d+(?:.\d*)?|.\d+): 匹配整数、小数或纯小数
- 分支 1:\d+(?:.\d*)?
- \d+:1个或多个数字(整数部分)
- (?:.\d*)?:可选的小数部分
- 分支 2:.\d+
- .\d+:纯小数形式
- 必须有小数点且至少一位小数
- 分支 1:\d+(?:.\d*)?
- (?:\d+(?:.\d*)?|.\d+): 匹配整数、小数或纯小数
- 科学计数法:
- (e[-+]?\d+)?: 可选的指数部分(e后跟整数)
参数设置
将参数值设置到结果对象
setKey(argv, key.split('.'), value);
setKey 函数分析
function isConstructorOrProto(obj, key) {
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
}
function setKey(obj, keys, value) {
var o = obj;
// 处理嵌套对象
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];
}
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];
}
}
别名同步
为所有别名设置相同的值
(aliases[key] || []).forEach(function (x) {
setKey(argv, x.split('.'), value);
});
6. - - 分隔处理分析
// 检查参数数组中是否存在 --
if (args.indexOf('--') !== -1) {
notFlags = args.slice(args.indexOf('--') + 1); // 获取 -- 之后的所有参数,存入notFlag
args = args.slice(0, args.indexOf('--')); // 保留 -- 之前的所有参数,存入args用于后续解析
}
7. args解析分析
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var key;
var next;
if ((/^--.+=/).test(arg)) {
// Using [\s\S] instead of . because js doesn't support the
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
key = m[1];
var value = m[2];
if (flags.bools[key]) {
value = value !== 'false';
}
setArg(key, value, arg);
} else if ((/^--no-.+/).test(arg)) {
key = arg.match(/^--no-(.+)/)[1];
setArg(key, false, arg);
} else if ((/^--.+/).test(arg)) {
key = arg.match(/^--(.+)/)[1];
next = args[i + 1];
if (
next !== undefined
&& !(/^(-|--)[^-]/).test(next)
&& !flags.bools[key]
&& !flags.allBools
&& (aliases[key] ? !aliasIsBoolean(key) : true)
) {
setArg(key, next, arg);
i += 1;
} else if ((/^(true|false)$/).test(next)) {
setArg(key, next === 'true', arg);
i += 1;
} else {
setArg(key, flags.strings[key] ? '' : true, arg);
}
} else if ((/^-[^-]+/).test(arg)) {
var letters = arg.slice(1, -1).split('');
var broken = false;
for (var j = 0; j < letters.length; j++) {
next = arg.slice(j + 2);
if (next === '-') {
setArg(letters[j], next, arg);
continue;
}
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
setArg(letters[j], next.slice(1), arg);
broken = true;
break;
}
if (
(/[A-Za-z]/).test(letters[j])
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
setArg(letters[j], next, arg);
broken = true;
break;
}
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
} else {
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
}
}
key = arg.slice(-1)[0];
if (!broken && key !== '-') {
if (
args[i + 1]
&& !(/^(-|--)[^-]/).test(args[i + 1])
&& !flags.bools[key]
&& (aliases[key] ? !aliasIsBoolean(key) : true)
) {
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 {
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
} else {
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
}
if (opts.stopEarly) {
argv._.push.apply(argv._, args.slice(i + 1));
break;
}
}
}
循环每一个args值
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var key;
var next;
...
...
}
处理长选项带等号(格式:–key=value)
if ((/^--.+=/).test(arg)) {
// Using [\s\S] instead of . because js doesn't support the
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
key = m[1];
var value = m[2];
if (flags.bools[key]) {
value = value !== 'false';
}
setArg(key, value, arg);
}
正则表达式^–([^=]+)=([\s\S]*)$,匹配‘–key=value’该格式内容,通过匹配获取到key和value。紧接着处理布尔值和向argv中添加键值
处理长选项否定形式 (–no-key)
else if ((/^--no-.+/).test(arg)) {
key = arg.match(/^--no-(.+)/)[1];
setArg(key, false, arg);
}
对于–no-key类型的解析,会被强制设置为false
处理长选项不带等号 (–key)
else if ((/^--.+/).test(arg)) {
key = arg.match(/^--(.+)/)[1]; // 获取key
next = args[i + 1]; // 获取value
// 判断值是否可以当作值
if (
next !== undefined
&& !(/^(-|--)[^-]/).test(next) // 判断值是不是选项
&& !flags.bools[key] // 判断当前选项是不是被设置为布尔类型
&& !flags.allBools // 是否开启全局布尔模式
&& (aliases[key] ? !aliasIsBoolean(key) : true) // 别名也不是布尔类型
) {
setArg(key, next, arg);
i += 1;
// 值是字面量布尔值
} else if ((/^(true|false)$/).test(next)) {
setArg(key, next === 'true', arg);
i += 1;
} else { // 默认处理
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
短选项处理 (-key)
else if ((/^-[^-]+/).test(arg)) {
// 获取除首尾字符外的所有字母
var letters = arg.slice(1, -1).split('');
var broken = false;
for (var j = 0; j < letters.length; j++) {
next = arg.slice(j + 2);
// 一个单独的短横线(-key-)
if (next === '-') {
setArg(letters[j], next, arg);
continue;
}
// 等号赋值格式(-key=value)
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
setArg(letters[j], next.slice(1), arg);
broken = true;
break;
}
// 数字格式值(-key123)
if (
(/[A-Za-z]/).test(letters[j])
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
setArg(letters[j], next, arg);
broken = true;
break;
}
// 特殊字符分隔的值(-key@)
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
} else {
// 默认布尔值(-key)
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
}
}
// 处理最后一个字符
key = arg.slice(-1)[0];
if (!broken && key !== '-') {
if (
args[i + 1] // 下一个参数存在
&& !(/^(-|--)[^-]/).test(args[i + 1]) // 不是选项参数
&& !flags.bools[key] // 判断当前选项是不是被设置为布尔类型
&& (aliases[key] ? !aliasIsBoolean(key) : true) // 别名也不是布尔类型
) {
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 {
// 设置默认值
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
}
- 一个单独的短横线(-key-)
功能:将单独的 - 字符作为值(因为如果-符号后面跟数字会把-符号当成负号)
if (next === '-') {
setArg(letters[j], next, arg);
continue;
}
2. 等号赋值格式(-key=value)
功能:将等号后面字符作为值
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
setArg(letters[j], next.slice(1), arg);
broken = true;
break;
}
3. 数字格式值(-key123)
功能:将key后面的字符作为值
if (
(/[A-Za-z]/).test(letters[j]) &&
(/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
setArg(letters[j], next, arg);
broken = true;
break;
}
4. 特殊字符分隔的值(-key@)
功能:包括特殊字符在内的后续字符作为值
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
}
5. 默认布尔值 (-key)
功能:字符串类型选项的值设置’',其余默认为true
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
6. 处理最后一个字符
分为三种情况
1. 参数存在并可以被作为值需要满足3个条件:(一)、不是以 - 或 – 开头的选项(二)、当前选项未声明为布尔类型(三)、别名(如果存在)也不是布尔类型
示例:node .\index.js -p 8080 -f file.txt
2. 参数是 “true” 或 “false”
示例:node .\index.js -v true -d false
3. 上面两个条件不满足,就走默认值
示例:node .\index.js -p
非选项参数处理
下面代码处理非选项参数(即不以 - 或 – 开头的参数)的逻辑,负责将这些参数收集到结果对象的 _ 数组中
else {
// 允许未知参数或通过未知参数检查
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
}
// 启用 stopEarly 模式
if (opts.stopEarly) {
argv._.push.apply(argv._, args.slice(i + 1));
break;
}
}
stopEarly 模式:开启模式后,遇到第一个非选项参数后,停止解析剩余参数,把所有剩余参数放入 _ 数组
8. 默认值处理
function hasKey(obj, keys) {
var o = obj;
keys.slice(0, -1).forEach(function (key) {
o = o[key] || {};
});
var key = keys[keys.length - 1];
return key in o;
}
Object.keys(defaults).forEach(function (k) {
// 检查参数是否已存在
if (!hasKey(argv, k.split('.'))) {
// 设置主选项默认值
setKey(argv, k.split('.'), defaults[k]);
// 设置别名默认值
(aliases[k] || []).forEach(function (x) {
// 为每个主选项的所有别名设置相同默认值
setKey(argv, x.split('.'), defaults[k]);
});
}
});
k.split(‘.’) 函数是对嵌套路径处理(例如:a.b.c)
hasKey函数执行分析
9. 处理已配置‘- -’选项
if (opts['--']) { // 启用‘--’配置
argv['--'] = notFlags.slice();// 把notFlags中的值放入‘- -’ 中
} else {
notFlags.forEach(function (k) {
argv._.push(k); // 否者推入_数组中
});
}
10. 返回argv
return argv;