如何计算管道中间的行数

如何计算管道中间的行数

我想计算管道中的行数,然后根据结果继续管道。

我试过

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 中)。它打印所有三行:

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

在 zsh 中它只打印两行。

但在两个 shell 中,变量 x 都不会在管道之后设置,甚至在管道的后半部分期间也不会设置。

我可以做什么来计算管道中的行数,然后根据该数字采取行动?我想避免临时文件。

答案1

这条评论是真的:

管道的每个部分独立于同一管道的其他部分启动。这意味着$x如果它设置在其他阶段之一,则无法在管道中间使用。

这并不意味着您不能做任何事情。管道可以被视为主要数据通道,进程仍然可以使用侧通道进行通信:文件、名为 fifos 或其他任何东西(尽管有时您需要格外小心,不要让它们阻塞)。

您想要计算行数并稍后有条件地处理整个数据流。这意味着您需要到达流的末尾,然后才传递整个流。所以你需要以某种方式保存整个流。临时文件看起来是一个明智的方法。您应该将管道分成至少两部分。第一部分应将数据保存在文件中;然后应该计算行数(我认为这个任务可能属于第一部分);那么最后一部分应该获取数字,读取文件以从头开始接收数据,并采取相应的行动。


如果您确实想避免临时文件,那么管道的某些部分应该像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代码可以简化,请在评论中给我留言。

答案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

相关内容