Shell-programozás

A Unix/Linux szerverek üzemeltetése wikiből
(Változatok közti eltérés)
a (Konfigurálható script)
a (új link: http://sed.sourceforge.net/grabbag/scripts/turing.sed)
 
(2 szerkesztő 25 közbeeső változata nincs mutatva)
43. sor: 43. sor:
 
Ez a szintaxis amúgy egy sereg további lehetőséget rejt magában; írhatunk mindenféle mágikus karaktert a változónév elé és mögé, amitől érdekesebbnél érdekesebb dolgok történhetnek. Hamarosan látunk erre példát.
 
Ez a szintaxis amúgy egy sereg további lehetőséget rejt magában; írhatunk mindenféle mágikus karaktert a változónév elé és mögé, amitől érdekesebbnél érdekesebb dolgok történhetnek. Hamarosan látunk erre példát.
   
Ha nem tudjuk, hogy a változó értékben nem lehet szóköz vagy egyéb speciális karakter, mindig idézőjelek között hivatkozzunk rá!
+
Ha nem tudjuk, hogy a változó értékében nem lehet szóköz vagy egyéb speciális karakter, mindig idézőjelek között hivatkozzunk rá!
   
 
Az alábbi script üres $1 esetén hibaüzenetet ad, szóközt tartalmazó $1 esetén pedig hülyeséget csinál; ha ügyesek vagyunk, ilyen jellegű hibákat tartalmazó scriptbe akár kódot is injektálhatunk alkalmas paraméterezéssel. A zsh az ilyen hibák egy részétől megvéd (mert külön kérés nélkül általában nem bontja szavakra a változók értékeit, amikor behelyettesíti őket), de ez nem jelenti azt, hogy a zsh-ban szabadon lehagyható az idézőjel az ismeretlen tartalmú változó neve mellől!
 
Az alábbi script üres $1 esetén hibaüzenetet ad, szóközt tartalmazó $1 esetén pedig hülyeséget csinál; ha ügyesek vagyunk, ilyen jellegű hibákat tartalmazó scriptbe akár kódot is injektálhatunk alkalmas paraméterezéssel. A zsh az ilyen hibák egy részétől megvéd (mert külön kérés nélkül általában nem bontja szavakra a változók értékeit, amikor behelyettesíti őket), de ez nem jelenti azt, hogy a zsh-ban szabadon lehagyható az idézőjel az ismeretlen tartalmú változó neve mellől!
87. sor: 87. sor:
 
Nyilván szeretnénk olyasmi argumentumfeldolgozást, mint a GNU programokban: --hosszú-opció=érték stb.
 
Nyilván szeretnénk olyasmi argumentumfeldolgozást, mint a GNU programokban: --hosszú-opció=érték stb.
   
Erre legalább két jó módszer van; általában rossz módszer az, ha előírjuk, hogy mi legyen az első, a második, harmadik stb. argumentum szerepe. Jobb a felhasználóra bízni, milyen sorrendben kényelmes neki megadni a kapcsolókat (pl. lehet, hogy nem az összeset akarja használni).
+
Erre legalább négy jó módszer van; általában rossz módszer az, ha előírjuk, hogy mi legyen az első, a második, harmadik stb. argumentum szerepe. Jobb a felhasználóra bízni, milyen sorrendben kényelmes neki megadni a kapcsolókat (pl. lehet, hogy nem az összeset akarja használni: mondjuk az első hármat kihagyná).
   
Az egyik jó módszer: <tt>getopts</tt> (bashben is van, de lehet, hogy nem pontosan úgy működik, mint az itt bemutatott zsh-féle).
+
Az egyik jó módszer: <tt>getopts</tt> (<tt>bash</tt>ben is van, de lehet, hogy nem pontosan úgy működik, mint az itt bemutatott zsh-féle).
   
 
<pre>
 
<pre>
#!/bin/zsh -e
+
#!/bin/zsh
 
#
 
#
 
# usage:
 
# usage:
190. sor: 190. sor:
   
 
* Látható, hogy így tudunk hosszú kapcsolókat is kezelni, nemcsak rövideket, meg olyan kapcsolókat is, amelyeknek két argumentuma van, stb.
 
* Látható, hogy így tudunk hosszú kapcsolókat is kezelni, nemcsak rövideket, meg olyan kapcsolókat is, amelyeknek két argumentuma van, stb.
  +
  +
Egy harmadik lehetőség az, hogy a script viselkedését befolyásoló beállításokat nem kapcsolók, hanem környezeti változók formájában várjuk; ez kicsit körülményes a felhasználónak, cserébe teljesen transzparens módon örökli az összes ilyen beállítást az összes gyermekfolyamatunk (azokat is, amiket mi magunk nem is értünk). Ezt a módszert leginkább akkor érdemes használni, ha wrappert írunk valami olyan program köré, ami eleve környezeti változókból veszi a beállításait.
  +
  +
Negyedik lehetőség: a zsh-hoz van egy <tt>zutil</tt> nevű "modul" (plugin), amiben van egy <tt>zparseopts</tt> parancs. Ez általános megoldás a rövid és hosszú kapcsolók és ezek opcionális argumentumainak a kezelésére.
   
 
== Alapértelmezések használata ==
 
== Alapértelmezések használata ==
248. sor: 252. sor:
   
 
* Az alapértelmezés szerinti +e állapotban a script nem lép ki, ha valamelyik parancssor végrehajtása sikertelen (de amúgy szintaktikailag helyes).
 
* Az alapértelmezés szerinti +e állapotban a script nem lép ki, ha valamelyik parancssor végrehajtása sikertelen (de amúgy szintaktikailag helyes).
  +
* -u: hibának számít, ha olyan változóra hivatkozunk, amelynek nem adtunk értéket.
 
* -x: minden parancssort kiír a script végrehajtása során.
 
* -x: minden parancssort kiír a script végrehajtása során.
 
** Hibakereséskor nagyon hasznos.
 
** Hibakereséskor nagyon hasznos.
277. sor: 282. sor:
   
 
A <tt>find(1)</tt> nemcsak fájlok és könyvtárak keresésére jó, hanem arra is, hogy egy műveletet sok, valamilyen tulajdonság alapján kiválasztott fájlon elvégezzünk.
 
A <tt>find(1)</tt> nemcsak fájlok és könyvtárak keresésére jó, hanem arra is, hogy egy műveletet sok, valamilyen tulajdonság alapján kiválasztott fájlon elvégezzünk.
Erre kézenfekvő megoldás a ciklus: <pre>for i in $(find . -uid 1001); do chown 1002 "$i"; done</pre> de ennél kifinomultabbak is lehetünk, legalább háromféleképpen:
 
   
* Az <tt>xargs</tt> segítségével. <pre>find . -uid 1001 -print0 | xargs -0 chown 1002</pre>
+
Sajnos abból adódóan, hogy a fájlnevekben szinte bármi lehet, sokmindenre oda kell figyelni. Például az, hogy
  +
  +
<pre>
  +
cat $(find . -type f) > ../collection
  +
</pre>
  +
  +
nem fog helyesen működni, ha a fájlnevekben szóköz, TAB vagy újsor karakter van, mert a shell ezek mentén a karakterek mentén vágja szavakra a <tt>find(1)</tt> kimenetét, tehát egy egy szóközt tartalmazó fájlnevet két fájlnévként fog átadni a cat-nek.
  +
  +
További hátránya ennek a megoldásnak, hogy ha nagyon sok a találat, túl hosszúvá válik a <tt>cat</tt>-nek átadott parancssor, ami vagy nagyon nagy memóriaigényhez, vagy hibához vezet (ha a kernel korlátozza a parancssor hosszát).
  +
  +
Egy másik kézenfekvő megoldás a ciklus: <pre>for i in $(find . -uid 1001); do chown 1002 "$i"; done</pre> de egyrészt ez is hibásan működik szóközt, TABot vagy újsort tartalmazó névre, másrészt szintén nagy lehet a memóriaigénye, ha sok a találat (hiszen továbbra is egyetlen listába kell őket összegyűjteni a <tt>for</tt> számára); és, amire sokan nem gondolnak: ha a find talál mondjuk egy <tt>*</tt> nevű fájlt, akkor a legtöbb shellben az adott fájlt tartalmazó könyvtárban levő ''összes'' fájlra le fog futni a <tt>chown</tt>. (<tt>zsh</tt>-ban nem, mert az alapból nem értelmezi a $(parancs) konstrukcióval meghívott parancsok kimenetében megjelenő glob karaktereket, tehát erre az aknára <tt>zsh</tt>-ban nehezebb rálépni.)
  +
  +
Próbálkozhatnánk <tt>while</tt> ciklussal is:
  +
  +
<pre>
  +
find . -uid 1001 | while read i; do chown 1002 "$i"; done
  +
</pre>
  +
  +
Ez szóközöket egyesével tartalmazó fájlnévekre jól működik és a globbinggal sem lőjük magunkat lábon, de nem működik helyesen újsort tartalmazó fájlnévre és akkor sem, ha egy fájlnév szóközzel vagy TABbal kezdődik vagy arra végződik; és akkor sem, ha egy fájlnévben több egymást követő szóköz van (a <tt>read</tt> levágja ill. a <tt>zsh</tt>-t kivéve összetömöríti egyetlen szóközzé).
  +
  +
A <tt>read</tt> parancs ráadásul a \-t is speciálisan értelmezi, így pl., ha egy fájlnév \-re végződik, akkor a while ciklusunk összevonja a következő fájlnévvel. <tt>bash</tt>-ben és <tt>zsh</tt>-ban ezt elkerülhetjük, ha a <tt>-r</tt> kapcsolóval hívjuk a <tt>read</tt>-et.
  +
  +
Kifinomultabbak is lehetünk, legalább háromféleképpen:
  +
  +
* A GNU <tt>xargs</tt> segítségével (a POSIX xargs használata fájdalmas). <pre>find . -uid 1001 -print0 | xargs -0 chown 1002</pre>
 
* Ez azért jobb, mert
 
* Ez azért jobb, mert
** csak annyi chown-parancssor fog generálódni, amennyi szükséges (nem annyi, ahány találat van), valamint
+
** nem annyiszor fog lefutni a chown, ahány találat van, hanem jóval kevesebbszer; valamint
 
** biztosan helyesen működik akkor is, ha a megtalált fájlok nevében szóköz, enter, idézőjel vagy egyéb nyalánkság van.
 
** biztosan helyesen működik akkor is, ha a megtalált fájlok nevében szóköz, enter, idézőjel vagy egyéb nyalánkság van.
 
* A <tt>find</tt> beépített mechanizmusaival. <pre>find . -uid 1001 -exec chown 1002 {} \;</pre>
 
* A <tt>find</tt> beépített mechanizmusaival. <pre>find . -uid 1001 -exec chown 1002 {} \;</pre>
 
** Ezt a megoldást nem nagyon szeretem, mert nem lehet tetszőlegesen rugalmasan összeállítani a végrehajtandó parancssort, mert a find közvetlenül hívja meg a megadott parancsot, nem a shellen keresztül. Végeredményben egyetlen programot hívhatunk meg, aminek a parancssorába valahová beszúrhatjuk a megtalált fájl nevét.
 
** Ezt a megoldást nem nagyon szeretem, mert nem lehet tetszőlegesen rugalmasan összeállítani a végrehajtandó parancssort, mert a find közvetlenül hívja meg a megadott parancsot, nem a shellen keresztül. Végeredményben egyetlen programot hívhatunk meg, aminek a parancssorába valahová beszúrhatjuk a megtalált fájl nevét.
 
* <pre>find . -uid 1001 -exec chown 1002 {} +</pre>
 
* <pre>find . -uid 1001 -exec chown 1002 {} +</pre>
** Ez majdnem ugyanaz, mint az <tt>xargs</tt>-os megoldás, azzal a különbséggel, hogy a megtalált fájlok listája nem kell, hogy a parancssor végén legyen.
+
** Ez majdnem ugyanaz, mint az <tt>xargs</tt>-os megoldás, azzal a különbséggel, hogy nem futtatunk <tt>xargs</tt> processzt. Sajnos a megtalált fájlok listája továbbra is a felépített parancssor végén kell, hogy legyen.
 
* A <tt>find printf</tt> akciója segítségével konstruálhatunk parancssorokat: <pre>find . -uid 1001 -printf "chown 1002 %f\n"|sh</pre>
 
* A <tt>find printf</tt> akciója segítségével konstruálhatunk parancssorokat: <pre>find . -uid 1001 -printf "chown 1002 %f\n"|sh</pre>
** Ez talán a legrugalmasabb megoldás, viszont annyi parancssorunk lesz, ahány találatunk.
+
** Ez talán a legrugalmasabb megoldás, viszont annyi parancssorunk lesz, ahány találatunk, és vigyáznunk kell a speciális karakterekkel -- pl. egy fájlnév végén levő backslash karakter könnyen vasrudat dughat scriptünk kerekeinek küllői közé (elnézést).
   
 
Néhány konkrét példa a <tt>find</tt> használatára:
 
Néhány konkrét példa a <tt>find</tt> használatára:
323. sor: 327. sor:
 
IFS="
 
IFS="
 
"
 
"
# Itt jobb lenne a NUL karaktert használni, mert az nem szerepelhet fájlnévben,
+
# Itt jobb lenne a NUL karaktert használni (read -d ''), mert az nem szerepelhet
# míg újsor igen.
+
# fájlnévben, míg újsor igen.
   
 
# Keressük a megadott alkönyvtárakban az összes fájlt, amelynek mérete a megadott
 
# Keressük a megadott alkönyvtárakban az összes fájlt, amelynek mérete a megadott
346. sor: 350. sor:
 
# Mi ennek a fájlnak a teljes elérési útja? A symlink létrehozásához kell
 
# Mi ennek a fájlnak a teljes elérési útja? A symlink létrehozásához kell
 
fullname="$(readlink -f "$file")"
 
fullname="$(readlink -f "$file")"
# Kis voodoo: állítsuk elő a symlink nevét hash.utolsókönyvtárnév_fájlnév.link alakban
+
# Ha még nincs az eredeti fájlra mutató symlink a hashről elnevezett fájl mellett, csináljunk.
ln -sf "$fullname" "$DESTDIR/${SUM}.${fullname:h:t}_${file:t}.link"
+
ls -l $DESTDIR | grep -q -- "-> $fullname"'$' || {
  +
# Kis voodoo: állítsuk elő a symlink nevét hash.valamiszám.eredetikiterjesztés alakban
  +
ln -sf "$fullname" "$DESTDIR/${SUM}.${LINKCOUNT}.${fullname:e}"
  +
((LINKCOUNT++))
  +
}
 
fi
 
fi
 
done
 
done
354. sor: 358. sor:
 
Itt láttunk többféle varázslatot is (ezeket nem kell megtanulni, csak tudni arról, hogy ilyesmik vannak, és amikor kellenek, ki lehet nézni őket a dokumentációból):
 
Itt láttunk többféle varázslatot is (ezeket nem kell megtanulni, csak tudni arról, hogy ilyesmik vannak, és amikor kellenek, ki lehet nézni őket a dokumentációból):
   
* <pre>if [[ "${file//..hardlinks/}" = "$file" ]]; then</pre> megnézi, hogy ha a $file tartalmából kihagyjuk azt, hogy ..hardlinks, akkor ugyanazt kapjuk-e (tehát szerepelt-e benne ez a sztring; hiszen ha szerepelt, akkor elhagyva belőle nem ugyanazt kapjuk). Ezzel egy <tt>grep</tt>et helyettesítettünk, és mivel így a ciklusmagban úsztunk meg egy külsőprogram-futtatást, jelentős az időnyereség.
+
* <pre>if [[ "${file//..hardlinks/}" = "$file" ]]; then</pre> megnézi, hogy ha a $file tartalmából kihagyjuk azt, hogy ..hardlinks, akkor ugyanazt kapjuk-e (tehát szerepelt-e benne ez a sztring; hiszen ha szerepelt, akkor elhagyva belőle nem ugyanazt kapjuk). Ezzel egy <tt>grep</tt>-et helyettesítettünk, és mivel így a ciklusmagban úsztunk meg egy külsőprogram-futtatást, jelentős az időnyereség.
 
* <pre>readlink -f "$file"</pre>ezzel tudunk "kanonicizálni" egy útvonalat (relatívból abszolút, symlinkek kiiktatva). Sajnos külső program, nem shell builtin.
 
* <pre>readlink -f "$file"</pre>ezzel tudunk "kanonicizálni" egy útvonalat (relatívból abszolút, symlinkek kiiktatva). Sajnos külső program, nem shell builtin.
* <pre>${fullname:h:t}_${file:t}</pre> a <tt>dirname</tt> és a <tt>basename</tt> megúszása zsh-san. A :h csak az útvonalat hagyja meg a változó fájlnévként értelmezett tartalmából, a :t pedig csak a fájlnevet. Az útvonalra alkalmazva a :t-t az adott fájl elérési útjában szereplő utolsó könyvtárnevet kapjuk.
+
* <pre>${fullname:h:t}_${file:t}</pre> a <tt>dirname</tt> és a <tt>basename</tt> megúszása zsh-san. A :h csak az útvonalat hagyja meg a változó fájlnévként értelmezett tartalmából, a :t pedig csak a fájlnevet. Az útvonalra alkalmazva a :t-t az adott fájl elérési útjában szereplő utolsó könyvtárnevet kapjuk. (Ez a konstrukció a script fenti változatában nem szerepel, de érdemes ismerni.)
  +
* <pre>${fullname:e}</pre> a $fullname tartalmából az utolsó pont utáni részt adja vissza (vagyis a "kiterjesztést").
   
 
Töröljük most ebből a ..hardlinks könyvtárból azokat a fájlokat, amelyekhez már nem tartozik másik hardlink!
 
Töröljük most ebből a ..hardlinks könyvtárból azokat a fájlokat, amelyekhez már nem tartozik másik hardlink!
   
<pre>find ..hardlinks -type f -links 1 -print0 | xargs -0 rm</pre>
+
<pre>find ..hardlinks -type f -links 1 -execdir rm -f {} +</pre>
   
A find még rengeteg szempont alapján tud keresni: jogosultságok, dátumok, reguláris kifejezések stb. Részletesebben l. <tt>man find</tt>.
+
Nézzünk egy másik lehetséges (sokkal gyorsabb, de kevesebb utólagos diagnosztikát lehetővé tevő) megoldást a fenti hardlinkelős problémára!
  +
  +
<pre>
  +
#!/bin/zsh
  +
#
  +
# Usage: ln-dup-files dir [dir2 [...]]
  +
  +
# MYIFS egyetlen soremelésből áll; itt is jobb lenne NULL-t használni, mert így helytelenül működik a script,
  +
# ha vannak újsort tartalmazó fájlnevek
  +
MYIFS="
  +
"
  +
SHAIFS=" " # szóköz
  +
OLDIFS="$IFS" # elmentjük az eredetit, hogy helyreállíthassuk (bár itt nem kell)
  +
  +
zmodload zsh/files # Hogy az ln(1) belső parancs legyen, ne külső
  +
zmodload zsh/stat # Hogy legyen belső zstat parancsunk (ezzel majd inode-számokat meg ilyesmiket kérdezünk le)
  +
  +
saved=0 # Legfeljebb ennyi byte-ot takarítottunk meg eddig
  +
  +
typeset -A metadata # Egy olyan hash, amely mode-uid-gid-size-sha1sum n-esekhez inode-fájlnév párokat rendel
  +
typeset -A seen_inodes # Egy olyan hash, amelyben sha1sum-okat és az inode-ok egyéb adatait cache-eljük. A kulcs az inode-szám, az adat a jogok, tulajdonos, méret, sha1sum.
  +
  +
find $@ -type f -size +1c -printf "%p\n" | while read file; do
  +
zstat -o -H stat "$file" # A stat nevű hash-be rakja bele $file jellemzőit
  +
statstr="$seen_inodes[$stat[inode]]" # Lássuk, megvan-e a cache-ben
  +
if [[ -z "$statstr" ]]; then # nincs
  +
IFS="$SHAIFS"
  +
sha1sum "$file" | read SUM foo # Nincs gond a fájlnévben (ill. leginkább az elején) levő szóközökkel: $foo értékét nem használjuk
  +
IFS="$MYIFS"
  +
statstr=$stat[mode]_$stat[uid]_$stat[gid]_$stat[size]_$SUM
  +
seen_inodes[$stat[inode]]=$statstr # Berakjuk a cache-be; az adott inode-hoz az imént konstruált sztring tartozik, ami egyben kulcs a metadata hash-hez
  +
fi
  +
# Nézzük meg most, hogy láttunk-e már olyan fájlt, amit ezzel a mostanival össze lehetne hardlinkelni!
  +
echo "$metadata[$statstr]" | {
  +
read inode
  +
read name
  +
} # Azért így, mert újsor választja el egymástól az inode-ot és a nevet; cserébe az újsort tartalmazó fájlnevekkel nem boldogulunk. Ezt a problémát kiküszöbölhetnénk pl. úgy, hogy a fájlnévben nem szerepeltethető "/" karakterrel választanánk el az inode-ot és a nevet, de akkor rondább lenne az a kód, ami értelmezi a metadata hash-ben tárolt adatokat.
  +
if [[ -n "$inode" ]]; then # Tartozik-e elmentett inode ehhez a statstr-hez?
  +
if ! [[ "$inode" = "$stat[inode]" ]]; then # Ha igen: az a file, amit most találtunk, már össze van vele hardlinkelve, tehát ugyanaz az inode-számuk?
  +
echo ln -f "$name" "$file" "$[stat[size]]" # Nem: összehardlinkeljük őket (itt még csak kiírjuk, hogy a felhasználó lássa, mit csinálunk)
  +
((saved+=$[stat[size]])) # És nyomon követjük, mennyi helyet spórolunk meg
  +
ln -f "$name" "$file"
  +
fi
  +
else
  +
metadata[$statstr]="$stat[inode]$MYIFS$file" # Most látunk ilyen file-t először; elmentjük az inode-számát és az elérési útját
  +
fi
  +
done
  +
echo "Saved up to $saved bytes."
  +
</pre>
  +
  +
Főbb különbségek a két script között:
  +
  +
* A második jóval gyorsabb, mert az <tt>sha1sum</tt>-on kívül nem hív külső parancsot a ciklusmagban (az elsőben volt <tt>cut</tt>, <tt>ls</tt>, <tt>grep</tt>, <tt>mkdir</tt> és <tt>readlink</tt> is).
  +
** Cserébe nem készít <tt>..hardlinks</tt> könyvtárat; emiatt utólag csak az inode-ok alapján keresve állapítható meg, mit hardlinkelt össze mivel.
  +
* A második script figyelembe veszi a fájlok jogosultságait és tulajdonosát is (de POSIX ACL-jeiket és bővített attribútumaikat nem); az első csak a tartalmukat nézi.
  +
** Nem lenne túl nehéz felkészíteni a scriptet a POSIX ACL-ek és az attribútumok kezelésére, csak nagyon lelassítaná, mert ezeket is külső programmal kellene lekérdezni, aztán mondjuk hash-elni és ezt a hasht is hozzáfűzni a <tt>statstr</tt>-hez.
  +
* A második script memóriaigénye a vizsgált fájlok számával arányos; az elsőé független tőle.
  +
  +
Vegyük észre:
  +
  +
* A hash nagyon hasznos eszköz, de ezzel a felhasználással már a határát súroljuk annak, amit shellben értelmesen ki lehet fejezni; összetettebb feladatoknál nagyon hiányzik a struct ("rekord") adattípus és a pointerek.
  +
** Pl. jó lenne olyan hasht csinálni, amelynek az elemei is hash-ek, de nem lehet (vagy legalábbis szépen nem lehet).
  +
* A második scriptben további optimalizációk lennének lehetségesek. Pl:
  +
** Előfordulhat, hogy egy fájl sha1 hash-ét feleslegesen számoljuk ki. Elég lenne ezt akkor megtenni, ha találunk másik olyan fájlt, amelyiknek a mérete, tulajdonosa és jogosultságbitjei ugyanilyenek, de az inode-ja különözik (tehát egyáltalán felmerül a hardlinkelés lehetősége).
  +
** A megtakarított hely mennyiségére csak felső becslést adunk, mert minden egyes alkalommal, amikor hardlinket készítünk, hozzáadjuk a kérdéses fájl méretét az esetleges megtakarított byte-ok számához -- holott ezt csak akkor tehetnénk meg, ha a hardlinkelés következtében a régi inode referenciaszámlálója nullára csökken.
  +
* Mindkét script természetesen egy-egy hatalmas versenyhelyzet: garantálni kell, hogy a vizsgált fájlok tartalma a futás ideje alatt ne változhasson.
  +
  +
A find egyébként még rengeteg szempont alapján tud keresni: jogosultságok, dátumok, reguláris kifejezések stb. Részletesebben l. <tt>man find</tt>.
   
 
== Barátunk, a <tt>sed</tt> ==
 
== Barátunk, a <tt>sed</tt> ==
390. sor: 394. sor:
 
** Vannak előredefiniált karakterosztályok, pl. <tt><nowiki>[[:alnum:]]</nowiki></tt>; ezeket kombinálhatjuk egyéb karakterekkel: <tt>[,.%:[:alnum:]_/*]</tt>. Ezeknek az az előnye, hogy locale-függőek, tehát pl. magyar locale esetén az <tt><nowiki>[[:alpha:]]</nowiki></tt> osztálynak részei a magyar ékezetes betűk is, míg angol locale esetén nem.
 
** Vannak előredefiniált karakterosztályok, pl. <tt><nowiki>[[:alnum:]]</nowiki></tt>; ezeket kombinálhatjuk egyéb karakterekkel: <tt>[,.%:[:alnum:]_/*]</tt>. Ezeknek az az előnye, hogy locale-függőek, tehát pl. magyar locale esetén az <tt><nowiki>[[:alpha:]]</nowiki></tt> osztálynak részei a magyar ékezetes betűk is, míg angol locale esetén nem.
 
* A <tt>.</tt> (pont) karakter egy darab tetszőleges karakterre illeszkedik.
 
* A <tt>.</tt> (pont) karakter egy darab tetszőleges karakterre illeszkedik.
  +
** Figyelem! A karakter és a byte nem ugyanaz. Az LC_CTYPE környezeti változó értékétől függ, milyen kódolást használnak a(z erre felkészített) programjaink. A Unicode-ban vannak olyan byte-sorozatok, amelyekhez nem tartozik karakter. Az ilyen "érvénytelen karakterekre" a <tt>.</tt> nem illeszkedik!
 
* A <tt>^</tt> (kalap) a sor elejére illeszkedik, tehát pl. a <tt>^a</tt> minden olyan sorra illeszkedik, amely a-betűvel kezdődik.
 
* A <tt>^</tt> (kalap) a sor elejére illeszkedik, tehát pl. a <tt>^a</tt> minden olyan sorra illeszkedik, amely a-betűvel kezdődik.
* A <tt>$</tt> (dollárjel) a sor végére illeszkedik, tehát pl. a <tt>^a$</tt> minden olyan sorra illeszkedik, amely pontosan egy darab a-betűt tartalmaz.
+
* A <tt>$</tt> (dollárjel) a sor végére illeszkedik, tehát pl. a <tt>^a$</tt> minden olyan sorra illeszkedik, amely pontosan egy darab a-betűből áll.
 
** Ritkábban szokott kelleni, de létezik: <tt>\<</tt> a szó eleje, <tt>\></tt> a szó vége, a fentiekhez hasonlóan.
 
** Ritkábban szokott kelleni, de létezik: <tt>\<</tt> a szó eleje, <tt>\></tt> a szó vége, a fentiekhez hasonlóan.
   
397. sor: 402. sor:
   
 
* A csillag karakter az őt megelőző reguláris kifejezés értelmét módosítja úgy, hogy nulla vagy bárhány illeszkedést megenged. <tt>a*</tt> jelentése: üres sztring, vagy tetszőleges hosszúságú kizárólag <tt>a</tt> betűkből álló sztring.
 
* A csillag karakter az őt megelőző reguláris kifejezés értelmét módosítja úgy, hogy nulla vagy bárhány illeszkedést megenged. <tt>a*</tt> jelentése: üres sztring, vagy tetszőleges hosszúságú kizárólag <tt>a</tt> betűkből álló sztring.
  +
** A Unicode miatt előfordulhat, hogy egy sor nem illeszkedik a <tt>^.*$</tt> reguláris kifejezésre; ha tartalmaz "érvénytelen karaktert" (olyan byte-sorozatot, amelyhez nem tartozik karakter), akkor -- mivel erre a sorozatra a pont nem illeszkedik -- nem lesz igaz, hogy a sor nulla vagy több tetszőleges karakterből áll, így a kifejezés nem fog illeszkedni a sorra.
 
* A plusz karakter hasonló, de az üres sztringet nem engedi meg. <tt>a+</tt> jelentése: legalább 1 karakter hosszúságú kizárólag <tt>a</tt> betűkből álló sztring.
 
* A plusz karakter hasonló, de az üres sztringet nem engedi meg. <tt>a+</tt> jelentése: legalább 1 karakter hosszúságú kizárólag <tt>a</tt> betűkből álló sztring.
 
* A kérdőjel az őt megelőző reguláris kifejezést opcionálissá teszi. <tt>a?</tt> jelentése: nulla vagy egy darab <tt>a</tt> betű.
 
* A kérdőjel az őt megelőző reguláris kifejezést opcionálissá teszi. <tt>a?</tt> jelentése: nulla vagy egy darab <tt>a</tt> betű.
470. sor: 476. sor:
   
 
A / helyett írhatunk bármilyen más karaktert is, csak mindhárom helyen ugyanazt kell használni. Pl. <tt>ssfoosbars</tt>. :)
 
A / helyett írhatunk bármilyen más karaktert is, csak mindhárom helyen ugyanazt kell használni. Pl. <tt>ssfoosbars</tt>. :)
  +
  +
:Beware! A single <tt>sed statement</tt> can turn a cat into cement!
   
 
=== sed példák ===
 
=== sed példák ===
477. sor: 485. sor:
 
* fájlok csoportos átnevezése
 
* fájlok csoportos átnevezése
 
<pre>find . -name "*.mp3" | while read i; do
 
<pre>find . -name "*.mp3" | while read i; do
mv -i "$i" "$(echo $i | sed 's/^[0-9]*//;s/_-_/-/g;s@./albums/.*/@singles/@')";
+
mv -i "$i" "$(echo $i | sed -r 's@/[0-9]*-@/@;s/_-_/-/g;s@./albums/([^-]*)-.*/(.*)$@singles/\1-\2@')";
 
done</pre>
 
done</pre>
 
* backslash-sel végződő sorokhoz fűzzük hozzá a következő sort: <tt>sed -e :a -e '/\\$/N; s/\\\n//; ta'</tt>
 
* backslash-sel végződő sorokhoz fűzzük hozzá a következő sort: <tt>sed -e :a -e '/\\$/N; s/\\\n//; ta'</tt>
483. sor: 491. sor:
 
* ''regexp'' első előfordulásától kezdve az összes sort írjuk ki: <tt>sed -n '/regexp/,$p'</tt>
 
* ''regexp'' első előfordulásától kezdve az összes sort írjuk ki: <tt>sed -n '/regexp/,$p'</tt>
 
* írjuk ki a 8-12. sorokat: <tt>sed -n '8,12p'</tt> vagy <tt>sed '8,12!d'</tt>
 
* írjuk ki a 8-12. sorokat: <tt>sed -n '8,12p'</tt> vagy <tt>sed '8,12!d'</tt>
* írjuk ki az 52. sort: <tt>sed -n '52p'</tt> vagy <tt>sed -n '52!d'</tt> vagy <tt>sed '52q;d'</tt>
+
* írjuk ki az 52. sort: <tt>sed -n '52p'</tt> vagy <tt>sed '52!d'</tt> vagy <tt>sed '52q;d'</tt>
 
* írjuk ki azokat a sorokat, amelyek ''regex1'' első és ''regex2'' első előfordulása közé esnek: <tt>sed -n '/regex1/,/regex2/p'</tt> (ha ''p'' helyett ''d'', és ''-n'' nélkül, akkor csak ezeket a sorokat NE)
 
* írjuk ki azokat a sorokat, amelyek ''regex1'' első és ''regex2'' első előfordulása közé esnek: <tt>sed -n '/regex1/,/regex2/p'</tt> (ha ''p'' helyett ''d'', és ''-n'' nélkül, akkor csak ezeket a sorokat NE)
  +
* írjuk ki a <tt>ps</tt> kimenetéből a fejlécet és a ''regex''-re illeszkedő sorokat: <tt>sed -n '1p;/regex/p'</tt>
  +
  +
Egy összetettebb példa: adott egy reguláris kifejezés. Olvassuk a standard inputot, és minden illeszkedést, de csak azokat, írjuk ki (tehát nem az illeszkedő sorokat, hanem a sorok illeszkedő részét; ha egy sorban több illeszkedő rész is van, akkor mindet); a kimenet úgy legyen sorokra tagolva, hogy minden sor pontosan egy illeszkedést tartalmazzon. Tehát pl. ha a reguláris kifejezés az, hogy <tt>[0-9a-f][0-9a-f][0-9a-f]+</tt> (tehát legalább három "hexadecimális számjegy" egymás után), a bemenet pedig a következő:
  +
  +
<pre>
  +
ez itt két illeszkedés: deadbeef 042
  +
ebben a sorban csak véletlenül van illeszkedés
  +
itt nincs
  +
  +
foobar
  +
654 a4131 849f
  +
</pre>
  +
  +
Akkor a kimenet legyen a következő:
  +
  +
<pre>
  +
deadbeef
  +
042
  +
ebbe
  +
654
  +
a4131
  +
849f
  +
</pre>
  +
  +
Megoldás:
  +
  +
<pre>
  +
sed -n 's/[0-9a-f][0-9a-f][0-9a-f]\+/\n&\n/
  +
ta
  +
b
  +
:a
  +
s/[^\n]*\n//
  +
tx
  +
:x
  +
P
  +
D'
  +
</pre>
  +
  +
Ez a script talán jól példázza a sed write-only jellegét. Kb. fél éve írtam, de dokumentációt kell olvasnom ahhoz, hogy most megértsem, hogyan működik. :)
  +
  +
# Aránylag tiszta sor: a cseresztringben a & karakter helyére az illeszkedő karaktersorozat kerül be; tehát az első sor annyit csinál, hogy megkeresi az aktuális bemeneti sorban az első illeszkedést, és elé is, mögé is beszúr egy-egy újsor-karaktert.
  +
# Ha végeztünk cserét (tehát volt illeszkedés a pufferben), átugrunk a 4. sorra, az "a" címkére.
  +
# Ha nem végeztünk cserét, a script végére ugrunk.
  +
# "a" címke.
  +
# A puffer elejéről az első újsor-karakterig mindent törlünk (az újsor-karaktert is). Ez az első újsor-karakter az, amit az első sorban szúrtunk be az illeszkedő karaktersorozat elé.
  +
# Ez egy kamu feltételes ugrás; a ''t'' parancs akkor ugrik, ha volt sikeres csere az aktuális sor beolvasása után az utolsó ''t'' parancs óta, akkor elágazik. Mivel még vissza fogunk menni a script elejére, ahol újabb cserét kísérelünk meg, aminek az eredményétől függően vagy elágazunk, vagy nem, itt mindenképpen "ki kell olvasni" az előző ''s///'' parancs "visszatérési értékét", hogy a 2. sorban levő ''t'' ne azért ágazzon el, mert az 5. sorban levő ''s///'' sikeres volt, hanem csakis akkor, ha az első sorban levő csere volt sikeres (tehát a sor még fel nem dolgozott részében volt illeszkedés a keresendő mintára).
  +
# Ide érkezik a kamu-elágazás.
  +
# A ''P'' parancs az első újsor-karakterig kiírja a puffer tartalmát (vagyis itt éppen egy illeszkedésnyit).
  +
# A ''D'' parancs az első újsor-karakterig törli a puffer tartalmát (vagyis éppen a már kiírt részt törli). Ezután visszaugrik a script elejére; ha a puffer üres, új sort olvas be, ha nem, akkor a pufferben levő adatokkal folytatja a feldolgozást.
   
 
További példák pl. a [http://sed.sourceforge.net/sed1line.txt http://sed.sourceforge.net/sed1line.txt] címen találhatók.
 
További példák pl. a [http://sed.sourceforge.net/sed1line.txt http://sed.sourceforge.net/sed1line.txt] címen találhatók.
  +
  +
== Miért zsh? ==
  +
  +
Időnként felmerül a kérdés, miért annyira jó a zsh, miért nem elég a Bash. Ide idővel összegyűjtök pár példát olyan feladatra, amit a zsh hatékonyabban, elegánsabban vagy egyszerűbben old meg, mint a Bash.
  +
  +
Kapásból: a zsh automatikusan megtöbbszörözi az outputot, vagy szekvencializál több inputot, ha többszörös átirányításokat adunk meg. Például (<tt>setopt multios</tt> kell hozzá):
  +
  +
<pre>
  +
echo exit 0 >>*.sh
  +
</pre>
  +
  +
<pre>
  +
ntfsclone -o - /dev/sda1 \
  +
>>(dd bs=32M of=/dev/sdb1 oflag=direct) \
  +
>>(dd bs=32M of=/dev/sdc1 oflag=direct) \
  +
>>(dd bs=32M of=/dev/sdd1 oflag=direct) \
  +
>>(dd bs=32M of=/dev/sde1 oflag=direct) \
  +
>>(dd bs=32M of=/dev/sdf1 oflag=direct) \
  +
>>(dd bs=32M of=/dev/sdg1 oflag=direct) \
  +
| dd bs=32M of=/dev/sdh1 oflag=direct
  +
</pre>
  +
  +
<pre>
  +
dd >file.img >>(md5sum >file.img.md5)
  +
</pre>
  +
  +
<pre>
  +
paste -d: <(cut -d: -f1,2 /etc/shadow) <(cut -d: -f3- /etc/passwd)
  +
</pre>
  +
  +
=== Fájlok átnevezése ===
  +
  +
Adott egy könyvtár, benne számozott .srt és .avi kiterjeszétsű fájlok. Nevezzük át az összes srt-t úgy, hogy ugyanaz legyen a neve, mint az azonos sorszámú avinak (nyilván a kiterjesztést leszámítva).
  +
  +
<pre>
  +
#!/bin/zsh
  +
setopt extendedglob
  +
for i in {01..14}; do mv *$i*srt *$i*avi(#q:s/.avi/.srt); done
  +
</pre>
  +
  +
Másik példa: egy könyvtárban van egy csomó képfájl. Nevezzük át őket úgy, hogy a nevük egy növekvő számsorozat legyen!
  +
  +
<pre>
  +
% autoload zmv
  +
% c=1 zmv '*.jpg' '$((c++)).jpg'
  +
</pre>
  +
  +
A [http://grml.org/zsh/zsh-lovers.html http://grml.org/zsh/zsh-lovers.html] oldal számos további érdekes és elvetemült példával szolgál.
  +
  +
== Ajánlott irodalom ==
  +
  +
* [http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html David A. Wheeler: Fixing Unix/Linux/POSIX Filenames: Control Characters (such as Newline), Leading Dashes, and Other Problems], 2012-12-14
  +
* [http://google-styleguide.googlecode.com/svn/trunk/shell.xml Google Shell Style Guide] (kikapcsolt javascripttel olvashatatlan)
  +
* [http://sed.sourceforge.net/grabbag/scripts/turing.sed Turing-gép sedben] :)

A lap jelenlegi, 2014. október 28., 23:02-kori változata

Ezen az oldalon összeszedek néhány olyan megoldást, ötletet, scriptrészletet, amit gyakran jól használhatunk a rendszeradminisztráció során. Nem célom a shell, mint programozási nyelv teljes bemutatása. Kizárólag Bourne jellegű shellekkel foglalkozom, és azon belül is főként zsh-val.

És azért persze nem maradhat ki a sed sem.

Tartalomjegyzék

[szerkesztés] 1 Az idézőjelek használatáról

A "dupla" idézőjelek között a shell nem értelmezi speciálisan a *, ?, [, ], {, }, ;, <, >, stb. karaktereket (nincs globbing), viszont pl. működik a változókra való hivatkozás és a külső parancs kimenetének behelyettesítése.

Pl.:

echo "files matching \"*.txt\": $(echo *.txt)"

Az 'aposztrófok' között a shell (szinte?) semmilyen speciális karaktert nem értelmez, még a dollárjelet sem, úgyhogy tetszőleges szöveget írhatunk aposztrófok közé betű szerint:

echo 'Value of $PATH='"$PATH"
  • Magát az aposztrófot sehogyan sem tudjuk aposztrófok közé írni, mivel a backslash is elveszíti a szokásos escape-funkcióját.
  • A kétféle idézőjelen belül mindig szabadon használhatjuk a másik fajtát:
echo "Couldn't change directory"
echo 'files matching "*.txt":' $(echo *.txt)

A `backtickek` közé írt parancssor standard outputját a shell behelyettesíti oda, ahol a backtickes kifejezés szerepelt, de jobb ehelyett a $(zárójeles) alakot használni, mivel az szabadon egymásbaágyazható.

#!/bin/sh
DIR=$(date +%Y%m%d)
OPER=${@:-dist-upgrade}
mkdir $DIR
cd $DIR || exit 1
for i in $(echo n | apt-get -d -u $OPER 2>&1 | grep '^[[:space:]]' | tr -d '*'); do
        ls | grep -q ${i}_ || dpkg-repack $i
done

[szerkesztés] 2 Hivatkozás változó értékére

Alapvetően $változónév, de ha pl. sztringet akarunk fűzni a változó tartalmához, akkor a hozzáfűzött sztring összefolyna a változónévvel. Ennek elkerülésére: ${változónév}. Ez a szintaxis amúgy egy sereg további lehetőséget rejt magában; írhatunk mindenféle mágikus karaktert a változónév elé és mögé, amitől érdekesebbnél érdekesebb dolgok történhetnek. Hamarosan látunk erre példát.

Ha nem tudjuk, hogy a változó értékében nem lehet szóköz vagy egyéb speciális karakter, mindig idézőjelek között hivatkozzunk rá!

Az alábbi script üres $1 esetén hibaüzenetet ad, szóközt tartalmazó $1 esetén pedig hülyeséget csinál; ha ügyesek vagyunk, ilyen jellegű hibákat tartalmazó scriptbe akár kódot is injektálhatunk alkalmas paraméterezéssel. A zsh az ilyen hibák egy részétől megvéd (mert külön kérés nélkül általában nem bontja szavakra a változók értékeit, amikor behelyettesíti őket), de ez nem jelenti azt, hogy a zsh-ban szabadon lehagyható az idézőjel az ismeretlen tartalmú változó neve mellől!

#!/bin/sh
if [ $1 -gt 0 ]; then
    echo positive
fi

Sok scriptben láthatunk ahelyett, hogy [ -z "$1" ] vagy [ "$1" = "" ] olyat, hogy [ x$1 = x ]. Ez Rossz, Értem? Ha $1-ben szóköz van, összezavarja a [ parancsot (merthogy a shellben ez is egy parancs, nem szintaktikai elem).

[szerkesztés] 3 C-jellegű aritmetika

foo=1
((foo++))
echo $foo
# 2-t ír ki

Vagyis nem kell szenvednünk az expr paranccsal, és a foo=$[foo+1] jellegű szintaxist sem kell erőltetnünk.

Egyébként C-szerű for-ciklust is csinálhatunk:

for ((i=0;i<10;i++)) {
    echo $i
}

Bár a preferált szintaxis ez:

for ((i=0;i<10;i++)) do
    echo $i
done

[szerkesztés] 4 Parancssor feldolgozása

Nyilván szeretnénk olyasmi argumentumfeldolgozást, mint a GNU programokban: --hosszú-opció=érték stb.

Erre legalább négy jó módszer van; általában rossz módszer az, ha előírjuk, hogy mi legyen az első, a második, harmadik stb. argumentum szerepe. Jobb a felhasználóra bízni, milyen sorrendben kényelmes neki megadni a kapcsolókat (pl. lehet, hogy nem az összeset akarja használni: mondjuk az első hármat kihagyná).

Az egyik jó módszer: getopts (bashben is van, de lehet, hogy nem pontosan úgy működik, mint az itt bemutatott zsh-féle).

#!/bin/zsh
#
# usage:
#
usage='build-vserver [ -a architecture ] [ -c contextid ] [ -d distribution ]
        [ -h hostname ] [ -f iface ] [ -i IP ] [ -n netmask ]
        [ -l lvprefix ] [ -m {mirrorhost|proto://mirror/path} ]
        [ -g volumegroup ] [ -v vservername ] [ -x ssmtp_mailhub ]
        [ -z ]
        [ -u volume-config-string [ -u volume-config-string [ ... ]]]'
# itt most egy csomó sort kihagyunk
while getopts :a:c:d:h:f:i:n:l:m:g:v:u:x:z OPT; do
    case ${OPT} in
        a)
            ARCH="$OPTARG"
            ;;
        c)
            CTX="$OPTARG"
            ;;
        d)
            DIST="$OPTARG"
            ;;
        h)
            HOSTNAME="$OPTARG"
            ;;
        f)
            IFACE="$OPTARG"
            ;;
        i)
            IP="$OPTARG"
            ;;
        n)
            MASK="$OPTARG"
            ;;
        l)
            LVPREFIX="$OPTARG"
            ;;
        m)
            MIRROR="$OPTARG"
            ;;
        g)
            VOLUMEGROUP="$OPTARG"
            ;;
        v)
            VSERVER="$OPTARG"
            ;;
        u)
            VOLUMECONF=($VOLUMECONF $OPTARG)
            ;;
        x)
            MAILHUB="$OPTARG"
            ;;
        z)
            VOLUMECONF=()
            ;;
        *)
            echo "${usage}" >&2
            exit 2
            ;;
    esac
