commander使用教程

619 阅读11分钟

一、作用

  • 作用:提供了用户命令行输入和参数解析强大功能(人话:强大解析process.argv功能及自动生成help)
  • 写作缘由:npm官网案例不多,且看完只知道api的大概使用规则,不好快速理解起概念,这边来理顺

二、分析命令行结构结构

// 命令行结构:
// 我们命令行是不是输入 ad asd zxczc sadasd adsf 类似结构的字符
// 然后我们可以把命令行 看成 多组 输入单元(对应commander的多个解析单元)
// 每组输入单元,我们理解为以下下面结构
  • [命令名] [选项名] [参数]
// 把选项拎出来理解命令行结构
  1. 以上作为一个输入单元,[]代表都是可选的
  2. 👇来分析一些结构
  3. git add . // 子命令add;子命令值'.'
  4. git commit -m 'feat: 完成101迭代ui' // 子命令commit;选项 -m;选项值 "feat: 完成101迭代u"
  5. ⬆️ 最后会完成git的操作命令

三、分析commander要做什么

  • 既然主要作用是解析命令行参数,然后doSomeThing,及生成友好的help提示
  • 我们由上可知,细化颗粒有:命令、选项、参数、及捕获相关的、及描述(用于提示)
  • command: 用于注册子命令(主代码其实就是主命令了),及该命令的描述
  • option: 用于注册选项及接收参数,及该选项的描述
  • version:用于标记当前命令的版本(可以理解为简化版选项)
  • arguments: 用于定义当前命令的接收参数
  • description:用于描述当前命令及该命令参数的描述
  • => 也就是我们可以由以上注册单元来描述我们如何去解析命令行(正则解析)
  • => commander根据我们的写的解析注册单元来 生成 相应的ast树(有子命令,即对应ast子节点)
  • => 需要注意的是因为选项
  • 以下,会重点分析各个注册解析单元怎么玩

四、命令

  • 我们的代码其实就是主命令
  • 这个命令的回调是注册一个action(理解主命令)
program
.action((t) => {
    console.log('top action call',t)
})
.parse(process.argv);

// 输入:node test
// 输出:top action call
// 输入:node test --help
// 观察
  • 注册一个子命令(理解子命令)
program
.action((t) => {
    console.log('top action call')
})
// 注意,command返回的是子命令对象,而非主命令
// 所以后续的.是在配置子命令对象
.command('cmd1')
.action((t) => {
    console.log('child1 action call')
})
// 从主命令这边解析,所以不是.parse。而是program.parse。
// 当然,如果我们也可以从某个子命令那里开始解析,但不推荐
program.parse(process.argv); 

// 说明:command语法结构:command('命令名 参数1 参数2 参数3','描述')
// 也就是支持多个参数,关于参数的使用下面会有案例分析
// 对于参数,和正常js函数的参数一样理解就行了
// <参数> : 代表这个参数是required的,需要在可选参数之前
// [参数] : 代表这个参数是可选的
// <参数...> 或 [参数...] : 和es6...一样理解就行了

// 输入:node test
// 输出:top action call
// 输入:node test cmd1
// 输出:child1 action call
// 说明:此时匹配的是子命令,而不是主命令,所以主命令回调不会执行
  • 注册两个子命令,及后代命令(理解子命令)
program
.action((t) => {
    console.log('top action call')
})
.command('cmd1')
.action((t) => {
    console.log('child1 action call')
})
.command('cmd11')
.action((t) => {
    console.log('child11 action call')
})

program
.command('cmd2')
.action((t) => {
    console.log('child2 action call')
})
program.parse(process.argv);

// 结构为:
// 主命令
//   --cmd1
//     --cmd11
//   --cmd2

// 输入:node test
// 输出:top action call
// 输入:node test cmd1
// 输出:child1 action call
// 输入:node test cmd1 cmd11
// 输出:child11 action call
// 输入:node test cmd2
// 输出:child2 action call

