Shell script

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.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License