Nodejs 实现命令行工具

133 阅读6分钟

创建如下目录结构,mycli 是根目录文件名:

mycli
└─bin 
  └─cli.js 

使用 npm init -y 初始化 package.json

# 根目录下执行
npm init -y

初始化完成后的 package.json 的配置和平常的对比,多了一个 bin 的配置。

意思是,定义了一个叫 mycli 的命令,执行这个命令会运行 bin/cli.js 这个文件。

{
  more···
  "bin": {
    "mycli": "bin/cli.js"
  },
  more···
}

使用 npm link 命令,让 mycli 成为一个全局命令。

bin/cli.js 添加一些代码:

#! /usr/bin/env node
console.log("hello");

现在,使用 mycli 命令,会打印出 hello

01.png

修改 bin/cli.js 的代码为:

#! /usr/bin/env node
console.log(process.argv);

process.argv 可以获取执行脚本时的命令行参数,效果如下:

02.png

这次执行 mycli 时增加了 --help 参数。

打印的结果里,有三个参数,第一个是执行脚本的 node.exe 程序,第二个是脚本所在的位置,这两个是固定参数。

第二个之后的全都是用户传入的参数,可以看到第三个就是传入的 --help 参数,在编写逻辑时,可以通过下标获取对应的参数。

命令参数处理

由于自己手动编码处理命令参数特别麻烦,需要考虑很多情况,比如,用户使用了哪些参数,没有使用哪些参数。

此时,借助别人封装好的方法来处理,是更为明智的选择。使用 commander 可以优雅的处理命令参数,安装方法如下:

# 根目录下执行
npm install commander

安装完成后,修改 bin/cli.js 代码:

#! /usr/bin/env node
const { program } = require("commander");

program.parse();

虽然只添加了两行代码,但是我们的 cli 工具却多了不止两个功能,看一下运行示例:

03.png

添加 --help 之后,会打印出我们的 cli 可以使用哪些参数的提示。commander 会帮我们实现一个默认的 --help 执行逻辑。

当使用未定义的 -f 参数时,commander 也会自动的发出没有这个参数选项的错误提示。

使用 program.option() 方法,给 cli 添加新的可执行参数。

#! /usr/bin/env node
const { program } = require("commander");

program.option("-f, --first <char>", "创建的第一个参数");

program.parse();

现在,可以向我们的 cli 传入 -f--firsr 参数。<char> 是这个命令对应传入的值,尖括号代表使用这个命令时,必须传入的参数。

04.png

可以看到,现在 --help 多出了 -f 命令相关的提示。使用 -f 参数,如果不传 <char>,会输出报错提示,只有输入正确的参数和值才能正常执行。

处理自定义指令选项

在使用 vue-cli 这个命令行工具时,可以使用 vue create myproject 这样的命令创建 vue 项目。下边使用 commander 实现一个类似这样的指令。

#! /usr/bin/env node
const { program } = require("commander");

program.option("-f, --first <char>", "创建的第一个参数");

program
  .command("create <project> [other...]")
  .alias("crt")
  .description("创建项目")
  .action((project, args) => {
    console.log(project);
    console.log(args);
  });

program.parse();

这里使用了一个 program.command() 的方法,这个方法可以链式调用。

  • command() 规定指令的名字和传入参数,<project> 是添加的必传参数,[other...] 是其他参数。
  • alias()create 指令增加一个别名。
  • description() 给指令添加 --help 时的提示内容。
  • action() 传入一个函数。执行指令时,会调用这个函数。

测试一下这个新参数:

05.png

测试通过,上方的设置均正常。

逻辑代码模块化拆分

把全部代码逻辑写在一个 bin/cli.js 文件夹内是不太优雅的,当需要实现的功能变多之后,代码会变得非常多且难以阅读,得对代码拆分优化一下。

拆分后的代码目录结构如下:

mycli
├─bin 
│ └─cli.js 
├─lib 
│ └─core 
│   ├─action.js 
│   ├─command.js 
│   └─help.js 
├─package-lock.json 
└─package.json 

bin/cli.js:

#! /usr/bin/env node
const { program } = require("commander");
const myHelp = require("../lib/core/help");
const myCommand = require("../lib/core/command");

