Bash skripte 4 — funkcije i skripta za produkciju

U trećem delu skripta je naučila da odlučuje i ponavlja. Ovaj deo je o razlici između skripte koja radi na tvom računaru i skripte kojoj možeš da veruješ na serveru u tri ujutru.

Funkcije

Funkcija je imenovani deo skripte koji pozivaš više puta:

#!/usr/bin/env bash

pozdrav() {
    echo "Zdravo!"
}

pozdrav
pozdrav

Funkcija mora biti definisana pre prvog poziva — bash čita fajl odozgo nadole.

Argumenti

Funkcije primaju argumente isto kao skripta: $1, $2, $@:

proveri_servis() {
    local servis="$1"

    if systemctl is-active --quiet "$servis"; then
        echo "$servis: radi"
    else
        echo "$servis: NE RADI"
    fi
}

proveri_servis httpd
proveri_servis sshd

Unutar funkcije $1 je prvi argument funkcije, ne skripte.

local — obavezna navika

Bez local promenljiva u funkciji je globalna i tiho gazi istoimenu iz ostatka skripte:

brojac=100

uvecaj() {
    brojac=5        # gazi globalnu!
}

uvecaj
echo "$brojac"      # 5

Sa local promenljiva živi samo unutar funkcije. Piši ga za svaku promenljivu u funkciji, bez izuzetka — ovo je jedan od najčešćih uzroka grešaka koje se teško nalaze.

Vraćanje vrednosti

Funkcija ne vraća tekst nego izlazni kod. Naredba return prima broj od 0 do 255:

fajl_postoji() {
    local putanja="$1"

    if [ -f "$putanja" ]; then
        return 0
    else
        return 1
    fi
}

if fajl_postoji "/etc/passwd"; then
    echo "Postoji"
fi

Ako ti treba stvarna vrednost, ispiši je i uhvati sa $( ):

zauzece_diska() {
    df "$1" | tail -1 | awk '{print $5}' | tr -d '%'
}

procenat=$(zauzece_diska /)
echo "Root je $procenat% pun"

Zato u funkcijama koje nešto vraćaju ne piši echo za poruke korisniku — pomešaće se sa rezultatom. Poruke šalji na standardni izlaz za greške:

echo "Proveravam disk..." >&2

set — kad skripta treba da stane

Podrazumevano bash nastavlja dalje i posle greške. To u backup skripti znači da može da obriše stare arhive iako nova nikad nije napravljena.

#!/usr/bin/env bash
set -euo pipefail

Tri prekidača, i vredi razumeti šta radi svaki:

ZastavicaEfekat
-eprekini skriptu čim neka komanda vrati grešku
-uprekini ako se koristi nepostojeća promenljiva
-o pipefailpajp je neuspešan ako bilo koja komanda u njemu padne

Bez pipefail, ovo se smatra uspehom iako prva komanda ne postoji:

nepostojeca_komanda | tail -5
echo $?     # 0 — jer je tail uspeo

Zastavica -u hvata greške u kucanju imena promenljive, što je inače tiha katastrofa:

