Wie kann ich Bash dazu bringen, bei einem Backtick-Fehler auf ähnliche Weise wie Pipefail zu beenden?

55

Deshalb mag ich es, meine Bash-Skripte zu härten, wo immer ich kann (und wenn ich nicht in der Lage bin, sie an eine Sprache wie Python / Ruby zu delegieren), um sicherzustellen, dass Fehler nicht unbeachtet bleiben.

In diesem Sinne habe ich eine strict.sh, die Dinge enthält wie:

set -e
set -u
set -o pipefail

Und Quell es in anderen Skripten. Während Pipefail jedoch abholen würde:

false | echo it kept going | true

Es wird nicht abholen:

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

Die Ausgabe wäre

Die Ausgabe ist ''

False gibt den Status ungleich Null und no-stdout zurück. In einer Pipe wäre es fehlgeschlagen, aber hier wird der Fehler nicht abgefangen. Wenn dies tatsächlich eine Berechnung ist, die für später in einer Variablen gespeichert wird, und der Wert auf leer gesetzt ist, kann dies zu späteren Problemen führen.

Also - gibt es eine Möglichkeit, bash zu veranlassen, einen Returncode ungleich Null in einem Backtick als Grund genug für das Beenden zu behandeln?

Danny Staple
quelle

Antworten:

51

Die genaue Sprache, die in der Single UNIX-Spezifikation zur Beschreibung der Bedeutung von verwendet wird,set -e lautet:

Wenn diese Option aktiviert ist und ein einfacher Befehl aus einem der in Folgen von Shell-Fehlern aufgeführten Gründe fehlschlägt oder einen Beendigungsstatuswert> 0 zurückgibt und nicht [ein bedingter oder negierter Befehl] ist, wird die Shell sofort beendet.

Es besteht eine Unklarheit darüber, was passiert, wenn ein solcher Befehl in einer Subshell auftritt . Aus praktischer Sicht kann die Subshell nur beendet werden und einen Status ungleich Null an die übergeordnete Shell zurückgeben. Ob die übergeordnete Shell beendet wird, hängt davon ab, ob dieser Nicht-Null-Status zu einem einfachen Befehl führt, der in der übergeordneten Shell fehlschlägt.

Ein solcher problematischer Fall ist der, auf den Sie gestoßen sind: ein Rückgabestatus ungleich Null von einer Befehlsersetzung . Da dieser Status ignoriert wird, wird die übergeordnete Shell nicht beendet. Wie Sie bereits festgestellt haben , können Sie den Exit-Status berücksichtigen, indem Sie die Befehlsersetzung in einer einfachen Zuweisung verwenden : Der Exit-Status der Zuweisung ist der Exit-Status der letzten Befehlsersetzung in der (den) Zuweisung (en) .

Beachten Sie, dass dies nur dann wie vorgesehen ausgeführt wird, wenn es eine einzelne Befehlsersetzung gibt, da nur der Status der letzten Ersetzung berücksichtigt wird. Der folgende Befehl ist beispielsweise erfolgreich (sowohl gemäß dem Standard als auch in jeder Implementierung, die ich gesehen habe):

a=$(false)$(echo foo)

Ein weiterer Fall zu beachten gilt ist ausdrücklich Subshells : (somecommand). Gemäß der obigen Interpretation gibt die Subshell möglicherweise einen Status ungleich Null zurück. Da dies jedoch kein einfacher Befehl in der übergeordneten Shell ist, sollte die übergeordnete Shell fortgesetzt werden. Tatsächlich lassen alle mir bekannten Muscheln die Eltern zu diesem Zeitpunkt zurückkehren. Dies ist in vielen Fällen hilfreich, z. B. (cd /some/dir && somecommand)wenn die Klammern verwendet werden, um einen Vorgang wie einen aktuellen Verzeichniswechsel lokal zu halten. Es verstößt jedoch gegen die Spezifikation, wenn set -ein der Subshell deaktiviert ist oder wenn die Subshell auf eine Weise einen Nicht-Null-Status zurückgibt würde es nicht beenden, z. B. mit !einem wahren Befehl. Zum Beispiel werden alle Befehle ash, bash, pdksh, ksh93 und zsh ohne Anzeige fooin den folgenden Beispielen beendet:

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

Doch kein einfacher Befehl ist fehlgeschlagen, solange er set -ein Kraft war!

Ein dritter problematischer Fall sind Elemente in einer nichttrivialen Pipeline . In der Praxis ignorieren alle Shells Ausfälle der Elemente der Pipeline mit Ausnahme des letzten und zeigen in Bezug auf das letzte Pipelineelement eines von zwei Verhaltensweisen:

  • ATT ksh und zsh, die das letzte Element der Pipeline in der übergeordneten Shell ausführen, verhalten sich wie gewohnt: Wenn ein einfacher Befehl im letzten Element der Pipeline fehlschlägt, führt die Shell diesen Befehl aus, der zufällig die übergeordnete Shell ist. Ausgänge.
  • Andere Shells approximieren das Verhalten, indem sie beendet werden, wenn das letzte Element der Pipeline einen Status ungleich Null zurückgibt.

