# 1. commander 使用教程总结

引用官方的一句话,commander是nodejs 命令行操作的完整解决方案

# 1.1. 安装

npm install commander

# 1.2. 声明 program 变量

  1. commander 默认导出的是一个方便快速使用的全局对象
const program = require('commander');
program.version('1.0.0');
program.parse(process.argv);
  1. 如果考虑大型项目,commander的使用方式可能有很多种,此时可以创建commander对象实例
const commander = require('commander');
const program = new commander.Command();
program.version('1.0.0');
program.parse(process.argv);

# 1.3. program.option 选项介绍

commander 提供的 options选项可以快捷定义命令行参数,并生成对应的参数配置文档 在--help命令中展示。 options 可以接收多个参数

  • 第一个参数flags,必填参数。 表示命令行选项的具体使用方式,每一个选项 都可以提供一个短标志(单个字符) 和一个长名称,两个标志之间用逗号隔开
const program = require('commander');
program
.option('-s, --source')
.option('-t --transform');

flags 的结尾还可以提供参数选项, 一般使用 [type], 表示该命令选项 需要提供的参数值。

注意 program 获取命令行参数值 options 只能接受一个可选参数 或 一个必填参数,如果需要接收多个参数, 可以通过处理函数处理连写的参数

当没有提供该结尾选项的时候, 命令行显示调用flag默认为true 当提供了该结尾选项的时候 ,<> 表示必填参数, []表示可选参数。可以在现实调用 flag 的时候传入自定义参数

  • 第二个参数 是一个可选参数,该参数表示作用的一个描述,该描述还会在--help 文档中展示
const program = require('commander');
program
.option('-s, --source', 'this is a source')
.option('-t --transform', 'this is a transform');
  • 第三个参数是一个可选参数,可以传入一个函数 或者一个字符串

    • 当传入的是一个函数的时候,表示的flag命令行参数具体值的处理函数,命令行参数 会被当作该函数的调用参数执行,该函数的返回结果被当作该命令传入的值
    const program = require('commander');
    program
    .option(
        '-s, --source', 
        'this is a source', 
        (v) => { 
            // 这里的v 就是传入的命令行参数
            return v + '-source'
        }
    )
    program.parse(process.argv);
    
    • 当传入的是一个字符串的时候,表示的是命令行参数默认值,flags 提供结尾参数选项的时候 才有效果, 当提供默认值时,即使<>必填参数,也可以不传值
  • 第四个参数是只有当第三个参数是一个处理函数的时候,才有效果, 该参数选项表示的是处理函数的绑定参数,和flag命令行参数的传入顺序是 先传入命令行传入的参数 ,然后才是第四个参数的绑定值,如果没有传入命令行传入的参数,第四个参数就直接作为该命令选项的默认值了

    const program = require('commander');
    program
    .option(
       '-s, --source', 
       'this is a source', 
       (v, n) => { 
           // 这里的v 就是传入的命令行参数
           // n 就是第四个参数
           return v + '-source' + n;
       },
       '-map'// 当不调用该命令选项时 直接返回第四个参数的值
    )
    
    program.parse(process.argv);
    

program.parse(process.argv) 解析命令行参数,调用了这个方法 就可以通过program变量 获得命令行参数的具体对应值了,在获取命令行参数对应值的时候 需要注意几点。

  • 如果flags 包含长命令方式,对应的program 获取参数值就是长命令的名称,如果长命令内部包含"-"连接符会自动识别成驼峰写法。

    const program = require('commander');
    program
        .option('-s, --source', 'this is a source')
        .option('-o, --output-file', 'this is output file');
    program.parse(process.argv);
    
    console.log(program.source);// 直接获取
    console.log(program.outputFile); // 驼峰获取
    
  • 如果flags 只有短命令方式,对应的program 获取方式就是,短命令的大写方式

    const program = require('commander');
    program
        .option('-s', 'this is a source')
        program.parse(process.argv);
    
    console.log(program.S);// 短命令大写获取
    
  • 如果flags 是用 --no-some 形式的长命令,对应的program只会获取 "no-" 后面的内容作为命令参数值,并且,当该选项没有参数选项的时候,命令参数值 是对应结果的反值

    const program = require('commander');
    program
        .option('--no-source', 'this is disable source')
    program.parse(process.argv);
    
    console.log(program.source);// 长命令获取 
    // 不传该命令选项的时候 该值 为true,传入该选项则为false
    

