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") &`
работает, но кажется, что должен быть лучший способ сделать это.
Ответы
Ответ 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)