Példaprogramok

Az alábbiakban néhány shellscript vizsgálatán keresztül illusztráljuk a shell parancsnyelvének eddig tanult jellemzőit, s azt, hogy hogyan lehet gyorsan meglepően komplex feladatok ellátására szolgáló keretprogramokat létrehozni. Javasoljuk az olvasónak, hogy egy-egy program megértése után gondolja el, hogy kedvenc programnyelvén mekkora munkával tudna azonos feladatot ellátó programot írni.

2, 3, 4, ...: többhasábos nyomtatás

Az alábbi szellemes program arra a célra szolgál, hogy a kimenet többhasábos nyomtatását tegye lehetővé: (A program állományneve furcsa módon egy számjegy.)

$ cat 2

# 2,3,...: print in multiple columns

 

pr -$0 -t -l1 $*

$ who

guest vt01 Apr 17 14:23

janos vt02 Apr 17 18:10

$ who | 2

guest vt01 Apr 17 14:2 janos vt02 Apr 17 18:1

$

Példaprogramunk nem csinál egyebet, minthogy a pr parancson engedi keresztül a kimenetet. A -t -l1 opciók révén az egyes lapokra nem kerül rá az állománynév, a dátum és a lapszám, valamint a lapméret egy sorra állítódik, ezek következtében folyamatos lesz a nyomtatás. A -$0 opcióban $0 helyére az állomány neve, jelen esetben 2 fog behelyettesítődni; -2 azonban a pr számára érvényes opció, és azt jelenti, hogy kéthasábos formában kell nyomtatnia a standard outputra. A $* megadás a shell-nek átadott összes pozicionális paramétert jelenti, azaz ha a 2 programot valamilyen opcióval, vagy bármi egyéb paraméterrel hívtuk meg, akkor az átadódik a pr programnak, s az fogja értelmezni őket. Ez a megoldás azt is illusztrálja, hogy hogyan lehet olyan keretprogramokat írni, amelyek csak a számukra fontos opciókat emelik le a paraméterek közül, a többit pedig transzparens módon átadják a feldolgozási sorban utánuk következő programnak. Ezt szemlélteti például a későbbiekben szereplő updt példaprogram, amely csak két opcionális paramétert használ fel maga, a parancssorban szereplő további opciókat pedig átadja a programsor utolsó tagjának, az ls programnak, s az állományneveket tartalmazó eredmény a megfelelő ls opciók szerinti formátumban jelenik meg. Például updt kimenete az ls parancs kimenetének felel meg, az updt -lrt parancsé viszont az ls -lrt parancs formátumával lesz azonos.

Nyilvánvaló, hogy a fenti program alkalmas tetszőleges hasábszámú nyomtatásra is, ha a kívánt hasábszámnak megfelelő néven tudjuk meghívni, hiszen a hívási név, mint a shell-nek átadott pozicionális paraméter, szolgáltatja a tördelési információt a pr parancs számára. A legegyszerűbb megoldás az lenne, ha a 2 programot a kívánt neveken lemásolnánk. Ez azonban felesleges helypocsékolás lenne, hiszen a UNIX ismeri a linkelés fogalmát, s így a már létező 2 nevű állományra tetszőleges neveken hivatkozhatunk. A linkelést megcsinálhatjuk úgy is, hogy egymás után végrehajtjuk az ln 2 3; ln 2 4; stb parancsokat. A shell ciklusszerkezetét kihasználva azonban kényelmesebben is eljárhatunk:

$ for i in 3 4 5 6; do ln 2 $i; done

$ ls -l 2 3 4 5 6

-rwxr-xr-x 5 guest other 56 Apr 17 20:15 2

-rwxr-xr-x 5 guest other 56 Apr 17 20:15 3

-rwxr-xr-x 5 guest other 56 Apr 17 20:15 4

-rwxr-xr-x 5 guest other 56 Apr 17 20:15 5

-rwxr-xr-x 5 guest other 56 Apr 17 20:15 6

$

A parancssorban megadott for ciklus az i ciklusváltozónak sorra a 3..6 értékeket adja, s az ln parancs végrehajtásakor 2 az i ciklusváltozóhoz aktuális értékéhez fog linkelődni (precízebben szólva, egy olyan állománynév jön létre a katalógusban, amely név azonos az i ciklusváltozó aktuális értékével, vagyis $i-vel).

bell: adott számú terminál hangjelzés

$ cat bell

n=${1-1}

while [ $n -gt 0 ]; do

echo '\07\c'

n=`expr $n - 1`

sleep 1

done

$

