Создайте кроссплатформенный интерфейс командной строки с помощью Deno за 5 минут
(Первоначально опубликовано на deno.com/blog.)
Интерфейсы командной строки («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" 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.