Programování v (bash) shellu

Vytváření skriptů pro shell je analogické vytváření .bat souborů v DOSu. Skript pro shell se skládá z jednotlivých příkazů, které normálně píšete do příkazové řádky. Když se podíváte na následující příklad, vidíte, že tyto příkazy běžně používáte (snad až na to echo).
cd $HOME
cp soubor.txt soubor.zaloha1.txt
cp soubor.txt soubor.zaloha2.txt
rm soubor.txt
echo soubor.txt byl zalohovan a nasledne smazan
Aby tento příklad mohl fungovat, je nejjednodušší vytvořit nový soubor, např. mujskript.sh (Přípona není důležitá jako v DOSu. Systém je inteligentní a rozpoznává soubory podle obsahu. Klidně můžete příponu vynechat.)
Na první řádek souboru napište řádku:
#!/bin/bash
Tuto řádku budete psát na začátek všech vašich skriptů. Informujete tím shell, že má jako interpretr spustit /bin/bash. Dále už můžete psát příkazy, které chcete, aby skript provedl. Je důležité, aby byl každý příkaz na samostatném řádku. Pokud chcete mermomocí umístit na řádek více příkazů, musíte je oddělit středníky:
mount /floppy; mv archiv.tgz /floppy; umount /floppy
Naopak pokud je příkaz moc dlouhý, můžete jej rozdělit na více řádků znakem \
cp /home/student/inf98/sommerm/.xsession \
   /home/student/inf98/sommerm/.xsession-zaloha-dne-10.4.2000

Soubor může obsahovat prázdné řádky pro zvýšení čitelnosti. Pokud někde uvedete znak #, vše za tímto znakem, až do konce řádku, se ignoruje (využívá se pro komentáře). Až skončíte s editací souboru, nezapomeňte označit skript jako spustitelný (chmod 755 mujskript.sh). No a nyní ho spustíte jako jakýkoliv jiný program:

./mujskript.sh
Dávejte si obzvlášť dobrý pozor na to, kde se skript vykonává. Pokud obsahuje nebezpečné příkazy (rm, mv, cat /dev/null > soubor apod.) a místo v adresáři záloha je najednou v adresáři data, jenom proto, že proměnná $zaloha_path byla vždy nadstavena "nějak sama", může to mít nedozírné následky.
Toto samozřejmě platí pro všechny programovací jazyky, ale programování v shellu je tak jednoduché a provázané se systémem, že se může vyskytnout více chyb z nepozornosti. Ze začátku tedy zkoušejte programovat s méně destruktivními příkazy, nechte si vypisovat proměnné, potvrzovat různé kroky a především: zálohujte!

Proměnné

Doteď bylo vše stejné jako v DOSu. (Kdyby to náhodou bylo jinak, už je to dávno, klidně mi napište). Proměnnou zavedete prostě tak, že ji použijete.
promennaA=1
promennaB=$promennaA
echo Hodnota druhe promenne je: $promennaB
Jak jistě tušíte, vypíše se: Hodnota druhe promenne je: 1
Na příkladu je vidět zajímavá věc. Proměnnou poznáte tak, že před vlastním jménem proměnné je znak dolar. Ale když použijete proměnnou poprvé, respektive do proměnné něco přiřazujete, tak se zde dolar nepíše. (Toto však neplatí pro systémové proměnné $@, $0, $1... do kterých se nedá přiřazovat ručně. Pouze číst.)

Pokud potřebujete oddělit jméno proměnné od ostatního textu, stačí ji obklopit složenými závorkami

cesta=/home/
skupina=inf98
echo Seznam ucastniku ve skupine $skupina:
ls ${cesta}student/$skupina
Příklad udělá to stejné jako ls /home/student/inf98. Zde jsme museli proměnné $cesta a $skupina spojit řetězcem student/ (bez mezer!), protože jinak by příkaz ls obdržel několik samostatných argumentů a došlo by k vypsání obsahu adresáře /home/ a pravděpodobně chybové hlášení, že adresář student a inf98 neexistují. Kdybychom jméno proměnné $cesta neoddělili závorkami, shell by si myslel, že se jedná o proměnnou $cestastudent, která však neexistuje, a předal by příkazu ls "nic"/inf98, což by vedlo na chybové hlášení, že /inf98 neexistuje.

