如何将多个命令的输出发送到单个 shell 管道?

How can I send multiple commands' output to a single shell pipeline?

我有多个管道,看起来像:

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 脚本一次只能看到一行,因此无法正常工作。

便携式方法

以下可移植到 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>".json"
  flock -x 4
  tee -a "" | jq -sRf json_log_s2o.jq >&4
}
exec 3> >(writelogs "$logfilename")

及以后:

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

因为 writelogs 函数中的 jq 进程锁定了输出文件,最终的 flock -s 命令无法 also 锁定输出文件直到 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,这些可以被 wait 编辑。因此,您可以使用另一种方法来等待 FIFO 退出:

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

...然后,稍后:

wait "$log_pid"

作为先前给出的 flock 方法的替代方法。显然,仅当您有 bash 4.4 可用时才执行此操作。