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

どこに行きました910

$ 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
$

なぜ出力がないのでしょうか (そして 1 秒の遅延)?

$ 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コマンドをそこで調整する必要があり、上記の 4095 ではなく 2321 (すべて BEL 文字) が返されます)。

答え1

unbuffer一部のコマンドの出力が端末デバイスに送信されない場合に、コマンドが行うバッファリングを無効にするツールです。

出力が端末デバイスに送られる場合、コマンドは実際にユーザーが出力をアクティブに見ているものと想定し、出力が利用可能になるとすぐに送信します。正確にはそうではなく、出力は行ベースで送信されます。つまり、出力の準備が整うとすぐに、完了した行が送信されます。

stdout が通常のファイルまたはパイプである場合など、端末デバイスに送信されない場合は、最適化のためにブロックで送信されます。つまり、 が少なくなりwrite()、パイプの場合は、もう一方の端にあるリーダーを頻繁に起動する必要がなくなるため、コンテキストの切り替えが少なくなります。

ただし、次の場合は次のようになります。

cmd | other-cmd

をターミナルで実行すると、 はother-cmd何らかのフィルタリング/変換コマンドであり、other-cmdの stdout は行バッファリングされますが、cmdはフルバッファリングされます。つまり、対話型ユーザーは、 の出力cmd( によって変換されたものother-cmd) をすぐには表示できず、遅延して大きなバッチで表示されます。

unbuffer cmd | other-cmd

cmdstdout がパイプに送られる場合でも、行ベースのバッファリングを復元するため役立ちます。

cmdこれを行うには、疑似端末で開始し、その疑似端末から来たものをパイプに転送します。そのためcmd、再びユーザーと通信していると判断し、行バッファリングを行います。

unbufferは実際には で書かれていますexpectexpectソースコード内のサンプルスクリプト多くの場合、expectOS が提供するパッケージに含まれています。

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、擬似端末は cmd の stdout に接続されているだけでなく、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(for pipe) オプションが登場します。コードに示されているように、 では-p、の代わりに を、さまざまなチャネルからのデータを処理するアクティブ ループとしてunbuffer使用します。interactexpect

ステートメントのみを使用するとexpectexpect(プログラム/TCL ライブラリ) は擬似端末から送られてくるもの (cmdつまり、たとえばスレーブ側の stdout または stderr 経由で書き込まれるもの) を読み取り、それを自身の stdout に送信します。

interactを使用すると、次のようにexpectなりますが、次のことも行われます。

  • 自身の標準入力から読み取ったものを擬似端末に送信する(cmdそこで読み取ることができる)
  • また、unbufferの stdin が端末デバイスである場合は、ローカルを無効にしたモードinteractにします。rawecho

