⌨️ Разработка CLI инструментов без боли. Часть II. (Основная)

Lebedev Konstantin
7 min readMay 30, 2021

--

В предыдущем посте я уже рассказал, какую задачу должен решать хороший CLI инструмент, а в этой, наконец покажу, как писать максимально информативный и понятный CLI без боли.

Приступим

OpenSource 💁🏻‍♂️, даже README есть :]

Но! Читать его не нужно, как я уже говорил, «Платформа» это минимум документации, максимум автоматики/команд, поэтому просто повторяйте за мной

# Открываем проект или создаём новый (не забываем про npm init -y)
# И запускаем
npx @mail-core/cli init

Всё! Ровно одна команда npx @mail-core/cli init и CLI готов☝🏻

Что тут произошло?

  • Первым делом, CLI создал директорию ./cli/ которая будет будет хранить все команды
  • Установил bin в package.json и добавил npm.scripts.cli для локального запуска
  • Так же была создана demo-команда
  • В финале выведена инструкция, как этим добром пользоваться
  • И всё это конечно на TypeScript

Просто подумайте, одной командой вы сразу решили все основные проблемы с cli:

  • Настройка окружения и файловая структура
  • Локальный запуск
  • Пример команды (как её кода, так и запуска)

Т.е. вам остаётся только думать об одном — о вашем коде, остальное уже выбрано и настроено за вас:

  • Парсинг параметров и организация команд — Yargs
  • Интерактивное взаимодействие с юзером—Inquirer
  • Стилизация текста—Chalk
  • Спиннер—Ora
  • а ещё расширенная Console, вывод рамок и так далее

Я ни в коем случаи не хочу сказать, что это самый верный выбор, но он сделан, настроен и обкатан. И то драгоценное время, который раньше приходилось тратить на выбор, вы можете потратить на решения реальных задач, а не на муки выбора и хождения по граблям.

Команда

Как я уже сказал, за команды отвечает Yargs, но не в чистом виде, т.к. голый yargs достаточно неудобная штука, поэтому мы скрыли его использование за createCommand.

Чтобы создать команду, вам опять же не нужно читать какую-либо документацию, просто выполняете npx mail-core-cli --help :

npx mail-core-cli --help

Узнаем, что есть команда create и выполняем её

npx mail-core-cli create

В итоге получите папку ./cli/command/test с примером команды:

Команда состоит из трех основный честей:

  • Название npx {package} {name} и описание, которое мы увидим при npx {package} --help
  • Опции — описание аргументов, которые принимает команда
  • И handler который будет выполнен при вызове команды

Если вы работали с yargs или command, то думаю сразу видите отличия, тут нет адового чейнинка и другой магии, всё чётко структурировано и обмазано приличным TS, поэтому пользоваться этим можно без документации 🤗

Запустим

Для этого, как написал create , просто выполняем npm run cli -- test

npm run cli -- test

Как видите, демо печатает описание команды, взаимодействует с юзером и потом печатает введенное значение и полученное через аргументы.

И вроде изян, да? Но, нет ;]

Посмотрите ещё раз на скриншот и потом на код handler

и ответьте: откуда взялись разделители? Почему описание жирным выделено и откуда взялись цвета? Ведь в коде этого нет (ну не считая style)

И вот тут вступает магия пакета @mail-core/cli и самая его мощная часть → console которую вы получаете в handler .

Мы назвали её ExtendedConsole , она имплементирует методы оригинальной, но кроме них, имеет несколько дополнительных:

  • spinner — создание Ora спинера
  • important — вывод важной информации
  • verbose — логирование для --verbose режима
  • list—вывод списка с возможность формирования колонок
  • raw—ссылка на native console
  • ok/fail/done и другие удобства

Но главное, что весь вывод через эту консоль имеет префикс в виде {package.name}@{package.version} чтобы когда вы смотрели логи сразу могли понять, какой именно пакет (+ его версия) это пишет.

Поверьте, это очень и очень удобно, особенно когда в рамках CI или другой задачи, команды выполняется много и разными пакетам. Кроме этого, разработчику не нужно изучать какой-то новый логгер, это всё так console, только лучше.

