仅需5分钟:使用Deno构建跨平台CLI

330 阅读4分钟

原文:deno.com/blog/build-…

作者:Andy Jiang 2023年9月1日

译者注:读者也可以参考我这篇《手把手教你用Deno写个脚手架

命令行界面("CLI")非常实用,简单易用,在许多情况下是完成任务的最快方式。虽然有许多构建CLI的方法,但Deno的零配置、一体化现代工具和将脚本编译成可移植可执行二进制文件的能力使构建CLI变得轻而易举。

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

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

通过构建CLI,我们将涵盖以下内容:

  • 设置你的CLI
  • 解析参数
  • 与浏览器方法交互
  • 管理状态
  • 测试
  • 编译和分发
  • 其他资源

设置你的CLI

如果你尚未安装Deno,请安装它,并设置你的IDE

接下来,创建一个用于你的CLI的文件夹。我们将其命名为greetme-cli

在该文件夹中,创建main.ts,其中包含逻辑,以及greetings.json,其中包含随机问候的JSON数组

在我们的main.ts中:

import greetings from "./greetings.json" assert { type: "json" };

function main(): void {
  console.log(
    `${greetings[Math.floor(Math.random() * greetings.length) - 1]}!`,
  );
}

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标准库中的flags模块而不是手动解析Deno.args,这是一组由核心团队审核的模块。这是一个示例:

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        显示此帮助并退出
-s --save        保存设置以供将来的问候使用
-n --name        设置问候时的您的名字
-c --color       设置问候的颜色

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 {
  const booleanArgs = [
    "help",
    "save",
  ];

  const stringArgs = [
    "name",
    "color",
  ];

  const alias = {
    "help": "h",
    "save": "s",
    "name": "n",
    "color": "c",
  };

  return parse(args, {
    alias,
    boolean: booleanArgs,
    string: stringArgs,
    stopEarly: false,
    "--": true,
  });
}

还创建一个printHelp函数,当启用--help标志时,将打印信息:

function printHelp(): void {
  console.log(`Usage: greetme [OPTIONS...]`);
  console.log("\nOptional flags:");
  console.log("  -h, --help                显示此帮助并退出");
  console.log("  -s, --save                保存设置以供将来的问候使用");
  console.log("  -n, --name                设置问候时的您的名字");
  console.log("  -c, --color               设置问候的颜色");
}

最后,让我们在我们的main函数中将所有这些连接起来:

function main(inputArgs: string[]): void {
  const args = parseArguments(inputArgs);

  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                显示此帮助并退出
  -s, --save                保存设置以供将来的问候使用
  -n, --name                设置问候时的您的名字
  -c, --color               设置问候的颜色


$ 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 (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 (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 (!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 compile,Deno可以轻松分发你的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的创建者如何在他们的GitHub Actions构建和发布工作流中使用deno compile请查看这篇博客文章

其他资源

虽然本教程展示了如何使用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工具。

使用这些模块和框架,你可以更轻松地构建功能强大的CLI工具,具体取决于你的需求和创造力。希望这些资源对你有所帮助,并使你的CLI项目更加有趣和实用!