Najlepsze praktyki dotyczące skryptów Bash: krótki przewodnik po niezawodnych i wydajnych skryptach Bash

Najlepsze praktyki dotyczące skryptów Bash: krótki przewodnik po niezawodnych i wydajnych skryptach Bash
Tapeta Shell od Manapi

Debugowanie skryptów basha jest jak szukanie igły w stogu siana, zwłaszcza gdy w istniejącej bazie kodu pojawiają się nowe dodatki, bez wcześniejszego rozważenia kwestii struktury, rejestrowania i niezawodności. Możesz znaleźć się w takich sytuacjach albo z powodu własnych błędów, albo podczas zarządzania złożonymi stosami skryptów.

Zespół Rozwiązania chmurowe Mail.ru przetłumaczył artykuł z zaleceniami, które pomogą Ci lepiej pisać, debugować i utrzymywać skrypty. Wierzcie lub nie, ale nic nie przebije satysfakcji z pisania 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 kilkoma typowymi błędami, które go zaskoczyły. Jest to ważne, ponieważ każdy programista na pewnym etapie swojej kariery pracuje ze skryptami automatyzującymi rutynowe zadania robocze.

Osoby zajmujące się pułapkami

Większość skryptów basha, z którymi się spotkałem, nigdy nie korzysta z skutecznego mechanizmu czyszczenia, gdy podczas wykonywania skryptu wydarzy się coś nieoczekiwanego.

Z zewnątrz mogą pojawić się niespodzianki, takie jak otrzymanie sygnału z rdzenia. Obsługa takich przypadków jest niezwykle ważna, aby mieć pewność, że skrypty będą wystarczająco niezawodne, aby można je było uruchomić na systemach produkcyjnych. Często używam programów obsługi wyjścia, aby odpowiedzieć na takie scenariusze:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
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ść w przypadku osób zajmujących się obsługą takich jak SIGINT, co powoduje przerwanie wykonywania skryptu.

Ponadto w większości przypadków należy tylko łowić EXIT, ale pomysł jest taki, że możesz faktycznie dostosować zachowanie skryptu dla każdego indywidualnego sygnału.

Wbudowane funkcje zestawu - szybkie zakończenie w przypadku błędu

Bardzo ważne jest, aby reagować na błędy natychmiast po ich wystąpieniu i szybko zatrzymywać wykonywanie. Nie ma nic gorszego niż dalsze uruchamianie takiego polecenia:

rm -rf ${directory_name}/*

Należy pamiętać, że zmienna directory_name niezdeterminowany.

Ważne jest, aby używać wbudowanych funkcji do obsługi takich scenariuszy setjak na przykład set -o errexit, set -o pipefail lub set -o nounset na początku skryptu. Funkcje te zapewniają, że skrypt zakończy działanie, gdy tylko napotka niezerowy kod zakończenia, użycie niezdefiniowanych zmiennych, nieprawidłowe polecenia przekazane potokiem itd.:

#!/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, np set -o errexit, zakończy działanie skryptu, gdy tylko pojawi się „surowy” kod powrotu (inny niż zero). Dlatego lepiej wprowadzić niestandardową obsługę 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

Pisanie skryptów w ten sposób zmusza do większej ostrożności w zachowaniu wszystkich poleceń w skrypcie i przewidywania możliwości wystąpienia błędu, zanim Cię zaskoczy.

ShellCheck do wykrywania błędów podczas programowania

Warto zintegrować coś takiego Sprawdzenie powłoki do potoków programowania i testowania, aby sprawdzić kod bash pod kątem najlepszych praktyk.

Używam go w moich lokalnych środowiskach programistycznych, aby uzyskać raporty dotyczące składni, semantyki i niektórych błędów w kodzie, które mogłem przeoczyć podczas tworzenia. Jest to narzędzie do analizy statycznej skryptów basha i gorąco polecam go używać.

Używanie własnych kodów wyjścia

Kody powrotu w POSIX to nie tylko zero lub jeden, ale zero lub niezerowa wartość. Użyj tych funkcji, aby zwrócić niestandardowe kody błędów (od 201 do 254) dla różnych przypadków błędów.

Informacje te mogą następnie zostać wykorzystane przez inne skrypty, które otaczają 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: należy zachować szczególną ostrożność przy definiowaniu nazw zmiennych, aby uniknąć przypadkowego nadpisania zmiennych środowiskowych.

Funkcje logowania

Piękne i uporządkowane rejestrowanie jest ważne, aby łatwo zrozumieć wyniki skryptu. Podobnie jak w przypadku innych języków programowania wysokiego poziomu, zawsze używam natywnych funkcji rejestrowania w moich skryptach basha, takich jak __msg_info, __msg_error i tak dalej.

Pomaga to zapewnić ujednoliconą strukturę rejestrowania poprzez wprowadzanie zmian 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"

Zwykle staram się mieć jakiś mechanizm w swoich skryptach __init, gdzie takie zmienne rejestratora i inne zmienne systemowe są inicjowane lub ustawiane na wartości domyślne. Zmienne te można także ustawić w opcjach wiersza poleceń podczas wywoływania skryptu.

Na przykład coś takiego:

$ ./run-script.sh --debug

Kiedy taki skrypt jest wykonywany, zapewnia, że ​​ustawienia ogólnosystemowe zostaną ustawione na wartości domyślne, jeśli są wymagane, lub przynajmniej zainicjowane na coś odpowiedniego, jeśli to konieczne.

Zwykle opieram wybór tego, co zainicjować, a czego nie, na kompromisie między interfejsem użytkownika a szczegółami konfiguracji, w które użytkownik może/powinien się zagłębić.

Architektura umożliwiająca ponowne wykorzystanie i czysty stan systemu

Kod modułowy/wielokrotnego użytku

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Prowadzę osobne repozytorium, którego mogę użyć do zainicjowania nowego projektu/skryptu bash, który chcę opracować. Wszystko, co można ponownie wykorzystać, można przechowywać w repozytorium i pobierać w innych projektach, które chcą skorzystać z tej funkcjonalności. Organizowanie projektów w ten sposób znacznie zmniejsza rozmiar innych skryptów, a także zapewnia, że ​​baza kodu jest niewielka i łatwa do przetestowania.

Podobnie jak w powyższym przykładzie, wszystkie funkcje logowania takie jak __msg_info, __msg_error i inne, takie jak raporty Slack, są zawarte osobno w common/* i dynamicznie łącz się w innych scenariuszach, np daily_database_operation.sh.

Zostaw czysty system

Jeśli ładujesz jakieś zasoby w trakcie działania skryptu, zaleca się przechowywanie wszystkich tych danych w udostępnionym katalogu o losowej nazwie, np. /tmp/AlRhYbD97/*. Aby wybrać nazwę katalogu, możesz użyć generatorów losowego tekstu:

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Po zakończeniu pracy można wyczyścić takie katalogi w omówionych powyżej procedurach obsługi haków. Jeśli katalogi tymczasowe nie są obsługiwane, gromadzą się i na pewnym etapie powodują nieoczekiwane problemy na hoście, takie jak zapełnienie dysku.

Korzystanie z plików blokady

Często trzeba się upewnić, że w danym momencie na hoście działa tylko jedna instancja skryptu. Można to zrobić za pomocą plików blokady.

Zwykle tworzę pliki blokujące w formacie /tmp/project_name/*.lock i sprawdź ich obecność na początku skryptu. Pomaga to w bezpiecznym zakończeniu skryptu i pozwala uniknąć nieoczekiwanych zmian stanu systemu przez inny skrypt działający równolegle. Pliki blokujące nie są potrzebne, jeśli chcesz, aby ten sam skrypt był wykonywany równolegle na danym hoście.

Mierz i ulepszaj

Często musimy pracować ze skryptami, które działają przez długi czas, na przykład podczas codziennych operacji na bazie danych. Takie operacje zazwyczaj obejmują sekwencję kroków: ładowanie danych, sprawdzanie anomalii, importowanie danych, wysyłanie raportów o stanie i tak dalej.

W takich przypadkach zawsze staram się podzielić skrypt na osobne, małe skrypty i zgłosić ich status oraz czas wykonania za pomocą:

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Później mogę zobaczyć czas wykonania za pomocą:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Pomaga mi to zidentyfikować problematyczne/powolne obszary w skryptach, które wymagają optymalizacji.

Powodzenia!

Co jeszcze przeczytać:

  1. Go i pamięci podręczne GPU.
  2. Przykład aplikacji sterowanej zdarzeniami opartej na webhookach w pamięci obiektowej S3 rozwiązań chmurowych Mail.ru.
  3. Nasz kanał telegramowy na temat transformacji cyfrowej.

Źródło: www.habr.com

Dodaj komentarz