[Перевод] «Правила», которым следуют терминальные программы

2709e23533d9bb47cabb1f59ae11bdbe.png

В последнее время я думала о том, что всё, происходящее в терминале — это та или иная комбинация

  1. Работы операционной системы

  2. Работы шелла

  3. Работы эмулятора терминала

  4. Работы той программы, которая у вас запущена (например, top, vim или cat)

Первые три (операционная система, шел и эмулятор терминала) — это достаточно известные переменные: если вы пользуетесь bash в GNOME Terminal Linux, то можете более-менее представлять, как всё это взаимодействует, а часть их поведения стандартизирована POSIX.

Но четвёртый элемент («программа, которая у вас запущена») как будто бы может делать ЧТО УГОДНО. Как узнать, что будет из себя представлять поведение программы?

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

Насколько я знаю, не существует каких-то конкретных стандартов поведения программ в терминале. Самое близкое, что я знаю — это:

  • POSIX, который по большей мере определяет, как должны взаимодействовать эмулятор терминала/ОС/шелл. Кажется, в нём определяется способ работы базовых утилит наподобие cp, но, насколько я помню, ничего не говорится о том, как, например, должен вести себя htop.

  • руководства по интерфейсу командной строки

Но даже при отсутствии стандартов, по моему опыту, программы в терминале ведут себя достаточно согласованно. Поэтому мне захотелось составить список «правил» которым они по большей мере следуют.

Это не список обязательных требований

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

Однако мне очень полезно знать, какого поведения следует ожидать от новой терминальной программы, которой я пользуюсь. Вместо подхода «вообще-то программы могут делать буквально что угодно» мне нужен «итак, вот основные правила, соблюдения которых я ожидаю, а вот короткий список исключений, который можно держать в голове».

То есть я просто зафиксирую то, что я наблюдала в поведении программ за свои двадцать лет работы с терминалом, расскажу о том, почему, по моему мнению, они ведут себя так, и приведу примеры «нарушения» этого правила.

Не всегда очевидно, за реализацию каких «правил» должна отвечать программа

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

  • файлы конфигураций должны сохраняться в ~/.BLAHrc, ~/.config/BLAH/FILE, в /etc/BLAH/ или примерно куда-то туда

  • --help должен выводить текст справки

  • программы должны передавать «обычный» вывод в stdout, а ошибки — в stderr

Но в этом посте я сделаю упор на то, что не совсем очевидно является ответственностью программы. Например, мне кажется «естественным», что при нажатии Ctrl-D должен выполняться выход из REPL, но программам часто нужно явным образом реализовывать поддержку этой функции — cat не нужно реализовывать поддержку Ctrl-D,  а ipython нужно. (подробнее об этом в «правиле 3» ниже).

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

Правило 1: неинтерактивные программы должны выполнять выход при нажатии Ctrl-C

Основная причина существования этого правила в том, что по умолчанию неинтерактивные программы завершаются при нажатии Ctrl-C , если они не настроили обработчик сигналов SIGINT, поэтому это как бы правило «по умолчанию».

Многих людей сбивает с толку то, что это не применимо к интерактивным программам наподобие python3, bc или less. Это вызвано тем, что в интерактивных программах Ctrl-C делает другую работу — если программа выполняет операцию (например, поиск в less или какой-то код на Python в python3), то Ctrl-C прервёт выполнение этой операции, но не остановит программу.

Пример того, как это работает в интерактивной программе: код в prompt-toolkit (библиотеке, которую iPython использует для обработки ввода), прерывающий поиск при нажатии Ctrl-C.

Правило 2: TUI должны выполнять выход при нажатии q

Программы с текстовым интерфейсом пользователя (TUI) (например, less или htop) обычно завершаются при нажатии q.

Это правило не применяется к программам, в которых выход при нажатии q нелогичен, например, к tmux или к текстовым редакторам.

Правило 3: REPL должны выполнять выход при нажатии Ctrl-D на пустой строке

REPL (наподобие python3 или ed) обычно выполняют выход при нажатии Ctrl-D на пустой строке. Это правило схоже с правилом для Ctrl-C — причина этого в том, что по умолчанию при работе программы (например, cat) в «cooked mode» операционная система возвращает EOF при нажатии Ctrl-D на пустой строке.

В большинстве используемых мной REPL (sqlite3, python3, fish, bash и так далее) cooked mode не используется, но во всех них реализована эта клавиатурная комбинация для имитации стандартного поведения.

Пример: код в prompt-toolkit , выполняющий выход при нажатии Ctrl-D, и тот же код в readline.

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

Кто-то сказал мне, что Erlang REPL не выполняет выход при нажатии Ctrl-D, так что, похоже, не все REPL следуют этому «правилу».

Правило 4: не использовать больше 16 цветов

