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:
| Zastavica | Efekat |
|---|---|
-e | prekini skriptu čim neka komanda vrati grešku |
-u | prekini ako se koristi nepostojeća promenljiva |
-o pipefail | pajp 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
tarpukne, skripta izlazi i stare arhive ostaju. - Briše nedovršenu arhivu. Prekinut
tarostavlja 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
1ako 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
Post a Comment