Když jsme to už nakousli, tak se pojďme podívat na systémové proměnné. Tyto proměnné nastavuje shell v závislosti na konkrétní situaci. Pokud bychom náš příklad spustili následovně:

mujskript.sh parametr1 parametr2 parametr3 ... parametrn
Hlavní systémové proměnné by vypadaly takto:

$? - obsahuje návratový kód posledního procesu spuštěného na popředí.
$$ - obsahuje pid aktuálního procesu (ať víte co zabíjet :-)
$0 - obsahuje jméno právě prováděného skriptu: mujskript.sh
$1 - obsahuje první argument, předaný vašemu skriptu, na příkazové řádce: parametr1
$9 - obsahuje 9-tý argument z příkazové řádky: parametr9
$* - obsahuje všechny argumenty volání programu (jako jedno slovo): parametr1 parametr2 ... parametrn
$@ - obsahuje všechny argumenty volání programu (jednotlivá slova): parametr1 parametr2 ... parametrn
$# - obsahuje počet argumentů z příkazové řádky (v našem případě n).

Pokud voláte skript s více než devíti argumenty, jsou nejvyšší (>9) parametry zatím nedostupné. Existuje příkaz shift, který posune argumenty o jedno dolů ($2 -> $1, $3 -> $2), takže desátý argument bude k dispozici v proměnné $9 a první argument se ztratí. Proměnná $# se automaticky zmenší o jedničku.

Vstup/výstup

Skript může získat od uživatele data příkazem read promenna. Tímto se načte do proměnné promenna vše až do stisku klávesy enter.
Pokud uvedeme více proměnných, uloží se do každé proměnné jedno slovo.
read promenna1 promenna2 .... promennaN
Jestliže je proměnných méně než vkládaných slov, uloží se do poslední proměnné celý zbytek řádku.

Jak jste si možná všimli, výstup programu zajišťuje příkaz echo. (Pro podrobnější seznámení doporučuji podívat se na manuálovou stránku echo).

echo Ahoj, ja jsem bash skript a jmenuju se $0. Co jsi ty?
V některých případech nebudete chtít, aby echo automaticky odřádkoval po ukončení výpisu (např. při otázce pro uživatele). Stačí jenom přidat parametr -n
echo -n Zadej svuj plat:
read plat
echo Nevypadas jako bys vydelaval $plat dolaru.
V souvislosti se vstupem a výstupem je vhodné se zmínit o různých uvozovkách a apostrofech. Tuto alchymii s řetězci byste měli ovládat, protože se rutinně používá, ale pro jistotu zde zopakujeme několik základních pravidel.

Logické výrazy

K vyhodnocení nějakého logického výrazu můžeme použít příkaz test vyraz, pripadne [ vyraz ] (mezera kolem závorek je povinná!). V praxi se používá hlavně druhý formát příkazu. Co vás může zarazit, že oproti běžným zvyklostem nastavuje test proměnnou $? na hodnotu různou od nuly při nepravdivosti a na 0 při pravdivosti tvrzení.
A=7
[ "$A" -eq 7 ] && echo Cisla se rovnaji
Co to je??? Takže popořádku. Nejprve do proměnné A uložíme hodnotu 7 (používáme ji poprvé, tudíž bez dolaru). Poté provedeme test výrazu "$A" -eq 7. Výraz se dá do češtiny přeložit jako "rovná se proměnná $A číslu 7?". Protože je to pravda, vrátí test této podmínky true a pokračuje se dalším příkazem (echo Cisla se rovnaji). Pokud by vrátil test false, výraz vyraz AND vyraz by nikdy nebyl pravdivý, tudíž se přeskočí vykonávání jeho pravé větve. Více testovacích operátorů najdete v tabulce 1-4.