Терминальные программы редко используют цвета, отличающиеся от базовых 16 цветов ANSI. Но это вызвано тем, что если вы задаёте цвета в шестнадцатеричном коде, то с большой вероятностью они вызовут конфликт с цветом фона части пользователей. Например, если я вывожу какой-то текст как #EEEEEE, то он будет почти невидим на белом фоне, однако будет прекрасно выглядеть на тёмном фоне.

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

Единственный тип программ, который, по моим наблюдениям, обычно нарушает это «правило» — текстовые редакторы. Например, Helix по умолчанию использует фиолетовый фон, не являющийся стандартным цветом ANSI. Мне кажется, вполне допустимо, что Helix нарушает это правило, потому что это не «базовая» программа; к тому же, любой пользователь Helix, которому не нравится цветовая схема, может просто поменять тему.

Правило 5: относительная поддержка комбинаций клавиш readline

Почти каждая используемая мной программа поддерживает комбинации клавиш readline, если это имеет смысл. Например, вот список разных программ и ссылки код, где в них определяется Ctrl-E для перехода к концу строки:

Ни одна из этих программ напрямую не использует readline, они просто имитируют комбинации клавиш emacs/readline. Они не всегда имитируют их точно: например, в atuin Ctrl-A, похоже, используется как префикс, поэтому Ctrl-A не выполняет переход в начало строки.

Кроме того, все эти программы, похоже, реализуют собственные буферы копирования и вставки, поэтому можно удалить строку с помощью Ctrl-U , а затем вставить её с помощью Ctrl-Y.

Исключения:

  • в некоторых программах (например, в git,  cat и nc) вообще нет никакой поддержки редактирования строк (за исключением backspace,  Ctrl-W и Ctrl-U)

  • как обычно, исключениями оказываются текстовые редакторы: у каждого текстового редактора свой подход к редактированию текста

Я подробнее рассмотрела вопрос «какие комбинации клавиш поддерживает программа?» в статье Entering text in the terminal is complicated.

Правило 5.1: Ctrl-W должно удалять последнее слово

Я ни разу не встречала программу (если не считать текстовые редакторы), в которой Ctrl-W не удаляло бы последнее слово. Это похоже на правило Ctrl-C — по умолчанию, если программа находится в cooked mode, при нажатии Ctrl-W операционная система удаляет последнее слово, а при нажатии Ctrl-U она удаляет строку целиком. То есть обычно программы имитируют это поведение.

Не могу вспомнить ни одного исключения из этого правила, кроме текстовых редакторов, но если они есть, то напишите мне об этом!

Правило 6: отключение цветов при записи в конвейер

Большинство программ отключает цвета при записи в конвейер (pipe). Например:

  • rg blah подсвечивает все вхождения blah в выводе, но если вывод выполняется в конвейер или в файл, то она отключает подсвечивание.

  • ls --color=auto использует цвет при записи в терминал, но не при записи в конвейер

Кроме того, эти программы по-разному форматируют свой вывод при записи в терминал:  ls упорядочивает файлы в столбцы, а ripgrep группирует вхождения с путями.

Если вы хотите заставить программу использовать цвет (например, потому, что вам нужно посмотреть на цвет), то можно использовать unbuffer , чтобы принудительно сделать вывод программы выводом в tty:

unbuffer rg blah |  less -R

Я уверена, что существуют программы, нарушающие это «правило», но не могу вспомнить ни одной. У некоторых программ есть флаг --color, который принудительно включает цвет: в примере выше можно было использовать и rg --color=always | less -R.

Правило 7: — означает stdin/stdout

Обычно при передаче программе - вместо имени файла, она выполнит чтение из stdin или запись в stdout (в зависимости от контекста). Например, если вы хотите отформатировать находящийся в буфере обмена код на Python при помощи black, а затем скопировать его, то можно выполнить такую команду:

pbpaste | black - | pbcopy

(pbpaste — это программа для Mac, в Linux что-то подобное можно сделать при помощи xclip)

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

Этим «правилам» мне пришлось долго учиться

На усвоение этих правил мне потребовалось много времени, потому что я должна была:

  1. Понять, что это правило вообще где-то применяется (»Ctrl-C выполняет выход из программ»)

  2. Заметить некоторые исключения («Так, Ctrl-C выполняет выход из find, но не из less»)

  3. Подсознательно вывести паттерн («Обычно Ctrl-C выполняет выход из неинтерактивных программ, но в интерактивных программах он может прерывать текущую операцию, а не выполнять выход из программы»)

  4. Постепенно сформулировать это в виде явного правила, которое мне известно

Откровенно говоря, во многом моё понимание терминала находится на этапе «подсознательного распознавания паттернов». Я трачу время на явное формулирование правил только для того, чтобы объяснить другим, как они работают. Надеюсь, эти «правила» немного облегчат изучение программ кому-то из читателей.

© Habrahabr.ru