Por que o set -e não funciona dentro de subshells com parêntesis () seguido por uma lista OR ||?

26

Eu encontrei alguns scripts como este recentemente:

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

Isso parece bom para mim, mas não funciona! O set -e não se aplica quando você adiciona o || . Sem isso, funciona bem:

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

No entanto, se eu adicionar o || , o set -e será ignorado:

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

Usar um shell real e separado funciona conforme o esperado:

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

Eu tentei isso em vários shells diferentes (bash, dash, ksh93) e todos se comportam da mesma maneira, então não é um bug. Alguém pode explicar isso?

    
por MadScientist 20.02.2013 / 22:45

4 respostas

27

De acordo com este tópico , é o comportamento que o POSIX especifica para o uso " set -e "em uma subcamada.

(Eu também fiquei surpreso).

Primeiro, o comportamento:

The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.

As segundas notas post,

In summary, shouldn't set -e in (subshell code) operate independently of the surrounding context?

     

Não. A descrição POSIX é clara que o contexto circundante afeta   se set -e é ignorado em um subshell.

Há um pouco mais no quarto post, também de Eric Blake,

Point 3 is not requiring subshells to override the contexts where set -e is ignored. That is, once you are in a context where -e is ignored, there is nothing you can do to get -e obeyed again, not even a subshell.

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

Even though we called set -e twice (both in the parent and in the subshell), the fact that the subshell exists in a context where -e is ignored (the condition of an if statement), there is nothing we can do in the subshell to re-enable -e.

Esse comportamento é definitivamente surpreendente. É contra-intuitivo: seria de se esperar que a reativação de set -e tivesse um efeito, e que o contexto circundante não precisasse de precedência; Além disso, a redação do padrão POSIX não deixa isso bem claro. Se você lê-lo no contexto em que o comando está falhando, a regra não se aplica: ele só se aplica ao contexto circundante, no entanto, aplica-se a ele completamente.

    
por 21.02.2013 / 03:20
4

De fato, set -e não tem efeito dentro de subshells se você usar || operator depois deles; por exemplo, isso não funcionaria:

#!/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 em sua resposta faz um ótimo trabalho de explicar por que ele se comporta dessa maneira.

Aqui está um pequeno truque que pode ser usado para corrigir isso: execute o comando interno em segundo plano e espere imediatamente por ele. O wait builtin retornará o código de saída do comando interno, e agora você está usando || após wait , não a função interna, portanto set -e funciona corretamente dentro do último:

#!/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

Aqui está a função genérica que se baseia nessa ideia. Ele deve funcionar em todos os shells compatíveis com POSIX se você remover local keywords, ou seja, substituir todos os local x=y por apenas 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
}

Exemplo de uso:

#!/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 "$@"

Executando o exemplo:

$ ./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

A única coisa que você precisa estar ciente ao usar este método é que todas as modificações de variáveis do shell feitas a partir do comando que você passar para run não se propagará para a função de chamada, porque o comando é executado em uma subcaixa.

    
por 11.01.2016 / 17:41
1

Eu não descartaria que é um bug só porque vários shells se comportam dessa maneira. ; -)

Eu tenho mais diversão para oferecer:

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

Posso citar o man bash (4.2.24):

The shell does not exit if the command that fails is [...] part of any command executed in a && or || list except the command following the final && or || [...]

Talvez o eval sobre vários comandos leve a ignorar o || contexto.

    
por 21.02.2013 / 03:00
0

Solução alternativa quando usint toplevel set -e

Eu cheguei a essa pergunta porque estava usando set -e como um método de detecção de erros:

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

e sem || , o script deixaria de ser executado e nunca alcançaria do_more_stuff .

Como não parece haver uma solução limpa, acho que só vou fazer um simples set +e em meus 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
    
por 29.06.2018 / 09:27