前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Commander.js —— 你也是命令行大师

玩转 Commander.js —— 你也是命令行大师

作者头像
winty
发布2021-10-11 13:06:08
5.3K0
发布2021-10-11 13:06:08
举报
文章被收录于专栏:前端Q前端Q

最近做团队底层构建工具架构升级和命令行交互打了不少交道,再加上在研究 Vue-CLI 的源码,觉得 Commander.js 作为 Node.js 下这么优秀的命令行交互工具,值得总结一下,文章主体内容搬运了 Commander.js 的官方文档,对一些晦涩的翻译部分进行了注解和必要的代码注释,适合躺在收藏夹,需要时拿出来查一查。

1. 安装

代码语言:javascript
复制
npm install commander

2. 声明 program 变量

直接引入对象(本文使用的方式):

代码语言:javascript
复制
const { program } = require('commander');
program.version('0.0.1');

创建实例方法:

代码语言:javascript
复制
const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');

TypeScript 用法:

代码语言:javascript
复制
// index.ts
import { Command } from 'commander';
const program = new Command();

3. 选项

Commander 使用.option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。

代码语言:javascript
复制
program
  .option('-j | --join','Join IMWeb now!')

解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。可以使用.getOptionValue().setOptionValue()操作单个选项的值。

代码语言:javascript
复制
program
  .option('-j | --join','Join IMWeb now!');
program.parse()
console.log(program.opts()) // {join: true}
console.log(program.getOptionValue('join')) // true
代码语言:javascript
复制
$ imweb-options -j
{ join:true }
{ true }

以上两个方法是我们最常用的选项声明和取值方法,下面也罗列了一些小Tips:

  • 对于多个单词的长选项,选项名会转为驼峰命名法(camel-case),例如--template-engine选项可通过program.opts().templateEngine获取
  • 多个短选项可以合并简写,其中最后一个选项可以附加参数。例如,-a -b -p 80也可以写为-ab -p80,甚至进一步简化为-abp80
  • --可以标记选项的结束,后续的参数均不会被命令解释,可以正常使用。
  • 默认情况下,选项在命令行中的顺序不固定,一个选项可以在其他参数之前或之后指定。

3.1. 常用选项类型,boolean 型选项和带参数选项

有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数;另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect <value>)。如果在命令行中不指定具体的选项及参数,则会被定义为?developer/article/1887034/undefined

代码语言:javascript
复制
program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small', 'small pizza size')
  .option('-p, --pizza-type <type>', 'flavour of pizza');

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);
代码语言:javascript
复制
$ pizza-options -p
error: option '-p, --pizza-type <type>' argument missing
$ pizza-options -d -s -p vegetarian
{ debug: true, small: true, pizzaType: 'vegetarian' }
pizza details:
- small pizza size
- vegetarian
$ pizza-options --pizza-type=cheese
pizza details:
- cheese

通过 program.parse(arguments) 方法处理参数,没有被使用的选项会存放在 program.args 数组中。该方法的参数是可选的,默认值为 process.argv,注意在一个大项目中,如果涉及到一些指令的注册,请注意在所有指令注册结束后再执行 program.parse(),因为这一操作会终止参数处理,导致指令不能正确注册,Commander.js 提供了单独处理选项的函数 program.parseOptions(arguments) 参数同 program.parse(arguments)

3.2. 选项的默认值

选项可以设置一个默认值。

代码语言:javascript
复制
program
  .option('-c, --cheese <type>', 'add the specified type of cheese', 'blue');

program.parse();

console.log(`cheese: ${program.opts().cheese}`);
代码语言:javascript
复制
$ pizza-options
cheese: blue
$ pizza-options --cheese stilton
cheese: stilton

3.3. 其他的选项类型,取反选项,以及可选参数的选项

可以定义一个以 no- 开头的 boolean 型长选项。在命令行中使用该选项时,会将对应选项的值置为 false。当只定义了带 no- 的选项,未定义对应不带 no- 的选项时,该选项的默认值会被置为 true

如果已经定义了 --foo,那么再定义 --no-foo并不会改变它本来的默认值。可以为一个 boolean 类型的选项指定一个默认的布尔值,在命令行里可以重写它的值。

