Ako ste pratili naše prethodne članke o programiranju u UNIX ljusci, sada već poznajete širok spektar naredbi kojima možete vršiti manipulaciju nad mapama i datotekama unutar Linux operativnog sustava. No, ako postoje zadaci koje želite obavljati s vremena na vrijeme, a zahtijevaju unos više linija kôda, postavlja se logično pitanje: zašto ne bismo napravili program, koji sadrži više linija kôda te koji ćemo moći pokrenuti na bilo kojem računalu koji posjeduje bash ljusku?
Već smo prije pisali o osnovama pisanja bash skripti. Podsjetimo se: linije kôda treba spremiti u datoteku s ekstenzijom.sh, nakon čega skriptu pokrećemo unosom [source ime_datoteke.sh] ili ako pomoću linije [chmod u+x ime_datoteke.sh] dozvolimo datoteci da se ponaša kao izvršna, pokrećemo jednostavno pomoću [./ime_datoteke.sh].
Na prvom primjeru skripte, kojeg možete vidjeti u okviru sa strane, pokazat ćemo osnovnu upotrebu naredbi promjene toka programa. Usput, podsjetit ćemo vas načinima upotrebe varijabli unutar bash ljuske.
Bash skripta: Prvi primjer
Ova skripta ispisuje datoteke koje su izmijenjene na današnji dan. U screenshotu možete vidjeti ponašanje skripte. Za početak, postavljena je dozvola za izvršavanje, te je skripta izvršena. Ona ispisuje današnji datum, u obliku MM/DD/YY te imena datoteka u aktivnom folderu koje su izmijenjene na današnji dan. Skripta ispisuje dvije datoteke, jer je treća (driver.txt) davno izmijenjena. Nadalje, pomoću naredbe echo i redirekcije izlaza, izmjenjujemo datoteku driver.txt te ponovno pokrećemo skriptu. Vidimo da se sada ispisuje naziv i te datoteke, jer je ona naredbom echo upravo izmjenjena.
Prvi primjer
Primjer 1#!/bin/bash
danas=$(date +%D)
# Linija [echo $danas] je tu samo da se vidi
# sadržaj varijable $danas.
echo $danas
for datoteka in *
do
if [ $(date -r “$datoteka” +%D) = $danas ]
then
echo $datoteka
fi
done
Prvi redak skripte primjer1.sh je standardan, njega bi se trebalo u svaku bash skriptu upisati. On govori koja se ljuska koristi pri izvršavanju skripte, u ovom slučaju bash ljuska. Korisno je steći naviku upisivanja ovog retka kako bi se postigla standardiziranost te jednoznačnost pokretanja skripte na različitim računalima. Na nekom drugom računalu s Linux operativnim sustavom je možda postavljena neka druga ljuska kao “default” pa se ovime točno određuje da se koristi bash ljuska.
U drugoj liniji kôda imamo naredbu date, koja u osnovi vraća trenutni datum i vrijeme. Pomoću opcije +% postiže se formatiranje trenutnog vremena, pa će u ovom slučaju +%D vraćati samo današnji datum, bez sati, minuta i sekundi. Postoji čitav niz formata koji se mogu koristiti, a lako ih je pregledati pomoću [man date]. Nakon što naredba date generira string koji opisuje današnji datum, pomoću oblika $(naredba) tu vraćenu vrijednost možemo i koristiti kao string te ga pridružiti nekoj varijabli (o ovome smo pisali u jednom od prethodnih tekstova).
U našem slučaju, string se dodjeljuje varijabli imena “danas”. Petom linijom kôda ispisuje se sadržaj te varijable, čisto da se vidi kako izgleda format +%D.
U šestoj liniji počinje petlja for. Ona se ponaša kao petlja foreach u nekim programskim jezicima: jednoj varijabli pridružuje vrijednost svakog člana neke liste, a naredbe unutar petlje se ponavljaju za svaki taj član. Dakle, ako se radi recimo o pridruživanju članova liste [1,2,3] varijabli x, naredbe bloka petlje će se izvršiti tri puta, i to jednom za x=1, jednom za x=2 i naposljetku za x=3. Nakon toga, program nastavlja s radom iza petlje.
U našem primjeru, blok petlje čine naredbe od sedmog do dvanaestog retka. Upravo ključne riječi “do” i “done” služe za definiranje bloka petlje for – sve linije između te dvije linije čine blok petlje.
U definiciji petlje, u šestom retku, imamo ključnu riječ “for”, zatim varijablu kojoj će se dodjeljivati članovi liste, nakon nje ključnu riječ “in” i na kraju listu po kojoj će petlja iterirati. U primjeru se varijabli imena “datoteka” dodjeljuju članovi liste “*”. Zvjezdica u našem slučaju generira listu svih datoteka unutar aktivnog direktorija, vrši se širenje imena datoteka. O ovom širenju je također bilo riječi u jednom od prethodnih tekstova.
Sada, unutar bloka petlje, varijabla $datoteka predstavljat će upravo ime jedne od datoteka unutar aktivne mape. Prvim prolaskom kroz petlju to će biti prva datoteka, drugim druga i tako sve dok petlja ne prođe kroz sve datoteke aktivne mape.
Osma linija kôda donosi nam naredbu grananja if. Upotreba ove naredbe je vrlo slična upotrebi u drugim programskim jezicima: Nakon ključne riječi “if” slijedi uvjet, koji je unutar uglatih zagrada, u sljedećem retku je ključna riječ za početak bloka, deseta linija predstavlja jedinu liniju bloka naredbe if te naposljetku ključna riječ “fi” predstavlja kraj bloka naredbe if.
Unutar uvjeta ispitujemo je li datum izmjene datoteke koja se obrađuje jednak današnjem datumu.
Opet koristimo naredbu date, kojoj u ovom slučaju dodajemo opciju “-r” i ime datoteke kao argument – pri takvom korištenju ona vraća vrijeme zadnje izmjene te datoteke i formatira ga istim formatom kojim smo formatirali varijablu $danas. Varijabla $datoteka je stavljena unutar navodnika kako ne bi došlo do greške ukoliko naziv datoteke sadrži razmake. Pomoću znaka jednakosti ispituje se je li datum izmjene datoteke jednak izračunatom današnjem datumu spremljenom u varijabli $danas. Ako jest, izvršava se osma linija kôda, odnosno, ispisuje se naziv te datoteke.
Ukoliko ste uvježbani u pisanju kôda u UNIX-u, pisanje ovakve jednostavne skripte će vam uzeti nekoliko minuta vremena, a dobit ćete izvršni program koji vam može biti od koristi. Skriptu koju napišete možete jednostavno i izmijeniti, bez potrebe za pisanjem nove, a u cilju prilagođavanju rada skripte vlastitim potrebama. Na sljedećem primjeru ćemo upravo to i učiniti.
Cilj je izmijeniti skriptu tako da joj možemo predati putanju do mape u kojoj želimo vršiti traženje. Također, omogućit ćemo korisniku da izabere način ispisa: ispis svakog imena u svoj red ili ispis svih imena u isti red. Odabir će se vršiti dodavanjem opcije u “UNIX-ovom stilu”. U našem slučaju, za ispis u isti redak, program će se pokretati pomoću [primjer2.sh -l putanja], pri čemu su opcija -l i putanja opcionalne. Ako se ne zada putanja, traženje se vrši u aktivnoj mapi. Skriptu možete vidjeti u okviru sa strane.
Petlja while i naredba getopts
Za početak, da napomenemo ugrubo u čemu se razlikuju kôdovi prvog i drugog primjera. Imamo dvije nove varijable: jedna je za pamćenje odabira načina ispisa, a druga je za pamćenje lokacije mape u kojoj se vrši pretraživanje. Nadalje, imamo while petlju i iza nje dodatnu naredbu if (od šeste do petnaeste linije). Petlja while obrađuje učitane opcije, odnosno, provjerava je li opcija -l uključena, dok naredba if iza petlje while, provjerava je li upisana putanja te ju obrađuje. Razlika je i u glavnoj for petlji – u novom primjeru iterira se pomoću varijable koja sadrži putanju, pošto putanja može biti ili aktivna mapa ili neka zadana putanja u Terminalu.
Bash skripta: Drugi primjer
U screenshotu možete vidjeti ponašanje skripte. Prvo izvršavanje ispisuje nazive datoteka koje su mijenjane na današnji dan, a nalaze se u aktivnoj mapi. Sljedećim izvršavanjem uključena je opcija -l, čime dobivamo ispis u jednoj liniji Terminala. Nadalje, ako uključimo opcije l i b, pomoću -lb, program se normalno izvršava, a ljuska daje grešku da program ne podržava opciju -b.
Četvrtim izvršavanjem pretraživanje se vrši u matičnom direktoriju (home folder) za koje je rezerviran znak ~ (tilda). Pronađene su tri mape koje su mijenjane. I naposljetku, petim i šestim izvršavanjem imamo ispis u jednoj liniji pri čemu su u šestom pokretanju navedene nedozvoljene opcije -a i -v.
Drugi primjer
Primjer 2#!/bin/bash
danas=$(date +%D)
sep=” ”
lokacija=”.”while getopts l opcija; do
if [ $opcija = “l” ]; then
sep=” “
fi;
doneif [ $OPTIND = $# ]; then
shift $((OPTIND – 1))
lokacija=$1
fifor datoteka in “$lokacija/”*; do
if [ $(date -r “$datoteka” +%D) = $danas ]; then
printf “$datoteka”
printf “$sep”
fi
done
echo “”
Vidimo da navođenjem pogrešnih opcija program normalno nastavlja s radom. Može se dodati u skriptu dodatan dio kôda koji će ispisivati određene poruke o greškama, no to nije potrebno, jer je u ljusku svakako ugrađen sustav hvatanja pogreški.
Prve dvije linije kôda drugog primjera su jasne. U trećoj liniji postavljamo varijablu $sep na vrijednost “ ”, što je specijalni znak za prelazak u novi redak. Dakle, po defaultu će program kasnije svaki ispis stavljati u novi red, a ako je uključena opcija -l, varijabla $sep će se promijeniti u “ “ te će ispisi biti odvojeni razmakom. U četvrtoj liniji varijabla $lokacija se postavlja na vrijednost “.”, što predstavlja trenutnu mapu. Kasnije će se vršiti provjera je li putanja zadana, te ako je, varijabla $lokacija će dobiti vrijednost putanje.
Petlja while počinje u šestoj liniji, tu je njezin header. Sintaksa joj je vrlo slična kao u slučaju for petlje, no princip rada je drukčiji. U uglatim zagradama, kao kod naredbe if, postavlja se uvjet ostanka u petlji, ukoliko se radi o ispitivanju logičkog izraza. Tek kada uvjet više nije zadovoljen, petlja prekida s radom, a program nastavlja s izvođenjem iza njezine naredbe done (deseta linija u našem slučaju).
U našem primjeru while petlje, ne ispituje se logički izraz te zbog toga se ne koriste uglate zagrade. Umjesto toga, tu je naredba getopts s dva argumenta: prvi je slovo l, a drugi je varijabla $opcija. Prvi argument govori koje su opcije podržane. Ako bismo stavili umjesto slova l riječ “lav”, program bi podržavao opcije -l, -a i -v, te upisom linije [./primjer2.sh -lav] u Terminal, ne bi se javila nikakva greška. U varijablu $opcija spremat će se opcija koja je učitana. Naredba getopts je prilagođena za upotrebu s petljom while te će se svakim prolaskom kroz petlju ispitivati sljedeća opcija koja je uključena upisom linije u Terminal. Za svaku učitanu opciju, varijabla $opcija će poprimiti njezinu vrijednost.
Unutar bloka petlje while imamo naredbu if, kojom ispitujemo je li učitana opcija upravo opcija -l. Ako je, vrijednost varijable $sep postaje “ “ te će se ispisi odvajati razmakom. U bilo kojem drugom slučaju, neće se ništa događati, jer nije potrebno. Ako nije uključena opcija -l, ne mijenja se separator, a ako su učitane nepodržane opcije, petlja će se jednostavno vrtiti sve dok ne dođe do kraja niza upisanih opcija u Terminalu.
Naredba if u dvanaestoj liniji provjerava je li upisana putanja. Unutar uvjeta imamo dvije rezervirane varijable: $OPTIND i $#. Prva predstavlja brojač kojeg koristi naredba getopts u while petlji; $OPTIND sadrži indeks argumenta iza zadnje pročitane opcije. Ako recimo imamo tri opcije, $OPTIND će sadržavati indeks 4, koji pokazuje upravo na putanju ukoliko je ona unešena. Varijabla $# sadrži indeks zadnjeg argumenta koji je predan skripti. Dakle, unutar uvjeta se ispituje je li putanja predana skripti. Ako je predana, tada će npr. uz tri opcije, $OPTIND biti 4, a $# će isto biti 4 pa će biti jednaki. Ako putanja nije predana, $# će biti 3, jer su tri opcije predane te u ovom slučaju neće biti jednaki i blok naredbe if se ne izvršava.
Unutar bloka prva korištena naredba je shift. Ova naredba umanjuje indekse argumenata koji su predani skripti za broj koji naredbi shift predamo kao argument. Recimo, ako imamo 3 argumenta, oni su redom pohranjeni u varijable $1, $2 i $3. Kada bismo upotrijebili naredbu [shift 1], vrijednost varijable $2 će biti u varijabli $1, a vrijednost varijable $3 će biti sada u varijabli $2. Varijabla $0 neće sadržavati vrijednost varijable $1, jer je ona strogo rezervirana.
Pošto nam nije poznato što će sve korisnik upisati u Terminal prilikom korištenja ove skripte, nemoguće je sa sigurnošću znati koji argument po redu predstavlja putanju, bez dodatnog računanja. Upravo zbog toga, u trinaestoj liniji kôda, indeks argumenta koji se nalazi prvi iza svih opcija ćemo postaviti da bude jednak 1. Varijabla $OPTIND sadrži indeks argumenta iza svih opcija (npr. uz tri opcije to je indeks 4), nju umanjujemo za jedan pa je sada 3, te indekse smanjujemo za 3. Tako će putanja, koja je u varijabli $4, sada biti i varijabli $1 te ju jednostavno sljedećom linijom spremamo u varijablu $lokacija.
Glavna petlja for je jako slična petlji iz prvog primjera pa ćemo pojasniti samo razlike. Iteriramo kroz sve datoteke u mapi “$lokacija/”*. Varijabla $lokacija će se širenjem pretvoriti u putanju, na nju će se nadodati znak “/” te nakon toga će se zvjezdica proširiti u nazive svih mapa i datoteka u tom direktoriju.
Ispis vršimo naredbom printf. Ona se koristi vrlo slično kao funkcija printf() u C-u, međutim, u ovom primjeru nam puna funkcionalnost nije potrebna. Koristimo ju umjesto naredbe echo, jer printf ne stavlja automatski prelazak u novi red. Kada bismo koristili echo, nikako ne bismo mogli postići ispis u jedan redak (doduše, mogli bismo uz bespotrebno kompliciranje dijela skripte).
Ovako eksplicitno određujemo separator: prvim ispisom ispisuje se ime datoteke, a drugim ispisom separator, koji je ranije određen. Na kraju skripte dodana je naredba echo, koja doprinosi urednosti ispisa. Ako b ismo ju izostavili, uz opciju -l, sljedeći korisnikov upis u Terminalu bi bio u istom retku s ispisom datoteka koje su pronađene.
Primijetili ste vjerojatno da je na nekim mjestima korišten znak točka-zarez. On odvaja linije kôda, tako da se u cilju preglednosti koristi na određenim mjestima, kao što su headeri petlji i naredbi grananja. Dakako, može se stavljati na kraj svake linije, no nema potrebe za tim – prelaskom u novi red bash ljuska automatski registrira novu liniju.
U ovom, drugom primjeru, novouvedena while petlja i dodatno if grananje predstavljaju na neki način klasični i standardni pristup pri obradi opcija i ostalih argumenata. Vrlo je jednostavno u petlju while dodati dodatne opcije koje želimo podržati, a pomoću if-a lako je pregledati sve argumente koji se nalaze iza opcija. Iako, u ovom primjeru provjeravamo jedan argument iza opcija, te će se pod određenim okolnostima program izvršavati neočekivano. U sljedećem primjeru skriptu ćemo nadograditi novim opcijama koje podržava.
Bash skripta: Treći primjer
Treći primjer možete vidjeti u okviru sa strane, kao i screenshot izvršavanja skripte sa svim uključenim opcijama koje podržava.
U trećem primjeru su, za početak, novost tri varijable koje uvodimo: num, brojac i samoDatoteke. Prva varijabla služi za pamćenje opcije -n, koja će numerirati nađene datoteke i mape ukoliko je uključena. Varijabla brojac će pamtiti broj retka ukoliko je numeriranje uključeno te naposljetku varijabla samoDatoteke će pamtiti je li uključena opcija -f, pomoću koje ispisujemo samo nađene datoteke, bez mapa.
Treći primjer
Primjer 3#!/bin/bash
danas=$(date +%D)
sep=” ”
lokacija=”.”
num=false
brojac=1
samoDatoteke=falsewhile getopts ldnf opcija; do case $opcija in l) sep=” “;; d) echo $danas;; n) num=true;; f) samoDatoteke=true;; esac
doneif [ $OPTIND = $# ]; then shift $((OPTIND – 1)) lokacija=$1
fifor datoteka in “$lokacija/”*; do if [ $(date -r “$datoteka” +%D) = $danas ]; then if [ $samoDatoteke = true ] && [ ! -f “$datoteka” ]; then continue fi if [ $num = true ]; then printf “%d “ $brojac brojac=$((brojac + 1)) fi printf “$datoteka” printf “$sep” fi
doneecho “”
Pošto sada imamo više opcija koje smo implementirali u skriptu, koristit ćemo naredbu case. Kako smo već rekli, upotreba petlje while pri radu s opcijama je praktički klasična i standardna, a sada ju dopunjujemo naredbom koja je najbolja u kontekstu pridruživanja određenih akcija svakoj opciji.
Ukoliko imate iskustva s bilo kojim programskim jezikom, naredba case će vam biti prilično jasna i intuitivna. Ona se koristi kada je potrebno provjeravati vrijednost jedne varijable i za svaku vrijednost pridružiti određeni blok naredbi. Najjednostavnije je naredbu case usporediti sa ponavljanjem naredbe if u kojoj bismo pri svakom if-u provjeravali sadrži li varijabla određenu vrijednost te ukoliko sadrži, izvršavali određeni blok naredbi.
Linija [case $opcija in] predstavlja header ove naredbe, gdje su “case” i “in” ključne riječi, a varijabla $opcija se provjerava. Headerom počinje djelovanje naredbe case, a blok se zatvara naredbom esac (primijetite da je riječ “esac” zapravo obrnuto napisana riječ “case”; takav slučaj imamo i kod naredbe if).
Nakon headera navode se vrijednosti, te za svaku vrijednost blok naredbi koji se izvršava, ukoliko je varijabla koja se uspoređuje jednaka upravo toj vrijednosti. U našem slučaju, svakoj vrijednosti je pridružena po jedna linija kôda koja se izvršava.
Sintaksa je vrlo jednostavna: navodi se vrijednost iza koje slijedi zatvorena zagrada, a zatim blok ili linija koja se izvršava za tu vrijednost. Vizualno, blok naredbe case podsjeća na ispit na zaokruživanje, što logički na neki način i jest, jer se provjerava koja je od vrijednosti točna za varijablu. Valja napomenuti i to da se umjesto vrijednosti može navoditi i regularni izraz, pa kad bismo dodali redak [ [a-c]) echo “Bla”;; ], za svaku uključenu opciju koja je -a, -b ili -c ispisivala bi se poruka “Bla”. Blok naredbi pridružen jednoj vrijednosti obavezno završava s dva znaka točka-zarez.
Ukoliko je uključena opcija -l, ishod je jednak onom u prethodnom primjeru. Opcija -d ispisuje današnji datum, dok opcije -n i -f postavljaju varijable num i samoDatoteke u istinu (true). Kasnije ćemo pri traženju datoteka provjeravati je li koja od tih varijabli istinita te ako je, poduzimati određene radnje.
U cijeloj skripti, preostalo je još za razjasniti dodatnu obradu unutar glavne for petlje. Dodana su dva bloka naredbe if, od kojih prvi provjerava želimo li ispis samo datoteka, a drugi je li potrebno numerirati ispise. Prvim blokom provjerava se složeni logički izraz.
Uglatim zagradama su odvojeni podizrazi između kojih se nalazi operator &&. Ovaj operator predstavlja logičko I (AND) što znači da ukoliko su svi podizrazi zadovoljeni koje ovaj operator povezuje, ukupan izraz će biti zadovoljen. Drugim riječima, u našem primjeru, ako je istinita varijabla $samoDatoteke i ako je nađeno nešto što nije datoteka, izvršava se nareddba continue.
Moglo bi se naivno zaključiti da drugi podizraz provjerava je li nađena mapa, no to nije apsolutno točno (iako je korektno u ovom primjeru). Operator ! negira izraz koji slijedi, a operator -f provjerava je li datoteka navedena iza njega upravo datoteka (-f, engl. file). Dakle, zapravo se provjerava je li nađena datoteka. Ako je, pomoću negacije podizraz je netočan, te uvjet if-a nije zadovoljen (moraju oba podizraza biti točna).
Ukoliko je nađeno nešto što nije datoteka, podizraz se negira operatorom negacije, pa negiranje neistine (false) daje za rezultat istinu (true) te je sada podizraz istinit. Postoji čitav niz operatora poput -f koji provjeravaju određena svojstva datoteke, a sve ih možete naći na jednom mjestu upisom u Terminal [man test]. Inače, test je izvorna naredba za provjeru uvjeta, dok je oblik s uglatim zagradama pojednostavljen zapis iste.
Naredba continue vraća izvođenje petlje na njezin header, odnosno, blok petlje ne nastavlja s radom, neko petlja odradi iteraciju, a izvođenje programa se nastavlja na prvoj liniji bloka petlje. U našem slučaju, postiže se sljedeće: ukoliko je uključena opcija za ispis samo datoteka, daljnja obrada se ignorira ako je nađena mapa. Tada nema potrebe provjeravati ni numeriranje, jer mape ne ispisujemo i program nastavlja s obradom sljedeće datoteke odnosno mape.
Drugi novouvedeni if provjerava je li potrebno numerirati ispise. Ako je, ispisuje se vrijednost varijable $brojac, a zatim ju se povećava za jedan.
Relativno je jednostavno napisati skriptu za Linux operativni sustav, ukoliko poznajete sintaksu Unix ljuske. Linuxaši općenito zasigurno češće koriste Terminal nego korisnici Windowsa CMD, te je tako većini korisnika Linuxa vjerojatno blizak rad u Terminalu. Često je potrebno neke programe u Linuxu instalirati upravo preko Terminala, npr. ukoliko želite nekakav određeni program kojeg ne možete skinuti s interneta pomoću ugrađenog Software Centera.
Nakon pažljivog čitanja članaka koje smo vam donijeli, znat ćete što upisujete u Terminal (npr. možda niste znali čemu služi sudo i što je uopće apt-get na Ubuntu-u, sada znate da je to naredba iza koje su njeni argumenti) te će vam sigurno Terminal biti jasniji nego prije. A što je najvažnije, sada možete pisati svoje programe specifične namjene, bez potrebe za dugim traženjem po internetu programa čije vam sve mogućnosti nisu niti potrebne. Do čitanja!
Piše: J.U.