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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20017805/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 00:26:18  来源:igfitidea点击:

Bash: Capture output of command run in background

bash

提问by rthur

I'm trying to write a bash script that will get the output of a command that runs in the background. Unfortunately I can't get it to work, the variable I assign the output to is empty - if I replace the assignment with an echo command everything works as expected though.

我正在尝试编写一个 bash 脚本,该脚本将获取在后台运行的命令的输出。不幸的是,我无法让它工作,我分配给输出的变量是空的 - 如果我用 echo 命令替换分配,一切都会按预期工作。

#!/bin/bash

function test {
    echo ""
}

echo $(test "echo") &
wait

a=$(test "assignment") &
wait

echo $a

echo done

This code produces the output:

此代码产生输出:

echo

done

Changing the assignment to

将分配更改为

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

works, but it seems like there should be a better way of doing this.

有效,但似乎应该有更好的方法来做到这一点。

采纳答案by Jo So

Bash has indeed a feature called Process Substitutionto accomplish this.

Bash 确实有一个称为Process Substitution的功能来实现这一点。

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

Here, the expression <(yes)is replaced with a pathname of a (pseudo device) file that is connected to the standard output of an asynchronous job yes(which prints the string yin an endless loop).

此处,表达式<(yes)替换为连接到异步作业标准输出yesy以无限循环方式打印字符串)的(伪设备)文件的路径名。

Now let's try to read from it:

现在让我们尝试阅读它:

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

The problem here is that the yesprocess terminated in the meantime because it received a SIGPIPE (it had no readers on stdout).

这里的问题是该yes进程在此期间终止,因为它收到了一个 SIGPIPE(它在 stdout 上没有读取器)。

The solution is the following construct

解决方案是以下构造

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

This opens the file as input fd 3 before the background job is started.

这会在后台作业启动之前打开文件作为输入 fd 3。

You can now read from the background job whenever you prefer. For a stupid example

您现在可以随时从后台作业中读取。举个愚蠢的例子

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

Note that this has slightly different semantics than having the background job write to a drive backed file: the background job will be blocked when the buffer is full (you empty the buffer by reading from the fd). By contrast, writing to a drive-backed file is only blocking when the hard drive doesn't respond.

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

Process substitution is not a POSIX sh feature.

进程替换不是 POSIX sh 功能。

Here's a quick hack to give an asynchronous job drive backing (almost) without assigning a filename to it:

这是一个快速的技巧,可以在不为其分配文件名的情况下(几乎)为异步作业驱动器提供支持:

$ 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 also recently got added the O_TEMPFILE option, which makes such hacks possible without the file ever being visible at all. I don't know if bash already supports it.

Linux 最近还添加了 O_TEMPFILE 选项,这使得在根本不可见文件的情况下进行此类黑客攻击成为可能。我不知道 bash 是否已经支持它。

UPDATE:

更新

@rthur, if you want to capture the whole output from fd 3, then use

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

output=$(cat <&3)

But note that you can't capture binary data in general: It's only a defined operation if the output is text in the POSIX sense. The implementations I know simply filter out all NUL bytes. Furthermore POSIX specifies that all trailing newlines must be removed.

但请注意,您通常无法捕获二进制数据:如果输出是 POSIX 意义上的文本,则它只是一个定义的操作。我知道的实现只是过滤掉所有 NUL 字节。此外,POSIX 规定必须删除所有尾随换行符。

(Please note also that capturing the output will result in OOM if the writer never stops (yesnever stops). But naturally that problem holds even for readif the line separator is never written additionally)

(另请注意yesread如果编写器永不停止(永不停止),捕获输出将导致 OOM 。但即使从未额外写入行分隔符,该问题自然也成立)

回答by gniourf_gniourf

One very robust way to deal with coprocesses in Bash is to use... the coprocbuiltin.

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

Suppose you have a script or function called bananayou wish to run in background, capture all its output while doing some stuffand wait until it's done. I'll do the simulation with this:

假设您有一个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."
}

You will then run bananawith the coprocas so:

然后,您将运行bananacoproc像这样:

coproc bananafd { banana; }

this is like running banana &but with the following extras: it creates two file descriptors that are in the array bananafd(at index 0for output and index 1for input). You'll capture the output of bananawith the readbuiltin:

这就像运行,banana &但具有以下附加功能:它在数组中创建两个文件描述符bananafd(在0输出索引1和输入索引处)。您将banana使用read内置函数捕获输出:

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

Try it:

尝试一下:

#!/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"

Caveat: you must be done with stuffbefore bananaends! if the gorilla is quicker than you:

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

#!/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"

In this case, you'll obtain an error like this one:

在这种情况下,您将收到如下错误:

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

You can check whether it's too late (i.e., whether you've taken too long doing your stuff) because after the coprocis done, bash removes the values in the array bananafd, and that's why we obtained the previous error.

您可以检查是否为时已晚(即,您是否花了太长时间执行stuff),因为coproc完成后,bash 会删除 array 中的值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

Finally, if you really don't want to miss any of gorilla's moves, even if you take too long for your stuff, you could copy banana's file descriptor to another fd, 3for example, do your stuff and then read from 3:

最后,如果你真的不想错过任何的大猩猩的动作,即使你花费太长时间你的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"

This will work very well! the last readwill also play the role of wait, so that outputwill contain the complete output of banana.

这将非常有效!最后read也会起到 的作用wait,这样output就会包含 的完整输出banana

That was great: no temp files to deal with (bash handles everything silently) and 100% pure bash!

太好了:无需处理临时文件(bash 静默处理所有内容)和 100% 纯 bash!

Hope this helps!

希望这可以帮助!

回答by anubhava

One way to capture background command's output is to redirect it's output in a file and capture output from file after background process has ended:

捕获后台命令输出的一种方法是将其输出重定向到文件中,并在后台进程结束后从文件中捕获输出:

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