// 观察
// 输入:node test --help
// 观察:commands: cmd1 cmd2
// 输入:node test cmd1 --help
// 观察:commands: cmd11
  • 命令 + 参数(理解参数的必传、选传、和...及action回调参数的值)
program
.command('cmd1 <arg1> [arg2] [arg3...]')
.action((...t) => {
    console.log('child1 action call',t)
})
program.parse(process.argv); 

// 说明:参数的概念前面有描述
// 这边重点介绍下action的参数:
// 自己可以输出t看结构:
// [arg1,arg2,arg3,{},currentCommandRef]
// 如果第一的时候是两个参数,则为
// [arg1,arg2,{},currentCommandRef]

// 输入:node test cmd1
// 输出:missing required argument 'arg1'
// 输入:node test 1
// 输出:child1 action call ['1',undefined,[],{},currentCommandRef]
// 输入:node test 1 2 3 4
// 输出:child1 action call ['1','2',['3','4'],{},currentCommandRef]
  • 命令 + 配置({isDefault:true})
program
.command('cmd1 <arg1> [arg2] [arg3...]',{isDefault:true})
.action((...t) => {
    console.log('child1 action call',t)
})
program.parse(process.argv); 

// 输入:node test 1
// 输出:child1 action call ['1',undefined,[],{},currentCommandRef]
// 说明:也就是isdefault:会配置当前命令为当前层级的默认命令
// 再说一遍:当前层级

// 案例二
program
.action((t) => {
    console.log('top action call')
})
.command('cmd1')
.action((t) => {
    console.log('child1 action call')
})
.command('cmd11',{isDefault:true})
.action((t) => {
    console.log('child11 action call')
})
program.parse(process.argv);

// 输入:node test
// 输出:top action call
// 输入:node test cmd1
// 输出:child11 action call

// 案例三
program
.action((t) => {
    console.log('top action call')
})
.command('cmd1',{isDefault:true})
.action((t) => {
    console.log('child1 action call')
})
.command('cmd11',{isDefault:true})
.action((t) => {
    console.log('child11 action call')
})
program.parse(process.argv); 

// 输入:node test
// 输出:child11 action call
  • 命令 + 配置({hidden: true})
program
.action((t) => {
    console.log('top action call')
})
.command('cmd1')
.action((t) => {
    console.log('child1 action call')
})
program.parse(process.argv);

// 输入:node test --help
// 观察:commands

program
.action((t) => {
    console.log('top action call')
})
.command('cmd1',{hidden:true})
.action((t) => {
    console.log('child1 action call')
})
program.parse(process.argv);

// 输入:node test --help
// 观察:commands
// 输入:node test cmd1
// 输出:child1 action call

// 总结:{hidden:true},只是在--help时不把改命令暴露出去,但不影响正常使用
  • 命令 + 描述(description)
// 案例一:主命令 + description
program
.description('主命令描述')
.action((...t) => {
    console.log('top action call',t)
})
.parse(process.argv);

// 输入:node test --help
// 输出:主命令描述

// 案例二:主命令接收参数
.arguments('<arg1> [arg2] [arg3]')
.description('主命令描述')
.action((...t) => {
    console.log('top action call',t)
})
.parse(process.argv); 

// 输入:node test 1
// 输出:top action call ['1',undefined,undefined,{},currentCommandRef]

// 案例三:主命令接受参数 + 描述参数
program
.arguments('<arg1> [arg2] [arg3]')
.description('主命令描述',{
    arg1:'这个是arg1 的描述',
    arg2:'这个是arg2 的描述',
    arg3:'这个是arg3 的描述', 
})
.action((...t) => {
    console.log('top action call',t)
})
.parse(process.argv); 

// 输入:node test --help
// 输出:
// 主命令描述
// Arguments:
//   arg1        这个是arg1 的描述
//   arg2        这个是arg2 的描述
//   arg3        这个是arg3 的描述

