Perché set -e non funziona all'interno di subshells con parentesi () seguito da un elenco OR ||?


30

Ho incontrato alcuni script come questo di recente:

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

Questo mi sembra perfetto, ma non funziona! Il set -enon si applica quando si aggiunge il ||. Senza quello, funziona benissimo:

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

Tuttavia, se aggiungo il ||, set -eviene ignorato:

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

L'uso di una shell separata e reale funziona come previsto:

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

Ho provato questo in più shell diverse (bash, dash, ksh93) e si comportano tutti allo stesso modo, quindi non è un bug. Qualcuno può spiegare questo?


Il costrutto `(....)` `avvia una shell separata per eseguirne il contenuto, tutte le impostazioni in essa contenute non si applicano all'esterno.
vonbrand,

@Vonbrand, hai perso il punto. Vuole che si applichi all'interno della subshell, ma l' ||esterno della subshell influisce sul comportamento all'interno della subshell.
cjm

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

Risposte:


32

Secondo questo thread , è il comportamento che POSIX specifica per l'utilizzo di " set -e" in una sottoshell.

(Sono stato anche sorpreso.)

Innanzitutto, il comportamento:

L' -eimpostazione deve essere ignorata quando si esegue l'elenco composto dopo la parola riservata while, if, if o elif, una pipeline che inizia con! parola riservata o qualsiasi comando di un elenco AND-OR diverso dall'ultimo.

Le note del secondo post,

In sintesi, impostare -e in (codice subshell) non dovrebbe operare indipendentemente dal contesto circostante?

No. La descrizione POSIX è chiara che il contesto circostante influisce sul fatto che set -e sia ignorato in una subshell.

C'è un po 'di più nel quarto post, anche di Eric Blake,

Il punto 3 non richiede che i subshells superino i contesti in cui set -eviene ignorato. Cioè, una volta che ti trovi in ​​un contesto in cui -eviene ignorato, non c'è nulla che tu possa fare per -eobbedire di nuovo, nemmeno una subshell.

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

Anche se abbiamo chiamato set -edue volte (sia nel genitore che nella subshell), il fatto che la subshell esista in un contesto in cui -eviene ignorata (la condizione di un'istruzione if), non è possibile fare nulla nella subshell per riabilitare -e.

Questo comportamento è decisamente sorprendente. È controintuitivo: ci si aspetterebbe che la riabilitazione set -eabbia un effetto e che il contesto circostante non abbia precedenti; inoltre, la formulazione dello standard POSIX non lo rende particolarmente chiaro. Se lo leggi nel contesto in cui il comando non riesce, la regola non si applica: si applica solo nel contesto circostante, tuttavia, si applica completamente ad esso.


Grazie per quei link, erano molto interessanti. Tuttavia, il mio esempio è (IMO) sostanzialmente diverso. La maggior parte di tale discussione è se -e insieme in un guscio di genitore è ereditato dalla subshell: set -e; (false; echo passed;) || echo failed. Non mi sorprende, in realtà, che in questo caso -e venga ignorato dato il testo dello standard. Nel mio caso, tuttavia, sto impostando esplicitamente -e nella subshell e mi aspetto che la subshell si chiuda in caso di errore. Non esiste un elenco AND-OR nella sottostruttura ...
MadScientist,

Non sono d'accordo. Il secondo post (Non riesco a far funzionare le ancore) dice " La descrizione di POSIX è chiara che il contesto circostante influenza se set -e viene ignorato in una subshell. " - La subshell è nella lista AND-OR.
Aaron D. Marasco,

Il quarto post (anche Erik Blake) dice anche " Anche se abbiamo chiamato set -e due volte (sia nel genitore che nella subshell), il fatto che la subshell esiste in un contesto in cui -e viene ignorata (la condizione di un if ), non c'è niente che possiamo fare nella subshell per riattivare -e. "
Aaron D. Marasco,

Hai ragione; Non sono sicuro di come ho letto male quelli. Grazie.
MadScientist,

1
Sono lieto di apprendere che questo comportamento su cui mi sto strappando i capelli risulta essere nelle specifiche POSIX. Allora, qual è il lavoro in giro ?! ife ||e &&sono contagiosi? questo è assurdo
Steven Lu

7

In effetti, set -enon ha alcun effetto all'interno dei sottotitoli se si utilizza l' ||operatore dopo di essi; ad esempio, questo non funzionerebbe:

#!/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 nella sua risposta fa un ottimo lavoro nel spiegare perché si comporta in questo modo.

Ecco un piccolo trucco che può essere utilizzato per risolvere questo problema: esegui il comando interno in background, quindi attendi immediatamente. Il waitbuiltin restituirà il codice di uscita del comando interno e ora stai usando ||dopowait , non la funzione interna, quindi set -efunziona correttamente all'interno di quest'ultimo:

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

Ecco la funzione generica che si basa su questa idea. Dovrebbe funzionare in tutte le shell compatibili con POSIX se rimuovete le localparole chiave, ovvero sostituite tutte 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
}

Esempio di utilizzo:

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

Eseguendo l'esempio:

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

L'unica cosa di cui devi essere consapevole quando usi questo metodo è che tutte le modifiche delle variabili Shell fatte dal comando che passi runnon si propagheranno alla funzione chiamante, perché il comando viene eseguito in una subshell.


2

Non escluderei che sia un bug solo perché diverse shell si comportano in questo modo. ;-)

Mi diverto di più da offrire:

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 citare da man bash (4.2.24):

La shell non si chiude se il comando che fallisce fa parte [...] di qualsiasi comando eseguito in un && o || elenco tranne il comando che segue l'ultimo && o || [...]

Forse l'eval su diversi comandi porta a ignorare il || contesto.


Bene, se tutte le shell si comportano in questo modo non è per definizione un bug ... è un comportamento standard :-). Potremmo lamentarci del comportamento come non intuitivo ma ... Il trucco con eval è molto interessante, questo è certo.
MadScientist,

Che shell usi? Il evaltrucco non funziona per me. Ho provato bash, bash in modalità posix e trattino.
Dunatotatos,

@Dunatotatos, come ha detto Hauke, era bash4.2. È stato "riparato" in bash4.3. Le shell basate su pdksh avranno lo stesso "problema". E diverse versioni di diverse shell presentano ogni sorta di "problemi" diversi set -e. set -eè rotto dal design. Non lo userei per nient'altro che il più semplice degli script di shell senza strutture di controllo, subshells o sostituzioni di comandi.
Stéphane Chazelas,

1

Soluzione alternativa quando si utilizza il livello superiore set -e

Sono arrivato a questa domanda perché stavo usando set -ecome metodo di rilevamento degli errori:

/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 senza ||, la sceneggiatura smetterebbe di funzionare e non raggiungerebbe mai do_more_stuff.

Dal momento che non sembra esserci una soluzione pulita, penso che farò solo una semplice set +e sui miei script:

/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.