Bash 将 stdin 拆分为 null 并通过管道传输到管道

Bash split stdin by null and pipe to pipeline

我有一个空分隔的流,其中的部分数量未知。对于每个分隔的部分,我想将其通过管道传输到另一个管道,直到读取最后一个部分,然后终止。 实际上,每个部分都非常大 (~1GB),所以我想在不将每个部分读入内存的情况下执行此操作。

例如,假设我有以下创建的流:

for I in {3..5}; do seq $I; echo -ne '[=11=]'; 
done

我会得到一个看起来像这样的蒸汽:

1
2
3
^@1
2
3
4
^@1
2
3
4
5
^@

通过管道传输时 cat -v

我想通过 paste -sd+ | bc 传输每个部分,所以我得到的流如下所示:

6
10
15

这只是一个例子。实际上流更大,管道更复杂,所以不依赖流的解决方案是不可行的。

我试过类似的东西:

set -eo pipefail
while head -zn1 | head -c-1 | ifne -n false | paste -sd+ | bc; do :; done

但我只得到

6
10

如果我停止 bc 我会得到

1+2+3
1+2+3+4
1+2+3+4+5

基本正确。这让我相信这个问题可能与缓冲以及每个进程实际与它们之间的管道交互的方式有关。

有什么方法可以修复这些命令交换流的方式,以便我可以获得所需的输出吗?或者,是否有其他方法可以实现此目的?

原则上这与 有关,我当然可以编写一个程序,将标准输入读入缓冲区,查找空字符,并将输出通过管道传输到衍生的子进程,作为公认的答案做那个问题。考虑到 bash 中对流和空定界符的普遍支持,我希望做一些更 "native" 的事情。特别是,如果我想走这条路,我将不得不在字符串中转义管道 (paste -sd+ | bc),而不是让相同的 shell 解释它。这本身并没有什么不好的,但它有点难看,需要一些容易出错的转义。

编辑

正如在回答中指出的那样,head 不保证它缓冲了多少。除非它一次只缓冲一个字节,这是不切实际的,否则这将永远行不通。因此,似乎唯一的解决方案是将其读入内存,或 .

您的原始代码的问题是 head 不能保证它读取的内容不会超过输出的内容。因此,它可以消耗多个(NUL 分隔的)输入块,即使它只发出一个输出块。

read,相比之下,保证它不会消耗超过你要求的。

set -o pipefail
while IFS= read -r -d '' line; do
  bc <<<"${line//$'\n'/+}"
done < <(build_a_stream)

如果你想要原生逻辑,没有什么比在shell.

中编写整个逻辑更原生的了

调用外部工具——包括bccutpaste 或其他——涉及fork() 惩罚。如果您每次调用只处理少量数据,那么工具的效率会被启动它们的成本所淹没。

while read -r -d '' -a numbers; do  # read up to the next NUL into an array
  sum=0                             # initialize an accumulator
  for number in "${numbers[@]}"; do # iterate over that array
    (( sum += number ))             # ...using an arithmetic context for our math
  done
  printf '%s\n' "$sum"
done < <(build_a_stream)

对于以上所有内容,我使用以下 build_a_stream 实现进行了测试:

build_a_stream() {
  local i j IFS=$'\n'
  local -a numbers
  for ((i=3; i<=5; i++)); do
    numbers=( )
    for ((j=0; j<=i; j++)); do
      numbers+=( "$j" )
    done
    printf '%s[=12=]' "${numbers[*]}"
  done
}

正如所讨论的,唯一真正的解决方案似乎是编写一个程序来专门执行此操作。我用 Rust 写了一个叫做 xstream-util。使用 cargo install xstream-util 安装后,您可以将输入通过管道传输到

xstream -0 -- bash -c 'paste -sd+ | bc'

获得所需的输出

6
10
15

并没有避免运行bash中的程序,所以如果流水线比较复杂,还是需要转义。此外,它目前仅支持单字节分隔符。