前端命令行交互解决方案(一)

3,424 阅读6分钟

本文参考了Commander.js中文文档跟着老司机玩转Node命令行inquirer.js —— 一个用户与命令行交互的工具

Commander.js

提供了用户命令行输入和参数解析的强大功能,帮助我们简化命令行开发,通过 主指令 + 子指令 + 参数 的模式运行命令,具有以下特性:

  • 参数解析
  • 强制多态
  • 可变参数
  • Git 风格的子命令
  • 自动化帮助信息
  • 自定义帮助等

准备工作

安装 node.js 安装 commander.js,执行 npm install commander --save

demo

/// 任意目录新建任意文件,比如 index.js
const program = require("commander");

program
    .version("1.0.0")
    .parse(process.argv);   

在控制台中执行 node index.js -V,就可以看到版本信息的输出

API

命令 说明
.version 显示版本命令,默认选项标识为 -V--version,当存在时会打印版本号并退出
.parse 和 .parseAsync 用于解析 process.argv,设置 options 以及触发 commands(解析命令行)
.option 定义命令的选项
.requiredOption 设置选项为必填,必填选项要么设有默认值
.command 和 .addCommand 添加命令名称
.alias 定义命令的别名
.usage 定义命令的用法
.description 定义命令的描述
.action 定义命令的回调函数
.help(cb) 输出帮助信息并立即退出
.outputHelp(cb) 输出帮助信息的同时不退出
.helpInformation() 获取除了--help以外的帮助信息
.helpOption(flags, description) 重写覆盖默认的帮助标识和描述
program.helpOption('-e, --HELP', 'read more information')
.addHelpCommand() 打开或关闭(.addHelpCommand(false))隐式的帮助命令

version

除使用默认的参数,还可以自定义标识,通过给 version方法再传递一个参数,语法与option方法一致

program.version('0.0.1', '-v, --vers', 'output the current version')

.parse 和 .parseAsync

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

program.parse(process.argv); // 显式的 node 约定
program.parse(); // 隐式的,自动监测的 electron

process.argv 属性会返回一个数组,其中包含当 Node.js 进程被启动时传入的命令行参数。 第一个元素是 process.execPath(启动 Node.js 进程的可执行文件的绝对路径名 )。 第二个元素是正被执行的 JavaScript 文件的路径。 其余的元素是任何额外的命令行参数

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

program.parse(['-f', 'filename'], { from: 'user' });

·option

使用 .option() 方法定义 commander 的选项 options,每个选项可以有一个短标识(单个字符)和一个长名字,它们之间用逗号或空格或 '|' 分开。

.option('-n, --name<path>(自定义标)', 'name description(选项描述)', 'default name(默认值)')

option接收三个参数:

  • 自定义标志<必须>:分为长短标识,中间用逗号、竖线或者空格分割;标志后面可跟必须参数或可选参数,前者用 <> 包含,后者用 [] 包含
  • 选项描述<非必须>:在使用 --help 命令时显示标志描述
  • 默认值<非必须>
  • 多词选项如 "--template-engine" 会被转为驼峰法 program.templateEngine
  • 多个短标识可以组合为一个破折号开头的参数:布尔标识和值,并且最后一个标识可以附带一个值。 例如,-a -b -p 80 也可以写作 -ab -p80 甚至 -abp80

最常用的两个选项类型是 boolean(选项后面不跟值)选项跟一个值(使用尖括号声明),除非在命令行中指定,否则两者都是 undefined

const { program } = require("commander");

program
  .version("1.0.0")
  .option("-d, --debug", "output extra debugging")
  .option("-s, --small", "small pizza size")
  .option("-p, --pizza-type <type>", "flavour of pizza")
  .option("-c, --cheese <type>", "add the specified type of cheese", "blue")
  .parse(process.argv);

if (program.debug) console.log(program.opts());
console.log("pizza details:");
if (program.small) console.log("- small pizza size");
if (program.pizzaType) console.log(`- ${program.pizzaType}`);
  • node index.js --help 自动化帮助信息 help
