Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
Toto jsou materiály pro cvičení 05 i 06. Kvízy na čtení před cvičením budou samostatné pro každý týden ale hodnocené úlohy (skriptování) budou spojeny, ale s přiděleným dvojnásobným počtem bodů.
V tomto cvičení téměř dokončíme naši cestu shellovým skriptováním: naučíte se, jak v shellu fungují podmínky a cykly, jak fungují proměnné – a mnohem víc.
Je důležité říct, že všechna následující témata fungují jak ve skriptech (tedy neinteraktivně), tak i interaktivně v terminálu. Obzvlášť cykly skrz seznamy souborů často používáme přímo na příkazové řádce, bez ukládání do plnohodnotného skriptu.
Nezapomeňte, že Čtení před cvičením je povinné a je z něj kvíz, který musíte vyplnit před cvičením.
Tohle je čtení před pátým cvičením.
Více o proměnných
Viděli jsme, že pro základní případy stačí následující pro vytvoření a použití proměnných ve skriptech.
output_file="out.txt"
echo "Writing to $output_file." >&2
head -n 1 /etc/passwd >"$output_file"
Neinicializované hodnoty a podobné nástrahy
Pokusíte-li se použít proměnnou, která není inicializovaná, shell k ní bude přistupovat, jakoby obsahovala prázdný řetězec. Přestože to někdy může být užitečné, je to také zdroj nepříjemných překvapení.
Jak jsme zmínili dříve, měli byste vždy začínat své shellové skripty set -u
, abyste byli v takových situacích varováni.
Někdy ale přesto potřebujeme číst z potenciálně neinicializované proměnné,
abychom ověřili, jestli je inicializovaná. Například můžeme chtít číst
$EDITOR
, abychom zjistili preferovaný editor uživatele, ale nabídnout
rozumnou výchozí hodnotu, pokud proměnná není nastavena. Toho je jednoduché
dosáhnout použitím notace ${VAR:-default_value}
. Je-li proměnná VAR
nastavena, použije se její hodnota, jinak se použije default_value
aniž by
to způsobilo varování vyvolané set -u
.
Můžeme tedy psát:
"${EDITOR:-mcedit}" file-to-edit.txt
Často je lepší ošetřit výchozí hodnoty na začátku skriptu tímto idiomem:
EDITOR="${EDITOR:-mcedit}"
Dále ve skriptu už můžeme editor spouštět jen takto:
"$EDITOR" file-to-edit.txt
Poznamenejme, že je také možné psát ${EDITOR}
pro explicitní oddělení
jména proměnné. To se hodí, pokud chceme vypsat proměnnou následovanou
nějakým písmenem.
file_prefix=nswi177-
echo "Will store into ${file_prefix}log.txt"
echo "Will store into $file_prefixlog.txt"
Expanze proměnných (a jiných konstrukcí)
Viděli jsme, že shell provádí různé typy expanzí. Expanduje proměnné, wildcardy, tildu, aritmetické výrazy, a spoustu dalších.
Je důležité pochopit, jak tyto expanze interagují navzájem. Namísto popisování formálního procesu (který je celkem složitý), ukážeme několik příkladů na demonstraci typických situací.
Budeme volat args.py
z předchozích cvičení pro ukázání, co se
děje. (Samozřejmě je potřeba jej volat ze správného adresáře.)
Zaprvé, zpracování parametrů (jejich dělení) se děje až po expanzi proměnných:
VAR="value with spaces"
args.py "$VAR"
args.py $VAR
Připravte si soubory pojmenované one.sh
a with space.sh
pro následující
příklad:
VAR="*.sh"
args.py "$VAR"
args.py $VAR
args.py "\$VAR"
args.py '$VAR'
Spusťte příkazy znova, ale odstraňte one.sh
po přiřazení do VAR
.
Expanze tildy (domovského adresáře) funguje trochu jinak:
VAR=~
echo "$VAR" '$VAR' $VAR
VAR="~"
echo "$VAR" '$VAR' $VAR
Důležité je si odnést, že expanze proměnných může být ošidná, ale vždy se dá snadno vyzkoušet namísto pamatování si všech chytáků. Tedy, když si budete pamatovat, že mezery a wildcardy vyžadují speciální pozornost, bude to v pohodě :-).
Nahrazování příkazů (neboli zachytávání stdout do proměnné)
Často potřebujeme uložit výstup příkazu do proměnné. To také zahrnuje uložení obsahu souboru (nebo jeho části) do proměnné.
Význačným příkladem je použití příkazu mktemp(1)
. Ten řeší problém s
bezpečným vytvářením dočasných souborů (vzpomeňme si, že vytváření dočasných
souborů s předurčeným názvem v adresáři /tmp
je nebezpečné). Příkaz
mktemp
vytvoří soubor (nebo adresář) s unikátním názvem a vypíše jeho
název na stdout. Pro použití souboru v dalších příkladech si proto musíme
jeho název uložit do proměnné.
Shell nabízí následující syntaxi pro tzv. nahrazování příkazů (command substitution):
my_temp="$( mktemp -d )"
Příkaz mktemp -d
se spustí a jeho výstup se uloží do proměnné $my_temp
.
Kam se uloží stderr? Answer.
Jak potom zachytit stderr?
Například takto:
my_temp="$( mktemp -d )"
stdout="$( the_command 2>"$my_temp/err.txt" )"
stderr="$( cat "$my_temp/err.txt" )"
Nahrazování příkazů se také často používá při logování nebo při úpravách
jmen souborů (podívejte se do manuálových stránek, co dělají date
,
basename
a dirname
):
echo "I am running on $( uname -m ) architecture."
input_filename="/some/path/to/a/file.sh"
backup="$( dirname "$input_filename" )/$( basename "$input_filename" ).bak"
other_backup="$( dirname "$input_filename" )/$( basename "$input_filename" .sh ).bak.sh"
Přesměrování větších částí shellu
Celé řídící struktury (např. for
, if
nebo while
se všemi příkazy
uvnitř) se chovají jako jeden příkaz. Můžeme na ně tedy použít přesměrování
jako na celek. Například:
if test -d .git; then
echo "We are in a root of a Git project"
else
echo "This is not a root of a Git project"
fi | tr 'a-z' 'A-Z'
Příkaz read
Potřebujeme-li ve skriptu číst ze stdin do proměnné, můžeme použít vestavěný
příkaz read
:
read FIRST_LINE <input.txt
echo "$FIRST_LINE"
Příkaz read
typicky používáme v cyklu while
pro iterování přes celý
vstup. read
taky dokáže rozdělit řádek na části oddělené bílými znaky a
přiřadit každou z nich do samostatné proměnné.
Máme-li vstup v tomto formátu, následující cyklus spočítá průměr čísel.
/dev/sdb 1008
/dev/sdb 1676
/dev/sdc 1505
/dev/sdc 4115
/dev/sdd 999
count=0
total=0
while read device duration; do
count=$(( count + 1 ))
total=$(( total + duration ))
done
echo "Average is about $(( total / count ))."
Jak můžete uhádnout z úryvku výše, read
vrací 0, dokud je schopný načítat
do proměnných. Dosažení konce souboru je oznámeno nenulovou návratovou
hodnotou.
read
se občas může chovat příliš chytře k některým vstupům. Například
interpretuje zpětná lomítka. Pro potlačení tohoto chování lze použít read -r
.
Další užitečné parametry jsou -t
nebo -p
: použijte read --help
pro
zobrazení jejich popisu.
Parametry skriptů a getopt
Když shell skript dostane parametry, můžeme k nim přistupovat přes speciální
proměnné $1
, $2
, $3
, …
Otestujte s následujícím skriptem:
echo "$#"
echo "${0}"
echo "${1:-parameter one not set}"
echo "${2:-parameter two not set}"
echo "${3:-parameter three not set}"
echo "${4:-parameter four not set}"
echo "${5:-parameter five not set}"
a spusťte jako
./script.sh
./script.sh one
./script.sh one two
./script.sh one two three
./script.sh one two three four
./script.sh one two three four five
./script.sh one two three four five six
Chceme-li přistupovat ke všem parametrům, je k tomu speciální proměnná
$@
. Zkuste přidat args.py "$@"
do skriptu výše a spustit znova.
Proměnná $@
musí být ohraničená uvozovkami, aby fungovala správně
(vysvětlení je mimo rámec tohoto kurzu). Speciální proměnná $#
obsahuje
počet argumentů na příkazové řádce a $0
obsahuje název skriptu (jako
sys.argv[0]
).
getopt
Potřebuje-li náš skript jeden argument, stačí přistupovat přímo k
$1
. Potřebujeme-li rozpoznávat přepínače, bude zpracování argumentů
složitější. Shell k tomu poskytuje příkaz getopt
.
Nebudeme popisovat všechny detaily tohoto příkazu. Namísto toho ukážeme příklad, který si můžete upravovat podle svých potřeb.
Hlavní argumenty řídící chování getopt
jsou -o
a -l
, které obsahují
popis přepínačů přijímaných naším programem.
Předpokládejme, že budeme chtít přijímat volby --verbose
, která říká, aby
byl náš skript výřečnější, a --output
specifikující alternativní výstupní
soubor. Také budeme chtít přijímat krátké verze těchto přepínačů: -o
a
-v
. Při použití --version
budeme chtít vypsat verzi našeho skriptu. A
také nemůžeme zapomenout na --help
. Ostatní argumenty (nepřepínače) budou
interpretovány jako názvy vstupních souborů.
Specifikace přepínačů pro getopt
je jednoduchá:
getopt -o "vho:" -l "verbose,version,help,output:"
Jednoznakové přepínače jsou specifikovány za -o
, dlouhé volby za -l
, a
dvojtečka :
za přepínačem značí, že bude očekávat argument.
Za to přidáme --
následované vlastními parametry. Vyzkoušejte si to:
getopt -o "vho:" -l "verbose,version,help,output:" -- --help input1.txt --output=file.txt
getopt -o "vho:" -l "verbose,version,help,output:" -- --help --verbose -o out.txt input2.txt
...
Jak vidíte, getopt
umí zpracovat vstup a zkonvertovat parametry do
unifikované podoby, přičemž argumenty, které nejsou volbami, přesune na
konec.
Následující “magický” řádek (kterému nemusíte rozumět, abyste ho mohli
používat) přenastaví $1
, $2
, atd. aby obsahovaly hodnoty po zpracování
příkazem getopt
.
eval set -- "$( getopt -o "vho:" -l "verbose,version,help,output:" -- "$@" )"
Vlastní zpracování je poté celkem přímočaré:
#!/bin/bash
set -ueo pipefail
opts_short="vho:"
opts_long="verbose,version,help,output:"
# Check for bad usage first (notice the ||)
getopt -Q -o "$opts_short" -l "$opts_long" -- "$@" || exit 1
# Actually parse them (we are here only if they are correct)
eval set -- "$( getopt -o "$opts_short" -l "$opts_long" -- "$@" )"
be_quiet=true
output_file=/dev/stdout
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: $0 ..."
exit 0
;;
-o|--output)
output_file="$2"
shift
;;
-v|--verbose)
be_quiet=false
;;
--)
shift
break
;;
*)
echo "Unknown option $1" >&2
exit 1
;;
esac
shift
done
$be_quiet || echo "Starting the script"
for inp in "$@"; do
$be_quiet || echo "Processing $inp into $output_file ..."
done
Některé části skriptu si zaslouží vysvětlení.
true
a false
nejsou boolovské hodnoty, ale můžou být použity
samostatně. Ve skutečnosti jde o jednoduché programy, které jen vrací
správnou návratovou hodnotu (0 nebo 1). Všimněte si, jak je používáme pro
řízení logování. (Mimochodem, $be_verbose && echo "Message"
by
nefungovalo. Vidíte proč?)
exit
okamžitě ukončí shellový skript. Jeho volitelný parametr představuje
návratovou hodnotu skriptu.
shift
je speciální příkaz, který posune proměnné $1
, $2
, … o jedno
místo. Po jeho zavolání se hodnota $3
přesune do $2
, $2
se přesune do
$1
a hodnota $1
bude zahozena. "$@"
se voláním shift
také
upraví. Celý cyklus tedy zpracuje všechny volby po nalezení --
, které
oddělují volby od ostatní argumentů. Následný cyklus for
poté iteruje jen
přes zbylé argumenty.
Funkce v shellu
V shellu lze definovat i funkce:
function_name() {
commands
}
Funkce má stejné rozhraní jako plnohodnotný shellový skript. Argumenty jsou
předány jako $1
, $2
, … Výsledek funkce je celé číslo se stejnou
sémantikou jako návratový kód. Kulaté závorky ()
jsou zde tedy jen pro
označení, že jde o funkci; není to seznam argumentů.
Pro podrobnosti k tomu, které proměnné jsou uvnitř funkce viditelné, se prosím podívejte do následující sekce.
Jednoduchá logovací funkce může vypadat takto:
msg() {
echo "$( date '+%Y-%m-%d %H:%M:%S |' )" "$@" >&2
}
Funkce vypisuje aktuální datum následované vlastní zprávou, vše do stderr.
Jako další příklad uveďme následující funkci:
get_load() {
cut -d ' ' -f "$1" </proc/loadavg
}
load_curr="$( get_load 1 )"
load_prev="$( get_load 2 )"
Všimněte si, jak je stdout funkce zachycen do proměnné.
Volání return
ukončí funkci, volitelným parametrem je její návratová
hodnota. (Použijete-li ve funkci exit
, ukončí se tím celý skript.)
is_shell_script() {
case "$( head -n 1 "$1" 2>/dev/null )" in
\#!/bin/sh|\#!/bin/bash)
return 0
;;
*)
return 1
;;
esac
}
Taková funkce může být použita v if
takto:
if is_shell_script "$1"; then
echo "$1 is a shell script"
fi
Všimněte si, jak dobré pojmenování zjednoduší čtení konečného programu. Také
pomůže dát argumentům funkce názvy namísto odkazování se na ně přes
$1
. Můžete je přiřadit do proměnné, ale je doporučeno označit proměnné
jako local
(viz následující sekce):
is_shell_script() {
local file="$1"
case "$( head -n 1 "$file" 2>/dev/null )" in
\#!/bin/sh|\#!/bin/bash)
return 0
;;
*)
return 1
;;
esac
}
Můžete si všimnout, že aliasy, funkce, builtiny, a normální příkazy se
všechny volají stejným způsobem. Shell proto má pevné pořadí priorit: Aliasy
se testují jako první, poté funkce, builtiny, a nakonec příkazy v $PATH
. V
souvislosti s tím se můžou hodit vestavěné příkazy command
a builtin
(např. ve funkcích stejného jména).
Navzdory mnoha odlišnostem od funkcí v jiných programovacích jazycích, funkce v shellu pořád představují nejlepší způsob, jak členit vaše skripty. Správně nazvaná funkce vytváří vrstvu abstrakce a zachycuje záměr skriptu, zároveň skrývá implementační detaily.
Subshell a viditelnost proměnných (scoping)
Tato část vysvětluje pár pravidel a faktů o viditelnosti proměnných a proč některé konstrukce nemůžou fungovat.
Shellové proměnné jsou ve výchozím stavu globální. Všechny proměnné jsou viditelné ve všech funkcích, jejich úpravy uvnitř funkcí jsou viditelné ve zbytku skriptu, atp.
Bývá praktické deklarovat proměnné ve funkcích jako lokální (local
), což
omezí jejich viditelnost jen na danou funkci. (Přesněji, proměnná je
viditelná v této funkci a všech funkcích volaných z ní. Můžete si
představit, že předchozí hodnota proměnné je uložena během vykonávání
local
a obnovena po návratu z funkce. To se liší od většiny programovacích
jazyků.)
Po spuštění jiného programu (včetně shellových a Pythoních skriptů), program dostane kopii všech exportovaných proměnných. Když tyto proměnné upraví, změny zůstanou jen uvnitř tohoto programu a nijak neovlivní původní shell. (Je to podobné tomu, jak funguje pracovní adresář.)
Když použijete pipu, je to stejné jako spuštění nového shellu: proměnné nastavené uvnitř pipeliny nejsou vidět v okolním kódu. (Jediný rozdíl je, že pipelina dostane i neexportované proměnné.)
Uzavření části našeho skriptu do ( .. )
vytvoří tzv. subshell, který se
chová, jako kdybychom spustili jiný skript. Proměnné upravené uvnitř tedy
opět nejsou viditelně pro okolní shell.
Přečtěte si a spusťte následující kód pro pochopení zmiňovaných věcí.
global_var="one"
change_global() {
echo "change_global():"
echo " global_var=$global_var"
global_var="two"
echo " global_var=$global_var"
}
change_local() {
echo "change_local():"
echo " global_var=$global_var"
local global_var="three"
echo " global_var=$global_var"
}
echo "global_var=$global_var"
change_global
echo "global_var=$global_var"
change_local
echo "global_var=$global_var"
(
global_var="four"
echo "global_var=$global_var"
)
echo "global_var=$global_var"
echo "loop:"
(
echo "five"
echo "six"
) | while read value; do
global_var="$value"
echo " global_var=$global_var"
done
echo "global_var=$global_var"
Cvičení
Hromadná konverze obrázků
Program convert
z ImageMagick umí převádět
obrázky mezi formáty použitím convert source.png target.jpg
(s téměř
jakýmikoliv příponami souborů). Převeďte všechny obrázky s příponou .png
v
aktuálním adresáři do JPEGu (s příponou .jpg
).
Mimochodem, ImageMagick umožnuje provádět spoustu různých operací, jednou z těch, které je dobré si pamatovat je změna rozměrů obrázků:
convert DSC0000.jpg -resize 800x600 thumbs/DSC0000.jpg
Standardní vstup nebo argumenty?
Napište fact.sh
s funkcí, která spočítá faktoriál zadaného čísla. Vytvořte
dvě verze:
-
Načtení vstupu ze stdin.
-
Načtení vstupu z prvního argumentu (
$1
). Answer.
Kterou verzi bylo jednodušší napsat? Která dává větší smysl?
Ad-hoc zpracování souborů CSV
Napište skript csv_sum.sh
, který přečte soubor CSV ze standardního
vstupu. Sečtěte všechna čísla ve sloupci, který je zadaný jako jediný
argument. Nezapomeňte skript ukončit s nenulovým návratovým kódem a vhodnou
chybovou hláškou, není-li žádný argument zadán.
Uvažme následující soubor nazvaný file.csv
.
family_name,first_name,age,points,email
Doe,Joe,22,1,joe_doe@some_mail.com
Fog,Willy,38,8,ab@some_mail.com
Zdepa,Pepa,10,1,pepa@some_mail.com
Výstupem příkazu ./csv_sum.sh points <file.csv
má být 10
.
Answer.
Sloupcové grafy (v shellovském stylu)
Napište bar_plot.sh
, který vypíše sloupcový graf s vodorovnými
sloupci. Vstupní čísla značí šířku sloupců. Vyberte si, která z možností
vstupů je pro vás vhodnější. Příklad:
$ ./bar_plots.sh 7 1 5
7: #######
1: #
5: #####
Je-li největší hodnota větší než 60, přeškálujte celý graf.
Answer.Úloha tree.py
v shellu
Napište implementaci úlohy tree.py
v shellu.
Hodnocené úlohy …
… pro toto cvičení jsou společné pro toto a další cvičení a budou zadány na dalším cvičení.
Učební výstupy
Znalosti konceptů
Znalost konceptů znamená, že rozumíte významu a kontextu daného tématu a jste schopni témata zasadit do většího rámce. Takže, jste schopni …
-
vysvětlit jak funguje expanze shellu a rozdělení argumentů příkazové řádky do pole
-
vysvětlit kdy se vyplatí použít Python a kdy bash
-
vysvětlit, co je to proměnná prostředí
-
vysvětlit rozdíl mezi neexportovanou a exportovanou proměnnou (prostředí) v shellu
-
vysvětlit problémy při souběžné práci s dočasnými soubory
-
vysvětlit, jak exit kódy umožňují řídit shellové skripty
-
vysvětlit, jak je vyhodnocen kód
if true; then ... fi
-
vysvětlit, jak funguje proměnná prostředí $PATH a jak ovlivňuje skripty se shebangem
-
vysvětlit, jak v shellu funguje omezení oblasti platnosti (scoping) pro proměnné
Praktické dovednosti
Praktické dovednosti se obvykle týkají použití daných programů pro vyřešení různých úloh. Takže, dokážete …
-
nastavovat a číst proměnné prostředí v shellu
-
vyhodnocovat matematické výrazy v shellu
-
používat nahrazování příkazů (command substitution)
-
používat bezpečně v shellových skriptech dočasné soubory
-
používat v shellových skriptech řídicí struktury (for, while, if, case)
-
používat příkaz read
-
používat getopt pro parsování argumentů příkazové řádky
-
vytvářet a používat funkce
-
číst proměnné prostředí v Pythonu (volitelné)