¿Por qué set -e no funciona dentro de subcapas con paréntesis () seguido de una lista OR ||?


30

Me he encontrado con algunas secuencias de comandos como esta recientemente:

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

Esto me parece bien, ¡pero no funciona! El set -eno se aplica, cuando se agrega el ||. Sin eso, funciona bien:

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

Sin embargo, si agrego el ||, set -ese ignora el:

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

El uso de un shell real separado funciona como se esperaba:

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

He intentado esto en varios shells diferentes (bash, dash, ksh93) y todos se comportan de la misma manera, por lo que no es un error. ¿Alguien puede explicar esto?


La construcción `(...)` `comienza un shell separado para ejecutar su contenido, cualquier configuración en él no se aplica fuera.
vonbrand

@vonbrand, te perdiste el punto. Quiere que se aplique dentro de la subshell, pero el ||exterior de la subshell afecta el comportamiento dentro de la subshell.
cjm

1
Comparar (set -e; echo 1; false; echo 2)con(set -e; echo 1; false; echo 2) || echo 3
Johan

Respuestas:


32

De acuerdo con este hilo , es el comportamiento que POSIX especifica para usar " set -e" en una subshell.

(También me sorprendió).

Primero, el comportamiento:

La -econfiguración se ignorará cuando se ejecute la lista compuesta siguiendo el tiempo, hasta, si, o si se reserva la palabra, una tubería que comience con! palabra reservada, o cualquier comando de una lista AND-OR que no sea la última.

Las notas del segundo post,

En resumen, ¿no debería establecer -e in (código de subshell) operar independientemente del contexto circundante?

No. La descripción de POSIX es clara de que el contexto circundante afecta si el conjunto -e se ignora en una subshell.

Hay un poco más en el cuarto post, también de Eric Blake,

El punto 3 no requiere subcapas para anular los contextos donde set -ese ignora. Es decir, una vez que se encuentra en un contexto donde -ese ignora, no hay nada que pueda hacer para ser -eobedecido nuevamente, ni siquiera una subshell.

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

Aunque llamamos set -edos veces (tanto en el padre como en el subshell), el hecho de que el subshell exista en un contexto donde -ese ignora (la condición de una instrucción if), no hay nada que podamos hacer en el subshell para volver a habilitar -e.

Este comportamiento es definitivamente sorprendente. Es contrario a la intuición: uno esperaría que la reactivación set -etuviera un efecto, y que el contexto circundante no tendría precedentes; Además, la redacción del estándar POSIX no deja esto particularmente claro. Si lo lee en el contexto donde el comando falla, la regla no se aplica: solo se aplica en el contexto circundante, sin embargo, se aplica por completo.


Gracias por esos enlaces, fueron muy interesantes. Sin embargo, mi ejemplo es (IMO) sustancialmente diferente. La mayor parte de esa discusión es si set -e en una cáscara de los padres es heredado por la subcapa: set -e; (false; echo passed;) || echo failed. No me sorprende, en realidad, que -e se ignore en este caso dada la redacción de la norma. Sin embargo, en mi caso, configuro explícitamente -e en la subshell y espero que la subshell salga en caso de falla. No hay una lista AND-OR en el subshell ...
MadScientist

Estoy en desacuerdo. La segunda publicación (no puedo hacer que funcionen las anclas) dice " La descripción de POSIX es clara que el contexto circundante afecta si el conjunto -e se ignora en una subshell ". La subshell está en la lista AND-OR.
Aaron D. Marasco

La cuarta publicación (también Erik Blake) también dice " Aunque llamamos a set -e dos veces (tanto en el padre como en la subshell), el hecho de que la subshell existe en un contexto donde -e se ignora (la condición de un if ), no hay nada que podamos hacer en la subshell para volver a habilitar -e " .
Aaron D. Marasco

Tienes razón; No estoy seguro de cómo leí mal esos. Gracias.
MadScientist

1
Estoy encantado de saber que este comportamiento por el que me estoy arrancando el cabello resulta estar en la especificación POSIX. Entonces, ¿cuál es el trabajo? ify ||y &&son infecciosas? esto es absurdo
Steven Lu

7

De hecho, set -eno tiene ningún efecto dentro de las subcapas si usa el ||operador después de ellas; por ejemplo, esto no funcionaría:

#!/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 en su respuesta hace un gran trabajo al explicar por qué se comporta de esta manera.

Aquí hay un pequeño truco que puede usarse para solucionar esto: ejecute el comando interno en segundo plano y luego espere inmediatamente. El waitincorporado devolverá el código de salida del comando interno, y ahora está usando ||después wait, no la función interna, por lo que set -efunciona correctamente dentro de este ú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

Aquí está la función genérica que se basa en esta idea. Debería funcionar en todos los shells compatibles con POSIX si elimina las localpalabras clave, es decir, reemplace todo local x=ycon solo 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
}

Ejemplo 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 "$@"

Ejecutando el ejemplo:

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

Lo único que debe tener en cuenta al usar este método es que todas las modificaciones de las variables de Shell realizadas desde el comando al que pasa runno se propagarán a la función de llamada, porque el comando se ejecuta en una subshell.


2

No descartaría que sea un error solo porque varios proyectiles se comportan de esa manera. ;-)

Tengo más diversión para ofrecer:

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

¿Puedo citar a man bash (4.2.24):

El shell no sale si el comando que falla es [...] parte de cualquier comando ejecutado en un && o || lista excepto el comando que sigue al final && o || [...]

Quizás la evaluación sobre varios comandos lleva a ignorar el || contexto.


Bueno, si todos los shells se comportan de esa manera, por definición no es un error ... es un comportamiento estándar :-). Podemos lamentar el comportamiento como no intuitivo, pero ... El truco con eval es muy interesante, eso es seguro.
MadScientist

¿Qué caparazón usas? El evaltruco no me funciona. Intenté bash, bash en modo posix y dash.
Dunatotatos

@Dunatotatos, como dijo Hauke, eso fue bash4.2. Fue "arreglado" en bash4.3. Los shells basados ​​en pdksh tendrán el mismo "problema". Y varias versiones de varios shells tienen todo tipo de "problemas" diferentes set -e. set -eEstá roto por diseño. No lo usaría para nada más que los scripts de shell más simples sin estructuras de control, subcapas o sustituciones de comandos.
Stéphane Chazelas

1

Solución alternativa cuando usint toplevel set -e

Llegué a esta pregunta porque estaba usando set -ecomo método de detección de errores:

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

y sin ||, el script dejaría de ejecutarse y nunca llegaría do_more_stuff.

Como parece que no hay una solución limpia, creo que solo haré un simple set +een mis 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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.