代码语言:javascript
复制
program
  .option('--no-sauce', 'Remove sauce')
  .option('--cheese <flavour>', 'cheese flavour', 'mozzarella')
  .option('--no-cheese', 'plain with no cheese')
  .parse();

const options = program.opts();
const sauceStr = options.sauce ? 'sauce' : 'no sauce';
const cheeseStr = (options.cheese === false) ? 'no cheese' : `${options.cheese} cheese`;
console.log(`You ordered a pizza with ${sauceStr} and ${cheeseStr}`);
代码语言:javascript
复制
$ pizza-options
You ordered a pizza with sauce and mozzarella cheese
$ pizza-options --sauce
error: unknown option '--sauce'
$ pizza-options --cheese=blue
You ordered a pizza with sauce and blue cheese
$ pizza-options --no-sauce --no-cheese
You ordered a pizza with no sauce and no cheese
$ pizza-options --cheese=blue --no-cheese
You ordered a pizza with sauce and no cheese

注意取反选项在命令行同时出现,后使用的会覆盖之前的选项,如上代码块的最后一行。

选项的参数使用方括号声明表示参数是可选参数(如--optional [value])。该选项在不带参数时可用作 boolean 选项,在带有参数时则从参数中得到值。

代码语言:javascript
复制
program
  .option('-c, --cheese [type]', 'Add cheese with optional type');

program.parse(process.argv);

const options = program.opts();
if (options.cheese === ?developer/article/1887034/undefined) console.log('no cheese');
else if (options.cheese === true) console.log('add cheese');
else console.log(`add cheese type ${options.cheese}`);
代码语言:javascript
复制
$ pizza-options
no cheese
$ pizza-options --cheese
add cheese
$ pizza-options --cheese mozzarella
add cheese type mozzarella

3.4. 必填选项

通过.requiredOption()方法可以设置选项为必填。必填选项要么设有默认值,要么必须在命令行中输入,对应的属性字段在解析时必定会有赋值。该方法其余参数与.option()一致。

代码语言:javascript
复制
program
  .requiredOption('-c, --cheese <type>', 'pizza must have cheese');

program.parse();
代码语言:javascript
复制
$ pizza
error: required option '-c, --cheese <type>' not specified

3.5. 变长参数选项

定义选项时,可以通过使用...来设置参数为可变长参数。在命令行中,用户可以输入多个参数,解析后会以数组形式存储在对应属性字段中。在输入下一个选项前(---开头),用户输入的指令均会被视作变长参数。与普通参数一样的是,可以通过--标记当前命令的结束。

代码语言:javascript
复制
program
  .option('-n, --number <numbers...>', 'specify numbers')
  .option('-l, --letter [letters...]', 'specify letters');

program.parse();

