Простые способы сделать консольную утилиту удобнее

07 May 2014


В 15 выпуске Perl-журнала PragmaticPerl опубликована моя статья “Простые способы сделать консольную утилиту удобнее”: ссылка. Здесь находится дополняемая и улучшаемая версия этой статьи.

Качественная командная строка – отличнейшее окружение для работы. История, автокомплит, конвейеры, перенаправления, широчайший набор готовых программ, возможность скопировать-и-вставить даже самую сложную команду – все это делает 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. Такой разнобой в именовании очень неудобен, особенно если приходится работать попеременно то с одним, то с другим вариантом программы.

Как объяснить пользователю, что он неправ

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

GetOptions(...) or die "can't parse command line arguments, stop\n";
die "You must give at least one search pattern" unless 
exists $OPT{search_pattern};

Важно, чтобы сообщение об ошибке было коротким и ясно говорило, что именно не так с параметрами. Недопустимо в ответ на неправильные параметры выводить полную справку – это никак не поможет вашему потребителю понять, что происходит.

Посмотрим, как ведут себя популярные программы:

> git up
git: 'up' is not a git command. See 'git --help'.

Did you mean one of these?
    pull
    push


> grep --li  
grep: option '--li' is ambiguous; possibilities: '--line-buffered' 
'--line-regexp' '--line-number'
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.

Здесь все коротко и по существу:

  • короткое сообщение об ошибке,
  • подсказка: где смотреть полную справку,
  • предположение: что на самом деле могло иметься в виду.

Когда приходится умирать

Если скрипт по каким-то внутренним причинам не может продолжать работу – пора вызывать die (=вывести сообщение и завершиться с ненулевым кодом).

Полезный совет: сообщение об ошибке завершать недвусмысленным ` ‘, stop.’` Это делает для пользователя очевидным, что программа остановилась именно из-за обнаруженной ошибки.

> show-releases.pl -n 10
can't connect to tracker, stop.

Умирать правильно

Скрипт обязательно должен возвращать честный код возврата: в случае успешного завершения – 0, в случае неудачи – что-нибудь другое. Правильный код выхода позволяет успешно использовать скрипт в makefile’ах, для svn-bisect и т.п.

Обратите внимание: у grep’а есть специальный режим, когда он вообще ничего ничего не печатает, только сигнализирует кодом выхода: нашел или не нашел.

Кстати, Perl’овый die автоматически обеспечивает ненулевой код завершения.

Справка (-h)

По -h (желательно и по --help тоже) скрипт должен выводить справку о себе.

Проверьте, что в справке описано:

  • назначение программы;
  • все опции и параметры, c делением на обязательные/необязательные и принятыми умолчальными значениями;
  • типичные и заковыристые примеры использования: пользователь сможет их скопировать, и сразу же получит пример работы программы.

Иногда справку пытаются выводить в stderr. Это неправильно. Справка должна попадать в stdout, чтобы ее легко было обрабатывать grep’ом, less’ом и т.п.

Еще можно обращать внимание на переменную окружения $PAGER и если она выставлена – передавать справку через пайп этой программе. Например, так поступает git help <command>.

Еще бывает, что вывод справки заканчивают ненулевым кодом выхода (exit 2;). Это неправильно. Если пользователь запрашивал справку, то ее вывод – успешно выполненная задача и скрипт должен сообщать, что закончился успешно (exit 0;).

И еще одна смешная и грустная иллюстрация того, “как не надо”: ссылка (подсказана в комментариях в PragmaticPerl).

Разумные умолчания

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

Чем чаще нужна какая-либо опция, тем проще она должна включаться, а самое частое значение параметра должно предполагаться по умолчанию. Идеал: программа выполняет наиболее часто требующуюся задачу вообще без параметров (например: 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:

use Term::ReadKey;

    ReadMode("noecho");
    chomp(my $password = <STDIN>);
    ReadMode(0);

Итого

Таковы, по моиму опыту, простейшие способы улучшения user experience консольных программ.

Если у вас тоже есть чем поделиться на эту тему – пишите в комментариях или ответными статьями, тема того заслуживает.

  1. О когнитивном сопротивлении можно прочитать в книге Алана Купера «Психбольница в руках пациентов», глава «Поведение, не связанное с физическими силами».

  2. Подход «написать однострочник и забыть» называется ad hoc ;)