Wie zuvor set -ebewirkt das Deaktivieren oder Verwenden einer Negation im letzten Element der Pipeline, dass der Status ungleich Null zurückgegeben wird, sodass die Shell nicht beendet wird. Andere Shells als ATT ksh und zsh werden dann beendet.

Die pipefailOption von Bash bewirkt, dass eine Pipeline sofort beendet wird, set -ewenn eines ihrer Elemente einen Nicht-Null-Status zurückgibt.

Beachten Sie, dass Bash als weitere Komplikation set -ein Subshells deaktiviert wird, es sei denn, es befindet sich im POSIX-Modus ( set -o posixoder POSIXLY_CORRECTin der Umgebung, wenn Bash gestartet wird ).

All dies zeigt, dass die POSIX-Spezifikation bei der Angabe der -eOption leider einen schlechten Job macht . Glücklicherweise sind die vorhandenen Muscheln in ihrem Verhalten größtenteils konsistent.

Gilles 'SO - hör auf böse zu sein'
quelle
Danke dafür. Eine Erfahrung, die ich gemacht habe, war, dass einige Fehler, die in einer späteren Version von bash entdeckt wurden, in früheren Versionen mit set -e ignoriert wurden. Ich beabsichtige hier, Skripte so zu härten, dass nicht behandelte Fehlerrückgabe- / Fehlerzustände dazu führen, dass ein Skript beendet wird. Dies ist ein Altsystem, von dem bekannt ist, dass es nach einer halben Stunde Chaos mit der falschen Umgebung Garbage-Output-Dateien und einen "0" fröhlichen Exit-Code erzeugt - es sei denn, Sie haben die Ausgabe wie ein Falke angesehen (und nicht alle Fehler) sind auf stderr, einige sind auf stdout und einige Teile sind / dev / null'd, Sie wissen es einfach nicht.
Danny Staple
"In den folgenden Beispielen werden z. B. alle Elemente ash, bash, pdksh, ksh93 und zsh ohne Anzeige von foo beendet.": BusyBox ash verhält sich nicht so, sondern zeigt die Ausgabe gemäß Spezifikation an. (Getestet mit BusyBox 1.19.4.) Es wird auch nicht mit beendet set -e; (cd /nonexisting).
dubiousjim
27

(Ich beantworte meine Frage, weil ich eine Lösung gefunden habe.) Eine Lösung besteht darin, diese immer einer Zwischenvariablen zuzuweisen. Auf diese Weise wird der Returncode ( $?) gesetzt.

Damit

ABC=`exit 1`
echo $?

Wird jedoch ausgegeben 1(oder stattdessen beendet, wenn set -evorhanden):

echo `exit 1`
echo $?

Wird 0nach einer Leerzeile ausgegeben . Der Rückkehrcode des Echos (oder eines anderen Befehls, der mit der Backtick-Ausgabe ausgeführt wurde) ersetzt den Rückkehrcode 0.

Ich bin immer noch offen für Lösungen, für die keine Zwischenvariable erforderlich ist, aber das hilft mir ein bisschen.

Danny Staple
quelle
23

Wie das OP in seiner eigenen Antwort ausgeführt hat, löst das Zuweisen der Ausgabe des Unterbefehls zu einer Variablen das Problem. das $?bleibt unversehrt.

Ein Randfall kann Sie jedoch immer noch mit falschen Negativen verwirren (dh der Befehl schlägt fehl, aber der Fehler tritt nicht auf), localVariablendeklaration:

local myvar=$(subcommand)Wir werden immer wiederkommen 0!

bash(1) weist darauf hin:

   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.

Hier ist ein einfacher Testfall:

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

Die Ausgabe:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1
mogsie
quelle
danke, die inkonsistenz zwischen VAR=...und local VAR=...hat mich so richtig durcheinander gebracht!
Jonny
13

Wie andere gesagt haben, localwird immer 0 zurückgegeben. Die Lösung besteht darin, die Variable zuerst zu deklarieren:

function testcase()
{
    local MYRESULT

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

    return 0
}

Ausgabe:

$ testcase
False returned false!
$ 
Wille
quelle
4

Um bei einem Fehler -ebei der Befehlsersetzung zu beenden, können Sie Folgendes explizit in einer Subshell festlegen:

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

Interessanter Punkt!

Ich bin noch nie darauf gestoßen, weil ich kein Freund von set -e(stattdessen ziehe ich es vor trap ... ERR) bin, aber das bereits getestet habe: trap ... ERRFang auch keine Fehler in $(...)(oder den altmodischen Backticks).

Ich denke das Problem ist (wie so oft), dass hier eine Subshell aufgerufen wird und -eexplizit die aktuelle Shell bedeutet.

Die einzige andere Lösung, die in diesem Moment in den Sinn kam, wäre read:

 ls -l ghost_under_bed | read name

Dies wirft ERRund mit -eder Shell wird beendet. Einziges Problem: Dies funktioniert nur für Befehle mit einer Ausgabezeile (oder Sie leiten durch etwas, das Zeilen verbindet).

ktf
quelle
1
Wenn Sie sich nicht sicher sind, was den Lesetrick angeht, führt dies dazu, dass die Variable nicht gebunden wird. Ich denke, das kann daran liegen, dass die andere Seite der Pipe tatsächlich eine Unterschale ist. Der Name wird nicht verfügbar sein.
Danny Staple