Psaní shellkódu
Psaní shellových skriptů
V programátorských příručkách bývá zvykem začínat primitivním programem hello world. Psaní shellových skriptů je programování hodně podobné, a proto si zkusíme na primitivním programu ukázat náležitosti shellového skriptu:
#!/bin/bash echo "Hello world."
Jednoduché, že? První řádka označuje, který interpret se má použít ke "spuštění" příslušného souboru. Na rozdíl od programovacích jazyků s kompilátory, které produkují spustitelný strojový kód, potřebují shellové skripty interpret, tedy program, který daný skript vykoná.
Takto opatřený soubor můžeme učinit spustitelným:
chmod a+x skript
Pak postačí jej spustit:
./skript
Všimněte si, že programy v aktuálním adresáři spouštíme tak, že se na aktuální adresář odkážeme v cestě. V unixových systémech totiž aktuální adresář není součástí cesty, která se prohledává při zadání příkazu. Je to vhodné bezpečnostní opatření pro případ, že někdo vytvoří škodlivý skript se stejným jménem jako některý z často používaných příkazů.
Výchozí cesta většinou obsahuje adresáře jako /bin
či /usr/bin
,
kde se nachází spustitelné soubory všech nainstalovaných uživatelských
programů. V GNU/Linuxu si tedy nemusíte dělat starosti s tím, kam se vám
nainstaloval třeba Firefox, protože jeho spustitelný soubor se nachází v
cestě, která se po zadání příkazu bude prohledávat. Není tedy problém
spustit Firefox přímo z příkazového řádku:
[michal@griffon ~]$ firefox
Skripty lze samozřejmě spouštět "ručně", pomocí interpretu, takto:
bash skript
Dosud jsme nerozlišovali typ použitých příkazů. Vlastní příkaz může být
název spustitelného souboru, který tvoří nějaký program, může to být ale i
vestavěná funkce shellu nebo třeba alias. Příkazy jako ls
, grep
, cat
či ssh
jsou
názvy spustitelných souborů, tedy programy. Naopak třeba příkaz cd
je
vestavěnou funkcí shellu. Není to žádný konkrétní program. Podobně jako
třeba echo
.
Této znalosti můžeme využít, pokud budeme v prostředí, kde máme sice shell,
ale už ne jednotlivé programy jako ls
.
Program ls
můžeme
nahradit právě použitím vestavěné funkce shellu echo
:
echo *
Poslední kategorií jsou aliasy. Ty můžete specifikovat vedle dalších
nastavení ve svém ~/.bashrc
.
Alias je jiné jméno pro nějaký příkaz. Můžete si tak namapovat nějaký
komplikovanější příkaz na snadno zapamatovatelný alias:
alias la='ls -la'
Pokud by se vám stýskalo po logických označeních jednotek, můžete si pomoci třeba takto:
alias c:='cd /mnt/windows'
Pokud nevíte, co je daný příkaz zač, můžete zkusit vestavěnou funkci shellu type
:
type prikaz
Proměnné slouží k uložení určitých hodnot či řetězců. Začneme proměnnými
prostředí. O jedné jsme už několikrát mluvili, a sice o cestě, která se
prohledává při zadání nějakého příkazu, který shell nerozpozná jako
vestavěnou funkci. Proměnná se jmenuje PATH
a
její obsah můžeme zobrazit takto:
echo $PATH
Všechny proměnné prostředí můžeme vypsat pomocí příkazu set:
set
Další zajímavou proměnnou prostředí je PS1
,
která určuje tvar výzvy (prompt). Výzvu už jsme tu mnohokrát měli, vypadá
třeba takto:
[michal@griffon ~]$
Mnoho informací o proměnných prostředí naleznete v manuálu bashe:
man bash
Proměnné nemusí ale být jenom proměnné prostředí. Můžete si je tvořit sami,
což se zejména ve skriptech velice hodí. Proměnnou vytvoříte pomocí znaku =
:
PROMENNA="Moje proměnná."
Vlevo je název proměnné, vpravo za =
je
její hodnota. Tento zápis netoleruje mezery. Pokud byste =
oddělili
mezerami, nebude to fungovat.
Hodnotu proměnné můžete dosadit kamkoliv, pokud před název proměnné zapíšete znak dolaru:
echo $PROMENNA
Ukažme si malý skriptík s proměnnou:
#!/bin/bash LOG="~/log.txt" cp soubor /tmp 2&>$LOG
Omlouvám se za takto primitivní příklad. Ano, jak tušíte, tento skript
překopíruje soubor soubor
do
adresáře /tmp
a
veškeré případné chybové hlášky zapíše do souboru, který je specifikován v
proměnné LOG
.
Pokud budete tvořit složitější skript, vyplácí se nějaké statické hodnoty
(třeba adresáře, soubory, apod.) specifikovat v proměnných. Pak, když budete
potřebovat hodnoty změnit, nemusíte procházet celý soubor, změníte pouze
hodnotu v proměnné.
Do proměnných můžete přirozeně ukládat i výstup nějakého příkazu:
PROMENNA=$(ps au | grep "bash")
Základní matematiku (celočíselné operace) provádí následující syntax:
$((vyraz))
Za vyraz
stačí
dosadit nějakou matematickou operaci, třeba:
$((4+5*(5-20/5)))
Operace vyžadující plovoucí desetinnou čárku můžete provádět pomocí programu bc
.
Jeho použití je ale trošku složitější:
echo "scale=2; 4*4-6*(1/25)" | bc
Proměnná scale
nastavuje
počet desetinných míst. Přirozeně, na místě pro jednotlivá čísla můžete
uvádět proměnné a výsledek ukládat do jiných proměnných.
Pokud často používáte nějaký sled příkazů, hodí se jej umístit do funkce, kterou lze pak spouštět jediným příkazem. Třeba takto:
#!/bin/bash function nase_funkce { echo "Kopiruji soubor ..." cp soubor.txt /tmp du -h /tmp/soubor } nase_funkce
Tohle je opět značně primitivní příklad, ale svůj účel plní. Vidíme, jak se funkce vytváří i jak se používá. Funkcím lze předávat parametry:
#!/bin/bash function funkce { echo "Parametr funkce c. 1: $1" echo "Parametr funkce c. 2: $2" } funkce parametr1 parametr2
O parametrech funkcí i skriptů se ale dozvíme více později.
Průběh skriptu může někdy narazit na situaci, kde bude třeba o něčem rozhodnout. Za tímto účelem máme k dispozici podmínku:
if [ podminka ]; then prikaz fi
To je nejjednodušší možná konstrukce podmínky. Pokud je podmínka podminka
pravdivá,
provede se prikaz
.
Je však možné konstruovat i složitější podmínky, třeba jako je tato:
if [ vyraz1 ]; then prikaz1 elif [ vyraz2 ]; then prikaz2 else prikaz3 fi
V tomto případě se provede prikaz1
,
jestliže je podmínka vyraz1
pravdivá.
Pokud není, ale je pravdivá podmínka vyraz2
,
provede se prikaz2
.
Pokud ani tato podmínka pravdivá, provede se prikaz3
.
Ještě než zkusíme nějaký reálný příklad, povíme si více o podmínkách.
Podmínka je nějaký výraz, který nabývá určitých logických hodnot (pravda
či nepravda). V zásadě, všechny podmínky interpretuje program test
,
takže když se podíváte do jeho manuálové stránky, objevíte přehledný
výpis všeho, co lze testovat. Já se pokusím o stručný vytah.
Tabulka 8.1. Podmínky obecně
Výraz | Význam |
---|---|
| podmínka je pravdivá, je-li výraz pravdivý |
| podmínka je pravdivá, pokud výraz není pravdivý (negace) |
| podmínka je pravdivá, pokud jsou oba výrazy pravdivé (logická spojka AND) |
| podmínka je pravdivá, pokud je alespoň jeden z výrazů pravdivý (logická spojka OR) |
Tabulka 8.2. Podmínky vztažené k řetězcům
Výraz | Význam |
---|---|
| řetězec je neprázdný |
| řetězec je prázdný |
| řetězce jsou shodné |
| řetězce jsou různé |
Tabulka 8.3. Podmínky vztažené k (celým) číslům
Výraz | Význam |
---|---|
| číslo1 = číslo2 (equals) |
| číslo1 ≥ číslo2 (greater or equal) |
| číslo1 > číslo2 (greater than) |
| číslo1 ≤ číslo2 (lower or equal) |
| číslo1 < číslo2 (lower than) |
| číslo1 ≠ číslo2 (not equal) |
Tabulka 8.4. Některé podmínky vztažené k souborům
Výraz | Význam |
---|---|
| soubor existuje |
| soubor existuje a je to soubor |
| soubor existuje a je to adresář |
| soubor existuje a je to symbolický link |
| soubor existuje a je to blokové zařízení |
| soubor existuje a je to znakové zařízení |
| soubor existuje a je to pojmenovaná roura |
| soubor existuje a má nenulovou velikost |
| soubor existuje a je to socket |
| soubor1 je novější (newer than) soubor2 |
| soubor1 je starší (older than) soubor2 |
Seznam podmínek není úplný (v případě souborů), ale jak už jsem
podotýkal, man
test
je vhodné si
projít. Příklady různých výrazů si ukážeme v následující sekci.
Pro začátek zkusme něco jednoduchého:
if [ `grep -c ^michal /etc/passwd` -eq 0 ]; then echo "Uživatel 'michal' neexistuje." else echo "Uživatel 'michal' existuje." fi
Příkaz grep
-c ^michal /etc/passwd
vypíše
počet řádků souboru /etc/passwd
,
které začínají na michal
.
Pokud je toto číslo nula, je jasné, že žádný uživatel michal
v
systému není. Jiným příkladem může být:
if [ -f /var/run/mysqld.pid ]; then echo "MySQL server pravděpodobně běží." else echo "MySQL server neběží." fi
Podmínka testuje existenci souboru, který by měl být vytvořen po startu
MySQL serveru (obsahuje PID běžící instance mysqld
)
a vymazán při jeho ukončení. Jistější by bylo použití programu ps
,
ale to by byl jednak příklad podobný předchozímu, a pak bych nemohl
poukázat na to, že v GNU/Linuxu a shellu lze jednotlivé úlohy řešit
různým způsobem, přičemž každá z variant může vést k cíli.
if [ -z "$PROMENNA" ]; then echo "Proměnná je prázdná." else echo "Proměnná obsahuje '$PROMENNA'" fi
Tato podmínka testuje, je-li proměnná PROMENNA
prázdná,
přičemž pokud není, vypíše, co obsahuje.
Řekněme, že máme následující skript:
#!/bin/bash PROMENNA="dve slova" if [ -z $PROMENNA ]; then echo "Promenna je prazdna." else echo "Promenna obsahuje $PROMENNA" fi
Pokud ho spustíme, dostaneme následující hlášku:
skript: line 5: [: dve: binary operator expected
Problém nastal na řádce 5 (tj. řádce z podmínkou). Jelikož proměnná,
kterou testujeme, obsahuje dvě slova oddělená mezerou a po dosazení
proměnné do podmínky při běhu skriptu se tato mezera interpretuje jako
oddělovač, otestuje se pouze řetězec "dve
" a
řetězec "slova
" se Bash pokusí mylně
interpretovat jako další část podmínky:
if [ -z dve slova ]; then
Tento problém lze odstranit uzavřením proměnné do uvozovek:
if [ -z "$PROMENNA" ]; then
Pak, po dosazení proměnné se Bash dopracuje k výrazu, kde "uvidí" pouze jediný řetězec:
if [ -z "dve slova" ]; then
Uzavření proměnné do uvozovek pomůže třeba i v situaci, kdy je příslušná proměnná prázdná. Další problém může nastat velmi snadno v případě, kdy je proměnná nesprávného typu. I když jsou v Bashi všechny proměnné považovány za řetězce, v některých případech může Bash očekávat pouze určité znaky. Třeba v situaci, kdy testujeme nějakou číselnou hodnotu, očekává Bash řetězec složený pouze z čísel:
#!/bin/bash CISLO="ahoj" if [ "$CISLO" -eq 5 ]; then echo "Cislo je rovne 5." else echo "Cislo neni rovne 5." fi
V tomto případě dostaneme hlášku:
skript: line 5: [: ahoj: integer expression expected
Tou se vám Bash snaží naznačit, že tam, kde očekával číslo, číslo není. V tomto případě můžeme před touto podmínkou testovat příslušnou proměnnou pomocí regulárních výrazů, zda-li obsahuje pouze čísla. Většinou ale postačí, pokud si tuto záležitost pouze sami ohlídáte.
Další velmi typickou chybou je zapomenutí nějaké párové značky, třeba uvozovek:
#!/bin/bash PROMENNA="ahoj if [ -z "$PROMENNA" ]; then echo "Promenna je prazdna." else echo "$PROMENNA" fi
Po spuštění tohoto skriptu nám Bash zahlásí:
./skript: line 7: unexpected EOF while looking for matching `"' ./skript: line 9: syntax error: unexpected end of file
V tomto případě nezbyde než problém najít a odstranit. V delších skriptech vám pomohou editory s barevným zvýrazněním syntaxe.
Ne vždycky ale bude chyba zcela zřejmá. Může se stát, že nevíme přesně,
kde chyba nastala. Pak můžeme použít parametr -x
,
buď takto:
bash -x skript
Nebo umístíme do záhlaví skriptu lehce pozměněnou úvodní řádku:
#!/bin/bash -x
Tak nám Bash bude vypisovat průběh skriptu. Můžeme takto označit třeba jen některou část skriptu:
#!/bin/bash CISLO="ahoj" set -x if [ "$CISLO" -eq 5 ]; then echo "Cislo je rovne 5." else echo "Cislo neni rovne 5." fi set +x [...]
Jiným trikem je třeba zakomentování nějakého podezřelého úseku:
#!/bin/bash CISLO="ahoj" if [ "$CISLO" -eq 5 ]; then echo "Cislo je rovne 5." #else # echo "Cislo neni rovne 5." fi
Pokud pracujeme s nějakým nebezpečným programem, můžeme při ladění skriptu nechat použití inkriminovaného programu nechat vypisovat:
#!/bin/bash cp /var/log/messages /tmp echo rm /tmp/messages
Což se bude hodit ještě více, pokud danému nebezpečnému programu budete předhazovat nějakou proměnnou, kde s jistotou nevíte, co se v ní bude v danou chvíli nacházet.
Pokud se rozhodujeme mezi dvěma možnostmi, je konstrukce if
optimální.
Pokud se naopak potřebujeme rozhodovat mezi více možnostmi podle obsahu
proměnné, můžeme použít konstrukci case
:
case $promenna in 1 ) echo "Promenna je rovna 1" ;; 2 ) echo "Promenna je rovna 2" ;; 3 ) echo "Promenna je rovna 3" ;; * ) echo "Promenna neni rovna 1, 2 ani 3" esac
Můžeme použít i složitější výrazy se zástupnými znaky:
case $promenna in [a-z] | [A-Z] ) echo "Jedno male nebo velke pismeno" ;; [0-9] ) echo "Jednociferne cislo" ;; * ) echo "Neco jineho" esac
Někdy potřebujeme určitou činnost opakovat. A to je úloha pro cykly. Ty
můžeme rozdělit do dvou kategorií. Cykly provádějící určitý počet opakování
(cyklus for
)
a cykly založené na podmínce (cykly while
a until
).
Syntaxe cyklu for
vypadá
takto:
for promenna in 1 2 3 4 5; do echo "$promenna" done
Cyklus pracuje tak, že každý z prvků za in
vždy
postupně dosadí do proměnné promenna
,
provede kód uvnitř svého těla a tento postup opakuje do té doby, dokud
nevyčerpá všechny prvky. V tomto případě jsme za prvky dosadili čísla od
1 do 5. Výstup tohoto cyklu by vypadal takto:
1 2 3 4 5
Pokud chceme použít určitou sekvenci čísel, nemusíme se namáhat s jejich
vyplňováním. Postačí použít příkaz seq
:
for i in `seq 1 50`; do echo $i done
Tento úsek kódu vypíše čísla od 1 do 50. Cyklus for
však
umožňuje, aby cyklem procházely i zcela jiné hodnoty. Kupříkladu,
následující skript vypíše obsah aktuálního adresáře:
for i in *; do echo $i done
Cykly while
a until
pracují
tak, že opakují určitý postup, dokud je splněna určitá podmínka (cyklus while
)
nebo dokud určitá podmínka splněna není (cyklus until
.
Syntaxe je prakticky shodná:
cislo=4 until [ $cislo -eq 5 ]; do echo "$cislo" cislo=$((cislo+1)) done while [ $cislo -gt 0 ]; do echo "$cislo" cislo=$((cislo-1)) done
Tento program vypíše:
4 5 4 3 2 1
První cyklus se opakuje tak dlouho, dokud nenastane jeho podmínka, tj.
dokud proměnná cislo
nabyde
hodnoty 5. Druhý cyklus se opakuje tak dlouho, dokud je jeho podmínka
splněná, tj dokud je proměnná cislo
větší
než 0.
Pomocí cyklu while
můžeme
třeba zpracovávat po řádcích nějaký soubor:
cat soubor | while read radka; do echo $radka done
Někdy můžeme mít zájem běh nějakého cyklu ukončit úplně nebo nedokončit
iteraci a provést další opakování. K tomu slouží příkazy break
a continue
:
#!/bin/bash for i in 1 2 3 4 5 6; do if [ $i -eq 3 ]; then continue elif [ $i -eq 5 ]; then break fi echo $i done
Výstupem tohoto skriptu bude:
1 2 4
Proč? Když se do proměnné i
dostala
trojka, použili jsme příkaz continue
,
který přeskočil zbytek těla konstrukce for
a
přistoupil k další hodnotě, tedy ke čtyřce. V momentě, kdy se do
proměnné i
dostala
pětka, provedl se příkaz break
,
který ukončil provádění celého cyklu.
Skriptům nebo funkcím můžeme předávat parametry. Ty jsou pak k dispozici buď
jako pole všech parametrů reprezentované proměnnou $@
nebo
jako jednotlivé parametry v proměnných $1
, $2
,
atd. Proměnná $0
obsahuje
jméno souboru s vlastním skriptem.
Vše si ozřejmíme na následujícím příkladě. Mějme skript:
#!/bin/bash echo "Soubor se skriptem: $0" echo "Prvni parametr: $1" echo "Druhy parametr: $2" echo "Treti parametr: $3" echo "Pole vsech parametru: $@"
Tento skript spustíme následujícím způsobem:
./skript jedna dve tri
Následně obdržíme tento výpis:
Soubor se skriptem: ./skript Prvni parametr: jedna Druhy parametr: dve Treti parametr: tri Pole vsech parametru: jedna dve tri
Někdy můžeme dát přednost interaktivnímu programu, který požádá uživatele o
zadání nějaké hodnoty. V takové situaci můžeme použít příkaz read
,
který uloží uživatelský vstup do proměnné. Ukažme si to na příkladu:
#!/bin/bash while true; do echo -n "Zadejte polomer (Ctrl-C ukonci skript): " read POLOMER echo -n "Obsah kruhu s polomerem $POLOMER je" echo "scale=3; 3.14*$POLOMER^2" | bc done
Tento program využívá mnohé z toho, co jsme se naučili už dříve. Umožňuje
spočítat obsah kruhu. Cyklus while
má
v tomto případě místo podmínky dosazený program, který navrací pravdivostní
hodnotu 1 nebo true
(pravda).
To znamená, že cyklus se bude opakovat, dokud uživatel násilně neukončí
skript pomocí Ctrl+C.
Dosavadních znalostí můžeme využít i pro vytvoření lepšího řešení:
#!/bin/bash function obsah { echo -n "Obsah kruhu s polomerem $1 je " echo "scale=3; 3.14*$1^2" | bc } if [ -z "$1" ]; then while true; do echo -n "Zadejte polomer (Ctrl-C ukonci skript): " read POLOMER obsah $POLOMER done else obsah "$1" fi
Tento skript využívá funkci, která provádí vlastní výpočet, dále podmínku, která hlídá, zda-li uživatel zadal skriptu při spuštění parametr nebo ne. Pokud ano, provede výpočet na hodnotě získané z parametru, pokud uživatel parametr nezadal, nabídne mu "interaktivní" režim, kdy může hodnotu zadat sám. Interaktivní režim pak pracuje stejně jako příklad výše.
Pokud budeme pracovat s běžnými příkazy, může se stát, že některý z nich nebude schopen příslušnou operaci provést a selže:
#!/bin/bash cp soubor adresar rm soubor
Tento skript sice nic smysluplného nedělá, ale můžeme si na něm ukázat, o co
jde. Pokud totiž první příkaz (cp
) selže, kopie
původního souboru se nevytvoří, a pokud bude skript pokračovat dál (což
bude), smaže zdrojový soubor. V tomto případě by se tedy hodilo zjistit,
jestli první příkaz proběhl úspěšně, a pokud ne, zastavit průběh skriptu.
Každý správně napsaný unixový program po sobě zanechá tzv. konečný stav
(exit status). To je číslo, které nám pomůže zjistit, jestli program úspěšně
skončil nebo jestli skončil s chybou. Nula představuje bezchybný konec,
nenulové číslo poukazuje na chybu při práci programu. Hodnota konečného
stavu naposledy spuštěného programu se uchovává v proměnné $?
.
Toho můžeme využít a skript adekvátně upravit:
#!/bin/bash cp soubor adresar if [ $? -gt 0 ]; then echo "Soubor se nepodarilo prekopirovat." exit 1 fi rm soubor
Na tomto příkladě jsme si ukázali dvě věci. Jednak zpracování chybového
stavu a jednak příkaz exit
,
který okamžitě ukončí běh skriptu. Jako parametr lze tomuto příkazu předat
hodnotu, kterou má vrátit jako konečný stav. V tomto případě jsme zvolili
nenulovou hodnotu, protože náš program v této situaci skončil s chybou.
Chybové stavy můžeme ošetřovat v rámci jednoho příkazu takto:
cp soubor adresar && echo "Soubor se zkopiroval."
Dva ampersandy za sebou fungují jako speciální oddělovač příkazů. Příkaz za nimi se provede pouze v případě, že příkaz před nimi bude korektně ukončen. Opačnou možnost nabízí dvě roury:
cp soubor adresar || echo "Nastala chyba."
V tomto případě se příkaz po dvou rourách provede pouze v situaci, kdy příkaz před nimi vrátí nenulový konečný stav.
Skripty mohou reagovat určitým způsobem na signály, které jim pošleme
třeba příkazem kill
.
Tato znalost se nám hodí třeba proto, abychom mohli po sobě uklidit,
když někdo požádá o násilné ukončení našeho skriptu.
K tomu, abychom mohli reagovat na určité signály, slouží příkaz trap
.
Syntaxe je následující:
trap "příkaz" signály
Příkladem tedy může být výmaz dočasného souboru při jakémkoliv násilném ukončení skriptu:
trap "rm $TEMP; exit" SIGHUP SIGINT SIGTERM
Pokud náš skript opatříme touto řádkou, v případě, že dojde k násilnému ukončení běhu skriptu, nezůstane po něm žádné "smetí".
Pokud provádímě nějaký příkaz na pozadí a odhlásíme se, pošle se všem
programům či skriptům, které jsme nechali pracovat na pozadí, signál SIGHUP
.
Ten zpravidla programy zpracují tak, že se ukončí. Pokud tedy chceme
spustit nějaký příkaz (program) na pozadí tak, aby se při našem
odhlášení neukončil, použijeme k tomu program nohup
:
nohup prikaz &