// 案例四:子命令 + 描述
program
.arguments('<arg1> [arg2] [arg3]')
.description('主命令描述',{
    arg1:'这个是arg1 的描述',
    arg2:'这个是arg2 的描述',
    arg3:'这个是arg3 的描述', 
})
.action((...t) => {
    console.log('top action call',t)
})
.command('cmd1 <carg1> [carg2]')
.description('子命令1描述',{
    carg1:'这个是carg1 的描述',
    carg2:'这个是carg2 的描述',
})
program.parse(process.argv); 

// 输入:node test --help
// 输入:node test cmd1 --help
// 总结:也就是为当前层的命令及参数增加描述介绍

五、选项

  • 理解option(基本概念)
program
.option('-a,--add','add something')
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test --help
// 观察options:
Options:
  -a,--add    add something
  -h, --help  display help for command
// 输入:node test
// 输出:{}
// 输入:node test -a
// 输出:{ add: true }

// 说明:options是用于注册选项
// 语法:option('-短描述 --长描述 参数1 参数2 参数3','描述',[入参格式函数],[迭代初始值])
// 对比命令command用action来执行回调,选项则使用.opts()来获取选项值
// 对比命令,同一个选项可以使用多次,但命令只是第一次有效
// 对于选项,我们可以把它理解为reduce,特别是:
// [入参格式函数],[迭代初始值] 和 recude(cb(),defaultValue) 一样理解就好
  • 选项 + 参数(接收单个参数)
program
.option('-a,--add <arg1>','add something')
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a 1
// 输出:{ add: '1' }

program
.option('-a,--add [arg1] [arg2]','add something')
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -a 1 2
// 输出:{ add: '1' }
// 说明:和命令的参数对比,选项只有定义了一个参数,不能arg1 arg2
// 思考:那要接收多个参数怎么办呢?⬇️
  • 选项 + 参数(接收多个参数)
// 方案一
program
.option('-a,--add [arg1...]','add something')
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -a 1 2 3
// 输出:{ add: [ '1', '2', '3' ] }
// 输入:node test -a
// 输出:{ add: true }
// 说明:和命令的参数对比,如果是可选参数,且是...,不传参数的值不是[]而是默认值

// 方案二
program
.option('-a,--add [arg1...]','add something')
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a 1
// 输出:{ add: ['1'] }
// 输入:node test -a 1 -a 2
// 输出:{ add: [ '1', '2' ] }
// 说明:也就是如果参数是...,值会叠加,否则值会覆盖
// 说明:对比命令,同一个选项可以使用多次,但命令只是第一次有效
  • 选项 + 参数 + 入参格式函数
// 案例一
const addFormat = (value,Accumulator) => {
    console.log(value,Accumulator)
    return value
}
program
.option('-a,--add [arg1]','add something',addFormat)
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a 1
// 输出:
1 undefined
{ add: '1' }

// 案例二
const addFormat = (value,Accumulator) => {
    console.log(value,Accumulator)
    return '我来修改返回值' + value
}
program
.option('-a,--add [arg1]','add something',addFormat)
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -a 1
// 输出:
1 undefined
{ add: '我来修改返回值1' }
// 输入:node test -a 1 -a 2
// 输出:
1 undefined
2 我来修改返回值1
{ add: '我来修改返回值2' }
// 说明:我们发现,第一次执行返回的值会作为第二次执行参数时候的第二个参数值
// 我们可以用:reduce来理解这个函数

// 案例三(加深对 入参格式函数 的作用对象 的理解)
const addFormat = (value,Accumulator) => {
    console.log(value,Accumulator)
    return '我来修改返回值' + value
}
program
.option('-a,--add [arg1...]','add something',addFormat)
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a 1 2 -a 3 4
// 输出:
1 undefined
2 我来修改返回值1
3 我来修改返回值2
4 我来修改返回值3
{ add: '我来修改返回值4' }
// 说明:我们发现入参格式函数作用于每个参数
  • 选项 + 参数 + 默认值
