Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11.

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ší. Naučíme se ale také, jak odhalit chyby v našich skriptech, aniž bychom je spustili.

Opět budeme průběžně používat jeden příklad, na kterém se naučíme nové konstrukce shellu, ale také se seznámíme s nástroji, které nám mohou pomoci odhalit chyby v našich programech, a podíváme se také na některé síťové nástroje.

Předstartovní kontrola

  • Umíte používat shellové proměnné a tzv. command substitution.
  • Pamatujete si, k čemu se používá Pandoc.
  • Nahráli jste váš veřejný klíč do jednoho ze souborů 05/key.[0-9].pub.

Čtení síťové konfigurace

Než se ponoříme do hlavního tématu, uděláme malou odbočku k jedné praktické věci, která se nám bude velmi hodit. A to jak zobrazit síťovou konfiguraci počítače z příkazového řádku.

V následujícím textu budeme předpokládat, že váš počítač je připojen k internetu (to se týká i virtualizované instalace systému Linux).

Základním příkazem pro nastavování a zobrazování konfigurace sítě je ip.

Prozatím je pro nás asi nejužitečnější ip addr.

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 54:e1:ad:9f:db:36 brd ff:ff:ff:ff:ff:ff
3: wlp58s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 44:03:2c:7f:0f:76 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.105/24 brd 192.168.0.255 scope global dynamic noprefixroute wlp58s0
       valid_lft 6209sec preferred_lft 6209sec
    inet6 fe80::9ba5:fc4b:96e1:f281/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
8: vboxnet0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff

Příkaz zobrazil seznam čtyř rozhraní (lo, enp0s31f6, wlp58s0 a vboxnet0), která jsou v počítači k dispozici. Váš seznam se bude lišit, stejně tak se mohou lišit jména rozhraní.

Název rozhraní naznačuje jeho typ.

  • lo je zařízení zpětné smyčky (loopback), a bude vždy přítomno. Pomocí něj můžete testovat síťové aplikace i bez “skutečného” připojení.
  • enp0s31f6 (často také eth*) je kabelový ethernet.
  • wlp58s0 je bezdrátový adaptér (wi-fi).
  • vboxnet0 je virtuální síťová karta, kterou VirtualBox používá při vytváření virtuální podsítě pro vaše virtuální počítače (pravděpodobně ji tam mít nebudete).

Pokud jste připojeni přes VPN, můžete vidět i rozhraní tun0.

Stav rozhraní (spuštěno – UP – nebo ne) je na stejném řádku jako název adaptéru.

Řádek začínající link/ obsahuje MAC adresu adaptéru. Řádky s inet udávají IP adresu přidělenou tomuto rozhraní včetně specifikace rozsahu sítě. V tomto příkladu má lo adresu 127.0.0.1/8 (samozřejmě), enp0s31f6 je bez adresy (state DOWN) a wlp58s0 má adresu 192.168.0.105/24 (tj. 192.168.0.105 se síťovou maskou 255.255.255.0).

Vaše adresy se budou mírně lišit, ale obvykle se také zobrazí privátní adresa (za NATem), protože se pravděpodobně připojujete přes směrovač (router) k vašemu poskytovateli internetu.

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 úložišti příkladů, ale jak vidíte v 08/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í.

Důrazně doporučujeme, abyste fragmenty zkopírovali z našeho repozitáře do svého (klidně použijte váš repozitář pro odevzdávání úkolů) a vytvořili revizi (commit) pro každou verzi aplikace. Použijte git z příkazového řádku a používejte dobré popisy commitů.

A když si pro každou část aplikace vytvoříte na Gitlabu samostatný úkol, který později uzavřete pomocí commit zprávy, procvičíte si také dobré praktiky softwarového inženýrství.

Modularizace skriptů a načítání konfigurace (. a source)

Dosud byly naše skripty vždy obsaženy v jediném souboru. Není divu: všechny byly poměrně krátké. Někdy však má smysl rozdělit kód do více souborů, aby bylo možné mít více skriptů, které budou některé části kódu sdílet.

To je snadné, pokud sdílený kód tvoří samostatný spustitelný skript. Představte si, že vytvoříme soubor s názvem msg.sh s následujícím obsahem:

#!/bin/bash

echo "$( date '+%Y-%m-%d %H:%M:%S |' )" "$@" >&2

