【问题标题】:How can I send multiple commands' output to a single shell pipeline?如何将多个命令的输出发送到单个 shell 管道?
【发布时间】:2017-05-17 09:40:17
【问题描述】:

我有多个管道,如下所示:

tee -a $logfilename.txt | jq string2object.jq >> $logfilename.json

tee -a $logfilename.txt | jq array2object.jq >> $logfilename.json

对于每个管道,我想应用于多个命令。

每组命令看起来像:

echo "start filelist:"
printf '%s\n' "$PWD"/*

echo "start wget:"
wget -nv http://web.site.com/downloads/2017/file_1.zip 2>&1
wget -nv http://web.site.com/downloads/2017/file_2.zip 2>&1

这些命令的输出都应该通过管道。


我过去尝试过将管道分别放在每个命令上:

echo "start filelist:" | tee -a $logfilename | jq -sRf array2object.jq >>$logfilename.json
printf '%s\n' "$PWD"/* | tee -a $logfilename | jq -sRf array2object.jq >>$logfilename.json

但在这种情况下,JSON 脚本一次只能看到一行,因此无法正常工作。

【问题讨论】:

  • 顺便说一句,我真的建议在您的 JQ 脚本上添加一个 shebang 并将其标记为可执行。通过json_log_s2ojq -s -R -f json_log_s2o.jq 更容易通过管道传输——这样所有关于应该如何执行 jq 脚本的信息都嵌入到脚本本身中。 (如果有一天你将它从 jq 重写为另一种语言,你就不需要改变它的调用者)
  • 这很有意义,减少参数和代码模块化,将按建议实现。
  • ATM,我正在用不同的 jq 程序测试不同的管道来处理不同的命令输入
  • 顺便说一句,您现在显示了两次相同的管道(并且将反引号显示为带引号的文字,使它们看起来像是周围代码的一部分)。我认为这实际上不太有用/比以前的公式更容易误导你正在尝试做的事情。
  • 啊,也许这需要更明显,例如string2object.jqarray2object.jq。目的是使用不同的 jq 程序运行不同的管道,以处理相似或不同 json 输出的不同命令输入。也许你是对的,这会混淆目标,不确定。

标签: bash shell unix exec jq


【解决方案1】:

便携式方法

以下内容可移植到 POSIX sh:

#!/bin/sh
die() { rm -rf -- "$tempdir"; [ "$#" -gt 0 ] && echo "$*" >&2; exit 1; }
logfilename="whatever"

tempdir=$(mktemp -d "${TMPDIR:-/tmp}"/fifodir.XXXXXX) || exit
mkfifo "$tempdir/fifo" || die "mkfifo failed"

tee -a "$logfilename" <"$tempdir/fifo" \
  | jq -sRf json_log_s2o.jq \
  >>"$logfilename.json" & fifo_pid=$!
exec 3>"$tempdir/fifo" || die "could not open fifo for write"

echo "start filelist:" >&3
printf '%s\n' "$PWD"/* >&3

echo "start wget:" >&3
wget -nv http://web.site.com/downloads/2017/file_1.zip >&3 2>&1
wget -nv http://web.site.com/downloads/2017/file_2.zip >&3 2>&1

exec 3>&-         # close the write end of the FIFO
wait "$fifo_pid"  # and wait for the process to exit
rm -rf "$tempdir" # delete the temporary directory with the FIFO

避免 FIFO 管理(使用 Bash)

使用 bash,可以避免使用进程替换来管理 FIFO:

#!/bin/bash
logfilename="whatever"

exec 3> >(tee -a "$logfilename" | jq -sRf json_log_s2o.jq >>"$logfilename.json")

echo "start filelist:" >&3
printf '%s\n' "$PWD/*" >&3

echo "start wget:" >&3
wget -nv http://web.site.com/downloads/2017/file_1.zip >&3 2>&1
wget -nv http://web.site.com/downloads/2017/file_2.zip >&3 2>&1

exec 3>&1

等待退出(使用 Linux-y 工具)

然而,这个不允许让你做的事情(没有 bash 4.4)是检测jq 何时失败,或者在脚本退出之前等待jq 完成写入。如果您想确保jq 在脚本退出之前完成,那么您可以考虑使用flock,如下所示:

writelogs() {
  exec 4>"${1}.json"
  flock -x 4
  tee -a "$1" | jq -sRf json_log_s2o.jq >&4
}
exec 3> >(writelogs "$logfilename")

及以后:

exec 3>&-
flock -s "$logfilename.json" -c :

因为writelogs 函数中的jq 进程在输出文件上持有锁,所以最终的flock -s 命令无法在输出文件上获得锁,直到jq 退出。


旁白:避免所有 >&3 重定向

在任一 shell 中,以下内容同样有效:

{
  echo "start filelist:"
  printf '%s\n' "$PWD"/*

  echo "start wget:"
  wget -nv http://web.site.com/downloads/2017/file_1.zip 2>&1
  wget -nv http://web.site.com/downloads/2017/file_2.zip 2>&1
} >&3

可能,但不建议将代码块通过管道传输到管道中,从而完全取代 FIFO 的使用或进程替换:

{
  echo "start filelist:"
  printf '%s\n' "$PWD"/*

  echo "start wget:"
  wget -nv http://web.site.com/downloads/2017/file_1.zip 2>&1
  wget -nv http://web.site.com/downloads/2017/file_2.zip 2>&1
} | tee -a "$logfilename" | jq -sRf json_log_s2o.jq >>"${logfilename}.json"

...为什么不建议这样做?因为 POSIX sh 无法保证管道的哪些组件(如果有的话)与脚本的其余部分在同一个 shell 解释器中运行;如果上面的不是在同一段脚本中运行,那么变量将被丢弃(并且没有诸如pipefail之类的扩展名,退出状态也是如此)。请参阅BashFAQ #24 了解更多信息。


等待退出 Bash 4.4

在 bash 4.4 中,进程替换将它们的 PID 导出到 $!,这些可以是 waited。因此,您可以获得另一种等待 FIFO 退出的方法:

exec 3> >(tee -a "$logfilename" | jq -sRf json_log_s2o.jq >>"$logfilename.json"); log_pid=$!

...然后,稍后:

wait "$log_pid"

作为前面给出的flock 方法的替代方案。显然,只有在您有 bash 4.4 可用时才这样做。

【讨论】:

  • 在“避免 FIFO 管理(使用 Bash)”中看起来像 exec 3&gt;&amp;1 应该在 exec 3&gt; &gt; 之前,否则文件描述符无效?并且2&gt;&amp;1 应该在命令之后而不是&gt;&amp;3 否则输出到控制台而不是管道?
  • 仍在进行不同的实现和测试,在流程替换方面还有更多工作要做,以确定事情的发展方向
  • @Gabe, ...然而,在&gt;&amp;3 2&gt;&amp;1 中,第二个&amp;1 指的是与&amp;3 相同的文件描述符,因为重定向是从左到右运行的。因此,我们首先使 FD 1 成为 FD 3 的副本,然后使 FD 2 成为 FD 1 的副本——这意味着在按此顺序运行这些操作之后,1、2 和 3 都指向同一个位置。
  • @Gabe,至于exec 3&gt; &gt;(...),如果 FD 3 不存在,它会从无到有创建。不需要之前的exec 3&gt;&amp;1。从字面上看,它正在调用 mkfifo() 创建一个 FIFO 对,然后使用 dup2()dup3() 将该对的输入端重命名为 FD 3。如果存在 did FD 3 在调用dup2() 之前,那个旧的将被关闭。
  • *.com/users/14122/charles-duffy ...与writelogs() 斗争,我可以使用exec 3&gt; &gt;(writelogs "$logfilename") 调用该函数,但是,它会创建没有标准输出的.txt 日志和带有a 的.json 日志来自 jq 的空值。似乎函数没有接收或命令没有发送到函数。命令应该放在哪里以便函数处理输出?在exec 3&gt; &gt;(writelogs "$logfilename") 之后?