為什麼 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-cmd的 stdout 是行緩衝的,但cmds 是全緩衝的,這意味著交互式用戶不會盡快看到 的輸出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,偽終端不僅連接到 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不從自己的標準輸入讀取任何內容,也不向 的標準輸入發送任何內容cmd

這意味著A | unbuffer cmd | B行不通。

這就是( -pfor pipe) 選項-punbuffer用武之地。interactexpect

僅使用該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的處理)、(流量控制,/ ( ))不會被停用。當輸入是終端設備時(這就是s 的使用方式,而不是),這很好,因為主機設備被置於模式下,這意味著處理從主機終端轉移到嵌入式偽終端終端,除了這兩個終端機都已停用,所以你看不到你輸入的內容。但是,當它不是終端設備時,這意味著輸入中的任何 0x3 位元組 ( )(當處理 的輸出時)都會觸發 SIGINT 並終止命令,任何 0x19 位元組 ( ) 都會停止流程。未被禁用解釋了為什麼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

  • 內部偽終端仍然啟用行編輯器,因此cmd除非有來自輸入的\r或字符,否則不會發送任何內容,這解釋了為什麼不列印任何內容。\nprintf foo | unbuffer -p cat

    由於該行編輯器對行的大小有限制,因此可以編輯(我的系統 (Linux) 上是 4095,tty 速度的五分之一1 在 FreeBSD 上),你最終會遇到這樣的問題:取消緩衝將所有字元轉換為響鈴?:當您嘗試在啞應用程式(例如 )中在鍵盤上輸入過長的行時,會發生同樣的情況cat。在 Linux 上,4094 個之後的所有字元都將被忽略,但\n會被接受並提交該行;在 FreeBSD 上,輸入 38400/5 個字元後,任何多餘的字元都會被拒絕(甚至\n),並導致 BEL 被送到終端²。這解釋了為什麼你在那裡得到 2321 BEL (10001 - 38400/5)。

  • 偽終端設備的 EOF 處理很笨重。當 的 stdin上看到 EOF 時unbuffer,它無法將該訊息轉發到cmd.因此seq 10 | od -vtc,在seq終止後,od仍在等待來自偽終端的更多輸入,而這些輸入永遠不會到來。相反,到那時,一切都被拆除並被od殺死(手冊頁確實提到了這個限制)。

unbuffer就其自身目的而言,如果將嵌入式偽終端置於raw -echo模式下並單獨保留主機終端設備(如果有),效果會更好。然而expect並不真正支援這種操作模式,它不是為此設計的。

現在,如果unbuffer是關於取消緩衝標準輸出,那麼它沒有理由接觸標準輸入和標準錯誤。

我們實際上可以透過以下方式解決這個問題:

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

它用於sh恢復原始的 stdin 和 stderr(由呼叫 shell 透過 fds 4 和 5 傳遞;不使用 fd 3,就像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

(它記錄失敗退出狀態,但不會傳播命令的退出狀態)。

shellzsh甚至內建了對偽 ttys 的支持,並且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因此,現在如果這些「fixed」unbuffer在主機終端會話的背景啟動,則cmd不會停止從主機終​​端讀取。例如,unbuffer cat&最終會從您的終端機讀取後台作業,造成嚴重損壞。


1 上限為 65536。速度對於偽終端來說是無關緊要的,但必須有一個廣告,我發現在我測試的 FreeBSD 系統上預設情況下它是 38400。由於速度是從控制終端的速度複製的expect,因此可以在呼叫之前執行stty speed 115200(最大值AFAICT)unbuffer以擴展該緩衝區。但您可能會發現您仍然沒有獲得完整的 10000 字元大行。那是驅動程式碼中解釋了。您會發現unbuffer -p cat僅傳回 4096 位元組,因為這是cat第一次呼叫時所要求的位元組數read(),而 tty 驅動程式從輸入行傳回了相同的位元組數但丟棄了其餘的(!)。如果替換為unbuffer -p dd bs=65536,您將獲得完整的行(最多 115200/5 位元組)。

² 您可以透過在腳本中替換set stty_init "-echo"為來避免這些 BEL ,但這不會幫助您取得資料。set stty_init "-echo -imaxbel"unbuffer

相關內容