/// 控制台输出
Usage: index [options]

Options:
  -V, --version            output the version number
  -d, --debug              output extra debugging
  -s, --small              small pizza size
  -p, --pizza-type <type>  flavour of pizza
  -c, --cheese <type>      add the specified type of cheese (default: "blue")
  -h, --help               display help for command

还可以通过监听 --help 来显示额外信息,在调用完成后触发

  .on("--help", () => {
    console.log("");
    console.log("Example call(使用例子):");
    console.log("  $ custom-help --help");
  }) // 要在 parse 之前使用
  .parse(process.argv);
...
/// 控制台比上面回多输出

Example call(使用例子):
  $ custom-help --help

此时 usage 的信息是系统自动生成的,可以通过设置 .usage.name 设置个性化帮助信息说明

	.name("my-command")
  .usage("[global options] command")
  .parse(process.argv);
  ...
/// 此时控制台输出
Usage: my-command [global options] command // 之前为Usage: index [options]

Options:
  • node index.js -d
/// 控制台返回
/// program.opts 展示的就是所有 option 的设置值
/// 此处的运行命令是 -d 就是启用选项,但是选项后面不跟值,所以对应的值为true,其他未设定为 undefined
{ version: '1.0.0',
  debug: true,
  small: undefined,
  pizzaType: undefined } // 多词选项转为驼峰
pizza details:
cheese: blue
  • node index.js -p
/// 控制台返回
error: option '-p, --pizza-type <type>' argument missing
/// 因为 p 后的参数是必要参数
  • node index.js -p vegetarian
/// 控制台返回
pizza details:
- vegetarian
cheese: blue
...
/// 此处等价于下面的写法
/// node index.js --pizza-type=vegetarian
  • node index.js -ds
/// 控制台返回
/// 多个短标示组合为一个参数 -ds
{ version: '1.0.0',
  debug: true,
  small: true,
  pizzaType: undefined }
pizza details:
- small pizza size
cheese: blue
  • 选项默认值 上面都会有个输出 cheese: blue 这个 blue-c 时对应的默认值
/// 虽然有默认值,如果要去设置相关option时,还是要传递一个值(此时时必传)
node index.js -c // error: option '-c, --cheese <type>' argument missing
  • 使用 -- 来指示选项的结束,任何剩余的参数会正常使用,而不会被命令解释
/// 按定义,如果不使用 -- 会提醒差参数,使用后就直接结束,不进行后面的解释
node index.js -- p
  • --no 前缀开头的多词选项是其后选项的布尔值的反。 例如,--no-sauceprogram.sauce 的值设置为 false
const { program } = require('commander');
program
	.version('1.0.0')
	.option('-n --no-sauce', 'Remove sauce')
	.parse(process.argv);
	
if (program.sauce) console.log("sauce");
...
/// 单独定义,选项默认值为 true
node index.js // { version: '1.0.0', sauce: true }
...
node index.js -n // { version: '1.0.0', sauce: false }

如果先定义 --foo,再加上 --no-foo 并不会改变它本来的默认值,需要自行定义

program
  .version("1.0.0")
  //   .option("-d, --debug", "output extra debugging")
  //   .option("-s, --small", "small pizza size")
  //   .option("-p, --pizza-type <type>", "flavour of pizza")
  //   .option("-c, --cheese <type>", "add the specified type of cheese", "blue")
  .option("-n --no-sauce", "Remove sauce")
  .option("-f --foo", "test foo")
  .option("-nf --no-foo", "test no foo")
  .parse(process.argv);

if (program.debug) console.log(program.opts());
console.log(program.opts());
...
/// 同时定义了 --foo、--no-foo 就有默认值
node index.js // { version: '1.0.0', sauce: true, foo: undefined }
...
/// 需要自行处理
node index.js -nf // { version: '1.0.0', sauce: true, foo: false }
自定义选项处理

option 还可以自定义处理函数,其接收两个参数:用户传入的值上一个值(previous value),它会返回新的选项值(可以将选项值强制转换为所需类型,或累积值,或完全自定义处理),在函数后面指定选项被当作默认或初始值

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

