【问题标题】:Bash: Capture output of command run in backgroundBash:捕获在后台运行的命令的输出
【发布时间】:2013-11-16 11:20:48
【问题描述】:

我正在尝试编写一个 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") &`

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

【问题讨论】:

    标签: bash


    【解决方案1】:

    Bash 确实有一个称为 Process Substitution 的功能来完成此操作。

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

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

    现在让我们尝试从中读取:

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

    这里的问题是yes 进程同时终止,因为它收到了一个 SIGPIPE(它在标准输出上没有读取器)。

    解决方案是以下构造

    $ 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 也存在该问题)

    【讨论】:

    • 谢谢,看起来很棒!有什么方法可以在不遍历每一行的情况下从阅读器获取所有输出?
    • 我是否正确假设调用 read 行也可以调用“等待”?
    • 警告:如果你想在现实生活中使用它,你需要用mktemp创建文件backingfile,甚至使用trap作为组合&gt;/ exec/rm 不是原子的!
    • @gniourf_gniourf:不,原子性没有问题。不,陷阱不会使任何东西更加原子化。
    • 对本次讨论的结论并不完全清楚。是否缺少对wait 的呼叫?另外,在cat &lt;&amp;3 之后是exec 3&lt;&amp;- 需要关闭文件描述符吗?
    【解决方案2】:

    在 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 &amp;,但具有以下附加功能:它在数组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!

    希望这会有所帮助!

    【讨论】:

    • 感谢您的详细回复。我最终使用了 Jo So 的答案,因为这似乎是一种将命令输出传递给另一个 fd 的更简单的方法——不过 coproc 确实很有用。
    • @user2352030 JoSo 的回答一点也不简单!它甚至更复杂,因为它迫使您创建一个文件并将其删除。如果您想以安全的方式使用他的答案,您需要使用mktemp... 甚至可能使用trap!不要被看起来简单的东西所迷惑!
    • 也许我错了,但据我了解,他的便携式 sh shell 方法迫使我这样做,但使用 bash 构造 exec 3&lt; &lt;(command) 不会。
    • @user2352030 coproc 的另一个优点是,您可以通过检查数组 bananafd 是否设置来确定后台进程是否已完成。
    • 嗯...这看起来是解决我遇到的问题的最佳解决方案,但事实上只有一个 coproc 可以同时运行,这限制了它。在我的情况下,它恰好适用于四个 coprocs(我正在运行四个昂贵的命令来初始化我的 .bashrc 中的四个不同的 bash 变量,并且不喜欢花 2.5 秒等待它们在并行执行时按顺序运行可以获得它下降到大约 0.9 秒),但转储了关于在第一个之后启动的三个 coprocs 的警告(因为第一个还没有完成)。
    【解决方案3】:

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

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

    【讨论】:

    • 在不使用文件的情况下有没有办法做到这一点?
    • 是的,有一种(仅限 bash)方式。看我的回答。
    • 这是唯一对我有用的方法,谢谢!
    【解决方案4】:

    只需将命令分组,当您在后台运行它们并等待两者时。

    { 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。但是你不能控制缓冲区的大小。

    【讨论】:

      【解决方案5】:

      我也使用文件重定向。喜欢:

      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) 存在错误,并且无法检索我启动的作业的状态。它在 bash 5 中运行良好,但文件重定向适用于所有版本。

      【讨论】:

        【解决方案6】:

        如果您有 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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-11-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-04-11
          • 2015-07-26
          • 1970-01-01
          相关资源
          最近更新 更多