Bash: Capture de la sortie de la commande exécutée en arrière-plan
J'essaie d'écrire un script bash qui obtiendra la sortie d'une commande qui s'exécute en arrière-plan. Malheureusement, je ne peux pas le faire fonctionner, la variable à laquelle j'attribue la sortie est vide-si je remplace l'affectation par une commande echo, tout fonctionne comme prévu.
#!/bin/bash
function test {
echo "$1"
}
echo $(test "echo") &
wait
a=$(test "assignment") &
wait
echo $a
echo done
CE code produit la sortie:
echo
done
Changer l'affectation à
a=`echo $(test "assignment") &`
Fonctionne, mais il semble qu'il devrait y avoir une meilleure façon de le faire.
3 réponses
Bash a en effet une fonctionnalité appeléeSubstitution de processus pour accomplir cela.
$ echo <(yes)
/dev/fd/63
Ici, l'expression <(yes)
est remplacée par un chemin d'accès d'un fichier (pseudo-périphérique) connecté à la sortie standard d'un travail asynchrone yes
(qui imprime la chaîne y
dans une boucle sans fin).
Essayons maintenant de lire:
$ cat /dev/fd/63
cat: /dev/fd/63: No such file or directory
Le problème ici est que le processus yes
s'est terminé entre-temps car il a reçu un SIGPIPE (il n'avait pas lecteurs sur stdout).
La solution est la construction suivante
$ exec 3< <(yes) # Save stdout of the 'yes' job as (input) fd 3.
Ouvre le fichier en entrée fd 3 avant le démarrage du travail d'arrière-plan.
Vous pouvez maintenant lire à partir du travail d'arrière-plan quand vous le souhaitez. Pour un exemple stupide
$ for i in 1 2 3; do read <&3 line; echo "$line"; done
y
y
y
Notez que cela a une sémantique légèrement différente de celle d'avoir le travail d'arrière-plan écrit dans un fichier sauvegardé sur un lecteur: le travail d'arrière-plan sera bloqué lorsque le tampon est plein (vous videz le tampon en lisant à partir du fd). Par en revanche, l'écriture dans un fichier soutenu par un lecteur ne bloque que lorsque le disque dur ne répond pas.
La substitution de processus N'est pas une fonctionnalité POSIX sh.
Voici un hack rapide pour donner un support de lecteur de travail asynchrone (presque) sans lui attribuer un nom de fichier:
$ 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 a récemment ajouté L'option O_TEMPFILE, ce qui rend de tels hacks possibles sans que le fichier ne soit visible du tout. Je ne sais pas si bash supporte déjà il.
Mise à JOUR:
@rthur, si vous voulez capturer toute la sortie de fd 3, Utilisez
output=$(cat <&3)
Mais notez que vous ne pouvez pas capturer de données binaires en général: ce n'est qu'une opération définie si la sortie est du texte au sens POSIX. Les implémentations que je connais filtrent simplement tous les octets nuls. De plus, POSIX spécifie que toutes les nouvelles lignes de fin doivent être supprimées.
(veuillez également noter que la capture de la sortie entraînera OOM si l'auteur ne s'arrête (yes
ne s'arrête jamais). Mais naturellement, ce problème est valable même pour read
si le séparateur de ligne n'est jamais écrit en plus)
Une façon très robuste de traiter les coprocessus dans Bash est d'utiliser... le coproc
intégré.
Supposons que vous ayez un script ou une fonction appelée banana
que vous souhaitez exécuter en arrière-plan, capturer toute sa sortie tout en faisant stuff
et attendre que ce soit fait. Je vais faire la simulation avec ceci:
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."
}
Vous exécuterez alors banana
avec le coproc
comme suit:
coproc bananafd { banana; }
C'est comme courir banana &
mais avec les extras suivants: il crée deux descripteurs de fichier qui sont dans le tableau bananafd
(index 0
pour la production et l'index 1
pour l'entrée). Vous allez capturer la sortie de banana
avec le read
intégré:
IFS= read -r -d '' -u "${bananafd[0]}" banana_output
Essayez-le:
#!/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"
Mise en garde: vous devez avoir terminé avec stuff
avant banana
se termine! si le gorille est plus rapide que vous:
#!/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"
Dans ce cas, vous obtiendrez une erreur comme celle-ci:
./banana: line 22: read: : invalid file descriptor specification
Vous pouvez vérifier s'il est trop tard (c'est-à-dire si vous avez pris trop de temps à faire votre stuff
) car une fois le coproc
terminé, bash supprime le valeurs dans le tableau bananafd
, et c'est pourquoi nous avons obtenu l'erreur précédente.
#!/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
Enfin, si vous ne voulez vraiment manquer aucun des mouvements de gorilla, même si vous prenez trop de temps pour votre stuff
, vous pouvez copier le descripteur de fichier de banana
dans un autre fd, 3
par exemple, faire vos trucs et ensuite lire à partir de 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"
Cela fonctionnera très bien! la dernière read
jouera également le rôle de wait
, de sorte que output
contiendra la production complète de banana
.
C'était Super: pas de fichiers temporaires à traiter (bash gère tout en silence) et 100% pur bash!
J'espère que cela aide!
Une façon de capturer la sortie de la commande d'arrière-plan est de rediriger sa sortie dans un fichier et de capturer la sortie du fichier après la fin du processus d'arrière-plan:
test "assignment" > /tmp/_out &
wait
a=$(</tmp/_out)