在 5 分鐘內使用 Deno 建立跨平台 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
,我们可以使用flags
模块,它来自Deno 的标准库,它是一组由核心团队审计的模块。以下是一个示例
// parse.ts
import { parse } from "https://deno.land/[email protected]/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/[email protected]/flags/mod.ts";
import type { Args } from "https://deno.land/[email protected]/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/[email protected]/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 使使用deno compile
分发您的 CLI(或任何 Deno 程序)变得容易,它会将您的 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 工具