Node.js 自定义命令行工具

Node.js CLI

Node.js CLI 是一种用 Node.js 编写的命令行程序,它可以被安装成系统命令,像 mkdir、git 一样在终端直接使用。它的作用是把 JavaScript/TypeScript 的程序能力封装成命令,用于自动化任务、项目脚手架、开发工具、运维脚本和批处理操作,具有跨平台、易发布、生态丰富、适合前端和全栈开发者的特点。


CLI 的 Hello World 项目

检查是否安装了 Node 环境,如果没有安装,请查看:https://blog.csdn.net/a1053765496/article/details/145473576

Windows PowerShell 中输入如下命令:

bash 复制代码
node -v
npm -v

新建一个项目文件夹 test

初始化项目

进入到 test 目录中,打开 Windows PowerShell

输入如下命令:

bash 复制代码
npm init -y

你会得到一个 package.json

新建一个 bin 目录

bin 目录下新建一个 js 文件, hello.js (如果是 Linux / MacOS 环境,需要给 hello.js 文件权限 chmod +x bin/hello.js )

hello.js 中的代码如下:

javascript 复制代码
#!/usr/bin/env node            // 第一行必须是这个代码, 意思是用 node 来执行这个文件

console.log('Hello CLI');      // 输出 Hello CLI

修改 package.json 文件,添加如下内容

javascript 复制代码
  "bin": {
    "hello": "./bin/hello.js"
  }

完整 package.json 文件内容如下

javascript 复制代码
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "bin": {
    "hello": "./bin/hello.js"
  }
}

在本地注册这个命令,执行如下命令:

bash 复制代码
# 把 hello 命令链接到系统 PATH 环境
npm link

执行自定义的 hello 命令,输出 Hello CLI


常见参数形式

写法 示例 在 argv 里的样子
位置参数 (命令后面要带一个参数叫位置参数) hello zhangsan ['zhangsan']
长参数 --name zhangsan ['--name', 'zhangsan']
短参数 -n zhangsan ['-n', 'zhangsan']
布尔参数 --force ['--force']
等号形式 --port=3000 ['--port=3000']

让命令接收参数

process.argv 介绍

process.argv 是 Node.js 提供的"命令行参数数组"

当你在终端输入命令时:

bash 复制代码
node app.js a b c

Node 会把整条命令拆成一个数组,放进 process.argv

无论你传不传参数,它永远至少有 2 个元素:

bash 复制代码
process.argv = [
  'node 的绝对路径',
  '当前执行脚本的绝对路径',
  ...你传的参数
]

打印 console.log(process.argv); 看效果

bash 复制代码
PS D:\project\test> hello
[
  'C:\\Users\\aoc\\AppData\\Local\\fnm_multishells\\31380_1767578153606\\node.exe',
  'C:\\Users\\aoc\\AppData\\Local\\fnm_multishells\\31380_1767578153606\\node_modules\\test\\bin\\hello.js'
]

process.argv.slice(2) 说明

slice(2) 因为前两个参数不是你关心的业务参数。

了解了一下 process.argv 后,我们来让 命令接收参数


让命令接收参数

修改 bin/hello.js

bash 复制代码
#!/usr/bin/env node

const args = process.argv.slice(2);

const param = args[0] || 'World';

console.log(`Hello ${param}`);

接收 长参数 示例

怎么接收如下格式的参数

bash 复制代码
hello --age 18 --city bj

代码如下:

javascript 复制代码
#!/usr/bin/env node

const args = process.argv.slice(2);

const result = {};
for (let i = 0; i < args.length; i++) {
  if (args[i].startsWith("--")) {
    const key = args[i].slice(2);
    const value = args[i + 1];
    result[key] = value;
    i++;
  }
}

console.log(result);

接收 等号参数 示例

怎么接收如下格式的参数

javascript 复制代码
hello port=3000

代码如下:

javascript 复制代码
#!/usr/bin/env node

