由于 sorrycc.com 自己实现了一遍,密码算法没有复用 wordpress 的,之前注册过的付费用户,请先「忘记密码」重新设置密码,再登录。

译:Building a CLI from scratch with TypeScript and oclif

Apr 18, 2024

TEST

编者注:TODO。

我目前正在开发一对命令行界面工具(CLI),一个我已经在这里写过 而另一个我很快会公布。我真的很喜欢良好的基于文本的界面,所以我为自己以及在工作中构建的很多工具都采用了这种形式。我当然还不是这个领域的专家(还),但我喜欢为选项、清晰的标志名称和有用的错误信息确定合理的默认值。尽管如此,我在派对上也还是相当有趣的。

在构建预算 CLI 的过程中,为了避免一些痛苦和痛苦去年我查看了现有的 JavaScript CLI 框架,并决定尝试 oclif它看起来具有正确的功能,大多数情况下不会妨碍你,但在玩了几个小时后,我就是让它做不了太多事情,所以我放弃了它,并选择了 内置的 Node 工具来解析参数util.parseArgs()。你可以在这里 看看我是如何使用它的。

快进到一年左右,我在一个新项目上又处于相同的位置。目前,我只是编译文件并直接使用 node ./dist/command.js 执行它们。这个新的 CLI 将有更多的命令和选项的足迹,所以标志和参数解析只是要完成的工作的一部分。oclif 包再次浮现在我的脑海中,所以我看了一下,并发现自从我上次尝试以来已经有了 2 个主要版本,带有许多我可能会使用的功能:插件、钩子和发布。看起来我能够将其集成到项目中,而不完全重新构架一切,这是一个很大的加分项。如果可以避免的话,我不喜欢被框架束缚。

我立刻开始使用它,并且很快就运行起来了,但不是很确定究竟发生了什么。入门教程 大约有半页长,算上介绍,对于理解我正在做什么帮助不大。你生成了一个完整的 CLI 项目,之后除了阅读代码外,几乎没有什么可继续的。指南API 参考 文档是不错的,但前提是你确切知道你在找什么。我花了大约一个小时阅读文档,并以更好地理解发生了什么,但我必须自己拼凑在一起。

我认为入门教程应该从零开始,逐步介绍基础知识,并在此基础上扩展理解。作为在我的预算命令行界面(CLI)中实现这一目标的一部分,我演示了需要添加的基础组件,贡献了一个命令 无需所有其他模板样板即可添加这些组件,并在此过程中编写了以下教程。希望这些组件在你决定编写 CLI 时能提供帮助!


本教程假设你:

  • 在你的系统上安装了 Node 和 npm
  • 正在或将会使用 TypeScript(以下简称 TS)来构建 CLI

oclif CLI 提供两种选项来创建你需要的文件:

  • generate 命令,它在新目录中从头创建一个新的 npm 项目
  • init 命令,它在现有项目中添加基本配置

generate 命令是快速获得一个完全工作的 CLI 的最简单方法,但它会留下你可能不需要的许多样板,并且关于下一步要做什么留下一些未解答的问题。

我们将从一个空目录开始本教程,并逐步工作,一步一步地创建一个功能性的 CLI,从 init 命令开始。我们将依赖于文档中的链接来扩展这里的内容,并且到最后,你应该为自己的 CLI 项目有一个清晰的前进路径。

首先,我们需要一个新目录和一个 package.json 文件,我们将通过初始化 npm 并安装 TS 来获取:

$ mkdir new-oclif-cli
$ cd new-oclif-cli
$ npm init
# ... 回答所有提示,对于本教程默认值就可以

$ npm install typescript

我们将做绝对最少的设置来让 TS 编译,因为这不是本教程的重点。如果你刚开始使用 TS,《TypeScript 工具在 5 分钟内》是一个很好的起点。现在,我们只想确保 TS 正在正确的地方编译我们的文件。

继续我们上次的进度,创建一个 TS 文件,输出到控制台:

$ mkdir src
$ echo 'console.log("Hi!");' >> src/index.ts

在项目的根目录下添加一个基本的 tsconfig.json 文件,配置源文件和输出目录:

