Bash

如何計算管道中間的線數

  • November 26, 2019

我想計算管道中的行數,然後根據結果繼續管道。

我試過

x=$(printf 'faa\nbor\nbaz\n' \
 | tee /dev/stderr | wc -l) 2>&1 \
 | if [[ $x -ge 2 ]]; then
     grep a
   else
     grep b
   fi

但它根本不過濾(“a”和“b”都沒有)。這是非常出乎意料的,因為至少這些工作符合預期:

printf 'faa\nbor\nbaz\n' | if true; then grep a; else grep b; fi
printf 'faa\nbor\nbaz\n' | if false; then grep a; else grep b; fi

似乎我無法從命令替換內部重定向標準錯誤,因為這也不起作用(在 bash 中)。它列印所有三行:

x=$(printf 'faa\nbor\nbaz\n' | tee /dev/stderr | wc -l) 2>&1 | grep a

在 zsh 中它只列印兩行。

但是在兩個 shell 中,變數 x 在管道之後都沒有設置,甚至在管道的後半部分也沒有設置。

我該怎麼做才能計算管道中的行數,然後根據該數字採取行動?我想避免使用臨時文件。

這個評論是真的:

管道的每個部分都獨立於同一管道的其他部分啟動。這意味著$x如果它設置在其他階段之一,則無法在管道中間使用。

這並不意味著你不能做任何事情。管道可能被認為是主要的數據通道,但程序仍然可以使用側通道進行通信:文件、命名 fifo 或其他任何東西(儘管有時您需要格外小心,不要讓它們阻塞)。

您想計算行數並稍後有條件地處理整個數據流。這意味著您需要到達流的末尾,然後才能傳遞整個流。所以你需要以某種方式保存整個流。臨時文件看起來像是一種理智的方法。您應該將管道分成至少兩部分。第一部分應將數據保存在文件中;然後應該計算行數(我認為這個任務可能屬於第一部分);然後最後一部分應該得到數字,從頭開始讀取文件以接收數據,並採取相應的行動。


如果您真的想避免使用臨時文件,那麼您的管道的某些部分應該以某種方式表現得像sponge. 為避免旁通道,應將行數作為輸出的第一行傳遞,並且管道的其餘部分應理解此協議。

考慮這個命令:

sed '$ {=; H; g; p;}; H; d'

它在保持空間中累積線。如果至少有一行,則在收到最後一行後sed列印行數,然後是空行和實際輸入。

空行是不必要的,但從這個簡單的程式碼中“自然”地出現。我不會試圖在 中避免它sed,而是稍後在管道中處理它(例如,使用sed '2 d')。

範例用法:

#!/bin/sh

sed '$ {=; H; g; p;}; H; d' | sed '2 d' | {
  if ! IFS= read -r nlines; then
     echo "0 lines. Nothing to do." >&2
  else
     echo "$nlines lines. Processing accordingly." >&2
     if [ "$nlines" -ge 2 ]; then
        grep a
     else
        grep b
     fi
  fi
}

筆記:

  • IFS= read -r是一個矯枉過正,因為第一行定義明確,它包含一個唯一的數字(或它不存在)。
  • 我用過/bin/sh。該程式碼也將在 Bash 中執行。
  • 您不能假設sed能夠保存任意數量的數據。POSIX 規範說:

模式和保持空間都應該能夠保持至少 8192 字節。

所以它的限制可能只有 8192 字節。另一方面,我可以想像一個臨時文件很容易保存 1TB 的數據。也許不要不惜一切代價避免臨時文件。


標題說“計算行數”,但您的範例試圖確定數字是否為 2 或更多(通常為 N 或更多)。這些問題是不等價的。在輸入第二(N)行之後,您知道後一個問題的答案,甚至行將無限期地出現。上面的程式碼不能處理不確定的輸入。讓我們在某種程度上修復它。

sed '
7~1 {p; d}
6 {H; g; i \
6+
p; d}
$ {=; H; g; p}
6! {H; d}
'

此命令的行為與之前的解決方案類似,但當它到達第 6 行時,它假定(列印)行數為6+. 然後列印已經看到的行,並在它們出現後立即列印以下行(如果有的話)(類似cat行為)。

範例用法:

#!/bin/sh

threshold=6

sed "
$((threshold+1))~1 {p; d}
$threshold {H; g; i \
$threshold+
p; d}
$ {=; H; g; p}
${threshold}! {H; d}
" | sed '2 d' | {
  if ! IFS= read -r nlines; then
     echo "0 lines. Nothing to do." >&2
  else
     echo "$nlines lines. Processing accordingly." >&2
     if [ "$nlines" = "$threshold+" ]; then
        grep a
     else
        grep b
     fi
  fi
}

筆記:

  • 修復了“在某種程度上”,因為sed(無論您的情況是什麼限制)的限制仍然適用。但是現在sed最多處理幾$threshold行;如果$threshold足夠低,那麼應該沒問題。
  • 範常式式碼僅針對測試,$threshold+但協議允許您區分 0、1、2、…、門檻值減一和門檻值或更多行。

我不是很熟練sed。如果我的sed程式碼可以簡化,請在評論中給我提示。

引用自:https://unix.stackexchange.com/questions/553866