console.log('Options: ', program.opts());
console.log('Remaining arguments: ', program.args);
代码语言:javascript
复制
$ collect -n 1 2 3 --letter a b c
Options:  { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
Remaining arguments:  []
$ collect --letter=A -n80 operand
Options:  { number: [ '80' ], letter: [ 'A' ] }
Remaining arguments:  [ 'operand' ]
$ collect --letter -n 1 -n 2 3 -- operand
Options:  { number: [ '1', '2', '3' ], letter: true }
Remaining arguments:  [ 'operand' ]

3.6. 版本选项

.version()方法可以设置版本,其默认选项为-V--version,设置了版本后,命令行会输出当前的版本号。

代码语言:javascript
复制
program.version('0.0.1');
代码语言:javascript
复制
$ ./examples/pizza -V
0.0.1

版本选项也支持自定义设置选项名称,可以在.version()方法里再传递一些参数(长选项名称、描述信息),用法与.option()方法类似。

代码语言:javascript
复制
program.version('0.0.1', '-v, --vers', 'output the current version');

3.7. 其他选项配置

大多数情况下,选项均可通过.option()方法添加。但对某些不常见的用例,也可以直接构造Option对象,对选项进行更详尽的配置。

代码语言:javascript
复制
program
  .addOption(new Option('-s, --secret').hideHelp())
  .addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
  .addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']));
代码语言:javascript
复制
$ extra --help
Usage: help [options]

Options:
  -t, --timeout <delay>  timeout in seconds (default: one minute)
  -d, --drink <size>     drink cup size (choices: "small", "medium", "large")
  -h, --help             display help for command

$ extra --drink huge
error: option '-d, --drink <size>' argument 'huge' is invalid. Allowed choices are small, medium, large.

3.8. 自定义选项处理

选项的参数可以通过自定义函数来处理,该函数接收两个参数,即用户新输入的参数值和当前已有的参数值(即上一次调用自定义处理函数后的返回值),返回新的选项参数值。

自定义函数适用场景包括参数类型转换,参数暂存,或者其他自定义处理的场景。

可以在自定义函数的后面设置选项参数的默认值或初始值(例如参数用列表暂存时需要设置一个初始空列表)。

代码语言:javascript
复制
function myParseInt(value, dummyPrevious) {
  // parseInt 参数为字符串和进制数
  const parsedValue = parseInt(value, 10);
  if (isNaN(parsedValue)) {
    throw new commander.InvalidArgumentError('Not a number.');
  }
  return parsedValue;
}

function increaseVerbosity(dummyValue, previous) {
  return previous + 1;
}

function collect(value, previous) {
  return previous.concat([value]);
}

function commaSeparatedList(value, dummyPrevious) {
  return value.split(',');
}

program
  .option('-f, --float <number>', 'float argument', parseFloat)
  .option('-i, --integer <number>', 'integer argument', myParseInt)
  .option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0)
  .option('-c, --collect <value>', 'repeatable value', collect, [])
  .option('-l, --list <items>', 'comma separated list', commaSeparatedList)
;

program.parse();

const options = program.opts();
if (options.float !== ?developer/article/1887034/undefined) console.log(`float: ${options.float}`);
if (options.integer !== ?developer/article/1887034/undefined) console.log(`integer: ${options.integer}`);
if (options.verbose > 0) console.log(`verbosity: ${options.verbose}`);
if (options.collect.length > 0) console.log(options.collect);
if (options.list !== ?developer/article/1887034/undefined) console.log(options.list);
代码语言:javascript
复制
$ custom -f 1e2
float: 100
$ custom --integer 2
integer: 2
$ custom -v -v -v
verbose: 3
$ custom -c a -c b -c c
[ 'a', 'b', 'c' ]
$ custom --list x,y,z
[ 'x', 'y', 'z' ]

4. 命令

通过.command().addCommand()可以配置命令,有两种实现方式:为命令绑定处理函数,或者将命令单独写成一个可执行文件(详述见后文)。子命令支持嵌套。

.command()的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()单独指定。参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。

使用.addCommand()program增加配置好的子命令。

例如:

代码语言:javascript
复制
// 通过绑定处理函数实现命令(这里的指令描述为放在`.command`中)
// 返回新生成的命令(即该子命令)以供继续配置
program
  .command('clone <source> [destination]')
  .description('clone a repository into a newly created directory')
  .action((source, destination) => {
    console.log('clone command called');
  });

// 通过独立的的可执行文件实现命令 (注意这里指令描述是作为`.command`的第二个参数)
// 返回最顶层的命令以供继续添加子命令
program
  .command('start <service>', 'start named service')
  .command('stop [service]', 'stop named service, or all if no name supplied');

// 分别装配命令
// 返回最顶层的命令以供继续添加子命令
program
  .addCommand(build.makeBuildCommand());

使用.command()addCommand()来指定选项的相关设置。当设置hidden: true时,该命令不会打印在帮助信息里。当设置isDefault: true时,若没有指定其他子命令,则会默认执行这个命令,。

4.1. 命令参数

如上所述,字命令的参数可以通过.command()指定。对于有独立可执行文件的子命令来说,参数只能以这种方法指定。而对其他子命令,参数也可用以下方法。

Command对象上使用.argument()来按次序指定命令参数。该方法接受参数名称和参数描述。参数可为必选的(尖括号表示,例如<required>)或可选的(方括号表示,例如[optional])。