done
  • A getopts paraméterlistájában a kettőspont azt jelzi, hogy az előző kapcsolóhoz tartozik argumentum; a z-nek nincs, ezért nincs utána kettőspont (nem azért, mert ő az utolsó).
  • Az első kettőspont arra jó, hogy ne adjon hibaüzenetet, ha nemlétező kapcsolót talál (ezt az esetet mi magunk kezeljük a case-ben).

Egy másik jó módszer: while, shift, case. Az alábbi példa egy olyan shellfüggvény-gyűjteményből származik, ami iptables-tűzfalscriptek írását teszi kevésbé fáradságossá:

    while [[ ! "$1" = "" ]]; do
        case "$1" in
            -c)
                ;&
            --chain)
                shift
                local CHAIN="${1:-${PORT}_input}"
                ;;   
            -a)
                ;&
            --aclfile)
                shift
                local ACL="${1:-$MYACLDIR/${PORT}}"
                ;;   
            -n)
                ;&
            --name)
                shift
                local FRIENDLYNAME="${1:-tcp/$PORT}"
                ;;   
            *)
                echo buildtcpchain ignoring unknown parameter \""$1"\".
                ;;   
        esac
        shift
    done
  • Látható, hogy így tudunk hosszú kapcsolókat is kezelni, nemcsak rövideket, meg olyan kapcsolókat is, amelyeknek két argumentuma van, stb.

