quitter le script shell à partir d'un sous-shell

30

Considérez cet extrait:

stop () {
    echo "${1}" 1>&2
    exit 1
}

func () {
    if false; then
        echo "foo"
    else
        stop "something went wrong"
    fi
}

Normalement, quand funcest appelé, le script se terminera, ce qui est le comportement souhaité. Cependant, s'il est exécuté dans un sous-shell, comme dans

result=`func`

il ne quittera pas le script. Cela signifie que le code appelant doit vérifier à chaque fois l'état de sortie de la fonction. Y a-t-il un moyen d'éviter cela? C'est pour ça set -e?

Ernest AC
la source
1
je veux une fonction "stop" qui imprime un message à stderr et arrête le script, mais elle ne s'arrête pas lorsque la fonction qui appelle stop est exécutée dans un sous-shell, comme dans l'exemple
Ernest AC
2
Bien sûr, car il sort du sous-shell et non pas du courant. Il suffit d' appeler directement la fonction: func.
1
je ne peux pas l'appeler directement car il renvoie une chaîne qui doit être stockée dans une variable
Ernest AC
1
@ErnestAC Veuillez fournir tous les détails dans la question d'origine. La fonction ci-dessus ne renvoie pas de chaîne.
1
@htor J'ai changé l'exemple
Ernest AC

Réponses:

10

Vous pourriez tuer le shell d'origine ( kill $$) avant d'appeler exit, et cela fonctionnerait probablement. Mais:

  • ça me semble plutôt moche
  • il se cassera si vous avez un deuxième sous-shell là-dedans, c'est-à-dire, utilisez un sous-shell à l'intérieur d'un sous-shell.

Au lieu de cela, vous pouvez utiliser l'une des nombreuses façons de renvoyer une valeur dans la FAQ Bash . La plupart d'entre eux ne sont pas si grands, malheureusement. Vous pouvez simplement être bloqué à la recherche d'erreurs après chaque appel de fonction ( -ea beaucoup de problèmes ). Soit cela, soit basculez vers Perl.

derobert
la source
5
Merci. Je préfère cependant passer à Python.
Ernest AC
2
Au moment où j'écris, c'est l'année 2019. Dire à quelqu'un de "passer à Perl" est ridicule. Désolé d'être contentieux, mais diriez-vous à quelqu'un frustré par 'C' de passer à Cobol, qui est équivalent à l'OMI? Comme le souligne Ernest, Python est un bien meilleur choix. Ma préférence serait Ruby. Quoi qu'il en soit, tout sauf Perl.
Graham Nicholls
38

Vous pouvez décider que le statut de sortie 77, par exemple, signifie quitter n'importe quel niveau de sous-shell, et faire

set -E
trap '[ "$?" -ne 77 ] || exit 77' ERR

(
  echo here
  (
    echo there
    (
      exit 12 # not 77, exit only this subshell
    )
    echo ici
    exit 77 # exit all subshells
  )
  echo not here
)
echo not here either

set -Een combinaison avec des ERRpièges est un peu comme une version améliorée de set -een ce qu'elle vous permet de définir votre propre gestion des erreurs.

Dans zsh, les interruptions ERR sont héritées automatiquement, vous n'avez donc pas besoin set -E, vous pouvez également définir des interruptions en tant que TRAPERR()fonctions et les modifier $functions[TRAPERR], commefunctions[TRAPERR]="echo was here; $functions[TRAPERR]"

Stéphane Chazelas
la source
1
Solution intéressante! Clairement plus élégant que kill $$.
3
Une chose à surveiller, ce piège ne gérera pas les commandes interpolées, par exemple echo "$(exit 77)"; le script continuera comme si nous avions écritecho ""
Warbo
Intéressant! Y a-t-il de la chance sur (assez ancien) bash qui n'a pas -E? peut-être que nous devons recourir à la définition d'un piège sur un signal USER et à l'utilisation d'un kill sur ce signal? Je vais aussi faire des recherches ...
Olivier Dulac
Comment savoir, quand on n'est pas dans un piège sous-shell, pour retourner 1 au lieu de 77?
ceving
7