const args = process.argv.slice(2);

args.forEach(arg => {
  if (arg.includes('=')) {
    const [key, value] = arg.split('=');
    console.log(key, value);
  }
});

process.argv 的问题

process.argv

  • 不解析 -abc
  • 不解析 --no-cache
  • 不支持 help
  • 不支持校验
  • 不支持子命令

所以真实项目中都是 process.argv + 参数解析库 的方式

主流的 参数解析库有 commander、yargs、minimist,当然底层都是 process.argv


Node.js commander

commander 是 Node.js 中最常用的命令行框架,用于快速构建专业的 CLI 工具,它可以基于 process.argv 自动解析命令和参数,支持子命令、选项(如 --port)、必填校验、默认值以及自动生成 --help 和 --version,让开发者用简洁、可维护的方式把 Node.js 程序封装成像 git、npm 一样的命令行工具。

安装 commander

javascript 复制代码
npm i commander

配置成 ESM 语法(TS/Vite 常见)

修改 package.json 的 type 为 module

javascript 复制代码
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "bin": {
    "hello": "./bin/hello.js"
  },
  "dependencies": {
    "commander": "^14.0.2"
  }
}

然后 js/ts 文件中,就可以使用 import { Command } from 'commander'; 这样的语法,而不是 const { Command } = require('commander'); 语法。


const program = new Command() 方法 详解

  • program 是 CLI 的根命令对象

  • 负责:

    • 定义命令名、版本、描述

    • 注册子命令

    • 解析参数

    • 生成 --help

    • 分发执行到对应 action

  • 一句话:program 是整个 CLI 的"指挥中心"。

方法详解

.name(name): 设置命令名(help 中显示)

示例:

javascript 复制代码
program.name('hello 程序');

.description(desc):命令描述

示例:

javascript 复制代码
program.description('hello 工具集');

.version(version, flags?, description?):自动提供版本号功能,等于内置了一个 --version 命令

示例:

javascript 复制代码
program.version('1.0.0');

.helpOption(flags, description):自定义 help 选项

示例:

javascript 复制代码
program.helpOption('-h, --help', '查看帮助');

.addHelpText(position, text):给 help 增加说明(非常常用)

javascript 复制代码
program.addHelpText('after', `
Examples:
  hello init
  hello build -m dev
`);

位置参数

.argument():<> 必填,[] 可选

javascript 复制代码
program.argument('<name>', '名字');

.arguments()

javascript 复制代码
program.arguments('<src> <dest>');

选项参数(flags)

.option(flags, description, defaultValue?)

javascript 复制代码
program.option('-p, --port <number>', '端口', '3000');

// 这样获取 program.opts().port // '3000'

.requiredOption():未提供会报错 + help

javascript 复制代码
program.requiredOption('-t, --token <string>', '必须提供');

选项值转换(parser)

javascript 复制代码
program.option(
  '-p, --port <number>',
  '端口',
  v => Number(v),
  3000
);

子命令(像 git 一样)

.command(name):创建子命令

示例:

javascript 复制代码
program
  .command('init')
  .description('初始化项目')
  .action(() => {
    console.log('init');
  });

子命令 + 参数 + 选项

示例:

javascript 复制代码
program
  .command('build')
  .argument('[env]', '环境', 'prod')
  .option('-m, --mode <mode>', '模式')
  .action((env, opts) => {
    console.log(env, opts.mode);
  });

执行相关方法(决定什么时候跑)

.action(fn):真正执行的地方

语法: // 子命令和根命令都可以有 action

javascript 复制代码
program.action((...args) => {
  // args = [arguments..., options, command]
});

.parse(argv?):触发解析(必须)不调用 CLI 什么都不干,通常放在文件最后

javascript 复制代码
program.parse(process.argv);

.parseAsync():异步 action

javascript 复制代码
// 如果 action 里有 async/await
await program.parseAsync();

