将stdout输出捕获到一个变量中,但仍在控制台显示

98
我有一个调用了几个长时间运行进程的bash脚本。我希望捕获这些调用的输出到变量中以进行处理。但是,由于这些是长时间运行的进程,我想要rsync调用的输出在控制台上实时显示,而不是事后显示。 为此,我找到了一种方法(链接),但它依赖于将文本输出到/dev/stderr。 我认为输出到/dev/stderr并不是一个好的做法。
VAR1=$(for i in {1..5}; do sleep 1; echo $i; done | tee /dev/stderr)

VAR2=$(rsync -r -t --out-format='%n%L' --delete -s /path/source1/ /path/target1 | tee /dev/stderr)

VAR3=$(rsync -r -t --out-format='%n%L' --delete -s /path/source2/ /path/target2 | tee /dev/stderr)

在上面的示例中,我多次调用rsync并希望在处理文件名时看到它们,但最终仍希望将输出存储在变量中,因为稍后我会解析它。是否有一种更'简洁'的方法来实现这一点?如果有区别,我正在使用Ubuntu 12.04和bash 4.2.24。
5个回答

94

在您的shell中复制&1(例如,复制到5),并在子shell中使用&5(这样您将会写入父shell的stdout(&1)):

exec 5>&1
FF=$(echo aaa|tee >(cat - >&5))
echo $FF

这将打印出“aaa”两次,一次是由于子shell中的echo语句,第二次则是打印变量的值。

在您的代码中:

exec 5>&1
VAR1=$(for i in {1..5}; do sleep 1; echo $i; done | tee >(cat - >&5))
# use the value of VAR1

4
描述符不应该在父Shell中关闭吗? - akhan
2
@akhan:我猜你是指 exec 5>&- 对吧? - hakre
9
FF=$(echo aaa | tee /dev/tty)怎么样? - knight42
1
这不会保留输出颜色。 - Shantanu Bhadoria
2
@user1011471,当你写tee >(inner_cmd...)时,shell会打开一个管道到内部命令,使得管道的写入端也被外部命令(tee)继承,通常在描述符63下。外部命令不知道shell做了什么有趣的事情,它得到了一个文件名参数/dev/fd/63。https://en.wikipedia.org/wiki/Process_substitution 不知道为什么你会得到这个错误,但它与内部命令写入描述符5无关。 - Beni Cherniavsky-Paskin
显示剩余2条评论

46

Op De Cirkel的答案是正确的,甚至可以进一步简化(避免使用cat):

exec 5>&1
FF=$(echo aaa|tee /dev/fd/5)
echo $FF

9
/dev/fd/5 是否与操作系统有关? - akhan
12
我一直在想,如果你只是使用tee /dev/fd/1,但由于输出仍然被$()捕获,所以这种方法行不通。因此,如果有人想知道同样的事情,确实需要使用额外的文件描述符(例如5)。 - dirtside
2
我们可以进一步简化并将其变成一个单行代码,而不需要使用 exec{ FF=$(echo aaa|tee /dev/fd/5); } 5>&1。花括号允许重定向在子shell命令运行之前发生,同时 $FF 仍然保留在当前shell的范围内(这对于普通括号 ( ) 不起作用)。这样甚至无需在此后关闭FD 5,这是一个被忽视的卫生习惯。 - tlwhitec
2
@akhan 不会的,Bash被认为是在操作系统本身不存在该路径时模拟该路径。 - tlwhitec
1
如果使用 sudo -u <其他非root用户> <脚本> 运行,则 Op De Cirkel 的答案有效,但此答案无效。写入 /dev/fd/5 等同于直接写入终端。 /dev/fd/5 是终端的 /dev/pts/ 文件的符号链接,由最初登录的用户拥有,并且不能被 sudo 用户写入。然而,cat - >&5 写入 bash 在进程内打开的文件描述符(这与写入 /dev/fd/5 不同)。该文件描述符通过每个父进程转发写入,避免了任何权限问题。 - Paul Donohue
为什么 @dirtside 的解决方案不起作用?具体来说,为什么不能只使用 tee /dev/fd/1 呢?我不明白如果你要使用 tee /dev/fd/5 ,其中 5 指向 1,那和只用 tee /dev/fd/1 有什么不同? - Nick

21

这里有一个例子,可以捕获标准错误 stderr、标准输出 stdout 和命令的退出代码。这是基于Russell Davis的答案。

exec 5>&1
FF=$(ls /taco/ 2>&1 |tee /dev/fd/5; exit ${PIPESTATUS[0]})
exit_code=$?
echo "$FF"
echo "Exit Code: $exit_code"

如果文件夹/taco/存在,这将捕获其内容。如果文件夹不存在,则会捕获错误消息并且退出代码为2。
如果省略2>&1,则只会捕获stdout

1
它捕获了stderrstdout和命令的退出代码。 - nh2
很好的澄清@nh2 - 我编辑了我的答案。 - Bryan Roach

9
如果您所说的“控制台”是指当前的TTY,请尝试:
variable=$(command with options | tee /dev/tty)

这是有点可疑的做法,因为有些人可能会尝试使用它,但当他们没有 TTY(如 cron 任务等)时,输出结果可能会出现在意料之外的位置。


1
它在Docker容器中无法工作。tee:/dev/tty:没有这样的设备或地址 - Hong
我不认为这是普遍的真实情况。在我的Debian Docker容器中,它可以正常工作。 - tripleee
在Docker中,您可以使用 tty=$(readlink /proc/$$/fd/2) 然后 NEW=$(echo "blah" | tee "$tty") - forgetso

7
您可以使用超过三个文件描述符。在这里尝试:http://tldp.org/LDP/abs/html/io-redirection.html
每个打开的文件都被分配一个文件描述符。其中,标准输入、标准输出和标准错误的文件描述符分别为0、1和2。对于打开其他文件,剩余的描述符为3至9。有时将这些额外的文件描述符之一作为临时重复链接分配给标准输入、标准输出或标准错误非常有用。
关键是是否值得让脚本变得更加复杂以实现此结果。实际上,您所做的方式并没有错。

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