V dalších skriptech pak můžeme volat skript msg.sh a pomocí něj vypisovat logovací zprávy.

...

./msg.sh "Starting computation..."
...
./msg.sh "Computation done."

To by mohlo fungovat dobře, ale kód je tak krátký, že by asi bylo vhodnější zapsat ho jako funkci, a asi bychom také chtěli mít více funkcí v jednom souboru. Zvláště u jednořádkových kódů je poněkud nepraktické mít pro každou “funkci” samostatný soubor.

Uděláme tedy funkci a uložíme ji do nového souboru logging.sh.


msg() {
    echo "$( date '+%Y-%m-%d %H:%M:%S |' )" "$@" >&2
}

Chceme-li takové funkce použít v jiném skriptu, musíme shellu pomocí konstrukce source nebo . (ano, samostatná tečka) říci, aby do skriptu začlenil náš soubor s funkcemi.

# Oba řádky jsou ekvivalentní, v reálu bychom použili jen jeden z nich
. logging.sh
source logging.sh

...

msg "Starting computation"

Proč by prosté volání našeho skriptu nefungovalo?Odpověď.

Existují i jiná použití příkazu source. Viděli jsme, že jej můžeme použít k načtení sdíleného kódu, ale velmi často se používá také k načtení konfigurace.

Představte si, že chceme uživateli umožnit definovat adresář, kam se mají ukládat vygenerované soubory.

results=/home/intro/results/

Jistě, můžeme se pokusit z tohoto souboru získat informace pomocí cut a grep, ale ve skutečnosti je mnohem jednodušší prostě načíst tento soubor pomocí source a pak rovnou přistupovat k $results.

Pravdou je, že uživateli dáváme mnohem více než obyčejný konfigurační soubor, ale nemusíme myslet na jeho konkrétní formát a pokročilí uživatelé si do konfiguračního skriptu mohou zahrnout další kouzla shellu, zatímco začátečníci ho mohou považovat za obyčejný soubor ve formátu promenna=hodnota.

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.

source se chová, jako kdyby místo řádku source byl skutečně vložen obsah zahrnovaného souboru. Bash nemá žádnou efektní podporu jmenných prostorů ani nic podobného.

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 tohoto skriptu.

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…

Než budete pokračovat, ujistěte se, že rozumíte, jak výše uvedený kód funguje. Měli byste zvládnout odpovědět na následující otázky:

  1. Proč není ssg.rc spustitelný (a proč nemá/nemusí být)?
  2. Proč je proměnná $site_title nastavená před vložením ssg.rc?
  3. Co by se stalo, pokud bychom vložili ssg.rc před volání mkdir -p public?

Ří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.

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 wildcards.

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í 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

Co vypíše následující kód, pokud předpokládáme, že neexistuje žádný *.txxt soubor v aktuálním adresáři?

for i in *.txxt; do
    echo "$i"
done

Nápověda.

Odpověď.

if a else

Podmínka if je v shellu trochu složitější.

Důležité je si zapamatovat, že podmínka je vždy příkaz, který se vykoná, a jehož výsledek (tzn. exit kód) určí, zda je podmínka splněná či ne.

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.