代码语言:javascript
复制
program
  .version('0.1.0')
  .argument('<username>', 'user to login')
  .argument('[password]', 'password for user, if required', 'no password given')
  .action((username, password) => {
    console.log('username:', username);
    console.log('password:', password);
  });

在参数名后加上...来声明可变参数,且只有最后一个参数支持这种用法。可变参数会以数组的形式传递给处理函数。例如:

代码语言:javascript
复制
program
  .version('0.1.0')
  .command('rmdir')
  .argument('<dirs...>')
  .action(function (dirs) {
    dirs.forEach((dir) => {
      console.log('rmdir %s', dir);
    });
  });

有一种便捷方式可以一次性指定多个参数,但不包含参数描述:

代码语言:javascript
复制
program
  .arguments('<username> <password>');
4.1.1. 其他参数配置

有少数附加功能可以直接构造Argument对象,对参数进行更详尽的配置。

代码语言:javascript
复制
program
  .addArgument(new commander.Argument('<drink-size>', 'drink cup size').choices(['small', 'medium', 'large']))
  .addArgument(new commander.Argument('[timeout]', 'timeout in seconds').default(60, 'one minute'))
4.1.2. 自定义参数处理

选项的参数可以通过自定义函数来处理(与处理选项参数时类似),该函数接收两个参数:用户新输入的参数值和当前已有的参数值(即上一次调用自定义处理函数后的返回值),返回新的命令参数值。

处理后的参数值会传递给命令处理函数,同时可通过.processedArgs获取。

可以在自定义函数的后面设置命令参数的默认值或初始值。

代码语言:javascript
复制
program
  .command('add')
  .argument('<first>', 'integer argument', myParseInt)
  .argument('[second]', 'integer argument', myParseInt, 1000)
  .action((first, second) => {
    console.log(`${first} + ${second} = ${first + second}`);
  })
;

4.2. 处理函数

命令处理函数的参数,为该命令声明的所有参数,除此之外还会附加两个额外参数:一个是解析出的选项,另一个则是该命令对象自身。

代码语言:javascript
复制
program
  .argument('<name>')
  .option('-t, --title <honorific>', 'title to use before name')
  .option('-d, --debug', 'display some debugging')
  .action((name, options, command) => {
    if (options.debug) {
      console.error('Called %s with options %o', command.name(), options);
    }
    const title = options.title ? `${options.title} ` : '';
    console.log(`Thank-you $${name}`);
  });

处理函数支持async,相应的,需要使用.parseAsync代替.parse

代码语言:javascript
复制
async function run() { /* 在这里编写代码 */ }

async function main() {
  program
    .command('run')
    .action(run);
  await program.parseAsync(process.argv);
}

使用命令时,所给的选项和命令参数会被验证是否有效。凡是有未知的选项,或缺少所需的命令参数,都会报错。如要允许使用未知的选项,可以调用.allowUnknownOption()。默认情况下,传入过多的参数并不报错,但也可以通过调用.allowExcessArguments(false)来启用过多参数的报错。

4.3. 独立的可执行(子)命令

.command()带有描述参数时,就意味着使用独立的可执行文件作为子命令。Commander 将会尝试在入口脚本(例如./examples/pm)的目录中搜索program-command形式的可执行文件,例如pm-installpm-search。通过配置选项executableFile可以自定义名字。

你可以在可执行文件里处理(子)命令的选项,而不必在顶层声明它们。

代码语言:javascript
复制
program
  .version('0.1.0')
  .command('install [name]', 'install one or more packages')
  .command('search [query]', 'search with optional query')
  .command('update', 'update installed packages', { executableFile: 'myUpdateSubCommand' })
  .command('list', 'list packages installed', { isDefault: true });

program.parse(process.argv);

如果该命令需要支持全局安装,请确保有对应的权限,例如755

4.4. 生命周期钩子

可以在命令的生命周期事件上设置回调函数。

代码语言:javascript
复制
program
  .option('-t, --trace', 'display trace statements for commands')
  .hook('preAction', (thisCommand, actionCommand) => {
    if (thisCommand.opts().trace) {
      console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
      console.log('arguments: %O', actionCommand.args);
      console.log('options: %o', actionCommand.opts());
    }
  });