解析结果读取

.opts():获取选项对象

javascript 复制代码
const opts = program.opts();

.args:剩余的参数数组(不常用)

javascript 复制代码
program.args;

.processedArgs:解析后的参数(调试用)


错误与退出控制

.exitOverride():阻止 commander 自动 process.exit()

javascript 复制代码
program.exitOverride();

try {
  program.parse();
} catch (err) {
  console.error(err.message);
  process.exit(1);
}

.allowUnknownOption():允许未知参数(一般不推荐)

javascript 复制代码
program.allowUnknownOption();

行为控制

.showHelpAfterError():错误后显示 help

javascript 复制代码
program.showHelpAfterError();

.showSuggestionAfterError():拼写错误时给建议

javascript 复制代码
program.showSuggestionAfterError();

完整语法流程:

javascript 复制代码
program
  .name('zkar')
  .description('工具集')
  .version('1.0.0')
  .option('--debug', '调试')
  .command('init')
  .description('初始化')
  .action(() => {})
  
program.parse();

执行流程如下:

输入命令

→ parse()

→ 匹配子命令

→ 解析参数/选项

→ 校验

→ 调用 action


commander 示例

示例一:help + version

javascript 复制代码
#!/usr/bin/env node
import { Command } from 'commander';

const program = new Command();

program
  .name('hello')
  .description('我的第一个 Node CLI')
  .version('1.0.0');

program.parse(process.argv);

如果想把 output the version number 和 display help for command 改成自己指定的内容,可以如下这样写:

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .name("hello")
  .description("我的第一个 Node CLI")
  .version("1.0.0", "-v, --version", "查看 hello 版本号")
  .helpOption("-h, --help", "查看使用说明");

program.parse(process.argv);

示例二:位置参数,如果有多个位置参数就写多个 argument ,不推荐 arguments

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program.argument("<name>", "必填名字").action((name) => {
  console.log("Hello", name);
});

program.parse(process.argv);

示例:选项参数

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .option("-p, --port <number>", "端口号", "3000")
  .option("--host <string>", "主机", "127.0.0.1")
  .requiredOption("--dry-run", "只演练不执行")  // 必填 requiredOption
  .action((opts) => {
    console.log(opts);
  });

program.parse(process.argv);

示例:类型转换与校验:.argParser() / 自定义 parser

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

const toInt = (v) => {
  const n = Number(v);
  if (!Number.isInteger(n)) throw new Error("port 必须是整数");
  return n;
};

program.option("-p, --port <number>", "端口", toInt, 3000);

program.parse(process.argv);

示例:子命令

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .command("init")
  .description("初始化项目")
  .option("--force", "强制覆盖")
  .action((opts) => {
    console.log("init", opts);
  });

program
  .command("build")
  .description("构建项目")
  .option("-m, --mode <mode>", "模式", "prod")
  .action((opts) => {
    console.log("build", opts.mode);
  });

program.parse(process.argv);
javascript 复制代码
hello init --force
hello build -m dev

示例:帮助信息定制

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program.helpOption("-h, --help", "查看帮助").addHelpText(
  "after",
  `
Examples:
  hello init --force
  hello build -m dev
`
);

program.parse(process.argv);

示例:获取解析结果:.opts() / .args / .processedArgs

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .name("hello")
  .description("parse / opts / args 示例")
  .option("-f, --force", "强制执行")
  .option("-m, --mode <mode>", "运行模式")
  .argument("<src>", "源路径")
  .argument("[dest]", "目标路径(可选)");

program.parse(process.argv);

// 所有 option(--xxx)
const opts = program.opts();

// 所有位置参数
const args = program.args;

console.log("opts =", opts);
console.log("args =", args);

示例:错误处理与退出码(做成"像系统命令一样")

javascript 复制代码
#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .name("hello")
  .description("exitOverride 示例")
  .exitOverride() // 关键:阻止自动 exit
  .argument("<src>", "源路径")
  .option("-f, --force", "强制执行");