Mimochodem, když se zkusíte podívat do /usr/bin, zjistíte, že se tam opravdu nachází spustitelný soubor pojmenovaný [. Ale v Bashi (tedy v našem shellu) je [ implementovaný i jako vestavěný příkaz, takže je o něco rychlejší, než kdybychom ho spouštěli jako externí program.

Občas se můžete setkat i s následujícím kódem:

if [[ -d .git ]]; then
    echo "Jsme v korenovem adresari Git projektu."
fi

Tyto dvojité závorky [[ ... ]] jsou zase jiným konstruktem a úzce souvisí se syntaxí $(( ... )) pro aritmetické výrazy. Tuto podmínku vyhodnocuje přímo Bash. Její syntaxe je o něco mocnější, ale funguje pouze v nejnovějších verzích Bashe a pravděpodobně tedy nebude fungovat v jiných shellech.

Budeme používat pouze tradiční variantu s [.

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 další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 další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 ;;
    *) defaultni_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ěď.

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 getopt

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.

getopt

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 getopt, který nám s jejích zpracováním pomůže.

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.

Parsování voleb příkazového řádku není bohužel příliš standardizováno napříč různými verzemi unixu. Zde uvedený přístup funguje dobře na jakémkoli nejnovějším Linuxu a jeho užívání je komfortní. Není však přenositelný na jiné podobné systémy. Existuje příkaz getopts (ano, rozdíl je pouze v tom s navíc na konci), který je mnohem přenositelnější, ale má mnohem omezenější možnosti.

Hlavní argumenty řídící chování příkazu getopt jsou -o a -l, pomocí kterých specifikujeme popis přepínačů, které náš program bude přijímat.

Předpokládejme, že budeme chtít podporovat přepínač --verbose, která říká, aby byl náš skript výřečnější, a volbu --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. Zbylé argumenty skriptu (které nejsou přepínač) 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 jménem volby značí, že bude očekávat argument.

Za to přidáme -- následované vlastními argumenty. Vyzkoušejte si to:

getopt -o "vho:" -l "verbose,version,help,output:" -- --help input1.txt --output=file.txt
# prints: --help --output 'file.txt' -- 'input1.txt'
getopt -o "vho:" -l "verbose,version,help,output:" -- --help --verbose -o out.txt input2.txt
# prints: --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. tak, 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
            ;;
        --version)
            echo "$0 version 1.0.0"
            exit 0
            ;;
        --)
            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 logické hodnoty, ale lze je jako takové použít. Vzpomeňte si, jak jsme je používali na cvičení 06 (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. Proměnná "$@" se voláním shift také upraví. Celý cyklus tedy zpracuje všechny volby, dokud nenarazí na argument --, který odděluje volby od ostatních argumentů. Následný cyklus for poté iteruje jen přes zbylé argumenty. Oddělovač -- není v argumentech od uživatele vyžadován a getopt ho případně doplní (podívejte se do výstupu kódu výše), tudíž smyčka for iteruje přes zbylé argumenty.

Pokračování průběžného příkladu

Upravíme náš skript tak, aby akceptoval přepínač -w nebo --watch, 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.

Do našeho obrazu Linuxu jsme zapomněli zahrnout potřebný program inotifywait. Spusťte následující příkaz (nejprve se vás zeptá na heslo) a nainstalujte tento program do systému Fedora.

sudo dnf install -y inotify-tools

Nejprve skript změníme, aby používal getopt, a poté přidáme podporu pro --watch.

#!/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"
    $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/
}

opts_short="vwh"
opts_long="verbose,version,help,watch"

getopt -Q -o "$opts_short" -l "$opts_long" -- "$@" || exit 1
eval set -- "$( getopt -o "$opts_short" -l "$opts_long" -- "$@" )"

LOGGER=:
watch_for_changes=false

while [ $# -gt 0 ]; do
    case "$1" in
        -h|--help)
            echo "Usage: $0 ..."
            exit 0
            ;;
        -v|--verbose)
            LOGGER=msg
            ;;
        -w|--watch)
            watch_for_changes=true
            ;;
        --)
            ;;
        *)
            echo "Unknown option $1" >&2
            exit 1
            ;;
    esac
    shift
done


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 --watch 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 --watch, 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 se 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/bash

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

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

Je otázkou názoru, zda by tento úkol nebyl lépe řešitelný v jiném programovacím jazyce. Vše závisí na kontextu a dalších požadavcích.

Shell obvykle vyniká v situacích, kdy potřebujeme kombinovat data z více souborů, které jsou v nějakém textovém (nejlépe řádkovém) formátu.

Výhodou shellu je jeho interaktivita. Dokonce i funkce v něm lze definovat interaktivně (tj. neukládat je nejprve do žádného souboru). Výslednou pipeline můžeme snadno sestavovat postupně a po přidání každého kroku průběžně kontrolovat výstup.

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 read
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

Nedostatky programu SCP

Pro ty, kterým záleží na bezpečnosti, bychom měli poznamenat, že protokol SCP má některé bezpečnostní chyby. Ty lze využít k napadení místního počítače v případě, že se připojí ke škodlivému serveru.

SCP je ve skutečnosti velmi starý protokol, na kterém je vidět jeho stáří. Mezi lepší náhrady patří SFTP (pozor, liší se od FTPS – FTP přes SSL/TLS) a Rsync.

Více informací o tomto tématu naleznete na LWN.net.