tab.1: souborové operátory
[ -e soubor ]soubor existuje
[ -d soubor ]soubor existuje a je to adresář
[ -f soubor ]soubor existuje a je to obyčejný soubor
[ -L soubor ]soubor existuje a je to symbolický link
[ -s soubor ]soubor existuje a má nenulovou velikost
[ -r soubor ]soubor existuje a dá se číst
[ -w soubor ]soubor existuje a dá se do něj zapisovat
[ -x soubor ]soubor existuje a je spustitelný
[ f1 -nt f2 ]soubor f1 je novější než soubor f2
[ f1 -ot f2 ]soubor f1 je starší než soubor f2
tab.2: aritmetické operátory
[ c1 -eq c2 ]číslo c1 se rovná číslu c2
[ c1 -ne c2 ]číslo c1 se nerovná číslu c2
[ c1 -gt c2 ]číslo c1 je větší než číslo c2
[ c1 -ge c2 ]číslo c1 je větší nebo rovno číslu c2
[ c1 -lt c2 ]číslo c1 je menší než číslo c2
[ c1 -le c2 ]číslo c1 je menší nebo rovno číslu c2
tab.3: řetězcové operátory
[ -z ret ]řetězec ret má nulovou velikost
[ -n ret ]řetězec ret má nenulovou velikost
[ ret1 = ret2 ]řetězec ret1 je shodný s řetězcem ret2
[ ret1 != ret2 ]řetězec ret1 není shodný s řetězcem ret2
tab.4: logické operátory
[ -a ]spojí dvě podmínky (and)
[ -o ]spojí dvě podmínky (or)
[ ! ]neguje hodnotu následující podmínky
[ \( \) ]mezi tyto závorky můžete spojovat jednotlivé výrazy

Pro vyhodnocování aritmetických výrazů se dá použít příkaz expr, který rozeznává operátory +, -, *, /, %. Příkaz pracuje pouze s celými čísly.

A=9
B=3
vysledek=`expr $A / $B + 1`
echo Vysledek vyrazu '$A / $B + 1' je $vysledek
Pokud si spustíte následující příklad, vypíše se: Vysledek vyrazu $A / $B + 1 je 4
V dalším příkladě vidíte využití několika operátorů z předchozích tabulek.

if [ \( -r file1 \) -a \( ! -s file2 -o -d file2 \) ]
  then mv file1 file2
  else echo Nebudu kopirovat.
fi
Oops. Teď jsme se dostali trošku dál, než zatím umíme, ale zkuste si vyhodnotit podmínku mezi hranatými závorkami na prvním řádku.
Příklad dělá to, že zjistí, zda soubor file1 existuje a je čitelný a zároveň zda druhý soubor file2 neexistuje nebo je nulový nebo to je adresář. Pokud soubory této podmínce vyhovují, přesune se file1 na file2. V opačném případě se vypíše hlášení.

Vidím, že je nejvyšší čas přejít k další kapitole.

Řídící struktury a cykly

Jistě sami tušíte, že podmínkové výrazy by byly k ničemu, kdyby neexistovala možnost, jak ovlivnit provádění programu. A co by to byl za jazyk, kdyby neměl příkaz if. Základní tvar příkazu je takovýto:
if [ podminka ]
  then
    prikazy
  else
    prikazy
fi
Jak vidíte, příkaz má syntaxi velmi podobnou tomu co znáte z jiných programovacích jazyků. Všimněte si uzavíracího fi na konci příkazu. (Na toto začátečníci rádi zapomínají).
V některých případech je možné vynechat větev else, nebo naopak rozšířit na tvar elif (else if):
if [ -e ${HOME}/.nexrc ]; then
    echo Cool. Pouzivate editor vi
elif [ -e ${HOME}/.emacs ]; then
    echo Dalsi emacsista
else
    echo Pouzivate neutralni editor
    echo Mel byste si konecne vybrat jedno nabozenstvi ';)'