Но это ладно, кроме этого, консоль уже раскрашена, например console.warn — будет yellow и не просто этот метод примениться ко все аргументам, а только к строкам, например:

Мелочь скажите вы, но эти мелочи повсюду, втыкая вам палки в колеса в самых неожиданных местах.

Но вернемся к выводу

Если с стилизацией всё понятно, то откуда же берется разделитель и главное зачем он?

В самом начале его не было и появился он только после того, когда я понял, что при взаимодействии с юзером (интерактивном)

Всё слиплось

Запрос ввода какой-то инфы, хороше бы отделить от предыдущей записей лога, так появился метод console.nl() который просто печатал пустую строку в нужном мне месте:

Уже лучше

Но это приходилось делать руками, что согласитесь не удобно и криво выглядело. Да, взаимодействие с юзером в то время осуществлялось через глобальный объект cliTools.interactive который по факту являлся оберткой над Inquirer (без которой пользоваться им было болью), кроме этого вывод был без название и версии пакета, из-за чего было не понятно, какой именно пакет запрашивает юзера.

Так появился console.cli

У него 4 основных метода

  • input — текстовый ввод
  • confirm—boolean
  • select—выбор из списка (list, radio, checkbox)
  • require—для запроса опции

И вот когда вы вызывает любой из этих методов или просто методы консоли, она проверяет какой тип вывод был перед этим, если мы просто печатали в консоль, а теперь пытаемся что-то спросить юзера или наоборот, консоль печатает разделитель.

Процесс выбора решения
Финал

Простая логика, но читаемость лога и UX/DX опыт увеличился в разы, а главное, всё это работает из коробки. Для всех. Одинаково.

Не копипастим

Как вы уже заметили, в handler пробрасываются название команды, описание и её опции, но зачем?

Чтобы вы не копипастили, а использовали эти данные/переменные для вывода, как это было показано в демо команде, но самое мощное, это запрос опций, раньше приходилось писать что-то типа этого:

Вот эти корявые || да и Enter name тоже попахивают копипастом, ведь есть уже desc , почему бы его не использовать 💁🏻‍♂️

Опять может показаться, что это мелочь, но на осознание этой мелочи ушло около года.

Кроме этого, console.cli.require решает ещё одну задачу, над которой вы когда-нибудь задумаетесь (если конечно начнёте писать cli, а я очень надеюсь, что после этой статьи вы изменись 🙄), но будет уже поздно.

Фича называет --yes , это когда вам нужно отключить интерактивное взаимодействие с юзером и использовать либо default , либо переданные аргументы и console.cli.require как раз решает эту задачу.

И это не всё

Очень часто, в результате выполнения команды нужно выдать юзеру какие-то инструкции, как например здесь

npx mail-core-cli create

Вроде опять изян, но нет, т.к. результатом выполнения команды может быть запуск N-го количество подкоманд.

Например именно так у нас разворачивается mail-core/platform, который по факту внутри себя вызывает множество других команд, таких как

  • npx @mail-core/cli init
  • npx @mail-core/git init
  • npx @mail-core/lint sync
  • и так далее

Каждая из них это отдельный процесс, но имеющие общего родителя по завершению которого и нужно вывести итоговую сводку:

addProcessSummary — принимает два аргумента, заголовок и детали, кроме такого есть три уровня вывода, это info (по умолчанию), warn и error, они будут выведены в своих «рамках» с соответствующем цветом.

И если вы думаете, что это, то нет, если эту сводную инфу можно получить и сконвертировать в markdown, который автоматически добавить в README.md 🤯

ПОЛЬЗУЙТЕСЬ 👨🏻‍🏫

На сегодняшний день я ваще не понимаю как жил без этого пакета, но судя по отсутствию cli в моих прошлых проекта, жил я так себе :]

Любые ваши мысли, хотелки, багрепорты или неудобства, пишите в issue, помните, мы всё же это обкатывали на себе и возможно могут быть шероховатости при взаимодействии с внешним миром ;]

--

--