Современному пользователю сложно представить себе взаимодействие с операционной системой без мышки или пальца на экране. Интерфейс однозначно ассоциируется с чем-то графическим и оконным, основанным на пользовательском опыте миллионов людей за несколько десятилетий. Это очень удобно, однако в разработке софта есть ещё удалённые уголки Вселенной, где для решения сложных комплексных задач просто нет готовых решений с графическими интерфейсами. Тут на помощь приходит старая добрая командная строка (Command Line Interface, CLI). Поводом для перевода и публикации этой статьи стал интерес команды Artezio к повышению удобства, читаемости и возможности поддержки CLI в части разработки. В конце концов это такой же интерфейс как и графический, он тоже должен быть удобным. Мы очень надеемся, что эти знания окажутся полезными для читателей блога.
Интерфейс командной строки (Command line interface, CLI) – это отличный способ создания прикладных приложений. На их разработку требуется значительно меньше времени, чем на веб-приложения, при этом CLI-приложения предоставляют гибкость для решения технических задач ИТ-инженерам. В веб-приложениях вы можете выполнять действия, которые запрограммировал разработчик. А с помощью CLI вы можете с легкостью самостоятельно объединить несколько инструментов для выполнения сложных сценариев и комплексных задач. Для их использования требуются технические знания, но они прекрасно подходят и для задач администратора, опытного пользователя или создания продуктов для разработчиков.
В Heroku разработали методологию под названием «Двенадцатифакторное приложение». Это набор практик, предназначенных для создания легко поддерживаемых веб-приложений. Ниже похожий набор - 12 правил, которые стоит учитывать при разработке CLI-приложений. Следуя этим принципам, вы сможете создавать приложения для командной строки, которые понравятся пользователям.
Мы также сделали CLI-фреймворк под названием oclif, разработанный с учетом этих принципов для создания интерфейсов командной строки в Node.
1. Хорошая справочная информация
Для CLI-приложений очень важно предоставить пользователям хорошую документацию. Намного важнее, чем при разработке веб-приложений, так как вы не можете подсказывать пользователю через интуитивно понятный графический интерфейс.
Хороший интерфейс командной строки умеет сам предоставлять справочную информацию о себе, а также обладает онлайн-документацией или файлом READMI. Это позволяет быстро разобраться прямо из командной строки, в то же время ваши пользователи могут и загуглить какой-то вопрос (кстати, убедитесь, что Google индексирует страницы с документацией).
Man-документация вам, скорее всего, не нужна (если только это не прямое требование ваших пользователей), она редко используется. Начинающие разработчики ничего о ней не знают, и к тому же она не работает под Windows. Оффлайн-поддержка не нужна, у вас уже есть help в CLI. При этом в нашем фреймворке в oclif мы планируем создать man-документацию. В случае фреймворка имеет смысл создать единое описание для всех интерфейсов.
Убедитесь, что все из вариантов ниже отображают справку в интерфейсе командной строки. Вы не знаете, что будет вводить пользователь, поэтому всё это должно показывать справку.
# list all commands
$ mycli
$ mycli --help
$ mycli help
$ mycli -h# get help for subcommand
$ mycli subcommand --help
$ mycli subcommand -h
-h,--help
следует зарезервировать только в качестве флага для получения справки. Когда речь идет о subcommand help, вы не можете гарантировать, что help не является аргументом, который надо передать подкоманде. Лучше договориться об общем правиле и всегда показывать справку или ошибку с недопустимым аргументом. Есть приложение Heroku под названием “help”, которое не раз вызывало у меня эту проблему.
Автодополнение для Shell – еще один хороший способ помочь пользователям.
Что касается самой справки, покажите описание команды, аргументов, всех флагов и, самое важное, предоставьте примеры типичного применения CLI, даже если оно очевидно для вас. Это самая распространённая часть документации, на которую ссылаются пользователи.
Конечно же, создавая интерфейс командной строки с oclif, вы всё это получаете бесплатно: онлайн-документация, документация в CLI и автодополнение. Мы даже работаем над средством контроля качества кода, чтобы помочь вам везде применять описания.
2. Лучше использовать флаги, чем аргументы
CLI принимает два типа входных данных shell: флаги и аргументы. При использовании флагов строчка ввода будет чуть длиннее, но они делают CLI более понятным. Например, в Heroku CLI мы часто работали с командой heroku fork. Она копировала из исходного приложения в целевое. Изначально использовался следующий флаг и аргумент:
$ heroku fork FROMAPP --app TOAPP
При этом было сложно понять, что было исходным, а что ― целевым приложением. Мы стали использовать флаги для обоих случаев:
$ heroku fork --from FROMAPP --to TOAPP
Так стало понятнее, что есть исходное, а что ― целевое приложение.
Обратите внимание, что мы удалили эту команду из Heroku CLI, но это хороший пример того, как аргументы могут сбивать с толку.
Иногда аргументы вполне можно использовать, когда они очевидны, например, $ rm file_to_remove. Есть хорошее правило: один тип аргумента – это хорошо, два типа – сомнительно, а три – бесполезно.
Для аргументов переменной длины можно применять несколько аргументов (например, $ rm field file2 file3). Но когда они разных типов, это сбивает пользователя с толку.
Для флагов гораздо проще написать логику автодополнения, так как вы точно знаете, каким должно быть значение.
В CLI, которые передают флаги любому другому процессу (например, heroku run), парсер флагов должен принимать -- аргумент для обозначения того, что он должен прекратить парсинг и передать все в качестве аргумента. Это позволяет запустить команду heroku run -a myapp -- myscript.sh -a arg1 (это демонстрирует, как –a может быть флагом для heroku run, а другая –a передается в динамические контейнеры).
3. Какую версию я использую?
Убедитесь, что вы можете узнать версию CLI при помощи:
$ mycli version # multi only
$ mycli --version
$ mycli –V
Если только это не однокомандный CLI, у которого тоже есть флаг -v,--verbose, команда $ mycli –v аналогично должна отображать версию CLI. Запускать три разные команды, чтобы узнать версию CLI, затруднительно до тех пор, пока вы не найдёте нужную.
Команда версии – это основное место, где вы будете запрашивать у пользователей отладочную информацию, поэтому здесь хорошо размещать любой полезный материал, помимо номера версии, который может помочь вам определить проблемы.
Я также предлагаю отправлять строку версии как User-Agent, чтобы вы могли исправить ошибки на стороне сервера (предположим, ваш CLI использует какой-либо API).
4. Следите за потоками
Потоки stdout и stderr позволяют выводить сообщения пользователю, а также перенаправлять их содержимое в файл. Например:
$ myapp > foo.txt
Warning: something went wrong
Поскольку этот Warning находится в stderr, он не попадает в файл. Направляя Warningи в stdout, вы не только скроете их от пользователя, но и создадите проблемы для структурированных данных, таких, как JSON или бинарные файлы. Используйте stderr для ошибок и предупреждений, которые по умолчанию всегда будут отображаться на экране, даже если stdout будет перенаправлен.
Хотя не все в stderr является ошибкой. Например, вы можете использовать команду curl для загрузки файла, и вывод её прогресса будет находиться в stderr. Это позволяет вам перенаправить stdout, но при этом видеть прогресс.
Итак, stdout необходим для вывода, stderr – для сообщений.
Если вы запускаете подкоманду в CLI, убедитесь, что вы всегда передаете stderr этой подкоманды пользователю. В таком случае все ошибки всегда будут выводиться на экран пользователя.
5. Отслеживайте проблемы и нестандартные ситуации
В CLI проблемы возникают гораздо чаще, чем в веб-приложениях. Не имея UI для помощи пользователю, единственное, что мы можем сделать – показать ошибку. Это логичное поведение и неотъемлемая часть использования любого CLI.
Прежде всего, ваши ошибки должны быть информативными. Идеальное сообщение об ошибке должно содержать следующее:
код ошибки,
название ошибки,
описание ошибки (опционально),
способы её исправления,
URL для получения дополнительной информации.
Например, если наш CLI выдал ошибку о проблеме с правами доступа к файлу, можно отобразить следующее сообщение:
$ myapp dump -o myfile.out
Error: EPERM - Invalid permissions on myfile.out
Cannot write to myfile.out, file does not have write permissions.
Fix with: chmod +w myfile.out
https://github.com/jdxcode/myapp
Только подумайте, если бы каждый интерфейс командной строки был таким полезным, как здорово было бы быть программистом.
Всегда есть возможность возникновения необрабатываемых ошибок, ситуации, когда вы не предполагали, что пользователь может с этим столкнуться. Для этого позаботьтесь о возможности просмотреть полную информацию трассировки и отладочную информацию с переменными окружения.
В oclif мы используем отладочный модуль, который позволяет нам выводить операторы отладки, сгруппированные по компонентам, если установлена переменная окружения DEBUG. Мы ведем очень подробное логирование при включенной отладке, это невероятно полезно при исправлении ошибок.
Логи ошибок также могут быть полезными для анализа и исправления ошибок, но убедитесь, что в них имеются временные метки. Рекомендуем периодически очищать их, чтобы они не занимали место на диске, и убедитесь в отсутствии цветовой кодировки ANSI.
6. Выделяйся!
Современным CLI не стоит стесняться быть яркими. Используйте цвета/подсветку для выделения важной информации. Спиннеры, прогресс-бары, счётчики и индикаторы выполнения для отображения длительных задач отлично подойдут, чтобы проинформировать пользователя о ведущейся работе. Применяйте нотификации операционной системы, чтобы показать выполнение длительной задачи.
В то же время у вас должна быть возможность вернуться к классическим схемам отображения информации и понимание, когда стоит это сделать. Например, если stdout пользователя не подключен к TTY (обычно это означает, что он передается в файл), то не отображайте цвета в stdout (аналогично stderr).
Счетчики и индикаторы выполнения также не являются хорошей идеей, если это не TTY. Они отрисовываются из элементов кодировки ANSI, и, конечно, это работает только на экране. В файле они вам совершенно точно не пригодятся.
У пользователя могут быть причины, по которым он не хочет видеть вывод, сияющий как новогодняя ёлка. Учитывайте это, если установлен TERM=dumb, NO COLOR, или, если указано --no-color. Я бы также предложил добавить переменную окружения MYAPP_NOCOLOR=1 для конкретного приложения на случай, если они захотят отключить цвет только в вашем CLI.
7. Предоставьте интерфейс с диалогом, если это возможно
Для принятия входных параметров, если stdin является TTY, предоставьте возможность передать параметры в диалоговом интерфейсе с подсказками, а не заставляйте пользователя обязательно указывать флаг. При этом никогда не делайте диалоги обязательными. Пользователь должен иметь возможность автоматизировать ваш CLI в скрипте.
Ещё одно отличное место для добавления диалогов с подсказками – это подтверждение опасных действий. Например, если вы хотите удалить приложение Heroku, вам придётся снова ввести его имя для подтверждения:
Флажки и радиокнопки — отличный способ улучшить взаимодействие с CLI, когда вы хотите визуально представить параметры пользователю:
8. Используйте таблицы
Обратите внимание, что команда cli.table() из cli-ux@5 позволяет легко создавать таблицы, следуя этим правилам.
Таблицы – распространённый способ вывода данных в CLI. Важно, чтобы каждая строка вывода представляла собой одну «запись» данных. Никогда не выводите границы таблицы. Это неудобно и сложно для парсинга. Ниже пример того, что не стоит делать:
Если каждая строка соответствует одной записи, вы можете использовать команду wc для подсчёта строк или grep для фильтрации каждой строки:
Помните о ширине экрана. Отображайте лишь несколько основных столбцов по умолчанию, но разрешите пользователю передавать команду --columns со списком имён столбцов, указанных через запятую, чтобы добавить и другие данные.
Обрезайте строки, которые будут выходить за пределы текущей ширины экрана, если не задан параметр --no-truncate.
Показывайте заголовки столбцов по умолчанию, но разрешайте их скрывать, используя --no-headers.
Дайте возможность пользователям задавать команду --filter для фильтрации определённых столбцов (обычно это может выполнить grep, но флаг может фильтровать определённые значения ячеек).
Разрешите сортировку по столбцам с помощью --sort. Разрешите обратную сортировку, а также сортировку по множеству колонок.
Разрешите выводить файлы в формате csv или JSON. Отображение необработанных данных в виде JSON – это отличный способ вывода структурированных данных. Ими можно управлять с помощью jq. Хоть jq очень полезен, cut и awk – более простые инструменты, которые лучше работают с данными в csv.
9. Будьте быстрыми
CLI нужно быстро запускаться. Используйте $ time mycli для проверки CLI. Ниже примерный гайд:
<100ms: очень быстро (к сожалению, не выполнимо для скриптовых языков);
100 ms – 500 мс: достаточно быстро, ставим цель попасть в этот диапазон;
500 ms – 2 с: приемлемо, но никого не впечатлит;
2s+: медленно, пользователи предпочтут не использовать ваш CLI на данном этапе.
Очевидно, если ваш CLI выполняет такую важную задачу, как загрузка большого файла или что-либо ограниченное возможностью процессора, выполнение не будет быстрым. В этом случае покажите прогресс-бар выполнения программы или хотя бы спиннер. Просто наличие спиннера создаст впечатление, что CLI быстрее, чем есть на самом деле.
oclif разработан с минимальным потреблением ресурсов. Прямо сейчас на моем компьютере оно составляет около 150 мс, что достаточно хорошо. Для этого не нужно иметь все файлы js в CLI, только команду, которая должна быть запущена. Таким образом, даже если у вас есть сотни команд, ресурсопотребление всё равно будут составлять 150 мс.
10. Стимулируйте доработки и дополнения со стороны пользователей
Сделайте ваш код открытым. Это позволяет пользователям изучать его и самостоятельно находить проблемы. Хорошая идея для сообщества – предлагать образец кода, если это может быть полезно другим. Это положительно скажется на имидже организаций.
Убедитесь также, что вы выбрали лицензию. GitHub и GitLab – это отличное место, где можно разместить ваш CLI, а READMI дает вам отличную возможность для обзора вашего CLI.
Напишите, как запустить CLI локально и выполнить наборы тестов. Предложите документ с рекомендациями по внесению дополнений, чтобы сообщить участникам, что вы ожидаете с точки зрения синтаксиса коммитов, качества кода, тестов и всего остального, что им важно знать.
Добавьте кодекс поведения, даже если вам кажется, что в этом нет необходимости. Для некоторых людей это имеет значение, и они чувствуют себя гораздо лучше, видя, что такой документ существует. Некоторые, возможно, даже не заметят его, но это будет полезно в случае, если кто-то ведёт себя грубо, а у вас будет документ с разъяснениями.
В oclif имеется система плагинов, которая предлагает отличный способ для дополнения вашего CLI. Эти плагины могут быть в дальнейшем включены в основной плагин для предоставления функционала всем пользователям.
11. Предоставьте чёткую информацию о подкомандах
Существуют два типа CLI: однокомандный и многокомандный. Однокомандный CLI – это базовый CLI вида UNIX, такой, как cp или grep. Многокомандный больше похож на git или npm, принимающий подкоманду в качестве первого аргумента.
Если CLI простой и выполняет только одну базовую задачу, он хорошо подходит для однокомандного CLI. Однако для большинства CLI лучше использовать подкоманды.
В любом случае, если пользователь не передает никаких аргументов в CLI, всегда лучше в ответ перечислить подкоманды (для многокомандного) или отобразить справку (для одногокомандного), а не выполнять какие-либо действия по умолчанию. Обычно пользователь делает это перед тем, как сделать что-либо еще.
Если вы начинаете использовать подкоманды, через какое-то время они превращаются в целые полезные подразделы (мы называем их topics (темами) в oclif). Git обычно отделяет тему от подкоманды пробелами:
$ git submodule add git@github.com:oclif/command
В то же время мы используем двоеточие в Heroku CLI:
$ heroku domains:add www.myapp.com
Двоеточие предпочтительнее, чтобы разграничить команду от аргументов, переданных команде. Пользователь быстро узнает, что аргумент 1 – это команда, и как получить о ней справку.
Проще говоря, есть и другая техническая причина, почему мы предпочитаем двоеточие. Для команд уровня тем, таких, как $ heroku domains, мы перечисляем все домены приложения. Если бы мы использовали пробелы для отделения команд от подкоманд и хотели бы, чтобы эта команда уровня темы принимала аргумент, то парсер не смог бы определить, является ли аргумент подкомандой или аргументом команды темы. Таким образом, использование пробелов для разделения приводит к тому, что вы не можете иметь команды уровня темы, которые также принимают аргумент.
12. Следуйте спецификации XDG
Спецификация XDG – это отличный стандарт, который стоит использовать, чтобы узнать, где размещать файлы. Если переменные окружения, такие, как XDG_CONFIG_HOME не говорят об обратном, используйте ~/.config/myapp для файлов конфигурации и ~/.local/share/myapp для файлов данных.
Для файлов кэша используйте ~/.cache/myapp для UNIX, но на MacOS лучше использовать по умолчанию ~/Library/Caches/myapp. На Windows можно применять %LOCALAPPDATA%\myapp.
Вместо вывода мы хотели бы вам предложить высказать свое мнение о применении описанных подходов. Насколько актуальным остается для вас и вашей компании поиск новых решений и инструментов программирования? Также мы хотим напомнить, что в Artezio открыто много вакансий для начинающих и опытных разработчиков, которым было бы интересно поучаствовать в интересных проектах и расширить свой кругозор благодаря новым знаниям и практикам. Полный список актуальный вакансий можно найти здесь.