Index 🏠 || 🔝 Nahoru


Publikováno:

Aktualizováno

Série »Linux shell«

Kategorie:

PSKáčko

Tagy:

Unixový shell se dá použít nejen pro interaktivní práci, ale i jako programovací jazyk. Jednotlivé příkazy můžeme zapsat do souboru a tento soubor nechat interpretovat příkazovým interpretem. Tento soubor je potom označován jako skript nebo shellový skript.

Shell je vynikající lepítko a jako programovací jazyk vyniká hlavně tehdy, když s jeho pomocí slepíte a sorchestrujete dohromady několik jiných programů, aby dělaly to, co zrovna potřebujete. Můžete si tak zautomatizovat a zjednodušit každodenní práci. Inspirovat se můžete třeba na mém Gitlabu.

Seskupení příkazů

Shell nabízí dva způsoby jak seskupit více příkazů do jednoho celku. Liší se tím, zda se příkazy vykonají v podřízeném nebo v aktuálním shellu.

( ) — podřízený shell

Příkazy uzavřené v kulatých závorkách se vykonají v podřízeném shellu (subshell). Veškeré změny proměnných i aktuálního adresáře zaniknou s ukončením subshell-u. Je to jako spustit zcela nové prostředí. Potomek může zdědit něco od svého rodiče, ale přenos zpět od potomka k rodiči není možný. Proměnné lze exportovat do podřízeného shellu, ale zpět k rodiči to nelze.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ X=rodicX
$ export Y=rodicY
$ pwd
/home/student

$ ( 
    echo X: $X
    echo Y: $Y
    X=potomekX
    Y=potomekY
    cd /tmp
    echo ZNOVU: $X $Y
    pwd
)
X:
Y: rodicY
ZNOVU: potomekX potomekY
/tmp

$ pwd
/home/student

$ echo $X $Y
rodicX rodicY

{ } — aktuální shell

Příkazy uzavřené ve složených závorkách se vykonají v aktuálním shellu. Změny proměnných i aktuálního adresáře přetrvají:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ X=rodicX
$ pwd
/home/student

$ { 
    echo X: $X
    X=potomekX
    cd /tmp
    echo ZNOVU: $X
    pwd
}
X: rodicX
ZNOVU: potomekX
/tmp

$ pwd
/tmp

$ echo $X
potomekX

Syntaxe složených závorek

Na rozdíl od kulatých závorek musí být za { mezera a poslední příkaz před } musí být ukončen středníkem (nebo novým řádkem). Tohle nejde: {příkaz}. Musí to vypadat takto: { příkaz; }

Seskupení jako celek

Skupina příkazů v ( ) nebo { } se chová jako jeden celek — má jednu návratovou hodnotu (návratovou hodnotu posledního příkazu). Díky tomu je možné skupinu kombinovat s operátory && a || a nahradit tak jednoduché podmínky if:

1
$ mkdir /tmp/projekt && { cd /tmp/projekt; git init; echo "hotovo"; }

Pokud mkdir uspěje, vykonají se všechny příkazy ve složených závorkách. Pokud selže, celá skupina se přeskočí.

1
$ grep -q "chyba" log.txt && { echo "Nalezena chyba:"; grep "chyba" log.txt; } || echo "Vše OK"

Obě formy seskupení se dají kombinovat s přesměrováním. Výstup celé skupiny příkazů je tak možné přesměrovat najednou:

1
$ { date; whoami; hostname; } > info.txt

Podobné je to se spuštěním na pozadí:

1
$ { sleep 3m; notify-send --icon info "Čaj je připraven"; }&

Pole a seznamy

Zsh má velmi silnou podporu polí (arrays). Indexuje se od 1 (na rozdíl od Bashe).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Deklarace
ovoce=(jablko hruska banan)

# Přístup k prvkům
echo $ovoce[1]       # jablko
echo $ovoce[-1]      # banan (poslední)
echo $ovoce[1,2]     # jablko hruska (rozsah)

# Počet prvků
echo $#ovoce         # 3

