Почему unbuffer -p искажает входные данные?

Почему unbuffer -p искажает входные данные?
$ 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-cmdstdout для буферизуется построчно, но 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/nullstderr, он объединен со stdout.

Теперь unbufferничего не читает из собственного stdin и ничего не отправляет на stdin cmd.

Это значит, что A | unbuffer cmd | Bэто не сработает.

Вот тут-то и пригодится опция -p(для pipe). Как видно из кода, при этом -pвместо используется unbufferактивный цикл, который обрабатывает данные, поступающие из разных каналов.interactexpect

С помощью expectодного только оператора expect(программа/библиотека TCL) считывает то, что поступает с псевдотерминала (то есть то, что cmdпишется на стороне подчиненного устройства через его stdout или stderr, например), и просто отправляет это в свой собственный stdout.

С interact, expectделает это, но также:

  • отправляет то, что он считывает со своего стандартного ввода, на псевдотерминал (чтобы cmdможно было прочитать это там)
  • а также, если unbufferstdin является терминальным устройством, 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's interactи предполагается использовать, но не 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«или» , что объясняет, почему ничего не печатается.\nprintf foo | unbuffer -p cat

    И поскольку этот редактор строк имеет ограничение на размер строки, он может редактировать (4095 в моей системе (Linux),пятая часть скорости телетайпа¹ на FreeBSD), вы в конечном итоге сталкиваетесь с такой проблемойНе буферизовать преобразование всех символов в колокольчик?: то же самое происходит, когда вы пытаетесь ввести слишком длинную строку на клавиатуре в таком глупом приложении, как cat. В Linux все символы после 4094 -го игнорируются, но \nпринимаются и отправляют строку; в FreeBSD после ввода 38400/5 символов любой дополнительный отклоняется (даже \n) и вызывает отправку BEL на терминал². Что объясняет, почему вы получаете 2321 BEL (10001 - 38400/5).

  • Обработка EOF неуклюжа с псевдотерминальными устройствами. Когда EOF виден на unbufferstdin, он не может переслать эту информацию в 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) в новом сеансе. Так что теперь, если эти "исправленные" unbuffers запускаются в фоновом режиме в сеансе хост-терминала, они 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скрипте, но это не поможет вам получить данные.

Связанный контент