program
.option('-a,--add [arg1]','add something',1)
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a
// 输出:{ add: 1 }
// 说明:我们发现输入的都是string类型,这边输出取的是默认值1
// 也就是如果没有设置默认值,默认的默认值为true
  • 选项 + 参数 + 入参格式函数 + 默认值
const addFormat = (value,Accumulator) => {
    console.log(value,Accumulator)
    return value
}
program
.option('-a,--add [arg1]','add something',addFormat,1)
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -a
// 输出:{ add: 1 }
// 输入:node test -a 2
// 输出:
2 1
{ add: '2' }
  • 选项 + 参数 + 入参格式函数(具体案例加深理解)
// 案例一、入参字符串类型 => 其他类型
const intFormat = (value,Accumulator) => {
    const formatValue = parseInt(value,10)
    if(isNaN(formatValue)) {
        throw new program.InvalidOptionArgumentError('Not a number.');
    } 
   return formatValue
}
program
.option('-i,--integer [arg1...]','to integer',intFormat)
.parse(process.argv);
const options = program.opts()
console.log(options)

// 输入:node test -i xx
// 输出:
error: option '-i,--integer [arg1...]' argument 'ad' is invalid. Not a number.
// 输入:12.6
// 输出:{ integer: 12 }

// 案例二、入参 => 其他格式
const splitFormat = (value,Accumulator) => {
 return value.split(', ')
}
program
.option('-s,--split [arg1...]','to integer',splitFormat)
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -s 1,2,3
// 输出:{ split: [ '1,2,3' ] }

// 案例三、迭代
const collectFormat = (value,Accumulator) => {
 Accumulator.push(value)
 return Accumulator
}
program
.option('-c,--collect [arg1...]','to integer',collectFormat,[])
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -c 1 2 -c 3
// 输出:{ collect: [ '1', '2', '3' ] }

// 案例四、迭代
const statisticsFormat = (value,Accumulator) => {
    Accumulator +=1
    console.log('currentValue',value,'execCounter',Accumulator)
    return Accumulator
}
program
.option('-s,--statistics [arg1...]','to integer',statisticsFormat,0)
.parse(process.argv);
const options = program.opts()
console.log(options)
// 输入:node test -s a b -s c
// 输出:
currentValue a execCounter 1
currentValue b execCounter 2
currentValue c execCounter 3
{ statistics: 3 }
  • version:我们可以理解为简化版的选项
program
.version('0.1.1','-v,--version','版本描述')
.parse(process.argv)

// 输入:node test -v
// 输出:0.1.1
// 作用:用于标记当前命令的版本
// 输入:node test --help
// 观察:Options
Options:
  -v,--version  版本描述
  -h, --help    display help for command
  • help:我们可以理解为简化版的选项
// 观察⬆️
// 具体修改没有测试,这边主要介绍概念

六、参数(以下内容上面都有涉及,主要是总结和加深理解)

// 案例一、 主命令接收方式注册方式
program
.action((...t)=>{
    console.log('top action call',t)
})
.parse()
// 输入:node test 1 2
// 输出:top action call [{},currentComandRef]
// 输入:node test --help
// 观察:Usage: test [options]
program
.arguments('<arg1> [arg2]')
.action((...t)=>{
    console.log('top action call',t)
})
.parse()
// 输入:node test 1 2
// 输出:top action call ['1','2',{},currentComandRef]
// 输入:node test --help
// 观察:Usage: test [options] <arg1> [arg2]
// 输入:node test
// 输出:error: missing required argument 'arg1'
// 总结:主命令接收参数使用arguments来注册,作用和.command('名称 参数1 参数2')是一样的

// 案例二、子命令接收参数
program
.arguments('<arg1> [arg2]')
.action((...t)=>{
    console.log('top action call',t)
})
.command('cmd1 <cArg1> [cArg2] [cArg3]')
.action((...t)=>{
    console.log('child1 action call',t)
})
program.parse()
// 输入:node test cmd1 1 2 3
// 输出:child1 action call ['1','2','3',{},currentComandRef]
// 输入:node test cmd1 --help
// 观察:Usage: test cmd1 [options] <cArg1> [cArg2] [cArg3]

