命令行工具是一种通过命令行界面执行特定任务或操作的软件程序,用户可以在终端窗口中输入指定的命令来调用这些工具。

命令行工具在前端开发中非常常见,常见的像 Webpack、Vite、Rollup等构件工具提供了命令行工具,运行时 Nodejs、包管理器NPM也都属于命令行工具。

基于 NodeJS 的运行时,我们可以通过 JavaScript 来编写命令行工具,一个可执行的js文件就可以被当作一个命令行工具,只需要将其声明到package.json的bin属性下即可,例如:

{
  "bin": {
    "log": "./dist/log.js",
    "split": "./dist/split.js"
  },
  // ......
}

这样就可以声明两个命令,logsplit,当在项目中安装依赖时,会自动将库中声明的命令链接到node_modules/.bin目录下。

process

process 对象是 Node.js 提供的一个全局对象,用于提供有关当前进程的信息和控制。它包含了许多有用的属性和方法,可以用来管理当前 Node.js 进程。

常用属性

  1. process.argv: 获取命令行参数数组。
  2. Process.env: 获取环境变量。
  3. procress.pid: 获取进程ID。
  4. process.platform: 获取操作系统平台。
  5. process.cwd(): 返回当前工作目录路径。

控制方法

  1. process.exit([code]): 退出当前进程,可选地指定退出码。
  2. process.kill(pid[, signal]): 向指定 PID 的进程发送信号(默认为 SIGTERM)。

监听事件

  1. exit:当进程退出时触发。
  2. uncatchException:当发生未捕获异常时触发。
// 输出命令行参数
console.log(process.argv);

// 输出环境变量
console.log(process.env);

// 监听 exit 事件
 process.on('exit', (code) => {
     console.log(`进程退出,退出码: ${code}`);
});

// 异常处理
 process.on('uncaughtException', (err) => {
        console.error('未捕获的异常:', err);
});

但是,使用process开发命令行工具过于原始,效率低下,我们可以通过一下优秀的工具来协助我们进行开发。

commander.js

Commander.js 是 Node.js 中一个流行的命令行界面(CLI)工具,用于解析用户输入的命令和选项。它使开发者可以方便地构建强大的命令行应用程序,并且提供了许多功能,如定义命令、添加选项、处理参数等。

  1. 定义命令:使用 Commander.js 可以很容易地定义各种不同的命令,并在执行时触发相应的函数。
  2. 添加选项:可以为每个命令设置各种选项(flags),像是 --verbose, -f 等。这些选项会作为参数传递给对应的事件函数,在其中进行相应处理。
  3. 支持子命令:通过嵌套定义子级别的子程序来实现复杂 CLI 结构,帮助组织代码并提高可读性。
  4. 自动生成帮助文档及版本号信息: Commander.js 能够根据你所设定好的各个 options、commands 自动生成基本格式化过后的本脚本用法帮助即刻对象;默认情况下还能够生成 –version 信息;
  5. 与其他模块结合: 配合其他 NPM 模块例如 inquirer 可以创建出更加交互式和友好CLI总之, 通过简单地调用 require('commander').parse(process.argv); , 使用者就能够完成一个最基础的命令行工具。

命令

最简单的命令通过program对象进行设置,可以设计最简单的命令属性,像命令描述等

import {program} from 'commander';
import { readFileSync } from 'fs';
import parse from 'parse-json';

const pkg = parse(readFileSync('package.json', 'utf-8'))

program
  .name('fapi') // 名称
  .description('Generate typeScript file by swagger json.') // 描述文案
  .version(pkg.version) // 版本信息, 通过-V参数查看
  .option('-c, --config', 'Configure through fapi.config.js.') // 自定义选项
  .action((options) => { // 命令回调函数,可以接收解析后的参数
    console.log('[ fapi options ] >', options)
  })

program.parse()

此时执行文件并添加--help参数可以查看到命令介绍

image-20240623213858650

