如何在Bash中创建管道循环

23
假设我有程序P0P1,... P(n-1),其中 n> 0。如何轻松地将程序Pi的输出重定向到程序P(i+1 mod n)中,对于所有的i0 <= i < n)?
例如,假设我有一个程序square,它反复读取数字并打印该数字的平方,并且一个程序calc,它有时会打印一个数字,然后期望能够读取它的平方。如何连接这些程序,以便每当calc打印一个数字时,square都会将其平方并将其返回给calc
编辑:我可能应该澄清一下我所说的“容易”。命名管道/fifo解决方案确实可以工作(我曾经使用过),但是与使用bash管道相比,实际上需要相当多的工作才能正确地执行它。(您需要获取尚不存在的文件名,使用该名称创建管道,运行“管道循环”,清除命名管道。)想象一下,如果您不能再写prog1 | prog2,并且始终必须使用命名管道来连接程序。
我正在寻找的是几乎像编写“正常”管道一样容易的东西。例如,类似{ prog1 | prog2 } >&0这样的东西会很棒。
7个回答

27

昨天花了相当一段时间尝试将stdout重定向到stdin,最终我使用了以下方法。虽然不是很好,但我认为比命名管道/ FIFO 的解决方案更好。

read | { P0 | ... | P(n-1); } >/dev/fd/0

{ ... } >/dev/fd/0用于将标准输出重定向到整个管道序列的标准输入(即将P(n-1)的输出重定向到P0的输入)。使用>&0或类似方法无效;这可能是因为bash假定0是只读的,而对/dev/fd/0的写入并不会有问题。

初始的read-pipe是必要的,因为如果没有它,输入和输出文件描述符将是相同的pts设备(至少在我的系统上),重定向将没有效果。(pts设备不能作为管道使用;向其中写入的东西将显示在屏幕上。)通过使{ ... }的输入为普通管道,可以实现所需的重定向效果。

以我的calc/square示例为例:

function calc() {
  # calculate sum of squares of numbers 0,..,10

  sum=0
  for ((i=0; i<10; i++)); do
    echo $i                   # "request" the square of i

    read ii                   # read the square of i
    echo "got $ii" >&2          # debug message

    let sum=$sum+$ii
  done

  echo "sum $sum" >&2           # output result to stderr
}

function square() {
  # square numbers

  read j                         # receive first "request"
  while [ "$j" != "" ]; do
    let jj=$j*$j
    echo "square($j) = $jj" >&2  # debug message

    echo $jj                     # send square

    read j                       # receive next "request"
  done
}

read | { calc | square; } >/dev/fd/0

运行上述代码会产生以下输出:

square(0) = 0
got 0
square(1) = 1
got 1
square(2) = 4
got 4
square(3) = 9
got 9
square(4) = 16
got 16
square(5) = 25
got 25
square(6) = 36
got 36
square(7) = 49
got 49
square(8) = 64
got 64
square(9) = 81
got 81
sum 285

当然,这种方法有点像hack。特别是read部分有一个不必要的副作用:终止“真正”的管道循环并不会导致整个程序的终止。我想不出比read更好的办法,因为似乎只有尝试写入一些东西才能确定管道循环是否已经终止。


1
好的解决方案。我曾经不得不使用循环内部的netcat来完成类似的工作,并通过“echo”来“关闭”其输入来解决“读取”副作用问题。 最终它看起来像这样:echo | read | { P0 | ... | P(n-1); } >/dev/fd/0 - Thiago Padilha
2
可以使用立即终止的命令,例如 : (true),代替 echo|read。例如:: | { cmd | cmd >/dev/fd/0}。 例如:: | { nc -lp 5000 >/dev/fd/0; } 是一个简单的 echo 服务器,在客户端 EOF 时正确终止。 - regnarg

15
一个命名管道可能可以解决这个问题:
$ mkfifo outside
$ <outside calc | square >outside &
$ echo "1" >outside ## Trigger the loop to start

