Создайте кроссплатформенный интерфейс командной строки с помощью Deno за 5 минут

(Первоначально опубликовано на deno.com/blog.)

Интерфейсы командной строки («CLI») полезны, просты в использовании и во многих случаях являются самым быстрым способом сделать что-то. Несмотря на то, что существует множество способов создания CLI, отсутствие необходимости настройки Deno, современные инструменты «все в одном», а также возможность скомпилировать ваш скрипт в портативный исполняемый двоичный файл значительно упрощают создание CLI.

В этом посте мы рассмотрим создание базового CLI — greetme-cli. Он принимает ваше имя и цвет в качестве аргументов и выводит случайное приветствие:

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

При создании CLI мы рассмотрим:

Настройте свой интерфейс командной строки

Если вы еще этого не сделали, установите Deno и настройте свою IDE.

Затем создайте папку для вашего CLI. Мы назовем наш greetme-cli.

В этой папке создайте main.ts, которая будет содержать логику, и greetings.json, которая будет содержать JSON-массив случайных приветствий.

В нашем main.ts:

import greetings from "./greetings.json" assert { 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

Давайте создадим новую функцию с именем parseArguments в main.ts:

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, которая будет console.log получать информацию при включении флага --help:

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.

Мы можем добавить постоянное хранилище в наш CLI, используя Deno KV, которое представляет собой хранилище данных ключ-значение, встроенное прямо во время выполнения. Он поддерживается локально SQLite и FoundationDB при развертывании в Deno Deploy (хотя интерфейсы командной строки не предназначены для развертывания).

Поскольку он встроен в среду выполнения, нам не нужно управлять какими-либо секретными ключами или переменными среды для его настройки. Мы можем открыть соединение с помощью одной строки кода:

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 и записывает данные с помощью .set(), если опция --save равна true. Если в команде не установлено --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 предлагает 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 упрощает распространение вашего 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 (некоторые из них интереснее других):

  • Яргс: современный пиратский преемник Оптимиста
  • cliffy: простой и типобезопасный фреймворк командной строки
  • denomander: фреймворк для создания CLI, основанный на Commander.js.
  • tui: простой фреймворк для создания пользовательских интерфейсов терминала
  • terminal_images: модуль TypeScript для отображения изображений в терминале.
  • cliui: создание сложных многострочных интерфейсов командной строки.
  • мел: раскрашивает вывод терминала (и вот модуль Deno)
  • figlet.js: создает изображение ASCII из текста.
  • dax: кроссплатформенные инструменты оболочки для Deno, вдохновленные zx

Вы что-то строите с Deno? Дайте нам знать в Твиттере или в Discord.