const { program } = require("commander");

const setFoo = (value, previousValue) => {
	// 传入的 value 默认会是 string 类型
  return Number(value) + 1;
};

program
  .version("1.0.0")
  .option("-f --foo <number>", "set foo info", setFoo) // 第三个参数接收这个函数
  .parse(process.argv);

console.log(program.opts());
...
node index.js -f 21; // { version: '1.0.0', foo: 22 } 接收的值为处理后的值

在函数后面指定值

const { program } = require("commander");

const setFoo = (value, previousValue) => {
  console.log("value", value);
  console.log("proValue", previousValue);
  return Number(value) + 1;
};

program
  .version("1.0.0")
  .option("-F --Foo <number>", "set foo info", setFoo, 20)
  .parse(process.argv);

console.log(program.opts());
...
node index.js -F 10; 
...
value 10 // 函数接收用户的传入值
proValue 20 // 20 是设定的默认值
{ version: '1.0.0', Foo: 11 }

必需选项

你可以使用 .requiredOption 指定一个必需(强制性)选项,可以指定或者给定一个默认值

const { program } = require("commander");

const setFoo = (value, previousValue) => {
  console.log("value", value);
  console.log("proValue", previousValue);
  return Number(value) + 1;
};

program
  .version("1.0.0")
  .requiredOption("-F --Foo <number>", "set foo info", setFoo, 20)
  .parse(process.argv);

console.log(program.opts());
...
node index.js; // { version: '1.0.0', Foo: 20 } 20是默认值
/// 若删除默认值会报 error: required option '-F --Foo <number>' not specified

command/addCommand

定义命令行指令

.command('rmdir <dir> [otherDirs...](命令名称)', 'install description(命令描述)', opts(配置选项))

参数解析:

  • 命令名称<必须>:命令后面可跟用 <> 或 [] 包含的参数;命令的最后一个参数可以是可变的,像实例中那样在数组后面加入 ... 标志;在命令后面传入的参数会被传入到 action 的回调函数以及 program.args 数组中
  • 命令描述<可省略>:如果存在,且没有显示调用action(fn),就会启动子命令程序,否则会报错
  • 配置选项<可省略>:可配置 noHelpisDefault
const { program } = require("commander");

program
  .command("clone <source> [destination]")
  .description("clone a repository into a newly created directory") // description 可以不在 command 中填写
  .action((source, destination) => {
    console.log(source); // 这里的 source 是从 command 中接收的
    console.log("clone command called");
  });

program.parse(process.argv);
console.log(program.args);
...
node index.js clone /bash;
/// 控制台输出
/bash // action 中的 source
clone command called
[ 'clone', '/bash' ] // 这是 program.args 输出
指定参数语法

可以通过使用 .arguments 来为最顶级命令(the top-level command)指定参数,并且对于子命令来说参数都在 .command 的对应回调中。

尖括号 <> 意味着必须的输入,而方括号 [] 则是代表了可选的输入

const program = require("commander");

program
  .version("0.1.0")
  .arguments("<cmd> [env]")
  .action(function(cmd, env) {
    cmdValue = cmd;
    envValue = env;
  });

program.parse(process.argv);

if (typeof cmdValue === "undefined") {
  console.error("no command given!");
  process.exit(1);
}
console.log("command:", cmdValue);
console.log("environment:", envValue || "no environment given");
...
node index.js cmd env // 对应顶级命令参数
/// 终端输出
command: cmd
environment: env

一个命令有且仅有最后一个参数是可变的,通过类似剩余参数的写法进行调用

const { program } = require("commander");

program
  .version("0.1.0")
  .command("rmdir <dir> [otherDirs...]")
  .action(function(dir, otherDirs) {
    console.log("rmdir %s", dir);
    if (otherDirs) {
      otherDirs.forEach(function(oDir) {
        console.log("rmdir %s", oDir);
      });
    }
  });

program.parse(process.argv);
...
node index.js rmdir foo info
/// 终端输出
rmdir foo
rmdir info
子命令的 action handler