你能解释一下这行代码 "<outside calc | square >outside &" 吗?我不确定 <outside> 和 >outside 的含义。 - Léo Léopold Hertz 준영
它们是标准的 shell 重定向 - 从“外部”读取并输出到“外部”。外部是一个 FIFO,因此写入其中的所有内容都会从读取端口输出。 - Douglas Leeder
我尝试了这段代码,但它没有运行。似乎是这一行:<outside calc | square >outside &会立即结束。 - RnMss
@RnMss 也许 awk 如果其 stdin 不是终端,就不会等待输入?我建议您提出一个新问题,解释为什么需要使用 awk 进行输入循环以及您已经尝试了什么。 - Douglas Leeder
还有一种不需要命名管道的解决方案[脚本-Bash:创建匿名FIFO-Super User](https://superuser.com/questions/184307/bash-create-anonymous-fifo#633185) - user202729

5

这是一个非常有趣的问题。我(隐约)记得17年前大学时有一个非常相似的作业。我们需要创建一个管道数组,代码会获得每个管道的输入/输出文件句柄。然后代码会fork并关闭未使用的文件句柄。

我认为你可以在bash中使用命名管道做类似的事情。使用mknod或mkfifo创建一组带有唯一名称的管道,然后fork您的程序。


3
我的解决方案使用pipexec(大部分函数的实现来自于你的回答):

square.sh

function square() {
  # square numbers

  read j                         # receive first "request"
  while [ "$j" != "" ]; do
    let jj=$j*$j
    echo "square($j) = $jj" >&2  # debug message

    echo $jj                     # send square

    read j                       # receive next "request"
  done
}

square $@

calc.sh

function calc() {
  # calculate sum of squares of numbers 0,..,10

  sum=0
  for ((i=0; i<10; i++)); do
    echo $i                   # "request" the square of i

    read ii                   # read the square of i
    echo "got $ii" >&2          # debug message

    let sum=$sum+$ii
 done

 echo "sum $sum" >&2           # output result to stderr
}

calc $@

该命令

pipexec [ CALC /bin/bash calc.sh ] [ SQUARE /bin/bash square.sh ] \
    "{CALC:1>SQUARE:0}" "{SQUARE:1>CALC:0}"

输出(与您的答案相同)

square(0) = 0
got 0
square(1) = 1
got 1
square(2) = 4
got 4
square(3) = 9
got 9
square(4) = 16
got 16
square(5) = 25
got 25
square(6) = 36
got 36
square(7) = 49
got 49
square(8) = 64
got 64
square(9) = 81
got 81
sum 285

注释:pipexec旨在启动进程并在它们之间构建任意管道。由于bash函数不能被处理为进程,因此需要将函数放在单独的文件中并使用单独的bash。


1

命名管道。

使用mkfifo创建一系列的FIFO,例如fifo0、fifo1等。

然后将每个进程依次连接到所需的管道上:

processn < fifo(n-1) > fifon


-1

我怀疑sh/bash做不到这一点。 ZSH会是更好的选择,因为它有MULTIOS和coproc功能。


1
你能举个关于Zsh的例子吗?我对它很感兴趣。 - Léo Léopold Hertz 준영

-2
一个命令堆栈可以从任意命令的数组中组成字符串,并使用 eval 进行评估。以下示例给出结果 65536。
function square ()
{
  read n
  echo $((n*n))
}    # ----------  end of function square  ----------

declare -a  commands=( 'echo 4' 'square' 'square' 'square' )

#-------------------------------------------------------------------------------
#   build the command stack using pipes
#-------------------------------------------------------------------------------
declare     stack=${commands[0]}

for (( COUNTER=1; COUNTER<${#commands[@]}; COUNTER++ )); do
  stack="${stack} | ${commands[${COUNTER}]}"
done

#-------------------------------------------------------------------------------
#   run the command stack
#-------------------------------------------------------------------------------
eval "$stack" 

1
我认为你没有回答问题。 - reinierpost

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接