如何使用node编写一个命令行工具

240 阅读3分钟

一、什么是命令行工具

命令行的定义:能够在终端执行的系统命令。 比如:前端同学熟悉的npmgitcreate-react-app以及linux系统的lsvim等等。

使用 which 追根究底,发现它们实际执行的路径在某一个 bin 目录下

image.png

为什么这些命令可以直接使用,而不用输入它的完整路径?

原因:在环境变量PATH 中路径的命令可在其它任意地方执行,而这些 bin 目录就在环境变量 PATH 中

image.png

所以开发命令行的原理也是如此,就是将你开发的命令行工具脚本置于环境变量 PATH 下的路径之中

二、Node 全局命令行的原理

  1. npm 把包下载到某个路径下,比如 :/usr/local/lib/node_modules
  2. 根据该包的 package.json 中 bin 字段的指示,把对应的命令行路径通过软连接的方式挂载到PATH
  3. 给对应的二进制脚本添加可执行文件权限

三、最简单的命令行工具demo

  1. 新建一个cli项目,比如learn-cli
  2. 在learn-cli下新建bin/index.js并输入下面内容
#!/usr/bin/env node # 告诉终端用node来运行这段代码
console.log("hello gdc");

3BDE216E-A76E-4833-89A8-7F481C6FD45F.png

#! 加解释器,标明该文件使用 /usr/bin/env node 来执行

/usr/bin/env 为 env 的绝对路径,用以在 PATH 路径中执行命令 (在各种不同的系统中,node 命令行的位置不同,因此使用 env node 找到路径并执行)

  1. 使用npm初始化项目
npm init -y

image.png 4. 修改package.json文件,添加下面代码 886F0C60-0650-4390-BFAB-F3874CE472BB.png 5. 给全局添加命令

sudo npm link

image.png

  1. 此时就可以全局使用learn-cli的命令了

image.png

通过which learn-cli,我们可以看到,命令learn-cli,实际指向.nvm/versions/node/v16.18.0/bin/下的一个名字叫learn-cli的软连接,而这个软连接又指向../lib/node_modules/learn-cli(learn-cli整个文件夹就是个软连接),这个软连接又指向了我们的项目。

image.png

同样通过npm i create-react-app -g全局安装的create-react-app命令,也是通过软连接的方式,指向实际的文件夹

image.png

window系统下,“软连接”的表现方式有所不同

1669271472312.png

粗读create-react-app.sp1的源码,可以看到window下软连接的表现形式,是power shell可执行文件。

image.png

四、命令行参数解析

node开发的命令行,可以通过process.argv来获取用户输入。比如:

image.png

image.png

当然解析参数也要参照 POSIX 兼容的基本规律: 格式、可选、必选、简写、说明、帮助等等。命令行工具命名协议 (opens new window)文章中已说的足够详细。

// 一个较为规整的命令行帮助
$ node --help
Usage: node [options] [ script.js ] [arguments]
       node inspect [options] [ script.js | host:port ] [arguments]

Options:
  -                                         script read from stdin (default if no file name is provided,
                                            interactive mode if a tty)
  --                                        indicate the end of node options
  --abort-on-uncaught-exception             aborting instead of exiting causes a core file to be generated for
                                            analysis
  -c, --check                               syntax check script without executing
  --completion-bash                         print source-able bash completion script
  --cpu-prof                                Start the V8 CPU profiler on start up, and write the CPU profile to
                                            disk before exit. If --cpu-prof-dir is not specified, write the profile
                                            to the current working directory.

但是github上已经有大佬帮我们写好了相关的包,比如:tj大神的commander

commander的基本用法

  1. 文档:github.com/tj/commande…
  2. demo
const { program } = require("commander");
const commands = require("./commands/index");
const { name, version } = require("../package.json");

program
  .name(name)
  .description("使用node封装的个人cli工具")
  .version(version, "-v, --version", "查看版本号")
  .option("-h, --help", "查看所有的用法")
  .helpOption(false)
  .addHelpCommand(false);

commands.forEach((item) => {
  program
    .command(item.command)
    .description(item.description)
    .argument(item.argument)
    .action(item.action);
});

program.parse(); // parse 默认值为process.argv

效果:

image.png

五、使用inquirer实现命令行可交互

1.安装

npm i inquirer

2.inquirer官方文档:github.com/SBoudrias/I…

3.设置问题和答案

#!/usr/bin/env node
const inquirer = require("inquirer");

const answerOptions = {
  获取初始化页面模版: "init",
  "启动项⽬": "start",
};

const question = [
  {
    type: "list" /* 上下选择框 */,
    message: "请选择操作?",
    name: "operation",
    choices: Object.keys(answerOptions),
  },
];

inquirer
  .prompt(question)
  .then((res) => {})
  .catch((err) => {});

4.效果

2021-11-05 16.27.22.gif

六、使用git-pull-or-clone克隆项目代码

1.安装

npm i git-pull-or-clone

2.代码

const { promisify } = require("util");
const download = promisify(require("git-pull-or-clone"));
const { spawn } = require("child_process");

const spawnFn = (...args) => {
  return new Promise((resolve) => {
    const proc = spawn(...args);
    proc.stdout.pipe(process.stdout);
    proc.stderr.pipe(process.stderr);
    proc.on("close", () => {
      resolve();
    });
  });
};

module.exports = () => {
  const name = "xxxx";
  const repo = "git@gitlab.com:xxxx";
   /**
   * path.resolve(`./${name}`) ===> 执行命令行时的路径 + /xxx
   * path.resolve(__dirname, `./${name}`) ===> 源代码所在的文件路径+/blog
   */
  const desc = path.resolve(`./${name}`);

  download(repo, desc)
    .then((res) => {
      console.log("安装依赖");
      // spawnFn("npm", ["install"], { cwd: `${desc}` }) 
      // cwd是package.json所在的路径,否则报spawn npm ENOENT错误
      spawnFn("yarn", [""], { cwd: `${desc}` }).then((res) => {
        console.log(`
          安装完成:
          To get Start:
          ===========================
          cd ./${name}
          yarn start:dev
          ===========================
        `);
      });
    })
    .catch((err) => {
      console.log("try err", err);
    });
};

3.效果

2022-11-23 22.45.59.gif

七、美化

1.打印欢迎画⾯

npm i figlet clear chalk
#!/usr/bin/env node

const { promisify } = require("util"); // util是node内置包;util.promisify() 这个方法,方便我们快捷的把原来的异步回调方法改成返回 Promise 实例的方法
const figlet = promisify(require("figlet"));
const clear = require("clear");
const chalk = require("chalk");
const log = (content) => console.log(chalk.green(content));

// 打印欢迎画⾯
clear(); // 先执行命令行的clear方法清屏
log(
  figlet.textSync("hello gdc", {
    width: 80,
  })
);

2021-11-05 16.08.52.gif

2.下载loading

npm i ora@5.3.0
// 最新版的ora不支持commonJS引入,只支持es module,所以需要降版本到5.3.0
#!/usr/bin/env node
const ora = require("ora");

const progress = ora();
progress.start('下载中....');
setTimeout(()=>{
    progress.succeed('下载成功');
}, 2000)
setTimeout(()=>{
    progress.fail('下载失败');
}, 4000)

2022-11-23 22.13.16.gif