Простые способы сделать консольную утилиту удобнее
Качественная командная строка — отличнейшее окружение для работы. История, автокомплит, конвейеры, перенаправления, широчайший набор готовых программ, возможность скопировать-и-вставить даже самую сложную команду — все это делает CLI мощным и удобным инструментом. Ничего удивительного, что разработчики охотно пишут собственные консольные программы и скрипты: автоматизация сборки и деплоя, разворачивание тестовой БД, статистика, мониторинг, хитроумный поиск — поводов не счесть.
Однако «консольная утилита» не означает автоматически «удобная в использовании утилита». Неудачное имя, неполное или отсутствующее описание, большое количество позиционных параметров, запутанное именование переключателей — командную строку можно наполнить когнитивным сопротивлением 1 похлеще самого запутанного графического интерфейса.
В этой статье собраны несложные советы, следование которым поможет сделать свои консольные скрипты более удобными в работе.
Оговорки: советы довольно простые, и если они покажутся вам само собой разумеющимися — отлично. К сожалению, даже о таких простых вещах временами забывают. Далее, я предполагаю, что и вы, и ваши пользователи живете в unix-подобном окружении: полноценный шелл и полноценный набор стандартных программ. Кроме того, я использую слова «программа», «скрипт» и «утилита» как взаимозаменяемые синонимы. И наконец, в качестве примеров в статье приведены фрагменты кода на Perl, однако все советы в равной мере относятся к скриптам и программам на любом языке программирования, а технические приемы легко транслируются на любой язык.
А стоит ли вообще писать новый скрипт?
Лучший скрипт — тот, который не надо писать. Может быть, задача решается простой комбинацией уже существующих программ? Утилиты make , sort , find , grep , lsof , netstat , strace и т.д. не только хороши сами по себе, но и отлично склеиваются через конвейеры («пайпы»).
А может быть, задача решается коротким perl-однострочником, который проще написать заново 2 , чем вспоминать название готового скрипта? Кстати, perl-однострочники тоже прекрасно встраиваются в конвейеры.
Хорошее имя
Какое оно — хорошее имя для хорошей программы? Короткое или длинное? Абстрактное или описывающее поведение? Однословное или составное?
Вот несколько простых правил:
чем реже используется программа, тем длиннее может быть ее имя, и наоборот; сравните часто используемую ls и гораздо более редкую apt-get install ;
чем более узкую задачу решает скрипт, тем более подробным и точным должно быть имя; сравните обобщающе-уклончивое make и донельзя конкретное ps2pdf ;
имя может быть абстрактным и ничего не значащим, но ни в коем случае оно не должно вводить в заблуждение; скрипт для подготовки релиза можно назвать и make-release , и rc1 , но не стоит называть его test-mainline , launch или new-version .
Именованные параметры
Если параметры скрипта становятся сложнее, чем простой список файлов (как у rm ) или пары «что–куда» (как в cp ), «что–где» (как в grep ) — скрипту нужны именованные параметры командной строки.
Getopt::Long входит в стандартную поставку perl с 1994 года и позволяет легко разбирать самые разнообразные опции:
флаги с автоматически доступным отрицанием ( --cache , --no-cache ),
синонимы ( -q и --quiet , -h и --help ),
опции с обязательными значениями (тип значения можно указать: строка, число, вещественное, шестнадцатеричное),
умолчальные значения для опций,
автоматическое заполнение массивов и хешей,
склеивание коротких опции (как perl -lane , ls -la ).
В общем, изучить документацию на Getopt::Long и попрактиковаться в его применении — стоящее дело.
Однобуквенные или многобуквенные опции?
Мне кажется, что практически для каждой опции стоит иметь и однобуквенный, и многобуквенный варианты (как -q и --quiet ). Однобуквенные ключи удобны при интерактивной работе, так как их быстрее набирать, а многобуквенные — в мейкфайлах и других скриптах, так как более понятны при чтении.
Традиционные имена параметров
Хотите, чтобы пользователи быстрее запомнили параметры вашей утилиты — называйте привычные действия привычными именами:
-h , --help — помощь,
-V , --version — вывод версии программы,
-q , --quiet , --silent — режим с менее подробным выводом,
-v , --verbose — режим с более подробным выводом (интересный пример находим в ssh : -v , -vv , -vvv дают все более и более подробное логирование),
-n , --dry-run — пробный запуск без выполнения пишущих действий,
или -n — количество элементов, которые следует обработать,
-o , --output — файл для записи результата,
-f , --file — файл с данными для обработки,
-r , --reverse — обработка в обратном порядке,
-j , --jobs , --parallel — во сколько процессов распараллеливать обработку.
Антипримеры можно часто наблюдать в windows-версиях популярных unix-программ. Например, ping : бесконечная отправка пакетов включается ключом -t вместо умолчального поведения, количество пакетов регулируется ключом -n вместо -c , размер пакета -l вместо -s , TTL -i вместо -t и т.п. Или tracert в сравнении с traceroute : максимальное число прыжков -d и -m соответственно, не резолвить адреса в имена -d и -n . Такой разнобой в именовании очень неудобен, особенно если приходится работать попеременно то с одним, то с другим вариантом программы.
Как объяснить пользователю, что он неправ
Если переданные скрипту параметры не проходят разбор и валидацию, надо корректно сообщить об этом пользователю:
Важно, чтобы сообщение об ошибке было коротким и ясно говорило, что именно не так с параметрами. Недопустимо в ответ на неправильные параметры выводить полную справку — это никак не поможет вашему потребителю понять, что происходит.
Посмотрим, как ведут себя популярные программы:
Здесь все коротко и по существу:
короткое сообщение об ошибке,
подсказка: где смотреть полную справку,
предположение: что на самом деле могло иметься в виду.
Когда приходится умирать
Если скрипт по каким-то внутренним причинам не может продолжать работу — пора вызывать die (=вывести сообщение и завершиться с ненулевым кодом).
Полезный совет: сообщение об ошибке завершать недвусмысленным ', stop.' это делает для пользователя очевидным, что программа остановилась именно из-за обнаруженной ошибки.
Умирать правильно
Скрипт обязательно должен возвращать честный код возврата: в случае успешного завершения — 0, в случае неудачи — что-нибудь другое. Правильный код выхода позволяет успешно использовать скрипт в makefile’ах, для svn-bisect и т.п.
Обратите внимание: у grep ’а есть специальный режим, когда он вообще ничего ничего не печатает, только сигнализирует кодом выхода: нашел или не нашел.
Кстати, Perl’овый die автоматически обеспечивает ненулевой код завершения.
Справка (-h)
По -h (желательно и по --help тоже) скрипт должен выводить справку о себе.
Проверьте, что в справке описано:
все опции и параметры, c делением на обязательные/необязательные и принятыми умолчальными значениями;
типичные и заковыристые примеры использования: пользователь сможет их скопировать и сразу же получит пример работы программы.
Иногда справку пытаются выводить в stderr . Это неправильно. Справка должна попадать в stdout , чтобы ее легко было обрабатывать grep ’ом, less ’ом и т.п.
Еще можно обращать внимание на переменную окружения $PAGER , и если она выставлена — передавать справку через пайп этой программе. Например, так поступает git help <command> .
Еще бывает, что вывод справки заканчивают ненулевым кодом выхода ( exit 2; ). Это неправильно. Если пользователь запрашивал справку, то ее вывод — успешно выполненная задача, и скрипт должен сообщать, что закончился успешно ( exit 0; ).
Разумные умолчания
Хорошие умолчательные значения критически важны для эффективной работы с программой. Выбирать умолчания следует исходя из того, что является основной задачей программы и каким образом она будет использоваться чаще всего.
Чем чаще нужна какая-либо опция, тем проще она должна включаться, а самое частое значение параметра должно предполагаться по умолчанию. Идеал: программа выполняет наиболее часто требующуюся задачу вообще без параметров (например: cal , debuild , gzip , ls , make , passwd , plackup ).
И опять хороший пример подает grep : поиск в stdin делается по умолчанию; поиск по списку файлов включается простым их перечислением; рекурсивный поиск, размер контекста и нечувствительность к регистру включаются однобуквенными опциями; экзотика типа управления буферизацией — многобуквенными опциями.
Интересно устроено у GNU grep управление цветной раскраской вывода: по умолчанию при выводе на интерактивный терминал вывод раскрашен, при выводе в файл — не раскрашен, а для ручного управления раскраской есть многобуквенная опция --color .
Автокомплит
Если у вашего скрипта много возможных параметров (особенно многобуквенных), напишите и выдайте вашим пользователям функции для автокомплита (автодополнения) в популярных шеллах. Документация: для zsh, для bash.
Кстати, обратите внимание на функцию gnu_generic в zsh : если по --help ваш скрипт рассказывает о своих параметрах в достаточно общепринятом формате, для включения автодополнения по параметрам будет достаточно сделать
compdef _gnu_generic my-script.pl
Маленькая хитрость: раскраска вывода
Если вашим скриптом будут пользоваться люди в интерактивном режиме — упростите восприятие вывода, раскрасив его в разные цвета. См. например Term::ANSIColor.
Маленькая хитрость-2: молчаливый запрос пароля
Если скрипту надо спросить у пользователя пароль или иную секретную информацию, отключите отображение вводимых символов. Например, с помощью Term::ReadKey:
Итого
Таковы, по моему опыту, простейшие способы улучшения user experience консольных программ.
Если вам тоже есть чем поделиться на эту тему — пишите в комментариях или ответными статьями, тема того заслуживает.
О когнитивном сопротивлении можно прочитать в книге Алана Купера «Психбольница в руках пациентов», глава «Поведение, не связанное с физическими силами»↩