我是 𝑯𝒖𝒕𝒂𝒐. 一名前端工程师,正在往全栈发展的道路上,主要关注前端工程化、前端性能和体验等方向,近期对低代码、富文本编辑器等技术有浓厚兴趣
hutaod / blog Goto Github PK
View Code? Open in Web Editor NEWblog
License: MIT License
blog
License: MIT License
我是 𝑯𝒖𝒕𝒂𝒐. 一名前端工程师,正在往全栈发展的道路上,主要关注前端工程化、前端性能和体验等方向,近期对低代码、富文本编辑器等技术有浓厚兴趣
arg.js
源码解读最近在读 nextjs
源码,看到命令行参数解析工具用的 arg.js
,用法比较简单,源码也只有 100 多行,相对于command.js
来说,更加容易阅读和理解,于是便精读每一行代码,并试图了解其原理,本篇文章也主要用于解析arg.js
原理。
源码链接:arg github 地址
为何要写这一篇文章?原因在于:
arg.js
麻雀虽小五脏俱全,源码只有 100 来行,但实现了很多功能,用了很多技巧说明:
-
开头的,我称其为短命令-
开头的,我称其为长命令规则:
=
赋值或者空格赋值,而短命令只支持空格赋值-vvt
=== -v -v -t
Boolean
类型或者用flagSymbol
标识的类型都需要传递参数arg.js
仅包含一个函数,用于解析命令行参数,并返回一个包含解析命令的结果的对象
下面分析主要实现步骤:
定义一个 flagSymbol
的标志(标识了类型,后面叫做 flagSymbol 类型),用于给自定义类型添加一个标志,以便于在处理参数的时候分类进行处理。
const flagSymbol = Symbol('arg flag')
/**
* 解析命令行参数
* @param {Object} opts 预定义命令规则
* @param {{ argv = process.argv.slice(2), permissive = false, stopAtPositional = false }} options
* argv: 命令行参数,默认wei为process.argv.slice(2)
* permissive: 是否规避错误,默认有错误直接抛出异常
* stopAtPositional: 是否设置在第一个参数处停止解析
* @return {Object} result { _: [] } 解析成功的命令直接存入result作为属性,不规范命令放入`result._`数组
*/
function arg(
opts,
{
argv = process.argv.slice(2),
permissive = false,
stopAtPositional = false,
} = {}
) {
// 具体实现
}
// 仅仅用于给自定义类型添加一个标志
arg.flag = fn => {
fn[flagSymbol] = true
return fn
}
// 自定义类型实例,且标志为了flagSymbol类型
arg.COUNT = arg.flag((v, name, existingCount) => (existingCount || 0) + 1)
module.exports = arg
个人喜欢源码注释的方式进行讲解,内部实现过程上面已经简单描述了。下面直接上代码了:
function arg(
opts,
{
argv = process.argv.slice(2),
permissive = false,
stopAtPositional = false,
} = {}
) {
// 必须传入预定义命令规则,否则抛出异常
if (!opts) {
throw new Error('Argument specification object is required')
}
// 1. 定义返回对象
const result = { _: [] }
// 2. 处理预设置的命令、包含对命令设置的规则校验
const aliases = {} // 存放与handlers中存在的命令的key的对应关系,就是为了复用命令,类似webpack中配置alias,给已有命令添加别名。
const handlers = {} // 存放需要执行的命令。
for (const key of Object.keys(opts)) {
// 2.1 命令key不能为空字符串
if (!key) {
throw new TypeError('Argument key cannot be an empty string')
}
// 2.2 命令key必须以'-'开头
if (key[0] !== '-') {
throw new TypeError(
`Argument key must start with '-' but found: '${key}'`
)
}
// 2.3 命令key长度等于1时,代表只有'-',不允许这样的命令存在
if (key.length === 1) {
throw new TypeError(
`Argument key must have a name; singular '-' keys are not allowed: ${key}`
)
}
// 2.4 如果命令key对应的值是字符串,则放入aliases,并且代表不会是一个新的命令,用于复用以定义的命令,直接继续下一个循环,但并不意味着aliases的命令必须定义在handlers之后
if (typeof opts[key] === 'string') {
aliases[key] = opts[key]
continue
}
let type = opts[key] // 获取命令类型
let isFlag = false // 用于标识type是Boolean或者通过flagSymbol定义的类型
// 2.5 分类型进行设置isFlag
if (
Array.isArray(type) &&
type.length === 1 &&
typeof type[0] === 'function'
) {
// 处理类型为数组情况,比如 "--tag": [Number]
const [fn] = type
// 改写type,可以让命令行中有出现多次同一个命令的时候,把结果存入数组中进行保存
type = (value, name, prev = []) => {
prev.push(fn(value, name, prev[prev.length - 1]))
return prev
}
// 判断是Boolean类型还是通过flagSymbol定义的类型
isFlag = fn === Boolean || fn[flagSymbol] === true
} else if (typeof type === 'function') {
// 处理类型为function的情况
// 判断是Boolean类型还是通过flagSymbol定义的类型
isFlag = type === Boolean || type[flagSymbol] === true
} else {
// 类型必须是一个方法,否则抛出异常
throw new TypeError(
`Type missing or not a function or valid array type: ${key}`
)
}
// 2.6 当type长度大于2时,第二个参数如果不是'-',则type不符合规范
// 说明一下key的规范(也是这里限制的规范):单'-'后面只能有一个字符串,也就是最大长度为0
// 长度大于2,三个字符串以上的时候,第二个字符就必须为'-',否则就会在这里报错
if (key[1] !== '-' && key.length > 2) {
throw new TypeError(
`Short argument keys (with a single hyphen) must have only one character: ${key}`
)
}
// 2.7 把符合规范的命令以及isFlag存入handlers
handlers[key] = [type, isFlag]
}
// 3. 循环处理命令行传递过来的参数,并对参数添加校验
for (let i = 0, len = argv.length; i < len; i++) {
const wholeArg = argv[i]
// 3.1 当 stopAtPositional 为true 时,且result._已经存入了第一个参数,后面的参数就合并到result._中,并且跳出循环
// 也就是说:当 stopAtPositional 为true 时,命令行参数列表中间有一个不以'-'开头的参数,就会导致该参数后面的参数都不会被正确解析
if (stopAtPositional && result._.length > 0) {
result._ = result._.concat(argv.slice(i))
break
}
// 3.2 如果参数为'--' ,则不处理剩余参数,直接把后面的参数就合并到result._中,并终止循环
if (wholeArg === '--') {
result._ = result._.concat(argv.slice(i + 1))
break
}
// 3.3 判断参数是否合法(合法标志,第一个字符串必须为'-',且长度大于1)
if (wholeArg.length > 1 && wholeArg[0] === '-') {
// 在这里进行分类处理
// a. wholeArg[1] === '-' 说明是双'-'开头,表示长命令
// b. wholeArg.length === 2 表示短命令,因为只有短命令长度才会为2
// 也就是说长命令或者正常的短命令都符合a和b条件,separatedArguments值就为[wholeArg]
// 那什么情况下会走另外一种方式呢?答案:当短命令后面还跟有字符串的时候
// 这里就是让因为短命令支持短命令缩写,也就是,比如:`-v`可以写成`-vvvv`
// `-vvvv`会被解析成 ['-v','-v','-v','-v'],就会连续触发四次'-v',在type为arg.COUNT时有效果
// 比如`-vt` 会被解析成 ['-v','-t'],如果后面跟有参数,参数会被认为是最后一个命令的(这个后面进行处理)
const separatedArguments =
wholeArg[1] === '-' || wholeArg.length === 2
? [wholeArg]
: wholeArg
.slice(1)
.split('')
.map(a => `-${a}`)
// 正常来说separatedArguments的长度为1,只有短命令连续时缩写或者短命令用=赋值才会大于1(注意:短命令用=赋值是不允许的)
for (let j = 0; j < separatedArguments.length; j++) {
const arg = separatedArguments[j]
// 长命令会解析=,也就是在这里进行支持长命令`=`赋值,而短命令不支持`=`赋值,这也是因为短命令需要支持缩写与这个有冲突
// originalArgName 为命令行的key,argStr 为`=`后面部分
const [originalArgName, argStr] =
arg[1] === '-' ? arg.split(/=(.*)/, 2) : [arg, undefined]
let argName = originalArgName
// 判断argName是否在aliases中,在则让argName指向设置的对应命令,比如-n指向--name,这里就会把-n转换为--name
while (argName in aliases) {
argName = aliases[argName]
}
// 检查命令是否在预定义命令中,不在就会根据permissive的设置来进行处理
// permissive 为 false 抛出异常 ---默认
// permissive 为 true 终止当前命令处理,继续下一个命令解析
if (!(argName in handlers)) {
if (permissive) {
result._.push(arg)
continue
} else {
const err = new Error(
`Unknown or unexpected option: ${originalArgName}`
)
err.code = 'ARG_UNKNOWN_OPTION'
throw err
}
}
const [type, isFlag] = handlers[argName]
// 短命令时,如果不是Boolean类型或者flagSymbol类型,缩写时该命令不是最后一个命令就会抛出异常
// 分析什么情况下会抛出该异常,命令可以分为三种类型,
// a. Boolean 不需要参数,且isFlag为true,不会走到这里来
// b. flagSymbol类型,isFlag为true,不会走到这里来(暂时这是设定的,自定义类型只有arg.COUNT)
// c. 其他类型,其他类型时都需要传递参数,短命令其他类型也一样,再加上短命令只能通过`-n xxx`来进行赋值,所以,
// 如果其他类型这时候后面还跟有命令,就会走入判断条件,抛出异常
// 比如:`-vnv`,被解析成['-v', '-n', '-v'] 因为'-n'需要参数,就会抛出异常
if (!isFlag && j + 1 < separatedArguments.length) {
throw new TypeError(
`Option requires argument (but was followed by another short argument): ${originalArgName}`
)
}
// 如果是Boolean类型或者flagSymbol类型
if (isFlag) {
// 把命令行的key和val存入result中
// Boolean类型时,val 为true,只用到第2个参数
// flagSymbol类型 时,执行自定义的方法
// 比如 arg.COUNT就是每次读取之前的结果传入自定义函数进行累加,只用到第3个参数
result[argName] = type(true, argName, result[argName])
} else if (argStr === undefined) {
// 既不是Boolean类型或者flagSymbol类型时,就只有其他类型,
// argStr 为 undefined说明未用`=`赋值,这时argv数组中下一个参数就是当前参数的值
// 第一部分 argv.length < i + 2 是用来判断是否最后一个参数,是最后一个参数则进入判断条件内部
// 第二部分 用于判断下一个参数的正确性,判断下一个参数是一个命令而不是Number类型或者BigInt的情况,是一个命令则进入判断条件内部
if (
argv.length < i + 2 ||
(argv[i + 1].length > 1 &&
argv[i + 1][0] === '-' &&
!(
argv[i + 1].match(/^-?\d*(\.(?=\d))?\d*$/) &&
(type === Number ||
// eslint-disable-next-line no-undef
(typeof BigInt !== 'undefined' && type === BigInt))
))
) {
// 进入该判断条件内部,说明其他类型没有传递给命令有效的值,则抛出异常
// 根据正常命令错误还是alias命令出现的错误,抛出不同的异常错误提示
const extended =
originalArgName === argName ? '' : ` (alias for ${argName})`
throw new Error(
`Option requires argument: ${originalArgName}${extended}`
)
}
// 把argv数组中下一个参数赋值给当前命令
result[argName] = type(argv[i + 1], argName, result[argName])
// ++i的原因是因为下一个参数被赋值给了当前命令,就需要跳过下一个参数进行解析
++i
} else {
// 走到这里也只有其他类型,且argStr有值
// 这时直接把值传递给type,进行类型转换
result[argName] = type(argStr, argName, result[argName])
}
}
} else {
// 命令行不合法,比如第一参数第一个字符串不为'-',或者后面除了赋值的参数,有其他参数第一个字符串不为'-'的情况会走到这里来
result._.push(wholeArg)
}
}
return result
}
如果你正在学习 nodejs,或者正在编写 cli,希望本篇文章可以帮助到你。
如果有不对或者解释不合理的地方欢迎指出,谢谢!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.