tee + cat:多次使用输出,然后合并结果


18

例如,如果我调用某个命令,则echo可以在中使用该命令的结果在其他多个命令中tee。例:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

使用cat我可以收集几个命令的结果。例:

cat <(command1) <(command2) <(command3)

我希望能够同时做这两种事情,这样我就可以tee在其他输出(例如,echo我编写的输出)上调用这些命令,然后使用以下命令将所有结果收集到一个输出中cat

保持结果为了这一点很重要,这意味着输出的线路command1command2并且command3不应该纠缠在一起,但订购的命令是(因为它与发生cat)。

可能有比cat和更好的选择,tee但是到目前为止,这些是我所知道的。

我想避免使用临时文件,因为输入和输出的大小可能很大。

我该怎么办?

PD:另一个问题是这种情况是循环发生的,这使得处理临时文件更加困难。这是我目前拥有的代码,适用于小型测试用例,但是当以我不理解的方式从auxfile读取和写入时,它会创建无限循环。

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

auxfile中的阅读和写作似乎重叠,导致所有内容爆炸。


2
我们在聊多大?您的要求将所有内容都保留在内存中。使结果保持顺序意味着在命令2和命令3甚至可以开始处理之前(除非您也希望首先将其输出收集到内存中),命令1必须首先完成(因此它大概已经读取了整个输入并打印了整个输出)。
弗罗斯特斯

没错,command2和command3的输入和输出太大,无法保存在内存中。我期待使用swap比使用临时文件更好。我遇到的另一个问题是,这是循环发生的,这使得处理文件更加困难。我使用的是单个文件,但由于某些原因,此时读取和写入文件存在一些重叠,导致文件不断增加。我将尝试更新问题,而不会给您带来太多细节。
特里克斯

4
您必须使用临时文件;输入echo HelloWorld > file; (command1<file;command2<file;command3<file)或输出echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output。这就是它的工作方式-Tee仅在所有命令并行工作和处理时才能派生输入。如果一个命令处于休眠状态(因为您不想进行交织),它将仅阻塞所有命令,以防止用输入来填充内存...
frostschutz 2013年

Answers:


27

您可以结合使用GNU stdbuf和pee来自moreutils

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

pee popen(3)是这3个shell命令行,然后fread是输入,然后是fwrite所有这三个,将被缓冲到1M。

这个想法是要有一个至少与输入一样大的缓冲区。这样,即使同时启动三个命令,它们也只会在pee pclose依次执行三个命令时看到输入输入。

对每个pclosepee将缓冲区刷新到命令并等待其终止。这保证了只要这些cmdx命令在收到任何输入之前就不开始输出任何东西(并且不要派生一个可能在其父级返回后继续输出的过程),那么这三个命令的输出就不会交错。

实际上,这有点像在内存中使用临时文件,但缺点是同时启动3个命令。

为了避免同时启动命令,可以编写pee为shell函数:

pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out

但是请注意,除zsh带有NUL字符的二进制输入外,其他外壳将失败。

这样可以避免使用临时文件,但这意味着整个输入都存储在内存中。

无论如何,您都必须将输入存储在内存或临时文件中的某个位置。

实际上,这是一个非常有趣的问题,因为它向我们展示了使几个简单的工具协作完成一项任务的Unix思想的局限性。