钩子函数支持async,相应的,需要使用.parseAsync代替.parse。一个事件上可以添加多个钩子。

支持的事件有:

  • preAction:在本命令或其子命令的处理函数执行前
  • postAction:在本命令或其子命令的处理函数执行后

钩子函数的参数为添加上钩子的命令,及实际执行的命令。

5. 自动化帮助信息

帮助信息是 Commander 基于你的程序自动生成的,默认的帮助选项是-h,--help

代码语言:javascript
复制
$ node ./examples/pizza --help
Usage: pizza [options]

An application for pizza ordering

Options:
  -p, --peppers        Add peppers
  -c, --cheese <type>  Add the specified type of cheese (default: "marble")
  -C, --no-cheese      You do not want any cheese
  -h, --help           display help for command

如果你的命令中包含了子命令,会默认添加help命令,它可以单独使用,也可以与子命令一起使用来提示更多帮助信息。用法与shell程序类似,注意这一指令不能被注销。

代码语言:javascript
复制
shell help
shell --help

shell help spawn
shell spawn --help

5.1. 自定义帮助

可以添加额外的帮助信息,与内建的帮助一同展示。

代码语言:javascript
复制
program
  .option('-f, --foo', 'enable some foo');

program.addHelpText('after', `

Example call:
  $ custom-help --help`);

将会输出以下的帮助信息:

代码语言:javascript
复制
Usage: custom-help [options]

Options:
  -f, --foo   enable some foo
  -h, --help  display help for command

Example call:
  $ custom-help --help

位置参数对应的展示方式如下:

  • beforeAll:作为全局标头栏展示
  • before:在内建帮助信息之前展示
  • after:在内建帮助信息之后展示
  • afterAll:作为全局末尾栏展示

beforeAllafterAll两个参数作用于命令及其所有的子命令,注意 Commander.js 不支持重载所有的帮助信息,只能实现和内建的帮助一同展示,如下图是 Commander.js 配合 CommandLineUsage 的效果,可以看到 Commander.js 内置的帮助信息在 CommandLineUsgae 之后输出:

第二个参数可以是一个字符串,也可以是一个返回字符串的函数。对后者而言,为便于使用,该函数可以接受一个上下文对象,它有如下属性:

  • error:boolean 值,代表该帮助信息是否由于不当使用而展示
  • command:代表展示该帮助信息的Command对象

5.2. 在出错后展示帮助信息

默认情况下,出现命令用法错误时只会显示错误信息。可以选择在出错后展示完整的帮助或自定义的帮助信息。

代码语言:javascript
复制
program.showHelpAfterError();
// 或者
program.showHelpAfterError('(add --help for additional information)');
代码语言:javascript
复制
$ pizza --unknown
error: unknown option '--unknown'
(add --help for additional information)

5.3. 使用代码展示帮助信息

.help():展示帮助信息并退出。可以通过传入{ error: true }来让帮助信息从 stderr 输出,并以代表错误的状态码退出程序。

.outputHelp():只展示帮助信息,不退出程序。传入{ error: true }可以让帮助信息从 stderr 输出。

.helpInformation():得到字符串形式的内建的帮助信息,以便用于自定义的处理及展示。

5.4. .usage 和 .name

通过这两个选项可以修改帮助信息的首行提示,name属性也可以从参数中推导出来。例如:

代码语言:javascript
复制
program
  .name("my-command")
  .usage("[global options] command")

帮助信息开头如下:

代码语言:javascript
复制
Usage: my-command [global options] command

5.5. .helpOption(flags, description)

每一个命令都带有一个默认的帮助选项。可以重写flagsdescription参数。传入false则会禁用内建的帮助信息。

代码语言:javascript
复制
program
  .helpOption('-e, --HELP', 'read more information');

5.6. .addHelpCommand()

如果一个命令拥有子命令,它也将有一个默认的帮助子命令。使用.addHelpCommand().addHelpCommand(false)可以打开或关闭默认的帮助子命令。

也可以自定义名字和描述:

代码语言:javascript
复制
program.addHelpCommand('assist [command]', 'show assistance');

5.7. 其他帮助配置

