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.

47
demandé sur rthur 2013-11-16 15:20:48

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)

50
répondu Jo So 2013-11-16 15:25:52

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!

23
répondu gniourf_gniourf 2016-10-28 00:48:10

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)
7
répondu anubhava 2013-11-16 11:24:04