Сегодня я хочу рассказать о том, как командные оболочки zsh и fish обнаруживают пропущенные символы перевода строки и выделяют соответствующие места в выводе программ, делая это в условиях, когда модель программирования Unix не даёт им возможности исследовать то, что выводят программы.
Большинство командных оболочек, включая bash, ksh, dash и ash, выводят приглашение командной строки в той позиции, в которой остался курсор после завершения работы предыдущей команды.
То, что приглашение (почти) всегда выводится в известном всем месте, в самой левой колонке следующей строки, объясняется тем фактом, что Unix-программы единодушно сотрудничают в деле размещения курсора именно в этой позиции после завершения их работы.
Делается это благодаря тому, что в конце того, что выводит программа, всегда ставится символ перевода строки n
(известный так же как «новая строка»):
vidar@vidarholen-vm2 ~ $ whoami
vidar
vidar@vidarholen-vm2 ~ $ whoami | hexdump -c
0000000 v i d a r n
Если программа не сможет выполнить это соглашение, то приглашение командной строки после этого окажется не там, где обычно:
vidar@vidarholen-vm2 ~ $ echo -n "hello world"
hello worldvidar@vidarholen-vm2 ~ $
Но недавно я заметил, что оболочки zsh и fish в подобных ситуациях выводят особые символы, указывающие на место, в котором должен был стоять знак перевода строки, и, всё равно, показывают приглашение там, где пользователи ожидают его увидеть:
vidarholen-vm2% echo -n "hello zsh"
hello zsh%
vidarholen-vm2%
vidar@vidarholen-vm2 ~> echo -n "hello fish"
hello fish⏎
vidar@vidarholen-vm2 ~>
Если сейчас вы несколько разочарованы тем, что материал, который вы читаете, посвящён такой вот мелочи, то это, скорее всего, значит, что вы никогда не пытались написать собственную командную оболочку. И то, о чём тут идёт речь, представляет собой одну из таких проблем, которые кажутся тем сложнее, чем больше о них узнаёшь.
Взято отсюда
Если вам в голову пришло какое-то простое решение этой задачи — возможно, нечто в духе if (!output.ends_with(»n»)) printf(»%n»);
— примите во внимание следующие ограничения:
- Несмотря на распространённое мнение, оболочка не располагается между программами и терминалом. У оболочки нет возможности перехватывать или исследовать то, что программы выводят в терминал.
- Модель программирования, используемая при написании программ для терминала, базируется на механизме, лежащем в основе телетайпов (их ещё называют TTY) — электромеханических печатных машинок из начала прошлого века. Они выводили данные на бумагу, печатая букву за буквой, поэтому в распоряжении того, кто работает с чем-то, устроенным так же, как телетайп, нет ни памяти, ни буфера экрана, из которого можно программно считать ранее записанные в него данные.
Учитывая это — вот несколько несовершенных способов решения вышеописанной задачи:
- Оболочка может использовать конвейеры для перехвата всего вывода программ, а после перехвата может перенаправлять вывод в терминал. Хотя в простых случаях, например, в случае с командой
whoami
, это и сработает, некоторые программы проверяют, представлен ли файлstdout
терминалом, и соответствующим образом меняют поведение, а другие идут в обход и общаются с TTY напрямую (например — приглашение ввода пароля, выводимоеssh
). Некоторые программы используют особые вызовыioctl
TTY и не будут работать в том случае, если им придётся выводить результаты не в TTY. Например — это происходит при запросе размеров окна или при подавлении вывода на экран введённых символов при вводе пароля. - Оболочка может исследовать процесс с помощью
ptrace
для того чтобы выяснить, что и куда он записывает. Это означает огромную дополнительную нагрузку на систему и нарушает работуsudo
,ping
и других команд, полагающихся на suid. - Оболочка может создать псевдо-tty (pseudo-tty, pty), выполнять в нём команды и переправлять данные между ним и настоящим терминалом, поступая подобно тому, как работают команды
ssh
илиscript
. Это — муторный и неуклюжий подход, при применении крайней формы которого потребуется переписать весь эмулятор терминала. - Командная оболочка может использовать возможности оповещения о позиции курсора, описанные в стандарте управляющих символов ECMA-48. Так, команда
printf 'e[6n'
, выполненная в терминале, поддерживающем эту возможность, приведёт к тому, что терминал будет воспроизводить пользовательский ввод в форме^[[y;xR
, гдеy
иx
— это строка и столбец. А командная оболочка может читать эти данные для того чтобы иметь представление о том, где находится курсор. Подобные вещи, хотя и выполнимы, отличаются достаточно сложной и заковыристой реализацией, если вспомнить, что речь идёт о наделении командной оболочки сравнительно простой возможностью.
В командных оболочках zsh и fish используется подход, который гораздо проще и разумнее тех, о которых мы только что говорили:
- Они всегда выводят индикатор пропущенного символа перевода строки, независимо от того, нужен он или нет.
- Затем они дополняют строку пробелами в количестве
$COLUMN-1
. - Потом идёт символ возврата каретки для перехода на первый столбец.
- И наконец — выводится приглашение.
Это — очень простое решение, так как для его реализации нужно лишь выводить строку фиксированного размера перед каждым приглашением, но оно чрезвычайно эффективно во всех терминалах. (Разработчик fish и читатель Hacker News ComputerGuru объяснили, что в деле переноса строк в разных терминалах есть множество «подводных камней», которые усложняют приведённую здесь схему работы.)
Почему?
Представим, что наш терминал имеет ширину, равную 10 колонкам и высоту в 3 строки. Обычная программа, выводящая короткую строку, выведет в конце и символ перевода строки:
[vidar ]
[| ]
[ ]
Курсор, показанный символом |
, находится в начале следующей строки. А вот что произойдёт на шаге 1 и 2:
[vidar ]
[% |]
[ ]
Мы видим индикатор, и так как мы вывели символы, количество которых точно соответствует $COLUMN
, курсор находится после последнего столбца. Теперь, на шаге 3, символ возврата каретки перемещает курсор обратно к началу строки:
[vidar ]
[|% ]
[ ]
После этого приглашение командной строки перекрывает индикатор, выводясь в той же строке:
[vidar ]
[~ $ | ]
[ ]
Итоговый результат будет таким же, как если бы мы просто вывели приглашение там, где был курсор.
А теперь давайте посмотрим на то, что произойдёт, если программа не выводит в конце выводимой ей строки символ перевода строки:
[vidar| ]
[ ]
[ ]
Снова выводится индикатор, но в этот раз пробелы, выведенные на шаге 2, приводят к тому, что символы переходят на следующую строку:
[vidar% ]
[ | ]
[ ]
Команда возврата каретки переводит курсор обратно к началу следующей строки:
[vidar% ]
[| ]
[ ]
Теперь приглашение выводится в той же строке и, в результате, не перезаписывает символ индикатора:
[vidar% ]
[~ $ | ]
[ ]
Вот, что у нас получилось. Задача, которая, на первый взгляд, кажется простой, оказалась куда сложнее, чем ожидалось, но разумное использование переноса строк позволило найти простое решение для неё.
Теперь, когда мы знаем о «секретном ингредиенте», мы, конечно, можем сделать то же самое и в bash:
PROMPT_COMMAND='printf "%%%$((COLUMNS-1))s\r"'
Надо отметить, что те же ограничения, о которых мы говорили в этой статье, применимы и к некоторым другим аспектам Unix:
- Хотя это может принести пользу, и хотя в этом часто возникает необходимость — нет надёжного способа получения вывода ранее выполненной команды.
- На удивление сложно делать скриншоты или дампы терминала, а соответствующие механизмы не универсальны и работают лишь в конкретных терминалах.
- Широко известно явление, когда вывод фоновых процессов «портит» внешний вид вывода процессов переднего плана, но эта проблема до сих пор не решена.
Случалось ли вам браться за решение сложных задач, которые, на первый взгляд, кажутся весьма простыми?
Автор: ru_vds
Спасибо за такой детальный пост. Я на своем ноутбуке пользуюсь zsh, а также использую впску на http:fastvps.ru. И вот, когда я к ней подключаюсь по ssh, можно ли сохранить среду zsh? Нужно поставить его на VPS? Спасибо.
Здравствуйте!