A minap belebotlottam egy apró érdekességbe shell script készítése közben. Elsőre furcsállottam a dolgot, de aztán kis utánajárással sikerült megoldani a problémát, és végül már egyértelművé is vált a dolog. Gondoltam megosztom, hátha másnak is hasznára válik, és meg tud spórolni ezzel egy kis keresgélést.
Először működő példának vegyünk egy egyszerű while ciklust, ami előtt beállítunk egy számlálót 0-ra, majd a ciklusban léptetjük ezt a számlálót és kiiratjuk az értéket, végül a ciklus után is kiírjuk a változó értékét.
1 2 3 4 5 6 7 #!/bin/bash c=0 # Változó kezdőérték while [[ $c < 5 ]] ; do # ciklus indul (( c++ )) # számláló növelése echo "$c. futás" done echo "Számláló értéke: $c" # változó kiiratása a cikluson kívül.
A példa pontosan azt is csinálja, amire számítunk. A kimenet pedig:
1. futás 2. futás 3. futás 4. futás 5. futás Számláló értéke: 5
Tehát itt nincs semmi furcsaság, úgy működik, ahogyan kell. De mi van akkor, ha például egy könyvtárban lévő fájlokon szeretnénk végighaladni, amikkel műveleteket szeretnénk végezni, és utólag feldolgozni vagy megjeleníteni a ciklusban használt és módosított változókat
A probléma
Adott tehát egy könyvtár, amiben most a tesztünk ideje alatt van négy darab fájl. Ezeken szeretnénk végighaladni, és különböző műveleteket végezni, majd a ciklus végeztével megjeleníteni a ciklusban összegyűjtött adatokat.
Az egyszerűség kedvéért most itt is csak növelünk egy számlálót a ciklusban, aminek lépésenként kiírjuk az értékét, utána a talált fájl nevét. Végül pedig a cikluson kívül ugyanígy kiiratjuk a számláló értékét:
1 2 3 4 5 6 7 8 #!/bin/bash c=0 # változó kezdőérték find . -type f | # fájlok keresése a könyvtárban és csővezetéken a ciklusba továbbítása while read fn; do # ciklus indul (( c++ )) # számláló növelése echo "$c. futás: $fn" # a számláló és a fájlnév kiiratása done echo "Számláló értéke: $c" # változó kiiratása a cikluson kívül.
Itt a kimenetünk már érdekesebb, mivel a ciklus után a kiiratott számláló értéke 0:
1. futás: ./probafile_3 2. futás: ./valtozok_elerese_ciklusokban.sh 3. futás: ./probafile_2 4. futás: ./probafile_1 Számláló értéke: 0
A ciklus rendesen tette a dolgát, csak az utána lévő számláló kiíratásnál jött elő a furcsaság.
Megoldás
Elsőre amikor belebotlik az ember, okozhat egy kis fejtörést ez a jelenség, de miután megvan a megoldás, már teljesen egyértelmű a dolog.
Ennek az érdekessége, abban rejlik, hogy a bash a csővezetékekben a parancsokat külön szálakon futtatja, azaz subshell-ekben. Idézet a bash man oldalából: "Each command in a pipeline is executed as a separate process (i.e., in a subshell).". A subshell-ekben lévő változók pedig megsemmisülnek, miután lefutottak benne a parancsok. Alapból nem lenne gond a subshell-el, de jelen esetben ez rossz helyzetben van, mivel csak a while ciklusunkra terjed ki, ahonnan később ki szeretnénk nyerni a változóink értékét. Így tehát a ciklus végeztével elvesznek az abban használt változóink, mivel azután a subshell megsemmisül.
Nem gondol feltétlenül ilyesmire az ember ciklusok írása közben, amíg egyszer csak szüksége nem lesz a benne lévő változók értékére a ciklus után is. De így már teljesen más a dolog fekvése, a megoldás innentől már egyszerű: Szervezzük külön blokkba a ciklust és az utána lévő részt, ahol szükség van a belső változók értékére:
1 2 3 4 5 6 7 8 9 #!/bin/bash c=0 # változó kezdőérték find . -type f | { # fájlok keresése a könyvtárban és csővezetéken a ciklusba továbbítása while read fn; do # ciklus indul (( c++ )) # számláló növelése echo "$c. futás: $fn" # a számláló és a fájlnév kiiratása done echo "Számláló értéke: $c" # változó kiiratása a cikluson kívül. }
Itt tehát annyit változtattunk, hogy a csővezeték utáni részt külön kódblokkba {} helyeztük, amiben a számláló kiiratása is benne van. Így az egész egy parancsnak minősül a csővezeték – és így a subshell – szempontjából. A kimenetünk pedig a következő:
1. futás: ./probafile_3 2. futás: ./valtozok_elerese_ciklusokban.sh 3. futás: ./probafile_2 4. futás: ./probafile_1 Számláló értéke: 4
Ez így működik is szépen, a ciklus után is hozzáférünk a benne létrehozott vagy módosított változókhoz, amíg még ugyanebben a kódblokkban vagyunk.
De mi van ha hosszú vagy összetett a scriptünk, amiben például jóval a ciklus után lenne szükség a változókra, így nem szeretnénk a további részeket is beágyazni ebbe a kódblokkba? Erre is van megoldás.
Alternatív megoldás
Ha nem szeretnénk felborítani a kódunk struktúráját és logikáját, akkor ezt a feladatot megoldhatjuk másképp is, mégpedig a csővezeték használatának elkerülésével. Erre lesz segítségünkre a here string-es kivitelezés, aminek a lényege, hogy nem használunk csővezetéket, így ugyanebben a futási szálban adhatjuk át az elemeket a ciklusnak:
1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash c=0 # változó kezdőérték while read fn; do # ciklus indul (( c++ )) # számláló növelése echo "$c. futás: $fn" # a számláló és a fájlnév kiiratása done <<< "$(find . -type f)" # ciklus elemek átadása a herestring-el # további kódrészek ugyanebben a kódblokkban # ... echo "Számláló értéke: $c" # változó kiiratása jóval a ciklus után.
Bár most a fájlkeresést oldottuk meg subshell-es lekérdezéssel, itt mégsem zavar be, mivel maga a ciklus és az azt követő kódrészek ugyanabban a shellben futnak, így a változók később is elérhetők maradnak a script további részében. A fájlok listájának lekérésénél pedig nem számít, hogy nem ugyanazon a szálon lett lefuttatva a find parancs.
A kimenet pedig pontosan megegyezik az előzővel:
1. futás: ./probafile_3 2. futás: ./valtozok_elerese_ciklusokban.sh 3. futás: ./probafile_2 4. futás: ./probafile_1 Számláló értéke: 4
Tehát láthatjuk, hogyan kerülhetjük el a csővezeték használatát, amikor annak egy nem kívánatos mellékhatásába ütközünk, ami miatt feleslegesen bonyolítani kellene a kódunkat.
Így utólag már teljesen logikus a dolog, és ezek után legközelebb biztosan eszébe jut az embernek, hogy hogyan tudja egyből elkerülni a csővezetékek okozta kellemetlenségeket.
- A hozzászóláshoz regisztráció és bejelentkezés szükséges
- 307 megtekintés