Comment attendre plusieurs sous-processus dans bash pour retourner le code de sortie! = 0 quand un sous-processus se termine avec le code! = 0?

Comment attendre dans un script bash plusieurs sous-processus issus de ce script pour terminer et renvoyer le code de sortie! = 0 lorsque l’un des sous-processus se termine par le code! = 0?

Script simple:

#!/bin/bash for i in `seq 0 9`; do doCalculations $i & done wait 

Le script ci-dessus attendra les 10 sous-processus générés, mais il indiquera toujours le statut de sortie 0 (voir help wait ). Comment puis-je modifier ce script pour qu’il détecte les états de sortie des sous-processus générés et renvoie le code de sortie 1 lorsque l’un des sous-processus se termine par le code! = 0?

Existe-t-il une meilleure solution que la collecte des PID des sous-processus, attendez-les dans l’ordre et additionnez les statuts de sortie?

wait aussi (éventuellement) prend le PID du processus à attendre, et avec $! vous obtenez le PID de la dernière commande lancée en arrière-plan. Modifiez la boucle pour stocker le PID de chaque sous-processus généré dans un tableau, puis effectuez une nouvelle boucle en attente sur chaque PID.

 # run processes and store pids in array for i in $n_procs; do ./procs[${i}] & pids[${i}]=$! done # wait for all pids for pid in ${pids[*]}; do wait $pid done 

http://jeremy.zawodny.com/blog/archives/010717.html :

 #!/bin/bash FAIL=0 echo "starting" ./sleeper 2 0 & ./sleeper 2 1 & ./sleeper 3 0 & ./sleeper 2 0 & for job in `jobs -p` do echo $job wait $job || let "FAIL+=1" done echo $FAIL if [ "$FAIL" == "0" ]; then echo "YAY!" else echo "FAIL! ($FAIL)" fi 

Si vous avez GNU Parallel installé, vous pouvez faire:

 # If doCalculations is a function export -f doCalculations seq 0 9 | parallel doCalculations {} 

GNU Parallel vous donnera le code de sortie:

  • 0 – Toutes les tâches ont été exécutées sans erreur.

  • 1-253 – Certains travaux ont échoué. Le statut de sortie indique le nombre de travaux échoués

  • 254 – Plus de 253 emplois ont échoué.

  • 255 – Autre erreur.

Regardez les vidéos d’introduction pour en savoir plus: http://pi.dk/1

