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

时间:2013-11-16 11:20:48

标签: 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") &`

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

6 个答案:

答案 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."
}

然后,您将bananacoproc一起运行:

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"

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