try {
  program.parse(process.argv);

  // 只有 parse 成功,才会走到这里
  const opts = program.opts();
  const args = program.args;

  console.log("解析成功");
  console.log("opts:", opts);
  console.log("args:", args);
} catch (err) {
  // 所有 Commander 的退出行为都会到这里
  console.error("参数解析失败");
  process.exit(1);
}

缺少必填参数

非法参数

参数正确


commander 项目最佳实践

项目目录结构

javascript 复制代码
mycli/
  bin/
    hello.js          # 入口:注册命令
  commands/
    init.js
    build.js
  utils
    logger.js
    fs.js

hello.js

javascript 复制代码
#!/usr/bin/env node
import { Command, CommanderError } from "commander";
import initCommand from "../commands/init.js";

const program = new Command();

program
  .name("hello")
  .description("My CLI Tool")
  .version("1.0.0", "-v, --version", "查看版本")
  .exitOverride()
  .configureOutput({
    writeErr: () => {}, // 禁止 commander 自动打印错误
  });

// 注册子命令
initCommand(program);

try {
  program.parse(process.argv);
} catch (err) {
  if (err instanceof CommanderError) {
    console.error("参数错误");
    console.error(err.message);
    process.exit(err.exitCode);
  }

  // 其他非 CLI 错误
  console.error("程序异常");
  console.error(err);
  process.exit(1);
}

init.js

javascript 复制代码
export default function initCommand(program) {
  program
    .command("init <projectName>")
    .description("初始化项目")
    .option("-f, --force", "强制覆盖")
    .action((projectName, options) => {
      console.log("init 命令执行");
      console.log("projectName:", projectName);
      console.log("options:", options);
    });
}

搭建 CLI 真实生产项目

在你想放项目的目录下执行

bash 复制代码
# npm create vite@latest <项目名称>
npm create vite@latest crawler-cmd

创建好的项目目录结构如下:

改造项目,删除 index.html文件、tsconfig.app.json、src下的所有文件、public文件夹

删除后项目目录结构如下:

改造 vite.config.ts 文件

TypeScript 复制代码
// 给 Vite 配置提供 类型提示
import { defineConfig } from "vite";
// 把相对路径转换成 绝对路径
import { resolve } from "node:path";

// 导出 Vite 的配置对象
export default defineConfig({
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  },
  build: {
    // 告诉 Vite,这是一个"库 / 工具"的构建,而不是前端应用。如果不用,vite 会默认找 index.html 构建成浏览器产物
    lib: {
      // 指定 CLI 的入口文件,使用 resolve 保证路径稳定,不依赖执行命令的目录
      entry: resolve(__dirname, "src/bin/dog.ts"),
      // 输出 ES Module 格式, package.json 写了 "type": "module",用 ESM 解析 不接受 CJS
      formats: ["es"],
      // 指定输出文件名
      fileName: () => "dog.js",
    },
    // 构建产物输出目录
    outDir: "dist",
    // 每次 build 前清空 dist
    emptyOutDir: true,
    // target: "node18",
    // 这些模块不要打进最终文件
    rollupOptions: {
      external: [
        "commander",
        "node:fs",
        "node:path",
        "node:os",
        "node:child_process",
      ],
    },
  },
});

改造 package.json,只留下 node、typescript、vite 相关的依赖,没有commander就安装。如果报如下错误:《加载引用"https://www.schemastore.org/package"时出现问题: 无法从"https://www.schemastore.org/package"加载架构: read ECONNRESET。》在 settings.json 中添加 "json.schemaDownload.enable": false 配置

