Dělení slov a IFS
Dělení slov je fáze, ve které shell rozdělí výsledky předchozích expanzí na jednotlivá slova, tedy na jednotlivé argumenty spouštěného příkazu. Je to zdaleka nejčastější zdroj nesrozumitelného chování skriptů, a proto jí věnujeme samostatnou podkapitolu.
Platí dvě zásadní pravidla:
- Dělení slov se aplikuje pouze na výsledky expanzí — na expanzi parametrů, aritmetickou expanzi a substituci příkazů. Text, který jsme ve skriptu napsali přímo, se nedělí (ten je na slova rozdělen dříve, při parsování řádku).
- Dělení slov se neprovádí uvnitř uvozovek ani apostrofů.
Proměnná IFS
Které znaky jsou oddělovači, určuje proměnná IFS (internal field separator).
Výchozí hodnotou je mezera, tabulátor a znak nového řádku.
Chování se liší podle toho, jestli je oddělovačem bílý znak, nebo ne:
- bílé znaky (mezera, tabulátor, nový řádek) v
IFS: posloupnost několika oddělovačů za sebou se chová jako jeden a oddělovače na začátku a na konci se ignorují; - ostatní znaky: každý výskyt odděluje, takže dva za sebou vytvoří prázdné slovo.
Rozdíl je vidět na následujícím příkladu.
> FOO="a b"
> printf '[%s]\n' ${FOO}
[a]
[b]
> IFS=:
> BAR="a::b"
> printf '[%s]\n' ${BAR}
[a]
[]
[b]
Právě proto se PATH prochází dvojtečkou jako oddělovačem korektně i tehdy,
když některá položka chybí.
Nastavíme-li IFS na prázdný řetězec, dělení slov se neprovede vůbec
a z expanze vznikne jediné slovo.
> IFS=
> FOO="a b c"
> printf '[%s]\n' ${FOO}
[a b c]
Znaková expanze
Do IFS často potřebujeme dostat znak, který se špatně zapisuje, typicky nový
řádek nebo tabulátor. Slouží k tomu zápis $'…' (tzv. ANSI-C quoting):
IFS=$'\n' # deleni pouze na radky
IFS=$'\t' # deleni pouze tabulatorem
IFS=$' \t\n' # vychozi hodnota
Uvnitř $'…' se interpretují sekvence známé z jazyka C: \n, \t, \\, \',
\xHH a další. Podrobněji viz Uvozování.
Měníme-li IFS, je slušné si původní hodnotu uložit a po skončení práce ji
obnovit, jak jsme to dělali v kapitole Cykly.
Alternativou je nastavit IFS jen pro jeden příkaz zápisem IFS=: prikaz,
který proměnnou změní pouze v prostředí tohoto příkazu.
> IFS=: read -r a b c <<< "prvni:druhy:treti"
> echo ${b}
druhy
Proč uvozovky kolem expanzí
Nyní můžeme přesně vysvětlit doporučení, které v textu opakovaně zaznívá. Uvažujme soubor, jehož jméno obsahuje mezeru.
> SOUBOR="muj soubor.txt"
> ls ${SOUBOR}
ls: cannot access 'muj': No such file or directory
ls: cannot access 'soubor.txt': No such file or directory
> ls "${SOUBOR}"
muj soubor.txt
Bez uvozovek se hodnota po expanzi rozdělila na dvě slova, a ls tak dostal dva
argumenty místo jednoho. S uvozovkami se dělení slov neprovede.
Druhý, méně nápadný případ nastane, když je proměnná prázdná. Bez uvozovek zmizí argument úplně; s uvozovkami zůstane prázdný argument.
> PRAZDNA=""
> printf '[%s]\n' ${PRAZDNA}
> printf '[%s]\n' "${PRAZDNA}"
[]
To je důvod, proč se v kapitole Větvení píše
[ -n "${x}" ] a ne [ -n ${x} ]: v druhém případě by test dostal jediný
argument -n, což vyhodnotí jako test neprázdnosti řetězce -n, tedy vždy
pravdivě.
$* a $@
Rozdíl mezi $* a $@, popsaný v kapitole Vstupní
argumenty, je nyní jen důsledkem
pravidel výše. Předpokládejme skript spuštěný jako
./script.sh "prvni argument" druhy.
| zápis | výsledek |
|---|---|
$* | jedno slovo, rozdělené dělením slov → prvni, argument, druhy |
$@ | jednotlivé argumenty, ale rozdělené dělením slov → prvni, argument, druhy |
"$*" | jediné slovo prvni argument druhy (spojené prvním znakem IFS) |
"$@" | tři slova: prvni argument, druhy — beze změny |
echo "--- \$* bez uvozovek"
for p in $*
do
echo "[${p}]"
done
echo "--- \"\$@\" v uvozovkach"
for p in "$@"
do
echo "[${p}]"
done
> ./script.sh "prvni argument" druhy
--- $* bez uvozovek
[prvni]
[argument]
[druhy]
--- "$@" v uvozovkach
[prvni argument]
[druhy]
Prakticky to znamená: argumenty předáváme dál vždy jako "$@". Totéž platí
pro pole, kde "${pole[@]}" zachová prvky tak, jak byly uloženy (viz
Pole).
Kdy uvozovky vynechat
Uvozovky vynecháváme jen tehdy, když dělení slov opravdu chceme — typicky když je v proměnné seznam položek, který má vzniknout více argumentů:
VOLBY="-l -a"
ls ${VOLBY} # zamerne bez uvozovek: dve volby
I tady je ovšem čistším řešením pole:
VOLBY=(-l -a)
ls "${VOLBY[@]}"