
Debugowanie skryptów bash jest jak szukanie igły w stogu siana, zwłaszcza gdy w istniejącej bazie kodu pojawiają się nowe dodatki bez odpowiedniego rozważenia kwestii struktury, logowania i niezawodności. W takich sytuacjach można napotkać problemy zarówno przez własne błędy, jak i przy zarządzaniu skomplikowanymi złożeniami skryptów.
Zespół Przetłumaczyłam artykuł z zaleceniami, dzięki którym będziesz mógł lepiej pisać, debugować i utrzymywać swoje skrypty. Chcesz wierzyć, chcesz nie, ale nic nie może się równać z satysfakcją z napisania czystego, gotowego do użycia kodu bash, który działa za każdym razem.
W artykule autor dzieli się tym, czego nauczył się przez ostatnie kilka lat, a także niektórymi powszechnymi błędami, które go zaskoczyły. To ważne, ponieważ każdy programista w pewnym momencie swojej kariery pracuje ze skryptami do automatyzacji rutynowych zadań.
Obsługa pułapek
Większość skryptów bash, z którymi się spotkałem, nigdy nie korzystała z efektywnego mechanizmu czyszczenia, gdy coś nieprzewidzianego zdarzyło się podczas wykonywania skryptu.
Niespodzianki mogą się zdarzyć z zewnątrz, na przykład przez otrzymanie sygnału z jądra. Obsługa takich przypadków jest niezwykle ważna, aby skrypty były wystarczająco niezawodne do uruchamiania w systemach produkcyjnych. Często używam obsługi wyjścia, aby zareagować na takie sytuacje:
function handle_exit() {
// Dodaj kod czyszczący tutaj
// na przykład: rm -f "/tmp/${lock_file}.lock"
// zakończ z odpowiednim kodem statusu
}
// pułapka
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap — to wbudowane polecenie powłoki, które pomaga zarejestrować funkcję czyszczenia, która jest wywoływana w przypadku jakichkolwiek sygnałów. Należy jednak zachować szczególną ostrożność z takimi obsługami jak SIGINT, która wywołuje przerwanie skryptu.
Ponadto w większości przypadków należy przechwytywać tylko EXIT, ale idea polega na tym, że można naprawdę skonfigurować zachowanie skryptu dla każdego pojedynczego sygnału.
Wbudowane funkcje set — szybkie zakończenie w przypadku błędu
Bardzo ważne jest reagowanie na błędy, gdy tylko się pojawią, i szybkie przerywanie działania. Nic nie może być gorsze niż kontynuowanie wykonywania polecenia takiego jak:
rm -rf ${directory_name}/*
Zauważ, że zmienna directory_name nie jest zdefiniowana.
Aby obsługiwać takie scenariusze, ważne jest stosowanie wbudowanych funkcji set, takich jak set -o errexit, set -o pipefail lub set -o nounset na początku skryptu. Te funkcje gwarantują, że twój skrypt zakończy działanie, gdy tylko napotka jakikolwiek niezerowy kod zakończenia, wykorzystanie nieokreślonych zmiennych, niewłaściwe polecenia przekazywane przez potok i tak dalej:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
Uwaga: wbudowane funkcje, takie jak set -o errexit, zakończą działanie skryptu, gdy tylko pojawi się "nieobsłużony" kod zwrotu (oprócz zera). Dlatego lepiej jest wprowadzić własne przetwarzanie błędów, na przykład:
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
Takie tworzenie skryptów zmusza cię do bardziej uważnego traktowania zachowania wszystkich poleceń w skrypcie i przewidywania możliwości wystąpienia błędu, zanim zaskoczy cię.
ShellCheck do wykrywania błędów podczas rozwoju
Warto zintegrować coś takiego jak w swoje procesy rozwoju i testowania, aby sprawdzać swój kod bash pod kątem najlepszych praktyk.
Używam go w swoich lokalnych środowiskach deweloperskich, aby uzyskiwać raporty na temat składni, semantyki i niektórych błędów w kodzie, które mogłem przeoczyć podczas rozwoju. To narzędzie analizy statycznej dla twoich skryptów bash, którego zdecydowanie zalecam używać.
Użycie własnych kodów wyjścia
Kody zwrotu w POSIX to nie tylko zero lub jeden, ale zero lub wartość niezerowa. Wykorzystaj te możliwości, aby zwrócić własne kody błędów (od 201 do 254) dla różnych przypadków błędów.
Te informacje mogą być następnie wykorzystywane przez inne skrypty, które owijają twój, aby dokładnie zrozumieć, jaki typ błędu wystąpił i odpowiednio zareagować:
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
Uwaga: Proszę być szczególnie ostrożnym z nazwami zmiennych, które definiujesz, aby nie doprowadzić do przypadkowego nadpisania zmiennych środowiskowych.
Funkcje logujące
Piękne i dobrze zorganizowane prowadzenie logów jest ważne, aby łatwo zrozumieć wyniki działania skryptu. Tak jak w innych językach programowania wysokiego poziomu, w moich skryptach bash zawsze używam własnych funkcji logowania, takich jak __msg_info, __msg_error i tym podobne.
Pomaga to zapewnić ustandaryzowaną strukturę logów, wprowadzając zmiany tylko w jednym miejscu:
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
Zazwyczaj staram się mieć w swoich skryptach jakiś mechanizm __init, w którym te zmienne loggera i inne zmienne systemowe są inicjowane lub ustawiane na wartości domyślne. Zmienne te można również ustawić z parametrów wiersza poleceń podczas wywoływania skryptu.
Na przykład coś takiego:
$ ./run-script.sh --debug
Gdy taki skrypt jest wykonywany, gwarantuje to, że globalne ustawienia systemowe są ustawione na wartości domyślne, jeśli są obowiązkowe, lub przynajmniej zainicjowane czymś odpowiednim, jeśli to konieczne.
Zazwyczaj dokonuję wyboru, co inicjować, a co nie, w oparciu o kompromis między interfejsem użytkownika a detalami konfiguracji, w które użytkownik może / powinien się zagłębić.
Architektura do ponownego wykorzystania i czystego stanu systemu
Modularny / wielokrotnego użytku kod
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
Trzymam osobne repozytorium, które można wykorzystać do inicjowania nowego projektu / skryptu bash, który chcę opracować. Wszystko, co można wykorzystać ponownie, może być zapisane w repozytorium i pozyskiwane w innych projektach, które chcą używać takich funkcji. Taka organizacja projektów znacznie zmniejsza rozmiar innych skryptów oraz zapewnia, że baza kodowa jest mała i łatwa do przetestowania.
Jak w powyższym przykładzie, wszystkie funkcje logowania, takie jak __msg_info, __msg_error i inne, na przykład raporty Slack, są przechowywane osobno w common/* i dynamicznie dołączane w innych skryptach, takich jak daily_database_operation.sh.
Zostaw za sobą czysty system
Jeśli podczas wykonywania skryptu ładujesz jakieś zasoby, zaleca się przechowywanie wszystkich takich danych w wspólnym katalogu o przypadkowej nazwie, na przykład /tmp/AlRhYbD97/*Możesz użyć generatorów losowego tekstu, aby wybrać nazwę katalogu:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Po zakończeniu pracy czyszczenie takich katalogów może być zapewnione przez handlerów pułapek, omawianych powyżej. Jeśli nie zadbasz o usunięcie katalogów tymczasowych, będą się one gromadzić, co w pewnym momencie spowoduje niespodziewane problemy na hoście, takie jak zapełniony dysk.
Użycie plików lock
Często trzeba zapewnić, że na hoście w danym momencie działa tylko jedna instancja skryptu. Można to osiągnąć za pomocą plików lock.
Zazwyczaj tworzę pliki lock w /tmp/project_name/*.lock i sprawdzam ich obecność na początku skryptu. To pomaga prawidłowo zakończyć działanie skryptu i uniknąć niespodziewanych zmian w stanie systemu przez inny skrypt działający równolegle. Pliki lock nie są potrzebne, jeśli musisz, aby ten sam skrypt był uruchamiany równolegle na danym hoście.
Zmierz i popraw
Często musimy pracować ze skryptami, które działają przez dłuższy czas, na przykład codziennymi operacjami bazodanowymi. Takie operacje zazwyczaj obejmują sekwencję kroków: ładowanie danych, sprawdzanie na wypadek anomalii, import danych, wysyłanie raportów stanu i tak dalej.
W takich przypadkach zawsze staram się dzielić skrypt na małe, niezależne skrypty i raportować ich stan oraz czas wykonania za pomocą:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Później mogę sprawdzić czas wykonania za pomocą:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Pomaga mi to określić problematyczne/wolne obszary w skryptach, które wymagają optymalizacji.
Powodzenia!
Co jeszcze przeczytać:
Źródło: habr.com