// 案例三、选项接收参数
program
.option('-a,--add [arg1...]')
program.parse()
// 输入:node test --help
// 观察:Options
-a,--add [arg1...]

七、描述(description)

  • 命令的描述
program
.description('这个是top命令的描述')
program.parse()
// 输入:node test --help
// 观察:这个是top命令的描述

program
.command('cmd1')
.description('这个是child1命令的描述')
program.parse()
// 输入:node test --help
// 观察:
Commands:
  cmd1            这个是child1命令的描述
// 说明:description、arguments也都可以为子命令服务

program
.command('cmd1','这个是child1命令的描述')
program.parse()
// 输入:node test --help
// 观察:
Commands:
  cmd1            这个是child1命令的描述
  • 选项的描述
program
.option('cmd1','这个是选项的描述')
program.parse()

// 输入:node test --help
// 观察:
Options:
  cmd1        这个是选项的描述
  • 参数的描述
// 案例一、主命令 参数的描述
program
.arguments('[arg1] <arg2>')
.description('这个是top命令的描述',{
    arg1:'arg1 的描述',
    arg2:'arg2 的描述'
})
program.parse()
// 输入:node test --help
// 观察:
这个是top命令的描述

Arguments:
  arg1        arg1 的描述
  arg2        arg2 的描述

// 案例二、子命令 参数的描述
program
.command('cmd1 [cArg1] [cArg2]')
.description('这个是child1命令的描述',{
    cArg1:'cArg1 的描述',
    cArg2:'cArg2 的描述'
})
program.parse()
// 输入:node test cmd1 --help
// 观察:
这个是child1命令的描述

Arguments:
  cArg1       cArg1 的描述
  cArg2       cArg2 的描述

八、总结:来git来做案例

#!/usr/local/bin/node
const program = require('commander');
const { execSync } = require('child_process');
const run = () => {
    program
    .version('2.24.3','-v,--version')
    .description('git command')
    
    const addCmd =program.command('add <path>')
    .description('This command updates the index using the current content...')
    .option('-f,--force','Allow adding otherwise ignored files.')
    .option('-a, --all','Update the index not only where the working tree...')
    .action((...args)=>{
        const [path,options,currentCmdRef] = args
        let gitCmd = `git add ${path}`
        if(options.force) {
            gitCmd = `git add ${path} --force`
        }
        if(options.all) {
            gitCmd = `git add ${path} --all`
        }
        try {
            execSync(gitCmd)
        } catch(res){console.log(res)}
    })
    
    const commitCmd = program.command('commit')
    .description('Create a new commit containing the current contents of the index...')
    .option('-m,--message <msg>','Use the given <msg> as the commit message.')
    .option('-e,--edit','The message taken from file with -F...')
    .option('--amend','Replace the tip of the current branch by creating a new commit')
    .action((...args)=>{
        const [options,currentCmdRef] = args
        const currentOptions = currentCmdRef.opts()
        let gitCmd = ``
        if(currentOptions.message) {
            gitCmd = `git commit -m ${currentOptions.message}`
        }
        if(currentOptions.edit) {
            gitCmd = `git commit --edit`
        }
        if(options.amend) {
            gitCmd = `git commit --amend`
        }
        try {
            execSync(gitCmd)
        } catch(res){console.log(res)}
    })
    
    const statusCmd = program.command('status')
    .description('Displays paths that have differences between the index file')
    .option('-s, --short','Give the output in the short-format')
    .option('-b, --branch','Show the branch and tracking info even in short-format')
    .action((...args)=>{
        const [options] = args
        console.log(args)
        let gitCmd = ``
        if(options.short) {
            gitCmd = `git status --short`
        }
        if(options.branch) {
            gitCmd = `git cstatus --branch`
        }
        try {
            execSync(gitCmd)
        } catch(res){console.log(res)}
    })
    
    program.parse(process.argv);  
}
run()