// tsconfig.json
{
	"include": [ "src/**/*" ],
	"compilerOptions": {
		"outDir": "./dist",
		"module": "nodenext"
	}
}

调用我们在这个项目中安装的 TS 包,将这个新文件编译到项目的 dist 目录中,并确保它可以被执行:

$ npm exec tsc
$ node ./dist/index.js
Hi!

如果你在达到这一步时遇到问题,请参考 TS 文档。如果没有,那么恭喜你,你已经用 TS 构建了一个 CLI!

我们将使用 npx 调用 oclif CLI,这将添加必要的 npm 模块、二进制文件和配置:

npx oclif init

这个命令会询问几个问题:

  1. 首先,系统会询问使用哪个目录进行安装。接受默认值以在当前工作目录中安装。
  2. 接下来,系统会询问你的项目导出的命令名称。发布你的项目时这一点很重要,但对于本练习的目的,你可以接受默认值。
  3. 接下来,系统会询问你的模块类型。Node 文档有一份关于模块的详细说明,如果你不确定使用哪一种,这是个很好的起点。虽然这个决定对你整个项目很重要,但对于本教程来说并不那么重要,所以选择你最熟悉的继续。
  4. 下一步将自动进行,因为我们已经使用 npm 安装了一个包。init 命令会根据锁文件的名称自动检测你正在使用的包管理器。命令看到了 package-lock.json 文件,使用 npm 在后台安装了 @oclif/core 包。

如果一切顺利完成,你应该会看到类似 “Created CLI new-oclif-cli” 的消息,并且控制台中没有错误。你还应该拥有:

  • 新的 ./bin 文件夹中有四个新文件
  • 你的 package.json 中有一个 oclif 对象,配置了 bin 名称、数据目录和命令发现策略。还有其他配置选项,我们将在本教程后面介绍其中的一些

在我们继续之前,我们需要根据在 oclif init 命令期间选择的模块类型来更新我们的 package.json 文件。为 ESM 添加一个顶级属性 type 设置为 module,或者对于 CommonJS 设置为 commonjs

// package.json
{
	// ... 其他属性
	"type": "module"
	// ... 或者
	"type": "commonjs"
}

现在我们准备好创建我们的第一个命令了!oclif CLI 包括一个有用的命令 oclif generate command COMMAND_NAME 我们可以使用,但是像 oclif generate 一样,它包括了很多样板代码,所以我们会从头开始构建我们的命令。

./src 中创建一个名为 commands 的目录,并添加一个名为 hello.ts 的文件:

$ mkdir ./src/commands
$ touch ./src/commands/hello.ts

hello.ts 文件中,添加以下内容:

// src/commands/hello.ts
import { Command } from "@oclif/core";

export default class Hello extends Command {
	public async run(): Promise<void> {
		this.log("Hello from oclif!");
	}
}

这是所有命令将采取的基本形式:扩展 Command 类并定义一个 run() 方法。在父类中有许多方法可用,包括我们在这里使用的 log() 方法,它将消息输出到 stdout

我们还没有将我们的 CLI 打包成可执行二进制文件,但我们可以通过使用初始化过程中添加的文件之一来轻松测试该命令:

$ npm exec tsc
$ ./bin/run.js hello
Hello from oclif!

注意: 向前看,我们将假设你在 TS 文件更改后运行 tsc,或者在另一个标签页中运行 tsc -w 以在更改时自动编译。

oclif 的一个卖点是它能够解析和验证当命令运行时传递的 参数标志

我们可以通过在我们创建的类上定义一个设定为对象的静态 args 属性来为我们的命令添加一个参数。这个对象中的键定义了我们在运行时将使用的属性名,值则指示我们期望的参数类型。

让我们给我们的命令添加一个参数,并简单地将值输出到终端:

// src/commands/hello.ts
import { Args, Command } from "@oclif/core";

export default class Hello extends Command {
  static override args = {
    arg1: Args.string(),
  };

  public async run(): Promise<void> {
    const { args } = await this.parse(Hello);
    this.log("Hello from oclif!");
    this.log("arg1: %s", args.arg1);
  }
}

在这个例子中,我们在第一个位置创建了一个 string 参数,解析了命令中的所有参数,然后使用 this.log 的格式化能力输出了值。当我们带一个参数运行命令时,我们可以立即看到值:

