Bash: 捕获在后台运行的命令的输出

我正在尝试编写一个 bash 脚本,它将获得在后台运行的命令的输出。不幸的是,我不能让它工作,我赋予输出的变量是空的-如果我用 echo 命令替换赋值,那么一切都会按预期工作。

#!/bin/bash


function test {
echo "$1"
}


echo $(test "echo") &
wait


a=$(test "assignment") &
wait


echo $a


echo done

这段代码生成输出:

echo


done

将任务更改为

a=`echo $(test "assignment") &`

有用,但似乎应该有更好的方法。

63017 次浏览

捕获 back command 输出的一种方法是重定向它在文件中的输出,并在后台进程结束后捕获文件中的输出:

test "assignment" > /tmp/_out &
wait
a=$(</tmp/_out)

Bash 确实有一个称为 程序替代的特性来实现这一点。

$ echo <(yes)
/dev/fd/63

在这里,表达式 <(yes)被替换为一个(伪设备)文件的路径名,该文件连接到异步作业 yes的标准输出(yes以无限循环的方式打印字符串 y)。

现在让我们试着从中读出:

$ cat /dev/fd/63
cat: /dev/fd/63: No such file or directory

这里的问题在于,yes进程同时终止,因为它接收到了 SIGPIPE (它在 stdout 上没有读取器)。

解决方案是下面的构造

$ exec 3< <(yes)  # Save stdout of the 'yes' job as (input) fd 3.

这将在启动后台作业之前以输入 fd3的形式打开该文件。

现在,您可以随时阅读后台作业

$ for i in 1 2 3; do read <&3 line; echo "$line"; done
y
y
y

请注意,这与将后台作业写入到驱动器备份文件的语义略有不同: 当缓冲区满时,后台作业将被阻塞(通过从 fd 读取数据来清空缓冲区)。相比之下,只有当硬盘没有响应时,写入到驱动器备份的文件才会被阻塞。

进程替换不是 POSIX sh 特性。

这里有一个快速的技巧,可以在不指定文件名的情况下(几乎)支持异步作业驱动器:

$ yes > backingfile &  # Start job in background writing to a new file. Do also look at `mktemp(3)` and the `sh` option `set -o noclobber`
$ exec 3< backingfile  # open the file for reading in the current shell, as fd 3
$ rm backingfile       # remove the file. It will disappear from the filesystem, but there is still a reader and a writer attached to it which both can use it.


$ for i in 1 2 3; do read <&3 line; echo "$line"; done
y
y
y

Linux 最近还添加了 O _ TEMPFILE 选项,这使得这样的修改在文件完全不可见的情况下成为可能。我不知道 Bash 是否已经支持了。

更新 :

@ rthur,如果你想从 fd3捕获整个输出,那么使用

output=$(cat <&3)

但是请注意,您一般不能捕获二进制数据: 如果输出是 POSIX 意义上的文本,那么这只是一个定义好的操作。我所知道的实现只是过滤掉所有 NUL 字节。此外,POSIX 指定必须删除所有拖尾换行。

(请注意,如果编写器从不停止(yes从不停止) ,捕获输出将导致 OOM。但是,如果从未另外编写行分隔符,那么即使对于 read来说,这个问题也是自然而然的)

在 Bash 中处理协同进程的一种非常健壮的方法是使用... ... 内置的 coproc

假设您希望在后台运行一个名为 banana的脚本或函数,在执行一些 stuff操作时捕获它的所有输出,并等待它完成。我用这个来做模拟:

banana() {
for i in {1..4}; do
echo "gorilla eats banana $i"
sleep 1
done
echo "gorilla says thank you for the delicious bananas"
}


stuff() {
echo "I'm doing this stuff"
sleep 1
echo "I'm doing that stuff"
sleep 1
echo "I'm done doing my stuff."
}

然后,您将运行 bananacoproc如下:

coproc bananafd { banana; }

这与运行 banana &类似,但有以下附加内容: 它创建了数组 bananafd中的两个文件描述符(输出位于索引 0,输入位于索引 1)。您将捕获具有 read内置功能的 banana的输出:

IFS= read -r -d '' -u "${bananafd[0]}" banana_output

试试看:

#!/bin/bash


banana() {
for i in {1..4}; do
echo "gorilla eats banana $i"
sleep 1
done
echo "gorilla says thank you for the delicious bananas"
}


stuff() {
echo "I'm doing this stuff"
sleep 1
echo "I'm doing that stuff"
sleep 1
echo "I'm done doing my stuff."
}


coproc bananafd { banana; }


stuff


IFS= read -r -d '' -u "${bananafd[0]}" banana_output


echo "$banana_output"

警告: 你必须在 banana结束之前完成 stuff! 如果大猩猩比你快:

#!/bin/bash


banana() {
for i in {1..4}; do
echo "gorilla eats banana $i"
done
echo "gorilla says thank you for the delicious bananas"
}


stuff() {
echo "I'm doing this stuff"
sleep 1
echo "I'm doing that stuff"
sleep 1
echo "I'm done doing my stuff."
}


coproc bananafd { banana; }


stuff


IFS= read -r -d '' -u "${bananafd[0]}" banana_output


echo "$banana_output"

在这种情况下,您将获得如下错误:

./banana: line 22: read: : invalid file descriptor specification

您可以检查是否为时已晚(例如,是否花费了太长时间来处理 stuff) ,因为在 coproc完成之后,bash 删除了数组 bananafd中的值,这就是我们获得上一个错误的原因。

#!/bin/bash


banana() {
for i in {1..4}; do
echo "gorilla eats banana $i"
done
echo "gorilla says thank you for the delicious bananas"
}


stuff() {
echo "I'm doing this stuff"
sleep 1
echo "I'm doing that stuff"
sleep 1
echo "I'm done doing my stuff."
}