A bell program egyet csenget a terminálon, ha argumentum nélkül hívtuk meg, egyébként pedig annyit, amekkora az első argumentuma. A program első sorában a paraméterek behelyettesítésének egy gyakori esetét láthatjuk. A ${var-value} konstrukció a value értéket adja vissza, ha a változó nincs beállítva. Ily módon tehát default paramétermegadást lehet biztosítani a shell változóinak. A konkrét példában az n shell-változó értéke azonos lesz az első pozicionális paraméterrel, ha olyan létezik; ha viszont pozicionális paraméter nem volt megadva, akkor n értéke 1 lesz.

A következő programsorban egy while ciklus kezdődik: a ciklus végrehajtási feltétele a szögletes zárójelek közé zárt kifejezés; ha ennek logikai értéke igaz, azaz az n shell-változó értéke nagyobb vagy egyenlő mint nulla, akkor végrehajtódik a ciklustörzs. Figyeljük meg, hogy mivel a do kulcsszó nem külön sorban szerepel, kell eléje a végrehajtási feltétel lezárására a pontosvessző szeparátor. (Maga a [ $n -gt 0 ] feltételmegadás egy formai rövidítés, a [ ... ] forma a UNIX test parancsának jelzésére szolgál. A feltételmegadás teljes formájában kiírva tehát while test $n -gt 0 lenne.)

A következő sorban az echo parancs kiírja a 07 ASCII kódú karaktert (CTRL-G), ami a terminálon a hangjelzést generálja. A \c biztosítja az echo parancsban azt, hogy a csengőkarakter "kiírása" után ne íródjék ki feleslegesen egy újsor karakter is. A következő sorban az expr parancs kifejezésként feldolgozza az argumentumokat, amelyekkel meghívták, s az eredményként kapott értéket (jelen esetben az n változó értékénél eggyel kisebb számot) kiírja a standard outputra; a `...` metakarakterek segítségével azonban ezt az értéket argumentumként átadhatjuk az n shell-változó új értékének beállítását végző parancsnak. E kissé bonyolult módon sikerült n értékét eggyel csökkenteni.

exch: állományok cseréje

Az exch shell program két állomány nevét cseréli fel. Elsőként egy ideiglenes állománynévre nevezi át az egyik állományt, majd hármas cserét hajtva végre végül a két kiindulási állomány neve felcserélődik. A programban két említésre érdemes vonás van. Az egyik az egyedi nevű állományok létrehozásával kapcsolatos. Számos esetben lehet szüksége arra egy programnak, hogy egyedi nevű állományokat hozzon létre, például ha egyidejűleg több felhasználó is futtatja a programot, s mindegyik futó példány önálló ideiglenes állományokat kell hogy generáljon. E célra igen alkalmas a shell $$ jelölésű shell-változójának használata: ez ugyanis mindig az adott folyamat azonosítóját, a PID értékét tartalmazza. Mivel a UNIX gondoskodik arról, hogy a PID értékek egyediek legyenek, az ennek felhasználásával létrehozott állománynevek is egyediek lesznek. Az alábbi program első sorában ennek kihasználásával hoztuk létre a TMP nevű ideiglenes állományt, pontosabban az állomány nevét. A következő sorban megadott utasításokat a && karakterek választják el; ez a rövidítés azt jelenti, hogy a parancssor következő parancsa csak akkor fut le, ha az előző parancs visszatérési értéke igaz volt (0 exit státusz), azaz ha az előző program sikeresen lefutott. (E megadás végső soron egy else ág nélküli if szerkezettel azonos.) A && jelölés párja a || karakterpár, mely azt jelzi, hogy a soron következő parancs csak akkor fut le, ha az előző logikai értéke hamis volt.

$ cat exch

TMP="exch$$"

mv $1 $TMP && mv $2 $1 && mv $TMP $2

$ ls -l semmi valami; exch semmi valami; ls -l semmi valami

-rw-r--r-- 1 guest other 124 Apr 17 20:08 semmi

-rw-r--r-- 1 guest other 42 Apr 17 20:07 valami

-rw-r--r-- 1 guest other 42 Apr 17 20:07 semmi

-rw-r--r-- 1 guest other 124 Apr 17 20:08 valami

$

same_nm: azonos nevű állományok keresése

A same_nm program végignézi a /bin, /usr/bin és /etc katalógusokat, s kiírja mindazokat az állományokat, amelyek ezen katalógusok valamelyikében szerepelnek, s azonos nevűek a paraméterként megadott állománnyal.

$ cat same_nm

# same_nm -- find files of same name in /bin,/usr/bin,/etc directories

usage='usage:\tsame_nm files'

s1="/bin"

s2="/usr/bin"

s3="/etc"

case $# in

0) echo $usage; exit;;

esac

case $1 in

'-?') echo $usage; exit;;

esac

for i

do

