跳到主要内容
Deno 2.4 发布,带来 deno bundle、字节/文本导入、OTel 稳定版等新特性
了解更多
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 和包含随机问候语 JSON 数组greetings.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/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 添加持久存储,Deno KV 是一个内置于运行时中的键值数据存储。它在本地由 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 时可以使用的有用模块(有些比其他更有趣)

  • yargsoptimist 的现代海盗主题继任者
  • cliffy:一个简单且类型安全的命令行框架
  • denomander:一个受 Commander.js 启发用于构建 CLI 的框架
  • tui:一个用于构建终端用户界面的简单框架
  • terminal_images:一个用于在终端显示图片的 TypeScript 模块
  • cliui:创建复杂的、多行 CLI
  • chalk:为终端输出着色(这是 Deno 模块
  • figlet.js:从文本创建 ASCII 艺术字
  • dax:受 zx 启发为 Deno 设计的跨平台 shell 工具

您正在使用 Deno 构建什么吗?请通过 TwitterDiscord 告诉我们。