内建帮助信息通过Help类进行格式化。如有需要,可以使用.configureHelp()来更改其数据属性和方法,或使用.createHelp()来创建子类,从而配置Help类的行为。

数据属性包括:

  • helpWidth:指明帮助信息的宽度。可在单元测试中使用。
  • sortSubcommands:以字母序排列子命令
  • sortOptions:以字母序排列选项

可以得到可视化的参数列表,选项列表,以及子命令列表。列表的每个元素都具有_term__description_属性,并可以对其进行格式化。关于其使用方式,请参考.formatHelp()

代码语言:javascript
复制
program.configureHelp({
  sortSubcommands: true,
  subcommandTerm: (cmd) => cmd.name() // 显示名称,而非用法
});

6. 自定义事件监听

监听命令和选项可以执行自定义函数。

代码语言:javascript
复制
program.on('option:verbose', function () {
  process.env.VERBOSE = this.opts().verbose;
});

program.on('command:*', function (operands) {
  console.error(`error: unknown command '${operands[0]}'`);
  const availableCommands = program.commands.map(cmd => cmd.name());
  mySuggestBestMatch(operands[0], availableCommands);
  process.exitCode = 1;
});

7. 零碎知识

7.1. .parse() 和 .parseAsync()

.parse的第一个参数是要解析的字符串数组,也可以省略参数而使用process.argv

如果参数遵循与 node 不同的约定,可以在第二个参数中传递from选项:

  • node:默认值,argv[0]是应用,argv[1]是要跑的脚本,后续为用户参数;
  • electronargv[1]根据 electron 应用是否打包而变化;
  • user:来自用户的所有参数。

例如:

代码语言:javascript
复制
program.parse(process.argv); // 指明,按 node 约定
program.parse(); // 默认,自动识别 electron
program.parse(['-f', 'filename'], { from: 'user' });

7.2. 解析配置

当默认的解析方式无法满足需要,Commander 也提供了其他的解析行为。

默认情况下,程序的选项在子命令前后均可被识别。如要只允许选项出现在子命令之前,可以使用.enablePositionalOptions()。这样可以在命令和子命令中使用意义不同的同名选项。

当启用了带顺序的选项解析,以下程序中,-b选项在第一行中将被解析为程序顶层的选项,而在第二行中则被解析为子命令的选项:

代码语言:javascript
复制
program -b subcommand
program subcommand -b

默认情况下,选项在命令参数前后均可被识别。如要使选项仅在命令参数前被识别,可以使用.passThroughOptions()。这样可以把参数和跟随的选项传递给另一程序,而无需使用--来终止选项解析。如要在子命令中使用此功能,必须首先启用带顺序的选项解析。

当启用此功能时,以下程序中,--port=80在第一行中会被解析为程序的选项,而在第二行中则会被解析为一个命令参数:

代码语言:javascript
复制
program --port=80 arg
program arg --port=80

默认情况下,使用未知选项会提示错误。如要将未知选项视作普通命令参数,并继续处理其他部分,可以使用.allowUnknownOption()。这样可以混用已知和未知的选项。

默认情况下,传入过多的命令参数并不会报错。可以使用.allowExcessArguments(false)来启用这一检查。

7.3. 指令重复注册的问题

在设计脚手架或者底层构建工具的时候,设计者往往会考虑到,如果开放插件系统给各个插件注册指令,难免会出现指令重复的问题,这里很遗憾 Commander.js 没有对这类问题做特殊处理,也没有提供相关 API 或者 HOOK 支持。重复注册时,Commander.js 会先后注册两个指令,在执行相关指令的时候也会依次执行,输入 -h 的时候也会出现两个指令,即使他们的命令规范是不同的,比如一个是 create <name> ,另一个只有 create,这里需要开发者人为封装一层 registerCommand ,按照自己的设定的规则注册相应的指令。