odrediste="/backup"
rm -rf "$odredsite"/*      # bez -u: rm -rf /*

Kad -e smeta

Ponekad očekuješ da komanda padne i to nije greška:

if grep -q "error" "$log"; then
    echo "Ima grešaka"
fi

Ovo je u redu — -e ne prekida komande koje su deo if uslova. Ali ovako bi prekinula:

broj=$(grep -c "error" "$log")     # pukne ako nema pogodaka

Rešenje:

broj=$(grep -c "error" "$log" || true)

trap — čišćenje na izlazu

trap izvršava komandu kada se skripta završi — bilo uredno, bilo greškom, bilo prekidom sa Ctrl+C:

#!/usr/bin/env bash
set -euo pipefail

privremeni=$(mktemp -d)

ocisti() {
    rm -rf "$privremeni"
}

trap ocisti EXIT

# rad sa "$privremeni"...

Bez ovoga bi svaki neuspeli poziv ostavio privremeni direktorijum za sobom. Komanda mktemp -d pravi direktorijum sa nasumičnim imenom — bolje nego da sam izmišljaš /tmp/moj-temp, koji se sudara ako se skripta pokrene dvaput.

Za hvatanje samo prekida:

trap 'echo "Prekinuto"; exit 130' INT TERM

Logovanje

Skripta koju pokreće cron nema kome da ispisuje na ekran. Napravi funkciju:

LOG="/var/log/backup.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}

log "Backup pokrenut"
log "Greška pri arhiviranju"

Komanda tee -a ispisuje i na ekran i u fajl, pa ista skripta radi i ručno i iz crona.

Alternativa je slanje u sistemski journal, gde se onda vidi kroz journalctl:

logger -t backup "Backup pokrenut"
# journalctl -t backup --since today

Sprečavanje dvostrukog pokretanja

Ako backup traje duže od predviđenog, cron će pokrenuti drugi primerak pre nego što se prvi završi. flock to sprečava:

exec 200>/var/run/backup.lock
flock -n 200 || { echo "Već je pokrenuto"; exit 1; }

Ako drugi primerak ne može da zaključa fajl, odmah izlazi.

Kompletna backup skripta

Sve iz sve četiri lekcije na jednom mestu. Ovo možeš staviti na server takvo kakvo je:

#!/usr/bin/env bash
#
# backup.sh — arhivira direktorijum i briše stare arhive
#
# Upotreba: ./backup.sh /putanja/do/izvora [odrediste] [dana]
#
# Primer:   ./backup.sh /var/www /backup 14
#

set -euo pipefail

# ---------- Podešavanja ----------

IZVOR="${1:-}"
ODREDISTE="${2:-/backup}"
CUVAJ_DANA="${3:-7}"
LOG="/var/log/backup.log"

# ---------- Funkcije ----------

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}

greska() {
    log "GREŠKA: $*"
    exit 1
}

pomoc() {
    echo "Upotreba: $0 /putanja/do/izvora [odrediste] [dana]"
    echo
    echo "  izvora      direktorijum koji se arhivira"
    echo "  odrediste   gde se čuvaju arhive (podrazumevano /backup)"
    echo "  dana        koliko dana čuvati arhive (podrazumevano 7)"
    exit 1
}

provere() {
    [ -n "$IZVOR" ] || pomoc

    [ -d "$IZVOR" ] || greska "Izvor ne postoji: $IZVOR"
    [ -r "$IZVOR" ] || greska "Nemam pravo čitanja: $IZVOR"

    if [ ! -d "$ODREDISTE" ]; then
        log "Pravim odredište: $ODREDISTE"
        mkdir -p "$ODREDISTE" || greska "Ne mogu da napravim $ODREDISTE"
    fi

    [ -w "$ODREDISTE" ] || greska "Nemam pravo pisanja: $ODREDISTE"
}

napravi_arhivu() {
    local naziv datum arhiva

    naziv=$(basename "$IZVOR")
    datum=$(date +%Y%m%d_%H%M%S)
    arhiva="$ODREDISTE/${naziv}_${datum}.tar.gz"

    log "Arhiviram $IZVOR"

    if tar -czf "$arhiva" -C "$(dirname "$IZVOR")" "$naziv" 2>>"$LOG"; then
        log "Napravljeno: $arhiva ($(du -h "$arhiva" | cut -f1))"
    else
        rm -f "$arhiva"
        greska "Arhiviranje nije uspelo"
    fi
}

obrisi_stare() {
    local naziv broj

    naziv=$(basename "$IZVOR")

    broj=$(find "$ODREDISTE" -maxdepth 1 -name "${naziv}_*.tar.gz" \
             -mtime "+$CUVAJ_DANA" -print | wc -l)

    if [ "$broj" -gt 0 ]; then
        log "Brišem $broj arhiva starijih od $CUVAJ_DANA dana"
        find "$ODREDISTE" -maxdepth 1 -name "${naziv}_*.tar.gz" \
             -mtime "+$CUVAJ_DANA" -delete
    else
        log "Nema arhiva za brisanje"
    fi
}

# ---------- Glavni tok ----------

exec 200>/var/run/backup.lock
flock -n 200 || { echo "Backup je već pokrenut"; exit 1; }

log "=== Backup pokrenut ==="

provere
napravi_arhivu
obrisi_stare

log "=== Backup završen ==="
exit 0

Postavljanje

# cp backup.sh /usr/local/bin/backup
# chmod +x /usr/local/bin/backup
# /usr/local/bin/backup /var/www /backup 14

Zatim u cron, svakog dana u 2:30:

# vi /etc/cron.d/backup
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin

30 2 * * *  root  /usr/local/bin/backup /var/www /backup 14

Šta ova skripta radi kako treba

  • Ne briše stare pre nego što napravi novu. Ako tar pukne, skripta izlazi i stare arhive ostaju.
  • Briše nedovršenu arhivu. Prekinut tar ostavlja neispravan fajl koji izgleda kao valjan backup — to je gore od nemanja backupa.
  • Ne može da se pokrene dvaput.
  • Vraća izlazni kod. Cron i alati za nadzor znaju da je pao.
  • Piše u log sa vremenskom oznakom.
  • Sve putanje su u navodnicima.
  • Ima -maxdepth 1 kod find-a, da brisanje ne odluta u poddirektorijume.

Šta joj i dalje nedostaje

Da bude poštena: ovo je backup na istoj mašini. Ako disk crkne, nema ni podataka ni arhiva. Sledeći korak je slanje na drugu lokaciju sa rsync-om, i — najvažnije — povremena provera da se arhiva zaista raspakuje. Backup koji nikad nije vraćen nije backup nego pretpostavka.

ShellCheck

Alat koji čita tvoju skriptu i pronalazi greške pre nego što te koštaju:

# dnf install -y ShellCheck
$ shellcheck backup.sh

Hvata nezaštićene promenljive, pogrešna poređenja, nedostižan kod i desetine sitnica. Pusti ga preko svake skripte pre nego što je staviš na server.

Kratak podsetnik

#!/usr/bin/env bash          # uvek na prvom redu
set -euo pipefail            # prekini na grešci
local unutar funkcija        # bez izuzetka
"$promenljiva"               # uvek u navodnicima
[ -f "$fajl" ] || exit 1     # proveri pre nego što radiš
trap ocisti EXIT             # počisti za sobom
exit 0                       # jasan izlazni kod

Vežba

Prepravi backup skriptu tako da:

  • prima više izvornih direktorijuma odjednom
  • preskače prazne direktorijume uz poruku u logu
  • na kraju ispisuje koliko je arhiva napravljeno i ukupnu veličinu
  • vraća izlazni kod 1 ako je bar jedan izvor podbacio

Sve što ti treba prošli smo kroz ova četiri teksta.


Serija: 1. Prva skripta · 2. Promenljive i argumenti · 3. Uslovi i petlje · 4. Funkcije i skripta za produkciju

Povezano: cron · find · tar · Programiranje

Comments

Popular posts from this blog

Konverzija tipova podataka u Pythonu

Početak u Linuxu — šta je i zašto se uči

groupadd