# Přidání prvku
ovoce+=(mango)

# Procházení
for o in $ovoce; do
    echo $o
done

Pole se hodí všude tam, kde je potřeba pracovat se seznamem hodnot — soubory, hostitelé, argumenty skriptu…

1
2
3
4
5
6
7
8
# $@ je pole všech argumentů skriptu
for arg in $@; do
    echo "Argument: $arg"
done

# Pole souborů přes globbing
soubory=(*.txt)
echo "Nalezeno $#soubory souborů .txt"

Typický případ — procházení pole serverů:

1
2
3
4
5
servery=(web1 web2 db1)
for server in $servery; do
    echo "Kontroluji: $server"
    ping -c1 $server &>/dev/null && echo "  OK" || echo "  nedostupný"
done
Syntaxe Význam
$ovoce[2] prvek na indexu 2
$ovoce[-1] poslední prvek
$ovoce[2,4] rozsah (slice) — prvky 2 až 4
$#ovoce počet prvků
"${ovoce[@]}" všechny prvky jako oddělené řetězce (bezpečné pro mezery)
"${ovoce[*]}" všechny prvky jako jeden řetězec (spojené $IFS)
${ovoce:#vzor} odfiltruje prvky shodující se se vzorem
${(j:,:)ovoce} join — spojí prvky oddělovačem (zde ,)
${(s:,:)retezec} split — rozdělí řetězec na pole

$IFS — oddělovač polí

$IFS (Internal Field Separator) je proměnná, která určuje, jakými znaky se rozdělují slova při expanzi. Výchozí hodnota je mezera, tabulátor a nový řádek.

Uplatní se zejména u "${pole[*]}" — prvky se spojí prvním znakem $IFS:

1
2
3
4
5
6
7
ovoce=(jablko hruska banan)

echo "${ovoce[@]}"   # jablko hruska banan  (každý prvek zvlášť)
echo "${ovoce[*]}"   # jablko hruska banan  (jeden řetězec, spojeno mezerou)

IFS=,
echo "${ovoce[*]}"   # jablko,hruska,banan

Změna $IFS se běžně dělá lokálně, aby neovlivnila zbytek skriptu:

1
2
3
4
5
6
csv() {
    local IFS=,
    echo "${ovoce[*]}"
}
csv                  # jablko,hruska,banan
echo "${ovoce[*]}"   # jablko hruska banan  (IFS nezměněno)

$IFS funguje i opačně — při rozdělení řetězce na pole. Stačí přiřadit řetězec do pole přes =( ):

1
2
3
4
5
6
7
8
$ IFS=,
$ read -rA ovoce <<< "jablko,hruska,banan"
$ echo $ovoce[1]
jablko
$ echo $ovoce[3]
banan
echo $#ovoce
3

Hlavička

Aby bylo možné skript jednoduše spouštět je nutné opatřit ho hlavičkou: První řádek začíná dvojicí znaků #! a pokračuje cestou k příkazovému interpretu:

1
#!/bin/zsh

Dvojice znaků #! uvozuje tzv. shebang. Tento mechanizmus je zcela obecný a uplatní se stejně i pro skripty v jiný jazycích. Například pro programovací jazyk Python by hlavička mohla vypadat takto:

1
#!/usr/bin/python3

nebo takto:

1
#!/usr/bin/env python3

(ponechám zvídavého čtenáře, aby si zjistil, co dělá příkaz/program env.)

Pokud by skript shebang na prvním řádku neměl, bylo by nutné explicitně na příkazovém řádku uvést interpret. Například:

1
$ zsh ~/bin/mujSkript.sh

nebo

1
$ python ~/.local/bin/miniterm.py

Aby se skript choval jako každý jiný program je jeho správná hlavička podmínkou nutnou nikoli postačující. Skript musí být spustitelný (chmod a+x ~/bin/mujSkript.sh) a musí být umístěn tak, aby ho příkazový interpret našel. Pokud je vše toto splněno může ho uživatel spouštět, jako by to byla běžná součást systému.

1
$ mujSkript.sh

Pro Zsh nebo Bash se hned za shebang doporučuje přidat:

1
set -euo pipefail
  • set -e (errexit) — skript se okamžitě ukončí, pokud jakýkoli příkaz selže. Bez toho Zsh tiše pokračuje dál i po chybě.
  • set -u (nounset) — selže při použití nenastavené proměnné. Bez toho se nenastavená proměnná expanduje na prázdný řetězec — klasický zdroj pohrom: rm -rf $TMPDIR/ se s prázdným $TMPDIR expanduje na rm -rf /.
  • set -o pipefail — pipeline selže, pokud selže jakýkoli příkaz v ní, nejen poslední.

Pro ladění skriptů se hodí přepínač -x — zsh před každým příkazem (na řádcích označených +) vypíše, co spustí (po expanzi proměnných).

-x můžete použít explicitně:

1
$ zsh -x mujSkript.sh

… nebo do shebang hlavičky

1
#!/bin/zsh -x

… nebo jen pro konkrétní část skriptu:

1
2
3
set -x    # zapnout výpis příkazů
...
set +x    # vypnout

Příkaz . (tečka) a source

Ano . je opravdu příkaz. Výše popsaný způsob spouštění má za následek, že námi vytvořený skript je spuštěn v podřízeném shell-u. To mimo jiné znamená, že změny provedené v proměnných prostředí nebudou po dokončení skriptu viditelné. Použijeme-li příkaz .:

1
2
3
$ . commands     # soubor leží v $PATH

$ . ./commands   # soubor leží v pracovním adresáři

…bude shell postupně číst soubor commands a jednotlivé příkazy vykonávat v aktuálním shellu. Výsledek bude stejný jako kdyby uživatel zadával příkazy rovnou v interaktivním prostředí.

Příkaz source je téměř to stejné jako .. Rozdíl je následující:

  • . hledá v $PATH a pokud soubor commands není v $PATH je třeba zadat explicitně celou cestu.

  • source hledá nejprve v pracovním adresáři a až potom v $PATH; není nuté zadávat explicitně celou cestu:

1
2
3
$ source commands   # soubor leží v pracovním adresáři

$ source commands   # soubor leží v $PATH

Poznámky

Poznámky se zapisují za znak #. Vše od znaku # do konce řádku je interpretem ignorováno. Názorný příklad je vidět o pár řádků výše…

Přebírání parametrů (argumentů)

Skriptům lze stejně jako programům předávat parametry. Ty jsou uvnitř skriptu dostupné pomocí speciálních proměnných.

Proměnná Význam
$0 jméno skriptu
$1 první parametr
$2 druhý parametr
$n n-tý parametr, n nabývá hodnoty 1 až 9
${n} libovolný n-tý parametr
$# počet parametrů
$* seznam všech parametrů — stejné jako "$1 $2 $3 ..."
$@ seznam všech parametrů — stejné jako "$1" "$2" "$3" ...

Příkaz shift N vymaže prvních N parametrů a posune význam proměnných $n a ${n}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/zsh
# Soubor:    mujSkript.sh
#  Popis:    Ukázka předávání parametrů
############################################################

echo Jmenuji se: $0
echo Parametry: $@
echo první: $1
echo třetí: ${3}

shift 3
echo ">>> první 3 parametry byly vymazány"
echo Parametry: $@
1
2
3
4
5
6
7
$ ./mujSkript.sh toto je ukazka predavani parametru
Jmenuji se: ./mujSkript.sh
Parametry: toto je ukazka predavani parametru
první: toto
třetí: ukazka
>>> první 3 parametry byly vymazány
Parametry: predavani parametru

Přepínače

Skripty mohou přijímat přepínače jako běžné programy (-v, --output soubor apod.). Z RAW pohledu není rozdíl mezi přepínačem a argumentem. Vše se dostane do skriptu jako poziční parametr a je potřeba explicitně říct: “hele, tak to, co začíná znakem - je přepínač”. Naštěstí je to všechno už vyřešené a není třeba to dělat ručně. Zsh má pro zpracování přepínačů vestavěnou funkci zparseopts. Reklamu této funkci dělám proto, že práce s ní mi přijde mnohem zábavnější, než práce s klasickým programem getopt nebo vestavě klasickou funkcí getopts:

1
2
3
4
zparseopts -D -E -F -- \
    h=help -help=help \
    v=verbose -verbose=verbose \
    o:=output -output:=output
  • -D — odstraní zpracované přepínače z $@, zbytek (poziční argumenty) zůstane
  • -E — přepínače a poziční argumenty mohou být v libovolném pořadí
  • -F — neznámý přepínač způsobí chybu (dostupné od Zsh 5.8)
  • přepínač bez : je příznak (ano/ne), s : očekává hodnotu

Výsledky jsou uloženy v polích — hodnotu dostaneme přes index [2]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/zsh
set -euo pipefail

zparseopts -D -E -F -- \
    h=help -help=help \
    v=verbose -verbose=verbose \
    o:=output -output:=output

if (( $#help )); then
    echo "Použití: $0 [-h] [-v] [-o SOUBOR] soubory..."
    exit 0
fi

[[ -n $verbose ]] && echo "Verbose mód zapnut"

soubor=${output[2]:-vystup.txt}    # výchozí hodnota pokud -o nebylo zadáno
echo "Výstup do: $soubor"
echo "Zbývající argumenty: $@"
1
2
3
4
$ ./skript.sh -v --output data.txt soubor1 soubor2
Verbose mód zapnut
Výstup do: data.txt
Zbývající argumenty: soubor1 soubor2

Jak bylo uvedeno každý program, který ukončí svou činnost korektně vrací jako svou návratovou hodnotu 0. Pokud se v průběhu programu objeví chyba, dává o tom program vědět svou nenulovou návratovou hodnotou.

Okamžité ukončení skriptu s konkrétní návratovou hodnotou vyvolá příkaz exit N.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/zsh
# Soubor:    navrat.sh
#  Popis:    Ukázka návratové hodnoty
############################################################

if [[ $1 == 'ano' ]]; then
    echo OK
    exit 0
else
    echo napiš ano
    exit 1
fi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ ./navrat
napiš ano
$ echo $?
1

$ ./navrat NEE
napiš ano
$ echo $?
1

$ ./navrat ano
OK
$ echo $?
0

Funkce

Shell umožňuje vytvářet funkce. Funkce se chová stejně jako samostatný skript, včetně předávání parametrů a návratové hodnoty.

Ukončení funkce s konkrétní návratovou hodnotou je zajištěna příkazem return N.

Bacha

Funkci je nuté ukončít pomocí příkazu return; nikoli příkazu exit — ten totiž ukončí celý skript.

Sada proměnných pro předávání parametrů funguje stejně jako u skriptu.

Syntaxe funkce v shellu:

1
2
3
4
5
název() {
    příkaz
    příkaz
    ....
}

Příklad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/zsh
# Soubor:    scitani.sh
#  Popis:    Ukázka funkce
############################################################


# zápis funkce
plus() {
    while [[ $2 ]]; do
        echo ${1}+${2}=$(( $1 + $2 ))
        shift 2
    done
}

# volání funkce
plus $@

(Aritmetické výrazy $(( )) jsou popsány v článku o expanzích.)

1
2
3
4
$ ./scitani.sh 1 2 3 4 5 6 7
1+2=3
3+4=7
5+6=11

Lokální proměnné

Proměnné uvnitř funkce jsou ve výchozím stavu globální — mohou přepsat proměnné volajícího skriptu. Pomocí příkazu local se proměnná stane lokální a po návratu z funkce zanikne:

1
2
3
4
5
6
7
8
pozdrav() {
    local jmeno=$1      # lokální -- mimo funkci neexistuje
    echo "Ahoj, $jmeno!"
}

jmeno=Marek
pozdrav Franta          # Ahoj, Franta!
echo $jmeno             # Marek  (globální proměnná nebyla přepsána)

Podmínky a cykly

Užití podmínek a cyklů není vázáno jen na skripty. Stejně tak je možné je použít v interaktivní práci.

Pravdivostní “VÝRAZy”

Na následujících řádcích u podmínek if a cyklů while je často použito zobecňující slovo VYRAZ:

  • VYRAZ je příkaz nebo posloupnost příkazů oddělených pomocí metaznaku || (logický součet — nebo) nebo metaznaku && (logický součin — and).
  • O pravdivosti nebo nepravdivosti VÝRAZu rozhoduje jeho návratová hodnota.
  • VYRAZ je možné negovat pomocí znaku ! na jeho začátku.

Aby mohl if nebo while rozhodnout o pokračování své činnosti potřebuje nějakou pravdivostní hodnotu. Je velmi klíčové pochopit, že tato pravdivostní hodnota je dána návratovou hodnotou příkazu.

Jako VYRAZ se velice často používá konstrukce [[ ]]rozšířená podmínka zabudovaná přímo do shellu. Starší varianta [ ] (synonymum externího programu test) stále funguje, ale [[ ]] je bezpečnější a mocnější. Protože:

  • proměnné nemusí být v uvozovkách — [[ $x = abc ]] funguje i pokud $x je prázdné
  • podporuje =~ pro porovnání s regulárním výrazem
  • logické operátory && a || fungují přímo uvnitř závorek
Operace [[ ]] starý [ ]
Řetězec je prázdný [[ -z $x ]] [ -z "$x" ]
Řetězce se rovnají [[ $x == abc ]] [ "$x" = abc ]
Číslo větší než [[ $n -gt 5 ]] [ "$n" -gt 5 ]
Soubor existuje [[ -f $f ]] [ -f "$f" ]
Regex shoda [[ $x =~ ^[0-9]+$ ]]

Onen klíčový bod je v tom, že pokud zapíšete

1
if [[ $1 == --help ]]; then

… tak [[ je příkaz a $1 == --help ]] jsou jeho čtyři parametry. Příkaz [[ otestuje jestli řetězec v $1 odpovídá --help a svou návratovou hodnotou řekne, jestli je to pravda. ]] je vlastně “jen” ukončovač.

1
2
3
4
5
6
7
$ type -a '[[' 
[[ is a reserved word

$ type -a [  
[ is a shell builtin
[ is '/usr/bin/['
[ is '/bin/['

Jako VYRAZ je ale možné použít libovolný jiný příkaz. Například pokud potřebuji otestovat jestli soubor obsahuje konkrétní řetězec může to vypadat takto:

1
2
3
4
if grep silvergold kniha.txt >/dev/null; then
    command1
    command2
fi

Příkaz grep nic nevypíše, protože jeho výstup je přesměrován do černé díry, ale jeho návratová hodnota rozhodne o tom, zda se vykoná command1 a command2.

(( )) — aritmetická podmínka

Pro číselné porovnání lze místo [[ ]] použít (( )). Chová se jako příkaz — je pravdivý (návratová hodnota 0), pokud je výsledek výrazu nenulový:

1
2
3
4
5
6
7
8
9
n=7
if (( n > 5 )); then 
    echo "větší";
fi

pocet=3
while (( pocet-- > 0 )); do  # C-like syntax
    echo $pocet
done

Uvnitř (( )) lze také přiřazovat bez $:

1
2
(( x = 5 + 3 ))
echo $x    # 8

Rozdíl oproti $(( ))

$(( )) je expanze — vrací hodnotu a nahrazuje se textem. (( )) je příkaz — vrací exit code a používá se tam, kde se čeká příkaz (podmínka, smyčka).

if — podmíněné vykonání

V následujících ukázkách jsem zvolil způsob zápisu, který mi připadá přehledný, ale platí, že středník ; může být zaměněn za konec řádku a naopak.

Syntaxe obecně vypadá takto:

1
2
3
4
if VYRAZ; then
    PRIKAZ
    ....
fi
1
2
3
4
5
6
7
if VYRAZ; then
    PRIKAZ
    ....
else
    PRIKAZ
    ....
fi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if VYRAZ; then
    PRIKAZ
    ....
elif VYRAZ; then
    PRIKAZ
    ....
elif VYRAZ; then
    PRIKAZ
    ....
...
else
    PRIKAZ
    ....
fi

case — shoda se vzorem

Vícenásobné větvení pomocí vzorů ukážeme na příkladu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/zsh
# Soubor:    case.sh
#  Popis:    Ukázka CASE
############################################################

case $1 in
    ahoj|nazdar|cau) echo pozdrav;;
    [tT]*) echo slovo zacina na T;;
    *e*) echo slovo obsahuje e;;
    *) echo cokoliv jiného;;
esac

Větev se ukončuje jedním z těchto ukončovačů:

Ukončovač Chování
;; konec větve, pokračuj za esac
;& fall-through — vykonej i tělo následující větve (bez testování vzoru)
;| pokračuj testovat další vzory (může se shodovat více větví)
  • Znak | slouží jako oddělovač vzorů ve významu nebo.
  • Pro vzory platí stejná pravidla jako pro expanzi jmen souborů — tedy Globbing.

Příklad ;| — slovo může spadnout do více větví najednou:

1
2
3
4
5
6
7
8
case "ahoj" in
    a*)    echo "začíná na a";|
    *j)    echo "končí na j";|
    ahoj)  echo "přesná shoda";;
esac
# začíná na a
# končí na j
# přesná shoda

for — pro každou položku seznamu

Syntaxe cyklu for vypadá takto:

1
2
3
4
5
for PROMENNA in SEZNAM; do
    PRIKAZ
    PRIKAZ
    .....
done

Nejprve se expanduje SEZNAM. PROMENNA nabývá při každé iteraci postupně jedné z hodnot SEZNAMu.

1
2
3
for cislo in 1 2 3 4; do
    echo cislo je $cislo
done

Jako seznam může ale být uvedeno neúplné jméno souboru

1
2
3
4
for soubor in *; do
    echo menim casove razitko: $soubor
    touch $soubor
done

… nebo program, který seznam vrátí.

1
2
3
for cislo in $(seq 5 10); do
    echo cislo je $cislo
done

Příklad s polem byl už uveden výše.

Pro čistě numerické iterace lze použít C-style zápis:

1
2
3
for (( i=1; i<=5; i++ )); do
    echo $i
done

while, until — opakování na základě podmínky

Syntaxe cyklu while a until vypadá naprosto stejně. Jediný rozdíl je v podmínce. Iterace cyklu while se vykoná pokud podmínka platí. Iterace cyklu until se vykoná pokud podmínka neplatí.

K okamžitému ukončení těla cyklu slouží příkaz break k přeskočení zbytku těla cyklu příkaz continue.

1
2
3
4
5
while VYRAZ; do
    PRIKAZ
    PRIKAZ
    ....
done

VYRAZ má stejný význam jako u podmínky if.

Note

Všimněte si, že zde nejsou žádné [[ ]] ani [ ]. ping svou návratovou hodnotou zdělí, jestli se spojení povedlo.

1
2
3
4
5
while ping -c 3 172.16.6.53 &>/dev/null; do
    echo PC je zapnute.
    sleep 1m
done
echo K PC se není možné připojit.

Standardní vstup z těla skriptu

Častokrát požadujeme, aby program spuštěný z těla skriptu byl “nakrmen” vstupními daty. Pro přesměrování standardního vstupu ze souboru sice slouží metaznak < ale vytváření samostatného souboru pro vstupní data není vždy efektivní. Proto použijeme metaznak <<.

1
2
3
4
5
6
prikaz <<OMEZOVAC
DATA
DATA
DATA
....
OMEZOVAC
  • OMEZOVAC je libovolná posloupnost znaků.
  • Ukončovací OMEZOVAC musí být uveden na samostatném řádku.
  • Ve vstupních datech je možné zapsat i proměnné.
1
2
3
4
cat <<EOF | wc -w
u textu, který jsem sem napsal se provede
pocitani slov. Pocet slov je:
EOF

<<EOF vs <<'EOF'

Pokud je omezovač bez uvozovek, proměnné a příkazy se expandují. Pokud je omezovač v jednoduchých uvozovkách (nebo dvojitých), vše je bráno doslova — žádná expanze se neprovede:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
jmeno=Marek

cat <<EOF
Ahoj, $jmeno! Dnes je $(date +%A).
EOF
# Ahoj, Marek! Dnes je čtvrtek.

cat <<'EOF'
Ahoj, $jmeno! Dnes je $(date +%A).
EOF
# Ahoj, $jmeno! Dnes je $(date +%A).

<<'EOF' se hodí například pro generování skriptů nebo konfiguračních souborů, kde $ má být součástí výstupu.

<<-EOF — odsazení tabulátory

Pomlčka před omezovačem způsobí, že shell odstraní úvodní tabulátory (nikoli mezery) z každého řádku. Heredoc tak může být odsazen spolu s okolním kódem a přitom nevyžaduje, aby omezovač byl na začátku řádku:

1
2
3
4
5
6
if true; then
    cat <<-EOF
        Tohle je odsazeno tabulátory.
        Shell úvodní tabulátory odstraní.
    EOF
fi

eval — vykonání řetězce jako příkazu

Příkaz eval vezme svůj argument jako řetězec, provede na něm expanzi proměnných a výsledek vykoná jako shellový příkaz:

1
2
3
$ prikaz="echo Ahoj světe"
$ eval $prikaz
ahoj světe

Typické použití je dynamická dereference proměnné — přístup k proměnné, jejíž jméno je uloženo v jiné proměnné:

1
2
3
4
$ barva=cervena
$ klic=barva
$ eval "echo \$$klic"
cervena

Bezpečnostní riziko

eval vykoná cokoliv — nikdy ho nepoužívejte s nevalidovaným vstupem od uživatele nebo z externího zdroje.

trap — reakce na signály a ukončení

Příkaz trap registruje příkaz, který se spustí při přijetí signálu nebo při ukončení skriptu:

1
trap 'PRIKAZ' SIGNAL [SIGNAL ...]

Nejpoužívanější signály:

Signál Kdy
EXIT při jakémkoliv ukončení skriptu
INT Ctrl+C
TERM kill (výchozí signál)
ERR při selhání příkazu (s set -e)

Nejčastější případ — cleanup dočasného souboru při ukončení skriptu, ať už normálním nebo po chybě:

1
2
3
4
5
6
7
8
9
#!/bin/zsh
set -euo pipefail

tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

# ... práce s tmpfile ...
echo "výsledky" > "$tmpfile"
cp "$tmpfile" /tmp/vystup.txt

trap s EXIT se spustí vždy — i při chybě nebo přerušení pomocí Ctrl+C — takže dočasné soubory nezůstanou na disku.

read — čtení vstupu

Příkaz read čte jeden řádek ze standardního vstupu a uloží ho do proměnné. Používá se pro interaktivní skripty i pro zpracování souborů:

1
read [-r] [-p PROMPT] [-s] [-t TIMEOUT] [-A POLE] PROMENNA
Přepínač Popis
-r neinterpretovat \ jako pokračování řádku (doporučuje se vždy)
-p zobrazit výzvu (prompt) před čtením
-s skrytý vstup — nezobrazovat zadané znaky (hesla)
-t timeout v sekundách; pokud uživatel nezadá nic, read selže
-A uložit slova do pole (rozdělení podle $IFS)
1
2
read -r -p "Zadej jméno: " jmeno
echo "Ahoj, $jmeno!"
1
2
3
read -r -s -p "Heslo: " heslo
echo           # nový řádek (po skrytém vstupu chybí)
echo "Délka hesla: ${#heslo} znaků"

Velmi časté je čtení řádků ze souboru v cyklu while:

1
2
3
while read -r radek; do
    echo ">> $radek"
done < soubor.txt

Nebo zpracování výstupu příkazu:

1
2
3
df -h | while read -r radek; do
    echo "partition: $radek"
done

Související posty