命令可以设置参数,通过argument方法进行设置,获取的值会传到毁掉函数的参数重,按照参数设置的顺序排列。可以通过arguments方法一次性设置多个参数,可以通过<>设置必须按参数,通过[]设置可选参数。

复杂的命令也可以通过构造函数创建,比如

import {  program, Argument } from 'commander'
import { readFileSync } from 'fs'
import parse from 'parse-json'

const pkg = parse(readFileSync('package.json', 'utf-8'))

program
  .name('fapi')
  .description('Generate typeScript file by swagger json.')
  .version(pkg.version)
  .addArgument(new Argument('<size>', 'drink cup size').choices(['small', 'medium', 'large']))
    .addArgument(new Argument('[timeout]', 'timeout in seconds').default(60, 'one minute'))
  // 省略其他
  })

子命令也是很常见的形式,比如docker run -d ......中,docker是一个主命令,run就是一个子命令,commander也提供了这种能力。通过command方法设置子命令,子命令的配置方式和主命令基本一致

import { program } from 'commander'
import { readFileSync } from 'fs'
import parse from 'parse-json'

const pkg = parse(readFileSync('package.json', 'utf-8'))

program
  .name('fapi')
  .description('Generate typeScript file by swagger json.')

// 设置子命令
program
  .command('generate')
  .description('Generate files from swagger json.')
  .argument('<url>', 'swagger.json url')
  .option('-m, --mock <path>', 'generate mock files and save to <path>(need use fapi mock util).', '/src/mock')
  .option('-s, --service <path>', 'generate request files and save to <path>.', '/src/service')
  .action((url, options) => {
    // generate exec
    console.log('[ generate options ] >', options)
  })

program.parse()

命令支持生命周期,通过hook方法来进行绑定,支持的生命周期有

事件名称 触发时机 参数列表
preAction, postAction 本命令或其子命令的处理函数执行前/后 (thisCommand, actionCommand)
preSubcommand 在其直接子命令解析之前调用 (thisCommand, subcommand)
program
  .name('fapi')
  .description('Generate typeScript file by swagger json.')
  .version(pkg.version)
  .option('-k, --hook')
  .hook('preAction', (thisCommand, actionCommand) => {
    console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
    console.log('arguments: %O', actionCommand.args);
    console.log('options: %o', actionCommand.opts());
  }).action(() => {
    // ......
   })

选项

option方法用于定义选项,语法为option(param[, description[, default]]),设置选项名称时可以顺带设置别名,比如上面命令一节中的-c, --config,在使用时可以通过两种形式使用,这也符合命令行工具的使用习惯。

选项有两种类型:布尔选项和带参数的选项,布尔选项仅用于开关,像上面的--config就是一个布尔选项,在启用时会拿到如下options

$ node bin/fapi.js --config
[ fapi options ] > { config: true }

布尔类型的选项可以设置取反,使用§no-开头的选项开启后会将值设为false,比如--no-cache选项启用时会在options中将cache置为false。如果已经开启cache选项则取反不生效

带参数的选项就是可以添加参数的选项,比如平时开发时启动服务器用到的命令

serve -p 80
serve -p80
serve --port 80
serve --port=80

参数可以设置默认值、非必填参数(解析后的值可以是布尔类型也可以是参数类型),设置方式如下

program
  .option('-d, --default <param>', 'option with default', 'defaultValue')
    .option('-n, --not-require [param]', 'with not require param')

当必填参数没有设置时会出现如下报错

$ node bin/fapi.js generate -d
error: option '-d, --default <param>' argument missing

commander默认会帮助开发者自动生成一些帮助信息,比如选项的信息。在前一节我们已经看到了--help的输出,我们也可以通过代码输出help信息,相关方法如下:

  • help():展示帮助信息并退出;
  • outputHelp():展示帮助信息不退出;
  • helpInformation():得到字符串形式的内建的帮助信息。

前端小白