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?
()
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.
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.
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.
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'