Pourquoi (exit 1) ne quitte-t-il pas le script?

48

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 supposerais voir la sortie:

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

Mais je vois réellement:

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

La ()commande chaînant crée- t-elle en quelque sorte une étendue? Qu'est-ce exitqui sort de, si ce n'est le script?

Minix
la source
5
Cela appelle une réponse en un seul mot: subshell
Joshua

Réponses:

87

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

De bash manuel:

(list) list est exécutée 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.

{ liste; } list est simplement exécuté dans l'environnement shell actuel. La liste doit être terminée par une nouvelle ligne ou un point-virgule. Ceci est connu comme 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. Comme ils ne provoquent pas de saut de mot, ils doivent être séparés de la liste par des espaces ou un autre métacaractère du shell.

Il est à noter que la syntaxe du shell est relativement cohérente et que le sous-shell participe également aux autres ()constructions telles que la substitution de commande (également avec la `..`syntaxe de style ancien ) ou la substitution de processus, de sorte que les éléments suivants ne sortent pas du shell actuel:

echo $(exit)
cat <(exit)

Bien qu'il soit évident que des sous-shell sont impliqués lorsque des commandes sont placées explicitement à l'intérieur (), le fait moins visible est qu'elles sont également générées 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 terminée par l'opérateur de contrôle &, le shell l'exécute en arrière-plan dans un sous-shell. Le shell n'attend pas la fin de la commande et le statut de retour est 0.

  • le pipeline

    exit | echo foo

    ne sort toujours que du sous-shell.

    Cependant, des obus différents se comportent différemment à cet égard. Par exemple, bashplace tous les composants du pipeline dans des sous-shell distincts (sauf si vous utilisez l' lastpipeoption dans les appels où le contrôle de travail n'est pas activé), mais AT & T kshet zshexécutez la dernière partie à l'intérieur du shell actuel (les deux comportements sont autorisés par POSIX). Ainsi

    exit | exit | exit

    ne fait fondamentalement rien dans bash, mais sort du zsh à cause du dernier exit .

  • coproc exitfonctionne également exitdans un sous-shell.

Jimmij
la source
5
Ah Maintenant, pour trouver tous les endroits où mon prédécesseur a utilisé les fausses accolades. Merci pour la perspicacité.
Minix
10
Prenez bien note de l'espacement dans la page de manuel: {et }ne sont pas une syntaxe, ce sont des mots réservés et doivent être entourés d'espaces. La liste doit se terminer par un terminateur de commande (point-virgule, nouvelle ligne, esperluette)
glenn jackman
Par intérêt, s'agit-il d'un autre processus ou simplement d'un environnement séparé dans une pile interne? J'utilise beaucoup () pour isoler les chdirs, et je dois avoir eu de la chance avec mon utilisation de $$, etc., si l'ancien.
Dan Sheppard
5
@DanSheppard C'est un autre processus, mais affiche l' (echo $$)ID de shell parent car $$est développé avant même que le sous-shell ne soit créé. L'impression d'un identifiant de processus de sous-shell pourrait être délicate, voir stackoverflow.com/questions/9119885/…
jimmij
@ jimmij, comment se peut-il qu'il $$soit étendu avant la création du sous-shell et $BASHPIDaffiche la valeur correcte pour un sous-shell?
Wildcard
13

L'exécution du exitdans un sous-shell est un piège:

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

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

Encore plus déconcertant est de contrecarrer l’effet de exiten l’enveloppant dans les fonctions localintégrées, 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é "année mois jour.log", c'est-à-dire 20141211.logpour aujourd'hui. La date est saisie par un utilisateur qui peut ne pas fournir une valeur raisonnable. Par conséquent, dans ma fonction, fnameje vérifie la valeur de retour de datepour 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. Laissez le script être nommé s.sh. Si l'utilisateur appelle le script avec ./s.sh "Thu Dec 11 20:45:49 CET 2014", le fichier 20141211.logest créé. Cependant, si 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 données d'entrée incorrectes ont été détectées dans le sous-shell. Mais la exit 1fin de la local …ligne n'est jamais déclenchée car la localdirective est toujours renvoyée 0. En effet, il localest exécuté après $(fname) et remplace donc son code de retour. Et à cause de cela, le script continue et appelle touchavec 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 que les vrais programmeurs n'utilisent pas les locaux. "

Pour que ce soit bien clair: Sans le local, le script s'interrompt comme prévu lorsqu'une date invalide est entrée.

La solution est de scinder la ligne comme

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

Le comportement étrange est conforme à la documentation de localla page de manuel de bash: "Le statut de retour est 0 sauf si local est utilisé en dehors d'une fonction, si un nom non valide est fourni ou si 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 suis conscient de la séquence d'exécution, localne doit pas masquer une affectation cassée, néanmoins.

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

Hermannk
la source
@mikeserv: j'ai ajouté un exemple pour montrer la pertinence.
Hermannk
@ mikeserv: Oui, vous avez raison. Même terser. Mais le piège est toujours là.
Hermannk
@ mikeserv: Désolé, mon exemple était cassé. J'avais oublié le test dans doit().
Hermannk
Continuons cette discussion sur le chat .
Hermannk
2

La solution actuelle:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

Le groupement d'erreurs ne s'exécutera que si blaun état d'erreur est renvoyé et qu'il exitne se trouve pas dans un sous-shell, le script entier s'arrête.

Walf
la source
1

Les crochets démarrent un sous - shell et la sortie ne quitte que ce sous-shell.

Vous pouvez lire le code de sortie avec $?et l'ajouter 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'
rubo77
la source