代码语言:javascript
复制
// 封装指令注册
  public registerCommand(command: string): Commander {
    // command是一个如 create <dir> [type] 的字符串,从中获取到命令 create 赋值给subCommand
    const subCommand = command.split(" ")[0];
    this.cli.on(`command:${subCommand}`, msg => {
      logger.debug(`执行指令${subCommand}`, msg);
      // TODO 上报
    });
    logger.debug(`正在注册指令${command}`);
    if (this.commandMap.has(subCommand)) {
      // 重复指令因为插件中的 commander 链式调用,直接返回其它类型会报错,所以再实例化一个 commander 作为子程序分离出去
      logger.warn(
        `${
          this.activePlugin
        } 正在注册的指令 ${subCommand} 已经被 ${this.commandMap.get(
          subCommand
        )} 注册过,跳过`
      );
      const tempProgram = new Commander();
      return tempProgram.command(command);
    }
    this.commandMap.set(subCommand, this.activePlugin);
    return this.cli.command(command);
  }

如上图是 IMFLOW 中的部分脱敏代码,这里要注意,因为编写插件的开发者在调用 registerCommand 方法的时候并不知道这一指令有没有注册过,所以大概率会按照 Commander.js 一往的链式调用方法书写下去,为了防止插件报错,当我们确定要弃用该指令的注册的时候,可以再实例化一个 Commander,将废弃的命令注册到该 Commander 身上而非主程序中的 Commander,这样既能够让该指令注册“流产”,也不会让插件在注册指令时报错。

7.4. 作为属性的遗留选项

在 Commander 7 以前,选项的值是作为属性存储在命令对象上的。这种处理方式便于实现,但缺点在于,选项可能会与Command的已有属性相冲突。通过使用.storeOptionsAsProperties(),可以恢复到这种旧的处理方式,并可以不加改动地继续运行遗留代码。

代码语言:javascript
复制
program
  .storeOptionsAsProperties()
  .option('-d, --debug')
  .action((commandAndOptions) => {
    if (commandAndOptions.debug) {
      console.error(`Called ${commandAndOptions.name()}`);
    }
  });

7.5. TypeScript

如果你使用 ts-node,并有.ts文件作为独立可执行文件,那么需要用 node 运行你的程序以使子命令能正确调用,例如:

代码语言:javascript
复制
node -r ts-node/register pm.ts

7.6. createCommand()

使用这个工厂方法可以创建新命令,此时不需要使用new,如

代码语言:javascript
复制
const { createCommand } = require('commander');
const program = createCommand();

createCommand同时也是Command对象的一个方法,可以创建一个新的命令(而非子命令),使用.command()创建子命令时内部会调用该方法。

7.7. Node 选项,如 --harmony

要使用--harmony等选项有以下两种方式:

  • 在子命令脚本中加上#!/usr/bin/env node --harmony。注:Windows 系统不支持;
  • 调用时加上--harmony参数,例如node --harmony examples/pm publish--harmony选项在开启子进程时仍会保留。

7.8. 调试子命令

一个可执行的子命令会作为单独的子进程执行。

如果使用 node inspector 的node -inspect等命令来调试可执行命令,对于生成的子命令,inspector 端口会递增 1。

如果想使用 VSCode 调试,则需要在launch.json配置文件里设置"autoAttachChildProcesses": true

7.9. 重写退出和输出

默认情况下,在检测到错误、打印帮助信息或版本信息时 Commander 会调用process.exit方法。其默认实现会抛出一个CommanderError,可以重写该方法并提供一个回调函数(可选)。

回调函数的参数为CommanderError,属性包括 Number 型的exitCode、String 型的codemessage。子命令完成调用后会开始异步处理。正常情况下,打印错误信息、帮助信息或版本信息不会被重写影响,因为重写会发生在打印之后。

代码语言:javascript
复制
program.exitOverride();

try {
  program.parse(process.argv);
} catch (err) {
  // 自定义处理...
}

Commander 默认用作命令行应用,其输出写入 stdout 和 stderr。对于其他应用类型,这一行为可以修改。并且可以修改错误信息的展示方式。

代码语言:javascript
复制
function errorColor(str) {
  // 添加 ANSI 转义字符,以将文本输出为红色
  return `\x1b[31m${str}\x1b[0m`;
}