其他细节注意点

  1. 可以通过调用 program.opts() 获取所参数值
  2. 短命令如果都是 boolean类型的 可以连写, -a -b -c === -abc, 并且连写传入参数的话默认作为最后一个短命令的参数

# 1.4. program.version 创建版本号

  • program.version方法添加具体版本号,该选项的默认调用 flag 为 -V, --version

    const program = require('commander');
    program.version('0.0.1')
    program.parse(process.argv);
    
    // shell 
    // index.js -V => 0.0.1
    
  • 也可以自定义修改设置版本号的flags,program.version()方法的第二个和第三个参数即可,分别对应版本号的flags和description

    const program = require('commander');
    program.version('0.0.1', '-v, --vers', 'output the current version')
    program.parse(process.argv);
    
    // shell 
    // index.js -v, --vers => 0.0.1
    

# 1.5. program.command 创建子命令

commander 提供了program.command 函数创建子级命令,使用program.command 的方法有两种

  • 使用 command 的 action 处理字级命令

    const program = require('commander');
    program
        .command('init <templateName> [otherTemplate...]')
        .description('初始化一个模版项目')
        .action((templateName, otherTemplates) => {
            console.log(templateName);
            otherTemplates.forEach(v => {
               console.log(v);
            })
        })
    
    • command 函数的第一个参数 指定一个 命令名称 以及执行命令所需要的参数。参数类型<> 是必填参数,[]可选参数,并且最后一个参数的个数可以是动态变化的 通过 “...” 语法
    • command 支持alias方法链式调用,可以给当前命令取别名
    const program = require('commander');
    program
        .version('0.0.1')
        .command('update <env>')
        .alias('u')
        .options('-e, --exec', 'this is exec option')
        .command('install <dir>')
        .alias('i')
        .parse(process.argv);
    
    • 除了command方法,program还支持了arguments函数语法,也是通过action 处理子级命令。
     const program = require('commander');
     program
        .version('0.0.1')
        .arguments('<cmd> [envs...]')
        .action((cmd, envs) => {
            if(cmd === 'install') {
                installAction(envs);
            }
        })
    

    arguments 方法本质上就是抽象的command 方法,可以支持 任意的 命令进入action,并针对不同的cmd,做不同的处理,而且 不会与command 方法冲突, 当有定义 command 对应的命令的时候 会优先使用 command执行的action。

    • command方法还支持提供option选项,actions可以获取到每一个传入的参数外,还可以额外获得command命令对象本身,可以从该对象获取到option配置,该对象作为action的最后一个参数传入
     const program = require('commander');
     program
        .command('remove <dir> [otherDirs...]')
        .option('-r, --rescurive', 'remove recursively')
        .option('-j, --jsource', 'this is source')
        .option('-l, --ldev', 'this is dev')
        .description('删除目录')
        .action((dir, otherDirs, cmdObj) => {
            console.log(dir);
            console.log(otherDirs);
            console.log(cmdObj.opts(), cmdObj.rescurive, cmdObj.ldev);
        })
     program.parse(process.argv);   
    
  • 使用单独的 可执行文件 当command函数传入第二个参数 description 参数,commander就会默认认为这是使用单独的可执行文件方式执行子命令。commander 会自动检索当前脚本文件的相对应名称的文件执行子命令.

    // ./lib/pm.js
    const program = require('commander');
    program
        .command('install <name>', 'this is a git style command')
        
    program.parse(program.argv);    
    
    // ./lib/pm-install.js
    const program = require('commander');
    program.parse(program.argv); 
    if(program.args.length <= 0) {
        console.error('need params');
        process.exit(1);
    }
    
    program.args.forEach(arg => {
        console.log(arg);
    })
    

    执行单独的可执行文件时,command方法第三个参数接收一个配置选项。

    • noHelp, 当为true的时候 --help 将不会展示该选项

      const program = require('commander');
      program
          .command('install <name>', 'this is a git style command', { noHelp: true })
      
      program.parse(program.argv);    
      
    • isDefault 当执行的命令的时候没有指定任意命令,可以通过该选项 默认指向一个命令(默认的isDefualt是 --help)

      const program = require('commander');
      program
          .command('install <name>', 'this is a git style command', { noHelp: true })
          .command('list', 'show list', { isDefault: true })// 不指定任何命令会执行该命令 
      
      program.parse(program.argv);    
      
    • executableFile 显示指定可执行文件的名称,执行可执行文件命令会使用这个文件

      const program = require('commander');
      program
         .command('update', 'update', { executableFile: 'updatePm' })
      
      program.parse(program.argv);    
      

    需要注意几点:

    • 当使用单独的可执行文件时不能继续使用action,否则可能会报错(我自己实践发现,只是可执行文件program.args 多个了一个位置参数传入,且action 不会执行,且执行其他命令会执行到action 反正行为很异常)
    • arguments 方法也不能使用,因为它匹配所有的方法,和传入action一样的效果,都会在单独的可执行文件中增加一个未知的参数传入('[object Object]')
    • 如果没有配置executableFile 选项,当执行单独的可执行文件默认的搜索文件规则是,当前文件名,加上command名,在当前脚本文件中搜索。例如当前脚本文件是 index.js ,注册的命令是install 可执行文件方式的command,那么,在当前目录下对应的的可执行文件名字叫 index-install.js, 如果命令是 search 即为index-search.js
    • program 匹配命令时 是自上而下执行的,会执行 所有匹配上的命令, 注意git style command配置选项 isDefault: true 时默认命令会和command 同时匹配, command 不和 arguments 不会同时匹配 但是在处理 没有注册的命令时 arguments 会和 默认命令同时匹配。
    • 命令程序多个匹配 只会发生在非链式调用中, commander 的一个链式调用 表示 一组命令,这一组命令 只会匹配一个, 只有多个链式调用之间 会存在同时匹配的情况

