Jak zweryfikować status poleceń w pipe

Spis treści

Ja zbytnio się nie nadaję na programistę ale czasem jakieś trzeciorzędne skrypty nawet potrafię napisać. Problem ze skryptami jest taki, że mogą one nie do końca działać jak należy. Zwykle w takich przypadkach, skrypt zwraca jakiś kod wyjścia. Z reguły też jest on inny od 0, który z kolei oznacza, że skrypt został wykonany prawidłowo. Załóżmy teraz, że wywołujemy szereg poleceń. Każde z nich przekierowuje swoje wyjście na wejście innego polecenia przy pomocy znaku | (pipe) . Jak w takim przypadku ustalić czy wszystkie z tych poleceń w łańcuchu wykonały się poprawnie? W tym wpisie postaramy się odpowiedzieć na to pytanie.

Sprawdzenie statusu przykładowego polecenia

Każde wykonane polecenie zwraca jakiś kod wyjścia. Ten kod można odczytać przy pomocy $? . Odpalmy sobie zatem terminal i wpiszmy w nim jakieś polecenie, które się wykona z powodzeniem:

$ echo "test" > /dev/null

$ echo $?
0

Dla porównania wpiszmy także polecenie, które zwróci jakiś błąd:

$ echo "test" > /etc/hosts
bash: permission denied: /etc/hosts

$ echo $?
1

W ten sposób można łatwo określić, czy dane polecenie wykonało się poprawnie. Niemniej jednak, nie możemy z tego sposobu skorzystać bezpośrednio w przypadku pipe.

Sprawdzenie kodu wyjścia poleceń w pipe

Weźmy sobie przykład skryptu, w którym mamy szereg poleceń wywoływanych w opisany we wstępie sposób. Dla przykładu niech to będzie ten poniższy kawałek kodu:

#!/bin/bash
...
cat /etc/peerblock/$set.gz |
gunzip |
cut -d: -f2 |
grep -E "^[-0-9.]+$" |
gawk -v my_set=$set '{print "add " my_set " " $1}' |
$ips --restore -exist;

Sam skrypt został nieco skrócony dla czytelności. Widzimy wyżej 5 znaków | . To są właśnie nasze pipe, przy pomocy których kierujemy wyjście jednego polecenia na wejście kolejnego polecenia. Wszystkie polecenia w tym łańcuchu są traktowane indywidualnie, tj. każde z nich ma własny kod wyjścia. Gdybyśmy teraz chcieli sprawdzić przy pomocy $? status tego całego łańcucha, to zostanie nam zwrócony tylko kod ostatniego polecenia. W ten sposób nie dowiemy się czy, np. drugie polecenie zakończyło się sukcesem i nie zwróciło żadnych błędów. Oczywiście można posiłkować się komunikatami zwracanymi na konsoli ale w przypadku skryptów, gdzie jedne polecenia są oparte o inne, nie zawsze nam takie wiadomości mogą pomóc.

Opcja pipefail

W przypadku shell'ów bash i zsh mamy dość ułatwione zadanie sprawdzenia kodu wyjścia wszystkich poleceń w pipe. Możemy w tych shell'ach ustawić opcję pipefail, która zwróci status 0 w przypadku, gdy żadne z poleceń w pipe nie zwróciło błędów. Uzupełnijmy zatem powyższy skrypt o w/w opcję i postarajmy się zweryfikować status wyjścia całego polecenia:

#!/bin/bash
...
set -o pipefail

cat /etc/peerblock/$set.gz |
gunzip |
cut -d: -f2 |
grep -E "^[-0-9.]+$" |
gawk -v my_set=$set '{print "add " my_set " " $1}' |
$ips --restore -exist;

if [ "$?" -ne "0" ]; then
    exit 1
fi

Zmienne $PIPESTATUS oraz $pipestatus

Istnieje jeszcze inna metoda na sprawdzenie status poleceń w pipe. Shell bash i zsh zachowują status wyjścia ostatniego pipe w zmiennych, odpowiednio $PIPESTATUS oraz $pipestatus . W ten sposób można ustalić, które z poleceń zawiodło. Poniżej przykład:

$ false | true | true | true | false; echo "${PIPESTATUS[@]}"
1 0 0 0 1

Dla jeszcze większego uproszenia i czytelności korzystamy z poleceń true i false , które zwracają odpowiedni status wyjścia. Widzimy, że tam, gdzie jest false , to mamy 1. Jeśli teraz interesowałoby nas trzecie polecenie, to możemy sprawdzić jego status w poniższy sposób:

$ false | true | true | true |false; echo "${PIPESTATUS[2]}"
0

Polecenia są numerowane od 0, dlatego w ${PIPESTATUS[2]} wstawiliśmy 2.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.