Egy harmadik lehetőség az, hogy a script viselkedését befolyásoló beállításokat nem kapcsolók, hanem környezeti változók formájában várjuk; ez kicsit körülményes a felhasználónak, cserébe teljesen transzparens módon örökli az összes ilyen beállítást az összes gyermekfolyamatunk (azokat is, amiket mi magunk nem is értünk). Ezt a módszert leginkább akkor érdemes használni, ha wrappert írunk valami olyan program köré, ami eleve környezeti változókból veszi a beállításait.

Negyedik lehetőség: a zsh-hoz van egy zutil nevű "modul" (plugin), amiben van egy zparseopts parancs. Ez általános megoldás a rövid és hosszú kapcsolók és ezek opcionális argumentumainak a kezelésére.

[szerkesztés] 5 Alapértelmezések használata

Ha egy változó értékét egy script felhasználója megadhatja, de nem kötelező megadnia, akkor gyakran szeretnénk valamilyen alapértelmezett értéket adni a változónak.

Az alábbi példa rossz:

FOO=defaultérték
FOO=$1

Ha a scriptet paraméter nélkül hívták, FOO értéke az üres sztring lesz.

Ehelyett használhatjuk az alábbi bonyolult megoldást:

if [ -z "$1" ]; then
    FOO=defaultérték
