命令是用户在可执行文件名之后输入的固定单词,用来指定业务领域或动作。在 git commit 中,git 是可执行文件,commit 是命令。在 func 中,命令由带有 @Command('commit') 的类建模。
path 既可以是一个字符串,也可以是字符串数组。多个命令可以共享前缀,func 会把所有 Command path 和 Handler path 编译成命令图,并按最长路径选择唯一入口。
按常见调用举例
| 用户输入 | 命中的路径 | 其余部分的含义 |
|---|---|---|
ship status | Command status | 没有额外输入,运行默认处理器。 |
ship config profile set dev | Command + Handler set | dev 是处理器收到的位置输入。 |
ship deploy --env prod | Command deploy | —env prod 是 deploy 的值选项。 |
ship --help | Module action --help | 根模块可以提供全局动作选项。 |
主命令和缺失命令在概念中有更加详细的介绍;关于模块的作用域动作见共享字段。需要 --help 时,请参考帮助。
这些概念有些复杂,我们不必一次全部弄懂,作为最简单的入门思维方式,只需要知道 status 命令是由一个使用 @Command('status') 装饰器的类驱动运行即可,这是固定的一对一关系。我们先跟随下方的起步项目看看 status 命令的源码和效果。
添加最小可用命令
最小命令只需要一个类和一个默认处理器。path 是用户实际输入的命令路径;description 称作命令的元数据,这里用于描述命令的用途。
import { Command, Handler } from 'func'
@Command({
path: 'status',
description: 'Print service status',
})
export class StatusCommand {
@Handler()
run() {
console.log('All systems operational')
}
}创建文件并添加装饰器后,还要把该类加入 Module 的命令列表:
import { StatusCommand } from './status.command'
@Module({
commands: [StatusCommand],
})
export class AppModule {}func 匹配到 StatusCommand 后会创建实例,并且开始在类中寻找 @Handler() 标注,随后开始运行 StatusCommand.run() 方法。简单来说 @Handler() 就是是命令的处理器,也就是默认运行入口。每个命令至少需要 1 个处理器,后续你也可以了解如何为单个类添加多个处理器。
命令的别名
别名是命令的其他调用途径,它不会创建新节点,也不会调用其他处理器,只提供便捷入口。
@Command({
path: 'status',
aliases: ['s', 'stat'],
description: 'Print service status',
})
export class StatusCommand {}现在我们运行 ship s 和 ship stat 现在都等同于 ship status,因为这些都是 status 命令的别名。
多个别名使用 aliases;单数 alias 与其合并并去重。命令别名可以超过一个字符,且只需要在同一父路径中保持唯一;-h 这样的选项别名则必须是非数字的单个字符。
命令路径
当多个命令共享一个固定前缀时,你可以将前缀固定在 Command 上作为路径,只有连续命中路径才会激活当前类。比如这里 config profile 默认一同出现,我们直接使用组合路径作为 Command 的命中条件。
在路径中,别名只代表最后一段,这里输入 ship config profile 和 ship config p 是相等的。
import { Command, Handler } from 'func'
@Command({
path: ['config', 'profile'],
aliases: ['p'],
description: 'Manage saved profiles',
})
export class ProfileCommand {
@Handler()
list() {
console.log('List profiles')
}
@Handler('set')
set() {
console.log('Set profile')
}
}注意,这里我们也通过 @Handler('set') 额外扩展了新的路径,这是一个新的语法,使用多个处理器分别处理不同的子路径:
- 默认处理器
@Handler():ship config profile - 子处理器
@Handler('set'):ship config profile set
你可能已经发现,子处理器与路径命令都是在做类似的事情,只不过它们处理的维度不同,如果你的业务逻辑庞大,建议使用 Command 路径进行分类,如果尾端分叉的只是细微逻辑,则可直接调用子处理。
多处理器
同一资源有多个紧密相关的动作时,可以共用一个命令类,通过处理器路径区分具体动作,例如 project、project create 和 project member add:
import { Command, Handler } from 'func'
@Command({
path: 'project',
aliases: ['p', 'proj'],
description: 'Manage projects and members',
})
export class ProjectCommand {
@Handler()
list() {
console.log('List projects')
}
@Handler('create')
create() {
console.log('Create project')
}
@Handler(['member', 'add'])
addMember() {
console.log('Add project member')
}
@Handler({ flag: 'version', alias: 'v', description: 'Print version' })
version() {
console.log('1.0.0')
}
}ship project运行默认的list()处理器。ship project create运行create()路径处理器。ship p member add通过命令别名和最长路径匹配运行addMember()。ship project -v通过短别名运行 version 方法。
处理器标志用来选择互斥动作;字段 @Flag() 只向已选中的处理器提供布尔值。处理器标志同时接受 alias 和 aliases,但每个别名都必须是非数字的单个字符。path 不能声明 aliases,也不能与处理器 flag 同时使用。
废弃命令
在 @Command 中设置 deprecated: true 可以标记整个命令,也可以传入字符串提供迁移说明。废弃命令仍然可以执行,会在结构化帮助中标明,并在实际分派时向 stderr 输出不影响退出码的提示。
在带有 @Handler({ flag }) 的方法上使用 @Deprecated(),可以只废弃该动作选项。默认 Handler 和 path Handler 不能使用 @Deprecated();这类场景应废弃其所属命令。
组织多个命令
写下完整的命令示例,区分其中的固定语法和可变数据,就可以确定各部分应使用的 func 装饰器:
| 类别 | 表现 | 评分 |
|---|---|---|
@Command | 一段或多段固定单词形成可复用的命令路径。 | ship deploy / ship config profile |
@Handler(path) | 固定的后续单词用于选择当前命令类中的一个动作。 | ship project member add |
@Flag / @Value | 具名数据需要类型、默认值、别名、描述或校验。 | ship deploy --env prod |
@Args().inputs | 剩余路径是动态可变数据,而不是固定语法。 | ship search alice team-a |
一个应用可以注册多个命令,通常每个业务领域对应一个类。较大的业务领域可以放入独立的 @Module,由根模块 import。共享前缀并不要求这些命令位于同一个类中;当不同动作需要不同依赖或团队边界时,可以拆成多个 Command。
一次 CLI 调用只会执行一个 Command 或动作入口。需要连续执行多个业务操作时,可以定义一个专用命令,并在处理器中调用共享 Provider。