
$ seq 10 | unbuffer -p od -vtc
0000000 1 \n 2 \n 3 \n 4 \n 5 \n 6 \n 7 \n 8 \n
Куда 9
и 10
куда делись?
$ printf '\r' | unbuffer -p od -An -w1 -vtc
\n
Почему было \r
изменено на \n
?
$ : | unbuffer -p printf '\n' | od -An -w1 -vtc
\r
\n
$ unbuffer -p printf '\n' | od -An -w1 -vtc
\r
\n
Что за фигня?
$ printf foo | unbuffer -p cat
$
Почему нет вывода (и задержка в одну секунду)?
$ printf '\1\2\3foo bar\n' | unbuffer -p od -An -w1 -vtc
$
Почему нет выходных данных?
$ (printf '\23'; seq 10000) | unbuffer -p cat
Почему он зависает и ничего не выводится?
$ unbuffer -p sleep 10
Почему я не вижу, что печатаю (и почему текст удаляется, даже если sleep
я его не читал)?
Кстати, также:
$ echo test | unbuffer -p grep foo && echo found foo
found foo
Почему grep
нашел foo
, но не распечатал строки, которые его содержат?
$ unbuffer -p ls /x 2> /dev/null
ls: cannot access '/x': No such file or directory
Почему ошибка не перешла в /dev/null
?
Смотрите такжеНе буферизовать преобразование всех символов в колокольчик?
$ echo ${(l[10000][foo])} | unbuffer -p cat | wc -c
4095
Это с:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux trixie/sid
Release: n/a
Codename: trixie
$ uname -rsm
Linux 6.5.0-3-amd64 x86_64
$ expect -c 'puts "expect [package require Expect] tcl [info patchlevel]"'
expect 5.45.4 tcl 8.6.13
$ /proc/self/exe --version
zsh 5.9 (x86_64-debian-linux-gnu)
То же самое в Ubuntu 22.04 или FreeBSD 12.4-RELEASE-p5 (за исключением того, что od
там команды нужно адаптировать, и я получаю 2321 (все символы BEL) вместо 4095, как указано выше).
решение1
unbuffer
— это инструмент для отключения буферизации, которую выполняют некоторые команды, когда их вывод не поступает на терминальное устройство.
Когда их вывод отправляется на терминальное устройство, команды предполагают, что есть реальный пользователь, активно просматривающий вывод, поэтому они отправляют его, как только он становится доступен. Ну, не совсем так, они отправляют его построчно, то есть отправляют завершенные строки, как только они готовы к выводу.
Когда он не идет на терминальное устройство, например, когда stdout является обычным файлом или каналом, в качестве оптимизации они отправляют его блоками. Это означает меньше write()
s, а в случае канала означает, что читатель на другом конце не должен быть разбужен так часто, что означает меньше переключений контекста.
Однако это означает, что в:
cmd | other-cmd
запустить в терминале, где other-cmd
есть какая-то команда фильтрации/преобразования, other-cmd
stdout для буферизуется построчно, но cmd
для буферизуется полностью, что означает, что интерактивный пользователь не видит вывод cmd
(преобразованный с помощью other-cmd
) сразу же, как только он становится доступен, а видит его с задержкой и большими пакетами.
unbuffer cmd | other-cmd
Помогает, поскольку восстанавливает буферизацию на основе строк, cmd
даже если ее стандартный вывод направляется в канал.
Чтобы сделать это, он запускается cmd
в псевдотерминале и пересылает то, что приходит с этого псевдотерминала в канал. Поэтому cmd
он думает, что снова общается с пользователем, и делает буферизацию строк.
unbuffer
на самом деле написано на expect
. Этопример скрипта в expect
исходном коде, часто включаемого в expect
пакет, поставляемый с ОС.
expect
это инструмент, используемый для выполнения автоматического взаимодействия с терминальными приложениями с использованием псевдотерминалов, так что эта unbuffer
команда тривиальна для написания в expect
. Шутка,ОШИБКИраздел unbuffer
страницы руководства содержит:Страница руководства длиннее программы.И действительно,программапросто:
#!/bin/sh
# -*- tcl -*-
# The next line is executed by /bin/sh, but not tcl \
exec tclsh8.6 "$0" ${1+"$@"}
package require Expect
# -*- tcl -*-
# Description: unbuffer stdout of a program
# Author: Don Libes, NIST
if {[string compare [lindex $argv 0] "-p"] == 0} {
# pipeline
set stty_init "-echo"
eval [list spawn -noecho] [lrange $argv 1 end]
close_on_eof -i $user_spawn_id 0
interact {
eof {
# flush remaining output from child
expect -timeout 1 -re .+
return
}
}
} else {
set stty_init "-opost"
set timeout -1
eval [list spawn -noecho] $argv
expect
exit [lindex [wait] 3]
}
Как вы можете видеть и как подтверждается страницей руководства, unbuffer
также поддерживается -p
опция.
В unbuffer cmd
псевдотерминал подключен не только к stdout cmd, но и к его stdin и stderr (помните, expect
это инструмент, предназначенный для взаимодействия с командами):
$ tty; unbuffer readlink /proc/self/fd/{0..2}
/dev/pts/14
/dev/pts/15
/dev/pts/15
/dev/pts/15
Это объясняет, почему unbuffer ls /x 2> /dev/null
ошибки не отправляются в /dev/null
stderr, он объединен со stdout.
Теперь unbuffer
ничего не читает из собственного stdin и ничего не отправляет на stdin cmd
.
Это значит, что A | unbuffer cmd | B
это не сработает.
Вот тут-то и пригодится опция -p
(для p
ipe). Как видно из кода, при этом -p
вместо используется unbuffer
активный цикл, который обрабатывает данные, поступающие из разных каналов.interact
expect
С помощью expect
одного только оператора expect
(программа/библиотека TCL) считывает то, что поступает с псевдотерминала (то есть то, что cmd
пишется на стороне подчиненного устройства через его stdout или stderr, например), и просто отправляет это в свой собственный stdout.
С interact
, expect
делает это, но также:
- отправляет то, что он считывает со своего стандартного ввода, на псевдотерминал (чтобы
cmd
можно было прочитать это там) - а также, если
unbuffer
stdin является терминальным устройством,interact
переводит его вraw
режим сecho
отключенной локальной поддержкой.
Это хорошо тем, что в A | unbuffer -p cmd | B
, A
вывод может быть прочитан как ввод , cmd
но это означает несколько вещей:
unbuffer
настраивает внутренний псевдотерминал сset stty_init "-echo"
, но не вraw
режиме. В частности,isig
(обработка^C
(\3
) /^Z
/^\
),ixon
(управление потоком,^Q
/^S
(\23
)) не отключены. Когда вход является терминальным устройством (именно такexpect
'sinteract
и предполагается использовать, но неunbuffer
), это нормально, так как хост-терминал переводится вraw
режим, так что это просто означает, что обработка перемещается с хост-терминала на встроенный псевдотерминал, за исключением того факта, чтоecho
отключено в обоих случаях, поэтому вы не можете видеть, что печатаете. Но когда это не терминальное устройство, это означает, что, например, любой байт 0x3 (^C
) во входных данных (как при обработке выходных данныхprintf '\3'
) запускает SIGINT и завершает команду, любой байт 0x19 (printf '\23'
) останавливает поток.icrnl
Неотключение объясняет, почему\r
's изменяются на\n
's.Он не делает то
stty -opost
, что он в противном случае делает без-p
. Это объясняет, почему\n
вывод 's поcmd
изменяется на\r\n
. А когда ввод является терминальным устройством, тот факт, что он помещает его вraw
, поэтому сopost
отключенным объясняет искаженный вывод терминала, когда символы новой строки, выводимые по ,od
не преобразуются в\r\n
.во внутреннем псевдотерминале по-прежнему включен редактор строк, поэтому ничего не будет отправлено, если только на входе
cmd
не будет символа\r
«или» , что объясняет, почему ничего не печатается.\n
printf foo | unbuffer -p cat
И поскольку этот редактор строк имеет ограничение на размер строки, он может редактировать (4095 в моей системе (Linux),пятая часть скорости телетайпа¹ на FreeBSD), вы в конечном итоге сталкиваетесь с такой проблемойНе буферизовать преобразование всех символов в колокольчик?: то же самое происходит, когда вы пытаетесь ввести слишком длинную строку на клавиатуре в таком глупом приложении, как
cat
. В Linux все символы после 4094 -го игнорируются, но\n
принимаются и отправляют строку; в FreeBSD после ввода 38400/5 символов любой дополнительный отклоняется (даже\n
) и вызывает отправку BEL на терминал². Что объясняет, почему вы получаете 2321 BEL (10001 - 38400/5).Обработка EOF неуклюжа с псевдотерминальными устройствами. Когда EOF виден на
unbuffer
stdin, он не может переслать эту информацию вcmd
. Поэтому вseq 10 | od -vtc
, послеseq
завершения,od
все еще ждет дополнительных входных данных от псевдотерминала, которые никогда не поступят. Вместо этого в этот момент все сносится иod
уничтожается (страница руководства упоминает это ограничение).
Для его собственных целей было бы гораздо лучше, если бы unbuffer
он перевел встроенный псевдо-tty в raw -echo
режим и оставил бы хост-терминальное устройство (если таковое имеется) в покое. Однако expect
на самом деле не поддерживает этот режим работы, он не был разработан для этого.
Теперь, если unbuffer
речь идет о дебуферизации stdout, то нет причин, по которым он должен затрагивать stdin и stderr.
На самом деле мы можем обойти это следующим образом:
unbuffer() {
command unbuffer sh -c 4<&0 5>&2 '
exec <&4 4<&- 2>&5 5>&- "$@"' sh "$@"
}
Это используется sh
для восстановления исходных stdin и stderr (передаваемых вызывающей оболочкой через fds 4 и 5; не используя fd 3, как это expect
происходит при явном использовании его внутри).
Затем:
$ echo test | unbuffer readlink /proc/self/fd/{0..2} 2> /dev/null | cat
pipe:[184479]
/dev/pts/16
/dev/null
Только стандартный вывод отправляется на псевдотерминал для расбуферизации.
И все остальные проблемы исчезают:
$ unbuffer ls /x 2> /dev/null
$ printf '\r' | unbuffer od -An -w1 -vtc
\r
$ : | unbuffer printf '\n' | od -An -w1 -vtc
\n
$ unbuffer printf '\n' | od -An -w1 -vtc
\n
$ printf foo | unbuffer cat
foo
$ printf '\1\2\3foo bar\n' | unbuffer od -An -w1 -vtc
001
002
003
f
o
o
b
a
r
\n
$ (printf '\23'; seq 10000) | unbuffer cat -vte | head
^S1$
2$
3$
4$
5$
6$
7$
8$
9$
10$
$ unbuffer sleep 10
I see what I type
$ I see what I type
zsh: command not found: I
$ echo test | unbuffer grep foo || echo not found
not found
$ echo ${(l[10000][foo])} | unbuffer cat | wc -c
10001
Кроме того, установка expect
(для которой требуется интерпретатор TCL) кажется немного излишней, когда все, что вам нужно, — это заставить stdout cmd
проходить через псевдотерминал.
socat
также можно сделать это:
$ echo test | socat -u system:'readlink /proc/self/fd/[0-2]; wc -c',pty,raw - 2> /dev/null | cat
pipe:[187759]
/dev/pts/17
/dev/null
5
(он регистрирует код завершения ошибки, но в остальном не распространяет код завершения команды).
Оболочка zsh
даже имеет встроенную поддержку псевдотерминалов, и unbuffer
функцию можно написать без особых усилий:
zmodload zsh/zpty
zmodload zsh/zselect
unbuffer() {
{
return "$(
exec 6>&1 >&5 5>&-
# here fds go:
# 0,3: orig stdin
# 1: orig stdout
# 2,4: orig stderr
# 5: closed
# 6: to return argument
zpty -b unbuffer '
stty raw
exec <&3 3<&- 2>&4 4>&-
# here fds go:
# 0: orig stdin
# 1: pseudo unbuffering tty
# 2: orig stderr
# 3,4,5: closed
# 6: to return argument
"$@" 6>&-
echo "$?" >&6
'
fd=$REPLY
until
zselect -r $fd
zpty -r unbuffer
(( $? == 2 ))
do
continue
done
)"
} 3<&0 4>&2 5>&1
}
Будьте осторожны, все они в конечном итоге запускаются в новом терминале и за исключением socat
подхода (если вы не используете параметры ctty
и setid
) в новом сеансе. Так что теперь, если эти "исправленные" unbuffer
s запускаются в фоновом режиме в сеансе хост-терминала, они cmd
не будут остановлены от чтения из хост-терминала. Например, unbuffer cat&
в конечном итоге будет фоновое задание, читающее из вашего терминала, вызывая хаос.
¹ Ограничено до 65536.скоростьдля псевдотерминала не имеет значения, но он должен быть объявлен, и я обнаружил, что по умолчанию на системе FreeBSD, на которой я это тестировал, это 38400. Поскольку скорость копируется со скорости терминала, управляющего expect
, можно сделать stty speed 115200
(максимальное значение AFAICT) перед вызовом unbuffer
для увеличения этого буфера. Но вы можете обнаружить, что все еще не получаете полную строку в 10000 символов. Этообъяснено в коде драйвера. Вы обнаружите, unbuffer -p cat
что возвращает только 4096 байт, потому что именно столько cat
было запрошено в первом read()
вызове, и драйвер tty вернул столько же из входной строкино отбросил остальное(!). Если заменить на unbuffer -p dd bs=65536
, то получится полная строка (ну, до 115200/5 байт).
² Вы можете избежать этих BEL, заменив set stty_init "-echo"
их на set stty_init "-echo -imaxbel"
в unbuffer
скрипте, но это не поможет вам получить данные.