Comme alternative à kill $$, vous pouvez également essayer kill 0, cela fonctionnera dans le cas de sous-coquilles imbriquées (tous les appelants et processus secondaires recevront le signal)… mais c'est toujours brutal et laid.

Stéphane Gimenez
la source
2
Est-ce que ce processus de destruction ne serait pas id 0?
Ernest AC
5
Cela tuera tout le groupe de processus. Vous pouvez toucher des choses que vous ne voulez pas (si, par exemple, vous avez commencé certaines choses en arrière-plan).
derobert
2
@ErnestAC voir la page de manuel kill (2), les pids ≤0 ont une signification particulière.
derobert
0

Essaye ça ...

stop () {
    echo "${1}" 1>&2
    exit 1
}

func () {
    if $1; then
        echo "foo"
    else
        stop "something went wrong"
    fi
}

echo "shell..."
func $1

echo "subshell..."
result=`func $1`

echo "shell..."
echo "result=$result"

Les résultats que j'obtiens sont ...

# test_exitsubshell true
shell...
foo
subshell...
shell...
result=foo
# test_exitsubshell false
shell...
something went wrong

Remarques

  • Paramétré pour permettre au iftest d'être trueou false(voir les 2 runs)
  • Lorsque le iftest est terminé false, nous n'atteignons jamais le sous-shell.
DocSalvager
la source
Ceci est très similaire à l'idée originale sur laquelle l'utilisateur postait et disait que cela ne fonctionnait pas. Je ne pense pas que cela fonctionne pour le cas de sous-shell. Votre test utilise de fausses sorties après le cas "shell" et ne parvient jamais au cas de test "subshell". Je crois que cela échouerait dans ce cas puisque le sous-shell sortirait de l'appel "exit 1" mais ne propagerait pas l'erreur vers la coque externe.
stuckj
0

(Réponse spécifique à Bash) Bash n'a aucun concept d'exceptions. Cependant, avec set -o errexit (ou l'équivalent: set -e) au niveau le plus externe, la commande ayant échoué entraînera la sortie du sous-shell avec un état de sortie non nul. S'il s'agit d'un ensemble de sous-coquilles imbriquées sans condition autour de l'exécution de ces sous-coquilles, il «enroulera» efficacement le script entier et quittera.

Cela peut être difficile lorsque vous essayez d'inclure des bits de divers codes bash dans un script plus grand. Un morceau de bash peut très bien fonctionner seul, mais lorsqu'il est exécuté sous errexit (ou sans errexit), se comporte de manière inattendue.

[192.168.13.16 (f0f5e19e) ~ 22:58:22] # bash -o errexit / tmp / foo
quelque chose a mal tourné
[192.168.13.16 (f0f5e19e) ~ 22:58:31] # bash / tmp / foo
quelque chose a mal tourné
Mais on est arrivés quand même
[192.168.13.16 (f0f5e19e) ~ 22:58:37] # cat / tmp / foo
#! / bin / bash
Arrêtez () {
    écho "$ {1}"
    sortie 1
}

si faux; puis
    écho "foo"
autre
    (
        arrêter "quelque chose s'est mal passé"
    )
    echo "Mais nous sommes quand même arrivés ici"
Fi
[192.168.13.16 (f0f5e19e) ~ 22:58:40] #
Brian Chrisman
la source
-2

Mon exemple pour sortir dans une doublure:

COMAND || ( echo "ERROR – executing COMAND, exiting..." ; exit 77 );[ "$?" -eq 77 ] && exit
vicente
la source
1
Cela ne semble pas être une réponse qui fonctionnerait avec la commande exécutée dans un sous-shell, comme demandé par OP ... Cela dit, même si je ne suis pas en désaccord avec les votes négatifs ayant donné la réponse. Les votes négatifs sans commentaire ni raison sont tout aussi inutiles que les mauvaises réponses.
DVS