myHelp(program);

myCommand(program);

program.parse();

lib/core/help.js:

const myHelp = function (program) {
  program.option("-f, --first <char>", "创建的第一个参数");
};

module.exports = myHelp;

lib/core/command.js:

const myAction = require("./action");

const myCommand = function (program) {
  program
    .command("create <project> [other...]")
    .alias("crt")
    .description("创建项目")
    .action(myAction);
};

module.exports = myCommand;

lib/core/action.js:

const myAction = function (project, args) {
  console.log(project);
  console.log(args);
};

module.exports = myAction;

拆分之后,记得测试是否还能正常执行 cli 相关命令。

命令行问答交互

借助 Inquirer.js 可以快速的实现命令行问答交互,使用下方命令安装:

# 根目录下执行
npm install inquirer@8

修改 lib/core/action.js 代码:

let inquirer = require("inquirer");

const myAction = function (project, args) {
  inquirer
    .prompt([
      {
        type: "list",
        name: "food",
        choices: ["苹果", "梨", "橙子"],
        message: "你喜欢吃下面那种水果",
      },
    ])
    .then((answer) => {
      console.log(answer);
    });
};

module.exports = myAction;

当我们执行 mycli create xxx 时,就会弹出一个命令行问答交互,这里实现了一个单选。

06.gif

  • type 是交互类型,有输入、单选、多选等,具体请查看文末参考链接的文档。
  • name 是用户提交答案的 key
  • choices 是选项值。
  • message 是你提出的问题。

当用户选择完之后,使用 then() 方法继续编写相关逻辑,answer 为回答结果。可以看出用的是 Promise 语法,用 async/await 来接收 answer 也是 ok 的。

等待提示交互

在使用 cli 时,会出现一些等待过程,比如,下载一些数据包。在这个过程中,需要给用户一些有好交互,让用户知道我们的 cli 还在正常工作,并不是挂了。

使用 ora 这个第三方模块可以实现等待提示交互,安装方法如下:

# 根目录下执行
npm install ora@5

修改 lib/core/action.js 代码:

const inquirer = require("inquirer");
const ora = require("ora");

const myAction = function (project, args) {
  inquirer
    .prompt([
      {
        type: "list",
        name: "food",
        choices: ["苹果", "梨", "橙子"],
        message: "你喜欢吃下面那种水果",
      },
    ])
    .then((answer) => {
      const spinner = ora();
      spinner.start();
      spinner.text = "正在执行中...";
      setTimeout(() => {
        spinner.succeed("结束");
      }, 3000);
    });
};

module.exports = myAction;

测试一下效果:

07.gif

  1. 引入 ora
  2. 执行 ora() 方法,生成一个 spinner
  3. 执行 spinner.start() 开始执行等待交互效果。
  4. spinner.text 设置等待时显示的文本。
  5. 执行 spinner.succeed()spinner.fail()spinner.warn()spinner.info() 等方法,显示不同的结束效果,具体看文末参考文档。

命令行样式输出

许多命令行工具在执行的时候 console.log() 打印的文本都是有颜色的。

使用 chalk 这个库可以快速给我们的 console.log() 增加颜色样式,安装命令如下:

# 根目录下执行
npm install chalk@4

修改 lib/core/action.js 代码:

const inquirer = require("inquirer");
const ora = require("ora");
const chalk = require("chalk");

const myAction = function (project, args) {
  inquirer
    .prompt([
      {
        type: "list",
        name: "food",
        choices: ["苹果", "梨", "橙子"],
        message: "你喜欢吃下面那种水果",
      },
    ])
    .then((answer) => {
      const spinner = ora();
      spinner.start();
      spinner.text = "正在执行中...";
      setTimeout(() => {
        spinner.succeed("结束");
        console.log(chalk.blue.bgRed.bold("Hello world!"));
      }, 3000);
    });
};

module.exports = myAction;

执行效果如下:

08.gif

  1. 引入 chalk
  2. 使用 chalk.blue.bgRed.bold() 等方法添加样式,具体看文末参考文档。

总结

虽然这个 cli 并没有实现什么具体功能,但是 cli 的大部分公共需求已经解决,通过这个流程,可以做出自己喜欢的 cli 工具。

参考链接