Comment capturer correctement le code de sortie / gérer les erreurs lors de l'utilisation de la substitution de processus?

13

J'ai un script qui analyse les noms de fichiers dans un tableau en utilisant la méthode suivante tirée d' un Q&A sur SO :

unset ARGS
ARGID="1"
while IFS= read -r -d $'\0' FILE; do
    ARGS[ARGID++]="$FILE"
done < <(find "$@" -type f -name '*.txt' -print0)

Cela fonctionne très bien et gère parfaitement tous les types de variations de noms de fichiers. Parfois, cependant, je passe un fichier inexistant au script, par exemple:

$ findscript.sh existingfolder nonexistingfolder
find: `nonexistingfile': No such file or directory
...

Dans des circonstances normales, je voudrais que le script capture le code de sortie avec quelque chose comme RET=$?et l'utilise pour décider comment procéder. Cela ne semble pas fonctionner avec la substitution de processus ci-dessus.

Quelle est la procédure correcte dans des cas comme celui-ci? Comment capturer le code retour? Existe-t-il d'autres moyens plus appropriés pour déterminer si quelque chose s'est mal passé dans le processus de substitution?

Glutanimate
la source

Réponses:

5

Vous pouvez assez facilement obtenir le retour de tout processus sous-shell en faisant écho à son retour sur sa sortie standard. Il en va de même pour la substitution de processus:

while IFS= read -r -d $'\0' FILE || 
    ! return=$FILE
do    ARGS[ARGID++]="$FILE"
done < <(find . -type f -print0; printf "$?")

Si je lance cela, alors la toute dernière ligne - (ou une \0section délimitée selon le cas) va être findle statut de retour de. readva retourner 1 quand il obtient un EOF - donc la seule heure $returnest définie $FILEpour le tout dernier bit d'information lu.

J'utilise printfpour éviter d'ajouter une ligne \nélectronique supplémentaire - c'est important parce que même une readrégulièrement effectuée - une dans laquelle vous ne délimitez pas sur des \0NUL - va retourner autre que 0 dans les cas où les données qu'elle vient de lire ne se terminent pas en un \newline. Donc, si votre dernière ligne ne se termine pas par une ligne \nélectronique, la dernière valeur de votre variable lue sera votre retour.

Exécuter la commande ci-dessus puis:

echo "$return"

PRODUCTION

0

Et si je modifie la partie substitution de processus ...

...
done < <(! find . -type f -print0; printf "$?")
echo "$return"

PRODUCTION

1

Une démonstration plus simple:

printf \\n%s list of lines printed to pipe |
while read v || ! echo "$v"
do :; done

PRODUCTION

pipe

Et en fait, tant que le retour que vous voulez est la dernière chose que vous écrivez à stdout à partir de la substitution de processus - ou de tout processus sous-shell à partir duquel vous lisez de cette façon -, $FILEle statut de retour que vous souhaitez quand il sera toujours est terminé. Et donc la || ! return=...partie n'est pas strictement nécessaire - elle est utilisée pour démontrer le concept uniquement.

mikeserv
la source
5

Les processus en substitution de processus sont asynchrones: le shell les lance et ne donne alors aucun moyen de détecter leur mort. Vous ne pourrez donc pas obtenir le statut de sortie.

Vous pouvez écrire l'état de sortie dans un fichier, mais c'est généralement maladroit car vous ne pouvez pas savoir quand le fichier est écrit. Ici, le fichier est écrit peu après la fin de la boucle, il est donc raisonnable de l'attendre.

 < <(find …; echo $? >find.status.tmp; mv find.status.tmp find.status)
while ! [ -e find.status ]; do sleep 1; done
find_status=$(cat find.status; rm find.status)

Une autre approche consiste à utiliser un canal nommé et un processus d'arrière-plan (que vous pouvez utiliser wait).

mkfifo find_pipe
find  >find_pipe &
find_pid=$!
 <find_pipe
wait $find_pid
find_status=$?

Si aucune approche ne convient, je pense que vous devrez vous diriger vers un langage plus performant, tel que Perl, Python ou Ruby.

Gilles 'SO- arrête d'être méchant'
la source
Merci pour cette réponse. Les méthodes que vous avez décrites fonctionnent bien mais je dois admettre qu'elles sont un peu plus compliquées que je ne l'avais prévu. Dans mon cas, je me suis contenté d'une boucle avant celle montrée dans la question qui parcourt tous les arguments et affiche une erreur si l'un d'eux n'est pas un fichier ou un dossier. Bien que cela ne gère pas d'autres types d'erreurs qui pourraient se produire dans le processus substitué, c'est assez bon pour ce cas spécifique. Si jamais j'ai besoin d'une méthode de gestion des erreurs plus sophistiquée dans des situations comme celle-ci, je reviendrai certainement sur votre réponse.
Glutanimate
2

Utilisez un coprocessus . À l'aide de la fonction coprocintégrée, vous pouvez démarrer un sous-processus, lire sa sortie et vérifier son état de sortie:

coproc LS { ls existingdir; }
LS_PID_=$LS_PID
while IFS= read i; do echo "$i"; done <&"$LS"
wait "$LS_PID_"; echo $?

Si le répertoire n'existe pas, waitquittera avec un code d'état différent de zéro.

Il est actuellement nécessaire de copier le PID dans une autre variable car $LS_PIDil sera désactivé avant d' waitêtre appelé. Voir Bash unsets * _PID variable avant de pouvoir attendre sur coproc pour plus de détails.

Feuermurmel
la source
1
Je suis curieux de savoir quand on utilisera <& "$ LS" vs read -u $ LS? - merci
Brian Chrisman
1
@BrianChrisman Dans ce cas, probablement jamais. read -udevrait aussi bien fonctionner. L'exemple était censé être générique et montrer comment la sortie du coprocess pouvait être canalisée dans une autre commande.
Feuermurmel
1

Une approche est la suivante:

status=0
token="WzNZY3CjqF3qkasn"    # some random string
while read line; do
    if [[ "$line" =~ $token:([[:digit:]]+) ]]; then
        status="${BASH_REMATCH[1]}"
    else
        echo "$line"
    fi
done < <(command; echo "$token:$?")
echo "Return code: $status"

L'idée est de faire écho à l'état de sortie avec le jeton aléatoire une fois la commande terminée, puis à utiliser des expressions régulières bash pour rechercher et extraire l'état de sortie. Le jeton est utilisé pour créer une chaîne unique à rechercher dans la sortie.

Ce n'est probablement pas la meilleure façon de le faire dans un sens général de programmation, mais c'est peut-être la façon la moins pénible de le gérer en bash.

orev
la source