else
    FOO="$1"
fi

Ez működik, de nem túl tömör.

Az if-től megszabadulhatunk a következő módon:

[ -z "$1" ] && FOO=defaultérték || FOO="$1"

De még ennél is egyszerűbb, ha csak annyit írunk, hogy

FOO=${1:-defaultérték}

[szerkesztés] 6 Konfigurálható script

  • A konfigurációkezelés kézenfekvő módja a VÁLTOZÓ=érték párokat tartalmazó script-töredék ("configfile") source-olása a "." paranccsal.
  • Ha kezelünk parancssori argumentumokat:
    • A script elején állítsunk be minden változót az alapértelmezésre, majd
    • töltsük be a konfigurációt:
      [ -r "$CONFIGFILE" ] && . "$CONFIGFILE"
    • végül dolgozzuk fel a parancssort.
  • Egyébként írhattunk volna ilyet is:
    FOO=${1:-${FOO:-defaultérték}}
    (itt ugye $1-nek volt a legnagyobb precedenciája, aztán jött a configfile, aztán a kódba drótozott default, itt "defaultérték").
    • Előny és egyben hátrány: nincs a script eleje tele a konfiguráció alapértelmezéseivel.

[szerkesztés] 7 A shell opciói

A legtöbb shellben mind a parancssorban, mind működés közben temérdek opciót tudunk állítgatni (elsősorban ki- ill. bekapcsolni). A parancssorban az opciókat egyszerűen a shell neve után írjuk:#!/bin/sh -e, magában a shellben pedig a set -e, set +e parancsokkal tudjuk kapcsolgatni őket (itt konkrétan az e opciót).

Ezek közül az opciók közül különösen hasznos az alábbi kettő:

  • -e: sikertelen visszatérési értékű parancssor végrehajtása után a script kilép
    • Akkor jó, ha nem foglalkoztunk igényes hibakezeléssel, viszont
    • esetleg katasztrófával járna, ha a script némelyik sikertelen művelet után úgy folytatná a munkáját, mintha mi sem történt volna. Pl:
cd "$1"; rm -rf *.csv
Ha a script nem tud beváltani a megadott könyvtárba, az aktuális munkakönyvtárból kezdene törölni.
  • Az alapértelmezés szerinti +e állapotban a script nem lép ki, ha valamelyik parancssor végrehajtása sikertelen (de amúgy szintaktikailag helyes).
  • -u: hibának számít, ha olyan változóra hivatkozunk, amelynek nem adtunk értéket.
  • -x: minden parancssort kiír a script végrehajtása során.
    • Hibakereséskor nagyon hasznos.

[szerkesztés] 8 Mezőkre osztott input feldolgozása

A /etc/passwd kettőspontokkal elválasztott mezőket tartalmaz. Olvassuk ki egy egyszerű shellscripttel a bela user emberi nevét!

OLDIFS="$IFS"
IFS=":"
while read username x uid gid gecos homedir shell; do
    [ "$username" = "bela" ] && echo "A bela neve: $gecos" && break
done </etc/passwd
IFS="$OLDIFS"
  • Persze igazából mindegy nekünk, milyen mezők jönnek még a gecos nevű után, úgyhogy a while-sort így is írhattuk volna:
     while read username x uid gid gecos mindenmas; do 
  • Az IFS értékét célszerű visszaállítani az alapértelmezettre, ha elállítottuk, nehogy a script egy későbbi részében meglepetés érjen.
  • Szépséghiba, hogy a gecos-mező a usernek nemcsak a nevét, hanem néhány egyéb adatát is tartalmazza, vesszőkkel elválasztva. Ezzel most nem törődünk.

(Persze ennek a feladatnak a valódi megoldása nem a fenti, hanem mondjuk az, hogy getent passwd bela|cut -d: -f5.)

A read amúgy egy echo-ból is tud olvasni, így feldarabolhatjuk a valahonnan olvasott vagy a user által megadott sztringet a mezőhatárok mentén: echo "$változó" | read mező1 mező2 mező3 ...

[szerkesztés] 9 A find használata

A find(1) nemcsak fájlok és könyvtárak keresésére jó, hanem arra is, hogy egy műveletet sok, valamilyen tulajdonság alapján kiválasztott fájlon elvégezzünk.

