NodeJS 下构建命令行工具 与 交互式命令界面 的实践
1. 概述
Commander.js
是一个在NodeJS 环境下便捷地用于构建搞质量命令行工具的库,vue-cli 等很多命令行工具都是由它构建。inquirer.js
是一个实现交互式命令行界面的 NodeJS 库,通过使用该库能够帮我们实现命令界面的交互式。kolorist
是一个
2. 命令的相关概念
3. 使用 Commander.js
搭建命令工行工具
3.1 安装
npm install commander # 或者: yarn add commander
3.2 引入 commander 的两种方式
方式1:
const { program } = require('commander');
方式2:
const { Command } = require('commander'); const program = new Command();
区别:
方式1中直接引入 commander
库中暴露(export
)的变量 program
是 Command
对象的实例,不需要再使用new
来创建 Command
对象的实例了,但对象名字只能是 program。
方式2中直接导入了 Command
对象,你需要手动创建它的实例:
const program = new Command();
这种方法下当然你可以将 Command
对象命名为其他的合法标识符,而不一定是program
。
3.3 从案例入门
3.3.1 例 1
1. 新建项目
新建一个 NodeJS 项目,默认初始化项目信息:
npm init -y
2. 安装commander.js
npm install commander
3. 编写脚本
建立文件eg1.js
,输入以下内容:
// eg1.js const { Command } = require('commander'); const program = new Command(); program .name('字符串工具') .description('一些JavaScript字符串实用程序的 命令行工具') .version('0.0.1'); program.command('split') .description('将字符串拆分成子字符串,并显示为数组') .argument('<string>', '要拆分的字符串') .option('--first', '仅显示第一个子字符串') .option('-s, --separator <char>', '分隔符字符', ',') .action((str, options) => { const limit = options.first ? 1 : undefined; console.log(str.split(options.separator, limit)); }); program.parse();
4. 运行脚本
现在在该项目的根目录下打开终端。在终端中测试我们的命令行工具脚本。
(1)获取你的 CLI tool 版本
node .\eg1.js -V
Out[]:
0.0.1
【评注】:可以看到,这个版本号就是上面代码块第8行.version('0.0.1');
所指定的版本号。
(2)获取CLI模块级帮助信息
node .\eg1.js -h
Out[]:
Usage: 字符串工具 [options] [command] 一些JavaScript字符串实用程序的 命令行工具 Options: -V, --version output the version number -h, --help display help for command Commands: split [options] <string> 将字符串拆分成子字符串,并显示为数组 help [command] display help for command
【评注】:可以看到,当前你的 CLI 模块一共有两个选项,分别是 -V
和 -h
这是模块级选项,不需要你手动定义,由 commander.js
内置提供。除了你自定义的命令split
外,还有一个名为help
的命令,它同样是由 commander.js
内置提供的。
(3)获取该 CLI 模块中某个命令的帮助信息
node .\eg1.js split -h
Out[]:
Usage: 字符串工具 split [options] <string> 将字符串拆分成子字符串,并显示为数组 Arguments: string 要拆分的字符串 Options: --first 仅显示第一个子字符串 -s, --separator <char> 分隔符字符 (default: ",") -h, --help display help for command
(4)测试你编写的命令
在脚本中,我们通过 .command(’split‘)
定义了一个名为split
的命令。
下面这个例子使用空格
作为分隔符:
node .\eg1.js split --separator=" " "hello, my name is Jack!"
Out[]:
[ 'hello,', 'my', 'name', 'is', 'Jack!' ]
下面这个例子指点逗号(,
)作为分隔符:
node .\eg1.js split --separator="," "hello, my name is Jack!"
Out[]:
[ 'hello', ' my name is Jack!' ]
以下是在 powershell 窗口实际运行的截图:
5. 归纳1:声明命令
在 3.2 小节中,我们介绍了引入commander的两种方式,默认情况下,使用program
作为实例对象的变量名。
在本例(【例1】) 我们使用program
构建了我们的第一个命令行工具。可以看到,这个例子中唯一的自定义命令split
就是通过program.command()
声明的的。
变量 program
是一个 Command 实例,而command()
是其下的一个实例方法,仍返回的Command:
Command.command(nameAndArgs: string, opts?: CommandOptions | undefined): Command
其详细的接口描述如下:
command(nameAndArgs: string, opts?: CommandOptions): ReturnType<this['createCommand']>; /** * 定义一个命令,在单独的可执行文件中实现。 * * @remarks * 命令描述作为第二个参数提供给 `.command`. * * @param nameAndArgs - 命令名和参数, args是 `<required>` 或者 `[optional]` ,last也可以是`variadic...` * @param description - 可执行命令的描述 * @param opts - 配置选项 * @returns 用于链接的 “this” 的 command */
例如:
program .command('start <service>', 'start named service') .command('stop [service]', 'stop named service, or all if no name supplied');
该方法还有一个重载
command(nameAndArgs: string, description: string, opts?: ExecutableCommandOptions): this; /** * 创建新的独立 command 的工厂例程。 * * 有关创建附加子命令的信息,请参见`.command()',该子命令使用此例程来创建命令。您可以覆盖 createCommand 来定制子命令。 */
6. 归纳2:声明选项
7. 归纳3:选项 类型 和 默认值
8. 拓展1:取反选项
9. 拓展2:可选参数的选项
10. 归纳4:处理函数
11. 归纳5:其它细节
(1)工具名称
通过.name()
方法可以用于声明你的命令行工具的版本号,例如在本例中:
program .name('字符串工具')
这样以后,我们在帮助文档中就可以看到你的命令行工具最开始的
Usage: 字符串工具 [options] [command]
(3)版本选项
通过.version()
方法可以用于声明你的命令行工具的版本号,用户在使用时,通过 3-3-1-4-1 小节 中的方法来来确定自己使用的工具脚本。例如:
program .version('0.0.1');
表示当前的版本号为0.0.1
。
3.4 进阶
这部分当前正在编辑中
3.4.1 补充
3.4.2 生命周期钩子
3.4.3 自动化帮助信息
3.5 接口声明
更多功能以及详细的接口语法格式,建议仔细阅读接口声明文件。在本章节中,给出了主要功能类的中文接口声明。
3.5.1 Command
类 接口声明
export class Command { args: string[]; processedArgs: any[]; commands: Command[]; parent: Command | null; constructor(name?: string); /** * 将程序版本设置为 `str` * * 该方法自动注册 "-V, --version" 标志,当传递时,该标志将打印版本号。 * * 您可以选择提供标志和描述来覆盖默认值。 */ version(str: string, flags?: string, description?: string): this; /** * 定义一个命令,使用 action 处理程序实现。 * * @remarks * 命令描述是使用 `.description` 提供的,而不是作为 `.command` 的参数。 * * @example * ```ts * program * .command('clone <source> [destination]') * .description('clone a repository into a newly created directory') * .action((source, destination) => { * console.log('clone command called'); * }); * ``` * * @param nameAndArgs - 命令名和参数,args 是 `<required>` 或者 `[optional]` 和 last 也可以是 `variadic...` * @param opts - 配置选项 * @returns new command */ command(nameAndArgs: string, opts?: CommandOptions): ReturnType<this['createCommand']>; /** * 定义一个命令,在单独的可执行文件中实现。 * * @remarks * 命令描述作为第二个参数提供给 `.command`。 * * @example * ```ts * program * .command('start <service>', 'start named service') * .command('stop [service]', 'stop named service, or all if no name supplied'); * ``` * * @param nameAndArgs - 命令名和参数,args 是 `<required>` 或者 `[optional]` 和 last 也可以是 `variadic...` * @param description - 可执行命令的描述 * @param opts - 配置选项 * @returns 用于链接的 `this` 命令 */ command(nameAndArgs: string, description: string, opts?: ExecutableCommandOptions): this; /** * 创建新的独立命令的工厂例程。 * * 有关创建附加子命令的信息,请参见`.command()`,该子命令使用此例程来创建命令。您可以覆盖createCommand来定制子命令。 */ createCommand(name?: string): Command; /** * 添加准备好的子命令。 * * 有关创建从其父命令继承设置的附加子命令,请参见 `.command()`。 * * @returns 用于链接的 `this` 命令 */ addCommand(cmd: Command, opts?: CommandOptions): this; /** * 创建新的独立参数的工厂例程。 * * 有关创建附加参数的信息,请参见`.argument()`,它使用此例程来创建参数。您可以重写 createArgument 以返回自定义参数。 */ createArgument(name: string, description?: string): Argument; /** * 定义命令的参数语法。 * * 默认情况下,参数是必需的,您可以在名称前后使用<>来明确指出这一点。 * 在可选参数的名称两边加上[]。 * * @example * ``` * program.argument('<input-file>'); * program.argument('[output-file]'); * ``` * * @returns 用于链接的 `this` 命令 */ argument<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this; argument(name: string, description?: string, defaultValue?: unknown): this; /** * 定义命令的参数语法,添加准备好的参数。 * * @returns 用于链接的 `this` 命令 */ addArgument(arg: Argument): this; /** * 定义命令的参数语法,一次添加多个(不带描述)。 * * See also .argument(). * * @example * ``` * program.arguments('<cmd> [env]'); * ``` * * @returns 用于链接的 `this` 命令 */ arguments(names: string): this; /** * 覆盖是否添加隐式帮助命令的默认决定。 * * @example * ``` * addHelpCommand() // 强制打开 * addHelpCommand(false); // 强制关闭 * addHelpCommand('help [cmd]', 'display help for [cmd]'); // 强制使用自定义细节 * ``` * * @returns 用于链接的 `this` 命令 */ addHelpCommand(enableOrNameAndArgs?: string | boolean, description?: string): this; /** * 为生命周期事件添加挂钩。 */ hook(event: HookEvent, listener: (thisCommand: Command, actionCommand: Command) => void | Promise<void>): this; /** * 注册回调以替换调用 process.exit。 */ exitOverride(callback?: (err: CommanderError) => never | void): this; /** * 显示错误信息并退出(或调用exitOverride)。 */ error(message: string, errorOptions?: ErrorOptions): never; /** * 您可以通过覆盖 createhelp 或使用 “configureHelp()” 覆盖帮助属性,用Help的子类自定义帮助。 */ createHelp(): Help; /** * 您可以通过使用configureHelp()覆盖帮助属性来自定义帮助,或者通过覆盖createHelp()使用帮助的子类。 */ configureHelp(configuration: HelpConfiguration): this; /** 获取配置 */ configureHelp(): HelpConfiguration; /** * 默认输出到stdout和stderr。您可以为特殊应用定制此功能。 * 您还可以通过覆盖outputError来自定义错误的显示。 * * 配置属性都是函数: * ``` * // 用于更改写入位置的函数,stdout 和 stderr * writeOut(str) * writeErr(str) * // 用于指定帮助换行宽度的匹配函数 * getOutHelpWidth() * getErrHelpWidth() * // functions based on what is being written out * outputError(str, write) // 用于显示错误,不用于显示帮助 * ``` */ configureOutput(configuration: OutputConfiguration): this; /** 获取配置 */ configureOutput(): OutputConfiguration; /** * 复制在 根命令 和 子命令 之间通用的有用设置。 * * (使用 `.command()' 添加命令时在内部使用,以便子命令继承父设置。) */ copyInheritedSettings(sourceCommand: Command): this; /** * 出现错误后显示帮助或自定义消息。 */ showHelpAfterError(displayHelp?: boolean | string): this; /** * 为未知命令显示类似命令的建议,或为未知选项显示选项。 */ showSuggestionAfterError(displaySuggestion?: boolean): this; /** * 为命令注册回调 `fn`。 * * @example * ``` * program * .command('serve') * .description('start service') * .action(function() { * // do work here * }); * ``` * * @returns 用于链接的 `this` 命令 */ action(fn: (...args: any[]) => void | Promise<void>): this; /** * 用 `flags`、`description`和可选强制`fn`定义选项。 * * `flags` 字符串包含短标志和/或长标志,由逗号、竖线或空格分隔。当使用 `-help' 时,以下都是有效的输出。 * * "-p, --pepper" * "-p|--pepper" * "-p --pepper" * * @example * ``` * // 简单boolean默认为false * program.option('-p, --pepper', 'add pepper'); * * --pepper * program.pepper * // => Boolean * * // 简单boolean默认为false * program.option('-C, --no-cheese', 'remove cheese'); * * program.cheese * // => true * * --no-cheese * program.cheese * // => false * * // 必需的参数 * program.option('-C, --chdir <path>', 'change the working directory'); * * --chdir /tmp * program.chdir * // => "/tmp" * * // 可选参数 * program.option('-c, --cheese [type]', 'add cheese [marble]'); * ``` * * @returns 用于链接的 `this` 命令 */ option(flags: string, description?: string, defaultValue?: string | boolean | string[]): this; option<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this; /** @deprecated 从v7开始,改为使用 `choices` 或自定义功能 */ option(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean | string[]): this; /** * 定义一个必需的选项,该选项在解析后必须有一个值。这通常意味着必须在命令行上指定该选项。 (否则与 `.option()` 相同。) * * `flags` 字符串包含 短标志 和/或 长标志,由逗号、竖线或空格分隔。 */ requiredOption(flags: string, description?: string, defaultValue?: string | boolean | string[]): this; requiredOption<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this; /** @deprecated 从v7开始,改为使用 `choices` 或自定义功能 */ requiredOption(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean | string[]): this; /** * 创建新的独立选项的工厂例程。 * * 有关创建附加选项的信息,请参见 `.option()`,它使用此例程来创建选项。您可以重写 createOption 以返回自定义选项。 */ createOption(flags: string, description?: string): Option; /** * 添加一个准备好的选项。 * * 有关在单个调用中创建和附加选项的信息,请参见 `.option()` 和 `.requiredOption()`。 */ addOption(option: Option): this; /** * 是将选项值作为属性存储在命令对象上,还是单独存储(指定false)。在这两种情况下,都可以使用 `.opts()` 来访问选项值。 * * @returns 用于链接的 `this` 命令 */ storeOptionsAsProperties<T extends OptionValues>(): this & T; storeOptionsAsProperties<T extends OptionValues>(storeAsProperties: true): this & T; storeOptionsAsProperties(storeAsProperties?: boolean): this; /** * 检索选项值。 */ getOptionValue(key: string): any; /** * 存储选项值。 */ setOptionValue(key: string, value: unknown): this; /** * 存储选项值以及该值的来源。 */ setOptionValueWithSource(key: string, value: unknown, source: OptionValueSource): this; /** * 检索选项值源。 */ getOptionValueSource(key: string): OptionValueSource; /** * 用可选值改变短标志的解析。 * * @example * ``` * // for `.option('-f,--flag [value]'): * .combineFlagAndOptionalValue(true) // `-f80` 被视为 `--flag=80`, 这是默认行为 * .combineFlagAndOptionalValue(false) // `-fb` 被视为 `-f -b` * ``` * * @returns 用于链接的 `this` 命令 */ combineFlagAndOptionalValue(combine?: boolean): this; /** * 允许命令行上的未知选项。 * * @returns 用于链接的 `this` 命令 */ allowUnknownOption(allowUnknown?: boolean): this; /** * 允许命令行上有过多的命令参数。传递false使多余的参数成为错误。 * * @returns 用于链接的 `this` 命令 */ allowExcessArguments(allowExcess?: boolean): this; /** * 启用位置选项。位置意味着在子命令之前指定全局选项,这允许子命令重用相同的选项名,也允许子命令打开 passThroughOptions 。 * * 默认行为是非定位的,全局选项可能出现在命令行的任何地方。 * * @returns 用于链接的 `this` 命令 */ enablePositionalOptions(positional?: boolean): this; /** * 传递命令参数之后的选项,而不是将它们视为命令选项, * 因此实际的命令选项在命令参数之前。 * 为子命令启用此选项需要在程序(父命令)上启用位置选项。 * * 默认行为是非定位的,选项可能出现在命令参数之前或之后。 * * @returns 用于链接的 `this` 命令 */ passThroughOptions(passThrough?: boolean): this; /** * 解析 `argv`,设置选项并在定义时调用命令。 * * 默认情况下,参数来自 node,应用程序为 argv[0],脚本在 argv[1] 中运行,其后是用户参数。 * * @example * ``` * program.parse(process.argv); * program.parse(); // 隐式使用 process.argv 和自动检测 node vs electron conventions * program.parse(my-args, { from: 'user' }); // 只是用户提供的参数,argv[0]没有什么特别的 * ``` * * @returns 用于链接的 `this` 命令 */ parse(argv?: readonly string[], options?: ParseOptions): this; /** * 解析 `argv`,设置选项并在定义时调用命令。 * * 如果您的任何操作处理程序是异步的,请使用 parseAsync 而不是 parse。 返回一个 Promise. * * 默认情况下,参数来自 node,应用程序为 argv[0],脚本在 argv[1] 中运行,其后是用户参数。 * * @example * ``` * program.parseAsync(process.argv); * program.parseAsync(); // 隐式使用 process.argv 和自动检测 node vs electron conventions * program.parseAsync(my-args, { from: 'user' }); // 只是用户提供的参数,argv[0]没有什么特别的 * ``` * * @returns Promise */ parseAsync(argv?: readonly string[], options?: ParseOptions): Promise<this>; /** * 从 `argv` 中解析选项,删除已知选项,并返回argv拆分为操作数和未知参数。 * * argv => operands, unknown * --known kkk op => [op], [] * op --known kkk => [op], [] * sub --unknown uuu op => [sub], [--unknown uuu op] * sub -- --unknown uuu op => [sub --unknown uuu op], [] */ parseOptions(argv: string[]): ParseOptionsResult; /** * 返回一个包含作为键值对的本地选项值的对象 */ opts<T extends OptionValues>(): T; /** * 返回一个对象,该对象包含作为键值对的合并的本地和全局选项值。 */ optsWithGlobals<T extends OptionValues>(): T; /** * 设置描述。 * * @returns 用于链接的 `this` 命令 */ description(str: string): this; /** @deprecated 从v8开始,改为使用 .argument 来添加带有描述的命令参数 */ description(str: string, argsDescription: {[argName: string]: string}): this; /** * 获取描述。 */ description(): string; /** * 设置摘要。当作为父命令的子命令列出时使用。 * * @returns 用于链接的 `this` 命令 */ summary(str: string): this; /** * 获取摘要。 */ summary(): string; /** * 为命令设置别名。 * * 您可以多次调用以添加多个别名。自动生成的帮助中只显示第一个别名。 * * @returns 用于链接的 `this` 命令 */ alias(alias: string): this; /** * 获取命令的别名。 */ alias(): string; /** * 为命令设置别名。 * * 自动生成的帮助中只显示第一个别名。 * * @returns 用于链接的 `this` 命令 */ aliases(aliases: readonly string[]): this; /** * 获取命令的别名。 */ aliases(): string[]; /** * 设置命令用法。 * * @returns 用于链接的 `this` 命令 */ usage(str: string): this; /** * 获取命令用法。 */ usage(): string; /** * 设置命令的名称。 * * @returns 用于链接的 `this` 命令 */ name(str: string): this; /** * 获取命令的名称。 */ name(): string; /** * 从脚本文件名设置命令的名称,例如 process.argv[1]、require.main.filename 或 __filename。 * * (虽然自述文件中没有记录,但在内部和公共场合使用。) * * @example * ```ts * program.nameFromFilename(require.main.filename); * ``` * * @returns 用于链接的 `this` 命令 */ nameFromFilename(filename: string): this; /** * 设置搜索此命令的可执行子命令的目录。 * * @example * ```ts * program.executableDir(__dirname); * // 或者 * program.executableDir('subcommands'); * ``` * * @returns 用于链接的 `this` 命令 */ executableDir(path: string): this; /** * 获取可执行文件搜索目录。 */ executableDir(): string; /** * 输出此命令的帮助信息。 * * 输出内置帮助和使用 `.addHelpText()` 添加的自定义文本。 * */ outputHelp(context?: HelpContext): void; /** @deprecated since v7 */ outputHelp(cb?: (str: string) => string): void; /** * 返回命令帮助文档。 */ helpInformation(context?: HelpContext): string; /** * 您可以传入标志和描述来覆盖命令的帮助标志和帮助描述。 * 传入false以禁用内置帮助选项。 */ helpOption(flags?: string | boolean, description?: string): this; /** * 输出帮助信息并退出。 * * 输出内置帮助和使用 `.addHelpText()` 添加的自定义文本。 */ help(context?: HelpContext): never; /** @deprecated since v7 */ help(cb?: (str: string) => string): never; /** * 添加要与内置帮助一起显示的附加文本。 * * Position为 `before` 或 `after` 时仅影响该命令,而 `beforeAll` 或 `afterAll` 时影响该命令及其所有子命令。 */ addHelpText(position: AddHelpTextPosition, text: string): this; addHelpText(position: AddHelpTextPosition, text: (context: AddHelpTextContext) => string): this; /** * 添加事件发生时的侦听器(回调)。(使用EventEmitter实现。) */ on(event: string | symbol, listener: (...args: any[]) => void): this; }
4. 使用 inquirer.js
构建交互式命令界面
当你使用 vue-cli (现在以被create-vue取代,但仍使用 inquirer )等脚手架创建 vue 项目时,一定见到过很炫酷的交互式问答,以让脚手架了解到你的项目需求,比如是否需要使用 vue-router,vuex(已被pinia替代),是否使用 CSS 预处理器(如less、sass),等等。这样的交互式问答功能就是由 使用 inquirer.js
所提供支持的。
当你自己搭建脚手架项目时,如果你的脚手架也要动态地了解用户本次执行的需求,那么使用 inquirer.js
是相当快捷、易于上手的选择。本章的任务就是学习和掌握如何使用 inquirer.js
来构造我们自己的一个交互式命令界面。
4.1 安装
npm install inquirer # 或者 yarn add inquirer
4.2 从一个示例开始
4.2.1 创建示例项目
在项目下安装依赖:
npm install --save inquirer@^8.0.0
新建文件 eg3.js (如果你喜欢也可以用其它文件名),并添加一下内容:
// eg3.js const inquirer = require('inquirer'); console.log('你好,欢迎体验 inquirer 命令行交互界面!'); console.log('下面我们将问你一些问题。。。'); const questions = [ { type: 'confirm', name: 'toBeDelivered', message: '今天天气好吗?(Y:好,N:不好)', default: true, }, { type: 'rawlist', name: 'select_number', message: '您是先生还是女士?(Y:先生,N:女士)', choices: ['先生', '女士'], }, { type: 'input', name: 'CSDN_name', message: "您的CSDN用户名是?", }, { type: 'input', name: 'phone', message: "您的手机号码是?", validate(value) { const pass = value.match( /^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i ); if (pass) { return true; } return '请输入有效的电话号码!!'; }, }, { type: 'list', name: 'fv_food', message: '您喜欢的食物类型是?', choices: ['甜食', '辛辣', '烧烤', '我不挑食'], }, { type: 'expand', name: 'select_key', message: '请依据 key 指定一个选项:', choices: [ { key: 'a', name: '选项1', value: 'A', }, { key: 'b', name: '选项2', value: 'B', }, { key: 'c', name: '选项3', value: 'C', }, ], }, { type: 'rawlist', name: 'select_number', message: '选择一个项目对应的编号', choices: ['第一', '第二', '第三'], }, { type: 'input', name: '评论', message: '输入一段话', default: '这是默认输入的一段话。', }, ]; inquirer.prompt(questions).then((answers) => { console.log('\n本次对话交互的信息如下:'); console.log(JSON.stringify(answers, null, ' ')); });
运行效果如下:
4.2.2 Inquirer 的导入方法
Inquirer v9 及更高版本是本机 esm 模块,这意味着您不能再使用 commonjs 语法require(‘inquirer’)。如果像本例中一样,使用传统 commonjs 语法,则需要像上面这样安装 Inquirer v8,否则需要使用import inquirer from 'inquirer'
的写法。比如本例使用:
npm install --save inquirer@^8.0.0 # 或者 yarn add inquirer@^8.0.0
则对应使用 commonjs 语法导入 inquirer:
const inquirer = require('inquirer');
而安装 Inquirer v9 之后的版本则可以直接安装,就像靠头那样:
npm install inquirer # 或者 yarn add inquirer
导入语法也对应换成 ES module的语法:
import inquirer from 'inquirer'
4.2.3 Inquirer 问题 与 question
对象
启动提示界面(查询会话)
inquirer.prompt(questions, answers) -> promise
4.2.4 Inquirer 回答(Answers)
4.2.5 Inquirer 分隔符(Separator)
4.2.6 Inquirer 问题提示类型(Separator)
(1)列表 list
例如:
{ type: 'list', name: 'fv_food', message: '您喜欢的食物类型是?', choices: ['甜食', '辛辣', '烧烤', '我不挑食'], },
(2)原始列表 rawlist
(3)扩展 expand
(4)选择框 checkbox
以下是一个使用了 checkbox 类型的完整示范:
const inquirer = require('inquirer'); inquirer .prompt([ { type: 'checkbox', message: '选择框', name: 'ck', choices: [ { name: 'option1' }, { name: 'option2'}, new inquirer.Separator('--- 下面的 option3 指定了默认被选中: ---'), { name: 'option3', checked: true, }, new inquirer.Separator('--- 下面的 option4 是一个被禁用的选项: ---'), { name: 'option4', disabled: '禁用的选项', }, { name: 'option5'}, ], validate(answer) { if (answer.length < 1) { return '您必须选择至少一个选项。'; } return true; }, }, ]) .then((answers) => { console.log("以下是你的选择结果:"); console.log(JSON.stringify(answers, null, ' ')); });
其效果如下:
(5)确认 confirm
例如:
{ type: 'confirm', name: 'toBeDelivered', message: '您的心情怎么样?(Y:好,N:不好)', default: true, },
(6)输入 input
例如:
以下是引导脚手架用户输入手机号码,并在手机号码输入错误的时候提示“请输入合法的手机号码!”例子:
{ type: 'input', name: 'phone', message: "您的手机号码是?", validate(value) { const pass = value.match( /^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i ); if (pass) { return true; } return '请输入合法的手机号码!'; }, },
以下是另外一个完整的示例:
const inquirer = require('inquirer'); inquirer .prompt([ { type: 'list', name: 'drinks', message: '😘你想来点什么呢?', choices: [ '咖啡', '奶茶' ], }, { type: 'list', name: 'size', message: '😘我们有以下几种被型,您要哪种呢?', choices: ['小杯(Small)', '中杯(Middle)', '大杯(Large)', '特大杯(Super Large)'], filter(val) { return val.toLowerCase(); }, }, ]) .then((answers) => { console.log(`您要的是${answers.size}的${answers.drinks}!`); });
其运行效果如下:
(7)数字 number
例如:
(8)密码 password
例如:
const inquirer = require('inquirer'); const requireLetterAndNumber = (value) => { if (/\w/.test(value) && /\d/.test(value)) { return true; } return '错误:密码至少需要有一个字母和一个数字'; }; inquirer .prompt([ { type: 'password', message: '这是不显示任何字符的密码输入形式 - 请输入密码:', name: 'psw1', validate: requireLetterAndNumber, }, { type: 'password', message: '这是使用屏蔽符显示密码输入形式 - 请输入密码:', name: 'psw2', mask: '*', validate: requireLetterAndNumber, }, ]) .then((answers) => { console.log(`完全不显示输入的密码是:${answers.psw1},显示为屏蔽符输入的密码是:${answers.psw2}`); });
其效果如下:
(9)编辑器 editor
例如:
5. 终端中使用色彩模块 kolorist
kolorist
不推荐使用。
5.1 安装 kolorist
npm install kolorist # 或者 yarn add kolorist # 或者 pnpm install kolorist
5.2 kolorist
用法
5.2.1 基础示例
import { red, cyan } from 'kolorist'; const line = red(`Error: something failed in ${cyan('my-file.js')}.`; console.log(line));
不仅是 console.log ,使用其它控制台输出,甚至是打印抛出错误的文本,都可以 通过 kolorist
来控制颜色,例如:
import { red } from 'kolorist' // ... if (values.shouldOverwrite === false) { throw new Error(red('✖') + ' Operation cancelled') }
5.2.2 接口
该模块接口声明如下:
export declare const enum SupportLevel { none = 0, ansi = 1, ansi256 = 2 } export declare let options: { enabled: boolean; supportLevel: SupportLevel; }; export declare function stripColors(str: string | number): string; export declare const reset: (str: string | number) => string; export declare const bold: (str: string | number) => string; export declare const dim: (str: string | number) => string; export declare const italic: (str: string | number) => string; export declare const underline: (str: string | number) => string; export declare const inverse: (str: string | number) => string; export declare const hidden: (str: string | number) => string; export declare const strikethrough: (str: string | number) => string; export declare const black: (str: string | number) => string; export declare const red: (str: string | number) => string; export declare const green: (str: string | number) => string; export declare const yellow: (str: string | number) => string; export declare const blue: (str: string | number) => string; export declare const magenta: (str: string | number) => string; export declare const cyan: (str: string | number) => string; export declare const white: (str: string | number) => string; export declare const gray: (str: string | number) => string; export declare const lightGray: (str: string | number) => string; export declare const lightRed: (str: string | number) => string; export declare const lightGreen: (str: string | number) => string; export declare const lightYellow: (str: string | number) => string; export declare const lightBlue: (str: string | number) => string; export declare const lightMagenta: (str: string | number) => string; export declare const lightCyan: (str: string | number) => string; export declare const bgBlack: (str: string | number) => string; export declare const bgRed: (str: string | number) => string; export declare const bgGreen: (str: string | number) => string; export declare const bgYellow: (str: string | number) => string; export declare const bgBlue: (str: string | number) => string; export declare const bgMagenta: (str: string | number) => string; export declare const bgCyan: (str: string | number) => string; export declare const bgWhite: (str: string | number) => string; export declare const bgGray: (str: string | number) => string; export declare const bgLightRed: (str: string | number) => string; export declare const bgLightGreen: (str: string | number) => string; export declare const bgLightYellow: (str: string | number) => string; export declare const bgLightBlue: (str: string | number) => string; export declare const bgLightMagenta: (str: string | number) => string; export declare const bgLightCyan: (str: string | number) => string; export declare const bgLightGray: (str: string | number) => string; export declare const ansi256: (n: number) => (str: string | number) => string; export declare const ansi256Bg: (n: number) => (str: string | number) => string; export declare function link(text: string, url: string): string;
6. 终端字符串样式模块: chalk
另一个功能上比较相似的模块是 chalk
(粉笔)。
注:
该模块可以采用功能更强大的颜色计算与终端输出工具 jc-color 替代,请参考博客《NodeJS、Borwser 中制作漂亮的炫彩控制台》:
6.1 chalk 的安装
npm install chalk # 或者 yarn add chalk # 或者 pnpm install chalk
6.2 chalk 的用法
6.2.1 入门案例
import chalk from 'chalk'; const lines = [ chalk.blue('Hello') + ' World' + chalk.red('!'), // 组合样式字符串和普通字符串 chalk.blue.bgRed.bold('Hello world!'), // 使用可链接API合成多种样式 chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'), // 传入多个参数 chalk.red('Hello', chalk.underline.bgBlue('world') + '!'), // 嵌套样式 chalk.green( '我是绿行 ' + chalk.blue.underline.bold('带有蓝色子串') + ' 那又变成绿色了!' ), // 使用模板字符串 ` CPU: ${chalk.red('90%')} RAM: ${chalk.green('40%')} DISK: ${chalk.yellow('70%')} `, chalk.rgb(123, 45, 67).underline('带下划线的红色'), // 在支持RGB颜色的终端模拟器中使用它。 chalk.hex('#DEADED').bold('Bold gray!') ]; for (const line of lines) { console.log(line); }
其效果如下:
6.2.2 ChalkInstance 接口
interface ChalkInstance { (...text: unknown[]): string; /** Chalk 的颜色支持。 默认情况下,会根据环境自动检测颜色支持。 Levels: - `0` - 禁用所有颜色。 - `1` - 基本16色支持。 - `2` - ANSI 256颜色支持。 - `3` - Truecolor支持1600万色。 */ level: ColorSupportLevel; rgb: (red: number, green: number, blue: number) => this; // 使用RGB值设置文本颜色。 hex: (color: string) => this; // 使用十六进制值设置文本颜色。 ansi256: (index: number) => this; // 使用一个8位无符号数设置文本颜色。 bgRgb: (red: number, green: number, blue: number) => this; // 使用RGB值设置背景颜色。 bgHex: (color: string) => this; // 使用十六进制值设置背景颜色。 bgAnsi256: (index: number) => this; // 使用一个8位无符号数设置背景色。 readonly reset: this; // Modifier: 重置当前样式。 readonly bold: this; // Modifier: 将文本加粗。 readonly dim: this; // Modifier: 使文本具有较低的不透明度。 readonly italic: this; // Modifier: 将文本设为斜体。 *(未得到广泛支持)* readonly underline: this; // Modifier: 在文本下面放一条水平线。 *(未得到广泛支持)* readonly overline: this; // Modifier: 在文本上方画一条水平线。 *(未得到广泛支持)* readonly inverse: this; // Modifier: 反转背景和前景颜色。 readonly hidden: this; // Modifier: 打印文本,但使其不可见。 readonly strikethrough: this; // Modifier: 在文本中心画一条水平线。 * (未得到广泛支持)* // Modifier: 仅当 Chalk 的色阶高于零时才打印文本。 // 对纯装饰性的东西很有用。 readonly visible: this; readonly black: this; readonly red: this; readonly green: this; readonly yellow: this; readonly blue: this; readonly magenta: this; readonly cyan: this; readonly white: this; readonly gray: this; // `blackBright` 的别名。 readonly grey: this; // `blackBright`的别名。 readonly blackBright: this; readonly redBright: this; readonly greenBright: this; readonly yellowBright: this; readonly blueBright: this; readonly magentaBright: this; readonly cyanBright: this; readonly whiteBright: this; readonly bgBlack: this; readonly bgRed: this; readonly bgGreen: this; readonly bgYellow: this; readonly bgBlue: this; readonly bgMagenta: this; readonly bgCyan: this; readonly bgWhite: this; readonly bgGray: this; // `bgBlackBright`的别名。 readonly bgGrey: this; // `bgBlackBright`的别名。 readonly bgBlackBright: this; readonly bgRedBright: this; readonly bgGreenBright: this; readonly bgYellowBright: this; readonly bgBlueBright: this; readonly bgMagentaBright: this; readonly bgCyanBright: this; readonly bgWhiteBright: this; }
7. 同时用于 NodeJS 和 浏览器的控制台记录器:consola
7.1 consola
介绍与安装
7.2 consola
的用法
7.2.1 基本用法示范
const consola = require('consola') // 也可以使用 ejs,注意不同环境: // import consola from 'consola/src/node' // import consola from 'consola/src/browser' // See types section for all available types consola.success('Built!') consola.info('Reporter: Some info') consola.error(new Error('Foo'))
8. 简单的CLI提示查询用户信息:prompts
8.1 安装 prompts
npm install prompts # 或 yarn add prompts # 或 pnpm install prompts
8.2 用法解析
8.2.1 入门示例
import prompts from 'prompts'; const questions = [ { type: 'select', name: 'meaning', message: 'What is your meaning of life?', choices: [ { title: 'Handsome Boy', value: 'Handsome Boy' }, { title: 'Beautiful Woman', value: 'Beautiful Woman' } ], }, { type: 'select', name: 'gender', message: 'Are you a boy or a girl?', choices: [ { title: 'Girl', value: 'firl' }, { title: 'Boy', value: 'boy' } ], }, { type: 'number', name: 'age', message: 'How old are you?' } ]; (async () => { const response = await prompts(questions); console.log('------------------------------------------'); console.log(`>> Your are a ${response.gender} loves ${response.meaning} and whose age is ${response.age}.`); console.log('------------------------------------------'); })();
8.2.2 提示类型
类型名 | 语法格式 | 描述 |
text |
text(message, [initial], [style]) | 自由文本输入的文本提示。 |
password |
password(message, [initial]) | 自由文本输入的文本提示。 |
invisible |
invisible(message, [initial]) | 提示用户输入不可见的文本。 |
number |
number(message, initial, [max], [min], [style]) | 提示用户输入不可见的文本。 |
confirm |
confirm(message, [initial]) | 经典是/否提示。 |
list |
list(message, [initial]) | 返回数组的列表提示。 |
toggle |
toggle(message, [initial], [active], [inactive]) | 返回数组的列表提示。 |
select |
select(message, choices, [initial], [hint], [warn]) | 返回数组的列表提示。 |
multiselect |
multiselect(message, choices, [initial], [max], [hint], [warn]) | 交互式多选提示。 |
autocompleteMultiselect |
autocompleteMultiselect(same) | 交互式多选提示。 自动完成是具有相同选项的可搜索多选提示。对于长列表很有用。 |
autocomplete |
autocomplete(message, choices, [initial], [suggest], [limit], [style]) | 交互式自动完成提示。 |
date |
date(message, [initial], [warn]) | 交互式自动完成提示。 |
9. 终端旋转亮片: ora
9.1 ora 的安装
npm install ora # or yarn add ora # or pnpm install ora
9.2 快速入门
import ora from 'ora'; const spinner = ora('Loading').start(); setTimeout(() => { spinner.color = 'yellow'; spinner.text = 'Loading ...'; }, 1000);
9.3 Ora 接口
我们使用的 ora 函数的类型声明如下:
export default function ora(options?: string | Options): Ora;
如果 ora 函数提供了 options 字符串,它将被视为 options.text
的快捷方式。其中参数 options
除了可以是一个字符串外还可以是一个
export interface Options { // 旋转亮片后显示的文本。 readonly text?: string; // 文本或返回文本以在旋转亮片前显示的函数。如果设置为空字符串,将不显示前缀文本。 readonly prefixText?: string | PrefixTextGenerator; // 所提供的旋转亮片之一的名称。在Windows上,它将总是使用line旋转亮片,因为Windows命令行没有适当的Unicode支持。 readonly spinner?: SpinnerName | Spinner; // 旋转亮片的颜色。 // 默认值为 'cyan' readonly color?: Color; // 设置为'false' 以阻止Ora 隐藏光标。 // 默认值为 true readonly hideCursor?: boolean; // 给定的空格数缩进旋转亮片。 // 默认值为 0 readonly indent?: number; // 每帧(frame)之间的间隔。 // 旋转亮片提供了它们自己推荐的时间间隔,所以您实际上不需要指定。 // 默认值: 由旋转器或“100”提供。 readonly interval?: number; // 来写入输出的流。 // 例如,您可以将其设置为“process.stdout”。 // 默认值为 process.stderr readonly stream?: NodeJS.WritableStream; /** Force enable/disable the spinner. If not specified, the spinner will be enabled if the `stream` is being run inside a TTY context (not spawned or piped) and/or not in a CI environment. Note that `{isEnabled: false}` doesn't mean it won't output anything. It just means it won't output the spinner, colors, and other ansi escape codes. It will still log text. */ readonly isEnabled?: boolean; // 禁用旋转亮片和所有日志文本。所有输出都被抑制,并且“isEnabled”将被视为“false”。 // 默认值为 false readonly isSilent?: boolean; // 如果是TTY,在运行时丢弃标准输入(除了Ctrl+C)。这防止了旋转器在输入时抖动,在按下“Enter”键时输出虚线,并且防止了旋转器运行时输入的缓冲。 // 这对Windows没有影响,因为没有好的方法来正确地实现丢弃stdin。 // 默认值为 true readonly discardStdin?: boolean; }
ora 函数返回一个 Ora 接口的对象,Ora 类型的接口声明如下:
export interface Ora { // 更改旋转亮片后的文本。 text: string; // 更改在旋转亮片之前返回文本的文本或函数。 // 如果设置为空字符串,将不显示前缀文本。 prefixText: string; // 改旋转亮片颜色。 color: Color; // 更改旋转亮片缩进。 indent: number; // 获取旋转亮片 get spinner(): Spinner; // 设置旋转亮片 set spinner(spinner: SpinnerName | Spinner); // 实例当前是否正在旋转的布尔值。 get isSpinning(): boolean; // 每帧之间的间隔。 // 间隔由选定的旋转亮片决定。 get interval(): number; // 启动旋转亮片 // text - 设置的当前文本 // 返回旋转亮片实例 start(text?: string): this; // 停止和清除旋转亮片 // 返回旋转亮片实例 stop(): this; // 停止旋转亮片,将其更改为绿色`✔`,并保持当前文本,或'text'(如果提供)。 // text - 如果提供,将保留文本。 // 返回旋转亮片实例 succeed(text?: string): this; // 停止旋转,将其更改为红色`✖`,并保留当前文本,或“text”(如果提供)。 // text - 如果提供,将保留文本。 // 返回旋转亮片实例 fail(text?: string): this; // 停止旋转亮片,将其更改为黄色`⚠`,并保持当前文本,或'text' (如果提供)。 // text - 如果提供,将保留文本。 // 返回旋转亮片实例 warn(text?: string): this; // 停止旋转亮片,将其更改为蓝色`ℹ`,并保持当前文本,或'文本'(如果提供)。 // text - 如果提供,将保留文本。 // 返回旋转亮片实例 info(text?: string): this; // 停止旋转亮片并更改符号或文本。 // 返回旋转亮片实例 stopAndPersist(options?: PersistOptions): this; // 清除旋转亮片 // 返回旋转亮片实例 clear(): this; // 手动渲染一个新的 frame. // 返回旋转亮片实例 render(): this; // 获取一个新的 frame. // 旋转亮片实例文本。 frame(): string; }
10. 终端渐变色字符串:gradient-string
10.1 简介与安装
还记得你使用 create-vue 脚手架 创建 vue 项目时出现的炫酷文字吗:
gradient-string
模块就可以实现这样的效果。其安装方法为:
npm i gradient-string # 或 yarn add gradient-string # 或 pnpm i gradient-string
注:
该模块可以采用功能更强大的颜色计算与终端输出工具 jc-color 替代,请参考博客《NodeJS、Borwser 中制作漂亮的炫彩控制台》:
10.2 gradient-string
入门案例
import gradient from 'gradient-string'; const g = gradient('cyan', 'pink')('Hello world!'); console.log(g);
11. 终端彩色文字动画: chalk-animation
11.1 简介与安装
如果终端彩色渐变字还不能让你感觉过瘾,那么就的好好试试这款炫酷的彩色文字动画模块了,它就是 chalk-animation
,其安装方式如下:
npm i chalk-animation # 或 yarn add chalk-animation # 或 pnpm i chalk-animation
11.2 chalk-animation
入门案例
13. 其它工具
8.1 通过boxen
在终端中创建方框
5.2.1 安装 kolorist
npm install boxen # 或者 yarn add boxen # 或者 pnpm install boxen