TypeScript 复制代码
{
  "name": "crawler-cmd",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "dog": "dist/dog.js"
  },
  "scripts": {
    "dev": "vite build && node dist/dog.js",
    "build": "vite build",
    "build:link": "vite build && npm link"
  },
  "devDependencies": {
    "@types/node": "^24.10.1",
    "typescript": "~5.9.3",
    "vite": "^7.2.4"
  },
  "dependencies": {
    "commander": "^14.0.2"
  }
}

然后就可以在,src 下面写 bin、commands 等内容了

如果想要配置 @ 别名,配置如下:

tsconfig.node.json 中新增

bash 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

vite.config.ts 中新增

TypeScript 复制代码
import { defineConfig } from "vite";
import { resolve } from "node:path";

export default defineConfig({
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  },
  build: {
    。。。省略
  },
});

部署到 Linux 服务器

当我们把 CLI 代码写好后,就要发布到线上环境了。发布步骤如下

  1. 需要把 package.json 文件一起打包到 dist 目录中(因为Node CLI是运行时解析(运行时再去找依赖),运行时需要node_modules中的依赖,和web不同web运行时在"浏览器",依赖必须在构建期全部打包),所以需要在编写一个打包脚本,代码如下:

src/scripts/build-dist.js

javascript 复制代码
import fs from "fs";

const rootPkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));

const distPkg = {
  name: rootPkg.name,
  version: rootPkg.version,
  private: true,
  type: "commonjs",
  bin: {
    dog: "./dog.js",
  },
  dependencies: {
    commander: rootPkg.dependencies.commander,
  },
};

fs.writeFileSync("dist/package.json", JSON.stringify(distPkg, null, 2));

if (fs.existsSync("package-lock.json")) {
  fs.copyFileSync("package-lock.json", "dist/package-lock.json");
}

修改 package.json 的 scripts

javascript 复制代码
  "scripts": {
    "build": "vite build && node src/scripts/build-dist.js",
  },
  1. 把 打包后的 dist 文件夹中的文件上传到 linux 的 /usr/local/lib/cli 目录

执行安装命令

javascript 复制代码
npm install
  1. 赋予可执行权限
javascript 复制代码
chmod +x /usr/local/lib/cli/dog.js
  1. 创建命令入口(wrapper)

不要直接把 dog.js 放 PATH 里

新建一个入口文件

bash 复制代码
vim /usr/local/bin/dog
bash 复制代码
#!/bin/bash
node /usr/local/lib/cli/dog.js "$@"

赋予执行权限

bash 复制代码
chmod +x /usr/local/bin/dog
  1. 验证 CLI 是否注册成功
bash 复制代码
which dog

// 输出 /usr/local/bin/dog
  1. 使用命令
bash 复制代码
dog --help
相关推荐
ZC·Shou1 个月前
Rust 之二 各组件工具的源码、构建、配置、使用(二)
开发语言·ide·rust·工具·命令·clippy·rustfmt
課代表2 个月前
bat 批处理文件重命名加时间戳
时间·重命名·bat·时间戳·命令·批处理·字符串截取
課代表2 个月前
WindoWs 系统管理批处理脚本
windows·安全·脚本·注册表·bat·命令·组策略
序属秋秋秋3 个月前
《Linux系统编程之入门基础》【Linux基础 理论+命令】(下)
linux·运维·服务器·学习·ubuntu·xshell·命令
web前端神器3 个月前
webpack,vite,node等启动服务时运行一段时间命令窗口就卡住
命令模式·命令
胡斌附体3 个月前
springbatch使用记录
数据库·接口·shell·命令·批量·springbatch·并发抽取
hqwest5 个月前
C#WPF实战出真汁06--【系统设置】--餐桌类型设置
c#·.net·wpf·布局·分页·命令·viewmodel
伊织code7 个月前
pmset - 控制 macOS 系统电源、睡眠、唤醒与节能
macos·命令·电源·睡眠·节能·唤醒·pmset
IT成长日记7 个月前
05【Linux经典命令】Linux 用户管理全面指南:从基础到高级操作
linux·运维·服务器·用户管理·命令