Bash: Capture 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’atsortingbue la sortie est vide – si je remplace l’assignation 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.

Bash a en effet une fonctionnalité appelée Substitution de processus pour y parvenir.

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

Ici, l'expression <(yes) est remplacée par un chemin d'access 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).

Maintenant, essayons de lire:

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

Le problème ici est que le processus yes terminé entre-temps car il a reçu un SIGPIPE (il n'y avait pas de lecteurs sur stdout).

La solution est la construction suivante

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

Cela ouvre le fichier en tant qu'entrée fd 3 avant le démarrage du job d'arrière-plan.

Vous pouvez maintenant lire à partir du job de fond 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 la sémantique est légèrement différente de celle qui consiste à écrire le travail d'arrière-plan dans un fichier sauvegardé par le lecteur: le job d'arrière-plan sera bloqué lorsque le tampon est plein (vous videz le tampon en le lisant). En revanche, l’écriture dans un fichier sauvegardé par un lecteur ne bloque que lorsque le disque dur ne répond pas.

La substitution de processus n'est pas une fonctionnalité de POSIX sh.

Voici un rapide hack pour donner un travail de sauvegarde asynchrone (presque) sans lui assigner 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 aussi récemment ajouté l'option O_TEMPFILE, ce qui rend ces hacks possibles sans que le fichier ne soit jamais visible du tout. Je ne sais pas si bash le supporte déjà.

MISE À JOUR :

@rthur, si vous voulez capturer la sortie entière de fd 3, alors utilisez

 output=$(cat <&3) 

Mais notez que vous ne pouvez pas capturer les données binarys en général: c'est seulement 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 NUL. En outre, POSIX spécifie que toutes les nouvelles lignes doivent être supprimées.

(Veuillez également noter que la capture de la sortie entraînera un MOO si le rédacteur ne s'arrête jamais ( yes ne s'arrête jamais). Mais ce problème persiste naturellement même en read si le séparateur de ligne n'est jamais écrit en plus)

Un moyen très efficace de gérer les coprocesses dans Bash consiste à utiliser … le coproc intégré.

Supposons que vous ayez un script ou une fonction appelée banana vous souhaitez exécuter en arrière-plan, capturer toutes ses sorties en faisant certaines stuff et attendre que cela 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 allez alors lancer la banana avec le coproc comme coproc :

 coproc bananafd { banana; } 

c’est comme exécuter banana & mais avec les extras suivants: il crée deux descripteurs de fichiers dans le tableau bananafd (à l’index 0 pour la sortie et l’index 1 pour l’entrée). Vous capturerez la sortie de banana avec le readin 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 des stuff avant banana fin de la banana ! si le gorille est plus rapide que toi:

 #!/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) car après le coproc , bash supprime les valeurs du 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 pas rater aucun des mouvements de gorille, même si vous prenez trop de temps, vous pouvez copier le descripteur de fichier de la banana dans un autre fd 3 par exemple, faites vos fichiers et lisez 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 wait , de sorte que la output contiendra la sortie complète de banana .

C’était génial: pas de fichiers temporaires à gérer (bash gère tout en silence) et 100% pure bash!

J’espère que cela t’aides!

Une façon de capturer la sortie de la commande en arrière-plan consiste à redirect sa sortie dans un fichier et à capturer la sortie du fichier une fois le processus en arrière-plan terminé:

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