简介
最近想要使用 node 写一个命令行工具,因网络上的分享的文章都过于冗长,且大部分都是脚手架、 commander 的版本,所以就有了这篇文章,只保留编写命令行工具所需的章节。
这里使用 yargs
、typescript
构建一个像 git 那样多级子命令的基础模版。已开源至
cli-template
什么是 yargs
通过链式 api 配置的方式处理命令行输入参数,提供子命令、参数校验、类型格式化、帮助提示、中间件等功能
基本用法
#!/usr/bin/env node
require('yargs')
.scriptName("pirate-parser")
.usage('$0 <cmd> [args]')
.command({
command: 'hello [name]',
describe: 'welcome ter yargs!',
builder: function (yargs) {
return yargs.options({
'name': {
type: 'string',
default: 'Cambi',
describe: 'the name to say hello to'
}
})
},
handler: function (argv) {
console.log('hello', argv.name, 'welcome to yargs!')
}
})
.help()
.argv
$ pirate-parser --help
pirate-parser <cmd> [args]
Commands:
pirate-parser hello [name] welcome ter yargs!
Options:
--help Show help [boolean]
添加命令行参数
官方提供了多种方式配置参数,如.alias(); .array()
等,但推荐使用 builder 的方式组织参数配置,方便管理。
基本参数类型
command({
builder: function (yargs) {
return yargs.options({
a: { type: 'boolean' },
b: { type: 'string' },
c: { type: 'number' },
file: {
type: 'string',
// 设置必填项,执行命令时未输入参数会提示:"缺少必须的选项:b"
require: true,
// 要求选项必须提供一个值
requiresArg: true,
// 别名,短参数名, 使用时输入 -file、-f 都是一样的
alias: 'f',
// 默认值
default: '/etc/passwd',
// 参数描述
describe: 'x marks the spot',
// 在 help 中隐藏此选项的提示
hidden: true
}
})
},
})
array
yargs.options({
arr: { type: 'array' }
})
$ cli --arr=1 --arr=2 --arr=3
# argv 输出
{
arr: [1, 2, 3]
}
枚举
输入的值只能使用给定的值,输入其他值抛出异常
yargs.options({
type: { choices: ['1', '2', '3'] }
})
$ cli --type=2
# argv 输出
{
type: '2'
}
计数器
# 将其解析值设置为参数出现的次数而不是true or false。因此默认值为0。
yargs.options({
count: { type: 'count' }
})
$ cli --count --count
# argv 输出
{
arr: 2
}
嵌套对象路径
# 在 typescript 中的 type 字段没有 object 类型
# 目前只能当做 unknown 处理,且对象子参数无格式校验,需手动处理。
yargs.options({
foo: {
# type: 'object' 这行不用写
}
})
$ cli --foo.bar.baz=33 --foo.quux=5
# argv 输出
{
foo: { bar: { baz: 33 }, quux: 5 }
}
提示参数过时
yargs.options({
hidden: { type: 'boolean', deprecated: 'use --display' },
'display': { type: 'string' }
})
# --help 输出
> cli
Options:
--hidden [deprecated: use --display] [boolean]
--display [string]
参数分组
yargs.options({
debug: { type: 'boolean', group: 'debug:' }
'log-level': { choices: ['debug', 'info', 'warn', 'error'], group: 'debug:' }
})
# --help 输出
> cli
debug:
--debug [布尔]
--log-level [可选值: "debug", "info", "warn", "error"]
特殊参数
$ hello A -n tom B C
# argv 输出
{
# 这里可以获取非连词线开头的参数。
_: ['A', 'B', 'C'],
# 默认值是执行文件名,也可以通过 api:scriptName 设置。
'$0': 'examples/cli.js',
}
创建子命令
通过 command 方法,设置 Git 风格的子命令。
export interface IOptions {
tip: string
}
const cmd: CommandModule<{}, IOptions> = {
// 作为配置主命令时 command 选项的 hello 可以替换为 $0
// 必选参数使用<>包裹,如:<param>;
// 可选参数使用[]包裹,如:[param];
// 推荐 command 只填子命令名称,参数通过 builder 配置
command: "hello <tip>",
describe: '子命令',
builder: function (yargs: Argv) {
return yargs
.options({
tip: {
type: string
}
})
// 命令使用格式
.usage('Usage: hello [options]')
.usage('Usage: [-f=<path>] [--debug]')
.example([
// 具体用法示例
['$0 --file "~/config.json"', 'Use custom config'],
['$0 --debug', 'Start in debug mode']
]);
},
handler: async (argv) => {
},
}
export default cmd;
在 main.ts 引入
import hello from 'hello';
const parser = yargs(hideBin(process.argv))
.command(hello)
$ hello --help
Commands:
index.ts 主命令 [默认值]
index.ts hello 子命令
Options:
...
别忘了在每个子命令中也要在 builder 的 yargs 实例里提供usage
和example
使用帮助提示
只需在 main 里面配置一次 help, 每个子命令都可以使用。
const argv = require('yargs')
// 配置一个隐式命令来显示帮助手册字符串
.help('help', '查看命令行帮助')
.alias('h', 'help');
// 在 help 末尾打印的消息
.epilog('如需了解更多信息,请在以下网址查看使用手册 http://example.com')
$ cli hello --help
选项:
-h, --help 查看命令行帮助 [boolean]
示例:
...
如需了解更多信息,请在以下网址查看使用手册 http://example.com
异常拦截
const argv = require('yargs')
.fail(function (msg, err, yargs) {
// handler执行的异常继续向上抛出
if (err) throw err // preserve stack
// 输出帮助信息
console.error(yargs.help());
// 提示参数输入异常
console.error(chalk.red(`\n\n\n===== Execute failed. =====\n\n${msg}\n`));
process.exit(1);
})
调试
使用 ts-node 启动
$ pnpm test
# 传递参数时需添加 --
$ pnpm test -- --file ./config.json
作为 bin 文件启动
1、在 package.json 添加内容
{
"bin": {
"cli": "./bin.js"
}
}
2、添加入口文件
#!/usr/bin/env node
# 将构建目录(dist)下的 index.js 作为脚手架的启动文件
require('../dist/index')
3、连接到环境变量运行
$ npm link . # 挂载到全局环境变量
$ cli -help # 启动
$ npm unlink . # 卸载
在移动项目文件夹时再 unlink 会失败,需要手动进系统目录删除命令,查询位置可以使用命令 where cli
发布到 npm
完善包信息 package.json
{
//包名,在包名称前加自己的 npm 账户名,采用 npm scope 的方式,包目录的组织方式和普通包不一样,而且可以有效的避免和他人的包名冲突
"name": "@mucen/cli"
// 表示包的入口文件
"main": "./lib/index.js",
// 关键字,方面别人搜索到你的包
"keywords": ["typescript", "cli", "template"],
// 作者
"author": "mucen",
// 告诉 npm,publish 时发布哪些文件到 npm 仓库
"files": ["package.json", "README.md", "dist"],
// 项目仓库
"repository": {
"type": "git",
"url": "https://github.com/liyongning/ts-cli.git"
},
}
1、增加发布命令
{
"script": {
"publish": "pnpm build; npm publish --access public"
}
}
2、执行发布
npm login
pnpm publish
其他事项
1、返回值 根据 Unix 传统,程序执行成功返回 0,否则返回 1 。
if (err) {
process.exit(1);
} else {
process.exit(0);
}
2、监听信号事件。
process.on('SIGINT', function () {
console.log('Got a SIGINT');
process.exit(0);
});
发送信号的方法如下:
$ kill -s SIGINT [process_id]
常用工具包
比webpack更加适合开发js库的构建工具
提供交互式命令界面
const yargs = require('yargs');
const inquirer = require('inquirer');
const askName = async () => {
const answers = await inquirer.prompt([
{
message: 'What is your name?',
name: 'name',
type: 'string'
}
]);
console.log(`Hello, ${answers.name}!`);
};
const argv = yargs()
.command('ask', 'use inquirer to prompt for your name', () => {}, askName)
.help('h').argv;
彩色日志输出
清空命令行输出
下载 git 仓库代码
图标、loading 动画
控制台输出艺术字
系统fs模块的扩展,提供了更多便利的 API,支持 promise
高性能模版引擎
轻量级 debug lib, 通过环境变量切换不同模块的 log 输出
在控制台中显示进度条,如:downloading [===== ] 39/bps 29% 3.7s
版本升级提示
获取 npm package manifests,可用于提示版本升级等功能
调用系统命令
编写 shell 脚本更舒适的工具, 写法如下:
// 全局引入 `$`
import 'zx/globals'
let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
- 需要在
package.json
设置"type": "module"
;tsconfig.json
设置"module": "ESNext"
- ts-node 需要如下命令启动:
node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts
不使用 ESModule 的话还可以选择 shelljs
优秀社区项目
Yeoman 是一个通用的脚手架系统,允许创建任何类型的应用程序。它允许快速开始新项目并简化现有项目的维护。如创建新项目、模块、工作流程
微型生成器工具,创建路由、控制器、组件等