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:createuser: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();