Jak mogę uzyskać bash, aby wyjść po awarii backtick w podobny sposób jak pipefail?

55

Lubię więc hartować moje skrypty bash, gdzie tylko mogę (a kiedy nie mogę delegować na język taki jak Python / Ruby), aby upewnić się, że błędy nie zostaną przechwycone.

W tym stylu mam plik strict.sh, który zawiera takie rzeczy jak:

set -e
set -u
set -o pipefail

I źródła w innych skryptach. Jednak podczas gdy pipefail odbierze:

false | echo it kept going | true

Nie odbierze:

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

Wynik byłby

Dane wyjściowe to „”

Fałsz zwraca stan niezerowy i no-stdout. W potoku zawiodłoby, ale tutaj błąd nie został wychwycony. Gdy jest to faktycznie obliczenie przechowywane w zmiennej na później, a wartość jest ustawiona na pustą, może to powodować późniejsze problemy.

Więc - czy istnieje sposób, aby bash potraktował niezerowy kod powrotu w backstick jako wystarczający powód do wyjścia?

Danny Staple
źródło

Odpowiedzi:

51

Dokładny język używany w specyfikacji Single UNIX do opisania znaczeniaset -e to:

Gdy ta opcja jest włączona, jeśli proste polecenie nie powiedzie się z jednego z powodów wymienionych w Konsekwencjach błędów powłoki lub zwróci wartość statusu wyjścia> 0 i nie jest [poleceniem warunkowym lub negowanym], wówczas powłoka natychmiast się kończy.

Nie wiadomo, co się stanie, gdy takie polecenie pojawi się w podpowłoce . Z praktycznego punktu widzenia wszystko, co może zrobić podpowłoka, to wyjść i zwrócić niezerowy stan powłoki nadrzędnej. To, czy powłoka nadrzędna z kolei zakończy działanie, zależy od tego, czy ten niezerowy status przekłada się na proste polecenie, które zawodzi w powłoce nadrzędnej.

Jednym z takich problematycznych przypadków jest przypadek, który napotkałeś: niezerowy status powrotu z podstawienia polecenia . Ponieważ ten status jest ignorowany, nie powoduje to zamknięcia powłoki nadrzędnej. Jak już odkryłeś , sposobem wzięcia pod uwagę statusu wyjścia jest użycie podstawienia polecenia w prostym przydziale : wtedy stanem wyjścia przypisania jest status wyjścia ostatniego podstawienia polecenia w przypisaniu (-ach) .

Zauważ, że będzie to działało zgodnie z przeznaczeniem tylko wtedy, gdy istnieje podstawienie pojedynczego polecenia, ponieważ brany jest pod uwagę tylko status ostatniego zastąpienia. Na przykład następujące polecenie jest skuteczne (zarówno zgodnie ze standardem, jak i w każdej implementacji, jaką widziałem):

a=$(false)$(echo foo)

Innym przypadkiem jest, aby obserwować wyraźne podpowłok : (somecommand). Zgodnie z powyższą interpretacją podpowłoka może zwrócić niezerowy status, ale ponieważ nie jest to proste polecenie w powłoce nadrzędnej, powłoka nadrzędna powinna kontynuować. W rzeczywistości wszystkie znane mi powłoki powodują, że rodzic powraca w tym momencie. Chociaż jest to przydatne w wielu przypadkach, na przykład gdy (cd /some/dir && somecommand)nawiasy są używane do utrzymania operacji takiej jak bieżąca zmiana katalogu lokalnego, narusza specyfikację, jeśli set -ejest wyłączona w podpowłoce lub jeśli podpowłoka zwraca niezerowy status w sposób, który nie zakończyłby go, na przykład używając !prawdziwego polecenia. Na przykład wszystkie programy ash, bash, pdksh, ksh93 i zsh wychodzą bez wyświetlania foow następujących przykładach:

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

Jednak żadne proste polecenie nie zadziałało, dopóki nie set -edziałało!

Trzecim problematycznym przypadkiem są elementy niebanalnego rurociągu . W praktyce wszystkie powłoki ignorują awarie elementów rurociągu innych niż ostatni i wykazują jedno z dwóch zachowań dotyczących ostatniego elementu rurociągu:

  • ATT ksh i zsh, które wykonują ostatni element potoku w powłoce nadrzędnej, działają w zwykły sposób: jeśli proste polecenie zawiedzie w ostatnim elemencie potoku, powłoka wykonująca to polecenie, która okazuje się być powłoką nadrzędną, wychodzi
  • Inne powłoki aproksymują zachowanie, wychodząc, jeśli ostatni element potoku zwraca niezerowy status.

Tak jak poprzednio, wyłączenie set -elub użycie negacji w ostatnim elemencie potoku powoduje, że zwraca niezerowy status w sposób, który nie powinien kończyć powłoki; powłoki inne niż ATT ksh i zsh zostaną wtedy zamknięte.

