Shell-programozás
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[elrejté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
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).
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
4 Parancssor feldolgozása
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: 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.
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}
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.
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 *.csvHa 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).
- -x: minden parancssort kiír a script végrehajtása során.
- Hibakereséskor nagyon hasznos.
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 ...
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.
Erre kézenfekvő megoldás a ciklus:for i in $(find . -uid 1001); do chown 1002 "$i"; donede ennél kifinomultabbak is lehetünk, legalább háromféleképpen:
- Az xargs segítségével.
find . -uid 1001 -print0 | xargs -0 chown 1002
- 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
- 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.
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, 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 grepet 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 jelenlegi 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 {} +
A find még rengeteg szempont alapján tud keresni: jogosultságok, dátumok, reguláris kifejezések stb. Részletesebben l. man find.
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.
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.
- 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űt tartalmaz.
- 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 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:
- ismétlés (alma* illeszkedik pl. arra, hogy "alm", "alma", "almaa", "almaaa", s.í.t.)
- összefűzés (alma|barack jelentése értelemszerű, nem pedig "alm" majd "a" vagy "b" majd "arack")
- 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:]]*\>
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'
10.3 Sorok törlése
A grep -v emulációja: sed '/regex/d'. A d parancs törli az illeszkedő sort.
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!
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. :)
- 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 címen találhatók.
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.
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.