在这里,我们希望有几种工具可以配合完成这项任务:

  • 源命令(此处echo
  • 调度程序命令(tee
  • 一些过滤器命令(cmd1cmd2cmd3
  • 和聚合命令(cat)。

如果他们都可以同时运行,并且对要立即处理的数据进行艰苦的工作,那就太好了。

对于一个过滤器命令,很简单:

src | tee | cmd1 | cat

所有命令同时运行,并在可用时立即cmd1开始对数据进行处理src

现在,使用三个过滤器命令,我们仍然可以执行相同操作:同时启动它们并将它们与管道连接:

               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

我们可以使用命名管道相对容易地做到这一点:

pee() (
  mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
  { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
  eval "$1 < tee-cmd1 1<> cmd1-cat &"
  eval "$2 < tee-cmd2 1<> cmd2-cat &"
  eval "$3 < tee-cmd3 1<> cmd3-cat &"
  exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

(以上} 3<&0是解决从&重定向的事实,我们通常避免打开要阻塞的管道,直到另一端()也打开为止)stdin/dev/null<>cat

为了避免使用命名管道,使用zshcoproc会更加痛苦:

pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    eval "coproc $cmd $ci $co"

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

现在的问题是:一旦所有程序都启动并连接,数据是否会流动?

我们有两个矛盾:

  • tee 以相同的速率馈送所有输出,因此它只能以最慢的输出管道的速率发送数据。
  • cat 只有从第一个管道(5)读取了所有数据后,才会从第二个管道(上图中的管道6)开始读取。

这意味着直到cmd1完成,数据才不会在管道6中流动。并且,与上述情况一样tr b B,这可能意味着数据也不会在管道3中流动,这意味着数据也不会在管道2、3或4中的任何一个中流动,因为tee以所有3的最低速率进行馈送。

实际上,这些管道的大小为非空值,因此一些数据将设法通过,至少在我的系统上,我可以使其正常工作:

yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c

除此之外,

yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

我们处于这种情况下的僵局:

               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

我们已经填充了管道3和6(每个管道64kiB)。tee已读取该多余的字节,并将其馈送到cmd1,但

  • 现在它正在等待将cmd2其清空,因此已阻止在管道3上进行写入
  • cmd2无法清空它,因为它阻塞了在管道6上的写入,正在等待cat清空它
  • cat 不能清空它,因为它一直等到管道5上没有更多输入。
  • cmd1无法判断cat没有更多输入,因为它正在等待的更多输入tee
  • 并且tee无法判断cmd1没有更多输入,因为它已被阻止...依此类推。

我们有一个依赖循环,因此陷入僵局。

现在,有什么解决方案?较大的管道​​3和4(足以容纳所有src的输出)可以做到。我们能做到这一点,例如通过插入pv -qB 1G之间teecmd2/3在那里pv了可以存储到数据等待1G cmd2cmd3阅读。不过,这将意味着两件事:

  1. 这可能会占用大量内存,而且将其复制
  2. 未能使所有3个命令配合使用,因为cmd2实际上只有在cmd1完成后才开始处理数据。

第二个问题的解决方案是将管道6和7也做得更大。假设这样做cmd2cmd3产生与消耗一样多的输出,那将不会消耗更多的内存。

避免重复数据的唯一方法(在第一个问题中)是实现数据在调度程序本身中的保留,即对tee数据进行变体,以最快的输出速率馈送数据(保留数据以馈送数据)。较慢的按自己的步调)。并非微不足道。

因此,最后,我们无需编程就可以合理地获得最好的结果(Zsh语法):

max_hold=1G
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    if ((n)); then
      eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
    else
      eval "coproc $cmd $ci $co"
    fi

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

没错,死锁是我到目前为止发现避免使用临时文件的最大问题。这些文件似乎相当快,但是,我不知道它们是否被缓存在某处,我担心磁盘访问时间,但是到目前为止它们似乎还算合理。
特里克斯

6
+1 精美的ASCII艺术的额外内容:-)
Kurt Pfeifle

3

您提出的建议无法通过任何现有命令轻松完成,并且也没有任何意义。管的整个构思(|在Unix / Linux中)的是,在cmd1 | cmd2所述cmd1写入输出(至多)直到存储器缓冲器填充,然后cmd2运行从缓冲器读取数据(最多),直到它是空的。即,cmd1同时cmd2运行,它们之间“运行中”的数据量永远不需要超过限制。如果要将多个输入连接到一个输出,如果其中一个阅读器落后于其他阅读器,要么停止其他阅读器(然后并行运行的目的是什么?),要么将输出保存下来,但尚未阅读(那么,没有中间文件有什么意义呢?)。 更复杂。

在我将近30年的Unix经验中,我不记得有任何情况会真正受益于这种多输出管道。

您今天可以将多个输出合并到一个流中,而不必采用任何交错的方式(应如何将它们的输出cmd1cmd2交错?将一行反过来?轮流写入10个字节?以某种方式定义的替代“段落”?不能长时间写任何东西?这一切都很难处理)。它是由完成,例如(cmd1; cmd2; cmd3) | cmd4,程序cmd1cmd2以及cmd3正在运行一前一后,输出被作为输入cmd4


3

对于您的重叠问题,在Linux上(并且带有bashzsh但不带有ksh93),您可以按以下方式进行操作:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

请注意,在每次迭代中都使用(...)代替{...}来获取新进程,因此我们可以拥有一个指向new的新fd 3 auxfile< /dev/fd/3是访问现在已删除文件的技巧。它不能在Linux之< /dev/fd/3类的非Linux系统上运行,dup2(3, 0)因此fd 0将以只读模式打开,并且光标位于文件末尾。

为了避免使用fork嵌套某些功能,可以将其编写为:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

该外壳将负责在每次迭代时备份 fd 3。但是,您最终会很快用完文件描述符。

尽管您会发现这样做的效率更高:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

也就是说,不要嵌套重定向。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.