Sajnos abból adódóan, hogy a fájlnevekben szinte bármi lehet, sokmindenre oda kell figyelni. Például az, hogy

cat $(find . -type f) > ../collection

nem fog helyesen működni, ha a fájlnevekben szóköz, TAB vagy újsor karakter van, mert a shell ezek mentén a karakterek mentén vágja szavakra a find(1) kimenetét, tehát egy egy szóközt tartalmazó fájlnevet két fájlnévként fog átadni a cat-nek.

További hátránya ennek a megoldásnak, hogy ha nagyon sok a találat, túl hosszúvá válik a cat-nek átadott parancssor, ami vagy nagyon nagy memóriaigényhez, vagy hibához vezet (ha a kernel korlátozza a parancssor hosszát).

Egy másik kézenfekvő megoldás a ciklus:
for i in $(find . -uid 1001); do chown 1002 "$i"; done
de egyrészt ez is hibásan működik szóközt, TABot vagy újsort tartalmazó névre, másrészt szintén nagy lehet a memóriaigénye, ha sok a találat (hiszen továbbra is egyetlen listába kell őket összegyűjteni a for számára); és, amire sokan nem gondolnak: ha a find talál mondjuk egy * nevű fájlt, akkor a legtöbb shellben az adott fájlt tartalmazó könyvtárban levő összes fájlra le fog futni a chown. (zsh-ban nem, mert az alapból nem értelmezi a $(parancs) konstrukcióval meghívott parancsok kimenetében megjelenő glob karaktereket, tehát erre az aknára zsh-ban nehezebb rálépni.)

Próbálkozhatnánk while ciklussal is:

find . -uid 1001 | while read i; do chown 1002 "$i"; done

Ez szóközöket egyesével tartalmazó fájlnévekre jól működik és a globbinggal sem lőjük magunkat lábon, de nem működik helyesen újsort tartalmazó fájlnévre és akkor sem, ha egy fájlnév szóközzel vagy TABbal kezdődik vagy arra végződik; és akkor sem, ha egy fájlnévben több egymást követő szóköz van (a read levágja ill. a zsh-t kivéve összetömöríti egyetlen szóközzé).

A read parancs ráadásul a \-t is speciálisan értelmezi, így pl., ha egy fájlnév \-re végződik, akkor a while ciklusunk összevonja a következő fájlnévvel. bash-ben és zsh-ban ezt elkerülhetjük, ha a -r kapcsolóval hívjuk a read-et.

Kifinomultabbak is lehetünk, legalább háromféleképpen:

  • A GNU xargs segítségével (a POSIX xargs használata fájdalmas).
    find . -uid 1001 -print0 | xargs -0 chown 1002
  • Ez azért jobb, mert
    • nem annyiszor fog lefutni a chown, ahány találat van, hanem jóval kevesebbszer; valamint
    • biztosan helyesen működik akkor is, ha a megtalált fájlok nevében szóköz, enter, idézőjel vagy egyéb nyalánkság van.
  • A find beépített mechanizmusaival.
    find . -uid 1001 -exec chown 1002 {} \;
    • Ezt a megoldást nem nagyon szeretem, mert nem lehet tetszőlegesen rugalmasan összeállítani a végrehajtandó parancssort, mert a find közvetlenül hívja meg a megadott parancsot, nem a shellen keresztül. Végeredményben egyetlen programot hívhatunk meg, aminek a parancssorába valahová beszúrhatjuk a megtalált fájl nevét.
  • find . -uid 1001 -exec chown 1002 {} +
    • Ez majdnem ugyanaz, mint az xargs-os megoldás, azzal a különbséggel, hogy nem futtatunk xargs processzt. Sajnos a megtalált fájlok listája továbbra is a felépített parancssor végén kell, hogy legyen.
  • A find printf akciója segítségével konstruálhatunk parancssorokat:
    find . -uid 1001 -printf "chown 1002 %f\n"|sh
    • Ez talán a legrugalmasabb megoldás, viszont annyi parancssorunk lesz, ahány találatunk, és vigyáznunk kell a speciális karakterekkel -- pl. egy fájlnév végén levő backslash karakter könnyen vasrudat dughat scriptünk kerekeinek küllői közé (elnézést).

Néhány konkrét példa a find használatára:

Azonos tartalmú fájlokat összehardlinkelő script (csak ésszel szabad használni!)

  • Cél: hozzon létre egy ..hardlinks nevű könyvtárat;
  • azon belül 0-99 nevű könyvtárakat;
  • ezeken belül azokhoz a fájlméretekhez tartozó könyvtárakat, amelyek százzal osztva az adott alkönyvtár nevét adják maradékul (tehát az 1-es könyvtárban lesz az 1, a 101, 201, az 1001 s.í.t.).
    • Ennek az az értelme, hogy nem lesz borzasztóan sok bejegyzés egyik konkrét könyvtárban sem (vagy legalábbis kevesebb lesz, mint ha mindet ugyanoda ömlesztenénk), így nem állítjuk kihívások elé a fájlrendszert.
  • Ezeken a könyvtárakban a fellelt adott méretű fájlok SHA1 hash-éről elnevezett fájlokat, amelyeket összehardlinkel a fájllal, valamint
  • symlinkeket, amelyek a fájlok helyére mutatnak és nevük tartalmazza a hash-t és az eredeti fájlnevet is.
#!/bin/zsh
#
# Usage: MINSIZE=n MAXSIZE=k ln-dup-files dir [dir2 [...]]

OLDSUM=0
OLDNAME=""

# Ha nincs megadva a MINSIZE, keressük meg a legkisebb file-t
if [[ "$MINSIZE" = "" ]]; then
    MINSIZE=$(find $@ -type f -printf "%s\n" | sort -n | head -1)
fi

# Hasonlóképpen MAXSIZE esetén a legnagyobbat
if [[ "$MAXSIZE" = "" ]]; then
    MAXSIZE=$(find $@ -type f -printf "%s\n" | sort -n | tail -1)
fi
# Ezt azért csináljuk, hogy a findnak mindenképpen megadhassuk alább a mérettel
# kapcsolatos feltételeket; elegánsabb lenne úgy, ha aszerint, hogy melyik
# méretkorlát adott, más-más find parancssort használnánk.

# Bevitelkor a mezőhatár az újsor karakter legyen (a readnek fog ez kelleni)
IFS="
"
# Itt jobb lenne a NUL karaktert használni (read -d ''), mert az nem szerepelhet
# fájlnévben, míg újsor igen.

# Keressük a megadott alkönyvtárakban az összes fájlt, amelynek mérete a megadott
# határok közé esik, és írjuk ki a teljes elérési utat, majd a fájlméretet
find $@ -type f \( -size ${MINSIZE}c -o -size +${MINSIZE}c \) -a \( -size -${MAXSIZE}c -o -size ${MAXSIZE}c \) -printf "%p\n%s\n" \
    | while read file; do
# Ha sikerült olvasnunk egy fájlnevet, olvassuk be a hozzá tartozó méretet is,
# amit a következő sorba írtunk (ahelyett, hogy NUL karaktert raktunk volna a
# név és a méret közé)
        read SIZE
        if [[ "${file//..hardlinks/}" = "$file" ]]; then # Ha a fájlnév nem azzal kezdődik, hogy ..hardlinks
            SUM=$(sha1sum "$file" | cut -d' ' -f1) || exit 111 # Számoljuk ki a hash-t
            DESTDIR="..hardlinks/$[SIZE%100]/$SIZE" # Döntsük el, melyik könyvtárban kell létrehozni a hardlinket
            [[ -d "$DESTDIR" ]] || mkdir -p "$DESTDIR" || exit 1 # Ha nem létezik, hozzuk létre
# Nézzük meg, van-e már ilyen hashről elnevezett fájl ott; ha igen, akkor
# azt kell odahardlinkelni a most megtalált fájl helyére. Ha nem, akkor
# a most megtalált fájlt kell erre a helyre hardlinkelni.
            [[ -f "$DESTDIR/$SUM" ]] \
                && ln -f "$DESTDIR/$SUM" "$file" \
                || ln -f "$file" "$DESTDIR/$SUM"
# Mi ennek a fájlnak a teljes elérési útja? A symlink létrehozásához kell
            fullname="$(readlink -f "$file")"
# Ha még nincs az eredeti fájlra mutató symlink a hashről elnevezett fájl mellett, csináljunk.
            ls -l $DESTDIR | grep -q -- "-> $fullname"'$' || {
# Kis voodoo: állítsuk elő a symlink nevét hash.valamiszám.eredetikiterjesztés alakban
		ln -sf "$fullname" "$DESTDIR/${SUM}.${LINKCOUNT}.${fullname:e}"
		((LINKCOUNT++))
	    }
        fi
    done

Itt láttunk többféle varázslatot is (ezeket nem kell megtanulni, csak tudni arról, hogy ilyesmik vannak, és amikor kellenek, ki lehet nézni őket a dokumentációból):

  • if [[ "${file//..hardlinks/}" = "$file" ]]; then
    megnézi, hogy ha a $file tartalmából kihagyjuk azt, hogy ..hardlinks, akkor ugyanazt kapjuk-e (tehát szerepelt-e benne ez a sztring; hiszen ha szerepelt, akkor elhagyva belőle nem ugyanazt kapjuk). Ezzel egy grep-et helyettesítettünk, és mivel így a ciklusmagban úsztunk meg egy külsőprogram-futtatást, jelentős az időnyereség.
  • readlink -f "$file"
    ezzel tudunk "kanonicizálni" egy útvonalat (relatívból abszolút, symlinkek kiiktatva). Sajnos külső program, nem shell builtin.
  • ${fullname:h:t}_${file:t}
    a dirname és a basename megúszása zsh-san. A :h csak az útvonalat hagyja meg a változó fájlnévként értelmezett tartalmából, a :t pedig csak a fájlnevet. Az útvonalra alkalmazva a :t-t az adott fájl elérési útjában szereplő utolsó könyvtárnevet kapjuk. (Ez a konstrukció a script fenti változatában nem szerepel, de érdemes ismerni.)
  • ${fullname:e}
    a $fullname tartalmából az utolsó pont utáni részt adja vissza (vagyis a "kiterjesztést").

Töröljük most ebből a ..hardlinks könyvtárból azokat a fájlokat, amelyekhez már nem tartozik másik hardlink!

find ..hardlinks -type f -links 1 -execdir rm -f {} +

Nézzünk egy másik lehetséges (sokkal gyorsabb, de kevesebb utólagos diagnosztikát lehetővé tevő) megoldást a fenti hardlinkelős problémára!

#!/bin/zsh
#
# Usage: ln-dup-files dir [dir2 [...]]

