跳到主要内容
How to build a CLI with Deno.

用 Deno 在 5 分钟内构建跨平台 CLI

命令行界面(“CLI”)非常有用、易于使用,并且在许多情况下,是完成某项工作的最快方式。虽然构建 CLI 的方法有很多,但 Deno 的零配置、一体化现代工具,以及将你的脚本编译成可移植可执行二进制文件的能力,使得构建 CLI 变得轻而易举。

在这篇文章中,我们将介绍构建一个基本的 CLI - greetme-cli。它接受你的名字和颜色作为参数,并输出一个随机的问候语

$ greetme --name=Andy --color=blue
Hello, Andy!

通过构建这个 CLI,我们将涵盖

设置你的 CLI

如果你还没有安装,安装 Deno设置你的 IDE

接下来,为你的 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" }
})

有关 parse() 的更多信息,请参阅此示例此文档

对于我们的 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 CodeDeno 测试会被自动检测到,你可以直接从你的 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 工具

你正在使用 Deno 构建什么?请在 TwitterDiscord 上告诉我们。