Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
Poslední aktuality jsou v issue #112 (z 15. dubna).
- Předstartovní kontrola
- Průběžný příklad
-
Načítání konfigurace (
.
asource
) - Řídicí struktury v shellových skriptech
-
Parametry skriptů a
getopts
-
Příkaz
read
- Větší cvičení I
- Poznámka na okraj: jak se publikují webové stránky
- SCP a rsync
- Větší cvičení II
- Úlohy k ověření vašich znalostí
- Učební výstupy a kontrola po cvičení
- Seznam změn na této stránce
Na tomto cvičení si rozšíříme znalosti o skriptování v shellu. Seznámíme se s řídicími strukturami a dalšími střípky, díky nimž budou naše shellové skripty výkonnější.
Pro vysvětlení nových konstrukcí opět použijeme průběžný příklad.
Téma druhého testu na cvičení bude také zahrnovat konstrukce uvedené v tomto cvičení. Úloha pro test bude mnohem menší než náš průběžný příklad, ale můžete očekávat, že budete muset použít některé konstrukce uvedené na tomto cvičení.
Ačkoliv se jedná o pravděpodobně nejdelší cvičení podle počtu slov, nových konceptů je tu poměrně málo. Proto se, prosím, nelekejte délky posuvníku :-).
Předstartovní kontrola
- Umíte používat shellové proměnné a tzv. command substitution.
- Pamatujete si význam exit kódu (návratová hodnota programu) a víte, co
znamená, když má hodnotu
0
. - Pamatujete si, k čemu se používá Pandoc.
Průběžný příklad
Nyní se ještě vrátíme k našemu příkladu s generováním webu. Zdrojové kódy jsou opět v našem repozitáři příkladů, ale jak vidíte v 09/web
, je zde nyní mnohem více stránek a soubory jsme rozdělili do více podadresářů.
Je zde adresář content
se vstupními soubory v jazyce Markdown, adresář
static
se souborem CSS a případně dalšími soubory, které budou zkopírovány
na webový server, a je tu též adresář templates
se šablonami Pandoc pro
naše stránky.
Nyní vytvoříme slušný shellový skript, který dokáže tento web vytvořit, a také web zkopíruje na webový server, aby byl veřejně dostupný.
Uznáváme, že existují speciální nástroje určené přesně na toto. Říká se jim statické generátory stránek (nebo jen SSG - static site generators) a je jich k dispozici obrovské množství. Tato úloha ale tvoří velmi dobré hřiště, na kterém si můžeme ukázat, čeho je shell schopen :-).
Začneme triviálním generátorem, který je v podstatě kopií generátoru webu z jednoho z předchozích cvičení.
Načítání konfigurace (.
a source
)
Již jsme viděli, že chování našich skriptů můžeme měnit pomocí parametrů
(připomeňme proměnné $1
, $2
, …) nebo nastavením proměnných před jejich
spuštěním (připomeňme náš skript ze cvičení 07 a volání ve tvaru
html_dir=out_www ./build.sh
).
Ale co kdybychom chtěli více takových nastavení? Nemůžeme je uložit do souboru?
To je ve skutečnosti docela obvyklá situace. Uložme tedy konfiguraci
html_dir
do souboru config.rc
(přípona .rc
je docela běžná a může
odkazovat třeba na běhovou konfiguraci – runtime configuration).
html_dir=out_www
Pokud nyní přidáme následující řádek do našeho skriptu build.sh
, bude se
chovat, jako by obsah souboru config.rc
byl součástí hlavního
skriptu. Proměnná bude nastavena a může být používána ve zbytku skriptu.
# Oba řádky jsou ekvivalentní, v reálu bychom použili jen jeden z nich
. config.rc
source config.rc
Toto je vlastně také způsob, jakým se konfiguruje samotný shell. Vzpomeňte
si, jak jsme do soubrou ~/.bashrc
přidali proměnnou EDITOR
. Tento soubor
se vykoná při spuštění Bashe. Nyní už by vám měl být jasný význam
následujícího úryvku, který ~/.bashrc
často obsahuje:
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
Pokud daný soubor existuje (ke správné syntaxi se dostaneme později na tomto
cvičení), zahrneme ho pomocí source
, tj. importujeme obsah souboru na toto
místo. Takže jsme importovali globální konfiguraci Bashe, která je uložená v
adresáři /etc
.
Speciální příkazy .
a source
lze také použít k načtení kódu
knihovny. Například v souboru logging.sh
můžete mít následující funkci,
kterou pak můžete načíst do dalších skriptů, aniž byste museli znovu a znovu
definovat funkci msg
.
msg() {
echo "$( date '+%Y-%m-%d %H:%M:%S |' )" "$@" >&2
}
Soubory, které mají být zahrnuty prostřednictvím source
, obvykle
neobsahují shebang a obvykle nejsou spustitelné. Je to většinou proto, aby
se zdůraznila skutečnost, že se nejedná o samostatné spustitelné soubory,
ale spíše o “knihovny”.
Totéž platí i pro moduly Pythonu: v hlavním programu obvykle vidíte shebang
(a nastavený bit x
), zatímco skutečné moduly (které importujete pomocí
import
) jsou často bez shebangů a mají pouze oprávnění rw-
.
Pokračování průběžného příkladu
Náš příklad přepracujeme na přizpůsobitelné řešení, ve kterém náš skript přečte konfiguraci webu poskytnutou uživatelem.
V adresáři s webovou stránkou vytvoříme následující soubor ssg.rc
.
# My site configuration
site_title="My site"
build_page "index.md"
build_page "rules.md"
build_page "alpha.md"
A náš hlavní skript upravíme, aby vypadal takto.
#!/bin/bash
set -ueo pipefail
msg() {
echo "$( date '+%Y-%m-%d %H:%M:%S | SSG |' )" "$@" >&2
}
get_version() {
git rev-parse --short HEAD 2>/dev/null || echo unknown
}
build_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".md" ).html"
msg "Generating $input_file => $output_file"
pandoc \
--template templates/main.html \
--metadata site_title="$site_title" \
--metadata page_version="$( get_version )" \
"src/$input_file" >"$output_file"
}
site_title="$( whoami )'s site"
mkdir -p public
source ssg.rc
cp -R static/* public/
Co jsme to vytvořili? Náš konfigurační soubor ssg.rc
obsahuje triviální
doménově specifický jazyk (DSL – domain-specific language), který řídí
generování webových stránek. Náš hlavní skript poskytuje funkci
build_page
, která se volá z konfiguračního souboru.
Uvnitř této funkce sestavíme název výstupního souboru (zkuste, co udělá
basename input.md .md
!) a spustíme Pandoc.
Ve skutečnosti se jedná o velmi jednoduchý kus kódu, ale podařilo se nám rozdělit konfiguraci a vlastní generování do samostatných souborů a vytvořit opakovaně použitelný nástroj. Porovnejte, kolik práce by to dalo v jiném jazyce. Jen si představte, kolik práce by dalo analyzovat konfigurační soubor…
Řídicí struktury v shellových skriptech
Nejprve musíme zmínit, že více příkazů může být odděleno pomocí ;
(středníku). Ačkoliv v shellových skriptech je lepší psát každý příkaz na
samostatný řádek, v interaktivním módu se často může hodit zapsat více
příkazů na jeden řádek (třeba už jen proto, že to umožňuje rychlejší pohyb v
historii pomocí šipky nahoru).
Středníky uvidíme v řídicích strukturách na různých místech v roli oddělovače.
Příkazy řízení toku představíme spíše stručně: jejich účel je stejný jako v jiných jazycích. Věnujte však pozornost tomu, jak jsou řízeny: to, co vypadá jako podmínka, je ve skutečnosti často spuštění programu.
for
cykly
For cykly v shellu vždy iterují přes sadu hodnot, které jsou předány při startu cyklu.
Formát je obecně následující:
for PROMENNA in VAL1 VAL2 VAL3; do
tělo cyklu
done
Typickým příkladem použití cyklu for
je iterování přes seznam
souborů. Tento seznam je často vygenerovaný pomocí expandovaných wildcardů.
Pojďme se podívat na příklad, který spočítá počet číslic ve všech souborech
*.txt
:
for i in *.txt; do
echo -n "$i: "
tr -c -d '0-9' <"$i" | wc -c
done
Všimněte si, že ve výrazu for
je jméno proměnné i
uvedeno bez $
. Dále
můžeme vidět, že proměnná může být expandovaná i uvnitř přesměrování stdin
(nebo stdout).
Když tento výraz začneme psát přímo v interaktivním shellu, prompt se změní na jednoduché >
(tedy pravděpodobně, závisí to na vašem nastavení).
Shell tím naznačuje, že na vstupu očekává zbytek cyklu.
Celý skript můžeme také dostat na jednu řádku (to se ale hodí pouze pro jednorázové skripty):
for i in *.txt; do echo -n "$i: "; tr -c -d '0-9' <"$i" | wc -c; done
Pokud chceme iterovat přes hodnoty obsahující mezery, musíme hodnoty uvést v uvozovkách. Expanze wildcardů je v tomto ohledu bezpečná a bude fungovat i když názvy souborů mezery obsahují.
for i in one "two three"; do
echo "$i";
done
if
a else
Podmínka if
je v shellu trochu složitější.
Podmínka tedy ve skutečnosti nikdy není v tradičním formátu a rovná se b,
protože if
se vždy řídí exit kódem.
Syntaxe podmínky if-then-else
obecně vypadá takto:
if prikaz_kontrolujici_podminku; then
uspech
elif jiny_prikaz_pro_vetev_else_if; then
dalsi_uspech
else
prikazy_v_else_vetvi
fi
Příkaz if
musí být ukončený pomoci fi
a větve elif
a else
jsou
volitelné.
Jednoduché podmínky lze vyhodnotit pomocí příkazu test
, který již
známe. Podívejte se na příkaz man test
a zjistěte, co všechno lze
testovat.
Pojďme se podívat, jak použít if
a test
pro kontrolu, jestli se
nacházíme v Git-ovém projektu:
if test -d .git; then
echo "Jsme v korenovem adresari Git projektu."
fi
Můžeme použít i trochu elegantnější syntaxi: [
(levá hranatá závorka) je
synonymem pro test
a chová se přesně stejně, až na to, že vyžaduje jako
svůj poslední argument ]
. Pomocí této syntaxe může náš příklad vypadat
následovně:
if [ -d .git ]; then
echo "Jsme v korenovem adresari Git projektu."
fi
I v tomto případě je ale [
pouze obyčejný příkaz, jehož exit kód určuje,
která z větví příkazu if
se provede.
Cyklus while
While cykly vypadají následovně:
while prikaz_kontrolujici_cyklus; do
prikazy_ke_spusteni
done
I v tomto případě se podmínka vyhodnotí jako pravdivá, pokud
prikaz_kontrolujici_cyklus
vrátí exit kód 0.
Následující příklad hledá první ještě nezabrané jméno pro logovací soubor. Pozor – tento kód není imunní vůči souběhům (race conditions), pokud je spuštěn paralelně. Může být tedy spuštěn vícekrát, ale nikdy ve více procesech současně.
counter=1
while [ -f "/var/log/myprog/main.$counter.log" ]; do
counter=$(( counter + 1 ))
done
logfile="/var/log/myprog/main.$counter.log"
echo "Loguji do souboru $logfile" >&2
Aby byl program odolný vůči chybám souběhu (tj. při běhu ve více instancích
paralelně), museli bychom použít příkaz mkdir
. Tento příkaz nahlásí chybu
v případě, že složka již existuje (a je dostatečně atomický na to, abychom
pomocí něho dokázali zjistit, že náš program byl opravdu úspěšný a nekrade
tedy soubor někomu jinému).
Všimněte si, že pro invertování úspěšnosti (exit-kódu) programu v
následujícím kódu používáme vykřičník !
.
counter=1
while ! mkdir "/var/log/myprog/log.$counter"; do
counter=$(( counter + 1 ))
done
logfile="/var/log/myprog/log.$counter/main.log"
echo "Loguji do souboru $logfile" >&2
break
a continue
Stejně jako v jiných jazycích je možné z cyklu vyskočit pomocí příkazu
break
. Obdobně můžete používat i příkaz continue
.
Switch (aneb case ... esac
)
Shell nabízí i konstrukci case
pro případy, ve kterých potřebujeme
rozvětvit náš program na základě hodnoty proměnné. Tato konstrukce je v
zásadě podobná konstrukci switch
v jiných jazycích, ale case
má pár
shellových specifik.
Syntaxe je následující:
case hodnota_pro_vetveni in
option1) prikazy_pro_prvni_moznost ;;
option2) prikazy_pro_druhou_moznost ;;
*) vychozi_vetev ;;
esac
Podobně jako u if
musíme příkaz uzavřít stejným klíčovým slovem zapsaným
pozpátku a u každé možnosti musíme ukončit sadu příkazů pomocí dvou
středníků ;;
.
Jednotlivé možnosti v rámci case
mohou kromě přímých hodnot obsahovat i
zástupné znaky (wildcards) a |
, aby bylo porovnávání trochu flexibilnější.
Jednoduchý příklad může vypadat takto:
case "$EDITOR" in
mcedit) echo 'Midnight Commander je nejlepší' ;;
joe) echo 'Malý, ale šikovný' ;;
emacs|vi*) echo 'Wow :-)' ;;
*) echo "To někdo opravdu používá $EDITOR?" ;;
esac
Pokračování průběžného příkladu
Vyzbrojeni znalostmi o dostupných řídicích konstrukcích můžeme náš skript pro generování stránek ještě vylepšit.
Zbavíme uživatele břemene ručního zadávání seznamu vstupních souborů a místo toho soubory vyhledáme sami. Uživatelský konfigurační soubor díku tomu bude zcela volitelný.
Náš skript změníme takto.
build_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".md" ).html"
msg "Generating $input_file => $output_file"
pandoc \
--template templates/main.html \
--metadata site_title="$site_title" \
--metadata page_version="$( get_version )" \
"$input_file" >"$output_file"
}
...
if [ -f ssg.rc ]; then
source ssg.rc
fi
for page in src/*.md; do
if ! [ -f "$page" ]; then
continue
fi
build_page "$page"
done
Upravili jsme build_page
tak, aby při spouštění pandoc
nepřidával src
k názvu souboru, a sami projdeme přes soubory Markdown v adresáři src
.
Znak !
je builtin příkaz, který obrátí význam výstupního kódu, tj. chová
se jako booleovský operátor negace.
Proč testujeme -f
uvnitř smyčky? Odpověď.
A ano: skript jsme dost upravovali. To je normální. Často máte jen mlhavou představu o tom, co potřebujete mít. Vycházíte z jednoduchých scénářů a podle potřeby je rozšiřujete.
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.
Pro ilustraci můžeme zprávu transformovat na velká písmena takto.
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'
Parametry skriptů a getopts
Připomeňme, že když skript shellu obdrží parametry, můžeme k nim přistupovat
prostřednictvím speciálních proměnných $1
, $2
, $3
. Pro přístup ke všem
parametrům existuje také proměnná $@
(připomeňme, že proměnná $@
musí
být pro správnou funkci obalena uvozovkami; vysvětlení přesahuje rámec
tohoto kurzu).
Speciální proměnná $#
obsahuje počet argumentů na příkazovém řádku a $0
označuje skutečný název skriptu.
getopts
Potřebuje-li náš skript jen jeden argument, stačí přistupovat přímo k
$1
. Potřebujeme-li ale rozpoznávat různé přepínače, bude zpracování
argumentů složitější. Shell k tomu poskytuje příkaz getopts
, který nám s
jejích zpracováním pomůže.
Ačkoliv getopts
je standardní způsob práce s přepínači, není příliš
uživatelsky přívětivý. Na druhou stranu, použití getopts
není něco, co
byste si měli pamatovat: je to přesně ten kus kódu, který budete kopírovat
ze skriptu do skriptu a v případě potřeby ho jen aktualizovat.
Ukážeme si, jak lze getopts
použít na jednoduchém skriptu: přijme seznam
souborů a převede je pomocí Pandocu do HTML (na standardní výstup). Bude
také podporovat -V
pro vypsání své verze a -o
pro zadání alternativního
výstupního souboru (místo stdout).
Specifikace přepínačů getopts
je jednoduchá. Vyjmenujeme názvy přepínačů,
za těmi, které vyžadují argument, následuje dvojtečka. Posledním argumentem
je jméno proměnné (bez dolaru!), do které bude vlastní volba uložena.
getopts "Vho:" opt
Příkaz bude vracet návratovou hodnotu 0 (jako svůj exit kód), dokud budou
zpracovávány přepínače. Po dokončení nám proměnná $OPTIND
sdělí, kolik
parametrů bylo skutečně zpracováno.
#!/bin/sh
usage() {
echo "Usage: $1 [-V] [-o filename] [-h] input [files]"
echo " -V Print program version and terminate."
echo " -o filename Store output into filename instead to stdout."
echo " -h Print this help and exit."
}
output_file="/dev/stdout"
print_version=false
while getopts "Vho:" opt; do
case "$opt" in
h)
usage "$0"
exit 0
;;
V)
print_version=true
;;
o)
output_file="$OPTARG"
;;
*)
usage "$0" >&2;
exit 1
;;
esac
done
shift $(( OPTIND - 1))
if $print_version; then
echo "My script, version 0.0.1"
exit 0
fi
cat "$@" | pandoc -t html >"$output_file"
Některé části skriptu si zaslouží vysvětlení.
true
a false
nejsou logické hodnoty, ale lze je jako takové použít.
Vzpomeňte si, jak jsme je používali na cvičení 07 (a že existují /bin/true
and /bin/false
).
exit
okamžitě ukončí shellový skript. Jeho volitelný parametr určuje
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í. S
parametrem posune proměnné vícekrát.
Všechny argumenty pak předáme programu cat
. Všimněte si, že to bude fungovat, i když nebudou předány žádné parametry a skript pak bude číst standardní vstup. Proč? Odpověď.
Pokračování průběžného příkladu
Upravíme náš skript tak, aby akceptoval přepínač -w
, s nímž program bude
neustále sledovat změny v souborech src/*.md
, a při každé takové změně web
přegeneruje.
Nejprve skript změníme, aby používal getopts
, a poté přidáme podporu pro
-w
.
#!/bin/bash
set -ueo pipefail
usage() {
echo "Usage: ..."
}
msg() {
echo "$( date '+%Y-%m-%d %H:%M:%S | SSG |' )" "$@" >&2
}
get_version() {
git rev-parse --short HEAD 2>/dev/null || echo unknown
}
build_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".md" ).html"
$LOGGER "Generating $input_file => $output_file"
pandoc \
--template templates/main.html \
--metadata site_title="$site_title" \
--metadata page_version="$( get_version )" \
"$input_file" >"$output_file"
}
generate_web() {
for page in src/*.md; do
if ! [ -f "$page" ]; then
continue
fi
build_page "$page"
done
cp -R static/* public/
}
LOGGER=:
watch_for_changes=false
while getopts "hvw" opt; do
case "$opt" in
h)
usage "$0"
exit 0
;;
v)
LOGGER=msg
;;
w)
watch_for_changes=true
;;
*)
usage "$0" >&2;
exit 1
;;
esac
done
shift $(( OPTIND - 1))
site_title="$( whoami )'s site"
mkdir -p public
if [ -f ssg.rc ]; then
source ssg.rc
fi
generate_web
Pro skutečnou podporu přepínače -w
použijeme inotifywait
, což je
speciální program, který obdrží seznam souborů a ukončí se v okamžiku, kdy
je některý ze souborů změněn. Náš skript tedy ve skutečnosti nebude dělat
nic, dokud nebude soubor změněn, protože inotifywait
do té doby
“zablokuje” jeho provádění.
Do našeho skriptu přidáme následující kód, který poběží dokola, bude
sledovat změny a automaticky obnovovat web. Když je skript spuštěn s
přepínačem -w
, musíme pro ukončení použít klávesy Ctrl-C
.
...
if [ -f ssg.rc ]; then
source ssg.rc
fi
generate_web
if $watch_for_changes; then
while true; do
$LOGGER "Waiting for file change..."
inotifywait -e modify src/* src static static/*
generate_web
done
fi
Příkaz read
Doposud naše skripty buď vůbec nepotřebovaly standardní vstup, nebo vstup zcela přenechávaly jiným programům.
Pokud však potřebujete samostatně zpracovávat řádky ze vstupu, je možné v shellu standardní vstup číst po řádcích.
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
hodnoty ze vstupu do proměnných. Nenulovou návratovou hodnotou je oznámeno
dosažení konce souboru.
Pro některé vstupy se read
občas může chovat příliš chytře – 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.
Pokud chceme číst z konkrétního souboru (předpokládejme, že jeho název je
uložen v proměnné $input
), můžeme také přesměrovat vstup pro celou smyčku
a skript zapsat takto:
while read device duration; do
count=$(( count + 1 ))
total=$(( total + duration ))
done <"$input"
To je vlastně docela běžné použití vzoru while read
.
Zkontrolujte, zda rozumíte tomu, jak funguje příkaz read
Předpokládejme, že máme následující textový soubor data.txt
.
ONE
TWO
Máme také následující skript reader.sh
:
#!/bin/sh
set -ueo pipefail
read -r data_one <data.txt
read -r data_two <data.txt
read -r stdin_one
read -r stdin_two
echo "data_one=${data_one}"
echo "data_two=${data_two}"
echo "stdin_one=${stdin_one}"
echo "stdin_two=${stdin_two}"
Vyberte všechna pravdivá tvrzení o výstupu následujícího volání.
./reader.sh <data.txt
You need to have enabled JavaScript for the quiz to work.
Větší cvičení I
Tuto implementaci použijeme později v našem průběžném příkladu (zatím tedy ne).
Představte si, že máme vstupní data s výsledky zápasů v následujícím formátu (tým, vstřelené branky, dvojtečka, branky vstřelené druhým týmem, druhý tým).
alpha 2 : 0 bravo
bravo 0 : 1 charlie
alpha 5 : 4 charlie
Napište shellový skript, který vypíše tabulku se souhrnnými výsledky.
Za vítězství přidělte 3 body, za remízu 1 bod. Váš program nemusí řešit situaci, kdy mají dva týmy stejný počet bodů.
Řešení
Začneme funkcí, která obdrží dva argumenty - počty gólů, které dala každá strana v jednom zápasu - a vypíše počet přidělených bodů.
get_points() {
local goals_mine="$1"
local goals_opponent="$2"
if [ "$goals_mine" -eq "$goals_opponent" ]; then
echo 1
elif [ "$goals_mine" -gt "$goals_opponent" ]; then
echo 3
else
echo 0
fi
}
Další funkce pak spočítá body z každého zápasu.
preprocess_scores() {
local team_one team_two
local goals_one goals_two
while read -r team_one goals_one colon goals_two team_two; do
if [ "$colon" != ":" ]; then
echo "WARNING: ignoring invalid line $team_one $goals_one $colon $goals_two $team_two" >&2
continue
fi
echo "$team_one" "$( get_points "$goals_one" "$goals_two" )"
echo "$team_two" "$( get_points "$goals_two" "$goals_one" )"
done
}
Tyto dvě funkce společně transformují vstup na následující:
alpha 3
bravo 0
bravo 0
charlie 3
alpha 3
charlie 0
Na těchto datech bychom mohli zavolat náš známý skript group_sum.py
, nebo
si posčítání můžeme napsat sami v shellu. Při implementaci v shellu budeme
počítat s tím, že data jsou již seřazena podle klíče, abychom implementaci
zjednodušili.
sum_by_sorted_keys() {
local key value
local prev_key=""
local sum=0
while read -r key value; do
if [ "$key" != "$prev_key" ]; then
if [ -n "$prev_key" ]; then
echo "$prev_key $sum"
fi
prev_key="$key"
sum=0
fi
sum=$(( sum + value ))
done
if [ -n "$prev_key" ]; then
echo "$prev_key $sum"
fi
}
Proč potřebujeme, aby data byla setříděná? Mohli bychom si je setřídit sami? Fungovala by následující úprava (pouze změna tohoto řádku)?
# replacing "while read -r key value; do"
sort | while read -r key value; do
Odpověď.
Jaká změna uvnitř této funkce by tedy fungovala? Odpověď.
Tyto funkce dohromady tvoří stavební kameny pro vyřešení celé skládačky:
preprocess_scores | sum_by_keys | sort -n -k 2 -r | column -t
Poznámka na okraj: jak se publikují webové stránky
Nyní si uděláme malou odbočku do oblasti (historie) publikování webových stránek. Publikování webových stránek dnes obvykle znamená pronájem webového prostoru, kam můžete nahrát své soubory HTML (nebo PHP), nebo dokonce pronájem nakonfigurované instance nějaké webové aplikace, například Wordpress.
Tradičně také uživatelé často obdrželi webový prostor jako součást unixového
účtu na nějakém sdíleném počítači. Nastavení se obvykle provádělo tak, že
cokoli se objevilo ve vašem $HOME/public_html
, bylo dostupné pod stránkou
example.com/~LOGIN
.
Možná jste se s takovými stránkami také setkali, typicky na univerzitních stránkách jednotlivých profesorů.
S rozvojem virtualizace (a cloudu) bylo snazší nedávat uživatelům přístup jako skutečným uživatelům systému, ale vložit další vrstvu, kde uživatel může manipulovat pouze s určitými soubory, aniž by měl přístup k shellu.
Webové stránky na laboratorních strojích
Naše laboratorní stroje (např. u-pl*
) tuto základní funkci také nabízejí.
Pomocí SSH se připojte k jednomu z nich (připomeňte si seznam adres z
cvičení 05) a vytvořte adresář ~/WWW
.
Vytvořte jednoduchý soubor HTML v adresáři WWW
(přeskočte, pokud jste sem
již předtím nahráli nějaké soubory).
echo '<html><head><title>Hello, World!</title><body><h1>Hello, World!</h1></body></html>' >index.html
Tato stránka bude k dispozici jako http://www.ms.mff.cuni.cz/~LOGIN/.
Poznamenejme, že je třeba přidat potřebná oprávnění souborového systému AFS
pomocí příkazu fs setacl
.
fs setacl ~/WWW www rl
fs setacl ~/. www l
SCP a rsync
Pro kopírování souborů mezi dvěma počítači se systémem Linux můžeme použít
příkaz scp
. Ten si uvnitř vytvoří spojení SSH a zkopíruje přes něj
soubory.
Syntaxe je velmi jednoduchá a odpovídá použití obyčejného cp
:
scp local_source_file.txt user@remote_machine:remote_destination_file.txt
scp user@remote_machine:remote_source_file.txt local_destination_file.txt
Rsync
Mnohem silnějším nástrojem pro kopírování souborů je rsync
. Podobně jako
scp
běží přes spojení SSH, ale musí být nainstalován na obou stranách (což
obvykle není problém).
Rsync umí kopírovat celé stromy adresářů, zpracovávat symbolické odkazy (symlinky), přístupová práva a další atributy souborů. Dokáže také rozpoznat, že některé soubory již na druhé straně existují (přesně nebo přibližně), a přenést pouze rozdíly.
Syntaxe jednoduchého kopírování je stejná jako u cp
a scp
:
rsync local_source_file.txt user@remote_machine:remote_destination_file.txt
rsync local_source_file.txt user@remote_machine:remote_destination_directory/
Větší cvičení II
Vytvořili jsme skript pro výpočet bodovací tabulky. Bylo by dobré ji vygenerovat během generování celého webu.
Rozšiřte náš průběžný příklad o následující funkce. Každý soubor *.bin
v
souboru src/
bude považován za skript, který se spustí a jeho výstup se
uloží do stejnojmenného souboru HTML.
Než se podíváte na naše řešení, vyzkoušejte si to nejprve sami.
Připomeňme, že přípona souboru není důležitá a že přípona .bin
je tak
obecná, že se pod ni může schovat jakýkoli (interpretovaný) programovací
jazyk (pokud má skript správný shebang). Ve skutečnosti bude fungovat i pro
kompilované (C, Rust a podobné) programy.
Řešení
Změna je poměrně jednoduchá. Pro přehlednost jsme také přejmenovali
build_page
na build_markdown_page
.
build_dynamic_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".bin" ).html"
$LOGGER "Generating $input_file => $output_file"
"$input_file" >"$output_file"
}
generate_web() {
local page
for page in src/*.md; do
if ! [ -f "$page" ]; then
continue
fi
build_markdown_page "$page"
done
local script
for script in src/*.bin; do
if ! [ -f "$script" -a -x "$script" ]; then
continue
fi
build_dynamic_page "$script"
done
cp -R static/* public/
}
A náš skript pro generování tabulek můžeme rozšířit na následující.
...
as_markdown_table() {
echo
echo '| Team | Points |'
echo '| ---- | -----: |'
while read team score; do
echo '|' "$team" '|' "$score" '|'
done
echo
}
. ssg.rc
(
echo '---'
echo 'title: Scoring table'
echo '---'
echo '# Scoring table'
preprocess_scores <scores.txt | sum_by_keys | sort -n -k 2 -r | as_markdown_table
) | pandoc \
--template templates/main.html \
--metadata site_title="$site_title" \
--metadata page_version="$( git rev-parse --short HEAD 2>/dev/null || echo unknown )"
Další vylepšení skriptu
Vypadá to dobře?
To sotva. V kódu se opakovaně objevuje fragment volání Pandoc. Náš generátor stránek není dokonalý.
Pojďme jej vylepšit.
Jako druhou verzi skriptu proveďte rozšíření, které rozliší skripty
*.md.bin
a *.html.bin
. Od skriptů s příponou .html.bin
se očekává, že
budou generovat přímo HTML, zatímco .md.bin
bude generovat Markdown, který
budeme zpracovávat sami.
Řešení
...
pandoc_as_filter() {
pandoc \
--template templates/main.html \
--metadata site_title="$site_title" \
--metadata page_version="$( get_version )" \
"$@"
}
build_markdown_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".md" ).html"
$LOGGER "Generating $input_file => $output_file"
pandoc_as_filter "$input_file" >"$output_file"
}
build_dynamic_html_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".html.bin" ).html"
$LOGGER "Generating $input_file => $output_file"
"$input_file" >"$output_file"
}
build_dynamic_markdown_page() {
local input_file="$1"
local output_file="public/$( basename "$input_file" ".md.bin" ).html"
$LOGGER "Generating $input_file => $output_file"
"$input_file" | pandoc_as_filter >"$output_file"
}
generate_web() {
local page
for page in src/*.md; do
if ! [ -f "$page" ]; then
continue
fi
build_markdown_page "$page"
done
local script
for script in src/*.md.bin; do
if ! [ -f "$script" -a -x "$script" ]; then
continue
fi
build_dynamic_markdown_page "$script"
done
for script in src/*.html.bin; do
if ! [ -f "$script" -a -x "$script" ]; then
continue
fi
build_dynamic_html_page "$script"
done
cp -R static/* public/
}
...
A náš skript pro generování tabulek table.md.bin
teď lze výrazně
zjednodušit.
...
echo '---'
echo 'title: Scoring table'
echo '---'
echo '# Scoring table'
preprocess_scores <scores.txt | sum_by_keys | sort -n -k 2 -r | as_markdown_table
Poslední vylepšení
Jako poslední cvičení rozšiřte náš skript tak, aby umožňoval práci s
opakovaně použitelnými skripty. Před spuštěním skriptů *.bin
bychom měli
rozšířit $PATH
o adresář bin/
v našem adresáři SSG.
Proč to chceme udělat? V tuto chvíli je cesta k bodovací tabulce napevno
vložená (hard-coded) uvnitř skriptu table.md.bin
a skript nelze použít pro
generování více tabulek (představte si, že bychom měli dvě oddělené skupiny
týmů). Pokud bychom ale měli tento skript v $PATH
pod jménem
score_table.sh
, mohli bychom ze souboru obsahujícího výsledky zápasů
udělat také “skript” s následujícím shebangem, a díky tomu použít
score_table.sh
pro zpracování více tabulek.
#!/usr/bin/env score_table.sh
alpha 2 : 0 bravo
bravo 0 : 1 charlie
alpha 5 : 4 charlie
Jistě, toto už hraničí se zneužitím shebangu, protože jsme ze souboru s daty udělali skript, ale mohou existovat i jiné případy použití než naše primitivní SSG, kde by takové rozšíření mělo smysl.
Zde si osvěžte paměť na env
, shebangy a $PATH
.
Řešení
Změny jsou ve skutečnosti triviální.
build_dynamic_html_page() {
...
env PATH="$PATH:$PWD/bin" "$input_file" >"$output_file"
}
build_dynamic_markdown_page() {
...
env PATH="$PATH:$PWD/bin" "$input_file" | pandoc_as_filter >"$output_file"
}
A soubor bin/score_table.sh
bude také upraven na jednom řádku.
grep -v '#' "$1" | preprocess_scores | sum_by_keys | sort -n -k 2 -r | as_markdown_table
Ze vstupu vypustíme všechny řádky obsahující #
, čímž se určitě vypustí
shebang, s tím že neočekáváme, že by název týmu mohl obsahovat znak #
(později si ukážeme regulární výrazy, které by umožnily přesnější
filtrování, ale zatím to stačí takhle).
Úlohy k ověření vašich znalostí
Očekáváme, že následující úlohy vyřešíte ještě před příchodem na cvičení, takže se budeme moci o vašich řešeních na cvičení pobavit.
Učební výstupy a kontrola po cvičení
Tato část podává zhuštěný souhrn základních konceptů a dovedností, které byste měli umět vysvětlit a/nebo použít po každém cvičení. Také obsahují absolutní minimum, které je potřebné pro pochopení navazujících cvičení (a dalších předmětů).
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 exit kódy umožňují řídit podmínky a cykly v shellových skriptech
-
vysvětlit pro konstrukci shellu
if true; then echo "true"; fi
, jaké příkazy se v ní provádějí a jakým způsobem se vyhodnocuje -
vysvětlit, jaká hlediska jsou důležitá při rozhodování mezi použitím shellu a Pythonu
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 …
-
používat v shellových skriptech řídicí struktury (
for
,while
,if
,case
) -
použít příkaz
read
-
používat
getopt
pro parsování argumentů příkazové řádky -
používat
.
asource
k načtení funkcí z různých souborů -
použít
scp
na kopírování souborů mezi místním a vzdáleným počítačem -
volitelné: použít
rsync
pro synchronizaci celých adresářů
Seznam změn na této stránce
-
2025-04-08: Aktualizace příkazu na nastavení AFS.
-
2023-04-14: Varování ohledně
--delete
ursync
.