Najlepsze praktyki skryptów bash: krótkie wprowadzenie do niezawodnych i wydajnych skryptów bash

Najlepsze praktyki skryptów bash: krótkie wprowadzenie do niezawodnych i wydajnych skryptów bash
Tapeta Shell autorstwa manapi

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ół Mail.ru Cloud Solutions 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 ShellCheck 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ć:

  1. Go i cache GPU.
  2. Przykład aplikacji sterowanej zdarzeniami, opartej na webhookach w obiektowym magazynie S3 Mail.ru Cloud Solutions.
  3. Nasz kanał na Telegramie dotyczący transformacji cyfrowej.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster