web-dev-qa-db-fra.com

Pourquoi (sortie 1) ne quitte pas le script?

J'ai un script qui ne se ferme pas quand je le veux.

Un exemple de script avec la même erreur est:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Je suppose que pour voir la sortie:

:~$ ./test.sh
1
:~$

Mais je vois en fait:

:~$ ./test.sh
1
2
:~$

Est-ce que le () le chaînage de commandes crée en quelque sorte une portée? Qu'est-ce que exit quitte, sinon le script?

51
Minix

() exécute des commandes dans le sous-shell, donc par exit vous quittez le sous-shell et revenez au shell parent. Utilisez des accolades {} si vous souhaitez exécuter des commandes dans le shell actuel.

Du manuel bash:

(list) list est exécuté dans un environnement de sous-shell. Les affectations de variables et les commandes intégrées qui affectent l'environnement du shell ne restent pas en vigueur une fois la commande terminée. Le statut de retour est le statut de sortie de la liste.

{list;} list est simplement exécuté dans l'environnement Shell actuel. la liste doit se terminer par une nouvelle ligne ou un point-virgule. C'est ce qu'on appelle une commande de groupe. Le statut de retour est le statut de sortie de la liste. Notez que contrairement aux métacaractères (et), {et} sont des mots réservés et doivent se produire lorsqu'un mot réservé est autorisé à être reconnu. Puisqu'ils ne provoquent pas de coupure dans Word, ils doivent être séparés de la liste par des espaces ou un autre métacaractère Shell.

Il convient de mentionner que la syntaxe du shell est assez cohérente et que le sous-shell participe également aux autres () des constructions comme la substitution de commandes (également avec l'ancien style `..` syntaxe) ou la substitution de processus, de sorte que les éléments suivants ne sortiront pas non plus du shell actuel:

echo $(exit)
cat <(exit)

Bien qu'il puisse être évident que des sous-coquilles sont impliquées lorsque des commandes sont placées explicitement à l'intérieur de (), le fait le moins visible est qu'ils apparaissent également dans ces autres structures:

  • commande démarrée en arrière-plan

    exit &
    

    ne quitte pas le shell actuel car (après man bash)

    Si une commande est interrompue par l'opérateur de contrôle &, le shell exécute la commande en arrière-plan dans un sous-shell. Le shell n'attend pas la fin de la commande et l'état de retour est 0.

  • le pipeline

    exit | echo foo
    

    ne sort toujours que du sous-shell.

    Cependant, différents obus se comportent différemment à cet égard. Par exemple, bash place tous les composants du pipeline dans des sous-boîtiers séparés (sauf si vous utilisez l'option lastpipe dans les appels où le contrôle des travaux n'est pas activé), mais AT&T ksh et zsh exécute la dernière partie à l'intérieur du shell actuel (les deux comportements sont autorisés par POSIX). Donc

    exit | exit | exit
    

    ne fait pratiquement rien dans bash, mais quitte zsh à cause de le dernierexit.

  • coproc exit exécute également exit dans un sous-shell.

87
jimmij

L'exécution de exit dans un sous-shell est un écueil:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Le script imprime 42, quitte le sous-shell avec le code retour 1 Et continue avec le script. Même le remplacement de l'appel par echo $(CALC) || exit 1 n'aide pas car le code retour de echo est 0 quel que soit le code retour de calc. Et calc est exécuté avant echo.

Encore plus déroutant contrecarre l'effet de exit en l'enveloppant dans local intégré comme dans le script suivant. Je suis tombé sur le problème lorsque j'ai écrit une fonction pour vérifier une valeur d'entrée. Exemple:

Je veux créer un fichier nommé "year month day.log", c'est-à-dire 20141211.log Pour aujourd'hui. La date est entrée par un utilisateur qui peut ne pas fournir une valeur raisonnable. Par conséquent, dans ma fonction fname je vérifie la valeur de retour de date pour vérifier la validité de l'entrée utilisateur:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Cela semble bon. Que le script soit nommé s.sh. Si l'utilisateur appelle le script avec ./s.sh "Thu Dec 11 20:45:49 CET 2014", Le fichier 20141211.log Est créé. Si, cependant, l'utilisateur tape ./s.sh "Thu hec 11 20:45:49 CET 2014", Le script génère:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

La ligne fname… Indique que les mauvaises données d'entrée ont été détectées dans le sous-shell. Mais la exit 1 À la fin de la ligne local … N'est jamais déclenchée car la directive local renvoie toujours 0. C'est parce que local est exécuté après$(fname) et écrase ainsi son code retour. Et à cause de cela, le script continue et appelle touch avec un paramètre vide. Cet exemple est simple mais le comportement de bash peut être assez déroutant dans une application réelle. Je sais, les vrais programmeurs n'utilisent pas les locaux.

Pour être clair: sans le local, le script s'interrompt comme prévu lorsqu'une date non valide est entrée.

La solution consiste à diviser la ligne comme

local FNAME
FNAME=$(fname "$1") || exit 1

Le comportement étrange est conforme à la documentation de local dans la page de manuel de bash: "Le statut de retour est 0 sauf si local est utilisé en dehors d'une fonction, un nom non valide est fourni ou le nom est une variable en lecture seule."

Bien que n'étant pas un bug, je pense que le comportement de bash est contre-intuitif. Je connais la séquence d'exécution, local ne doit pas masquer une affectation cassée, néanmoins.

Ma réponse initiale contenait quelques inexactitudes. Après une discussion révélatrice et approfondie avec mikeserv (merci pour cela), je suis allé les réparer.

13
hermannk

La solution réelle:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Le regroupement d'erreurs ne s'exécutera que si bla renvoie un état d'erreur et que exit n'est pas dans un sous-shell, donc le script entier s'arrête.

2
Walf

Les crochets commencent un sous-shell et la sortie ne quitte que ce sous-shell.

Vous pouvez lire le code de sortie avec $? et ajoutez ceci dans votre script pour quitter le script si le sous-shell a été quitté:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
1
rubo77