[ -s "$s1/$i" ] && ls -l $s1/$i

[ -s "$s2/$i" ] && ls -l $s2/$i

[ -s "$s3/$i" ] && ls -l $s3/$i

done

$

A program első case szerkezetében azt vizsgáljuk, hogy hány paraméterrel lett meghívva same_nm. $# az adott program pozicionális paramétereinek számát adja meg ($0 nem számít bele); ha ez az érték nulla akkor same_nm kiírja help szövegként az előre definiált usage shell-változó tartalmát, és az exit parancs hatására terminálja magát.

A következő case szerkezet az első pozicionális paramétert vizsgálja, s ha az -?, akkor szintén a helpszöveget írja ki, majd befejeződik. Nem véletlen a '-?' minta megadásánál a '...' idézőjelpár használata: ha nem védenénk le ezáltal a -? stringet a shell-től, akkor az a ? karaktert metakarakterként értelmezné, s minden - karakterrel kezdődő argumentum hatására ezt az ágat hajtaná végre.

Végül a for ciklusban minden pozicionális paraméterre végrehajtódik a három if ág; a rövidítések miatt nehezen értelmezhető első ránézésre, de már láttuk, hogy a [ .. ] karakterek közti rész a test utasításnak felel meg, az utánuk írt && pedig az előző (test) parancs sikeres lefutása esetén hajtódik csak végre: azaz ha a "$s1/$i" nevű állomány létezik és nem nulla a hossza, akkor az ls paranccsal kiírja részletes adatait.

$ same_nm passwd

-r-sr-sr-x 1 root sys 22272 Mar 28 1990
/bin/passwd

-r--r--r-- 1 root sys 1190 Mar 13 10:08
/etc/passwd

$

updt: frissen módosított állományok listázása

Az updt program azoknak az állományoknak az adatait listázza ki (az ls opciói használhatóak), amelyeket egy adott időn belül módosítottunk. Default beállítása szerint a kiindulási katalógusban végzi el a vizsgálatot, az aznap módosított állományokat keresve. Az opciók utáni első paraméter, ha szerepel, a katalógus, ahonnan kiindulva keresünk, ezután lehet megadni az időhatárt, a find parancs jelölésmódja szerint.

 

$ cat updt

# updt -- list files updated in [dir] in the last [n] day

usage='Usage:\tupdt [-?logtasdrucifp] [dir (def:$HOME)] [[+-] day (def:0)]\n

\t\t+/-<n> means more/less than <n> days'

f=/tmp/updt.dat

until test $# -eq 0

do

case $1 in

'-?') echo $usage; exit;;

-*) flags="$flags $1"; shift;;

*) break;;

esac

done

if test "$flags"

then

find ${1-$HOME} -mtime ${2-0} -type f -print >$f

if test -s $f

then ls $flags `cat $f`

fi

rm -f $f

else

find ${1-$HOME} -mtime ${2-0} -type f -print

fi

exit

$

Az until ágban addig marad a program, amíg valamennyi pozicionális paramétert fel nem dolgozta, azaz amíg $# értéke nullára nem csökken. A paraméterfeldolgozást a case ciklus végzi, amely az első pozicionális paramétert vizsgálja, a same_nm esetében látott módon lekezeli a segélykérő -? opciót, az összes többi opciót pedig hozzáírja a flags nevű shell-változóhoz. A shift parancs arra szolgál, hogy a pozicionális változókat eggyel balra léptesse, azaz $1 törlődik, $2-ből $1 lesz, s így tovább. Amikor nem opció a pozicionális paraméter, akkor a ciklus megszakad, a további pozicionális paramétereket változatlanul hagyva.

A program további része egy if-else szerkezetbe ágyazódik. Ha nem adtunk meg opciókat (flags nullstring, ez az else ág), akkor a find parancs hajtódik végre, s a talált állományoknak pusztán az elérési neveit írja ki. Ha ellenben definiáltunk opciókat, akkor a find parancs az f ideiglenes állományba irányítja kimenetét.

Ha a find talált állományokat, akkor az f ideiglenes állomány hossza nagyobb mint nulla (ezt vizsgálja a test -s parancs), s ekkor az ls parancs argumentumlistaként megkapja a talált állományok neveit. A paraméterátadás azért történt ilyen bonyolult módon, mert ha az ls `find ...` szerkezetet használtuk volna, akkor a program nem működne helyesen. Ha ugyanis a find nem talál a feltételeknek megfelelő állományt, akkor argumentumként egy nullstringet ad át ls-nek; ls viszont ignorálja a nullstringet, s tényleges argumentum hiányában az aktuális katalógust listázza ki.

$ updt -?

Usage: updt [-?logtasdrucifp] [dir (def:$HOME)]
[[+-] day (def:0)]