# 1.6. help 方法的介绍

  • 自定义help方法 通过事件监听的方式,监听help 触发,并在help输出之后跟随输出自定的内容

    const program = require('commander');
    // 必须在program.parse 之前执行, 因为node emit help 是瞬间就执行完的
    program.on('--help', function(){
        console.log('')
        console.log('Examples:');
        console.log('  $ custom-help --help');
        console.log('  $ custom-help -h');
    });
    program.parse(process.argv);
    
  • outputHelp(cb) 方法 可以输出 help 信息,并不退出当前进程,提供一个回调函数, program 允许函数在显示 help 文档之前执行预处理

    const program = require('commander');
    const colors = require('colors');
    program.parse(process.argv);
    if(program.argv.slice(2).length <= 0) {
        program.outputHelp(make_red);
    }
    
    const make_red = (text) => {
        return colors.red(text); // 用红色字体显示help 文档
    }
    
  • helpOption(flags, description) 覆盖修改help 默认 flags 和description

    program
      .helpOption('-e, --HELP', 'read more information');
    
  • help(cb) 方法 可以输出 help 文档,并立即退出当前进程,提供一个回调函数, program 允许函数在显示 help 文档之前执行预处理

    const program = require('commander');
    const colors = require('colors');
    program.parse(process.argv);
    if(program.argv.slice(2).length <= 0) {
        program.help(make_red);
    }
    const make_red = (text) => {
        return colors.red(text); // 用红色字体显示help 文档
    }
    

# 1.7. 自定义事件监听

commander 允许监听 命令command 或者 选项设置 option 来执行一些自定义操作.

const program = require('commander');
// 监听某一个已经注册的flag
program.on('option:flag', () => {
    console.log(this.flag);
})

// 匹配空执行
if(!process.argv.slice(2).length) {
    program.help();// 输出help 文档并立即退出
}

// 监听command
program.on('command:*', (cmdObj) => {
    const [cmd, envs, command] = cmdObj;
    if(gitStyleCommands.indexOf(cmd) === -1) {
        program.outputHelp();
        console.log(`${chalk.red('Unknown command')} ${chalk.yellow(cmd)}.`)
        process.exit(1);
    }
});

注意:

  1. 只能监听已经注册的flag,
  2. 监听command 回调函数传入的 对象是一个数组 包括 cmd 命令 传入参数数组, 以及commander 对象
  3. command 的事件 不会和 command action注册的事件同时触发,但是会和 git style command 风格的命令同时触发

