Shell-programozás
a (→Parancssor feldolgozása: két jó módszer, a következő négy:) |
a (új link: http://sed.sourceforge.net/grabbag/scripts/turing.sed) |
||
(egy szerkesztő 8 közbeeső változata nincs mutatva) | |||
252. 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. |
||
281. 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> |
||
292. sor: | 292. sor: | ||
** 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. |
** 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: |
||
327. 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 |
||
364. sor: | 364. sor: | ||
* <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. (Ez a konstrukció a script jelenlegi változatában nem szerepel, de érdemes ismerni.) |
+ | * <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"). |
* <pre>${fullname:e}</pre> a $fullname tartalmából az utolsó pont utáni részt adja vissza (vagyis a "kiterjesztést"). |
||
378. sor: | 378. sor: | ||
# Usage: ln-dup-files dir [dir2 [...]] |
# Usage: ln-dup-files dir [dir2 [...]] |
||
− | # MYIFS egyetlen soremelésből áll |
+ | # 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=" |
MYIFS=" |
||
" |
" |
||
− | OLDIFS="$IFS" |
+ | 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/files # Hogy az ln(1) belső parancs legyen, ne külső |
||
389. sor: | 389. sor: | ||
typeset -A metadata # Egy olyan hash, amely mode-uid-gid-size-sha1sum n-esekhez inode-fájlnév párokat rendel |
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 |
+ | 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 |
find $@ -type f -size +1c -printf "%p\n" | while read file; do |
||
395. sor: | 395. sor: | ||
statstr="$seen_inodes[$stat[inode]]" # Lássuk, megvan-e a cache-ben |
statstr="$seen_inodes[$stat[inode]]" # Lássuk, megvan-e a cache-ben |
||
if [[ -z "$statstr" ]]; then # nincs |
if [[ -z "$statstr" ]]; then # nincs |
||
− | IFS="$OLDIFS" |
+ | IFS="$SHAIFS" |
− | sha1sum "$file" | read SUM foo |
+ | 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" |
IFS="$MYIFS" |
||
statstr=$stat[mode]_$stat[uid]_$stat[gid]_$stat[size]_$SUM |
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 |
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 |
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]" | { |
echo "$metadata[$statstr]" | { |
||
read inode |
read inode |
||
read name |
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, ha 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. |
+ | } # 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 [[ -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? |
+ | 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) |
+ | 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 |
((saved+=$[stat[size]])) # És nyomon követjük, mennyi helyet spórolunk meg |
||
ln -f "$name" "$file" |
ln -f "$name" "$file" |
||
423. sor: | 424. sor: | ||
** 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. |
** 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. |
* 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. |
* A második script memóriaigénye a vizsgált fájlok számával arányos; az elsőé független tőle. |
||
434. sor: | 436. sor: | ||
* 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. |
* 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 még rengeteg szempont alapján tud keresni: jogosultságok, dátumok, reguláris kifejezések stb. Részletesebben l. <tt>man find</tt>. |
+ | 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> == |
||
462. sor: | 464. 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. |
||
469. sor: | 472. 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ű. |
||
658. sor: | 662. sor: | ||
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. |
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 29., 00: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 *.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).
- -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"; donede 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:
- 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:]]*\>
[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. :)
- 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.
[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
- David A. Wheeler: Fixing Unix/Linux/POSIX Filenames: Control Characters (such as Newline), Leading Dashes, and Other Problems, 2012-12-14
- Google Shell Style Guide (kikapcsolt javascripttel olvashatatlan)
- Turing-gép sedben :)