+/-<n> means more/less than <n> days

$ updt

/usr/guest/.lastlogin

/usr/guest/valami

/usr/guest/vers1

/usr/guest/semmi

/usr/guest/vers2

/usr/guest/2

/usr/guest/log.log

/usr/guest/lo

/usr/guest/semmi1

/usr/guest/delta

/usr/guest/bundle

/usr/guest/same_nm

/usr/guest/updt

/usr/guest/exch

/usr/guest/bell

/usr/guest/3

/usr/guest/4

/usr/guest/5

/usr/guest/6

bundle: programcsomagok tömörítése és kibontása

Ez a program a shell programozás egyik gyöngyszeme, egyszerűsége és hasznossága révén egyaránt. Arra a célra szolgál, hogy számos apró kis shell programocskát egyetlen állományba összecsomagolva lehessen máshova elküldeni, floppyra menteni stb, s rendeltetési helyén automatikusan lehessen az eredeti programokat kicsomagolni és visszanyerni. (Az állományok nem feltétlenül kell hogy shellscript-ek legyenek, az egyetlen megkötés az, hogy ne bináris állományok legyenek.)

Az összecsomagolandó állományokat egyszerűen a bundle file(s) paranccsal lehet összevonni (defaultban bundle is a standard outputra ír). Kicsomagoláskor, ha mondjuk pkg volt a csomag neve, az sh pkg parancs automatikusan létrehozza az eredeti állományokat.

$ cat bundle

# bundle: group files into distribution package

 

echo '# To unbundle, sh this file'

for i; do

echo "echo $i 1>&2"

echo "cat >$i <<'=== End of $i ==='"

cat $i

echo "=== End of $i ==="

done

$

 

$ mkdir newdir; bundle bundle 2 same_nm
>newdir/package.pkg;

$ cd newdir; ls -l

total 2

-rw-r--r-- 1 guest other 769 Apr 17 20:20
package.pkg

$

 

 

$ cat package.pkg

# To unbundle, sh this file

echo bundle 1>&2

cat >bundle <<'=== End of bundle ==='

# bundle: group files into distribution package

 

echo '# To unbundle, sh this file'

for i do

echo "echo $i 1>&2"

echo "cat >$i <<'=== End of $i ==='"

cat $i

echo "=== End of $i ==="

done

=== End of bundle ===

echo 2 1>&2

cat >2 <<'=== End of 2 ==='

# 2,3,...: print in multiple columns

 

pr -$0 -t -l1 $*

=== End of 2 ===

echo same_nm 1>&2

cat >same_nm <<'=== End of same_nm ==='

# same_nm -- find files of same name in /bin,/usr/bin,/etc directories

usage='usage:\tsame_nm files'

s1="/bin"

s2="/usr/bin"

s3="/etc"

case $# in

0) echo $usage; exit;;

esac

case $1 in

'-?') echo $usage; exit;;

esac

for i

do

[ -s "$s1/$i" ] && ls -l $s1/$i

[ -s "$s2/$i" ] && ls -l $s2/$i

[ -s "$s3/$i" ] && ls -l $s3/$i

done

=== End of same_nm ===

$

 

$ sh package.pkg; ls -l

bundle

2

same_nm

total 8

-rw-r--r-- 1 guest other 56 Apr 17 20:21 2

-rw-r--r-- 1 guest other 175 Apr 17 20:21 bundle

-rw-r--r-- 1 guest other 769 Apr 17 20:20 package.pkg

-rw-r--r-- 1 guest other 343 Apr 17 20:21
same_nm

$

A bundle működése azon a trükkön alapszik, hogy maga is egy végrehajtható shellscript-et generál, amelyben a kicsomagoló parancsok, illetve az összecsomagolt programok szövege is benne van, s ezeket a "vételi oldalon" a beágyazott input mechanizmusa segítségével állítja helyre.

- Első lépésként a keletkező állományba egy shell commentet tesz be, ez arra jó, hogy a vételi oldalon tájékoztassa a felhasználót, hogyan is kell kicsomagolnia a keletkező állományt.

- A továbbiakban egy for ciklus révén a tömörítendő valamennyi állományra végrehajtja az alábbiakat:

- Beír a keletkező állományba egy olyan parancsot, amely a standard error csatornára kiírja az aktuális állomány nevét (itt ugyanis a shell már kifejtette a $i paramétert, tehát egy konkrét állománynév szerepel);

- beír a keletkező állományba egy olyan parancsot, amelyik beágyazott inputként az aktuális állományba írja az === End of állománynév === stringig terjedő részt;

- most a keletkező állományba beírja magát a továbbítandó állományt;

 

Tartalomjegyzék