命令行工具开发(Commander.js / yargs)
命令行工具设计原则
CLI 设计规范
命令行工具的设计应遵循以下核心原则。
单一职责
一个 CLI 工具只做一件事,并把它做好。如果功能过于复杂,应考虑拆分为多个独立的子命令。
符合直觉
命令名称、参数命名应直观易懂,用户无需查阅文档即可大致猜测用法。例如 git commit -m "message" 一目了然。
子命令风格
CLI 工具通常采用两种子命令风格:
| 风格 | 示例 | 适用场景 |
|---|---|---|
| Git 风格 | git remote add origin <url> | 子命令层层嵌套,适合复杂工具 |
| Docker 风格 | docker container run --name xxx | 子命令分组,适合功能模块化 |
Help 文档规范
每条命令都应提供完善的 --help 输出,内容包括:
- 命令名称与简短描述
- 用法(Usage)示例
- 参数列表及说明
- 子命令列表(如有)
Exit Code 约定
| Exit Code | 含义 | 说明 |
|---|---|---|
| 0 | 成功 | 命令正常执行完毕 |
| 1 | 一般错误 | 执行失败,如文件不存在、参数错误等 |
| 2 | 误用 | 内置错误,如参数解析失败 |
使用 process.exit(code) 显式退出,或在 action handler 中抛出错误让框架自动处理退出码。
参数设计
位置参数 vs 命名选项
- 位置参数(Positional Arguments):按顺序传入,适用于必填的核心输入,如
input-file output-file。 - 命名选项(Options/Flags):以
--或-前缀传入,适用于可选配置,如--format json。
通用原则:必选的、核心的数据使用位置参数;可选的、修饰性的配置使用命名选项。
短选项 vs 长选项
每条选项应同时提供短选项(单字符,前加 -)和长选项(单词,前加 --)。短选项适用于高频操作的快捷输入,长选项用于代码可读性和文档化。
my-cli --output ./dist -v必选参数 vs 可选参数 vs 变长参数
| 类型 | 说明 | 示例 |
|---|---|---|
| 必选参数 | 必须提供的参数,缺失时报错 | <input> |
| 可选参数 | 可省略的参数,有默认值 | [output] |
| 变长参数 | 可接收多个值的参数 | <files...> |
子命令参数设计最佳实践
- 全局选项(如
--verbose、--config)应在顶层定义,对所有子命令生效。 - 子命令特有选项应在子命令层面定义,避免污染全局命名空间。
- 使用
--help应在每个子命令层级都可访问,且只显示当前层级的信息。 - 避免过深的子命令嵌套(建议不超过 3 层)。
# 好的设计
my-cli --verbose deploy --env production ./app
# 避免的设计
my-cli deploy --verbose --env productionCommander.js 开发
创建 CLI
安装
npm install commanderCommander.js 是 Node.js 生态中最流行的 CLI 框架之一,GitHub Stars 超过 28k,不依赖其他第三方包。
基本结构
#!/usr/bin/env node
import { Command } from 'commander';
const program = new Command();
program
.name('my-cli')
.version('1.0.0')
.usage('[command] [options]')
.description('一个示例命令行工具');
program.parse(process.argv);name:设置命令名称,显示在 help 信息中。version:设置版本号,默认注册-V/--version选项。usage:自定义 usage 字符串。description:命令描述。program.parse(process.argv):解析命令行参数,必须在所有命令定义之后调用。
如果使用 CommonJS:
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
// ...
program.parse(process.argv);命令定义
command + argument + option
program
.command('build <input> [output]')
.description('构建项目')
.option('-m, --mode <mode>', '构建模式', 'production')
.option('--sourcemap', '生成 source map')
.action((input: string, output: string | undefined, options: { mode: string; sourcemap: boolean }) => {
console.log(`构建输入: ${input}`);
console.log(`输出目录: ${output || 'dist'}`);
console.log(`模式: ${options.mode}`);
});子命令
通过嵌套命令实现子命令层级:
const deploy = program.command('deploy').description('部署相关命令');
deploy
.command('prod')
.description('部署到生产环境')
.option('-r, --region <region>', '部署区域')
.action((options) => {
console.log('部署到生产环境', options.region);
});
deploy
.command('staging')
.description('部署到预发布环境')
.action(() => {
console.log('部署到预发布环境');
});运行效果:
my-cli deploy prod --region us-east-1
my-cli deploy staging链式 API
Commander 的所有配置方法都返回 this,支持链式调用:
program
.command('serve')
.alias('s')
.description('启动开发服务器')
.option('-p, --port <number>', '端口号', '3000')
.option('--host <host>', '主机地址', 'localhost')
.option('--open', '自动打开浏览器')
.action((options) => {
console.log(`服务启动在 ${options.host}:${options.port}`);
});选项处理
选项定义
program
.option('-o, --output <path>', '输出路径') // 字符串参数
.option('-d, --debug', '启用调试模式') // 布尔标志
.option('-c, --config <path>', '配置文件路径', './config.json') // 带默认值
.option('-n, --number <n>', '数量', parseInt) // 自定义解析函数
.option('--no-color', '禁用颜色输出') // 否定选项,默认 true
.option('--tag <tags...>', '标签列表') // 变长参数短选项自动推导
Commander 会根据长选项的首字母自动推导短选项。如果首字母冲突,需要显式指定:
.option('--output <path>', '输出路径') // 自动推导 -o
.option('--optimize', '启用优化') // 冲突,不会自动推导
.option('-O, --optimize', '启用优化') // 显式指定短选项必选选项
使用 .requiredOption() 替代 .option():
program
.requiredOption('-u, --url <url>', '目标 URL(必填)')
.requiredOption('-t, --token <token>', '认证令牌(必填)');选项类型
boolean:不带参数的选项,如--debug,类型为boolean。string:带参数的选项,如--name <name>,类型为string。
Commander 自动根据定义的尖括号 <> 或方括号 [] 判断选项是否需要参数。
参数验证
自定义校验函数
function parsePort(value: string): number {
const port = parseInt(value, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`无效的端口号: ${value}`);
}
return port;
}
program
.option('-p, --port <number>', '端口号', parsePort);内置校验
// Commander 不会自动校验数字类型——需要自定义 or 使用 commander 9+ 的选项类型
// Commander 9+ 支持选项类型参数
// @ts-check
// import { Option } from 'commander';
// new Option('-n, --number <number>', '数字').argParser(parseInt)Commander 9+ 参数解析钩子
Commander 9 引入了更灵活的参数解析机制:
import { Option } from 'commander';
program
.addOption(new Option('-p, --port <number>', '端口号')
.default(3000)
.argParser((value: string) => {
const n = parseInt(value, 10);
if (isNaN(n)) throw new Error('必须是数字');
return n;
})
);参数校验常见方法
// 整数校验
function isInteger(value: string): number {
const n = Number(value);
if (!Number.isInteger(n)) {
throw new Error(`需要整数: ${value}`);
}
return n;
}
// 枚举值校验
function parseMode(value: string): string {
const modes = ['development', 'production', 'test'];
if (!modes.includes(value)) {
throw new Error(`模式必须是 ${modes.join(', ')} 之一`);
}
return value;
}命令动作
action handler
program
.command('generate <template>')
.option('-o, --output <dir>', '输出目录')
.action((template: string, options: { output?: string }) => {
// 同步 action
console.log(`生成模板: ${template}`);
if (options.output) {
console.log(`输出到: ${options.output}`);
}
});参数注入
Commander 向 action handler 注入的参数顺序为:
- 所有位置参数(按定义顺序)
- 选项对象
- 命令对象本身(若有)
program
.command('copy <src> <dest>')
.option('-f, --force', '强制覆盖')
.action((src: string, dest: string, options: { force: boolean }) => {
// src 和 dest 是位置参数
// options 包含 --force
});Async Action
action handler 可以是 async 函数,Commander 会等待 Promise 完成:
program
.command('fetch <url>')
.option('-o, --output <file>', '输出文件')
.action(async (url: string, options: { output?: string }) => {
const response = await fetch(url);
const data = await response.text();
if (options.output) {
await fs.promises.writeFile(options.output, data, 'utf-8');
console.log(`已保存到 ${options.output}`);
} else {
console.log(data);
}
});错误处理
program
.command('process <file>')
.action(async (file: string) => {
try {
await processFile(file);
} catch (error) {
console.error(`处理失败: ${error.message}`);
process.exit(1);
}
});
// 或利用 Commander 的异常处理
program
.command('process <file>')
.action(async (file: string) => {
// 抛出异常时 Commander 会自动输出错误信息并以 exit code 1 退出
const data = await fs.promises.readFile(file, 'utf-8');
if (!data) throw new Error('文件为空');
});帮助信息定制
自定义 helpInformation
Commander 的 help 信息由 helpInformation() 方法生成,可以覆盖该方法:
// 方式一:整体替换
program.helpInformation = () => {
return `用法: my-cli [command] [options]
命令:
build 构建项目
deploy 部署项目
选项:
-V, --version 输出版本号
-h, --help 输出帮助信息
了解更多: https://example.com/my-cli
`;
};addHelpText
Commander 7+ 提供了更精细的 addHelpText 方法,可以在 help 输出中插入自定义内容:
program
.addHelpText('before', '自定义帮助信息(在默认 help 之前)')
.addHelpText('after', '自定义帮助信息(在默认 help 之后)')
.addHelpText('beforeAll', '全局前置信息(所有 help 之前)')
.addHelpText('afterAll', '全局后置信息(所有 help 之后)');支持的位置参数:
before|after:在当前命令的 help 前后插入。beforeAll|afterAll:在最终输出的 help 内容最前/最后插入。
自定义 help 命令
// 禁用默认的 --help 或 -h
program.helpOption(false);
// 自定义 help 行为
program
.option('--guide', '查看使用指南')
.action((options) => {
if (options.guide) {
console.log('详细使用指南...');
process.exit(0);
}
});
// 或覆盖 help 命令
program
.command('help [command]')
.description('显示指定命令的帮助信息')
.action((command?: string) => {
if (command) {
program.commands.find(c => c.name() === command)?.help();
} else {
program.help();
}
});Yargs 开发
Yargs 创建
安装
npm install yargsYargs 是另一个流行的 Node.js CLI 框架,以简洁的 API 和强大的参数解析能力著称。
基本结构
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
yargs(hideBin(process.argv))
.scriptName('my-cli')
.usage('$0 [command] [options]')
.epilog('了解更多: https://example.com/my-cli')
.parse();hideBin(process.argv):去掉argv中的前两个元素(node路径和脚本路径),返回纯参数数组。scriptName:设置脚本名称,在 help 中替换$0。usage:自定义用法字符串,$0会被替换为脚本名称。epilog:在 help 底部显示的内容。.parse():必须在最后调用以触发解析。
命令定义
command 方法(含 builder/handler)
yargs(hideBin(process.argv))
.command({
command: 'build <input> [output]',
describe: '构建项目',
builder: (yargs) => {
return yargs
.option('mode', {
alias: 'm',
type: 'string',
default: 'production',
describe: '构建模式',
})
.option('sourcemap', {
type: 'boolean',
describe: '生成 source map',
});
},
handler: (argv) => {
console.log(`构建输入: ${argv.input}`);
console.log(`输出目录: ${argv.output || 'dist'}`);
console.log(`模式: ${argv.mode}`);
},
})
.parse();builder 函数用于定义该命令特有的选项,handler 是命令执行时的回调函数。
子命令嵌套
通过 builder 中再嵌套 command 实现子命令:
yargs(hideBin(process.argv))
.command({
command: 'deploy',
describe: '部署相关命令',
builder: (yargs) => {
return yargs
.command({
command: 'prod',
describe: '部署到生产环境',
builder: (yargs) => {
return yargs
.option('region', {
alias: 'r',
type: 'string',
describe: '部署区域',
});
},
handler: (argv) => {
console.log(`部署到生产环境: ${argv.region}`);
},
})
.command({
command: 'staging',
describe: '部署到预发布环境',
handler: () => {
console.log('部署到预发布环境');
},
})
.demandCommand(1, '请指定子命令');
},
handler: (argv) => {
// 如果没有匹配的子命令,会进入这里的 handler
console.log('deploy 需要子命令');
},
})
.parse();选项定义
option / positional
yargs(hideBin(process.argv))
.command({
command: 'convert <input>',
describe: '文件格式转换',
builder: (yargs) => {
return yargs
// 命名选项
.option('format', {
alias: 'f',
type: 'string',
choices: ['json', 'yaml', 'xml'],
describe: '输出格式',
})
.option('pretty', {
type: 'boolean',
default: true,
describe: '美化输出',
})
.option('level', {
type: 'number',
describe: '详细级别',
})
// 位置参数
.positional('input', {
type: 'string',
describe: '输入文件路径',
demandOption: true,
});
},
handler: (argv) => {
console.log(`输入文件: ${argv.input}`);
console.log(`输出格式: ${argv.format}`);
},
})
.parse();常用选项类型
yargs(hideBin(process.argv))
.option('name', { type: 'string', describe: '姓名' }) // 字符串
.option('count', { type: 'number', describe: '数量' }) // 数字
.option('items', { type: 'array', describe: '项目列表' }) // 数组
.option('verbose', { type: 'count', describe: '详细程度' }) // 计数 (-v -vv -vvv)
.option('color', { type: 'boolean', describe: '启用颜色' }) // 布尔
.option('mode', { type: 'string', choices: ['a', 'b'], describe: '模式' }) // 枚举
.demandOption(['name', 'mode'], '请提供必填选项'); // 必选选项type: 'count' 是一个特殊类型:每出现一次 -v,计数器加 1。适合实现 -v、-vv、-vvv 的详细级别。
positional 详解
yargs(hideBin(process.argv))
.command({
command: 'copy <src> <dest>',
builder: (yargs) => {
return yargs
.positional('src', {
describe: '源路径',
type: 'string',
demandOption: true,
})
.positional('dest', {
describe: '目标路径',
type: 'string',
default: './dist',
});
},
handler: (argv) => {
// argv.src, argv.dest
},
});中间件机制
Yargs 支持中间件(middleware),可以在 handler 执行前插入通用逻辑。
基本中间件
yargs(hideBin(process.argv))
.middleware((argv) => {
// 解析前处理
console.log(`[中间件] 命令执行时间: ${new Date().toISOString()}`);
})
.command({
command: 'process <file>',
handler: (argv) => {
console.log(`处理文件: ${argv.file}`);
},
})
.parse();全局配置中间件
用于加载全局配置文件:
import fs from 'fs';
import path from 'path';
yargs(hideBin(process.argv))
.middleware((argv) => {
const configPath = argv.config as string || './cli.config.json';
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
Object.assign(argv, config);
}
}, true) // 第二个参数 true 表示在所有命令前执行
.option('config', {
type: 'string',
describe: '配置文件路径',
})
.command({
command: 'deploy',
handler: (argv) => {
// argv 中已合并了配置文件中的默认值
console.log(`部署: ${argv.region || 'default'}`);
},
})
.parse();鉴权中间件
yargs(hideBin(process.argv))
.middleware((argv) => {
const token = process.env.API_TOKEN || argv.token as string;
if (!token && argv._[0] !== 'login') {
console.error('请先登录: my-cli login');
process.exit(1);
}
argv.apiToken = token;
})
.option('token', { type: 'string', hidden: true })
.command('login', '登录', () => {}, (argv) => {
console.log('登录成功');
})
.command('fetch', '获取数据', () => {}, (argv) => {
console.log(`使用 token: ${argv.apiToken} 获取数据`);
})
.parse();Yargs vs Commander.js 对比
| 对比维度 | Commander.js | Yargs |
|---|---|---|
| 包大小 | 小(无依赖) | 中(有依赖) |
| API 风格 | 链式调用 + 类 OOP | 函数式 + builder 模式 |
| 子命令实现 | .command() 链式嵌套 | builder 内嵌 command() |
| 位置参数 | command('<input> [output]') 字符串定义 | positional() 方法定义 |
| 中间件 | 无原生支持 | 原生 .middleware() |
| 选项校验 | 自定义 parser 函数 | choices/type 内置校验 |
| Help 定制 | .helpInformation() / .addHelpText() | usage() / epilog() / .showHelp() |
| TypeScript 支持 | 好(官方类型定义) | 好(官方类型定义) |
| 学习曲线 | 平缓 | 中等 |
选型建议
- 团队偏好函数式编程或需要中间件机制时,选择 Yargs。
- 偏好简洁的类 OOP 风格、需要更精细的 help 定制时,选择 Commander.js。
- 两个框架都在活跃维护,pkg 质量和社区支持都很好。
交互式 CLI
Inquirer.js
Inquirer.js 是最流行的 Node.js 交互式命令行提示库。
安装
npm install inquirer基本用法
import inquirer from 'inquirer';
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '项目名称是什么?',
default: 'my-project',
},
]);
console.log(`项目名称: ${answers.name}`);输入类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
input | 文本输入 | 自由文本输入 |
confirm | 是/否 | 确认操作 |
list | 单选列表 | 从选项中选择一个 |
checkbox | 多选列表 | 从选项中选择多个 |
password | 密码输入 | 隐藏输入内容 |
editor | 编辑器打开 | 长文本编辑 |
rawlist | 编号单选 | 键盘快速选择 |
expand | 展开式列表 | 快捷键映射 |
示例:完整交互流程
import inquirer from 'inquirer';
interface ProjectAnswers {
name: string;
type: string;
features: string[];
useTypescript: boolean;
port: number;
}
async function createProject(): Promise<void> {
const answers = await inquirer.prompt<ProjectAnswers>([
{
type: 'input',
name: 'name',
message: '项目名称:',
validate: (input: string) => {
if (!input.trim()) return '项目名称不能为空';
if (!/^[a-z0-9-]+$/.test(input)) return '仅支持小写字母、数字和连字符';
return true;
},
},
{
type: 'list',
name: 'type',
message: '项目类型:',
choices: ['Web 应用', 'Node.js 库', 'CLI 工具', '其他'],
},
{
type: 'checkbox',
name: 'features',
message: '选择功能特性:',
choices: [
{ name: 'TypeScript', value: 'ts', checked: true },
{ name: '单元测试', value: 'test' },
{ name: 'Linter', value: 'lint' },
{ name: 'Git Hooks', value: 'hooks' },
],
},
{
type: 'confirm',
name: 'useTypescript',
message: '是否使用 TypeScript?',
default: true,
when: (answers) => !answers.features.includes('ts'),
},
{
type: 'input',
name: 'port',
message: '开发服务器端口:',
default: '3000',
validate: (input: string) => {
const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) {
return '请输入有效的端口号(1-65535)';
}
return true;
},
filter: (input: string) => parseInt(input, 10),
},
]);
console.log('项目配置完成:', JSON.stringify(answers, null, 2));
}搜索与自动完成
Inquirer 支持搜索和自动完成功能(通过 inquirer-autocomplete-prompt 插件):
import inquirer from 'inquirer';
import AutocompletePrompt from 'inquirer-autocomplete-prompt';
inquirer.registerPrompt('autocomplete', AutocompletePrompt);
const choices = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
const answer = await inquirer.prompt([
{
type: 'autocomplete',
name: 'fruit',
message: '选择或搜索水果:',
source: async (_: unknown, input: string) => {
if (!input) return choices;
return choices.filter(c => c.includes(input.toLowerCase()));
},
},
]);验证与默认值
await inquirer.prompt([
{
type: 'input',
name: 'email',
message: '请输入邮箱:',
validate: (input: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(input)) return '邮箱格式不正确';
return true;
},
default: 'user@example.com',
},
{
type: 'password',
name: 'password',
message: '请输入密码:',
mask: '*',
validate: (input: string) => {
if (input.length < 8) return '密码长度至少 8 位';
return true;
},
},
]);Ora 加载动画
Ora 提供终端中的加载动画(spinner),用于表示耗时操作正在进行中。
安装
npm install ora基本用法
import ora from 'ora';
const spinner = ora('正在处理...').start();
try {
// 模拟耗时操作
await someAsyncTask();
spinner.succeed('处理完成');
} catch (error) {
spinner.fail(`处理失败: ${error.message}`);
}成功 / 失败 / 信息 / 警告
const spinner = ora('加载数据...').start();
try {
const data = await fetchData();
spinner.succeed(`加载完成: ${data.length} 条记录`);
} catch (error) {
spinner.fail(`加载失败: ${error.message}`);
}
// 其他状态
spinner.info('这是一条提示信息');
spinner.warn('这是一条警告信息');
// 停止 spinner 并清除文本
spinner.stop();
spinner.clear();颜色控制
const spinner = ora({
text: '正在构建...',
color: 'cyan', // spinner 颜色: black/red/green/yellow/blue/magenta/cyan/white/gray
spinner: 'dots', // spinner 样式
prefixText: '[构建]', // 前缀文本
}).start();
// 动态改变颜色
spinner.color = 'yellow';
spinner.text = '编译模块...';多 spinner
import ora from 'ora';
const spinners = [
ora('任务 A').start(),
ora('任务 B').start(),
ora('任务 C').start(),
];
// 模拟三个并行任务
await Promise.all([
delay(2000).then(() => spinners[0].succeed('任务 A 完成')),
delay(3000).then(() => spinners[1].succeed('任务 B 完成')),
delay(1500).then(() => spinners[2].fail('任务 C 失败')),
]);定时器
import ora from 'ora';
const spinner = ora({
text: '下载中...',
spinner: 'dots',
}).start();
// 更新 spinner 文本显示耗时
const startTime = Date.now();
const interval = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
spinner.text = `下载中... (${elapsed}s)`;
}, 100);
try {
await downloadFile();
clearInterval(interval);
spinner.succeed(`下载完成 (${((Date.now() - startTime) / 1000).toFixed(1)}s)`);
} catch (error) {
clearInterval(interval);
spinner.fail('下载失败');
}Chalk 颜色输出
Chalk 是终端文字样式库,支持链式调用和模板标签。
安装
npm install chalk基本样式
import chalk from 'chalk';
console.log(chalk.red('错误信息'));
console.log(chalk.green('成功信息'));
console.log(chalk.blue('提示信息'));
console.log(chalk.yellow('警告信息'));
console.log(chalk.gray('次要信息'));样式修饰
console.log(chalk.bold('粗体'));
console.log(chalk.dim('暗淡'));
console.log(chalk.italic('斜体')); // 终端支持有限
console.log(chalk.underline('下划线'));
console.log(chalk.strikethrough('删除线'));
console.log(chalk.inverse('反色'));
console.log(chalk.hidden('隐藏'));16 色 / 256 色 / TrueColor
// 16 色
chalk.black('黑色');
chalk.red('红色');
chalk.green('绿色');
chalk.yellow('黄色');
chalk.blue('蓝色');
chalk.magenta('品红');
chalk.cyan('青色');
chalk.white('白色');
chalk.gray('灰色');
// 256 色
chalk.hex('#FF5733')('十六进制颜色');
chalk.rgb(255, 87, 51)('RGB 颜色');
chalk.ansi256(196)('256 色索引');
// TrueColor
chalk.hex('#3498db')('TrueColor 蓝色');
chalk.rgb(52, 152, 219)('TrueColor 蓝色');链式调用
console.log(
chalk.bold.red('错误:'),
chalk.underline.yellow('文件未找到'),
chalk.dim.gray(' (path/to/file)')
);
// 不同的组合
const success = chalk.bold.green;
const error = chalk.bold.red;
const warning = chalk.hex('#FFA500');
console.log(success('操作成功'));
console.log(error('操作失败'));
console.log(warning('操作警告'));模板标签
Chalk 支持 ES 模板标签语法:
import chalk from 'chalk';
const name = '项目';
const status = '成功';
console.log(chalk`{green.bold ${status}}: {blue ${name}} 已创建`);
// 嵌套模板
const message = chalk`
{bold.green 构建结果}
{dim ──────────────────────}
状态: {bold ${status === '成功' ? chalk.green('通过') : chalk.red('失败')}}
耗时: {yellow 3.2s}
`;进度条
cli-progress
cli-progress 提供终端中的进度条显示。
安装
npm install cli-progress单条进度条
import cliProgress from 'cli-progress';
// 创建进度条
const bar = new cliProgress.SingleBar({
format: '进度 |{bar}| {percentage}% | {value}/{total} 项 | 耗时: {duration}s',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true,
});
// 启动进度条
bar.start(100, 0);
// 模拟进度更新
for (let i = 1; i <= 100; i++) {
await delay(50);
bar.update(i);
}
// 停止
bar.stop();多组进度条
import cliProgress from 'cli-progress';
const multibar = new cliProgress.MultiBar({
clearOnComplete: false,
hideCursor: true,
format: '{name} |{bar}| {percentage}% | {value}/{total}',
});
const bar1 = multibar.create(200, 0, { name: '任务 A' });
const bar2 = multibar.create(100, 0, { name: '任务 B' });
const bar3 = multibar.create(150, 0, { name: '任务 C' });
// 并行更新
await Promise.all([
(async () => {
for (let i = 0; i <= 200; i++) {
bar1.update(i);
await delay(10);
}
})(),
(async () => {
for (let i = 0; i <= 100; i++) {
bar2.update(i);
await delay(20);
}
})(),
(async () => {
for (let i = 0; i <= 150; i++) {
bar3.update(i);
await delay(15);
}
})(),
]);
multibar.stop();ETA 显示
const bar = new cliProgress.SingleBar({
format: '进度: {bar} | {percentage}% | ETA: {eta_formatted} | {value}/{total}',
etaBuffer: 10, // ETA 计算的缓冲区大小
});
bar.start(500, 0);
for (let i = 1; i <= 500; i++) {
await delay(Math.random() * 100);
bar.update(i);
}
bar.stop();内置的格式占位符:
| 占位符 | 说明 |
|---|---|
{bar} | 进度条图形 |
{percentage} | 百分比 |
{value} | 当前值 |
{total} | 总值 |
{duration} | 已耗时间 |
{duration_formatted} | 格式化耗时 |
{eta} | 预估剩余秒数 |
{eta_formatted} | 格式化 ETA |
发布与分发
Node.js CLI 发布
package.json bin 字段配置
将 CLI 工具发布到 npm 时,需在 package.json 中配置 bin 字段:
{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "./dist/cli.js"
},
"files": [
"dist"
]
}bin 字段的值可以是一个对象(多命令)或字符串(单命令):
{
"bin": {
"my-cli": "./dist/cli.js",
"my-cli-init": "./dist/init.js"
}
}当用户全局安装该包时,npm 会在 PATH 中创建对应的可执行脚本,将 ./dist/cli.js 映射到 my-cli 命令。
files 字段
files 字段指定发布到 npm 时包含的文件,避免将源码、测试等无关文件发布到 registry。
Shebang
CLI 入口文件顶部必须包含 shebang 行:
#!/usr/bin/env node
// 其余代码如果是 TypeScript 项目,需在编译后的 JS 文件中保留 shebang,或使用 ts-node 运行时直接执行 TS 文件。
同时支持 npx
配置 bin 字段后,用户不安装也可以使用 npx 运行:
npx my-cli buildnpx 会自动下载包并执行,适合一次性使用场景。
发布到 npm
# 构建 TypeScript 项目
npm run build
# 登录 npm
npm login
# 发布
npm publish
# 发布测试版本
npm publish --tag beta打包为独立可执行文件
将 Node.js CLI 打包为独立可执行文件的工具主要有 pkg 和 nexe。
pkg
npm install -g pkg将 CLI 打包为各平台可执行文件:
# 为当前平台打包
pkg ./dist/cli.js --output ./bin/my-cli
# 为多平台打包
pkg ./dist/cli.js --output ./bin/my-cli --targets node18-win-x64,node18-linux-x64,node18-macos-x64pkg 常用选项:
| 选项 | 说明 |
|---|---|
--targets | 目标平台,格式为 node版本-平台-架构 |
--output | 输出路径 |
--compress | 压缩级别(GZip) |
支持的平台标识:
node18-win-x64 # Windows x64
node18-linux-x64 # Linux x64
node18-macos-x64 # macOS Intel
node18-macos-arm64 # macOS Apple Siliconnexe
npm install -g nexenexe ./dist/cli.js --output ./bin/my-cli --buildnexe 的可选配置:
nexe ./dist/cli.js \
--output ./bin/my-cli \
--target windows-x64-18.0.0 \
--resource ./assets/** \
--temp ./tmppkg vs nexe 对比
| 对比维度 | pkg | nexe |
|---|---|---|
| 上手难度 | 简单 | 中等 |
| 跨平台打包 | 内置支持 | 需指定 target |
| 打包速度 | 快 | 慢(需编译 Node.js) |
| 文件体积 | 中(~40MB) | 大(~50MB+) |
| 活跃度 | 维护缓慢 | 社区活跃 |
| Node.js 版本 | 固定版本 | 可指定版本编译 |
打包注意事项
Native Addon 兼容性:
pkg和nexe对.node原生模块(如sharp、bcrypt)的支持有限。需要在目标平台上重新编译,或使用纯 JS 替代方案(如bcryptjs替代bcrypt)。动态 require:
pkg在打包时只打包静态分析的依赖,动态require()的模块可能被遗漏。typescript// 问题:以下动态 require 在 pkg 中不会被打包 const plugin = require(`./plugins/${pluginName}`); // 解决方案:使用 pkg 的 assets 配置 // 在 package.json 中添加 { "pkg": { "assets": ["plugins/**/*"] } }__dirname 和 __filename:在打包后的可执行文件中,
__dirname指向临时解压目录而非原始路径。使用path.join(__dirname, 'assets')可能无法找到资源文件,建议使用path.resolve()或通过pkg的assets配置处理。文件体积:打包后的可执行文件通常包含完整的 Node.js 运行时,体积在 30-50MB 左右。使用 UPX(Ultimate Packer for Executables)可进一步压缩体积。
Shell 自动补全
yargs completion
Yargs 内置了 shell 补全支持:
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
yargs(hideBin(process.argv))
.command('build', '构建项目')
.command('deploy', '部署项目')
.option('verbose', { type: 'boolean' })
.completion('completion') // 注册 completion 子命令
.parse();用户可以通过以下方式启用补全:
# 生成补全脚本
my-cli completion >> ~/.bashrc
# 或直接评估(推荐)
source <(my-cli completion)
# zsh
my-cli completion >> ~/.zshrc自定义补全脚本
如果使用 Commander.js 或不使用 Yargs 的补全,可以手动创建补全脚本。
Bash 补全示例:
# my-cli-completion.bash
_my_cli_completions() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
local commands="build deploy init help"
local opts="--help --version --verbose --config"
case "${prev}" in
build)
COMPREPLY=( $(compgen -W "--mode --output --sourcemap" -- "${cur}") )
;;
deploy)
COMPREPLY=( $(compgen -W "--env --region --force" -- "${cur}") )
;;
*)
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
else
COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") )
fi
;;
esac
}
complete -F _my_cli_completions my-cliFig 自动补全
Fig 是一个跨终端的自动补全工具,支持为任意 CLI 创建补全规范:
// ~/.fig/user/completions/my-cli.ts
const completionSpec: Fig.Spec = {
name: 'my-cli',
description: '我的 CLI 工具',
subcommands: [
{
name: 'build',
description: '构建项目',
options: [
{ name: ['-m', '--mode'], description: '构建模式', args: { suggestions: ['production', 'development'] } },
{ name: '--sourcemap', description: '生成 source map' },
{ name: ['-o', '--output'], description: '输出目录', args: { template: 'folders' } },
],
},
{
name: 'deploy',
description: '部署项目',
options: [
{ name: ['-e', '--env'], description: '环境', args: { suggestions: ['prod', 'staging', 'dev'] } },
{ name: ['-r', '--region'], description: '区域' },
],
},
],
options: [
{ name: ['-h', '--help'], description: '显示帮助' },
{ name: ['-V', '--version'], description: '显示版本' },
],
};
export default completionSpec;oh-my-zsh 插件
如果用户使用 oh-my-zsh,可以创建自定义补全插件:
# ~/.oh-my-zsh/custom/plugins/my-cli/my-cli.plugin.zsh
# my-cli zsh 补全
#compdef my-cli
_my_cli() {
local -a commands
commands=(
'build:构建项目'
'deploy:部署项目'
'init:初始化项目'
'help:显示帮助'
)
_arguments \
'(-h --help)'{-h,--help}'[显示帮助]' \
'(-V --version)'{-V,--version}'[显示版本]' \
'(-v --verbose)'{-v,--verbose}'[详细输出]' \
'--config[配置文件路径]:filename:_files' \
'*: :->command'
case $state in
command)
_describe 'command' commands
;;
esac
}
compdef _my_cli my-cli启用插件:
# ~/.zshrc
plugins=(git my-cli)通用补全建议
- 优先使用 yargs 的内置
completion功能,减少维护成本。 - 如果使用 Commander.js,可评估 commander-completion 等第三方包。
- 为 CLI 提供
--generate-completion选项,让用户自行生成补全脚本。 - 补全脚本应随 CLI 一起发布到 npm,便于用户安装后直接启用。