# MYIFS egyetlen soremelésből áll; itt is jobb lenne NULL-t használni, mert így helytelenül működik a script,
# ha vannak újsort tartalmazó fájlnevek
MYIFS="
"
SHAIFS=" " # szóköz
OLDIFS="$IFS" # elmentjük az eredetit, hogy helyreállíthassuk (bár itt nem kell)

zmodload zsh/files # Hogy az ln(1) belső parancs legyen, ne külső
zmodload zsh/stat  # Hogy legyen belső zstat parancsunk (ezzel majd inode-számokat meg ilyesmiket kérdezünk le)

saved=0 # Legfeljebb ennyi byte-ot takarítottunk meg eddig

typeset -A metadata # Egy olyan hash, amely mode-uid-gid-size-sha1sum n-esekhez inode-fájlnév párokat rendel
typeset -A seen_inodes # Egy olyan hash, amelyben sha1sum-okat és az inode-ok egyéb adatait cache-eljük. A kulcs az inode-szám, az adat a jogok, tulajdonos, méret, sha1sum.

find $@ -type f -size +1c -printf "%p\n" | while read file; do
	zstat -o -H stat "$file" # A stat nevű hash-be rakja bele $file jellemzőit
	statstr="$seen_inodes[$stat[inode]]" # Lássuk, megvan-e a cache-ben
	if [[ -z "$statstr" ]]; then # nincs
		IFS="$SHAIFS"
		sha1sum "$file" | read SUM foo # Nincs gond a fájlnévben (ill. leginkább az elején) levő szóközökkel: $foo értékét nem használjuk
		IFS="$MYIFS"
		statstr=$stat[mode]_$stat[uid]_$stat[gid]_$stat[size]_$SUM
		seen_inodes[$stat[inode]]=$statstr # Berakjuk a cache-be; az adott inode-hoz az imént konstruált sztring tartozik, ami egyben kulcs a metadata hash-hez
	fi
# Nézzük meg most, hogy láttunk-e már olyan fájlt, amit ezzel a mostanival össze lehetne hardlinkelni!
	echo "$metadata[$statstr]" | {
		read inode
		read name
	} # Azért így, mert újsor választja el egymástól az inode-ot és a nevet; cserébe az újsort tartalmazó fájlnevekkel nem boldogulunk. Ezt a problémát kiküszöbölhetnénk pl. úgy, hogy a fájlnévben nem szerepeltethető "/" karakterrel választanánk el az inode-ot és a nevet, de akkor rondább lenne az a kód, ami értelmezi a metadata hash-ben tárolt adatokat.
	if [[ -n "$inode" ]]; then # Tartozik-e elmentett inode ehhez a statstr-hez?
		if ! [[ "$inode" = "$stat[inode]" ]]; then # Ha igen: az a file, amit most találtunk, már össze van vele hardlinkelve, tehát ugyanaz az inode-számuk?
			echo ln -f "$name" "$file" "$[stat[size]]" # Nem: összehardlinkeljük őket (itt még csak kiírjuk, hogy a felhasználó lássa, mit csinálunk)
			((saved+=$[stat[size]])) # És nyomon követjük, mennyi helyet spórolunk meg
			ln -f "$name" "$file"
		fi
	else
		metadata[$statstr]="$stat[inode]$MYIFS$file" # Most látunk ilyen file-t először; elmentjük az inode-számát és az elérési útját
	fi
done
echo "Saved up to $saved bytes."

Főbb különbségek a két script között:

  • A második jóval gyorsabb, mert az sha1sum-on kívül nem hív külső parancsot a ciklusmagban (az elsőben volt cut, ls, grep, mkdir és readlink is).
    • Cserébe nem készít ..hardlinks könyvtárat; emiatt utólag csak az inode-ok alapján keresve állapítható meg, mit hardlinkelt össze mivel.
  • A második script figyelembe veszi a fájlok jogosultságait és tulajdonosát is (de POSIX ACL-jeiket és bővített attribútumaikat nem); az első csak a tartalmukat nézi.
    • Nem lenne túl nehéz felkészíteni a scriptet a POSIX ACL-ek és az attribútumok kezelésére, csak nagyon lelassítaná, mert ezeket is külső programmal kellene lekérdezni, aztán mondjuk hash-elni és ezt a hasht is hozzáfűzni a statstr-hez.
  • A második script memóriaigénye a vizsgált fájlok számával arányos; az elsőé független tőle.

Vegyük észre:

  • A hash nagyon hasznos eszköz, de ezzel a felhasználással már a határát súroljuk annak, amit shellben értelmesen ki lehet fejezni; összetettebb feladatoknál nagyon hiányzik a struct ("rekord") adattípus és a pointerek.
    • Pl. jó lenne olyan hasht csinálni, amelynek az elemei is hash-ek, de nem lehet (vagy legalábbis szépen nem lehet).
  • A második scriptben további optimalizációk lennének lehetségesek. Pl:
    • Előfordulhat, hogy egy fájl sha1 hash-ét feleslegesen számoljuk ki. Elég lenne ezt akkor megtenni, ha találunk másik olyan fájlt, amelyiknek a mérete, tulajdonosa és jogosultságbitjei ugyanilyenek, de az inode-ja különözik (tehát egyáltalán felmerül a hardlinkelés lehetősége).
    • A megtakarított hely mennyiségére csak felső becslést adunk, mert minden egyes alkalommal, amikor hardlinket készítünk, hozzáadjuk a kérdéses fájl méretét az esetleges megtakarított byte-ok számához -- holott ezt csak akkor tehetnénk meg, ha a hardlinkelés következtében a régi inode referenciaszámlálója nullára csökken.
  • Mindkét script természetesen egy-egy hatalmas versenyhelyzet: garantálni kell, hogy a vizsgált fájlok tartalma a futás ideje alatt ne változhasson.

A find egyébként még rengeteg szempont alapján tud keresni: jogosultságok, dátumok, reguláris kifejezések stb. Részletesebben l. man find.

[szerkesztés] 10 Barátunk, a sed

sed == stream editor. Alapvetően az stdinről olvasott szöveg-streamen végez programozható átalakításokat, és az eredményt az stdoutra írja. Persze tud fájlból is dolgozni.

  • Programozható, de elég write-only.
  • Megírható benne a hanoi tornyai, vagy egy inverz lengyel notációt használó számológép; minden bizonnyal egy webszerver is.
  • Kiválóan alkalmas arra, hogy sor-szinten strukturált szövegformátumokat egymásba átalakítsunk.
  • Egyszerre valósítja meg a cut, a grep, a tr, a head és a tail parancs (és még valószínűleg több másik) funckionalitását.

Itt most nem tanuljuk meg teljes mélységében, csak kicsit megkarcoljuk a felszínt néhány konkrét alkalmazás bemutatásával.

[szerkesztés] 10.1 Reguláris kifejezések

A reguláris kifejezés egy szövegminta, pl. egy rendszám: három nagybetű, kötőjel, három számjegy. Ez így nézne ki: [A-Z][A-Z][A-Z]-[0-9][0-9][0-9].

Arra jó, hogy jellegzetes mintákat keressünk szövegben; pl. egy szövegfájlban szereplő összes magyar rendszámot gyűjtsük ki. Vagy pl. egy naplófájl alapján számoljuk össze, hány esemény történt 21:00 és 22:00 között, napokra lebontva.

Többféle szintaxis van (elterjedt pl. a POSIX és a perl); itt most a GNU egrep által támogatott bővített (extended) szintaxist ismerjük meg.

