Disons que nous avons un script bash comme ceci:
echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait
existe-t-il un moyen de collecter les codes de sortie des sous-shells/sous-processus? Vous cherchez un moyen de le faire et vous ne trouvez rien. J'ai besoin d'exécuter ces sous-coquilles en parallèle, sinon oui ce serait plus facile.
Je recherche une solution générique (j'ai un nombre inconnu/dynamique de sous-processus à exécuter en parallèle).
La réponse d'Alexander Mills qui utilise handleJobs m'a donné un excellent point de départ, mais m'a aussi donné cette erreur
avertissement: run_pending_traps: mauvaise valeur dans trap_list [17]: 0x461010
Ce qui peut être un problème de condition de course bash
Au lieu de cela, je viens de stocker le pid de chaque enfant et d'attendre et d'obtenir le code de sortie pour chaque enfant spécifiquement. Je trouve cela plus propre en termes de sous-processus engendrant des sous-processus dans les fonctions et évitant le risque d'attendre un processus parent où je voulais attendre l'enfant. C'est plus clair ce qui se passe parce qu'il n'utilise pas le piège.
#!/usr/bin/env bash
# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?)
function wait_and_get_exit_codes() {
children=("$@")
EXIT_CODE=0
for job in "${children[@]}"; do
echo "PID => ${job}"
CODE=0;
wait ${job} || CODE=$?
if [[ "${CODE}" != "0" ]]; then
echo "At least one test failed with exit code => ${CODE}" ;
EXIT_CODE=1;
fi
done
}
DIRN=$(dirname "$0");
commands=(
"{ echo 'a'; exit 1; }"
"{ echo 'b'; exit 0; }"
"{ echo 'c'; exit 2; }"
)
clen=`expr "${#commands[@]}" - 1` # get length of commands - 1
children_pids=()
for i in `seq 0 "$clen"`; do
(echo "${commands[$i]}" | bash) & # run the command via bash in subshell
children_pids+=("$!")
echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0; # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"
echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
Utilisez wait
avec un PID, qui :
Attendez que le processus enfant spécifié par chaque ID de processus pid ou spécification de travail jobspec quitte et renvoie l'état de sortie de la dernière commande attendue.
Vous devrez enregistrer le PID de chaque processus au fur et à mesure:
echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!
Vous pouvez également activer le contrôle des travaux dans le script avec set -m
et utilisez un %n
jobspec, mais vous ne voulez certainement pas - le contrôle des tâches a beaucoup d'autres effets secondaires .
wait
renverra le même code que le processus terminé avec. Vous pouvez utiliser wait $X
à tout moment (raisonnable) ultérieur pour accéder au code final en tant que $?
ou utilisez-le simplement comme vrai/faux:
echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"
wait
s'arrêtera jusqu'à ce que la commande se termine si ce n'est pas déjà fait.
Si vous voulez éviter le blocage comme ça, vous pouvez définir un trap
sur SIGCHLD
, compter le nombre de terminaisons et gérer tous les wait
s à la fois quand ils ont tous fini. Vous pouvez probablement vous en sortir en utilisant wait
seul presque tout le temps.
Si vous aviez un bon moyen d'identifier les commandes, vous pouvez imprimer leur code de sortie dans un fichier tmp puis accéder au fichier spécifique qui vous intéresse:
#!/bin/bash
for i in `seq 1 5`; do
( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done
wait
for i in `seq 1 5`; do # or even /tmp/cmd__*
echo "process $i:"
cat /tmp/cmd__${i}
done
N'oubliez pas de supprimer les fichiers tmp.
Utiliser un compound command
- mettez l'instruction entre parenthèses:
( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &
donnera la sortie
x
X: 0
TRUE: 0
FALSE: 1
Une manière vraiment différente d'exécuter plusieurs commandes en parallèle est d'utiliser GNU Parallel . Faites une liste des commandes à exécuter et placez-les dans le fichier list
:
cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D
Exécutez toutes les commandes en parallèle et collectez les codes de sortie dans le fichier job.log
:
cat list | parallel -j0 --joblog job.log
cat job.log
et la sortie est:
Seq Host Starttime JobRuntime Send Receive Exitval Signal Command
1 : 1486892487.325 1.976 0 0 7 0 sleep 2 ; exit 7
2 : 1486892487.326 3.003 0 0 55 0 sleep 3 ; exit 55
c'est le script générique que vous recherchez. Le seul inconvénient est que vos commandes sont entre guillemets, ce qui signifie que la coloration syntaxique via votre IDE ne fonctionnera pas vraiment. Sinon, j'ai essayé quelques autres réponses et c'est la meilleure. Cette réponse intègre l'idée d'utiliser wait <pid>
donné par @Michael mais va plus loin en utilisant la commande trap
qui semble fonctionner le mieux.
#!/usr/bin/env bash
set -m # allow for job control
EXIT_CODE=0; # exit code of overall script
function handleJobs() {
for job in `jobs -p`; do
echo "PID => ${job}"
CODE=0;
wait ${job} || CODE=$?
if [[ "${CODE}" != "0" ]]; then
echo "At least one test failed with exit code => ${CODE}" ;
EXIT_CODE=1;
fi
done
}
trap 'handleJobs' CHLD # trap command is the key part
DIRN=$(dirname "$0");
commands=(
"{ echo 'a'; exit 1; }"
"{ echo 'b'; exit 0; }"
"{ echo 'c'; exit 2; }"
)
clen=`expr "${#commands[@]}" - 1` # get length of commands - 1
for i in `seq 0 "$clen"`; do
(echo "${commands[$i]}" | bash) & # run the command via bash in subshell
echo "$i ith command has been issued as a background job"
done
wait; # wait for all subshells to finish
echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
merci à @michael homer de m'avoir mis sur la bonne voie, mais utiliser la commande trap
est la meilleure approche AFAICT.
Une autre variante de la réponse de @rolf:
Une autre façon de sauvegarder le statut de sortie serait quelque chose comme
mkdir /tmp/status_dir
puis avoir chaque script
script_name="${0##*/}" ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"
Cela vous donne un nom unique pour chaque fichier d'état, y compris le nom du script qui l'a créé et son identifiant de processus (dans le cas où plusieurs instances du même script sont en cours d'exécution) que vous pouvez enregistrer pour référence plus tard et les mettre toutes dans le même endroit afin que vous puissiez simplement supprimer le sous-répertoire entier lorsque vous avez terminé.
Vous pouvez même enregistrer plusieurs statuts de chaque script en faisant quelque chose comme
tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"
qui crée le fichier comme précédemment, mais y ajoute une chaîne aléatoire unique.
Ou, vous pouvez simplement ajouter plus d'informations sur l'état au même fichier.
script3
ne sera exécuté que si script1
et script2
réussissent et script1
et script2
sera exécuté en parallèle:
./script1 &
process1=$!
./script2 &
process2=$!
wait $process1
rc1=$?
wait $process2
rc2=$?
if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0 ]];then
./script3
fi