我正在尝试编写一个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") &`
有效,但似乎应该有更好的方法。
答案 0 :(得分:55)
Bash确实有一个名为 Process Substitution 的功能来完成此任务。
$ echo <(yes)
/dev/fd/63
这里,表达式<(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.
这会在后台作业启动之前将文件作为输入fd 3打开。
您现在可以随时阅读后台作业。对于一个愚蠢的例子
$ 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,如果你想从fd 3捕获整个输出,那么使用
output=$(cat <&3)
但请注意,一般情况下无法捕获二进制数据:如果输出是POSIX意义上的文本,则它只是一个已定义的操作。我知道的实现只是过滤掉所有NUL字节。此外,POSIX指定必须删除所有尾随换行符。
(请注意,如果作者永远不会停止(yes
永不停止),捕获输出将导致OOM。但如果行分隔符永远不会被写入,那么即使read
也存在问题。 )
答案 1 :(得分:30)
在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."
}
然后,您将banana
与coproc
一起运行:
coproc bananafd { banana; }
这就像运行banana &
但有以下附加功能:它创建了两个文件描述符,它们位于数组bananafd
中(索引为0
,用于输出和索引1
输入)。您将使用内置banana
内容来捕获read
的输出:
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"
警告:在stuff
结束之前,您必须完成banana
!如果大猩猩比你快:
#!/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会删除数组{{1}中的值这就是我们获得之前错误的原因。
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
的文件描述符复制到另一个fd,{ {1}}例如,做你的东西,然后阅读banana
:
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
的完整输出。
那很棒:没有临时文件可以处理(bash处理所有内容)和100%纯粹的bash!
希望这有帮助!
答案 2 :(得分:7)
捕获后台命令输出的一种方法是将其输出重定向到文件中,并在后台进程结束后从文件中捕获输出:
test "assignment" > /tmp/_out &
wait
a=$(</tmp/_out)
答案 3 :(得分:0)
我也使用文件重定向。喜欢:
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
a_value=(toto titi tata tutu)
msg=""
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
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
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) 有问题,无法检索我启动的作业的状态。它在 bash 5 中运行良好,但文件重定向适用于所有版本。
答案 4 :(得分:0)
只需将命令分组,当您在后台运行它们并等待两者时。
{ 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.
脚本的总运行时间为三秒,但两个后台作业的总睡眠时间为五秒。并且后台作业的输出是有序的。
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
。但是这样你就无法控制缓冲区大小了。
答案 5 :(得分:0)
如果您有 GNU Parallel,您可能可以使用 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"