用 Deno 在 5 分钟内构建跨平台 CLI
命令行界面(“CLI”)非常有用、易于使用,并且在许多情况下,是完成某项工作的最快方式。虽然构建 CLI 的方法有很多,但 Deno 的零配置、一体化现代工具,以及将你的脚本编译成可移植可执行二进制文件的能力,使得构建 CLI 变得轻而易举。
在这篇文章中,我们将介绍构建一个基本的 CLI - greetme-cli
。它接受你的名字和颜色作为参数,并输出一个随机的问候语
$ greetme --name=Andy --color=blue
Hello, Andy!
通过构建这个 CLI,我们将涵盖
设置你的 CLI
接下来,为你的 CLI 创建一个文件夹。我们将其命名为 greetme-cli
。
在该文件夹中,创建 main.ts
,它将包含逻辑,以及 greetings.json
,它将包含 一个随机问候语的 JSON 数组。
在我们的 main.ts
中
import greetings from "./greetings.json" with { type: "json" };
/**
* Main logic of CLI.
*/
function main(): void {
console.log(
`${greetings[Math.floor(Math.random() * greetings.length) - 1]}!`,
);
}
/**
* Run CLI.
*/
main();
当我们运行它时,我们应该看到一个随机的问候语
$ deno run main.ts
Good evening!
很酷,但互动性不足。让我们添加一种解析参数和标志的方法。
解析参数
Deno 将自动解析来自命令行的参数到一个 Deno.args
数组中
// The command `deno run main.ts --name=Andy --color=blue`
console.log(Deno.args); // [ "--name=Andy", "--color=blue" ]
但是,与其手动解析 Deno.arg
,我们可以使用来自 Deno 标准库的 flags
模块,这是一组经过核心团队审核的模块。这是一个例子
// parse.ts
import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts";
console.dir(parse(Deno.args));
当我们使用标志和选项运行 parse.ts
时,parse(Deno.args))
返回一个对象,其中标志和选项映射到键和值
$ deno run parse.ts -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }
$ deno run parse.ts -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }
但是 parse()
最好的部分是能够通过传递一个可选对象来定义类型、分配默认值以及为每个参数创建别名
const flags = parse(Deno.args, {
boolean: ["help", "save"],
string: [ "name", "color"]
alias: { "help": "h" }
default: { "color": "blue" }
})
对于我们的 greetme-cli
示例,让我们添加以下标志
-h --help Display this help and exit
-s --save Save settings for future greetings
-n --name Set your name for the greeting
-c --color Set the color of the greeting
让我们在 main.ts
中创建一个名为 parseArguments
的新函数
import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts";
import type { Args } from "https://deno.land/std@0.200.0/flags/mod.ts";
function parseArguments(args: string[]): Args {
// All boolean arguments
const booleanArgs = [
"help",
"save",
];
// All string arguments
const stringArgs = [
"name",
"color",
];
// And a list of aliases
const alias = {
"help": "h",
"save": "s",
"name": "n",
"color": "c",
};
return parse(args, {
alias,
boolean: booleanArgs,
string: stringArgs,
stopEarly: false,
"--": true,
});
}
以及一个 printHelp
函数,当启用 --help
标志时,它将 console.log
信息
function printHelp(): void {
console.log(`Usage: greetme [OPTIONS...]`);
console.log("\nOptional flags:");
console.log(" -h, --help Display this help and exit");
console.log(" -s, --save Save settings for future greetings");
console.log(" -n, --name Set your name for the greeting");
console.log(" -c, --color Set the color of the greeting");
}
最后,让我们在我们的 main
函数中将它们结合起来
function main(inputArgs: string[]): void {
const args = parseArguments(inputArgs);
// If help flag enabled, print help.
if (args.help) {
printHelp();
Deno.exit(0);
}
let name: string | null = args.name;
let color: string | null = args.color;
let save: boolean = args.save;
console.log(
`%c${
greetings[Math.floor(Math.random() * greetings.length) - 1]
}, ${name}!`,
`color: ${color}; font-weight: bold`,
);
}
现在,让我们使用新支持的标志运行 CLI
$ deno run main.ts --help
Usage: greetme [OPTIONS...]
Optional flags:
-h, --help Display this help and exit
-s, --save Save settings for future greetings
-n, --name Set your name for the greeting
-c, --color Set the color of the greeting
$ deno run main.ts --name=Andy --color=blue
It's nice to see you, Andy!
$ deno run main.ts -n=Steve -c=red
Morning, Steve!
看起来不错。但是我们如何为 --save
选项添加功能呢?
管理状态
根据你的 CLI,你可能希望在用户会话之间持久化状态。例如,让我们通过 --save
标志为 greetme-cli
添加保存功能。
我们可以使用 Deno KV 为我们的 CLI 添加持久化存储,这是一个内置于运行时的键值数据存储。它在本地由 SQLite 支持,在部署到 Deno Deploy 时由 FoundationDB 支持(尽管 CLI 并非旨在部署)。
由于它是内置于运行时的,我们不需要管理任何密钥或环境变量来设置它。我们可以通过一行代码打开连接
const kv = await Deno.openKv("/tmp/kv.db");
请注意,我们需要在 .openKv()
中传递显式路径,因为编译后的二进制文件没有设置默认存储目录。
让我们更新我们的 main
函数以使用 Deno KV
- function main(inputArgs: string[]): void {
+ async function main(inputArgs: string[]): Promise<void> {
const args = parseArguments(inputArgs);
// If help flag enabled, print help.
if (args.help) {
printHelp();
Deno.exit(0);
}
let name: string | null = args.name;
let color: string | null = args.color;
let save: boolean = args.save;
+ const kv = await Deno.openKv("/tmp/kv.db");
+ let askToSave = false;
+ if (!name) {
+ name = (await kv.get(["name"])).value as string;
+ }
+ if (!color) {
+ color = (await kv.get(["color"])).value as string;
+ }
+ if (save) {
+ await kv.set(["name"], name);
+ await kv.set(["color"], color);
+ }
console.log(
`%c${
greetings[Math.floor(Math.random() * greetings.length) - 1]
}, ${name}!`,
`color: ${color}; font-weight: bold`,
);
}
这个简单的添加打开了与 Deno KV 的连接,并在 --save
选项为 true
时使用 .set()
写入数据。如果在命令中未设置 --name
或 --color
,它将使用 .get()
读取数据。
让我们试用一下。请注意,我们需要添加 --unstable
标志来使用 Deno KV,以及 --allow-read
和 --allow-write
来读写文件系统
$ deno run --unstable --allow-read --allow-write main.ts --name=Andy --save
Greetings, Andy!
$ deno run --unstable --allow-read --allow-write main.ts
It's nice to see you, Andy!
CLI 在第二个命令中记住了我的名字!
与浏览器方法交互
有时你可能希望提供除命令行标志之外的其他交互模式。使用 Deno 执行此操作的一种简单方法是通过浏览器方法。
Deno 在可能的情况下提供 Web 平台 API,浏览器方法也不例外。这意味着 你可以访问 alert()
、confirm()
和 prompt()
,所有这些都可以在命令行中使用。
让我们更新我们的 main()
函数,在未设置标志的情况下添加一些交互式提示
async function main(inputArgs: string[]): Promise<void> {
const args = parseArguments(inputArgs);
// If help flag enabled, print help.
if (args.help) {
printHelp();
Deno.exit(0);
}
let name: string | null = args.name;
let color: string | null = args.color;
let save: boolean = args.save;
const kv = await Deno.openKv("/tmp/kv.db");
let askToSave = false;
// If there isn't any name or color, then prompt.
if (!name) {
name = (await kv.get(["name"])).value as string;
+ if (!name) {
+ name = prompt("What is your name?");
+ askToSave = true;
+ }
}
if (!color) {
color = (await kv.get(["color"])).value as string;
+ if (!color) {
+ color = prompt("What is your favorite color?");
+ askToSave = true;
+ }
}
+ if (!save && askToSave) {
+ const savePrompt: string | null = prompt(
+ "Do you want to save these settings? Y/n",
+ );
+ if (savePrompt?.toUpperCase() === "Y") save = true;
+ }
if (save) {
await kv.set(["name"], name);
await kv.set(["color"], color);
}
console.log(
`%c${
greetings[Math.floor(Math.random() * greetings.length) - 1]
}, ${name}!`,
`color: ${color}; font-weight: bold`,
);
}
现在,当我们不带标志运行命令时,我们将收到提示
$ deno run --unstable --allow-read --allow-write main.ts
What is your name? Andy
What is your favorite color? blue
Do you want to save these settings? Y/n Y
Howdy, Andy!
$ deno run --unstable --allow-read --allow-write main.ts --name=Steve
Pleased to meet you, Steve!
太棒了!第二次读取了我们选择通过提示保存的变量。
浏览器方法是在你的脚本或 CLI 中添加交互性的快速简便方法。
测试
在 Deno 中设置测试运行器很容易,因为它内置于运行时。
让我们编写一个简单的测试,以确保 CLI 正确解析输入标志。让我们创建 main_test.ts
并使用 Deno.test()
注册一个测试用例
import { assertEquals } from "https://deno.land/std@0.200.0/testing/asserts.ts";
import { parseArguments } from "./main.ts";
Deno.test("parseArguments should correctly parse CLI arguments", () => {
const args = parseArguments([
"-h",
"--name",
"Andy",
"--color",
"blue",
"--save",
]);
assertEquals(args, {
_: [],
help: true,
h: true,
name: "Andy",
n: "Andy",
color: "blue",
c: "blue",
save: true,
s: true,
"--": [],
});
});
现在,我们可以使用 deno test
和必要的标志来运行测试
$ deno test --unstable --allow-write --allow-read
What's happening, Andy!
running 1 test from ./main_test.ts
parseArguments should correctly parse CLI arguments ... ok (16ms)
ok | 1 passed | 0 failed (60ms)
请注意,如果你正在使用 VS Code,Deno 测试会被自动检测到,你可以直接从你的 IDE 运行它们。
编译和分发
Deno 使分发你的 CLI(或任何 Deno 程序)变得容易,使用 deno compile
,它可以将你的 JavaScript 或 TypeScript 文件编译成一个可在所有主要平台上运行的单个可执行二进制文件。
让我们 deno compile
我们的 main.ts
,并带上运行二进制文件所需的标志
$ deno compile --allow-read --allow-write --unstable main.ts --output greetme
Check file:///Users/andyjiang/deno/greetme-cli/main.ts
Compile file:///Users/andyjiang/deno/greetme-cli/main.ts to greetme
现在你应该在同一目录下有一个 greetme
二进制文件。让我们运行它
$ ./greetme --name=Andy --color=blue --save
It's nice to see you, Andy!
如果我们再次运行它
$ ./greetme
Howdy, Andy!
现在,你可以共享该二进制文件,以便在所有主要平台上运行。有关 Homebrew 的创建者如何使用 deno compile
作为其 GitHub Actions 构建和发布工作流程的一部分的示例,请查看这篇博客文章。
其他资源
虽然本教程展示了如何使用 Deno 构建 CLI,但它非常简单,不需要任何第三方依赖项。对于更复杂的 CLI,拥有模块或框架可以帮助开发。
以下是一些在构建 CLI 时可以使用的有用模块(有些比其他更有趣)
- yargs:现代的、以海盗为主题的 optimist 继任者
- cliffy:一个简单且类型安全的命令行框架
- denomander:一个受 Commander.js 启发的用于构建 CLI 的框架
- tui:一个用于构建终端用户界面的简单框架
- terminal_images:一个用于在终端中显示图像的 TypeScript 模块
- cliui:创建复杂的多行 CLI
- chalk:为终端输出着色(以及 这是 Deno 模块)
- figlet.js:从文本创建 ASCII 艺术字
- dax:受 zx 启发的 Deno 跨平台 shell 工具