Hlavní navigace

Hrátky z řádky: první krůčky při programování v Bashi

17. 3. 2008
Doba čtení: 7 minut

Sdílet

V dnešním díle se podíváme na základy práce s proměnnými a příkazy pro řízení běhu skriptu (if-then-else, for, while). Bash se pro vás tak nepopiratelně stane plnohodnotným programovacím jazykem. Jak však bylo možná vidět i v dřívějších ukázkách, na řadě míst vás drobný překlep snadno dostane do velkých potíží.

Skripty jsou interpretovány doslova řádek po řádku, takže např. neošetřená chyba na jednom znamená „jen“ to, že se problematický řádek přeskočí a pokračuje se dalším. Sotva kdy ale v takhle nepředpokládané situaci budou následující příkazy dělat to, co jste zamýšleli. Navíc skripty z principu spoléhají na celou řadu příliš slabě kontrolovaných entit: že je správně nastavená proměnná $PATH, že jsou k dispozici takové verze programů, které znají přepínače, jaké používáte. (Pověstný je v tomto směru rozdíl mezi Solarisem a jinými unixy jako je např. Linux. Standardní programy jako sort se sice jmenují stejně, ale chovají se pokaždé malinko jinak.)

Při programování v bashi buďte proto velmi opatrní, testujte vše a nevěřte ničemu. A nepište žádné velké skripty…

Co je to skript a jak se liší od „programu“

Skript se od „plnohodnotného programu“ liší jen dvěma drobnostmi: skript je textový soubor čitelný i prostému lidskému oku, kdežto „program“ je rovnou kompilován a instrukce v něm přímo čte procesor. Druhá drobnost je ta, že skript ke spuštění potřebuje interpret, tj. program, který skript zpracovává a „chová se podle něj“. Jinak se skript a program neliší, pro oba platí to, co jsme psali o vstupech, výstupech, návratových hodnotách a podobně v dřívějším dílu.

Z obyčejného textového souboru uděláme skript tak, že mu prostě nastavíte příznak spustitelnosti:

chmod +x muj_skript

Dobrým zvykem ovšem je napsat na první řádek skriptu, který interpret se má použít (jinak se použije aktuální shell). Formulka zvaná familiérně hashbang ( #!) pro bash vypadá typicky takto:

#!/bin/bash

Mimochodem právě hashbang je rozhodující znak pro utilitku file; buď váš skript prohlásí za anglický text v ASCII, nebo za skript pro bash.

Stejný hashbang (ovšem s jiným interpretem) se užívá pro všechny skriptovací jazyky, a až v bashi zvládnete příkazy read a select, bude pro vás hračka napsat si v bashi interpret vlastního jazyka…

Proměnné interní a proměnné prostředí

Jedním ze základních stavebních kamenů každého programovacího jazyka jsou proměnné, úložné místo pro mezivýsledky výpočtu. Bash je současně trošku programovací jazyk a současně nástroj pro řízení prostředí Unixu. Proto u proměnných odlišuje dva základní stavy: proměnná pouze interní a proměnná „exportovaná“ mezi další proměnné prostředí. Exportované proměnné mohou číst i programy, které skript spustí. Dobrým zvykem je pojmenovávat proměnné prostředí jen velkými písmeny a interní proměnné naopak malými písmeny, závazné to ale není.

Hodnotu proměnné nastavíte rovnítkem, případné mezery je třeba ochránit, jak jsme si již říkali dříve. Exportovat proměnnou můžete buď samostatným příkazem export nebo export a přiřazení spojit:

promenna=slovo_1" a druha cast"; export promenna
# nebo
export promenna=hodnota

V bashi je navíc možnost exportovat proměnné jen pro jeden příkaz těsně před jeho spuštěním:

prom1=hod1 prom2=hod2 prikaz
# po skončení prikazu už prom1, ani prom2 definovány nejsou

Operace s proměnnými

Na několika příkladech ukážu základní „úpravy“ hodnot proměnných. Místo podrobného komentáře ale znovu zdůrazním, proč byste své programování v bashi měli omezit na minimum:

  • Každá mezera v hodnotě proměnné a každá nečekaně prázdná proměnná vás kousne, pokud proměnnou byť jedinkrát zapomenete obalit uvozovkami.
  • Aritmetika v bashi je jen celočíselná, hodí se tak na jednoduché čítače.
  • Proměnné se hodí jen na poměrně krátké řetězce, větší data udržujte raději vždy v pomocných souborech (a tam je zase problém se soupeřením skriptů, ale o tom někdy jindy).
  • Běh bashových skriptů je pomalý a nákladný na systémové zdroje, skoro každý příkaz znamená spuštění nějakého programu.

A teď ty náměty pro vaši fantazii. Zabezpečení proti všemu nečekanému nechávám na vás (např. cokoli jiného než číslo v $i), stejně jako některé další možnosti:

i=$(($i+1)) # $(()) je "aritmetická expanze", užili jsme ji na inkrementaci čítače
fn=$dir/file$i.gz # spojení řetězců -- prostě napište řetězce za sebe
soub=${fn//file/soubor} # náhrada všech výskytů řetězce "file" v hodnotě proměnné $fn
ungzfn=${fn/.gz/} # nebezpečné odstranění .gz, odstraní první výskyt, nikoli nutně příponu
ungzfn=${fn/%.gz/} # bezpečné odstranění přípony .gz, odstraní jen na konci řetězce
unfilefn=${fn#$dir/file} # bezpečné odstranění předpony "$dir/file", odstraní jen na začátku řetězce
surefn=${fn:-nahradni_soubor} # hodnota proměnné $fn, nebo náhradní hodnota, není-li $fn definováno
surefn=${fn:=nahradni_soubor} # hodnota proměnné $fn, nebo náhradní hodnota, není-li $fn definováno, navíc *nastaví i proměnnou fn*

Řízení běhu – větvení

Řízení běhu (větvení a cykly) je v bashi založeno na návratových hodnotách programů, viz dřívější díl. Zde je příklad větvení:

logf=muj_log.txt
if grep Failed $logf; then \
  echo "Můj log už obsahuje signál chyby."; \
elif grep Succeeded $logf; then \
  echo "Už máme úspěch!"; \
else \
  echo "Zatím se nic neví"; \
fi

Středníky a zpětná lomítka na koncích řádků uvádím pro zdůraznění, že bash musí dostat celý if-then-else-fi najednou. Pokud větvení píšete do skriptu nebo na příkazovou řádku, můžete „ \“ před koncem řádky vynechat – bash ví, že něco musí následovat, a načte další kousek skriptu nebo vás promptem poprosí o pokračování. Pokud však větvení bashi dáváte nějak nepřímo (např. v Makefilu, v definici aliasu atp.), středníky potřebujete a „ \<newline>“ je zavedená opisná sekvence pro „tady řádek vlastně nekončí“.

Povšimněte si, že za then, elif a else středník není (nesmí být), před nimi naopak musí.

Řadu ifů lze někdy nahradit matchováním výrazů; takhle např. můžete detekovat, kde je váš notebook (zpětná lomítka na konci řádek opět jen proto, abyste se nebáli případně řádky spojit hned za sebe):

case $(/sbin/ifconfig | grep -A1 ^eth0 | sed -n 's/^.*inet addr:\([^ ]*\).*/\1/p') in \
195.113.*) echo škola ;;  \
10.*|192.168.*) echo doma ;; \
*) echo nevím ;; \
esac