$ ./bin/run.js hello an_argument
Hello from oclif!
arg1: an_argument

如果我们添加第二个参数而不修改命令代码,我们将看到一个错误:

./bin/run.js hello an_argument another_argument
   Error: Unexpected argument: another_argument
   See more help with --help

USAGE
  $ new-oclif-cli hello [ARG1]

parse() 方法有两项工作:它既验证传入的参数,也使它们可以在 run() 方法的逻辑中使用。如果你的命令使用了参数或标志,那么这应该在 run() 函数的第一行调用,以避免部分执行。

使用 命令参数 可能会做更多事情,包括文档、预处理、默认值等等。花一些时间来玩转不同的参数类型和选项,以感受可以做什么。

现在,让我们给我们的命令添加一个标志(flag)。在 oclif 中,标志的解析和验证既强大又灵活,所以我们只会在本教程中触及表面。

让我们调整我们的命令来添加一个简单的标志。下面的代码为了简单起见,省略了上面的参数代码,但两者可以共存:

import { Command, Flags } from "@oclif/core";

export default class Hello extends Command {
	static override flags = {
		flag: Flags.boolean(),
	};

	public async run(): Promise<void> {
		const { flags } = await this.parse(Hello);
		this.log("Hello from oclif!");
		this.log("flag: %s", flags.flag ? "yes" : "no");
	}
}

你会注意到这里的语法和参数非常相似。我们有一个静态属性 flags 设置为一个对象,键定义了标志名称,值指示了标志类型。

如果我们带着标志运行我们的命令,输出应该是:

$ ./bin/run.js hello --flag 
Hello from oclif!
flag: yes

类似于参数,如果我们运行一个我们没有定义的标志的命令,结果是一个错误和使用说明文档:

./bin/run.js hello --notflag
   Error: Nonexistent flag: --notflag
   See more help with --help

USAGE
  $ new-oclif-cli hello [--flag]

FLAGS
  --flag

你可以用 命令标志 做更多的事情,包括字符别名、依赖其他标志、可逆性等等。

现在我们对如何构建命令有了更多了解,oclif 可以生成的命令应该更有意义了。运行以下命令,使用一个模板来创建一个新命令:

$ npm exec oclif generate command hello2
Adding hello2 to new-oclif-cli!
Creating src/commands/hello2.ts

这将创建一个新文件 ./src/commands/hello2.ts,包含参数和标志。运行这个新命令的帮助标志将显示它的使用方法:

./bin/run.js hello2 --help            
describe the command here

USAGE
  $ new-oclif-cli hello2 [FILE] [-f] [-n <value>]

ARGUMENTS
  FILE  file to read

FLAGS
  -f, --force
  -n, --name=<value>  name to print

DESCRIPTION
  describe the command here

EXAMPLES
  $ new-oclif-cli hello2

尝试运行基础命令时加上 --help 标志来查看输出。

最后,我们希望用户知道如何使用 CLI,因此我们将使用 oclif 来创建一个 README 文件。首先,在项目目录中创建一个 README.md 文件或打开已有的文件。在文件中的任意位置添加以下模板:

## 目录
<!-- toc -->

## 使用方法
<!-- usage -->

## 命令
<!-- commands -->

注意,顺序、标题和使用哪些标签都由你决定。如果你只想输出命令,就只使用 <!-- commands --> 标签。当你把一切都安排到希望的位置后,运行 oclif 的 readme 命令:

$ npm exec oclif readme 
replacing <!-- usage --> in README.md
replacing <!-- commands --> in README.md
replacing <!-- toc --> in README.md

现在,你已经使用 oclif 构建并记录了一个功能性的 CLI!

推荐的下一步是:

  • 如果你正在构建一个包含多个命令的大型 CLI,请考虑添加一个自定义基类来管理重复参数、继承标志和共享功能。
  • 如果你的 CLI 需要用户定义的功能,请考虑研究插件
  • 如果你需要协助解决问题,请查看 oclif 的调试功能错误处理
  • 当你准备好将你的命令推向世界时,oclif 可以通过发布方面提供多种帮助。