Rsync

Mnohem výkonně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 (to 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/

Pokračování průběžného příkladu

Použijte manuálovou stránku rsync a rozšiřte průběžný příklad o přepínač --upload, se kterým skript nahraje vygenerované soubory na vzdálený server zadaný pomocí $rsync_target.

Jako rozumný cíl pro nahrávání do počítačů v laboratoři Rotunda můžete použít LOGIN@u-plNNNN.ms.mff.cuni.cz:WWW/nswi177.

Řešení.

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.

Klidně se k tomuto úkolu vraťte později: je v pohodě ho teď přeskočit a přejít na další část :-).

Ř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).

Lintování zdrojového kódu pomocí nástroje ShellCheck

Už jste napsali poměrně hodně shellových skriptů. Je tedy čas představit vám ShellCheck.

ShellCheck je nástroj, který kontroluje skripty shellu a hledá v nich časté problémy. Tyto problémy nejsou syntaktické ani logické chyby. Problémy, na které ShellCheck upozorňuje, jsou vzory, o kterých je dobře známo, že způsobují neočekávané chování, snižují výkon nebo dokonce mohou skrývat nějaká nepříjemná překvapení.

Jedním takovým příkladem může být, když váš skript obsahuje následující kus kódu.

cat input.txt | cut -d: -f 3

Víte, co by mohlo být špatně?

Technicky je tento kód správně a sám o sobě neobsahuje chybu. Použití příkazu cat je ale redundantní, protože vypisuje jen jeden soubor – kód by se dal zjednodušit do následujícího tvaru beze změny funkčnosti:

cut -d: -f 3 <input.txt

Jak vidíte, v zásadě nejde o nic škodlivého.

Může to ale znamenat, že jste chtěli spojit více souborů, ale některé vám vypadly, nebo je cat jen pozůstatek nějaké předchozí verze skriptu. Proto vás ShellCheck varuje.

Jiný problém, se kterým ShellCheck může pomoci, je následující kód:

dir_name=results/
if [ -d $dri_name ]; then
    echo "$dir_name already exists."
fi

Tady ShellCheck detekuje překlep, protože do proměnné dri_name nebylo předem nic přiřazeno.

Další nástraha čeká v následujícím kódu:

if [ -d ]; then
    echo "$dir_name already exists."
fi