これはA | unbuffer -p cmd | BAの出力を の入力として読み取ることができるという点で優れていますcmdが、次のことが意味します。

  • unbufferは、内部擬似端末を で設定しますset stty_init "-echo"が、rawモードでは設定しません。特に、( ( ) / / isigの処理)、(フロー制御、/ ( )) は無効になっていません。入力が端末デバイスの場合 ( はこのように使用することを想定していますが、 は使用しないでください)、ホスト端末がモードに設定されるため、処理がホスト端末から組み込み擬似端末に移動されるだけなので問題ありません。ただし、 は両方で無効になっているため、入力内容を確認することはできません。ただし、端末デバイスでない場合は、たとえば、入力内の 0x3 バイト ( ) は ( の出力を処理する場合など) SIGINT をトリガーしてコマンドを終了し、 0x19 バイト ( ) はフローを停止します。が無効になっていないことが、が に変更される理由です。^C\3^Z^\ixon^Q^S\23expectinteractunbufferrawecho^Cprintf '\3'printf '\23'icrnl\r\n

  • stty -opostは、 がなければ行う を行いません。これが、による の出力が に変更される-p理由です。また、入力が端末デバイスの場合、 がそれを に入れるという事実、つまり が無効な場合、 による改行文字出力が に変換されないときに端末出力が壊れるという事実が説明されます。\ncmd\r\nrawopostod\r\n

  • 内部疑似端末では行エディタがまだ有効になっているため、入力からまたは文字が来cmdない限り何も送信されません。これが、何も印刷されない理由です。\r\nprintf foo | unbuffer -p cat

    そして、その行エディタは行のサイズに制限があるので、編集することができます(私のシステムでは4095(Linux)tty速度の5分の1¹ FreeBSDの場合、次のような問題が発生します。すべての文字をベルに変換するバッファ解除?: のようなダム アプリケーションでキーボードから長すぎる行を入力しようとした場合と同じことが起こりますcat。Linux では、4094 番目以降の文字はすべて無視されますが、\nは受け入れられ、行が送信されます。FreeBSD では、38400/5 文字が入力された後は、それ以上の文字は拒否され ( であっても\n)、BEL が端末に送信されます²。そこで 2321 個の BEL (10001 - 38400/5) が返されるのはそのためです。

  • 擬似端末デバイスでは、EOF の処理が不格好です。unbufferの stdin に EOF が見つかると、その情報を に転送できませんcmd。そのため、 では、が終了したseq 10 | od -vtc後も、決して届かない擬似端末からの入力を待機し続けます。代わりに、その時点で、すべてが破棄され、強制終了されます (man ページにはその制限について記載されています)。seqodod

unbuffer独自の目的のためには、組み込みの疑似 tty をraw -echoモードにして、ホスト端末デバイス (存在する場合) をそのままにしておく方がはるかに良いでしょう。ただし、expect実際にはその動作モードをサポートしておらず、そのために設計されていません。

さて、unbufferstdout をバッファリングしないことが目的であれば、stdin と stderr に触れる理由はありません。

実際には、次のようにすることでこれを回避できます。

unbuffer() {
  command unbuffer sh -c 4<&0 5>&2 '
    exec <&4 4<&- 2>&5 5>&- "$@"' sh "$@"
}

これは、sh元の stdin と stderr (呼び出しシェルによって fd 4 と 5 を介して渡されますexpect。内部で明示的に使用する場合のように fd 3 は使用されません) を復元するために使用されます。

それから:

$ echo test | unbuffer readlink /proc/self/fd/{0..2} 2> /dev/null | cat
pipe:[184479]
/dev/pts/16
/dev/null

stdout のみが疑似端末に送信され、バッファリングされません。

そして他のすべての問題は解決します:

$ 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 インタープリターが必要) は少しやり過ぎのように思えます。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には疑似 tty のサポートも組み込まれており、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およびオプションを使用しない限り) 実行されることに注意してください。したがって、これらの「固定」がホスト ターミナル セッションのバックグラウンドで開始された場合、ホスト ターミナルからの読み取りは停止されません。たとえば、ターミナルからの読み取りがバックグラウンド ジョブで終了し、大混乱を引き起こします。setidunbuffercmdunbuffer cat&


¹ 65536まで上限。スピード擬似端末の場合は関係ありませんが、宣伝されているものがあり、私がテストしたFreeBSDシステムではデフォルトで38400であることがわかりました。速度は端末制御の速度からコピーされるため、そのバッファを拡大するために呼び出す前に(AFAICTの最大値)expectを実行できます。しかし、それでも10000文字の大きな行全体を取得できない場合があります。stty speed 115200unbufferドライバーコードで説明4096バイトしか返されないことがわかります。これは、最初の呼び出しで要求されたunbuffer -p cat量と同じで、ttyドライバが入力行から返した量と同じだからです。catread()残りは捨てた(!) を に置き換えるとunbuffer -p dd bs=65536、完全な行 (つまり、115200/5 バイトまで) が表示されます。

² スクリプト内でset stty_init "-echo"を に置き換えることでこれらの BEL を回避できますが、データの取得には役立ちません。set stty_init "-echo -imaxbel"unbuffer

関連情報