跳轉到主要內容
Deno 2 終於來了 🎉️
了解更多
How to build a CLI with Deno.

在 5 分鐘內使用 Deno 建立跨平台 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,我们可以使用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" }
})

有关 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/[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 CodeDeno 测试会自动检测到,您可以直接从 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 工具

你正在使用 Deno 构建东西吗?在 TwitterDiscord 上告诉我们。