CAC:源码仓库地址
目录说明
- CAC功能介绍
- 源码目录分析
- 程序流程图
- 执行流程举例分析
- FAQ
- 总结
CAC功能介绍
解析
使用CAC做参数解析
examples/basic-usage.js
展示帮助信息和版本
examples/help.js
Command-specific Options
可以附加options到命令行
const cli = require('cac')() cli .command('rm <dir>', 'Remove a dir') .option('-r, --recursive', 'Remove recursively') .action((dir, options) => { console.log('remove ' + dir + (options.recursive ? ' recursively' : '')) }) cli.help() cli.parse()
action回调函数第一个参数dir为
的参数,options为接受到 的如通过-r, --recursive附加的参数,如果在使用命令行时使用了unknown options,会报错。如果不定义action,传入unknown options不会报错,如果需要使用unknown options,请使用command.allowUnknownOptions
Option中的-
option中所有短横线属性都会被映射成对应小驼峰属性,比如:
--clear-screen和--clearScreen都对应options.clearScreen
括号
- command中的
<>为必选参数,[]为可选参数 - option中,
<>参数的值可以是字符串或者是数字,[]参数的值可以是字符串,数字,不传的时候值为boolean类型的true
注意:如果传入false,false会被转成字符串的false
Negated Options
- 如果需要一个option的值为
false,不是'false',我们需要手动指定一个no-xx的参数,比如: --no-clear-screen默认不传参数,clearScreen值为true--no-clear-screen false指定clearScreen为false
强调:如果我们需要指定一个值为
false,需要通过类-no-xx的参数去指定,通过xx false指定的false会转变为'false'
command中的可选参数
command里面的最后一个参数是可选的,类似es6的扩展运算符,比如:
.command('build <entry> [...otherFiles]', 'Build your app')node examples/variadic-arguments.js build a.js b.js c.js --fooa.js会被映射到entry, b.js c.js属于剩余参数,会被全部映射到otherFiles
点嵌套option
类似'--env.API_SECRET xxx',我们可以在env对象里面添加自定义属性,比如:
node examples/dot-nested-options.js build --env.foo foo --env.bar baroption.env={foo:"foo",bar:"bar"}
默认Command
当未匹配到Command时,使用默认Command。
在CAC的parse方法中,可以看到name不存在时,会尝试获取执行命令的第二个参数,否则name默认为cli
if (!this.name) {
this.name = argv[1] ? getFileName(argv[1]) : 'cli'
}
数组作为option参数
node cli.js --include project-a --include project-b
# option{ include: ['project-a', 'project-b'] }
错误处理
使用try..catch
文件目录分析
- examples。用于存放代码例子文件,可直接命令行运行。对应
- scripts。存放脚本文件
- readme中所举的功能实例
- src。存放源码实现文件和测试文件
- test。存放测试文件
- .editorconfig。帮助开发人员在不同的编辑器和IDE之间定义和维护一致的编码样式
- .gitattributes
- .gitignore git提交忽略文件
- .prettierrc prettier配置文件
- circle.yml
- index-compat.js
- jest.config.js jest测试配置文件
- LICENSE 开源协议
- mod_test.ts
- package.json npm配置文件
- rollup.config.js roollup打包配置文件
- tsconfig.json ts配置文件
- yarn.lock
程序流程图
执行流程举例分析
// 实例化CAC实例cli
// 示例化GlobalCommand,globalCommand继承自Command
const cli = require('../src/index').cac()
cli
// 实例化Command,将globalCommand挂在command.globalCommand上,并将其存储在CAC实例的commands上,返回command实例本身,以=实现链式调用
.command('build', 'desc')
// 实例化Option,并将解析好的option存储在CAC实例的options上,返回command实例,实现链式调用
// '--env <env>'解析为option对象,option中属性为选项name为'env',required为true,negated为false,该option属性名称为env,是必选参数,不是no-参数
.option('--env <env>', 'Set envs')
// '--foo-bar <value>'解析为option对象,name为fooBar,required为true,negated为false
.option('--foo-bar <value>', 'Set foo bar')
// 存储'--env.API_SECRET xxx'至command的examples,提示我们env可以传入属性,可以是一个对象,返回command实例,实现链式调用
.example('--env.API_SECRET xxx')
// 将回调函数存储在command实例上commandAction,以便后续调用
.action((options) => {
console.log(options)
})
// 当执行命令出现-h, --help时展示help信息
cli.help()
// 解析参数
cli.parse()
new Command
实例化Command。定义command的规则,能够接受什么命令,命令名称是什么,带什么样的参数,参数名称是什么,是必选参数,还是可选参数,还是不确定的多个参数
this.options = []
this.aliasNames = []
// 去掉尖括号或者方括号后面的内容,获取command的名称。比如rawName为build <cli>,name会被处理为build
// 这里传入build,name为build
this.name = removeBrackets(rawName)
// 找到所有的括号,解析为一个数组,每个数组中包含三个属性,required是否必选,由是否是尖括号决定,value属性名称,variadic,包含...为variadic,可包括多个不确定的参数,最后一个参数才可以是多选参数
// rawName为build,args为空
this.args = findAllBrackets(rawName)
this.examples = []
new Option
实例化Option。记录选项参数的规则,选项参数的名称,是否可选,必选,多选参数。--a-b的参数会被转换为aB小驼峰形式。
this.config = Object.assign({}, config)
// 因为可能存在 '--env.* [value]'的情况,将参数放在env的属性里面。替换掉`.*`,留下--env[value]
// You may use cli.option('--env.* [value]', 'desc') to denote a dot-nested option
rawName = rawName.replace(/\.\*/g, '')
this.negated = false
// 去掉括号后的内容
this.names = removeBrackets(rawName)
.split(',')
.map((v: string) => {
// 去掉--或者-
let name = v.trim().replace(/^-{1,2}/, '')
if (name.startsWith('no-')) {
this.negated = true
// 去掉开头的no-
name = name.replace(/^no-/, '')
}
// 转换为小驼峰
return camelcaseOptionName(name)
})
// 长的放后面。如:'-h,--help'
.sort((a, b) => (a.length > b.length ? 1 : -1)) // Sort names
// 取最长的。取到help
// Use the longest name (last one) as actual option name
this.name = this.names[this.names.length - 1]
if (this.negated && this.config.default == null) {
this.config.default = true
}
if (rawName.includes('<')) {
// 尖括号判断
this.required = true
} else if (rawName.includes('[')) {
// 方括号判断
this.required = false
} else {
// No arg needed, it's boolean flag
// 没有参数时,默认为布尔值true
this.isBoolean = true
}
cli.help()
在globalCommand中实例化-h, --help的option规则,可传入回调,标记showHelpOnExit为true。后续在parse时会将全局option和各个子command的option进行数组合并,所以全局option可以得到处理。
help(callback?: HelpCallback) {
// 解析'-h, --help'存储在globalCommand的options中
this.globalCommand的options中.option('-h, --help', 'Display this message')
// 存储helpCallback
this.globalCommand.helpCallback = callback
// 标记showHelpOnExit为true
this.showHelpOnExit = true
// 返回
return this
}
cli.parse()
接受命令行参数,同command和option进行匹配对比
parse(
argv = processArgs,
{
/** Whether to run the action for matched command */
run = true,
} = {}
): ParsedArgv {
this.rawArgs = argv
// 在new CAC时如果有传递,则等于注册的command名称,或者获取argv第二个参数作为名称
if (!this.name) {
this.name = argv[1] ? getFileName(argv[1]) : 'cli'
}
let shouldParse = true
// Search sub-commands
// 遍历已注册的command
for (const command of this.commands) {
// 获取到解析后的命令args的值和option的值
const parsed = this.mri(argv.slice(2), command)
// 取第一个参数为command名称
const commandName = parsed.args[0]
// 和提前注册好的command的名称做对比,如果能匹配上
if (command.isMatched(commandName)) {
shouldParse = false
// 截取args名称后面的参数
const parsedInfo = {
...parsed,
args: parsed.args.slice(1),
}
// 将解析好args,option,command,commandName存储在CAC,方便后续返回
this.setParsedInfo(parsedInfo, command, commandName)
// 分发通知
this.emit(`command:${commandName}`, command)
}
}
if (shouldParse) {
// 如有默认command执行默认command匹配解析逻辑
// Search the default command
for (const command of this.commands) {
if (command.name === '') {
shouldParse = false
const parsed = this.mri(argv.slice(2), command)
this.setParsedInfo(parsed, command)
this.emit(`command:!`, command)
}
}
}
if (shouldParse) {
// 最后,执行默认解析和存储
const parsed = this.mri(argv.slice(2))
this.setParsedInfo(parsed)
}
// 如果option传入--help -h 则打印帮助信息
if (this.options.help && this.showHelpOnExit) {
this.outputHelp()
run = false
this.unsetMatchedCommand()
}
// 如果option传入--version -v 则打印版本信息
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
this.outputVersion()
run = false
this.unsetMatchedCommand()
}
const parsedArgv = { args: this.args, options: this.options }
if (run) {
// 执行注册好的对应command的action
this.runMatchedCommand()
}
if (!this.matchedCommand && this.args[0]) {
// 未匹配到,分发通知
this.emit('command:*')
}
// 返回解析好的args和options
return parsedArgv
}
CAC.mri
获取解析完成的命令args参数值和option参数值,核心是需要通过mri库进行转换,额外功能添加了嵌套属性支持和类型配置转换支持。
private mri(
argv: string[],
/** Matched command */ command?: Command
): ParsedArgv {
// All added options
// 合并全局option
const cliOptions = [ ...this.globalCommand.options, ...(command ? command.options : []),
]
// 获取别名和布尔配置
const mriOptions = getMriOptions(cliOptions)
// Extract everything after `--` since mri doesn't support it
let argsAfterDoubleDashes: string[] = []
const doubleDashesIndex = argv.indexOf('--')
if (doubleDashesIndex > -1) {
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1)
argv = argv.slice(0, doubleDashesIndex)
}
// 通过mri解析参数
let parsed = mri(argv, mriOptions)
parsed = Object.keys(parsed).reduce(
(res, name) => {
return {
...res,
// 转换成小驼峰
[camelcaseOptionName(name)]: parsed[name],
}
},
{ _: [] }
)
const args = parsed._
const options: { [k: string]: any } = {
'--': argsAfterDoubleDashes,
}
// Set option default value
const ignoreDefault =
command && command.config.ignoreOptionDefaultValue
? command.config.ignoreOptionDefaultValue
: this.globalCommand.config.ignoreOptionDefaultValue
let transforms = Object.create(null)
for (const cliOption of cliOptions) {
if (!ignoreDefault && cliOption.config.default !== undefined) {
for (const name of cliOption.names) {
options[name] = cliOption.config.default
}
}
// If options type is defined
// 如果option定义了类型,则将类型shouldTransform置为true,存储transformFunction为转换函数
if (Array.isArray(cliOption.config.type)) {
if (transforms[cliOption.name] === undefined) {
transforms[cliOption.name] = Object.create(null)
transforms[cliOption.name]['shouldTransform'] = true
transforms[cliOption.name]['transformFunction'] =
cliOption.config.type[0]
}
}
}
// Set option values (support dot-nested property name)
for (const key of Object.keys(parsed)) {
if (key !== '_') {
const keys = key.split('.')
// 支持属性嵌套
setDotProp(options, keys, parsed[key])
// 执行参数类型转换
setByType(options, transforms)
}
}
return {
args,
options,
}
}
setDotProp
构造嵌套属性
export const setDotProp = (
obj: { [k: string]: any },
keys: string[],
val: any
) => {
let i = 0
let length = keys.length
let t = obj
let x
for (; i < length; ++i) {
x = t[keys[i]]
t = t[keys[i]] =
i === length - 1
? val
: x != null
? x
: !!~keys[i + 1].indexOf('.') || !(+keys[i + 1] > -1)
? {}
: []
}
}
setByType
类型转换
export const setByType = (
obj: { [k: string]: any },
transforms: { [k: string]: any }
) => {
for (const key of Object.keys(transforms)) {
const transform = transforms[key]
if (transform.shouldTransform) {
obj[key] = Array.prototype.concat.call([], obj[key])
if (typeof transform.transformFunction === 'function') {
obj[key] = obj[key].map(transform.transformFunction)
}
}
}
}
FAQ
Brackets 应该如何使用
方括号和尖括号?
- 方括号为可选参数,尖括号为必选参数。
- option中,
<>参数的值可以是字符串或者是数字,[]参数的值可以是字符串,数字,不传的时候值为boolean类型的true
Negated Options 是如何实现的?
Option类中constructor函数,将rawName中括号去掉后,对name进行遍历,如果name是以no-开头的,则将name替换成不包含no-的的name,比如'no-config'对应options中的config参数。并且将option.negated=true,以便后续option在其他地方被使用
this.names = removeBrackets(rawName)
.split(',')
.map((v: string) => {
let name = v.trim().replace(/^-{1,2}/, '')
if (name.startsWith('no-')) {
this.negated = true
name = name.replace(/^no-/, '')
}
return camelcaseOptionName(name)
})
.sort((a, b) => (a.length > b.length ? 1 : -1))
如何实现连续调用的api
cli
// 返回command实例
.command('cook <...food>', 'Cook some good')
// 返回command实例
.option('--bar <bar>', 'Bar is a boolean option')
// 返回command实例
.action((food, options) => {
console.log(food, options)
})
如何理解command
- 执行命令的对象
- 包含了命令的名称,参数,参数的性质
- 我们可以用于自定义command
如何理解option
- option参数。
- 收集了option对象的属性和值,支持了多种类型
- 可以用于自定义option方便后续程序处理
如何理解action
- action可以简单理解为当这个command被执行的回调
- 通过command.action收集一个回调函数,存储在对应command,当parse解析命令行参数,匹配中该command时,会执行这个command.action
总结
- CAC就是一个帮助你解析命令行参数的库。
- 我们可以根据已有规则定义我们的自定义命令command以及选项参数options,在命中定义好的command后会帮我们将参数和值解析好输出,action就是命中以及解析的回调,在action里面我们拿到args和options可以做我们后续想做的其他处理。