fi
Jak můžete vidět, slovo then je zvykem psát na řádek s if. Nesmí se však zapomenout na středník za podmínkou. To samé platí i pro větve elif - je to jen zkrácený zápis else if, tudíž musíte za podmínkou uvést klíčové slovo then.

Složené konstrukci if je velmi podobný příkaz case, který použijeme, když testujeme jednu podmínku na více hodnot:

case vyraz in
  vzorek1)
    prikazy;;
  vzorek2)
    prikazy;;
esac
Ne, nepřepsal jsem se. Na konci každé sekce jsou opravdu dva středníky (aby se rozlišil konec sekce, pokud píšete více příkazů na jeden řádek).
Příkaz se snaží porovnat vyraz se všemi vzorekN. Vzorky mohou obsahovat metaznaky *, ?, [, ], které mají stejný význam jako když specifikujete jméno souboru (pozor, toto nejsou regulární výrazy!).
Pro jednu větev můžete specifikovat více vzorků, které oddělíte svislou čárou | (znamená to, že stačí najít shodu pouze v jednom z uvedených - logické or).
case $1 in
     -r) parametry=$1
         echo Nastavil jsem parametr -r;;
  -v|-V) echo Parametry -v a -V zatim nic nedelaji;;
      *) echo Zadali jste neznamy parametr
         exit 1;;
esac
Výše uvedený kus kódu může být použit pro vyhodnocení prvního parametru z příkazové řádky ($1). Pro zpracování všech zadaných parametrů by se využil cyklus while a příkaz shift (viz. další část textu).

V dnešní době je nepředstavitelné, aby programátor nemohl opakovaně vykonávat zadanou sérii příkazů. Samozřejmě je možné okopírovat příkazy tolikrát, kolikrát je potřebujeme vykonat, ale stále to neřeší případ, kdy předem nevíme, kolikrát se příkazy mají opakovat (nehledě na estetickou stránku věci :). Proto zde máme několik možností:

Cyklus while


Cyklus se provádí tak dlouho, pokud test logickeho_vyrazu skončí úspěchem (výstupní hodnota je 0).
while [ logicky_vyraz ]   # while [ "$#" -ne 0 ]
do                        # do
  prikazy                 #  echo "parametr je $1"; shift
done                      # done
Syntaxe příkazu je zhruba taková, jakou byste očekávali. Nicméně je zde ještě jedna podoba tohoto příkazu, na kterou si asi budete muset trochu zvykat. (unixovská filozofie)
                          # echo "parametr je $1"
while prikaz              # while shift
do                        # do
  prikazy                 #  echo "parametr je $1"
done                      # done
Praktický příklad vpravo má dělat to stejné jako předchozí ukázka, ale pozorný čtenář odhalí jeho slabinu: vytiskne se o jeden řádek více. (jestli vymyslím lepší příklad, určitě to updatuju.)

Cyklus until

Velice podobný cyklus je until. Probíhá stejně jako while, ale test podmínky musí být neúspěšný, aby se přistoupilo k další iteraci.
until [ logicky_vyraz ]   # until [ "$#" -eq 0 ]
do                        # do
  prikazy                 #  echo "parametr je $1"; shift
done                      # done

Cyklus for

Pokud známe počet kroků, kolikrát se má cyklus opakovat, můžeme využít příkaz for.
for promenna in seznam
do
  prikazy
done
Posloupnost příkazů se postupně provádí pro všechny prvky seznamu. Tyto prvky jsou v seznamu odděleny mezerami nebo tabulátory. Pokud neuvedete část in seznam, provádí se cyklus nad seznamem parametrů, se kterými byl skript vyvolán (proměnná $*)
for i in 1 3 5 7
do
  echo $i
done
Vytiskne číslice 1, 3, 5, 7, každou na novém řádku. Síla tohoto příkazu spočívá ve způsobu, jakým se vytváří seznam, přes který se iteruje.
for i in `ls *.wav`
do
  echo $i
