Shell-programozás

A Unix/Linux szerverek üzemeltetése wikiből
(Változatok közti eltérés)
a (Konfigurálható script: kicsit bővebb magyarázat)
a (A <tt>find</tt> használata: typo)
225. sor: 225. sor:
 
* Cél: hozzon létre egy ..hardlinks nevű könyvtárat;
 
* Cél: hozzon létre egy ..hardlinks nevű könyvtárat;
 
* azon belül 0-99 nevű könyvtárakat;
 
* 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 adja maradékul (tehát az 1-es könyvtárban lesz az 1, a 101, 201, az 1001 s.í.t.).
+
* 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.
 
** 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
 
* 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

A lap 2007. október 29., 23:33-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

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ékben 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

3 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).

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 -e
#
# 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.

4 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}

5 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 töltsük be a konfigurációt:
      [ -r "$CONFIGFILE" ] && . "$CONFIGFILE"
      azokat a defaultokat, amiket sem a konfiguráció, sem a parancssor nem ír felül, valahogy így juttathatjuk érvényre:
      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").
  • Ha nem kezelünk parancssori argumentumokat, még egyszerűbb a dolog:
    • a script elején állítsunk be minden változót az alapértelmezésre, majd
    • töltsük be a konfigurációt. Ami szerepel benne, az felülírja a defaultot, ami nem, az meg marad default.

6 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"; done
de 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 a megtalált fájlok listája nem kell, hogy a parancssor végén 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")"
# Kis voodoo: állítsuk elő a symlink nevét hash.utolsókönyvtárnév_fájlnév.link alakban
            ln -sf "$fullname" "$DESTDIR/${SUM}.${fullname:h:t}_${file:t}.link"
        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.

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 -print0 | xargs -0 rm

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

Személyes eszközök