Come posso ottenere bash per uscire in caso di errore backtick in modo simile a pipefail?


55

Quindi mi piace rafforzare i miei script bash dove posso (e quando non sono in grado di delegare a un linguaggio come Python / Ruby) per garantire che gli errori non vengano rilevati.

In tal senso, ho un strict.sh, che contiene cose come:

set -e
set -u
set -o pipefail

E fonte in altri script. Tuttavia, mentre pipefail riprendeva:

false | echo it kept going | true

Non raccoglierà:

echo The output is '`false; echo something else`' 

L'output sarebbe

L'output è ''

False restituisce uno stato diverso da zero e no-stdout. In una pipe non avrebbe funzionato, ma qui l'errore non viene rilevato. Quando si tratta in realtà di un calcolo memorizzato in una variabile per dopo e il valore è impostato su vuoto, ciò può causare problemi successivi.

Quindi - c'è un modo per ottenere bash per trattare un codice di ritorno diverso da zero all'interno di un backtick come motivo sufficiente per uscire?

Risposte:


51

Il linguaggio esatto utilizzato nella specifica Single UNIX per descrivere il significato diset -e è:

Quando questa opzione è attiva, se un comando semplice non riesce per uno dei motivi elencati in Conseguenze degli errori della shell o restituisce un valore dello stato di uscita> 0 e non è [un comando condizionale o negato], la shell deve uscire immediatamente.

C'è un'ambiguità su cosa succede quando un tale comando si verifica in una subshell . Da un punto di vista pratico, tutto ciò che la subshell può fare è uscire e restituire uno stato diverso da zero alla shell genitore. La chiusura della shell padre dipende dal fatto che questo stato diverso da zero si traduca in un semplice comando che non riesce nella shell padre.

Uno di questi casi problematici è quello che hai riscontrato: uno stato di ritorno diverso da zero da una sostituzione di comando . Poiché questo stato viene ignorato, non provoca la chiusura della shell padre. Come hai già scoperto , un modo per tenere conto dello stato di uscita è utilizzare la sostituzione dei comandi in un compito semplice : lo stato di uscita dell'assegnazione è lo stato di uscita dell'ultima sostituzione di comando nei compiti .

Si noti che ciò funzionerà come previsto solo se esiste una singola sostituzione di comando, poiché viene preso in considerazione solo lo stato dell'ultima sostituzione. Ad esempio, il seguente comando ha esito positivo (sia in base allo standard che in ogni implementazione che ho visto):

a=$(false)$(echo foo)

Un altro caso di guardare per è sottoshell esplicite : (somecommand). Secondo l'interpretazione di cui sopra, la subshell potrebbe restituire uno stato diverso da zero, ma poiché questo non è un semplice comando nella shell padre, la shell padre dovrebbe continuare. In effetti, tutte le shell che conosco fanno tornare il genitore a questo punto. Sebbene ciò sia utile in molti casi, ad esempio (cd /some/dir && somecommand)quando le parentesi vengono utilizzate per mantenere locale un'operazione come una modifica della directory corrente, viola la specifica se set -eè disattivata nella subshell o se la subshell restituisce uno stato diverso da zero in un modo che non lo terminerebbe, ad esempio utilizzando !un comando vero. Ad esempio, tutti i ash, bash, pdksh, ksh93 e zsh escono senza essere visualizzati foonei seguenti esempi:

set -e; (set +e; false); echo "This should be displayed"
set -e; (! true); echo "This should be displayed"

Eppure nessun comando semplice è fallito mentre set -eera in vigore!

Un terzo caso problematico sono gli elementi di una pipeline non banale . In pratica, tutte le shell ignorano i guasti degli elementi della pipeline diversi da quello precedente e presentano uno dei due comportamenti relativi all'ultimo elemento della pipeline:

  • ATT ksh e zsh, che eseguono l'ultimo elemento della pipeline nella shell padre, fanno affari come al solito: se un comando semplice fallisce nell'ultimo elemento della pipeline, la shell che esegue quel comando, che risulta essere la shell padre, uscite.
  • Altre shell approssimano il comportamento uscendo se l'ultimo elemento della pipeline restituisce uno stato diverso da zero.

Come prima, la disattivazione set -eo l'utilizzo di una negazione nell'ultimo elemento della pipeline fa sì che restituisca uno stato diverso da zero in un modo che non dovrebbe terminare la shell; le shell diverse da ATT ksh e zsh usciranno quindi.

L' pipefailopzione di Bash fa uscire immediatamente una pipeline set -ese uno dei suoi elementi restituisce uno stato diverso da zero.