coproc bananafd { banana; }


stuff


if [[ -n ${bananafd[@]} ]]; then
IFS= read -r -d '' -u "${bananafd[0]}" banana_output
echo "$banana_output"
else
echo "oh no, I took too long doing my stuff..."
fi

最后,如果你真的不想错过任何大猩猩的动作,即使你的 stuff花了太长的时间,你可以复制 banana的文件描述符到另一个 fd,例如 3,做你的东西,然后从 3读取:

#!/bin/bash


banana() {
for i in {1..4}; do
echo "gorilla eats banana $i"
sleep 1
done
echo "gorilla says thank you for the delicious bananas"
}


stuff() {
echo "I'm doing this stuff"
sleep 1
echo "I'm doing that stuff"
sleep 1
echo "I'm done doing my stuff."
}


coproc bananafd { banana; }


# Copy file descriptor banana[0] to 3
exec 3>&${bananafd[0]}


stuff


IFS= read -d '' -u 3 output
echo "$output"

这将工作非常好!最后的 read也将发挥 wait的作用,使 output将包含 banana的完整输出。

这是伟大的: 没有临时文件处理(bash 处理一切静默)和100% 纯 bash!

希望这个能帮上忙!

我也使用文件重定向,比如:

exec 3< <({ sleep 2; echo 12; })  # Launch as a job stdout -> fd3
cat <&3  # Lock read fd3

更真实的案子 如果我想要4个并行工人的输出: toto,titi,tata 和 tutu。 我将每个文件重定向到一个不同的文件描述符(在 fd变量中)。 然后读取这些文件描述符将被阻塞,直到 EOF < = 管道破碎 < = 命令完成

#!/usr/bin/env bash


# Declare data to be forked
a_value=(toto titi tata tutu)
msg=""


# Spawn child sub-processes
for i in {0..3}; do
((fd=50+i))
echo -e "1/ Launching command: $cmd with file descriptor: $fd!"
eval "exec $fd< <({ sleep $((i)); echo ${a_value[$i]}; })"
a_pid+=($!)  # Store pid
done


# Join child: wait them all and collect std-output
for i in {0..3}; do
((fd=50+i));
echo -e "2/ Getting result of: $cmd with file descriptor: $fd!"
msg+="$(cat <&$fd)\n"
((i_fd--))
done


# Print result
echo -e "===========================\nResult:"
echo -e "$msg"

应输出:

1/ Launching command:  with file descriptor: 50!
1/ Launching command:  with file descriptor: 51!
1/ Launching command:  with file descriptor: 52!
1/ Launching command:  with file descriptor: 53!
2/ Getting result of:  with file descriptor: 50!
2/ Getting result of:  with file descriptor: 51!
2/ Getting result of:  with file descriptor: 52!
2/ Getting result of:  with file descriptor: 53!
===========================
Result:
toto
titi
tata
tutu

注1 : coproc 只支持一个协进程,而不支持多个协进程

注2 : wait 命令在旧 bash 版本(4.2)中存在 bug,无法检索我启动的作业的状态。它在 bash5中工作得很好,但是文件重定向适用于所有版本。

当您在后台运行这些命令并同时等待这两个命令时,只需对它们进行分组。

{ echo a & echo b & wait; } | nl

产出将包括:

     1  a
2  b

但是请注意,如果第二个任务比第一个任务运行得更快,则输出可能无序。

{ { sleep 1; echo a; } & echo b & wait; } | nl

反向输出:

     1  b
2  a

如果有必要将两个后台作业的输出分开,则有必要将输出缓冲到某个位置,通常是在一个文件中。例如:

#! /bin/bash


t0=$(date +%s)                               # Get start time


trap 'rm -f "$ta" "$tb"' EXIT                # Remove temp files on exit.


ta=$(mktemp)                                 # Create temp file for job a.
tb=$(mktemp)                                 # Create temp file for job b.


{ exec >$ta; echo a1; sleep 2; echo a2; } &  # Run job a.
{ exec >$tb; echo b1; sleep 3; echo b2; } &  # Run job b.


wait                                         # Wait for the jobs to finish.


cat "$ta"                                    # Print output of job a.
cat "$tb"                                    # Print output of job b.


t1=$(date +%s)                               # Get end time


echo "t1 - t0: $((t1-t0))"                   # Display execution time.

该脚本的整个运行时为3秒,但是两个后台作业的合并睡眠时间为5秒。后台作业的输出是有序的。

a1
a2
b1
b2
t1 - t0: 3

还可以使用内存缓冲区存储作业的输出。但是,只有当缓冲区足够大,能够存储作业的全部输出时,这种方法才能起作用。

#! /bin/bash


t0=$(date +%s)


trap 'rm -f /tmp/{a,b}' EXIT
mkfifo /tmp/{a,b}


buffer() { dd of="$1" status=none iflag=fullblock bs=1K; }


pids=()
{ echo a1; sleep 2; echo a2; } > >(buffer /tmp/a) &
pids+=($!)
{ echo b1; sleep 3; echo b2; } > >(buffer /tmp/b) &
pids+=($!)


# Wait only for the jobs but not for the buffering `dd`.
wait "${pids[@]}"


# This will wait for `dd`.
cat /tmp/{a,b}


t1=$(date +%s)


echo "t1 - t0: $((t1-t0))"

上面的方法也可以用 cat代替 dd,但是你不能控制缓冲区的大小。

如果你有 GNU 并行,你可以使用 parset:

myfunc() {
sleep 3
echo "The input was"
echo "$@"
}
export -f myfunc
parset a,b,c myfunc ::: myarg-a "myarg  b" myarg-c
echo "$a"
echo "$b"
echo "$c"

见: https://www.gnu.org/software/parallel/parset.html