CLI
命令行接口(CLI)程序是通过终端以文本输入和文本输出形式进行交互的程序。
Deepkit中的CLI应用程序可以完全访问DI容器,因此可以访问所有提供者和配置选项。
CLI应用程序的参数和选项由方法参数通过TypeScript类型控制,并自动进行序列化和验证。
CLI是Deepkit Framework应用程序的三个入口点之一。在Deepkit框架中,应用程序总是通过CLI程序启动,该程序本身是由用户用TypeScript编写的。因此,没有针对Deepkit的全局CLI工具来启动Deepkit应用程序。这是你启动HTTP/RPC服务器、执行迁移或运行你自己的命令的方式。这都是通过同一个入口点,同一个文件完成的。一旦通过从`@deepkit/framework`中导入`FrameworkModule`来使用Deepkit框架,应用程序就会得到应用服务器、迁移等的额外命令。
CLI框架允许你基于简单的类轻松注册自己的命令。事实上,它是以`@deepkit/app`为基础的,这个小包只是为了这个目的,也可以在没有Deepkit框架的情况下独立使用。这个包包含了装饰CLI控制器类所需的装饰器。
控制器由依赖性注入容器管理或实例化,因此可以使用其他提供者。更多细节见依赖注入一章。
Installation
由于Deepkit中的CLI程序是基于运行时类型的,因此有必要正确安装@deepkit/type。参见运行时类型安装。
如果成功完成,可以安装@deepkit/app或Deepkit框架,该框架已经在引擎盖下使用该库。
npm install @deepkit/app
注意,`@deepkit/app`是基于TypeScript装饰器的,该功能必须通过`experimentalDecorators`相应启用。
文件:tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "es6",
"moduleResolution": "node",
"experimentalDecorators": true
},
"reflection": true
}
一旦库被安装,可以直接使用其中的API。
使用
为了给你的应用程序创建一个命令,你需要创建一个CLI控制器。这是一个简单的类,它有一个方法`exeecute`,并配备了关于命令的信息。
文件:app.ts
#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';
@cli.controller('test', {
description: 'My first command'
})
class TestCommand {
async execute() {
console.log('Hello World')
}
}
new App({
controllers: [TestCommand]
}).run();
在装饰器`@cli.controller`中,CLI应用程序的唯一名称被定义为第一个参数。进一步的选项,如描述,可以选择在对象中的第二个位置添加。
这段代码已经是一个完整的CLI应用程序,可以像这样启动:
$ ts-node ./app.ts
VERSION
Node
USAGE
$ ts-node app.ts [COMMAND]
COMMANDS
test
你可以看到,一个 "测试 "命令可用。要运行这个,必须将名称作为参数传入:
$ ts-node ./app.ts test
Hello World
也可以用`chmod +x app.ts`使文件可执行,这样`./app.ts`命令已经足以启动它。应该注意的是,这时需要一个所谓的
shebang。Shebang指的是脚本程序开始时的字符组合`#!。在上面的例子中,已经出现了:
#!/usr/bin/env ts-node-script`,并且使用了`ts-node`的脚本模式。
$ ./app.ts test
Hello World
通过这种方式,可以创建和注册任何数量的命令。在`@cli.controller`中给出的唯一名称应该选择得当,并允许用`:`字符对命令进行分组(例如,user:create
,user:remove
,等等)。
Arguments
为了添加参数,新参数被添加到`execute`方法中,并用`@arg`装饰器进行装饰。
import { cli, arg } from '@deepkit/app';
@cli.controller('test')
class TestCommand {
async execute(
@arg name: string
) {
console.log('Hello', name);
}
}
如果你现在执行这个命令而没有指定一个名字,将会产生一个错误:
$ ./app.ts test
RequiredArgsError: Missing 1 required arg:
name
使用`--help`会给你更多关于所需参数的信息:
$ ./app.ts test --help
USAGE
$ ts-node-script app.ts test NAME
一旦名字被作为参数传递,TestCommand中的`execute`方法将被执行,名字将被正确传递。
$ ./app.ts test "beautiful world"
Hello beautiful world
Flags
Flags是另一种向命令传递数值的方式。通常这些是可选的,但不一定是。用`@flag name`装饰的参数可以通过`--name value`或`--name=value`传递。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag id: number
) {
console.log('id', id);
}
}
$ ./app.ts test --help
USAGE
$ ts-node app.ts test
OPTIONS
--id=id (required)
在帮助视图中,你现在可以在 "OPTIONS "中看到一个`--id`标志是必要的。如果指定正确,命令就会收到这个值。
$ ./app.ts test --id 23
id 23
$ ./app.ts test --id=23
id 23
布尔标志
标志的优点是,它们也可以作为无值标志使用,例如,激活某种行为。只要一个参数被标记为可选的布尔值,这个行为就会被激活。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag remove: boolean = false
) {
console.log('delete?', remove);
}
}
$ ./app.ts test
delete? false
$ ./app.ts test --remove
delete? true
多个标志
为了向同一个标志传递多个值,可以将一个标志标记为一个数组。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag id: number[] = []
) {
console.log('ids', id);
}
}
$ ./app.ts test
ids: []
$ ./app.ts test --id 12
ids: [12]
$ ./app.ts test --id 12 --id 23
ids: [12, 23]
单字符标志
为了让标志也能以单字符形式传递,可以使用`@flag.char('x')`。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag.char('o') output: string
) {
console.log('output: ', output);
}
}
$ ./app.ts test --help
USAGE
$ ts-node app.ts test
OPTIONS
-o, --output=output (required)
$ ./app.ts test --output test.txt
output: test.txt
$ ./app.ts test -o test.txt
output: test.txt
可选/默认
`execute`方法的签名定义了哪些参数或标志是可选的。如果参数被标记为可选,则不需要指定。
class TestCommand {
async execute(
@arg name?: string
) {
console.log('Hello', name || 'nobody');
}
}
$ ./app.ts test
Hello nobody
有默认值的参数也是如此:
class TestCommand {
async execute(
@arg name: string = 'body'
) {
console.log('Hello', name);
}
}
$ ./app.ts test
Hello nobody
这也以同样的方式适用于标志。
序列化/验证
所有的参数和标志都会根据它们的类型自动反序列化,进行验证,并且可以提供额外的限制。
因此,定义为数字的参数在控制器中总是被保证为实数,尽管命令行界面是基于文本的,因此是字符串。这种转换是通过xref:serialization.adoc#serialisation-loosely-conversion功能自动发生的。
class TestCommand {
async execute(
@arg id: number
) {
console.log('id', id, typeof id);
}
}
$ ./app.ts test 123
id 123 number
可以用`@deepkit/type`的类型装饰器来定义额外的约束。
import { Positive } from '@deepkit/type';
class TestCommand {
async execute(
@arg id: number & Positive
) {
console.log('id', id, typeof id);
}
}
`id`的`Postive`类型表示只需要正数。如果用户现在传递一个负数,`执行’中的代码根本不被执行,并出现一个错误信息。
$ ./app.ts test -123
Validation error in id: Number needs to be positive [positive]
如果数字是正数,命令的功能又和以前一样。这种额外的验证非常容易进行,使命令更加强大,可以防止错误的输入。更多信息请参见Validation章节。
描述
为了描述一个标志或参数,可以使用`@flag.description`或`@arg.description`。
import { Positive } from '@deepkit/type';
class TestCommand {
async execute(
@arg.description('The users identifier') id: number & Positive,
@flag.description('Delete the user?') remove: boolean = false,
) {
console.log('id', id, typeof id);
}
}
在帮助视图中,这个描述出现在标志或参数的后面:
$ ./app.ts test --help
USAGE
$ ts-node app.ts test ID
ARGUMENTS
ID The users identifier
OPTIONS
--remove Delete the user?
退出代码
退出代码默认为0,这意味着命令被成功执行。要改变退出代码,应该在`exucute`方法中返回一个非0的数字。
@cli.controller('test')
export class TestCommand {
async execute() {
console.error('Error :(');
return 12;
}
}
$ ./app.ts
Error :(
$ echo $?
12
依赖注入
命令的类由DI容器管理,所以可以定义依赖关系,通过DI容器解决。
#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';
import { Logger, ConsoleTransport } from '@deepkit/logger';
@cli.controller('test', {
description: 'My super first command'
})
class TestCommand {
constructor(protected logger: Logger) {
}
async execute() {
this.logger.log('Hello World!');
}
}
new App({
providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}],
controllers: [TestCommand]
}).run();