Подтвердить что ты не робот

Bash: Захват вывода команды выполняется в фоновом режиме

Я пытаюсь написать bash script, который получит результат команды, которая работает в фоновом режиме. К сожалению, я не могу заставить его работать, переменная, которой я назначаю вывод, пуста - если я заменю назначение командой эха, все работает как ожидалось.

#!/bin/bash

function test {
    echo "$1"
}

echo $(test "echo") &
wait

a=$(test "assignment") &
wait

echo $a

echo done

Этот код выводит результат:

echo

done

Изменение назначения на

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

работает, но кажется, что должен быть лучший способ сделать это.

4b9b3361

Ответ 1

Bash действительно имеет функцию подстановки процесса, чтобы выполнить это.

$ 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 его.

UPDATE

@rthur, если вы хотите захватить весь вывод из fd 3, используйте

output=$(cat <&3)

Но обратите внимание, что вы не можете записывать двоичные данные в целом: это только определенная операция, если вывод представляет собой текст в смысле POSIX. Реализации, которые я знаю, просто отфильтровывают все байты NUL. Кроме того, POSIX указывает, что все конечные новые строки должны быть удалены.

(Обратите внимание также, что захват вывода приведет к OOM, если автор никогда не останавливается (yes никогда не останавливается). Но, естественно, эта проблема сохраняется даже для read, если разделитель строк никогда не записывается дополнительно)

Ответ 2

Один очень надежный способ справиться с сопроцессорами в Bash - использовать... встроенный coproc.

Предположим, что у вас есть script или функция с именем 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 builtin:

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 удаляет значения в массиве 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, например, do и затем прочитайте от 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!

Надеюсь, это поможет!

Ответ 3

Один из способов захвата вывода фоновой команды - перенаправить его вывод в файл и захватить вывод из файла после завершения фонового процесса:

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