パイプの途中のラインの数を数える方法

パイプの途中のラインの数を数える方法

パイプ内の行数をカウントし、結果に応じてパイプを続行したいと思います。

私は試した

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

コマンド置換の内部から stderr をリダイレクトすることはできないようです。これも (bash では) 機能しないからです。3 行すべてが出力されます:

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

zsh では 2 行しか出力されません。

しかし、どちらのシェルでも、変数 x はパイプラインの後には設定されず、パイプラインの後半でも設定されません。

パイプライン内の行数をカウントし、その数に応じて動作させるにはどうすればよいですか? 一時ファイルは避けたいと思います。

答え1

このコメント本当です:

パイプラインの各部分は、同じパイプラインの他の部分とは独立して開始されます。つまり、$x他のステージのいずれかで設定されている場合、パイプラインの途中で使用することはできません。

これは、何もできないという意味ではありません。パイプラインはメインのデータ チャネルとみなすことができますが、プロセスはファイル、名前付き FIFO などのサイド チャネルを使用して通信できます (ただし、ブロックされないように細心の注意を払う必要がある場合もあります)。

行数をカウントし、後でデータ ストリーム全体を条件付きで処理したいとします。つまり、ストリームの最後に到達してから、ストリーム全体を渡す必要があります。そのため、何らかの方法でストリーム全体を保存する必要があります。一時ファイルは妥当なアプローチのように見えます。パイプを少なくとも 2 つの部分に分割する必要があります。最初の部分では、データをファイルに保存し、次に行数をカウントします (このタスクは最初の部分に属すると思います)。最後の部分では、数を取得し、ファイルを読み取り、先頭からデータを受け取り、それに応じて動作します。


本当に一時ファイルを回避したい場合は、パイプラインの一部が のように動作する必要がありますsponge。サイド チャネルを回避するには、行数を出力の最初の行として渡し、パイプラインの残りの部分がこのプロトコルを理解する必要があります。

次のコマンドを考えてみましょう:

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

ホールド スペースに行を蓄積します。少なくとも 1 行ある場合は、最後の行を受信した後、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最初の行は明確に定義されており、数字が 1 つだけ含まれている (または存在しない) ため、これはやり過ぎです。
  • を使用しました/bin/sh。コードは Bash でも実行されます。
  • sed任意の量のデータを保持できるとは想定できません。POSIX仕様言う:

    パターンスペースとホールドスペースはそれぞれ少なくとも 8192 バイトを保持できる必要があります。

    したがって、制限は 8192 バイトだけである可能性があります。一方、一時ファイルに 1 TB のデータを簡単に保持できることは想像できます。一時ファイルを避けることは絶対に避けるべきでしょう。


タイトルには「行数を数える」とありますが、例では行数が 2 以上 (一般的には N 以上) かどうかを判断しようとしています。これらの問題は同等ではありません。後者の問題の答えは、入力の 2 行目 (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、…、しきい値マイナス 1、しきい値以上の行を区別できます。

私はあまり熟練していませんsedsedコードを簡素化できる場合は、コメントにヒントを残してください。

答え2

議論と Kamil の sed コードに基づいて、次の awk ソリューションを見つけました。

awk -v th="$threshold" '
  function print_lines() { for (i in lines) print lines[i] }
  NR < th { lines[NR] = $0 }
  NR > th { print }
  NR == th { print th; print_lines(); print }
  END { if (NR < th) { print NR; print_lines(); } }' \
| if read nlines; then
    if [ "$nlines" -eq "$threshold" ]; then
      grep a
    else
      grep b
    fi
  fi

関連情報