可以为一个子命令的 option 添加 action handler,这个 回调函数,接收两个参数。 第一个是声明的参数的变量,第二个命令对象自己。

const program = require("commander");

program
  .command("rm <dir>")
  .option("-r, --recursive", "Remove recursively")
  .action(function(dir, cmdObj) {
    console.log("remove " + dir + (cmdObj.recursive ? " recursively" : ""));
  });

program.parse(process.argv);
...
node index.js rm obj -r
/// 终端输出
remove obj recursively
自定义事件监听

通过 program.on() 监听相关 option 或 command

const program = require("commander");
program.version("0.0.1").option("-l --list", "show list");

program.on("option:list", function() {
  console.log("option list call");
});
program.parse(process.argv);
...
node index.js -l
/// 终端输出
option list call

inquirer.js

在开发的过程中,我们需要频繁的跟命令行进行交互,借助 inquirer 这个模块就能轻松实现,它提供了用户界面和查询会话流程(就是那种问答式交互)

安装

npm i -S inquirer

基本用法

const inquirer = require('inquirer');

const promptList = [
    // 具体交互内容 (问题列表)
];

inquirer.prompt(promptList).then(answers => { // 返回结果 })
  .catch(error => {
    if(error.isTtyError) {
      // 无法在当前环境中呈现提示
    } else {
      // 别的错误
    }
  });

具体交互内容的格式如下:

{
    type: "input",
    message: "设置一个用户名:",
    name: "name",
    default: "test_user" // 默认值
  }

下面是一个完整的例子

const inquirer = require("inquirer");

const promptList = [
  {
    type: "input",
    message: "设置一个用户名:",
    name: "name",
    default: "test_user" // 默认值
  }
];

inquirer.prompt(promptList).then(answer => {
  console.log(answer);
});

API

参数 类型 描述
type String 表示提问的类型
包括input(默认), number, confirm, list, rawlist, expand, checkbox, password, editor
name String 存储当前问题回答的变量
message ``String Function``
default ``String Number
choices ``Array Function``
validate Function 对用户的回答进行校验
filter Function 对用户的回答进行过滤处理,返回处理后的值
transformer Function 对用户回答的显示效果进行处理(如:修改回答的字体或背景颜色),但不会影响最终的答案的内容;
when Function, Boolean 根据前面问题的回答,判断当前问题是否需要被回答
pageSize Number 修改某些type类型下的渲染行数
prefix String 修改message默认前缀
suffix String 修改message默认后缀
askAnswered Boolean 如果答案已经存在,则强制提示该问题
loop Boolean 启用列表循环(默认值 true)

常见例子

input

const promptList = [
  {
    type: "input",
    message: "设置一个用户名:",
    name: "name",
    default: "test_user" // 默认值
  },
  {
    type: "input",
    message: "请输入手机号:",
    name: "phone",
    validate: function(val) { // validate的使用例子
      if (/^1[3456789]\d{9}$/.test(val)) {
        // 校验手机号是否正确
        return true;
      }
      return "请输入正确的手机号";
    }
  }
];

number

const promptList = [
  {
    type: "number",
    message: "你的手机号:",
    name: "phone"
  }
];

输入的非数字会被转为 NaN,这个参数有点 input 语法糖的意思。input 和 validate 能实现一样的功能

confirm

const promptList = [
  {
    type: "confirm",
    message: "是否使用监听?",
    name: "watch",
    prefix: "前缀"
  },
  {
    type: "confirm",
    message: "是否进行文件过滤?",
    name: "filter",
    suffix: "后缀",
    when: function(answers) { // 上一个问题答案为true时展示该问题(when的用法)
      return answers.watch;
    }
  }
];

list

const promptList = [
  {
    type: "list",
    message: "请选择一种水果:",
    name: "fruit",
    choices: ["Apple", "Pear", "Banana"],
    filter: function(val) { // filter 的用法
      // 使用filter将回答变为小写
      return val.toLowerCase();
    }
  }
];

rawlist

list 一样,都是列表展示,这个会显示数字

const promptList = [
  {
    type: "rawlist",
    message: "请选择一种水果:",
    name: "fruit",
    choices: ["Apple", "Pear", "Banana"]
  }
];

expand

指定命令的联想输入

const promptList = [
  {
    type: "expand",
    message: "请选择一种水果:",
    name: "fruit",
    choices: [
      {
        key: "a",
        name: "Apple",
        value: "apple"
      },
      {
        key: "O",
        name: "Orange",
        value: "orange"
      },
      {
        key: "p",
        name: "Pear",
        value: "pear"
      }
    ]
  }
];

只能输入 aOP 这些设定好的参数,如果故意输错,按确定系统还好把这些问题转为 rawlist 供选择

checkbox

const promptList = [
  {
    type: "checkbox",
    message: "选择颜色:",
    name: "color",
    choices: [
      {
        name: "red"
      },
      new inquirer.Separator(), // 添加分隔符
      {
        name: "blur",
        checked: true // 默认选中
      },
      {
        name: "green"
      },
      new inquirer.Separator("--- 分隔符 ---"), // 自定义分隔符
      {
        name: "yellow"
      }
    ]
  }
];

运行时会有这么一个提示:Press <space> to select, <a> to toggle all, <i> to invert selection(空格 选择,a 全选,i 反选)

/// 还有另外一种格式
const promptList = [
  {
    type: "checkbox",
    message: "选择颜色:",
    name: "color",
    choices: ["red", "blur", "green", "yellow"],
    pageSize: 2 // 设置行数
  }
];

这种模式下,可以固定设置展示几行,可以通过上下的方向键查看选项,选项是会循环,如果不想循环可以设置 loop: false 停用

password

const promptList = [
  {
    type: "password", // 密码为密文输入
    message: "请输入密码:",
    name: "pwd"
  }
];

editor

const promptList = [
  {
    type: "editor",
    message: "请输入备注:",
    name: "editor"
  }
];

运行后会进入到一个 vim 的编辑器

插件

除了上面提到的 type 类型,inquirer.js 提供了可以自定义的插件方法 inquirer.registerPrompt(name, prompt)(上面使用的方法是 inquirer.prompt(questions) -> promise),社区已经有了些比较优秀的插件

inquirer-table-prompt

这是一个 table 表格插件,针对那种大量选择的场景比较有效(其他插件到 inquirer 的 github 主页去查看)

const inquirer = require("inquirer");
// 要先安装 inquirer-table-prompt
inquirer.registerPrompt("table", require("inquirer-table-prompt"));

const promptList = [
  {
    type: "table", // registerPrompt 的第一个参数,第二个参数是引入相关库
    name: "workoutPlan",
    message: "Choose your workout plan for next week",
    columns: [
      {
        name: "Arms",
        value: "arms"
      },
      {
        name: "Legs",
        value: "legs"
      },
      {
        name: "Cardio",
        value: "cardio"
      },
      {
        name: "None",
        value: undefined
      }
    ],
    rows: [
      {
        name: "Monday",
        value: 0
      },
      {
        name: "Tuesday",
        value: 1
      },
      {
        name: "Wednesday",
        value: 2
      },
      {
        name: "Thursday",
        value: 3
      },
      {
        name: "Friday",
        value: 4
      },
      {
        name: "Saturday",
        value: 5
      },
      {
        name: "Sunday",
        value: 6
      }
    ]
  }
];

inquirer.prompt(promptList).then(answer => {
  console.log(answer);
});

chalk.js

一个美化插件,node终端样式库,没有什么特别好介绍的,具体颜色和API参看 官方文档

可以利用这个库,把我们写出的交互命令适当的添加些颜色,方便查看区分

const chalk = require("chalk");

const promptList = [
  {
    type: "number",
    message: "你的手机号:",
    name: "phone",
    validate: function(val) {
      // validate的使用例子
      if (/^1[3456789]\d{9}$/.test(val)) {
        // 校验手机号是否正确
        return true;
      }
      return chalk.red("请输入正确的手机号"); // 把需要变色的部分包裹起来即可
    }
  }
];