Ezen az oldalon a shell programozás alapjairól van szó. Elkészítésében legnagyobb segítséget a https://www.shellscript.sh/ oldal nyújtotta. Összefoglaló oldal: https://devhints.io/bash. A lentiek előfeltétele a Linux alapismeretek ismerete.
Helló, világ!
Első példaként hozzuk létre az alábbit (firstscript.sh):
#!/bin/sh
# My first shell script.
echo Hello world!
Adjunk futtatási jogosultságot:
chmod 755 firstscript.sh
Majd indítsuk el:
./firstscript.sh
Ha mindent jól csináltunk, megjelenik a képernyőn a jó öreg Hello world! Némi magyarázat:
- A shell scriptnek elvileg bármi lehet a kiterjesztése, de általában .sh.
- Az első sorban általában meg szoktuk adni, hogy melyik shell futtassa. Kisebb eltérések ugyanis vannak az egyes shellek között, aminek nagyobb program esetén már lehet jelentősége. A /bin/sh egy szimbolikus link, ami egy valódi, az adott rendszeren alapértelmezett shellre mutat, pl. a bash-ra vagy dash-ra. (Ilyen esetben gyakorlatilag azt jelezzük, hogy elvileg bármelyik shell képes futtatni a scriptünket.)
- A shell scriptekben a # karakter vezeti be a megjegyzést; az adott sorban az azt követő részt futtatáskor a shell nem veszi figyelembe.
- Több soros megjegyzést a : ' sorral tudunk indítani, ami a ' sorral zárul:
: '
Multiline
comment
'
Utasítások végrehajtása
Általában egy utasítást írunk egy sorba, ami egymás után hajtódik végre. (Kivéve a vezérlő utasításokat.) Több utasítást a következő elválasztó karakterekkel tudjuk egy sorba írni:
- ;: olyan, mintha egymás után írtuk volna.
- &&: a második csak akkor hajtódik végre, ha az első sikeres volt.
- ||: a második csak akkor hajtódik végre, ha az első sikertelen volt. Ez lehet akár nem létező parancs is, vagy olyan parancs, melynek a visszatérési értéke valamilyen hiba.
Próbáljuk ki a következőket:
- echo "hello"; echo "world": eredménye hello és world egymás alatt.
- echo "hello" && echo "world": mint előbb.
- echo "hello" || echo "world": azt írja ki, hogy hello, és mivel sikeres volt, a world nem jelenik meg.
- wrongcmd; echo "world": kiírja a hibát, majd azt, hogy world.
- wrongcmd && echo "world": kiírja a hibát, de a world-öt nem.
- wrongcmd || echo "world": kiírja a hibát, majd a world-öt is.
További lehetőségek:
- | az első utasítás outputja a második inputja, pl. cat file.txt | grep apple
- operation1 `operation2`: az operation2 outputja az operation1 paramétere, pl. echo `ls`
Műveletek számokkal
- n=5: a változókat nem kell deklarálni ill. nem kell megadni a típusát sem, hanem egyszerűen csak értéket adnunk neki.
- echo $n: a $ jellel kapjuk meg a változó értékét.
- echo ‘expr $n + 2`: ilyen kacifántosan lehet csak műveletet végrehajtani. Fontosan a szóközök; a megadott példa eredménye az 5 lesz, de ha nem tennénk szóközt a + jel köré akkor 3+2 lenne.
- echo `expr $a \* 2`: szorozni még problémásabb, a * karakter speciális jelentése miatt.
- echo `expr $a / 2`: egész számú osztás.
- echo `expr $a % 2`: egész számú osztás maradéka.
- echo ’6.5 / 3.7' | bc -l: a shell scriptben nem lehet lebegőpontos aritmetikát végrehajtani (nagyon kisarkítva: a számítógép alapból nem alkalmas számolásra…); ahhoz a bc (basic calculator, alap számológép) nevű programot tudjuk használni. Ezt elképzelhető, hogy külön fel kell telepíteni. Ezt a programot alapvetően interaktív módra alkották meg; a megadott módon tudjuk script-ben használni.
String
Igen gazdag a shell script string kezelése, viszont sajnos nem elég bőbeszédű ahhoz, hogy jó lehessen érteni. Az alábbi példák ezt illusztrálják.
- fruit=apple: értéket ad.
- ${fruit} (a $fruit is működik): a változó értéke; pl. az echo $fruit kiírja azt, hogy apple.
- ${fruit/p/q}: lecseréli a p első előfordulását q-ra (aqple).
- ${fruit//p/q}: az összes előfordulást lecserélni (aqqle).
- ${fruit:1:2}: részstring (0-tól sorszámozva az 1-es sorszámútól 2 karaktert vesz, azaz pp).
- ${fruit%le}: leveszi a posztfixet, ha van (app).
- ${fruit#ap}: leveszi a prefixet (ple).
- ${#fruit}: a változó értékének a hossza (5).
- ${fruit:-peach}: a változó értéke, vagy peach, ha nincs beállítva. Ellenpróba: ${myfruit:-peach}. Az első esetben apple lesz az eredmény, a másodikban peach.
- ${fruit:+peach}: a fenti fordítottja, azaz ez esz peach, a ${myfruit:+peach} pedig üres
- ${fruit:?Error}: hibát ír ki, ha nincs beállítva, egyébként a változó értéke az eredmény. Ellenpróba: ${myfruit:?Error}.
- ${fruit:=peach}: ha be van állítva, akkor megmarad az értéke, de ha nincs, akkor felveszi a megadottat. Ebben a példában tehát marad apple, de a ${myfruit:=peach} eredménye az lesz, hogy a myfruit értéke peach lesz.
A stringet tehetjük "" és '' közé. A kettő között az a különbség, hogy a változókat az első feloldja, a második nem. Folytatva a fenti (n=5) példát, próbáljuk ki az alábbiakat:
- echo "$n": eredménye 5.
- echo '$n': eredménye $n.
Tömb
A tömbök kezelését az alábbi példák illusztrálják:
- Fruits=('Apple' 'Banana' 'Orange'): tömb létrehozása.
- Fruits+=('Watermelon'): elem hozzáadása.
- Fruits[4]=('Peach'): adott pozícióban az elem megadása. Automatikusan átméretezi, ha kell.
- echo ${Fruits[@]}: kiírja a tömb összes elemét.
- echo ${Fruits}: kiírja a tömb első elemét.
- echo ${Fruits[0]}: ugyanaz, mint az előző. A tömb 0-tól sorszámozódik, és a sorszám megadásával tudjuk az elemet megadni.
- unset Fruits[2]: törli a harmadik (kettes sorszámú) elemet.
- echo ${#Fruits[@]}: elemek száma
- echo ${#Fruits[3]}: a harmadik elem hossza
- echo ${Fruits[@]:3:2}: tömb része; a harmadik elemtől két elem lesz az eredmény.
- for i in "${Fruits[@]}"; do echo $i; done: ezzel tudunk végiglépkedni a tömb elemein.
Asszociatív tömb
A kulcs-érték párokból álló asszociatív tömbök a shell scriptben alapstruktúrának számítanak. Az alábbi példák illusztrálják a használatukat:
- declare -A prices: ezzel lehet létrehozni, bár valójában a tapasztalatom szerint enélkül is működik.
- prices[apple]=15, prices[peach]=30: a kulcsoknak így tudunk értéket adni.
- echo ${prices[apple]}: adott kulcsú elem értékének lekérdezése.
- echo ${prices[@]}: az összes érték lekérdezése.
- echo ${!prices[@]}: az összes kulcs lekérdezése.
- echo ${#prices[@]}: az elemek számát adja vissza.
- unset prices[peach]: elem törlése.
Feltételkezelés
A klasszikus if…else szerkezetre tekintsük az alábbi példát!
if [[ -z $1 ]]
then
echo "Add meg paraméterként a nevedet!"
elif [[ $1 == "szia" ]]
then
echo "A nevedet kérem!"
else
echo "Szia, $1!"
fi
A feltételek persze nem ám csak úgy vannak a megszokott módon! A következőek a lehetőségek:
- String feltételek
- [[ -z STRING ]]: üres string
- [[ -n STRING ]]: nem üres string
- [[ STRING1 == STRING2 ]]: a két string egyenlő
- [[ STRING1 != STRING2 ]]: a két string nem egyenlő
- [[ STRING1 ~= STRING2 ]]: a STRING1 illeszkedik a STRING2 reguláris kifejezésre
- Szám feltételek
- [[ NUM1 -eg NUM2 ]]: a két szám egyenlő
- [[ NUM1 -ne NUM2 ]]: a két szám nem egyenlő
- [[ NUM1 -lt NUM2 ]]: a NUM1 kisebb mint NUM2
- [[ NUM1 -le NUM2 ]]: a NUM1 kisebb vagy egyenlő mint NUM2
- [[ NUM1 -gt NUM2 ]]: a NUM1 nagyobb mint NUM2
- [[ NUM1 -ge NUM2 ]]: a NUM1 nagyobb vagy egyenlő mint NUM2
- Hagyományos egyenlőtlenség vizsgálat
- (( NUM1 <= NUM2 )): a NUM1 kisebb vagy egyenlő mint NUM2 (ez a többi összehasonlító operátorral is működik)
- Fájl feltételek
- [[ -e FILE ]]: létezik
- [[ -h FILE ]]: szimbolikus link
- [[ -f FILE ]]: fájl
- [[ -d FILE ]]: könyvtár
- [[ -r FILE ]]: olvasható
- [[ -w FILE ]]: írható
- [[ -x FILE ]]: futtatható
- [[ -s FILE ]]: a mérete nagyobb mint 0
- [[ FILE1 -nt FILE2 ]]: az első fájl újabb mint a második
- [[ FILE1 -ot FILE2 ]]: az második fájl újabb mint az első
- [[ FILE1 -ef FILE2 ]]: a két fájl ugyanaz
- Logikai műveletek feltételekkel
- [[ ! EXPR ]]: tagadás
- [[ EXPR1 ]] && [[ EXPR2 ]] : logikai és
- [[ EXPR1 ]] || [[ EXPR2 ]] : logikai vagy
A [[ … ]] feltételnél figyeljünk a szóközökre, a (( … )) esetén nem kötelező a szóköz.
Klasszikus for ciklus
A C nyelvben megszokott módon for ciklust az alábbi módon tudunk létrehozni:
for((i=1; i<=5; i++))
do
echo $i
done
Végiglépkedés egy tömb elemein
A másik, ma már valószínűleg leggyakoribb for ciklus az a végiglépkedés egy tömb elemein. A tömb elemeit fel tudjuk sorolni az alábbi módon:
for i in 1 2 3 4 5
do
echo "$i"
done
A követező módszerrel tudunk generálni elemeket:
for i in {1..5}
do
echo $i
done
A while ciklus
Az elöltesztelős while ciklust az alábbi példával szemléltetjük, ami a fenti for ciklusok megfelelője:
i=1
while [ $i -le 5 ]
do
echo "$i"
i=`expr $i + 1`
done
Összetett feltételkezelés
A klasszikus switch…case…default struktúra shell megfelelőjét az alábbi, a hét napjait kiíró példával illusztráljuk:
case $1 in
1)
echo "hétfő"
;;
2)
echo "kedd"
;;
3)
echo "szerda"
;;
4)
echo "csütörtök"
;;
5)
echo "péntek"
;;
6)
echo "szombat"
;;
7)
echo "vasárnap"
;;
*)
echo "hiba"
;;
esac
A programot meghívva pl. a 4 paraméterrel azt írja ki, hogy csütörtök.
Beolvasás
read NAME: beolvas a konzolról egy sort, ami a NAME változó értéke lesz.
Függvények
A shell scriptben függvényt is tudunk írni, ami szintén kissé nyakatekert a nem script programozási nyelvekhez képest.
Egy shell script, ami egy helló, viág függvényt tartalmaz, meghívással a következő:
#!/bin/sh
function hello() {
echo "Hello world!"
}
hello
A hívásnál tehát nem kell a zárójel. Valójában a function is opcionális, azaz a következő is működik:
#!/bin/sh
hello() {
echo "Hello world!"
}
hello
A jobb olvashatóság érdekében kiteszem a példákban a function kulcsszót.
Paraméter listát nem tudunk létrehozni; a paraméterekre a következőképpen tudunk hivatkozni: $1, $2 stb., pl.:
#!/bin/sh
function salute() {
echo "Hello, $1!"
}
salute Csaba
$@ az összes paraméterre hivatkozik:
#!/bin/sh
function fruits() {
echo "List of fruits: $@"
}
fruits apple banana peach
A függvények esetén felmerül a visszatérési érték kérdése. Lássunk egy két paraméteres példát a klasszikus max függvénnyel!
#!/bin/sh
function max() {
if [[ $1 -gt $2 ]]
then echo $1
else echo $2
fi
}
max 2 3
Valójában ez is csak kiírja az eredményt. Viszont emlékezzünk: ha egy parancsot a `` jelek között hajtunk végre, akkor az, amit kiír a program a konzolra, az az előtte álló parancs paraméter listája lesz, és ez igaz a függvények esetén is, pl.:
#!/bin/sh
function max() {
if [[ $1 -gt $2 ]]
then echo $1
else echo $2
fi
}
result=`max 2 3`
echo "Result = $result"
A példában a függvény visszatérési értéke a result változóba kerül, amit utána kiírunk.
Függvények esetén általában működik a rekurzió, ami a a fentiekből már adódik is:
#!/bin/sh
function factorial() {
if [[ $1 -gt 1 ]]
then
minus1=`expr $1 - 1`
factorial_minus1=`factorial $minus1`
echo `expr $1 \* $factorial_minus1`
else echo 1
fi
}
factorial 5
Valójában amiatt ennyire komplikált mert nehézkes a számokkal való művelet. Ha lefuttatjuk, akkor azt fogjuk tapasztalni, hogy meglehetősen lassú, ami nem meglepő, mivel interpretált nyelvről van szó.
Végül fontos megemlítenünk azt is, hogy a shell scriptben az összes változó globális, azaz ha a függvényen belül értéket adunk egy olyan változónak, ami azon kívül is létezik, akkor az felülíródik:
#!/bin/sh
function global() {
x=3
}
x=2
global
echo $x
Ennek a script a végén azt írja ki, hogy 3. Ehhez némi pontosítás: a $1, $2 stb. változók külön értelmezendők script szinten és függvény szinten (ez utóbbit ld. lejjebb).
Paraméterek és visszatérési érték
A script paramétereire - ahogyan a függvényekére is - $1, $2 stb. módon tudunk hivatkozni, ill. speciálisan az összesre így: $@, a paraméterek számára meg így: $#. Példa (hello.sh):
#!/bin/sh
echo Hello, $1!
Majd hívjuk meg pl. így:
./hello.sh Csaba
A visszatérési érték kicsit trükkösebb, ugyanis csak függvénynek adhatunk visszatérési értéket. Hozzuk léptre pl. a következőt, ret.sh néven:
#!/bin/sh
function test() {
if [[ $1 -lt 5 ]]
then return 1
else return 0
fi
}
Majd úgymond "source-oljuk" a parancssorból:
source ret.sh
Ezzel a test() függvény elérhető parancssorból, mintha utasítás lenne. A fent megadott módon tudjuk kezelni a visszatérési értéket, pl. a && vagy || operátorokkal:
- test 3 || echo X: X, mert 3 < 5, a visszatérés érték 1, ami igazából hibakód.
- test 8 || echo X: üres, mert 8 > 5, a visszatérési érték 0, ami a sikeres lefutás kódja.
Tippek és trükkök
A fentiekből tkp. adódik, de érdemes külön megemlíteni néhány olyan tulajdonságot, ami első ránézésre nem megszokott, de működik:
- mkdir ab{1,2,3,4,5}cd: létrehozza az ab1cd, ab2cd, ab3cd, ab4cd és ab5cd nevű könyvtárakat.
- for i in *; do echo "$i"; done: kiírja az aktuális könyvtárban található fájl- és alkönyvtár neveket.