done
Tento relativně jednoduchý příkaz vypíše všechny soubory s příponou wav v aktuálním adresáři, což není nic ohromujícího. Zaměníme-li však řádku echo $i třeba na
lame -h -b 192 $i ${HOME}/hudba/${i}.mp3
provede skript to, že všechny wav soubory z aktuálního adresáře zkomprimuje do formátu mp3 pomocí enkodéru lame a výsledné mp3 soubory uloží do adresáře hudba ve vašem domovském adresáři.

Následující komplikovanější příklad všem uživatelům přihlášeným na tomto počítači vypíše na konzoli text "Odhlas se, chci byt sam!". Veškerý chybový výstup programu write je přesměrován do černé díry /dev/null.
Podle návratové hodnoty příkazu write je vypsáno jedno nebo druhé hlášení.

for i in `users`
do
  echo "Odhlas se, chci byt sam!"|write $i 2>/dev/null \
    && echo $i dostal zpravu || \
    echo zprava pro $i nemohla byt dorucena.
done
Skript by chtělo ještě vylepšit, ale pro základní pochopení problému nám to stačí.

Další užitečné drobnosti

Povel sleep způsobí pozastavení provádění skriptu na dobu zadanou v jednotkách [smhd] (sekundy, minuty, hodiny, dny)
i=80
echo "Nemame kam spechat, pockame $i sekund"
while [ $i -gt 0 ]
do
  sleep 1s
  echo -n "."
  i=`expr $i - 1`
done
echo ""
echo "no tak budeme pokracovat, no."
Ono to vypadá jako blbost, ale někdy se hodí s vykonáváním programu počkat (třeba aby si uživatel něco stihl přečíst, nebo pokud dáte programu nějaký čas a pokud do té doby neskončí, tak ho zabijete).

Pokud na konci příkazu uvedete znak ampersand &, bude se příkaz vykonávat na pozadí, tudíž se nebude čekat na jeho dokončení a běh skriptu bude okamžitě pokračovat dále. To je výhodné, třeba když nechcete čekat na zkopírování 600MB dat z jedné partition na jinou.

echo "Jenom tak budu kopirovat /dev/hda do /dev/null"
cp /dev/hda /dev/null&
echo "Zatim co mi to hrka s diskem, je cas na postu"
mutt

V nějakém příkladu jsme narazili na řádku prikaz1 && prikaz2. Kdo trochu programuje, tuší, že && je logická spojka AND a v tomto konkrétním případě znamená, že prikaz2 se provede jenom tehdy, pokud prikaz1 vrátí pravdivou hodnotu (tj. 0). Toto je velice častá konstrukce, která, spolu s kolegyní prikaz1 || prikaz2, umí pěkně zpřehlednit kód (místo testování if prikaz1 ; then prikaz2). Pěkné ukázky jsou ve startovacích skriptech /etc/init.d/

cd /tmp || exit
rm *.tmp
Poslední příklad s || je výhodný, pokud budete následně v adresáři /tmp mazat způsobem rm -rf *. Bez tohoto testu by se sice vypsalo chybové hlášení, že cd nemůže vstoupit do adresáře /tmp, ale ale další příkazy, včetně onoho rm -rf *, by se vykonaly v aktuálním adresáři. S jednoduchým || testem se při nesplněné podmínce skript ukončí.
[ -f $HOME/.emacs ] && (echo "pouzivate OS emacs, nabootuju vam ho"; emacs)
Když se tak dívám na předchozí příklad, napadá mě, že jsem použil jednu věc, kterou ještě neznáte: závorky. Nebudu to zde vysvětlovat (man bash). Zatím vám bude stačit vědět, že se používají k seskupování příkazů. Kdybych je nepoužil, emacs by se spustil vždy, protože vazba && se váže jenom na následující příkaz (echo).

Několik záludností

pro pokročilejší práci doporučuji prostudovat man bash, případně se porozhlédnout po internetu. Kdo občas jezdí do ciziny, může si v tamních knihkupectvích vybrat z přehršle zajímavých příruček.

Dobrou noc přeje chicky