Voici ce que j’ai inventé jusqu’ici. Je voudrais voir comment interrompre la commande de sumil si un enfant se termine, afin de ne pas avoir à accorder WAITALL_DELAY à son utilisation.

 waitall() { # PID... ## Wait for children to exit and indicate whether all exited with 0 status. local errors=0 while :; do debug "Processes remaining: $*" for pid in "$@"; do shift if kill -0 "$pid" 2>/dev/null; then debug "$pid is still alive." set -- "$@" "$pid" elif wait "$pid"; then debug "$pid exited with zero exit status." else debug "$pid exited with non-zero exit status." ((++errors)) fi done (("$#" > 0)) || break # TODO: how to interrupt this sleep when a child terminates? sleep ${WAITALL_DELAY:-1} done ((errors == 0)) } debug() { echo "DEBUG: $*" >&2; } pids="" for t in 3 5 4; do sleep "$t" & pids="$pids $!" done waitall $pids 

Que diriez-vous simplement de:

 #!/bin/bash pids="" for i in `seq 0 9`; do doCalculations $i & pids="$pids $!" done wait $pids ...code continued here ... 

Mettre à jour:

Comme le soulignent plusieurs commentateurs, ce qui précède attend que tous les processus soient terminés avant de continuer, mais ne se termine pas et échoue si l’un d’eux échoue, il peut être fait avec la modification suivante suggérée par @Bryan, :

 #!/bin/bash pids="" RESULT=0 for i in `seq 0 9`; do doCalculations $i & pids="$pids $!" done for pid in $pids; do wait $pid || let "RESULT=1" done if [ "$RESULT" == "1" ]; then exit 1 fi ...code continued here ... 

Voici un exemple simple utilisant wait .

Exécuter des processus:

 $ sleep 10 & $ sleep 10 & $ sleep 20 & $ sleep 20 & 

Attendez-les ensuite avec la commande wait :

 $ wait < <(jobs -p) 

Ou wait (sans arguments) pour tous.

Cela attendra que tous les travaux en arrière-plan soient terminés.

Si l'option -n est fournie, attend la fin du travail suivant et renvoie son statut de sortie.

Voir: help wait et help jobs pour la syntaxe.

Cependant, l'inconvénient est que cela ne reviendra que sur le statut du dernier identifiant. Vous devez donc vérifier l'état de chaque sous-processus et le stocker dans la variable.

Ou faites votre fonction de calcul pour créer un fichier en cas d’échec (vide ou avec échec du journal), puis vérifiez si ce fichier existe, par exemple

 $ sleep 20 && true || tee fail & $ sleep 20 && false || tee fail & $ wait < <(jobs -p) $ test -f fail && echo Calculation failed. 

Pour paralléliser cela …

 for i in $(whatever_list) ; do do_something $i done 

Traduire en ceci …

 for i in $(whatever_list) ; do echo $i ; done | ## execute in parallel... ( export -f do_something ## export functions (if needed) export PATH ## export any variables that are required xargs -I{} --max-procs 0 bash -c ' ## process in batches... { echo "processing {}" ## optional do_something {} }' ) 
  • Si une erreur survient dans un processus, elle n’interrompra pas les autres processus, mais cela entraînera un code de sortie différent de zéro de la séquence dans son ensemble .
  • L’exportation de fonctions et de variables peut être nécessaire ou non, dans un cas particulier.
  • Vous pouvez définir --max-procs fonction de la quantité de parallélisme souhaitée ( 0 signifie “tout à la fois”).
  • GNU Parallel offre des fonctionnalités supplémentaires lorsqu’il est utilisé à la place de xargs – mais il n’est pas toujours installé par défaut.
  • La boucle for n’est pas ssortingctement nécessaire dans cet exemple car echo $i ne fait que régénérer la sortie de $(whatever_list ). Je pense juste que l’utilisation du mot-clé for rend un peu plus facile de voir ce qui se passe.
  • La gestion des chaînes de Bash peut être déroutante – j’ai trouvé que l’utilisation de guillemets simples était plus adaptée à l’encapsulation de scripts non sortingviaux.
  • Vous pouvez facilement interrompre toute l’opération (en utilisant ^ C ou similaire), contrairement à l’approche plus directe du parallélisme Bash .

Voici un exemple de travail simplifié …

 for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c ' { echo sleep {} sleep 2s }' 

Je vois beaucoup de bons exemples énumérés ici, je voulais aussi jeter le mien.

 #! /bin/bash items="1 2 3 4 5 6" pids="" for item in $items; do sleep $item & pids+="$! " done for pid in $pids; do wait $pid if [ $? -eq 0 ]; then echo "SUCCESS - Job $pid exited with a status of $?" else echo "FAILED - Job $pid exited with a status of $?" fi done 

J’utilise quelque chose de très similaire pour démarrer / arrêter des serveurs / services en parallèle et vérifier chaque état de sortie. Fonctionne très bien pour moi. J’espère que cela aide quelqu’un!

Je ne crois pas que ce soit possible avec la fonctionnalité intégrée de Bash.

Vous pouvez recevoir une notification lorsqu’un enfant quitte:

 #!/bin/sh set -o monitor # enable script job control trap 'echo "child died"' CHLD 

Cependant, il n’y a aucun moyen apparent d’obtenir le statut de sortie de l’enfant dans le gestionnaire de signaux.

Obtenir cet état enfant est généralement le travail de la famille d’ wait des fonctions dans les API POSIX de niveau inférieur. Malheureusement, le support de Bash pour cela est limité – vous pouvez attendre un processus enfant spécifique (et obtenir son statut de sortie) ou vous pouvez les attendre tous et obtenir toujours un résultat 0.

Ce qu’il semble impossible de faire est l’équivalent de waitpid(-1) , qui bloque jusqu’à ce qu’un processus enfant revienne.

Le code suivant attend la fin de tous les calculs et renvoie le statut de sortie 1 si l’un des doCalculations échoue.

 #!/bin/bash for i in $(seq 0 9); do (doCalculations $i >&2 & wait %1; echo $?) & done | grep -qv 0 && exit 1 

Voici ma version qui fonctionne avec plusieurs pids, enregistre les avertissements si l’exécution prend trop de temps et arrête les sous-processus si l’exécution prend plus de temps qu’une valeur donnée.

 function WaitForTaskCompletion { local pids="${1}" # pids to wait for, separated by semi-colon local soft_max_time="${2}" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0. local hard_max_time="${3}" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0. local caller_name="${4}" # Who called this function local exit_on_error="${5:-false}" # Should the function exit program on subprocess errors Logger "${FUNCNAME[0]} called by [$caller_name]." local soft_alert=0 # Does a soft alert need to be sortingggered, if yes, send an alert once local log_ttime=0 # local time instance for comparaison local seconds_begin=$SECONDS # Seconds since the beginning of the script local exec_time=0 # Seconds since the beginning of this function local retval=0 # return value of monitored pid process local errorcount=0 # Number of pids that finished with errors local pidCount # number of given pids IFS=';' read -a pidsArray <<< "$pids" pidCount=${#pidsArray[@]} while [ ${#pidsArray[@]} -gt 0 ]; do newPidsArray=() for pid in "${pidsArray[@]}"; do if kill -0 $pid > /dev/null 2>&1; then newPidsArray+=($pid) else wait $pid result=$? if [ $result -ne 0 ]; then errorcount=$((errorcount+1)) Logger "${FUNCNAME[0]} called by [$caller_name] finished monitoring [$pid] with exitcode [$result]." fi fi done ## Log a standby message every hour exec_time=$(($SECONDS - $seconds_begin)) if [ $((($exec_time + 1) % 3600)) -eq 0 ]; then if [ $log_ttime -ne $exec_time ]; then log_ttime=$exec_time Logger "Current tasks still running with pids [${pidsArray[@]}]." fi fi if [ $exec_time -gt $soft_max_time ]; then if [ $soft_alert -eq 0 ] && [ $soft_max_time -ne 0 ]; then Logger "Max soft execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]." soft_alert=1 SendAlert fi if [ $exec_time -gt $hard_max_time ] && [ $hard_max_time -ne 0 ]; then Logger "Max hard execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]. Stopping task execution." kill -SIGTERM $pid if [ $? == 0 ]; then Logger "Task stopped successfully" else errrorcount=$((errorcount+1)) fi fi fi pidsArray=("${newPidsArray[@]}") sleep 1 done Logger "${FUNCNAME[0]} ended for [$caller_name] using [$pidCount] subprocesses with [$errorcount] errors." if [ $exit_on_error == true ] && [ $errorcount -gt 0 ]; then Logger "Stopping execution." exit 1337 else return $errorcount fi } # Just a plain stupid logging function to replace with yours function Logger { local value="${1}" echo $value } 

Exemple: attendez la fin des trois processus, consignez un avertissement si l’exécution prend moins de 5 secondes, arrêtez tous les processus si l’exécution dure plus de 120 secondes. Ne quittez pas le programme en cas d’échec.

 function something { sleep 10 & pids="$!" sleep 12 & pids="$pids;$!" sleep 9 & pids="$pids;$!" WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false } # Launch the function someting 

Stockez simplement les résultats hors du shell, par exemple dans un fichier.

 #!/bin/bash tmp=/tmp/results : > $tmp #clean the file for i in `seq 0 9`; do (doCalculations $i; echo $i:$?>>$tmp)& done #iterate wait #wait until all ready sort $tmp | grep -v ':0' #... handle as required 

Si vous avez bash 4.2 ou plus tard disponible, ce qui suit pourrait vous être utile. Il utilise des tableaux associatifs pour stocker les noms des tâches et leur “code”, ainsi que les noms des tâches et leurs pids. J’ai également intégré une méthode simple de limitation de taux qui pourrait s’avérer utile si vos tâches consumnt beaucoup de temps processeur ou d’E / S et que vous souhaitez limiter le nombre de tâches simultanées.

Le script lance toutes les tâches dans la première boucle et consum les résultats dans la seconde.

C’est un peu exagéré pour les cas simples, mais cela permet d’avoir des trucs bien ordonnés. Par exemple, on peut stocker des messages d’erreur pour chaque tâche dans un autre tableau associatif et les imprimer une fois que tout est réglé.

 #! /bin/bash main () { local -A pids=() local -A tasks=([task1]="echo 1" [task2]="echo 2" [task3]="echo 3" [task4]="false" [task5]="echo 5" [task6]="false") local max_concurrent_tasks=2 for key in "${!tasks[@]}"; do while [ $(jobs 2>&1 | grep -c Running) -ge "$max_concurrent_tasks" ]; do sleep 1 # gnu sleep allows floating point here... done ${tasks[$key]} & pids+=(["$key"]="$!") done errors=0 for key in "${!tasks[@]}"; do pid=${pids[$key]} local cur_ret=0 if [ -z "$pid" ]; then echo "No Job ID known for the $key process" # should never happen cur_ret=1 else wait $pid cur_ret=$? fi if [ "$cur_ret" -ne 0 ]; then errors=$(($errors + 1)) echo "$key (${tasks[$key]}) failed." fi done return $errors } main 

Je viens de modifier un script en arrière-plan et de paralléliser un processus.

J’ai fait des expérimentations (sur Solaris à la fois avec bash et ksh) et découvert que “wait” restitue le statut de sortie si ce n’est pas zéro ou une liste de tâches qui renvoient une sortie non nulle si aucun argument PID n’est fourni. Par exemple

Frapper:

 $ sleep 20 && exit 1 & $ sleep 10 && exit 2 & $ wait [1]- Exit 2 sleep 20 && exit 2 [2]+ Exit 1 sleep 10 && exit 1 

Ksh:

 $ sleep 20 && exit 1 & $ sleep 10 && exit 2 & $ wait [1]+ Done(2) sleep 20 && exit 2 [2]+ Done(1) sleep 10 && exit 1 

Cette sortie est écrite dans stderr, une solution simple à l’exemple des OP pourrait donc être:

 #!/bin/bash trap "rm -f /tmp/x.$$" EXIT for i in `seq 0 9`; do doCalculations $i & done wait 2> /tmp/x.$$ if [ `wc -l /tmp/x.$$` -gt 0 ] ; then exit 1 fi 

Alors que ce:

 wait 2> >(wc -l) 

renverra également un compte mais sans le fichier tmp. Cela pourrait également être utilisé de cette manière, par exemple:

 wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi) 

Mais ce n’est pas beaucoup plus utile que le fichier tmp IMO. Je ne pouvais pas trouver un moyen utile d’éviter le fichier tmp tout en évitant d’exécuter le “wait” dans un sous-shell, ce qui ne fonctionnerait pas du tout.

J’ai essayé de combiner toutes les meilleures parties des autres exemples ici. Ce script exécute la fonction checkpids la checkpids tout processus en arrière-plan et checkpids le statut de sortie sans recourir à l’interrogation.

 #!/bin/bash set -o monitor sleep 2 & sleep 4 && exit 1 & sleep 6 & pids=`jobs -p` checkpids() { for pid in $pids; do if kill -0 $pid 2>/dev/null; then echo $pid is still alive. elif wait $pid; then echo $pid exited with zero exit status. else echo $pid exited with non-zero exit status. fi done echo } trap checkpids CHLD wait 
 #!/bin/bash set -m for i in `seq 0 9`; do doCalculations $i & done while fg; do true; done 
  • set -m vous permet d’utiliser fg & bg dans un script
  • fg , en plus de mettre le dernier processus au premier plan, a le même statut de sortie que le processus au premier plan
  • while fg arrêtera de boucler quand un while fg sortira avec un statut de sortie non nul

Malheureusement, cela ne gère pas le cas où un processus en arrière-plan se termine avec un état de sortie non nul. (la boucle ne se terminera pas immédiatement. Elle attendra la fin des processus précédents.)

Cela fonctionne, devrait être juste un bon sinon mieux que la réponse de @ HoverHell!

 #!/usr/bin/env bash set -m # allow for job control EXIT_CODE=0; # exit code of overall script function foo() { echo "CHLD exit code is $1" echo "CHLD pid is $2" echo $(jobs -l) for job in `jobs -p`; do echo "PID => ${job}" wait ${job} || echo "At least one test failed with exit code => $?" ; EXIT_CODE=1 done } trap 'foo $? $$' CHLD DIRN=$(dirname "$0"); commands=( "{ echo "foo" && exit 4; }" "{ echo "bar" && exit 3; }" "{ echo "baz" && exit 5; }" ) 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 for all to finish wait; echo "EXIT_CODE => $EXIT_CODE" exit "$EXIT_CODE" # end 

et bien sûr, j’ai immortalisé ce script, dans un projet NPM qui vous permet d’exécuter des commandes bash en parallèle, utiles pour tester:

https://github.com/ORESoftware/generic-subshell

piège est ton ami. Vous pouvez intercepter ERR dans beaucoup de systèmes. Vous pouvez intercepter EXIT ou DEBUG pour exécuter un morceau de code après chaque commande.

Ceci en plus de tous les signaux standard.

 set -e fail () { touch .failure } expect () { wait if [ -f .failure ]; then rm -f .failure exit 1 fi } sleep 2 || fail & sleep 2 && false || fail & sleep 2 || fail expect 

L’ set -e en haut fait que votre script s’arrête en cas d’échec.

expect retournera 1 si un sous-job a échoué.

Il y a déjà beaucoup de réponses ici, mais je suis surpris que personne ne semble avoir suggéré d’utiliser des tableaux … Alors voici ce que j’ai fait – cela pourrait être utile pour certains dans le futur.

 n=10 # run 10 jobs c=0 PIDS=() while true my_function_or_command & PID=$! echo "Launched job as PID=$PID" PIDS+=($PID) (( c+=1 )) # required to prevent any exit due to error # caused by additional commands run which you # may add when modifying this example true do if (( c < n )) then continue else break fi done # collect launched jobs for pid in "${PIDS[@]}" do wait $pid || echo "failed job PID=$pid" done 

Je l’ai utilisé récemment (grâce à Alnitak):

 #!/bin/bash # activate child monitoring set -o monitor # locking subprocess (while true; do sleep 0.001; done) & pid=$! # count, and kill when all done c=0 function kill_on_count() { # you could kill on whatever criterion you wish for # I just counted to simulate bash's wait with no args [ $c -eq 9 ] && kill $pid c=$((c+1)) echo -n '.' # async feedback (but you don't know which one) } trap "kill_on_count" CHLD function save_status() { local i=$1; local rc=$2; # do whatever, and here you know which one stopped # but remember, you're called from a subshell # so vars have their values at fork time } # care must be taken not to spawn more than one child per loop # eg don't use `seq 0 9` here! for i in {0..9}; do (doCalculations $i; save_status $i $?) & done # wait for locking subprocess to be killed wait $pid echo 

A partir de là, on peut facilement extrapoler et déclencher (toucher un fichier, envoyer un signal) et modifier les critères de comptage (nombre de fichiers touchés ou autres) pour répondre à ce déclencheur. Ou si vous voulez juste “tout” non nul, tuez simplement le verrou de save_status.

J’avais besoin de cela, mais le processus cible n’était pas un enfant du shell actuel, auquel cas l’ wait $PID ne fonctionnait pas. J’ai plutôt trouvé l’alternative suivante:

 while [ -e /proc/$PID ]; do sleep 0.1 ; done 

Cela repose sur la présence de procfs , qui peuvent ne pas être disponibles (Mac ne le fournit pas par exemple). Donc, pour la portabilité, vous pouvez utiliser ceci à la place:

 while ps -p $PID >/dev/null ; do sleep 0.1 ; done 

Le piégeage du signal CHLD peut ne pas fonctionner car vous pouvez perdre certains signaux si vous les recevez simultanément.

 #!/bin/bash trap 'rm -f $tmpfile' EXIT tmpfile=$(mktemp) doCalculations() { echo start job $i... sleep $((RANDOM % 5)) echo ...end job $i exit $((RANDOM % 10)) } number_of_jobs=10 for i in $( seq 1 $number_of_jobs ) do ( trap "echo job$i : exit value : \$? >> $tmpfile" EXIT; doCalculations ) & done wait i=0 while read res; do echo "$res" let i++ done < "$tmpfile" echo $i jobs done !!! 

Il peut y avoir un cas où le processus est terminé avant d’attendre le processus. Si nous déclenchons l’attente d’un processus déjà terminé, cela déclenchera une erreur comme pid n’est pas un enfant de ce shell. Pour éviter de tels cas, la fonction suivante peut être utilisée pour déterminer si le processus est terminé ou non:

 isProcessComplete(){ PID=$1 while [ -e /proc/$PID ] do echo "Process: $PID is still running" sleep 5 done echo "Process $PID has finished" } 

Je pense que le moyen le plus simple d’exécuter des travaux en parallèle et de vérifier le statut consiste à utiliser des fichiers temporaires. Il y a déjà quelques réponses similaires (par exemple Nietzche-jou et mug896).

 #!/bin/bash rm -f fail for i in `seq 0 9`; do doCalculations $i || touch fail & done wait ! [ -f fail ] 

Le code ci-dessus n’est pas thread-safe. Si vous craignez que le code ci-dessus s’exécute en même temps que lui-même, il est préférable d’utiliser un nom de fichier plus unique, tel que fail. $$. La dernière ligne doit répondre à l’exigence suivante: “retourner le code de sortie 1 lorsque l’un des sous-processus se termine par le code! = 0?” J’ai jeté une exigence supplémentaire pour nettoyer. Il était peut-être plus clair de l’écrire comme ceci:

 #!/bin/bash trap 'rm -f fail.$$' EXIT for i in `seq 0 9`; do doCalculations $i || touch fail.$$ & done wait ! [ -f fail.$$ ] 

Voici un extrait similaire pour la collecte des résultats de plusieurs tâches: Je crée un répertoire temporaire, raconte les sorties de toutes les sous-tâches dans un fichier distinct, puis les vide pour révision. Cela ne correspond pas vraiment à la question – je la lance en bonus:

 #!/bin/bash trap 'rm -fr $WORK' EXIT WORK=/tmp/$$.work mkdir -p $WORK cd $WORK for i in `seq 0 9`; do doCalculations $i >$i.result & done wait grep $ * # display the results with filenames and contents 

Je pense que peut-être exécuter doCalculations; echo “$?” >> / tmp / acc dans un sousshell envoyé en arrière-plan, l’attente, puis / tmp / acc contiendrait les états de sortie, un par ligne. Cependant, je ne connais aucune conséquence des multiples processus ajoutés au fichier accumulateur.

Voici un essai de cette suggestion:

Fichier: doCalcualtions

 #! / bin / sh

 aléatoire -e 20
 dormir $?
 aléatoire -e 10

Fichier: essayez

 #! / bin / sh

 rm / tmp / acc

 pour i en $ (seq 0 20) 
 faire
         (./doCalculations "$ i"; echo "$?" >> / tmp / acc) &
 terminé

 attendez

 cat / tmp / acc |  fmt
 rm / tmp / acc

Sortie de l’exécution ./try

  5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8