# 2. 完整实践

const program = require('commander');
const chalk = require('chalk');
program.version('1.0.1'); // 程序的版本设置

program
    .option('-a, --absolute', 'this is absolute option') // 普通option
    .option('-b, --backend <dir>', 'this is backend option') // option 接收一个必填参数
    .option('-c, --callback <dir>', 'this is callback option', 'prod') // option 接收一个必填参数,如果不传会给一个默认值 prod
    .option('-d, --divide [env]', 'this is divide option') // option 接收一个选填参数
    .option('-e, --eval <num>', 'this is eval option', parseInt) // option 接收一个必填,并添加参数处理函数 parseInt 将字符串转为数字
    .option('-f, --found <num>', 'this is found option', function(num, baseNum) {
        return parseInt(num) + baseNum;
    }, 2) // option 接收一个必填参数,并添加参数处理函数,以及处理函数默认值
    .option('-g, --grace-template', 'this is graceTemplate option') // 连写长命令 取 对应属性值的时候 使用驼峰名取 program.graceTemplate
    .option('--no-happy', 'this is no happy option') // --no-[some] 类型的长命令。取对应的属性值。为no- 之后的名称 即 program.happy 。且当不设置任何参数时,使用该长命令获取的是 false, 不设置则反之为true 
    .option('-i', 'this is a I option') // 只有短命令是 获取对应的属性值 取的是 大写名称,program.I
    .parse(process.argv) 

console.log(program.opts());   
console.log(program.absolute);
console.log(program.backend);
console.log(program.callback);
console.log(program.divide);
console.log(program.eval);
console.log(program.graceTemplate);
console.log(program.happy);
console.log(program.I);
program.on('option:verbose', (kiss) => {
    console.log('option:graceTemplate', kiss);
})

program
    .command('init <templateName> [envs...]') // 创建命令 <> 必填参数 [] 选填参数 ... 可接收多个
    .alias('i') // 命令取别名
    .description('this is a init command') // 命令的描述
    .option('-j, --jade', 'this is a jade option') // 该命令相关的选项配置 
    .action((templateName, envs, cmdObj) => {
        console.log(templateName);
        console.log(envs); // 如果有命令两个参数 第二个参数就是该变量。 如果没有第二个参数 且包含 option 选项配置的时候 第二个参数就是 command 本身
        console.log(cmdObj.opts()); // 如果有两个参数 且包含option  ,第三个参数就是command本身
    });

program
    .arguments('<cmd> [envs...]') // 匹配任意 命令 且, 命令即参数被当作参数传入 action
    .action((cmd, envs) => {
        console.log(cmd); // 命令
        console.log(envs); // 参数 
    }); // 当执行 已经注册的命令时 不会进入 arugments 通配命令中,只会执行对应的命令action
   
// git style command
program
    .command('install <templateName>', 'this is git style command') // 默认查找 当前目录的 相关文件 [当前文件名]-install exec-install.js 并执行
    .command('publish <name>', 'this is git style command', { excutableFile: 'execPublish' }) // 当提供配置选项的时候 会执行 excutableFile 指定的文件  
    .command('default <name>', 'this is git style command', { isDefault: true }) // 当不传入命令的时候 或没有匹配到任意,命令时 会搜索 exec-defualt.js 并执行,

// 自定义监听已经注册的 option 选项    
program.on('option:eval', (eval) => {
    console.log('option:eval', eval);
})

program.on('option:graceTemplate', (graceTemplate) => {
    console.log('option:graceTemplate', graceTemplate);
})

const gitStyleCommands = [
    'install',
    'publish',
    'default',
]
// 自定义监听命令
program.on('command:*', (cmdObj) => {
    const [cmd, envs, command] = cmdObj;
    console.log(cmd, envs);
    if(gitStyleCommands.indexOf(cmd) === -1) {
        program.outputHelp();
        console.log(`${chalk.red('Unknown command')} ${chalk.yellow(cmd)}.`)
        process.exit(1);
    }
})

// 匹配空执行
if(!process.argv.slice(2).length) {
    program.help();
}

program.parse(process.argv);