Hlavní navigace

Programování v (bash) shellu

16. 5. 2000
Doba čtení: 12 minut

Sdílet

Uživatel počítače je leckdy postaven před úkol, který by mu zabral mnoho času a přitom se přímo nabízí možnost napsat pro tento účel jednoduchý prográmek a usnadnit (a urychlit) si tak práci. Jenže "velké" programovací jazyky bývají často zbytečně těžkopádné a komplikované. A právě skriptovací jazyk shellu může být tím pravým kompromisem mezi možnostmi a složitostí. Dnes se seznámíme se skriptováním v bashi.

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.

  • "ret" Normální uvozovky. Řetězec uvnitř těchto uvozovek je chápán shellem literárně (obyčejný text). To znamená, že různé metaznaky jsou ignorovány (středník neznamená konec příkazu, ale prostě středník). Do textu jsou vkládány hodnoty proměnných.
    text=10; echo "text je $text;" # vytiskne: text je 10;
  • 'ret' Apostrof vedle klávesy enter. Řetězec uvnitř těchto apostrofů je chápán shellem literárně (obyčejný text). Na rozdíl od normálních uvozovek je navíc zamezeno nahrazování proměnných jejich hodnotou.
    text=10; echo 'text je $text;' # vytiskne: text je $text;
  • `ret` Obrácené apostrofy. Řetězec je shellem chápán jako příkaz k vykonání; Tento řetězec je vykonán před zpracováváním zbytku řádku a výsledek příkazu nahradí původní řetězec  `ret`
    text=whoami; echo `$text;` # v mem pripade vytiskne: chicky

    Následuje lepší příklad:

    echo "V aktualnim adresari je `ls | wc -l` souboru"

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.

Tabulka č. 62
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

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čí.

root_podpora

[ -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í

  • v podmínce if [ vyraz ] musí být testovaný výraz dodělen mezerou od závorek [ ].
  • u přiřazení promenna=hodnota nesmí být u rovnítka mezera (na žádné straně)

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.

Byl pro vás článek přínosný?

Autor článku