Povšimněte si roury jako oddělovače jednotlivých vzorů, kulaté závorky jako oddělovače vzoru od příkazů a (povinného) dvojitého středníku pro ukončení každé varianty. Bash provede první odpovídající variantu a ostatní přeskočí. Pro případ, že žádný vzor neodpovídá, jsme připravili závěrečné „ *)“; bez něj by celý case neudělal nic.

Řízení běhu – cykly

Dvě ukázky for-cyklu:

for f in *.gz; do gunzip < $f > ${fn/%.gz/}; done
# ke každému zabalenému souboru vyrobíme i rozbalený
# běžný gunzip *.gz by originály smazal

for i in nula jedna `seq -w 2 6` sedm; do echo $i; done
# legrační počítání, vypíše: nula jedna 2 3 4 5 6 sedm

A dvě ukázky while:

# aktivní čekání, až příkaz ping uspěje
while ! ping -c 1 www.ja-sam.cz; do sleep 2; done; echo "Server naběhl"

# Takto konzoli změníte na užitečného interaktivního pomocníka:
# Ukončete pomocí Ctrl-D
prompt="Zadej doménu: "; echo -n $prompt; \
while read dom; do whois $dom; echo -n $prompt; done; echo "Konec"

Příkaz testu

Zatím umíme větvení a cykly založit např. na grepu. Při kapce invence by vás napadlo použít

cat soubor >/dev/null 2>&1

jako nákladný test existence souboru a hrozivé echo-echo-diff pro rovnost proměnných. Naštěstí existují lepší alternativy: základní „ [“ a vylepšený „ [[“ test. Základní test má chudší repertoár operátorů a je zachován pro zpětnou kompatibilitu.

Příkaz testu (a nadále se držme vylepšeného) je příkaz jako každý jiný, jen se jmenuje divně „ [[“ a navíc vyžaduje, aby poslední jeho argument byl řetězec „ ]]“. Navenek to vypadá, že se testy píší do dvojitých hranatých závorek; fakt, že jde o obyčejný příkaz a jeho argument, však vyžaduje mezery kolem závorek (a přesný počet argumentů uvnitř).

Často užívané operátory a obraty si představíme v příkladech:

[[ x$a == x ]] && echo "proměnná a je prázdná" # protože když jí předřadíte x, dostanete zas jen x
[[ -z $a ]] && echo "jiný test na prázdnost"
[[ -e soubor ]] && echo "soubor existuje"
[[ -d adr ]] && echo "adr existuje a je adresář"
[[ 0 == 0.0 ]] && echo "řetězec 0 je shodný s 0.0"  # není shodný, samozřejmě
[[ 50 -eq 050 ]] && echo "číslo 50 je sice rovno 050, ale můj bash to nepozná, je váš lepší?"  # celočíselná rovnost
[[ 215 < 23 ]] && echo "v abecední pořadí platí: 215<23"
[[ 215 -lt 23 ]] && echo "tohle nenastane, v celočíselném uspořádání neplatí: 215<23"

Ve výše uvedené ukázce while jste si možná všimli vykřičníku před příkazem ping pro negaci (booleovského výkladu) návratové hodnoty. Stejně můžete negovat výsledek příkazu testu, a příkaz testu navíc akceptuje vykřičník i uvnitř:

CS24_early

# následující dva příkazy jsou ekvivalentní
[[ ! -z $a ]]
! [[ -z $a ]]
[[ -n $a ]] # a tohle je třetí možnost testu neprázdnosti

Kombinace testů (a jiných příkazů) je samozřejmě možná pomocí || a &&, závorkování pomocí složených závorek. Dejte pozor na povinný středník před zavírací složenou závorkou:

if ! [[ -e s1 ]] || { [[ ! -e s2 ]] && grep a s3; } ; then echo x; fi

A tím pro dnešek už opravdu ukončíme.

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

Autor článku