A reguláris kifejezéseket rekurzívan szokás definiálni:

  • Reguláris kifejezés minden olyan karakter, amelynek nincs speciális jelentése; pl. a. Ez pontosan egy darab a betűre illeszkedik. A speciális jelentéssel rendelkező karaktereket a backslash ("\") karakter megfosztja a speciális jelentéstől.
  • A szögletes zárójelek segítségével karakterosztályt definiálhatunk: [A-Fa-f0123456789]. Ha a karakterosztály első karaktere a kalap ("^"), akkor az osztály egy darab olyan karakterre illeszkedik, amely nincs a felsoroltak között; ha nem, akkor egy olyan karakterre, amely a felsoroltak között van.
    • A tartományokkal vigyázzunk; a locale-től függően [a-d] jelentheti [abcd]-t és [aBbCcDd]-t is.
    • Vannak előredefiniált karakterosztályok, pl. [[:alnum:]]; ezeket kombinálhatjuk egyéb karakterekkel: [,.%:[:alnum:]_/*]. Ezeknek az az előnye, hogy locale-függőek, tehát pl. magyar locale esetén az [[:alpha:]] osztálynak részei a magyar ékezetes betűk is, míg angol locale esetén nem.
  • A . (pont) karakter egy darab tetszőleges karakterre illeszkedik.
    • Figyelem! A karakter és a byte nem ugyanaz. Az LC_CTYPE környezeti változó értékétől függ, milyen kódolást használnak a(z erre felkészített) programjaink. A Unicode-ban vannak olyan byte-sorozatok, amelyekhez nem tartozik karakter. Az ilyen "érvénytelen karakterekre" a . nem illeszkedik!
  • A ^ (kalap) a sor elejére illeszkedik, tehát pl. a ^a minden olyan sorra illeszkedik, amely a-betűvel kezdődik.
  • A $ (dollárjel) a sor végére illeszkedik, tehát pl. a ^a$ minden olyan sorra illeszkedik, amely pontosan egy darab a-betűből áll.
    • Ritkábban szokott kelleni, de létezik: \< a szó eleje, \> a szó vége, a fentiekhez hasonlóan.

A következő posztfix "ismétlőoperátorok" értelmezettek:

  • A csillag karakter az őt megelőző reguláris kifejezés értelmét módosítja úgy, hogy nulla vagy bárhány illeszkedést megenged. a* jelentése: üres sztring, vagy tetszőleges hosszúságú kizárólag a betűkből álló sztring.
    • A Unicode miatt előfordulhat, hogy egy sor nem illeszkedik a ^.*$ reguláris kifejezésre; ha tartalmaz "érvénytelen karaktert" (olyan byte-sorozatot, amelyhez nem tartozik karakter), akkor -- mivel erre a sorozatra a pont nem illeszkedik -- nem lesz igaz, hogy a sor nulla vagy több tetszőleges karakterből áll, így a kifejezés nem fog illeszkedni a sorra.
  • A plusz karakter hasonló, de az üres sztringet nem engedi meg. a+ jelentése: legalább 1 karakter hosszúságú kizárólag a betűkből álló sztring.
  • A kérdőjel az őt megelőző reguláris kifejezést opcionálissá teszi. a? jelentése: nulla vagy egy darab a betű.
  • a{n,m}: legalább n, legfeljebb m darab a betű. m elhagyható, ekkor csak legalább n. Ha a vesszőt is elhagyjuk, akkor pontosan n, tehát a rendszám pl. [A-Z]{3}-[0-9]{3}.

Egyéb szabályok:

  • Két reguláris kifejezés konkatenáltja azokra a sorokra illeszkedik, amelyekben az első kifejezésre illeszkedő sztringet a másodikra illeszkedő sztring követ.
  • Két reguláris kifejezést összekapcsolhatunk a | (pipe) infix operátorral. Az így létrehozott kifejezés minden olyan sorra illeszkedik, amely a két eredeti kifejezés közül legalább az egyikre illeszkedik. (Pl. alma|barack azokra a sorokra illeszkedik, amikben az "alma" vagy a "barack" szó, vagy mindkettő előfordul (sorrendtől függetlenül).

Precedenciaszabályok:

  1. ismétlés (alma* illeszkedik pl. arra, hogy "alm", "alma", "almaa", "almaaa", s.í.t.)
  2. összefűzés (alma|barack jelentése értelemszerű, nem pedig "alm" majd "a" vagy "b" majd "arack")
  3. VAGY-kapcsolat

Ezeket zárójelezéssel felülbírálhatjuk, pl. ^((alma|barack)*|(körte.*fókazsír))$.

A zárójeles kifejezésekre "visszahivatkozhatunk" a \szám konstrukcióval: ^(alma|barack)\1*$ (olyan sor, amiben csak az alma, vagy csak a barack szó szerepel, legalább egyszer).

Néhány példa:

  • páros szám: [0-9]*[02468][^0-9]|$
  • két nagybetűvel kezdett szó egymás után (pl. név): \<[[:upper:]][[:lower:]]* [[:upper:]][[:lower:]]*\>

[szerkesztés] 10.2 grep-emuláció seddel

sed -n '/regex/p'

Amit érdemes megfigyelni:

  • -n kapcsoló: nem ír ki minden sort, csak azokat, amiket a p paranccsal kiíratunk.
  • /regex/: feltétel; az adott reguláris kifejezésre illeszkedő sorokra hajtja végre a megadott parancso(ka)t.
    • A sed igazából címzésnek tekinti.

Az illeszkedő sorok megkettőzése:

sed -n '/regex/{p;p}'

Vagyis két "print" parancsot is végrehajtunk.

Ha a többi sor is kell, de az illeszkedők kétszer:

sed '/regex/p'

[szerkesztés] 10.3 Sorok törlése

A grep -v emulációja: sed '/regex/d'. A d parancs törli az illeszkedő sort.

[szerkesztés] 10.4 Az s, mint svájcibicska sed-parancs

  • s/foo/bar/ foo első előfordulását bar-ra cseréli minden sorban;
  • s/foo/bar/g az összeset;
  • s/foo/bar/2 csak a másodikat;
  • /baz/s/foo/bar/g az összeset, de csak a "baz"-t tartalmazó sorokban;
  • /baz/!s/foo/bar/g az összeset, de csak a "baz"-t NEM tartalmazó sorokban.

Ebben az a pláne, hogy "foo" és "baz" lehet reguláris kifejezés, "bar"-ban pedig hivatkozhatunk ennek a részeire.

Pl. rendszámban számok és betűk felcserélése, a konkrét rendszám ismerete nélkül, általánosan:

sed -r 's/([A-Z][A-Z][A-Z])-([0-9][0-9][0-9])/\2-\1/g'
  • -r: bővített reguláris kifejezések használata (enélkül \ kell a zárójelek elé).

A / helyett írhatunk bármilyen más karaktert is, csak mindhárom helyen ugyanazt kell használni. Pl. ssfoosbars. :)

Beware! A single sed statement can turn a cat into cement!

[szerkesztés] 10.5 sed példák

  • szóközök törlése sorok végéről: sed 's/[[:space:]]*$//'
  • blokk kommentezése (minden sor elejére hashmark beszúrása: sed 's/^/#/'
  • fájlok csoportos átnevezése
find . -name "*.mp3" | while read i; do
    mv -i "$i" "$(echo $i | sed -r 's@/[0-9]*-@/@;s/_-_/-/g;s@./albums/([^-]*)-.*/(.*)$@singles/\1-\2@')";
done
  • backslash-sel végződő sorokhoz fűzzük hozzá a következő sort: sed -e :a -e '/\\$/N; s/\\\n//; ta'
  • keressük azokat a sorokat, amikben AAA, BBB és CCC tetszőleges sorrendben előfordul: sed '/AAA/!d; /BBB/!d; /CCC/!d'
  • regexp első előfordulásától kezdve az összes sort írjuk ki: sed -n '/regexp/,$p'
  • írjuk ki a 8-12. sorokat: sed -n '8,12p' vagy sed '8,12!d'
  • írjuk ki az 52. sort: sed -n '52p' vagy sed '52!d' vagy sed '52q;d'
  • írjuk ki azokat a sorokat, amelyek regex1 első és regex2 első előfordulása közé esnek: sed -n '/regex1/,/regex2/p' (ha p helyett d, és -n nélkül, akkor csak ezeket a sorokat NE)
  • írjuk ki a ps kimenetéből a fejlécet és a regex-re illeszkedő sorokat: sed -n '1p;/regex/p'

Egy összetettebb példa: adott egy reguláris kifejezés. Olvassuk a standard inputot, és minden illeszkedést, de csak azokat, írjuk ki (tehát nem az illeszkedő sorokat, hanem a sorok illeszkedő részét; ha egy sorban több illeszkedő rész is van, akkor mindet); a kimenet úgy legyen sorokra tagolva, hogy minden sor pontosan egy illeszkedést tartalmazzon. Tehát pl. ha a reguláris kifejezés az, hogy [0-9a-f][0-9a-f][0-9a-f]+ (tehát legalább három "hexadecimális számjegy" egymás után), a bemenet pedig a következő:

ez itt két illeszkedés: deadbeef 042
ebben a sorban csak véletlenül van illeszkedés
itt nincs

foobar
654 a4131 849f

Akkor a kimenet legyen a következő:

deadbeef
042
ebbe
654
a4131
849f

Megoldás:

sed -n 's/[0-9a-f][0-9a-f][0-9a-f]\+/\n&\n/
        ta
        b
        :a
        s/[^\n]*\n//
        tx
        :x
        P
        D'

Ez a script talán jól példázza a sed write-only jellegét. Kb. fél éve írtam, de dokumentációt kell olvasnom ahhoz, hogy most megértsem, hogyan működik. :)

  1. Aránylag tiszta sor: a cseresztringben a & karakter helyére az illeszkedő karaktersorozat kerül be; tehát az első sor annyit csinál, hogy megkeresi az aktuális bemeneti sorban az első illeszkedést, és elé is, mögé is beszúr egy-egy újsor-karaktert.
  2. Ha végeztünk cserét (tehát volt illeszkedés a pufferben), átugrunk a 4. sorra, az "a" címkére.
  3. Ha nem végeztünk cserét, a script végére ugrunk.
  4. "a" címke.
  5. A puffer elejéről az első újsor-karakterig mindent törlünk (az újsor-karaktert is). Ez az első újsor-karakter az, amit az első sorban szúrtunk be az illeszkedő karaktersorozat elé.
  6. Ez egy kamu feltételes ugrás; a t parancs akkor ugrik, ha volt sikeres csere az aktuális sor beolvasása után az utolsó t parancs óta, akkor elágazik. Mivel még vissza fogunk menni a script elejére, ahol újabb cserét kísérelünk meg, aminek az eredményétől függően vagy elágazunk, vagy nem, itt mindenképpen "ki kell olvasni" az előző s/// parancs "visszatérési értékét", hogy a 2. sorban levő t ne azért ágazzon el, mert az 5. sorban levő s/// sikeres volt, hanem csakis akkor, ha az első sorban levő csere volt sikeres (tehát a sor még fel nem dolgozott részében volt illeszkedés a keresendő mintára).
  7. Ide érkezik a kamu-elágazás.
  8. A P parancs az első újsor-karakterig kiírja a puffer tartalmát (vagyis itt éppen egy illeszkedésnyit).
  9. A D parancs az első újsor-karakterig törli a puffer tartalmát (vagyis éppen a már kiírt részt törli). Ezután visszaugrik a script elejére; ha a puffer üres, új sort olvas be, ha nem, akkor a pufferben levő adatokkal folytatja a feldolgozást.

További példák pl. a http://sed.sourceforge.net/sed1line.txt címen találhatók.

[szerkesztés] 11 Miért zsh?

Időnként felmerül a kérdés, miért annyira jó a zsh, miért nem elég a Bash. Ide idővel összegyűjtök pár példát olyan feladatra, amit a zsh hatékonyabban, elegánsabban vagy egyszerűbben old meg, mint a Bash.

Kapásból: a zsh automatikusan megtöbbszörözi az outputot, vagy szekvencializál több inputot, ha többszörös átirányításokat adunk meg. Például (setopt multios kell hozzá):

echo exit 0 >>*.sh
ntfsclone -o - /dev/sda1 \
	>>(dd bs=32M of=/dev/sdb1 oflag=direct) \
	>>(dd bs=32M of=/dev/sdc1 oflag=direct) \
	>>(dd bs=32M of=/dev/sdd1 oflag=direct) \
	>>(dd bs=32M of=/dev/sde1 oflag=direct) \
	>>(dd bs=32M of=/dev/sdf1 oflag=direct) \
	>>(dd bs=32M of=/dev/sdg1 oflag=direct) \
	| dd bs=32M of=/dev/sdh1 oflag=direct
dd >file.img >>(md5sum >file.img.md5)
paste -d: <(cut -d: -f1,2 /etc/shadow) <(cut -d: -f3- /etc/passwd)

[szerkesztés] 11.1 Fájlok átnevezése

Adott egy könyvtár, benne számozott .srt és .avi kiterjeszétsű fájlok. Nevezzük át az összes srt-t úgy, hogy ugyanaz legyen a neve, mint az azonos sorszámú avinak (nyilván a kiterjesztést leszámítva).

#!/bin/zsh
setopt extendedglob
for i in {01..14}; do mv *$i*srt *$i*avi(#q:s/.avi/.srt); done

Másik példa: egy könyvtárban van egy csomó képfájl. Nevezzük át őket úgy, hogy a nevük egy növekvő számsorozat legyen!

% autoload zmv
% c=1 zmv '*.jpg' '$((c++)).jpg'

A http://grml.org/zsh/zsh-lovers.html oldal számos további érdekes és elvetemült példával szolgál.

[szerkesztés] 12 Ajánlott irodalom

Személyes eszközök