Si noti che come ulteriore complicazione, bash si spegne set -ein subshells a meno che non sia in modalità POSIX ( set -o posixo POSIXLY_CORRECTpresente nell'ambiente all'avvio di bash).

Tutto ciò dimostra che la specifica POSIX purtroppo fa un cattivo lavoro nel specificare l' -eopzione. Fortunatamente, le shell esistenti sono per lo più coerenti nel loro comportamento.


Grazie per questo. Un'esperienza che ho avuto è stata che alcuni errori rilevati in una versione successiva di bash, sono stati ignorati nelle versioni precedenti con set -e. Il mio intento qui è di rafforzare gli script nella misura in cui qualsiasi condizione di restituzione / errore non gestita provocherà la chiusura di uno script. Questo è in un sistema legacy che è stato conosciuto per produrre file di output di immondizia e un codice di uscita felice "0" dopo mezz'ora di caos con l'ambiente errato - a meno che tu non stia guardando l'output come un falco (e non tutti gli errori sono su stderr, alcuni sono su stdout e alcune parti sono / dev / null'd), semplicemente non lo sai.
Danny Staple,

"Ad esempio, tutte le ash, bash, pdksh, ksh93 e zsh escono senza visualizzare foo nei seguenti esempi": BusyBox ash non si comporta in questo modo, ma visualizza l'output secondo le specifiche. (Testato con BusyBox 1.19.4.) Né esce con set -e; (cd /nonexisting).
dubiousjim,

27

(Rispondere alla mia perché ho trovato una soluzione) Una soluzione è quella di assegnare sempre questa a una variabile intermedia. In questo modo $?viene impostato il codice di ritorno ( ).

Così

ABC=`exit 1`
echo $?

Verrà emesso 1(o invece uscirà se set -eè presente), tuttavia:

echo `exit 1`
echo $?

Verrà emesso 0dopo una riga vuota. Il codice di ritorno dell'eco (o altro comando eseguito con l'output backtick) sostituirà il codice di ritorno 0.

Sono ancora aperto a soluzioni che non richiedono la variabile intermedia, ma questo mi dà un po 'di strada.


24

Come ha sottolineato l'OP nella sua risposta, l'assegnazione dell'output del sottocomando a una variabile risolve il problema; l' $?è lasciato indenne.

Tuttavia, un caso limite può ancora confonderti con falsi negativi (cioè il comando fallisce ma l'errore non si riempie), localdichiarazione variabile:

local myvar=$(subcommand)sarà sempre tornare 0!

bash(1) sottolinea questo:

   local [option] [name[=value] ...]
          ... The return status is 0 unless local is used outside a function,
          an invalid name is supplied, or name is a readonly variable.

Ecco un semplice test case:

#!/bin/bash

function test1() {
  data1=$(false) # undeclared variable
  echo 'data1=$(false):' "$?"
  local data2=$(false) # declaring and assigning in one go
  echo 'local data2=$(false):' "$?"
  local data3
  data3=$(false) # assigning a declared variable
  echo 'local data3; data3=$(false):' "$?"
}

test1

L'output:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1

grazie, l'incoerenza tra VAR=...e local VAR=...davvero mi ha confuso!
Jonny

13

Come altri hanno già detto, localrestituirà sempre 0. La soluzione è dichiarare prima la variabile:

function testcase()
{
    local MYRESULT

    MYRESULT=$(false)
    if (( $? != 0 )); then
        echo "False returned false!"
        return 1
    fi

    return 0
}

Produzione:

$ testcase
False returned false!
$ 

4

Per uscire in caso di errore di sostituzione dei comandi, è possibile impostare esplicitamente -euna subshell, in questo modo:

set -e
x=$(set -e; false; true)
echo "this will never be shown"

1

Punto interessante!

Non mi sono mai imbattuto in questo, perché non sono amico di set -e(invece preferisco farlo trap ... ERR) ma l' ho già provato: trap ... ERRinoltre non cogliere errori all'interno $(...)(o nei backtick vecchio stile).

Penso che il problema sia (come spesso) che qui viene chiamata una subshell e -esignifica esplicitamente la shell corrente .

Solo un'altra soluzione che mi è venuta in mente in questo momento sarebbe usare la lettura:

 ls -l ghost_under_bed | read name

Questo genera ERRe con -ela shell verrà terminato. Unico problema: funziona solo per i comandi con una riga di output (o si esegue il pipe attraverso qualcosa che unisce le righe).


1
Non sono sicuro del trucco di lettura, il tentativo di portarlo alla variabile non viene associato. Penso che ciò possa essere dovuto al fatto che l'altro lato della pipe è effettivamente una subshell, il nome non sarà disponibile.
Danny Staple,
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.