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
$

出力がない理由(および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-cmdstdoutはラインバッファリングされますが、sは完全にバッファリングされます。つまり、インタラクティブユーザーは、出力が利用可能になるとすぐに(変換を介して)cmd出力を表示できないことを意味します。しかし、遅れてボリュームが高い。cmdother-cmd

unbuffer cmd | other-cmd

cmd標準出力がパイプに入ってもラインベースのバッファリングを復元するので便利です。

これを行うには、cmd疑似端末から開始し、その疑似端末の内容をパイプに渡します。したがって、cmdユーザーと再会話し、ラインバッファリングを実行すると考えてください。

unbuffer実際にはに記録されていますexpectexpectサンプルスクリプトのソースコード、通常はオペレーティング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擬似端末は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

これは、エラーがstderrunbuffer ls /x 2> /dev/nullに送信されず、stdoutがマージされる理由を説明します。/dev/null

これで、unbuffer独自の標準入力から何も読み取られず、独自の標準入力に送信されることもありませんcmd

A | unbuffer cmd | Bこれは機能しないことを意味します。

これが-ppipe用)オプションが入ってくるところです。コードに示すように、withは別のチャンネルのデータを処理するアクティブループとして代わりに使用され-pます。unbufferinteractexpect

このexpectステートメントのみを使用するとexpect(プログラム/ TCLライブラリ)、疑似端末からのもの(cmdstdoutまたはstderrを介してスレーブ側で作成されたもの)を読み、それを独自のstdoutに送信します。

さらに、次のものを使用できますinteractexpect

  • 独自の標準入力から読み取った内容を擬似端末に送信します(cmdそこで読むことができるように)。
  • また、unbufferstdinがターミナルデバイスの場合、ローカル無効モードinteractに切り替わります。rawecho

A | unbuffer -p cmd | Bの出力をA入力として読み取ることができるので大丈夫ですが、次のcmdことを意味します。

  • unbuffer内部擬似端末を設定するために使用されますが、モードではset stty_init "-echo"使用されません。raw特に、 isig(()//処理)、(フロー制御、/())は無効になりません。入力がターミナルデバイスの場合(代わりにsが使用される方法)は大丈夫です。ホストデバイスがモードになっているからです。これは、両方の端末が無効になっていることを除いて、処理がホスト端末から組み込み擬似端末に送信されることを意味します。だからあなたはあなたが入力した内容を見ることができません。ただし、ターミナルデバイスではない場合、入力(出力が処理されるとき)の0x3バイト()がSIGINTをトリガし、コマンドを終了し、0x19バイト()がプロセスを停止することを意味します。 Disabled では、s が s に変更された理由を説明します。^C\3^Z^\ixon^Q^S\23expectinteractunbufferrawecho^Cprintf '\3'printf '\23'icrnl\r\n

  • stty -opostなしではできないことをします-p。これは\n、出力がcmdに変わる理由を説明します\r\n。入力がターミナルデバイスの場合はそのデバイスを挿入するrawため、opost無効を使用すると、od出力の改行がに変換されないときにターミナル出力が破損することがわかります\r\n

  • 内部擬似端末にはまだ行エディタが有効になっているため、cmdinputにまたは文字がないと何も送信されません\r。これは何も印刷されない理由を説明します。\nprintf foo | unbuffer -p cat

    このラインエディタはラインサイズに制限があるので編集が可能です(私のシステムの4095(Linux)ttyスピードの1/5FreeBSDの場合1)、次の結果になります。アンバッファリングすると、すべての文字がリングに変換されますか?:愚かなアプリケーション(たとえばcat、.Linuxでは、4094番目以降のすべての文字は無視されますが、許可されて行が送信されます。FreeBSDでは、38400/5文字以降の追加文字が拒否され(さらに)、 BEL結果が端末²に送信されます。これが2321 BEL(10001 - 38400/5)を取得する理由を説明します。\n\n

  • 擬似端末装置に対するEOF処理が難しい。 stdinでEOFが見つかった場合は、そのunbuffer情報をcmd。代わりに、その時点ですべてが解体され終了します(マンページにはこの制限が記載されています)。seq 10 | od -vtcseqodod

unbuffer独自の目的のために、組み込みraw -echoの擬似端末をモードに設定し、ホスト端末装置(存在する場合)をそのままにしておくと便利です。ただし、expectこの動作モードは実際にはサポートされておらず、そのためには設計されていません。

今、unbufferstdoutのバッファリングを解除することについては、stdinとstderrに触れる理由はありません。

実際には、次の方法でこの問題を解決できます。

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

これは元のstdinとstderrを復元するために使用されますsh(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

バッファリング解除のために、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

(失敗した終了状態を記録しますが、コマンドの終了状態を伝播しません)。

シェルにはzshpseudo-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オプションを使用しない限り)、それらのすべては最終的に新しい端末で実行されます。setidしたがって、これらの「固定」は、unbufferホスト端末セッションのバックグラウンドで開始された場合、cmdホスト端末からのデータの読み取りを停止しません。たとえば、unbuffer cat&バックグラウンド操作は最終的に端末で読み取られ、混乱を招きます。


1上限は65536です。スピード疑似端末の場合は関係ありませんが、広告が必要であり、テストしたFreeBSDシステムでは基本的に38400であることがわかりました。速度は制御端末の速度からコピーされるため、そのバッファを拡大するためにexpect呼び出す前にstty speed 115200(MAX AFAICT)を実行できます。unbufferしかし、まだ10,000文字の全行を取得できない可能性があります。それはドライバコードに記載されている。最初の呼び出しで要求されたバイト数unbuffer -p catであるため、4096バイトのみが返され、ttyドライバは入力ラインから同じ数のバイトを返すことがわかります。catread()しかし残りは捨てた。(!)。に置き換えると、unbuffer -p dd bs=65536行全体(最大115200/5バイト)が得られます。

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

関連情報