Perché (uscita 1) non esce dallo script?


48

Ho una sceneggiatura che non esce quando lo voglio.

Uno script di esempio con lo stesso errore è:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Suppongo di vedere l'output:

:~$ ./test.sh
1
:~$

Ma in realtà vedo:

:~$ ./test.sh
1
2
:~$

Il ()comando concatenato crea in qualche modo un ambito? Da cosa exitesce, se non la sceneggiatura?


5
Ciò richiede una sola parola di risposta: subshell
Joshua,

Risposte:


87

()esegue i comandi nella subshell, quindi exitsi esce dalla subshell e si ritorna alla shell padre. Utilizzare le parentesi graffe {}se si desidera eseguire comandi nella shell corrente.

Dal manuale di bash:

(elenco) elenco viene eseguito in un ambiente subshell. Le assegnazioni di variabili e i comandi integrati che influiscono sull'ambiente della shell non rimangono attivi dopo il completamento del comando. Lo stato di ritorno è lo stato di uscita dell'elenco.

{ elenco; } elenco viene semplicemente eseguito nell'attuale ambiente shell. l'elenco deve essere terminato con una nuova riga o punto e virgola. Questo è noto come comando di gruppo. Lo stato di ritorno è lo stato di uscita dell'elenco. Notare che, diversamente dai metacaratteri (e), {e} sono parole riservate e devono apparire quando una parola riservata può essere riconosciuta. Dal momento che non causano un'interruzione di parole, devono essere separati dall'elenco da spazi bianchi o da un altro metacarattere della shell.

Vale la pena ricordare che la sintassi della shell è abbastanza coerente e la subshell partecipa anche ad altri ()costrutti come la sostituzione dei comandi (anche con la `..`sintassi di vecchio stile ) o la sostituzione dei processi, quindi anche i seguenti non usciranno dalla shell corrente:

echo $(exit)
cat <(exit)

Mentre può essere ovvio che i subshells sono coinvolti quando i comandi vengono inseriti esplicitamente all'interno (), il fatto meno visibile è che vengono generati anche in queste altre strutture:

  • comando avviato in background

    exit &

    non esce dalla shell corrente perché (dopo man bash)

    Se un comando viene terminato dall'operatore di controllo &, la shell esegue il comando in background in una subshell. La shell non attende il completamento del comando e lo stato di ritorno è 0.

  • la pipeline

    exit | echo foo

    esce ancora solo dalla subshell.

    Tuttavia diverse shell si comportano diversamente in questo senso. Ad esempio bashmette tutti i componenti della pipeline in sottotitoli separati (a meno che non si usi l' lastpipeopzione nelle invocazioni in cui il controllo del lavoro non è abilitato), ma AT&T kshed zshesegue l'ultima parte all'interno della shell corrente (entrambi i comportamenti sono consentiti da POSIX). così

    exit | exit | exit

    non fa praticamente nulla in bash, ma esce dallo zsh a causa dell'ultimo exit .

  • coproc exitfunziona anche exitin una subshell.


5
Ah. Ora per trovare tutti i posti in cui il mio predecessore ha usato le parentesi graffe sbagliate. Grazie per la comprensione.
Minix,

10
Prendi nota della spaziatura nella pagina man: {e }non sono sintassi, sono parole riservate e devono essere circondate da spazi, e l'elenco deve terminare con un terminatore di comando (punto e virgola, newline, e commerciale)
glenn jackman,

Per interesse, questo tende ad essere effettivamente un altro processo o solo un ambiente separato in uno stack interno? Uso molto () per isolare i chdir e devo essere stato fortunato con il mio uso di $$ etc se il primo.
Dan Sheppard,

5
@DanSheppard È un altro processo, ma (echo $$)stampa l'id della shell padre perché $$viene espanso anche prima della creazione della subshell. In realtà la stampa subshell processo id potrebbe essere difficile, vedere stackoverflow.com/questions/9119885/...
jimmij

@jimmij, Come può essere $$espanso prima della creazione della subshell e tuttavia $BASHPIDmostra il valore corretto per una subshell?
Carattere jolly

13

L'esecuzione di exitin una subshell è una trappola:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Lo script stampa 42, esce dalla subshell con codice di ritorno 1e continua con lo script. Anche sostituire la chiamata con echo $(CALC) || exit 1non aiuta perché il codice di ritorno di echoè 0 indipendentemente dal codice di ritorno di calc. E calcviene eseguito prima di echo.

Ancora più sconcertante è contrastare l'effetto exitavvolgendolo in localbuiltin come nel seguente script. Mi sono imbattuto nel problema quando ho scritto una funzione per verificare un valore di input. Esempio:

Voglio creare un file chiamato "year month day.log", cioè 20141211.logper oggi. La data viene inserita da un utente che potrebbe non fornire un valore ragionevole. Pertanto, nella mia funzione, fnamecontrollo il valore restituito di dateper verificare la validità dell'input dell'utente:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Sembra buono. Lascia che lo script sia chiamato s.sh. Se l'utente chiama lo script con ./s.sh "Thu Dec 11 20:45:49 CET 2014", il file 20141211.logviene creato. Se, tuttavia, l'utente digita ./s.sh "Thu hec 11 20:45:49 CET 2014", quindi lo script genera:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

La riga fname…dice che i dati di input errati sono stati rilevati nella sottostruttura. Ma la exit 1fine della local …riga non viene mai attivata perché la localdirettiva ritorna sempre 0. Questo perché localviene eseguito dopo $(fname) e quindi sovrascrive il suo codice di ritorno. E per questo motivo, lo script continua e invoca touchcon un parametro vuoto. Questo esempio è semplice ma il comportamento di bash può essere abbastanza confuso in una vera applicazione. Lo so, i veri programmatori non usano la gente del posto

Per chiarire: senza il local, lo script si interrompe come previsto quando viene inserita una data non valida.

La correzione è di dividere la linea come

local FNAME
FNAME=$(fname "$1") || exit 1

Lo strano comportamento è conforme alla documentazione localall'interno della pagina man di bash: "Lo stato di ritorno è 0 a meno che non sia usato local al di fuori di una funzione, viene fornito un nome non valido o il nome è una variabile di sola lettura."

Sebbene non sia un bug, ritengo che il comportamento di bash sia controintuitivo. Sono consapevole della sequenza di esecuzione, tuttavia localnon dovrei mascherare un incarico rotto.

La mia risposta iniziale conteneva alcune imprecisioni. Dopo una discussione rivelatrice e approfondita con Mikeserv (grazie per quello) sono andato per risolverli.


@mikeserv: ho aggiunto un esempio per mostrare la pertinenza.
Hermannk,

@mikeserv: Sì, hai ragione. Ancora più terser. Ma la trappola è ancora lì.
Hermannk,

@mikeserv: mi dispiace, il mio esempio è stato rotto. Avevo dimenticato il test doit().
Hermannk,


2

La vera soluzione:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Il raggruppamento degli errori verrà eseguito solo se blarestituisce uno stato di errore e exitnon si trova in una subshell, quindi l'intero script si arresta.


1

Le parentesi iniziano una subshell e l'uscita esce solo da quella subshell.

È possibile leggere il codice di uscita con $?e aggiungerlo nel proprio script per uscire dallo script se la subshell è stata chiusa:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
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.