Pourquoi set -e ne fonctionne-t-il pas à l'intérieur des sous-coquilles avec des parenthèses () suivies d'une liste OU ||?

30

J'ai récemment rencontré des scripts comme celui-ci:

( set -e ; do-stuff; do-more-stuff; ) || echo failed

Cela me semble bien, mais cela ne fonctionne pas! Le set -ene s'applique pas lorsque vous ajoutez le ||. Sans cela, cela fonctionne très bien:

$ ( set -e; false; echo passed; ); echo $?
1

Cependant, si j'ajoute le ||, le set -eest ignoré:

$ ( set -e; false; echo passed; ) || echo failed
passed

L'utilisation d'un vrai shell séparé fonctionne comme prévu:

$ sh -c 'set -e; false; echo passed;' || echo failed
failed

J'ai essayé cela dans plusieurs shells différents (bash, dash, ksh93) et tous se comportent de la même manière, donc ce n'est pas un bug. Quelqu'un peut-il expliquer cela?

Scientifique fou
la source
La construction `(....)` `démarre un shell séparé pour exécuter son contenu, tous les paramètres qu'elle contient ne s'appliquent pas à l'extérieur.
vonbrand
@vonbrand, vous avez raté le point. Il veut que cela s'applique à l'intérieur du sous-shell, mais l' ||extérieur du sous-shell affecte le comportement à l'intérieur du sous-shell.
cjm
1
Comparer (set -e; echo 1; false; echo 2)avec(set -e; echo 1; false; echo 2) || echo 3
Johan

Réponses:

32

Selon ce fil , c'est le comportement que POSIX spécifie pour utiliser " set -e" dans un sous-shell.

(J'ai également été surpris.)

Tout d'abord, le comportement:

Le -eparamètre doit être ignoré lors de l'exécution de la liste composée à la suite du temps, jusqu'à ce que, si, ou elif mot réservé, un pipeline commençant par! mot réservé ou toute commande d'une liste ET-OU autre que la dernière.

Le deuxième post note,

En résumé, set -e in (code de sous-shell) ne devrait-il pas fonctionner indépendamment du contexte environnant?

Non. La description POSIX indique clairement que le contexte environnant affecte si l'ensemble -e est ignoré dans un sous-shell.

Il y a un peu plus dans le quatrième post, également par Eric Blake,

Le point 3 n'exige pas que les sous-coquilles remplacent les contextes où set -eest ignoré. Autrement dit, une fois que vous êtes dans un contexte où il -eest ignoré, vous ne pouvez plus rien faire pour vous faire -eobéir à nouveau, pas même un sous-shell.

$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' 
hi 
0 

Même si nous avons appelé set -edeux fois (à la fois dans le parent et dans le sous-shell), le fait que le sous-shell existe dans un contexte où -eest ignoré (la condition d'une instruction if), nous ne pouvons rien faire dans le sous-shell pour réactiver -e.

Ce comportement est certainement surprenant. C'est contre-intuitif: on s'attendrait à ce que la réactivation de set -eait un effet, et que le contexte environnant ne prenne pas le pas; en outre, le libellé de la norme POSIX ne le rend pas particulièrement clair. Si vous la lisez dans le contexte où la commande échoue, la règle ne s'applique pas: elle ne s'applique que dans le contexte environnant, cependant, elle s'y applique complètement.

Aaron D. Marasco
la source
Merci pour ces liens, ils étaient très intéressants. Cependant, mon exemple est (OMI) substantiellement différent. La plupart de cette discussion est de savoir si ensemble -e dans une coquille mère est héritée par le sous - shell: set -e; (false; echo passed;) || echo failed. Cela ne m'étonne pas, en fait, que -e soit ignoré dans ce cas étant donné le libellé de la norme. Dans mon cas, cependant, je définis explicitement -e dans le sous-shell et je m'attends à ce que le sous-shell se termine en cas d'échec. Il n'y a pas de liste ET-OU dans le sous-shell ...
MadScientist
Je ne suis pas d'accord. Le deuxième article (je n'arrive pas à faire fonctionner les ancres) dit " La description POSIX est claire que le contexte environnant affecte si l'ensemble -e est ignoré dans un sous-shell. " - le sous-shell est dans la liste AND-OR.
Aaron D. Marasco
Le quatrième article (également Erik Blake) dit également " Même si nous avons appelé set -e deux fois (à la fois dans le parent et dans le sous-shell), le fait que le sous-shell existe dans un contexte où -e est ignoré (la condition d'un if ), nous ne pouvons rien faire dans le sous-shell pour réactiver -e. "
Aaron D. Marasco
Tu as raison; Je ne sais pas comment je les ai mal lues. Merci.
MadScientist
1
Je suis ravi d'apprendre que ce comportement que j'arrache les cheveux se révèle être conforme aux spécifications POSIX. Alors, quel est le travail autour?! ifet ||et &&sont infectieux? c'est absurde
Steven Lu
7

En effet, set -en'a aucun effet à l'intérieur des sous-coquilles si vous utilisez l' ||opérateur après eux; par exemple, cela ne fonctionnerait pas:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aaron D. Marasco dans sa réponse explique très bien pourquoi il se comporte de cette façon.

Voici une petite astuce qui peut être utilisée pour résoudre ce problème: exécutez la commande interne en arrière-plan, puis attendez-la immédiatement. La fonction waitintégrée renverra le code de sortie de la commande interne, et maintenant vous utilisez ||après wait, pas la fonction interne, donc set -efonctionne correctement à l'intérieur de cette dernière:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Voici la fonction générique qui s'appuie sur cette idée. Cela devrait fonctionner dans tous les shells compatibles POSIX si vous supprimez des localmots clés, c'est-à-dire remplacez tout local x=ypar juste x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Exemple d'utilisation:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Exécuter l'exemple:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

La seule chose dont vous devez être conscient lorsque vous utilisez cette méthode est que toutes les modifications des variables Shell effectuées à partir de la commande à laquelle vous passez runne se propageront pas à la fonction appelante, car la commande s'exécute dans un sous-shell.

skozin
la source
2

Je n'exclurais pas que ce soit un bug simplement parce que plusieurs obus se comportent de cette façon. ;-)

J'ai plus de plaisir à offrir:

start cmd:> ( eval 'set -e'; false; echo passed; ) || echo failed
passed

start cmd:> ( eval 'set -e; false'; echo passed; ) || echo failed
failed

start cmd:> ( eval 'set -e; false; echo passed;' ) || echo failed
failed

Puis-je citer man bash (4.2.24):

Le shell ne se ferme pas si la commande qui échoue fait [...] partie d'une commande exécutée dans un && ou || liste à l'exception de la commande suivant le && ou || final [...]

Peut-être que l'évaluation sur plusieurs commandes conduit à ignorer le || le contexte.

Hauke ​​Laging
la source
Eh bien, si tous les shells se comportent de cette façon, ce n'est par définition pas un bug ... c'est un comportement standard :-). On peut déplorer le comportement non intuitif mais ... L'astuce avec eval est très intéressante, c'est sûr.
MadScientist
Quelle coquille utilisez-vous? L' evalastuce ne fonctionne pas pour moi. J'ai essayé bash, bash en mode posix et dash.
Dunatotatos
@Dunatotatos, comme l'a dit Hauke, c'était bash4.2. Il a été "corrigé" dans bash4.3. Les shells basés sur pdksh auront le même "problème". Et plusieurs versions de plusieurs shells ont toutes sortes de "problèmes" différents set -e. set -eest cassé par conception. Je ne l'utiliserais pas pour autre chose que le plus simple des scripts shell sans structures de contrôle, sous-shell ou substitutions de commandes.
Stéphane Chazelas
1

Solution de contournement lorsque usint toplevel set -e

Je suis venu à cette question parce que j'utilisais set -ecomme méthode de détection d'erreur:

/usr/bin/env bash
set -e
do_stuff
( take_best_sub_action_1; take_best_sub_action_2 ) || do_worse_fallback
do_more_stuff

et sans ||, le script cesserait de fonctionner et n'atteindrait jamais do_more_stuff.

Puisqu'il ne semble pas y avoir de solution propre, je pense que je vais simplement faire un simple set +esur mes scripts:

/usr/bin/env bash
set -e
do_stuff
set +e
( take_best_sub_action_1; take_best_sub_action_2 )
exit_status=$?
set -e
if [ "$exit_status" -ne 0 ]; then
  do_worse_fallback
fi
do_more_stuff
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
la source