program
  .configureOutput({
    // 此处使输出变得容易区分
    writeOut: (str) => process.stdout.write(`[OUT] ${str}`),
    writeErr: (str) => process.stdout.write(`[ERR] ${str}`),
    // 将错误高亮显示
    outputError: (str, write) => write(errorColor(str))
  });

8. 例子

在只包含一个命令的程序中,无需定义处理函数。

代码语言:javascript
复制
const { program } = require('commander');

program
  .description('Wanna Join TENCENT IMWEB?')
  .option('-s, --social', 'I\'m a candidate with work experience')
  .option('-g, --graduate', 'I\'m a graduate')
 .options('-p, --phone','Your phone number')

program.parse();

const options = program.opts();
console.log('Welcome to IMWEB');
if (options.social) console.log('-- has work experience');
const phone = !options.phone ? 'NA' : options.phone;
console.log('  - %s Phone Number', phone);

在包含多个命令的程序中,应为每个命令指定处理函数,或独立的可执行程序,下文的链式调用也是 IMFLOW 中使用的,开发者可以封装顶层的 .command 方法,加入上报和命令 Set 等操作,返回一个 command 实例以继续链式调用。

代码语言:javascript
复制
const { Command } = require('commander');
const program = new Command();

program
  .version('0.0.1')
  .option('-c, --config <path>', 'set config path', './deploy.conf');

program
  .command('setup [env]')
  .description('run setup commands for all envs')
  .option('-s, --setup_mode <mode>', 'Which setup mode to use', 'normal')
  .action((env, options) => {
    env = env || 'all';
    console.log('read config from %s', program.opts().config);
    console.log('setup for %s env(s) with %s mode', env, options.setup_mode);
  });

program
  .command('exec <script>')
  .alias('ex')
  .description('execute the given remote cmd')
  .option('-e, --exec_mode <mode>', 'Which exec mode to use', 'fast')
  .action((script, options) => {
    console.log('read config from %s', program.opts().config);
    console.log('exec "%s" using %s mode and config %s', script, options.exec_mode, program.opts().config);
  }).addHelpText('after', `
Examples:
  $ deploy exec sequential
  $ deploy exec async`
  );

program.parse(process.argv);

9. 支持

当前版本的 Commander 在 LTS 版本的 Node 上完全支持。Node 版本应不低于v12,使用更低版本 Node 的用户建议安装更低版本的 Commander。Commander 2.x 具有最广泛的支持,但注意 2.x 版本的 API 和本文存在较多出入,请以官方文档为准。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-26,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 前端Q 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 安装
  • 2. 声明 program 变量
  • 3. 选项
    • 3.1. 常用选项类型,boolean 型选项和带参数选项
      • 3.2. 选项的默认值
        • 3.3. 其他的选项类型,取反选项,以及可选参数的选项
          • 3.4. 必填选项
            • 3.5. 变长参数选项
              • 3.6. 版本选项
                • 3.7. 其他选项配置
                  • 3.8. 自定义选项处理
                  • 4. 命令
                    • 4.1. 命令参数
                      • 4.1.1. 其他参数配置
                      • 4.1.2. 自定义参数处理
                    • 4.2. 处理函数
                      • 4.3. 独立的可执行(子)命令
                        • 4.4. 生命周期钩子
                        • 5. 自动化帮助信息
                          • 5.1. 自定义帮助
                            • 5.2. 在出错后展示帮助信息
                              • 5.3. 使用代码展示帮助信息
                                • 5.4. .usage 和 .name
                                  • 5.5. .helpOption(flags, description)
                                    • 5.6. .addHelpCommand()
                                      • 5.7. 其他帮助配置
                                      • 6. 自定义事件监听
                                      • 7. 零碎知识
                                        • 7.1. .parse() 和 .parseAsync()
                                          • 7.2. 解析配置
                                            • 7.3. 指令重复注册的问题
                                              • 7.4. 作为属性的遗留选项
                                                • 7.5. TypeScript
                                                  • 7.6. createCommand()
                                                    • 7.7. Node 选项,如 --harmony
                                                      • 7.8. 调试子命令
                                                        • 7.9. 重写退出和输出
                                                        • 8. 例子
                                                        • 9. 支持
                                                        领券
                                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                                                        http://www.vxiaotou.com