Tohle je samozřejmě úplně špatně. Ale co myslíte – test (nebo [) to příjme a vyhodnotí jako true. Vypadá to zvláštně, ale test s přesně jedním argumentem ověřuje, jestli je argument neprázdný.

Dnes už pro tohle máme test -n, ale dříve jsme neměli a musíme zachovat zpětnou kompatibilitu. Vizte tuto stránku pro podrobnosti.

Je to tedy korektní kus shellového kódu, ale nejspíš nedělá to, co jsme chtěli. Zde přichází ShellCheck, aby nám pomohl.

ShellCheck umí varovat před stovkami možných problémů, jak lze vidět na této stránce. Zvykněte si jej běžně spouštět na svých shellových skriptech.

Z naší zkušenosti ShellCheck zřídkakdy dává falešně pozitivní výsledky (false positives – případy, kdy nástroj hlásí chybu, ale kód je v pořádku), ale mnohokrát nás zachránil.

Některé z hodnocených úloh, které odevzdáte, budou kontrolovány i ShellCheckem (a můžeme penalizovat vaše řešení, pokud obsahuje ShellCheckové chyby).

Spuštění kontroly Shellcheck

Spuštění Shellchecku je opravdu snadné.

shellcheck ssg.sh

Pokud chcete zobrazit i připomínky ke stylu kódu, přidejte -o all nebo použijte -i pro selektivnější kontrolu.

shellcheck -o all ssg.sh

Cvičení

Vraťte se k shellovým skriptům, které jste odevzdali, a spusťte na nich nástroj ShellCheck. Opravte všechny nalezené chyby, nebo zdůvodněte, proč je jejich ponechání v pořádku.

Ostatní jazyky

Podobné nástroje existují i pro jiné jazyky.

Pylint je takový nástroj pro Python, který dokáže odhalit spoustu problémů a je také velmi dobře přizpůsobitelný.

Jako cvičení si najděte takové nástroje pro váš zvolený jazyk a začněte je pravidelně používat. Mnoho nástrojů obsahuje také rozšíření IDE pro lepší uživatelský komfort.

Co si odnést

Začněte používat ShellCheck, Pylint, případně libovolné další nástroje pro váš oblíbený jazyk.

Neodhalí se tím logické chyby (nebo aspoň ne všechny), ale určitě se odhalí tzv. code smells: místa v kódu, která často vedou k chybám, nedefinovanému chování, nebo jiným problémům.

Tohle je dvakrát tak důležité, učíte-li se nějaký nový jazyk: je tu větší šance, že jste si z programovacího jazyka něco špatně vyložili, spíše než že by byla chyba v nástroji.

Ú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.

Automatické testy zveřejníme později.

Automatické testy jsou již k dispozici.

Program convert z ImageMagick umí převádět obrázky mezi různými formáty pomocí convert source.png target.jpg (s téměř libovolnými příponami souborů).

Převeďte všechny obrázky PNG (s příponou .png) v aktuálním adresáři na JPEG (přípona .jpg).

Řešení.

Rozšiřte předchozí příklad tak, aby nedocházelo k přepsání existujících souborů.

Řešení.

Poslední příklad s ImageMagickem. Pomocí -resize 800x600 lze změnit velikost obrázku tak, aby se vešel do zadané obálky.

Vytvořte nástroj, který vytvoří náhledy ze souborů zadaných na příkazovém řádku, přičemž změní název souboru z dir/filename.ext na dir/filename.thumb.ext.

Řešení.

Vytvořte funkci factorial, která vypočítá faktoriál daného čísla.

Řešení.

Rozšiřte náš skript bodovací tabulky z průběžného příkladu tak, aby správně řešil situace, kdy více týmů má stejný počet bodů a my potřebujeme rozlišit jejich pořadí podle počtu vstřelených branek (což je poměrně časté pravidlo v mnoha turnajích).

Z následujících údajů bychom proto rádi vypočítali poněkud odlišnou tabulku.

alpha 2 : 0 bravo
bravo 7 : 1 charlie
alpha 1 : 4 charlie
| Team | Points | Goals |
| ---- | -----: | ----: |
| bravo | 3 | 7 |
| charlie | 3 | 5 |
| alpha | 3 | 3 |

Řešení.

Napište shellový skript pro kreslené sloupcového (řádkového) grafu. Uživatel zadá data v následujícím formátu:

12  First label
120 Second label
1 Third label

Skript vytiskne pak takovýhle graf:

First label (12)   | #
Second label (120) | #######
Third label (1)    |

Skript dostane soubor s daty jako první argument a šířku určí podle velikosti obrazovky. Také zarovná popisky jak je vidět v grafu výše.

Můžete předpokládat, že vstupní soubor bude vždy existovat a můžete ho několikrát číst. Žádné další argumenty není třeba řešit.

Nápověda

Šířka obrazovky je uložena v proměnné $COLUMNS. Předpokládejte hodnotu 80 pokud není nastavena. (Můžete předpokládat, že bude buď prázdná (nenastavená) nebo bude obsahovat platné číslo).

Graf bude nakreslen tak, aby vyplnil celou šířku obrazovky (tj. zvětšen/zmenšen).

Můžete slít více mezer do jedné (i pro popisky), první a druhý sloupeček je oddělen mezerou (mezerami).

Podívejte se, co dělá wc -L.

Všimněte si, že první testy používají popisky stejné délky, abyste mohli lépe odstartovat vaši implementaci.

Zvažte použití printf pro tištění zarovnaných popisků.

Následující zajistí, že bc bude počítat s racionálními čísly ale zobrazí výsledek jako integer (což se hodí pro složitější shellové výpočty).

echo 'scale=0; (5 * 2.45) / 1' | bc -l

Příklady

2 Alpha
4 Bravo
# COLUMNS=20
Alpha (2) | ####
Bravo (4) | ########
2 Alpha
4 Bravo
16 Delta
# COLUMNS=37
Alpha (2)  | ###
Bravo (4)  | ######
Delta (16) | ########################

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 08/barplot.sh a commitněte ho (pushněte) do GitLabu.

Napište skript pro výpis velikosti souborů.

Skript bude částečně imitovat chování ls: bez argumentů vypíše informace o souborech v aktuálním adresáři, argumenty bude brát jako seznam souborů, o kterých má zjišťovat více.

Příklad spuštění může vypadat takto:

./08/dir.sh /dev/random 08/dir.sh 08
/dev/random  <special>
08/dir.sh          312
08               <dir>

Druhý sloupeček zobrazí velikost souboru pro normální soubory, <dir> pro adresáře a <special> pro jiné soubory. Velikost souboru lze získat třeba pomocí programu stat(1).

Neexistující soubory budou hlášeny jako SOUBOR: no such file or directory. na stderr.

Můžete předpokládat, že máte přístup ke všem souborům vyjmenovaným na příkazové řádce.

Asi se vám bude hodit prográmek column, především následující spuštění:

column --table --table-noheadings --table-columns FILENAME,SIZE --table-right SIZE

Můžete také předpokládat, že názvy souborů budou rozumné (bez mezer). Pro zjednodušení nebudeme kontrolovat, že návratový kód je nenulový, pokud některý soubor neexistuje.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 08/dir.sh a commitněte ho (pushněte) do GitLabu.

ping je nástroj, který odesílá pakety ICMP a často se používá jako základní test, zda je vzdálený počítač v provozu. Pravdou je, že stroj se může rozhodnout filtrovat požadavky ICMP a vůbec na ně neodpovídat (a proto se chová jako vypnutý) a naopak, stroj reagující na ping může mít všechny ostatní služby nefunkční. Přesto je to užitečný nástroj pro kontrolu a ladění základních problémů s připojením.

Zkuste spustit ping d3s.mff.cuni.cz a podívejte se na jeho výstup. Nástroj odesílá pakety donekonečna, ukončete jej tedy pomocí Ctrl-C.

Vaším úkolem je vytvořit nástroj, který bere následující argumenty a vypíše stav počítače na základě výstupu ping (samozřejmě je třeba použít ping v režimu, kdy odesílá pouze jeden požadavek a má krátký timeout).

  • -d nebo --delimiter, který akceptuje řetězec používaný k oddělení sloupců na výstupu, výchozí hodnota je mezera
  • -v nebo --verbose, kdy se výstup příkazu ping vypíše na standardní chybový výstup (ve výchozím nastavení se výstup pingu neobjeví vůbec)
  • -w pro zadání jiného časového limitu, než je výchozí jedna sekunda

Obyčejnými parametry jsou DNS jméno nebo IP adresy, které se mají kontaktovat prostřednictvím ping a u kterých vypíšete jejich stav.

Exit kód označuje množství strojů ve stavu DOWN (můžete předpokládat, že nikdy nebude více než 126 parametrů a nemusíte tedy řešit, zda je ukončovací kód signed nebo unsigned byte atd.).

Očekáváme, že použijete getopt pro zpracování voleb na příkazové řádce.

Následující příklady ukazují volání s různými parametry a očekávaný výstup.

Výchozí spuštění

08/ping.sh seznam.cz google.com google.comx
seznam.cz UP
google.com UP
google.comx DOWN

Použití -d a --verbose

08/ping.sh seznam.cz -d : google.com --verbose

Poznamenejme, že výstup obsahuje jak stdout, tak stderr.

PING seznam.cz (77.75.77.222) 56(84) bytes of data.
64 bytes from www.seznam.cz (77.75.77.222): icmp_seq=1 ttl=56 time=4.46 ms

--- seznam.cz ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 4.460/4.460/4.460/0.000 ms
seznam.cz:UP
PING google.com (142.251.36.78) 56(84) bytes of data.
64 bytes from prg03s10-in-f14.1e100.net (142.251.36.78): icmp_seq=1 ttl=114 time=3.64 ms

--- google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.642/3.642/3.642/0.000 ms
google.com:UP

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 08/ping.sh a commitněte ho (pushněte) do GitLabu.

Učební výstupy

Učební výstupy podávají 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, co je to kontrola stylu a linter

  • vysvětlit, jaké problémy mohou odhalit nástroje pro kontrolu stylu

  • vysvětlit problémy se souběžností, které mohou nastat při používání dočasných souborů

  • 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 bezpečně v shellových skriptech dočasné soubory

  • 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 . a source k načtení funkcí z různých souborů

  • používat a interpretovat výsledky ShellChecku

  • 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

  • 2024-04-04: Zveřejněny automatické testy.