pipefailOpcja Bash powoduje natychmiastowe wyjście potoku pod, set -ejeśli którykolwiek z jego elementów zwraca niezerowy status.

Zauważ, że jako kolejna komplikacja, bash wyłącza się set -ew podpowłokach, chyba że jest w trybie POSIX ( set -o posixlub znajduje się POSIXLY_CORRECTw środowisku, gdy zaczyna się bash).

Wszystko to pokazuje, że specyfikacja POSIX niestety źle radzi sobie z określeniem -eopcji. Na szczęście istniejące powłoki są w większości spójne w swoim zachowaniu.

Gilles „SO- przestań być zły”
źródło
Dzięki za to. Doświadczyłem, że niektóre błędy wychwycone w późniejszej wersji bash zostały zignorowane we wcześniejszych wersjach z zestawem -e. Moim zamiarem jest zahartowanie skryptów w takim stopniu, aby wszelkie nieobsługiwane warunki powrotu / niepowodzenia błędu spowodowały zamknięcie skryptu. Jest to w starszym systemie, o którym wiadomo, że produkuje śmieciowe pliki wyjściowe i szczęśliwy kod wyjścia „0” po pół godzinie chaosu z niewłaściwą env - chyba że oglądałeś wyjście jak jastrząb (i nie wszystkie błędy są na stderr, niektóre na stdout, a niektóre są / dev / null'd), po prostu nie wiesz.
Danny Staple
„Na przykład wszystkie programy ash, bash, pdksh, ksh93 i zsh wychodzą bez wyświetlania foo w poniższych przykładach”: Ash BusyBox nie zachowuje się w ten sposób, wyświetla dane wyjściowe zgodnie ze specyfikacją. (Testowane z BusyBox 1.19.4.) Nie wychodzi również z set -e; (cd /nonexisting).
dubiousjim
27

(Odpowiadając na własne, ponieważ znalazłem rozwiązanie) Jednym z rozwiązań jest zawsze przypisanie tego do zmiennej pośredniej. W ten sposób $?ustawiany jest kod powrotu ( ).

Więc

ABC=`exit 1`
echo $?

Wyjdzie 1(lub zamiast tego wyjdzie, jeśli set -ejest obecny), jednak:

echo `exit 1`
echo $?

Wyjdzie 0po pustej linii. Kod powrotu echa (lub innego polecenia, które uruchomiono z wyjściem wstecznym) zastąpi kod powrotu 0.

Nadal jestem otwarty na rozwiązania, które nie wymagają zmiennej pośredniej, ale to mi trochę pomogło.

Danny Staple
źródło
23

Jak OP wskazał w swojej własnej odpowiedzi, przypisanie wyjścia komendy do zmiennej rozwiązuje problem; $?pozostaje nietknięty.

Jednak jeden przypadek na krawędzi może nadal łamać fałszywe negatywy (tzn. Polecenie nie powiedzie się, ale błąd się nie pojawi), localdeklaracja zmiennej:

local myvar=$(subcommand)będzie zawsze wrócić 0!

bash(1) wskazuje to:

   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.

Oto prosty przypadek testowy:

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

Wyjście:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1
mogsie
źródło
dziękuję, niespójność między mną VAR=...i local VAR=...naprawdę mnie oszołomiła!
Jonny
13

Jak powiedzieli inni, localzawsze zwróci 0. Rozwiązanie polega na zadeklarowaniu zmiennej najpierw:

function testcase()
{
    local MYRESULT

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

    return 0
}

Wynik:

$ testcase
False returned false!
$ 
Będzie
źródło
4

Aby zakończyć działanie po niepowodzeniu podstawienia polecenia, możesz jawnie ustawić -ew podpowłoce:

set -e
x=$(set -e; false; true)
echo "this will never be shown"
errr
źródło
1

Ciekawy punkt!

Nigdy się z tym nie natknąłem, ponieważ nie jestem przyjacielem set -e(wolę raczej trap ... ERR), ale już to przetestowałem: trap ... ERRnie wychwytywaj też błędów $(...)(lub staromodnych backticków).

Myślę, że problemem jest (jak często) to, że tutaj wywoływana jest podpowłoka i -ewyraźnie oznacza bieżącą powłokę.

Jedynym innym rozwiązaniem, które przyszło mi do głowy w tym momencie, byłoby użycie odczytu:

 ls -l ghost_under_bed | read name

To rzuca ERRiz -epociskiem zostanie zakończone. Jedyny problem: działa to tylko w przypadku poleceń z jednym wierszem danych wyjściowych (lub przepuszczasz coś, co łączy linie).

ktf
źródło
1
Nie jestem pewien sztuczki odczytu, próba jej doprowadzenia spowoduje, że zmienna nie zostanie powiązana. Myślę, że może tak być, ponieważ druga strona potoku jest w rzeczywistości podpowłoką, nazwa nie będzie dostępna.
Danny Staple