Lukáš Ondráček
Katedra teoretické informatiky a matematické logiky
ondracek@ktiml.mff.cuni.cz
Pracovna S207 (Malá Strana, 2. patro)

Cvičení z Úvodu do UNIXu, pátek 12:20

V letním semestru 2018/2019 vedu cvičení k předmětu NSWI095, Úvod do UNIXu.

Zápočet je možné získat splněním 2/3 domácích úkolů. Ty budou zadávány většinou po každém cvičení a bude je možné odevzdávat do začátku cvičení následujícího. Řešení posílejte jako textový soubor emailem.

Předmět emailů souvisejících s tímto cvičením prosím uvozujte textem [unix], (např. [unix] 1. úkol).

Zadání příkladů uvedená v závorce jsou bonusová a předpokládám, že většina lidí se k nim na cvičení nedostane. Naopak nestihnete-li na cvičení nějaký z neuzávorkovaných příkladů, doporučuji vyzkoušet si jej doma. Připadá-li vám, že je příkladů na cvičení příliš, dejte mi vědět; můžeme je probrat individuálně, příp. jich více můžu označovat za bonusové.

Pro vzdálený přístup k UNIXovým počítačům v Rotundě můžete použít příkaz ssh uzivatel@u-pl3.ms.mff.cuni.cz, kde místo uzivatel uvedete svoje přihlašovací jméno a místo u-pl3 příp. název jiného počítače v labu. Na Windows by ke stejnému účelu měla sloužit aplikace PuTTY.

Máte-li jakékoliv dotazy, neváhejte se ozvat emailem nebo zeptat na cvičení.

Domácí úkoly

  1. Vytvořte jedním příkazem složky a1, a1/b, a1/b/c, a1/d. Vytvořte příkazem cp kopii složky a1 pod názvem a2. Vše poté jedním příkazem smažte. Pokuste se najít co nejkratší řešení. (do cvičení 1. 3.)
  2. Vytvořte skript backup.sh, který zazálohuje aktuální adresář. Skript zjistí úplnou cestu k aktuálnímu adresáři, nahradí v ní lomítka za pomlčky a pod tímto názvem složku zkopíruje do složky ~/.backup/. Pokud zde již taková složka existuje, nejprve ji přejmenuje přidáním pořadového čísla. Pří kopírování zachovejte atributy souborů (čas, práva, ...). Použijte příkazy pwd, tr a příkaz cp s přepínačem pro přejmenování a číslování existujících souborů v Linuxové verzi příkazu. Po spuštění tohoto příkazu ve složce /tmp/a tedy dojde k jejímu zkopírování pod názvem ~/.backup/-tmp-a. Tento skript zatím není možné spouštět v domovském adresáři, protože by se pokusil zazálohovat i předchozí zálohy. (do 8. 3.)
    Oprava: Nejprve aktuální adresář zkopírujte pod názvem ~/.backup/tmp, poté této kopii změnte název na požadovaný příkazem mv s oním zálohovacím parametrem. (Ukázalo se, že se zálohování příkazem cp chová trochu jinak než jsem předpokládal.)
  3. Napište skript realUserNames.sh, který ze souboru /etc/passwd nebo výstupu příkazu getent passwd získá skutečná jména uživatelů a jejich přihlašovací jména a vypíše je ve tvaru Pepa Novak:novakp abecedně setříděné. Budete potřebovat příkazy cut, paste, sort a mktemp pro vytváření dočasných souborů, všechny dočasné soubory na konci smažte. Řešení bez použití dočasných souborů je také vítano. (do 15. 3.)
  4. Přepište skript backup.sh pomocí POSIXových prostředků tak, aby bral jako parametry názvy souborů k zálohování. Zavoláním backup.sh . tedy docílíme podobné funkce jako u předchozí verze, nově ale bude možné zálohovat i běžné soubory, nejenom složky. Bude-li skriptu předáno více parametrů, měl by se chovat stejně, jako by byl spuštěn postupně s každým z těchto parametrů; nebude-li mu předán žádný parametr, měl by vypsat návod k použití a informaci, kam se vlastně zálohuje. Číslování souborů tentokrát vyřešte pomocí řídících konstrukcí shellu (while, if, for, ...). Kam přesně a pod jakými jmény se mají zálohované soubory kopírovat si zvolte sami, aby pro vás bylo používání skriptu pohodlné; jen případné odchylky od předchozí verze popište v těle emailu, ať vím, co je záměr a co chyba. (Mít v adresáři soubory začínající pomlčkou může působit problémy. Také vám nemuselo vyhovovat, že je složka .backup skrytá. Možná by už první vytvořený soubor mohl mít číslo. Nebo by se třeba místo pomlček mohly používat jiné znaky, příp. mezery by se daly taky něčím nahrazovat. Přidaná čísla by mohla mít vždy třeba 3 číslice (viz příkaz printf). Jakákoliv rozšíření jsou vítaná.) (do 22. 3.)
    Doplnění: Pro inkrementování čísla v proměnné můžete použít aritmetickou expanzi promenna=$((promenna+1)).
  5. Napište skript unpack.sh, který dostane jako jediný parametr název archivu (tar, zip, tar.gz, rar, 7z, ...) a zavolá příslušný příkaz k jeho rozbalení. Pokud by rozbalením mělo vzniknout více souborů, uloží je do nové složky v aktuálním adresáři; má-li vzniknout jen jeden soubor/složka uloží jej přímo do aktuálního adresáře. Vyberte si aspoň tři typy archivů, pro něž bude skript fungovat; k rozpoznání typu souboru použijte file a k větvení case. (do 29. 3.)
  6. Napište v sedu skript rev, který vypíše každý řádek pozpátku. Napište v sedu skript tac, který vypíše řádky vstupu v opačném pořadí. (do 5. 4.)
  7. Napište v sedu skript isPalindrome, který zjistí, zda je text na řádku palindromem, a napíše před něj ANO v kladném případě a NE v záporném. Palindrom je text, který se čte z obou stran stejně. Před odevzdáním vyzkoušejte na vstupech abccba, abcba a abcda. (do 12. 4.)
  8. Napište skript postfixCalc.sh, který bude fungovat jako jednoduchá kalkulačka s postfixovou notací. Kalkulačka postupně vyhodnocuje jednotlivá slova ve výrazu: Je-li slovem číslo, přidá jej na zásobník; je-li slovem operace (např. sečtení), odebere potřebný počet operandů ze zásobníku (zde dva), provede s nimi danou operaci a výsledek opět vloží na zásobník. Pořadí operandů operátoru zůstává stejné, jako bylo při vkládání na zásobník (tedy opačné, než v jakém je ze zásobníku odebíráme). Při řešení se snažte upřednostňovat konstrukce shellu, např. sed a awk zde tedy nebudete potřebovat. Podrobné řešení vám pošlu emailem, je potřeba jej přesně dodržet; ozvěte se, pokud by email nedošel. (do 26. 4.)
  9. Vytvořte skript logger.sh, který bude v exponenciálně se zvětšujících intervalech (počínaje dvěma sekundami) vypisovat informaci o tom, že váš skript stále běží. Formát výstupu bude datum uživatel: hláška, kde datum je výstup příkazu date bez parametrů, uživatel výstup příkazu whoami a hláška náhodná hláška z vámi zvoleného seznamu (čím delší seznam, tím lépe; spočítejte si, kolik řádků bude vypsáno za jakou dobu). Skript spouštějte na u-pl16, můžete z něj volat skript /tmp/unix/randomLine.sh, který vypíše náhodný řádek ze svého vstupu, -- přečtěte si příklad použití na začátku tohoto souboru. Skript by tedy po spuštění měl vyčkat 2s, vypsat některou hlášku, vyčkat 4s, vypsat hlášku, vyčkat 8s, ... Důkladně otestujte, že skript funguje. Poté skript upravte, aby namísto standardního výstupu připisoval řádky do souboru /tmp/unix/log.txt. Využijte k tomu přesměrování výstupu příkazu echo, který řádky vypisuje, -- nepřesměrovávejte výstup celého skriptu. Skript spusťte na u-pl16 v aplikaci tmux, tu ukončete bez ukončení aktuální relace (skript bude stále běžet). Zkontrolujte v obsahu souboru log.txt, že váš skript i skripty ostatních fungují korektně. Skript tentokrát není potřeba posílat emailem, úkol budu počítat za splněný, jakmile budete mít v souboru log.txt na u-pl16 aspoň 10 řádků. Skript nechte běžet až do následujícího cvičení a mějte jej zálohovaný pro případ, že by došlo ke smazání log.txt a úkol jsem musel vyhodnocovat jinak. (do 3. 5.)
  10. Vytvořte v AWK skript htmlStructure, který dostane soubor html a má strukturovaně vypsat jeho obsah. Skript bude postupně zpracovávat jednotlivé tagy. Každý řádek výstupu bude začínat dvojnásobným počtem mezer, než je aktuální hloubka zanoření, poté bude následovat název tagu velkými písmeny a jeho atributy; alternativně může po bloku mezer následovat dvojtečka a text, který se nachází mezi tagy, neobsahuje-li jen bílé znaky, v tomto textu nahraďte posloupnosti bílých znaků jednou mezerou. Můžete předpokládat, že jde o XHTML dokument, každý tag má tedy buď uzavírací tag (<h1></h1>), nebo obsahuje uzavírací lomítko (<br/>). Příklad vstupu a výstupu naleznete zde. (prodlouženo, do 17. 5.)
  11. Napište skript uniqFiles na rozpoznávání duplicitních souborů. Skript bude číst ze vstupu seznam souborů (jeden soubor na řádek), a pokud soubor s daným obsahem ještě neviděl, vypíše tento řádek na výstup. Z duplicitních souborů se tedy vždy vypíše ten, který byl na vstupu jako první. Předpokládejte, že soubory můžou být velké, na uložení jejich obsahu do proměnné není paměť a na jejich opakované čtení čas. Možný postup: Použijte sha256sum na vytvoření kontrolních součtů souborů (stejné soubory budou mít stejný, různé velmi pravděpodobně různý); součty použijte jako klíče asociativního pole v AWK a kontrolujte, jestli již v poli existují nebo ne. (do 30. 6.)
  12. Vytvořte skript pipeChat, který dostane jako parametr název místnosti a bude komunikovat s druhou instancí přes pojmenované roury /tmp/pipeChat.NAZEV.1 a /tmp/pipeChat.NAZEV.2. Pokud tyto soubory neexistují, vytvoří je (a při ukončení smaže) a bude používat jeden pro čtení a druhý pro zápis; pokud soubory již existují bude je využívat pro zápis a čtení v opačném pořadí. Po spuštění dvou instancí skriptu se stejným názvem místnosti se tedy text psaný do jedné z nich bude objevovat ve druhé a naopak. (do 30. 6.)

1. cvičení (22. 2. 2019)

  1. Použijte příkaz man a seznamte se s příkazy, které budeme potřebovat (echo, ls, cd, mkdir, rmdir, touch, rm). Zobrazit řešení
        man prikaz zobrazí manuálovou stránku k příkazu prikaz na aktuálním systému,
        man 1p prikaz na Linuxu zobrazí manuálovou stránku POSIXové verze příkazu prikaz.
  2. Vypište obsah domovského adresáře, poté znova včetně skrytých souborů. Zobrazit řešení
        ls
        ls -a / ls -A
  3. Vypište podrobný obsah adresáře /etc. Zobrazit řešení
        ls -l /etc
  4. Vytvořte v domovském adresáři složku a a přejděte do ní příkazem cd. Zobrazit řešení
        mkdir a
        cd a
  5. Vytvořte jedním příkazem složky b a b/c. Zobrazit řešení
        mkdir -p b/c
  6. Vytvořte soubor b/--help, přejděte do složky b a soubor smažte. Zobrazit řešení
        touch b/--help
        cd b
        rm -- --help (vše za parametrem -- se interpretuje jako soubor, bez něj by se vypsala nápověda)
  7. Vraťte se o úroveň výš (do složky ~/a) a příkazem touch vytvořte soubory textovy soubor.txt a *. Zobrazit řešení
        cd ..
        touch "textovy soubor.txt" "*" / touch textovy\ soubor.txt \*
  8. Smažte soubor * a ověřte příkazem ls, že nebylo smazáno nic jiného. Zobrazit řešení
        rm "*" (bez uvozovek by se hvezdička nahradila seznamem všech neskrytých souborů)
        ls
  9. Vytvořte soubor .skryty.txt a vypište všechny soubory s příponou .txt. Zobrazit řešení
        touch .skryty.txt
        ls *.txt .*.txt (hvězdička se neexpanduje na skryté soubory)
        (ls -a *.txt nefunguje, protože ls od shellu dostane celý seznam neskrytých souborů a -a už nic neovlivní)
  10. Smažte jedním příkazem vše v aktuálním adresáři (~/a). Zobrazit řešení
        rm -r * .*
  11. Přejděte o úroveň výš a smažte příkazem rmdir adresář a. Zobrazit řešení
        cd ..
        rmdir a
  12. (Projděte si základní ovládání textového editoru vi pomocí příkazu vimtutor.)

2. cvičení (1. 3. 2019)

  1. Vypište příkazem cat obsah souboru /etc/passwd. Zobrazit řešení
        cat /etc/passwd / cat < /etc/passwd
  2. Vytvořte soubor prvni.txt, který bude obsahovat text Toto je první soubor., použijte příkaz echo. Zobrazit řešení
        echo "Toto je první soubor." > prvni.txt
  3. Vytvořte soubor druhy.txt, který bude obsahovat libovolný víceřádkový text, použijte příkaz cat. Zobrazit řešení
        cat << DomluvilJsem > druhy.txt
        první řádek
        druhý řádek
        DomluvilJsem
  4. Přejmenujte soubor druhy.txt na prvni.txt, čímž se tento smaže. Zkontrolujte obsah souboru prvni.txt. Zobrazit řešení
        mv druhy.txt prvni.txt
        cat prvni.txt
  5. Vytvořte soubor datum.txt s textem Datum a čas: a připište do něj příkazem date aktuální datum a čas. Zobrazit řešení
        echo "Datum a čas:" > datum.txt (Vytváříme nový soubor / přepisujeme existující.)
        date >> datum.txt (Připisujeme na konec souboru.)
  6. Zobrazte příkazem less výstup příkazu getent passwd. Zobrazit řešení
        getent passwd | less
        Příkaz less nedostal žádné parametry, bude tedy číst standardní vstup, kde najde výstup příkazu getent.
        Příkaz getent passwd vrací na Linuxu obsah /etc/passwd rozšířený o záznamy z jiných zdrojů;
        používejte v labu, ne u písemky.
  7. Vytvořte soubor jehož název bude stejný jako přihlašovací jméno aktuálního uživatele, použitje příkaz whoami. Zobrazit řešení
        touch "$(whoami)" (Bez uvozovek by se $(...) mohlo vyexpandovat na více parametrů touch.)
        touch "`whoami`"
        Obě verze mají stejný význam:
            Provede se příkaz whoami a jeho výstup se vloží namísto $(...) nebo `...`.
            Provede se příkaz touch s novým parametrem.
  8. Projděte si základní ovládání textového editoru vi pomocí příkazu vimtutor. Zde aspoň první lekci, doporučuji ale projít později i zbytek.
  9. Vytvořte skript hello.sh, který vypíše příkazem echo text Hello world, nastavte mu potřebná práva příkazem chmod a spusťte jej jako ./hello.sh. Zobrazit řešení
        Otevřeme soubor příkazem vim hello.sh a napíšeme do něj (po stisku i):
            #!/bin/bash
            echo "Hello world"
        Stiskem Esc se vrátíme do příkazového režimu a příkazem :wq soubor uložíme a zavřeme.
        Nastavíme skriptu právo ke spouštění příkazem chmod +x hello.sh.
        Spustíme skript příkazem ./hello.sh.
  10. Vytvořte skript shellTest.sh, který vypíše text test expanze -{A,B}{C,D}-: -{A,B}{C,D}- v POSIXovém shellu a test expanze -{A,B}{C,D}-: -AC- -AD- -BC- -BD- v bashi. Vyzkoušejte za #! různé shelly: /bin/sh, /bin/bash, /bin/dash. Zobrazit řešení
        Vytvoříme (pomocí vimu) soubor shellTest.sh s obsahem:
            #!/bin/bash
            echo "test expanze -{A,B}{C,D}-:" -{A,B}{C,D}-
        Nastavíme práva: chmod +x shellTest.sh
        Spustíme pomocí ./shellTest.sh, výstup:
            test expanze -{A,B}{C,D}-: -AC- -AD- -BC- -BD-
        Upravíme první řádek na #!/bin/dash a spustíme, výstup:
            test expanze -{A,B}{C,D}-: -{A,B}{C,D}-
        Alternativně můžeme spustit pomocí bash shellTest.sh nebo dash shellTest.sh,
        v tom případě se první řádek ignoruje a použuje se shell, který jsme zavolali.
        Na Linuxu je obvykle k dispozici POSIXový dash, o mnoho funkcí rozšířený bash
        a sh, který se bude chovat jako jeden z nich.
        V přenositelných aplikacích bychom měli používat sh
        a psát skripty tak, aby je korektně interpretoval bash i dash.
        Pozor na to, že i při použití POSIXového shellu jsou k dispozici nePOSIXové verze příkazů.
  11. (Vytvořte skript favouriteShells.sh, který ze seznamu uživatelů passwd získá poslední sloupec s výchozími shelly a vypíše sestupně řazený seznam, kde na každém řádku bude počet výskytů shellu a cesta k němu -- např. 12 /bin/bash. Použijte příkazy sort, cut a uniq.) Zobrazit řešení
        getent passwd | cut -d: -f7 | sort | uniq -c | sort -nr (jen pro Linux)
        cat /etc/passwd | cut -d: -f7 | sort | uniq -c | sort -nr (přenositelně)

3. cvičení (8. 3. 2019)

  1. Připojte se k počítači u-pl3 v Rotundě příkazem ssh u-pl3 ze školního počítače nebo příkazem ssh uzivatel@u-pl3.ms.mff.cuni.cz z vlastního. Připište do souboru /tmp/unix/users.txt své uživatelské jméno a volitelně za mezeru další informace (vše na stejný řádek). Podívejte se na ostatní jména v seznamu. Pošlete někomu zprávu příkazem write. Zůstaňte přihlášeni pro příjem zpráv od ostatních. Jste-li ke vzdálenému počítači přihlášeni ve více oknech můžete v některých vypnout příjem zpráv příkazem mesg n. Pro průběžné sledování změn v souboru users.txt lze využít příkaz watch.
  2. Vytvořte si na svém počítači soubor /tmp/unix/users.txt s několika fiktivními uživatelskými jmény. Vytvořte skript broadcast.sh, který si nejprve uloží vše ze standardního vstupu do proměnné a poté pro každý řádek souboru /tmp/unix/users.txt vypíše obsah proměnné a na další řádek text write uzivatel, kde uzivatel je jméno uživatele ze souboru. Skript dobře otestujte. Mj. je potřeba, aby ignoroval veškeré další informace za uživatelským jménem až do konce řádku. Zobrazit řešení
        Skript broadcast.sh:
            #!/bin/bash
            vstup="$(cat)"
            while read uzivatel zbytek; do (proměnnou $zbytek nepotřebujeme, ale zařídí pohlcení ostatních slov na řádku)
                echo "$vstup"
                echo write "$uzivatel"
            done < /tmp/unix/users.txt (přesměrování vstupu přímo u read by četlo stále 1. řádek)
        Příklad souboru /tmp/unix/users.txt:
            novop Pepa Novotny
            user123
        Výstup příkazu echo "Zprava pro vsechny" | ./broadcast.sh:
            Zprava pro vsechny
            write novop
            Zprava pro vsechny
            write user123
  3. Pokud předchozí skript funguje upravte ho, aby namísto vypisování provedl příkaz write uzivatel a na jeho vstup mu předal text z proměnné. Skript by tedy měl poslat zadanou zprávu všem uživatelům v souboru /tmp/unix/users.txt. Vyzkoušejte jej na počítači u-pl3. Nepoužíváte-li školní počítače se sdílenou domovskou složkou, bude potřeba skript nejprve poslat na cílový počítač příkazem scp soubor uzivatel@u-pl3.ms.mff.cuni.cz:.. Zobrazit řešení
        Předchozí skript broadcast.sh po drobné úpravě:
            #!/bin/bash
            vstup="$(cat)"
            while read uzivatel zbytek; do
                echo "$vstup" | (za znakem | může být zalomený řádek)
                write "$uzivatel"
            done < /tmp/unix/users.txt

4. cvičení (15. 3. 2019)

  1. Napište skript submatrix.sh, který dostane 5 parametrů: oddělovač, řádek1, sloupec1, řádek2, sloupec2; skript dostane na vstup tabulku s prvky na řádcích oddělenými daným oddělovačem (jako /etc/passwd pro :) a vypíše její výřez, tedy řádky řádek1--řádek2 obsahující sloupce sloupec1--sloupec2. Řádky i sloupce jsou počítány od 1 a intervaly jsou uzavřené. Použijte příkazy head, tail, cut; nepoužívejte cykly, vše lze vyřešit na jednom řádku. Zobrazit řešení
        Skript submatrix.sh:
            #!/bin/bash
            head -n"$4" | tail -n+"$2" | cut -d"$1" -f"$3-$5"
        Přehlednější verze submatrix.sh:
            #!/bin/bash
            oddelovac=$1
            radek1=$2
            sloupec1=$3
            radek2=$4
            sloupec2=$5
            head "-n$radek2" | (pošleme dál prvních $radek2 řádků)
            tail "-n+$radek1" | (pošleme dál vše od řádku $radek1)
            cut "-d$oddelovac" "-f$sloupec1-$sloupec2" (vybereme jen dané rozmezí sloupců)
        Příklad spuštění:
            ./submatrix.sh : 10 5 12 7 < /etc/passwd
            ls -ltr | tr -s ' ' ' ' | ./submatrix.sh ' ' 2 6 11 100000
                (10 nejstarších souborů s daty modifikace)
  2. Napište skript catHeads.sh. Ten dostane jako parametry názvy souborů a pro každý má vypsat řádek --- nazev souboru ---, 20 úvodních řádků z jeho obsahu a jeden prázdný řádek. Pro vypsání začátku souboru využijte příkaz head. Zobrazit řešení
        #!/bin/bash
        for soubor; do
         echo "--- $soubor ---"
         head -n20 "$soubor"
         echo
        done
  3. Napište funkci randStr, která dostane jako první parametr množinu znaků (ve stejném formátu jako u příkazu tr), jako druhý parametr dostane počet znaků a vypíše náhodný textový řetězec dané délky složený ze znaků dané množiny. Použijte speciální soubor /dev/urandom, který obsahuje nekonečné množství náhodných dat; stačí je profiltrovat příkazem tr a vypsat pouze daný počáteční úsek příkazem head; za výstupem je vhodné ještě vypsat znak konce řádku. Zobrazit řešení
        randStr() { tr -cd "$1" < /dev/urandom | head -c "$2"; echo; }
  4. Použijte předchozí funkci k vypsání náhodného čtyřmístného hexadecimálního čísla. Zobrazit řešení
        randStr "0-9A-F" 4

5. cvičení (22. 3. 2019)

  1. Napište příkaz, který ve výstupu příkazu df sečte sloupec s volným místem na jednotlivých discích a vypíše výsledek v GiB. Spusťte jej na u-pl3 pro porovnání výsledků. Zobrazit řešení
        df -k | {
            total=0
            while read a b c space rest; do
                total=$((total + space))
            done
            echo $((total / 1024 / 1024)) GiB
        }
        Příkaz by také mohl být zapsán na jednom řádku s použitím středníků namísto konců řádků:
            df -k | { total=0; while read a b c space rest; do total=$((total + space)); done; echo $((total / 1024 / 1024)) GiB; }
        Každý příkaz v sekvenci oddělené | se provádí v subshellu,
        proto sčítání včetně vypsání výsledku musí být ve složených závorkách.
        Následující řešení nefunguje:
            total=0
            df -k |
            while read a b c space rest; do
                total=$((total + space))
            done (sečtení proběhlo v subshellu, který zde končí)
            echo $((total / 1024 / 1024)) GiB (tento příkaz opět vidí v $total nulu)
        Pro pohodlnou editaci delších příkazů v interaktivním shellu
        můžete použít ^X^E (tedy Ctrl+x následované Ctrl+e);
        tím se aktuálně upravovaný řádek otevře ve vimu,
        po uložení a zavření vimu se upravený příkaz provede.
  2. Upravte skript catHeads.sh z minula tak, aby vypisoval začátky pouze u textových souborů; u složek ať vypíše text Soubor je adresář. a u ostatních souborů Soubor je neznámého typu.. Pro větvení použijte konstrukci case ... in ... esac na výstup příkazu file, který typ souboru určí. Zobrazit řešení
        Soubor catHeads.sh:
            #!/bin/bash
            for soubor; do
                case "$(file --mime-type -b "$soubor")" in
                    text/*)
                        echo "--- $soubor ---"
                        head -n20 "$soubor";;
                    inode/directory)
                        echo "Soubor je adresář.";;
                    *)
                        echo "Soubor je neznámého typu.";;
                esac
                echo
            done
        POSIXová verze příkazu file nezná přepínače
        --mime-type pro vypsání typu jako MIME ani -b pro neopisování názvu souboru
        a konkrétní názvy typů bez těchto přepínačů se můžou na různých systémech lišit,
        čemuž je třeba přizpůsobit patterny v jednotlivých větvích case.
  3. Najděte všechny soubory s příponou .h v adresáři /usr/share a zjistěte jejich počet. Zobrazit řešení
        find /usr/share -name '*.h' 2>/dev/null | wc -l
        Pattern *.h musí být v uvozovkách,
        protože jej zde chceme předat beze změny příkazu find,
        ten jej poté bude interpretovat podobným způsobem jako shell.
        Přesměrování chybového výstupu 2> do /dev/null zakáže vypisování chyb o chybějících oprávněních,
        takovéto soubory budou tiše ignorovány.
  4. Zjistěte počet adresářů v /etc (i rekurzivně). Zobrazit řešení
        find /etc -type d 2>/dev/null | wc -l
  5. Najděte všechny aplikace v /usr/bin větší než 30MB. Zobrazit řešení
        find /usr/bin -perm -ug+x -type f -size +$((1024*1024*30))c
  6. Zjistěte celkovou velikost všech běžných souborů ve složce /var/log, které nebyly za posledních 14 dní změněny. Pro zjištění celkové velikosti přimějte příkaz find, aby jednou spustil příkaz du -csh soubory se všemi soubory splňujícími podmínku jako parametry. Zobrazit řešení
        find /var/log -type f -mtime +13 -exec du -csh {} + 2>/dev/null | tail -n1 | cut -f1
  7. Zjistěte počet souborů v /usr/bin, které jsou novější než /bin/bash. Zobrazit řešení
        find /usr/bin -type f -newer /bin/bash | wc -l

6. cvičení (29. 3. 2019)

  1. Použijte příkaz grep na vypsání řádků vstupu, které obsahují slovo s prvním písmenem velkým a ostatními malými. Otestujte, že příkaz funguje např. na vstupech Honza, TeX, vis, jaky je nelepsi editor?. Zobrazit řešení
        Pomocí základních regulárních výrazů (\| není v POSIXu):
            grep '\(^\|[^A-Za-z]\)[A-Z][a-z]*\([^A-Za-z]\|$\)'
        Pomocí rozšířených regulárních výrazů:
            egrep '(^|[^A-Za-z])[A-Z][a-z]*([^A-Za-z]|$)'
            (POSIXové řešení vyžaduje grep -E ... namísto egrep ....)
        V obou případech hledáme slovo začínající velkým písmenem a pokračující malými,
        kterému předchází začátek řádku nebo jiný znak než písmeno
        a po němž následuje jiný znak než písmeno nebo konec řádku.
  2. Nahraďte příkazem sed každé číslo na vstupu třemi otazníky. Př. 100kc je asi 5 dolaru -> ???kc je asi ??? dolaru. Zobrazit řešení
        Základní regulární výrazy:
            sed 's/[0-9][0-9]*/???/g'
        Rozšířené regulární výrazy:
            sed -r 's/[0-9]+/???/g'
        Výraz [0-9]* odpovídá i prázdnému řetězci,
        ten se sám o sobě nachází
        mezi každými dvěma sousedními znaky vstupu i na začátku a konci řetězce;
        následující příkaz proto nefunguje:
            sed 's/[0-9]*/???/g'
        Pro vstup ab dostaneme ???a???b???.
  3. Rozdělte příkazem sed každé číslo na vstupu od konce po trojicích mezerami. Př. Bude to stat 12500kc. -> Bude to stat 12 500kc., muj telefon je +420123456789 -> muj telefon je +420 123 456 789. Zobrazit řešení
        Základní regulární výrazy:
            sed ':begin; s/\([0-9]\)\([0-9]\{3\}\([^0-9]\|$\)\)/\1 \2/; t begin'
            (ukotvení ^ a $ v závorkách nemusí dle POSIXu fungovat)
        Rozšířené regulární výrazy:
            sed -r ':begin; s/([0-9])([0-9]{3}([^0-9]|$))/\1 \2/; t begin'
            (v souladu s POSIXem)
        V obou případech hledáme čtyři číslice následované nečíslicí nebo koncem řádku
        a za první z těchto číslic připíšeme mezeru;
        tuto substituci opakujeme dokud je úspěšná.

7. cvičení (5. 4. 2019)

  1. Otevřete ve vimu stránku Linuxu na anglické wikipedii příkazem wget 'https://en.wikipedia.org/wiki/Linux' -O- | vim -, uložte soubor pod názvem linux.txt (bez zavření vimu) a proveďte v něm následující změny příkazy edu ve vimu: Odstraňte všechny řádky před řádkem začínajícím <p><b>Linux. Odstraňte všechny řádky počínaje řádkem obsahujícím pouze text </p>. Odstraňte všechny párové tagy sup včetně obsahu mezi nimi -- pro správné spárování otevíracího a ukončovacího tagu pomůže nejprve zalomit řádek za ukončovacími. Smažte všechny ostatní tagy, text mezi párovými tagy ponechte. Nakonec libovolnými prostředky vimu smažte závorku za úvodním slovem Linux a soubor uložte. Výsledkem by měl být soubor s úvodním textem z dané stránky Wikipedie bez číslovaných referencí a výslovnosti slova Linux, kde je každý odstavec na samostatném řádku; soubor by měl obsahovat 365 slov na 6-ti řádcích, ověřte příkazem wc. Zobrazit řešení
        Příkaz wget stahuje soubory z webu,
        parametrem -O mu určíme jako výstupní soubor -,
        což zde odpovídá standardnímu výstupu;
        ve vimu otevřeme soubor -,
        což jako vstupní soubor odpovídá standardnímu vstupu.
        Po otevření souboru zadáváme příkazy:
            :w linux.txt
                Soubor uložíme jako linux.txt.
            :1,/^<p><b>Linux/-1 d
                Smažeme vše od řádku 1 až po 1 řádek před daným textem.
            :/^<\/p>$/,$ d
                Smažeme vše od řádku obsahujícího právě </p>,
                je potřeba ukotvení ^ a $.
            :%s=</sup>=&\r=g
                Za uzavírací tagy sup vložíme znak nového řádku.
                Pro adresování všech řádků lze ve vimu použít %,
                namísto \n je ve vimu potřeba \r v textu k nahrazení;
                g umožní i opakované nahrazení v jednom řádku.
            :%s=<sup .*\n==
                Smažeme vše od otevíracího tagu sup až do konce řádku,
                který následuje po uzavíracím tagu, vč. znaku konce řádku;
                v regulárním výrazu se používá \n.
            :%s=<[^>]*>==g
                Smažeme všechny zbývající tagy.
                Vnitřek tagu může obsahovat cokoliv kromě znaku >,
                povolení všech znaků by způsobilo smazání všeho
                od otevření prvního tagu po uzavření posledního.
            ggwd%x
                Příkazem gg se přesuneme na první řádek.
                Příkazem w na následující slovo -- otevírací závorku.
                Příkazem d smažeme vše až po místo,
                kam se dostaneme následujícím příkazem pohybu,
                tím je %, které přesune na párovou závorku.
                Nyni zbývá smazat přebytečnou mezeru příkazem x.
            :x (ekvivalent :wq)
                Soubor uložíme a zavřeme vim.
  2. Použijte sed na vypsání souboru linux.txt s každou větou na samostatném řádku. Každá věta začíná velkým písmenem a končí tečkou. Výstupem je 19 řádků. Zobrazit řešení
        Základní regulární výrazy:
            sed 's/\(\.\) \([A-Z]\)/\1\n\2/g'
        Rozšířené regulární výrazy:
            sed -r 's/(\.) ([A-Z])/\1\n\2/g'
  3. Použijte sed na seřazení posloupnosti nul a jedniček na každém řádku -- nejprve vypište všechny nuly, pak všechny jedničky. Př. 00110100 -> 00000111. Zobrazit řešení
        sed ':begin; s/10/01/; t begin'
  4. Použijte grep na vypsání řádků, které lze rozdělit na dva stejné podřetězce. Zobrazit řešení
        Základní regulární výrazy:
            grep '^\(.*\)\1$'
        Rozšířené regulární výrazy:
            egrep '^(.*)\1$' (grep -E ... pro POSIXové řešení)
  5. (Napište skript emailsFromWeb.sh, který dostane jako parametr webovou adresu a vypíše všechny na ní uvedené emailové adresy v abecedním pořadí bez duplicit.) Zobrazit řešení
        Skript může vypadat například takto:
            #!/bin/bash
            wget "$1" -O- 2>/dev/null |
            egrep -o '[-a-zA-Z.0-9]+@[-a-zA-Z.0-9]+' |
            sort | uniq
        Regulární výraz pro hledání adres často bude fungovat,
        ale je hodně nepřesný,
        výraz odpovídající všem emailovým adresám podle příslušného RFC by byl složitější.

8. cvičení (12. 4. 2019)

  1. Napište skript avg.sh, který dostane na každém řádku vstupu jedno číslo a má vypsat jejich aritmetický průměr s přesností na dvě desetinná místa. Použijte příkaz bc. Zobrazit řešení
        #!/bin/bash
        cnt=0
        sum=0
        while read number; do
            cnt=$((cnt+1))
            sum=$(bc -q <<< "scale=2; $sum + $number")
        done
        bc -q <<< "scale=2; $sum / $cnt"
  2. Napište skript, který vypíše své PID (ID procesu), bude vyčkávat 10s (viz příkaz sleep) a při ukončení vypíše text konec, k ukončení může dojít i pomocí signálů. Vyzkoušejte ukončení pomoci Ctrl+C i pomocí příkazu kill. Zobrazit řešení
        #!/bin/bash
        echo $$
        trap "echo konec" EXIT
        sleep 10
  3. Napište skript queue1.sh, který bude bude zpracovávat vstup po řádcích následovně: neprázdné řádky bude přidávat do fronty a při výskytu prázdného naopak jeden řádek z fronty odebere a vypíše. Využijte toho, že skript nepožaduje parametry a ukládejte obsah fronty v $1, $2, $3, ... pomocí set -- "prvni prvek" "druhy prvek" ... a shift. Zobrazit řešení
        #!/bin/bash
        set --
        while read line; do
            if [ -z "$line" ]; then
                echo $1; shift
            else
                set -- "$@" "$line"
            fi
        done
  4. Napište skript queue2.sh, který bude obsahovat funkce enqueue na přidání řádku do fronty a dequeue na odebrání řádku a bude fungovat stejně jako předchozí; obsah fronty bude tentokrát uchovávat v běžné proměnné pomocí konstrukcí ${var#pattern}, ${var##pattern}, ${var%pattern}, ${var%%pattern}. Můžete předpokládat, že se na jednotlivých řádcích nevyskytují mezery. Zobrazit řešení
        #!/bin/bash
        queue=""
        enqueue() {
            queue="$queue$* "
        }
        dequeue() {
            elem="${queue%% *}"
            queue="${queue#* }"
        }
        while read line; do
            if [ -z "$line" ]; then
                dequeue
                echo $elem
            else
                enqueue $line
            fi
        done

9. cvičení (26. 4. 2019)

  1. Napište ve vimu skript timeGuard.sh, který každých 15 sekund vypíše řádek se svým prvním argumentem a s aktuálním časem. Namísto ukončování vimu používejte Ctrl+Z pro jeho pozastavení a příkaz fg pro obnovení. Spusťte skript s parametrem A a zkuste jej také pozastavit. Zjistěte příkazem jobs čísla a stav spuštěných úloh (vimu a skriptu) a přepínejte se mezi nimi. Obnovte běh skriptu na pozadí příkazem bg, spusťte skript na pozadí vícekrát pomocí & s různými parametry. Zkuste některé z instancí ukončit příkazem kill s použitím čísla úlohy z jobs, jiné pomocí Ctrl+C po přenesení do popředí. Zobrazit řešení
        Skript:
            #!/bin/bash
            while true; do
                echo "$1 " `date`
                sleep 15
            done
        Hlavním cílem bylo následné vyzkoušení příkazů:
            ^Z (Ctrl+Z)
                Pozastavení úlohy běžící na popředí.
            jobs
                Zobrazení stavu úloh spuštěných v aktuálním terminálu vč. číslování.
            fg, fg %3 (foreground)
                Přenesení úlohy (aktuální nebo s daným číslem) do popředí, příp. obnovení běhu.
            bg, bg %3 (background)
                Obnovení běhu pozastavené úlohy na pozadí.
            kill %3
                Ukončení úlohy s daným číslem.
        Pro spuštění úlohy na pozadí lze za příkaz dopsat &.
  2. Napište skript filterLines.sh, který dostane jako parametry název vstupního a výstupního souboru. Skript bude postupně vypisovat řádky vstupního souboru a po každém bude čekat na stisk jedné klávesy; bude-li to y, připíše řádek do výstupního souboru, jinak jej zahodí. Pokud výstupní soubor před spuštěním skriptu již existoval, přepište jej. (Vedle standardního vstupu si otevřete ještě jeden vstup pro čtení souboru.) Zobrazit řešení
        #!/bin/bash
        if [ $# -ne 2 ]; then
            echo "$0 INPUT_FILE OUTPUT_FILE"
            exit
        fi
        while read line <&3; do (řádky čteme z 3. vstupu)
            read -sn1 -p "$line" (zde používáme standardní vstup a výstup)
            echo
            if [ "$REPLY" == y ]; then
                echo "$line" >&4 (řádky vypisujeme do 4. výstupu)
            fi
        done 3<"$1" 4>"$2" (jako 3. vstup otevřeme soubor $1 a jako 4. výstup $2)
        Alternativně je možné přesměrovat standardní vstup/výstup
        a s uživatelem komunikovat pomocí /dev/tty.
  3. Napište skript delayedSend.sh, který dostane jako parametr název souboru, bude vyčkávat na vytvoření daného souboru a poté pošle jeho obsah na email uvedený v proměnné prostředí LOGMAIL. Využijte toho, že cat vrací nenulový návratový kód při neexistenci souboru. Před spuštěním skriptu si nastavte proměnnou LOGMAIL na svůj email příkazem export. Zobrazit řešení
        #!/bin/bash
        [ -n "$LOGMAIL" ] || exit 1
        until cat "$1" 2>/dev/null; do
            sleep 5;
        done |
        mail -s "[unix][script] file $1" "$LOGMAIL"
  4. Spusťte předchozí skript na nějaký neexistující soubor pomocí nohup. Zkontrolujte příkazem ps, že skript stále běží. Vytvořte příslušný soubor s nějakým obsahem, ověřte, že byl skript ukončen a že Vám přišel email. Zobrazit řešení
        export LOGMAIL=email@example.com
            LOGMAIL se předá jako proměnná prostředí každému příkazu
            spuštěnému v aktuálním terminálu.
        nohup ./delayedSend.sh file.txt &
            Skript běží na pozadí a nevyruší jej ani odhlášení.
        ps x (Ověříme že běží.)
        echo test >> file.txt
        ps x (Ověříme že byl ukončen.)
  5. Seznámení se s aplikací tmux (terminal multiplexer), která umožňuje práci s více terminály v jednom okně a odpojení od aktuální relace bez ukončení běžících aplikací (např. při vzdálené práci přes SSH). Zobrazit řešení
        Spuštění s vytvořením nové relace:
            tmux
        Spuštění s připojením k existující relaci:
            tmux a (attach)
        Odpojení od aktuální relace bez jejího ukončení:
            ^B d (Ctrl+B, pak d jako detach)
        Rozdělení okna vertikálně:
            ^B "
        Rozdělení okna horizonálně:
            ^B %
        Přepínání mezi okny:
            ^B následované šipkami
        Psaní do všech oken současně (zapnutí i vypnutí):
            ^B :set synchronize-panes
            Lze v konfiguračním souboru namapovat na klávesovou zkratku.

10. cvičení (3. 5. 2019)

  1. Vypište pomocí AWK první a poslední slovo z každého řádku. Zobrazit řešení
        awk '{ print $1, $NF }'
  2. Napište v AWK skript usersLogins.awk, který pro vstup ve formátu souboru passwd vypíše řádky ve tvaru Uzivatel JMENO_UZIVATELE ma login LOGIN.. Výstup ať obsahuje pouze uživatele, v jejichž jméně každé slovo začíná velkým písmenem. Vyzkoušejte na výstup getent passwd a ověřte nepřítomnost úvodních uživatelů root, bin, daemon, ... Zobrazit řešení
        Skript:
            #!/usr/bin/awk -f
            BEGIN { FS=":" }
            $5 ~ /^([A-Z][a-z]*\s*)+$/ {print "Uzivatel " $5 " ma login " $1 "."}
        Nepřítomnost úvodních záznámů ověříme (třeba na u-pl3) příkazem:
            getent passwd | ./usersLogins.awk | head
  3. Napište skript todayFeaturedArticle.sh, který vypíše z úvodní stránky české Wikipedie https://cs.wikipedia.org/wiki/Hlavní_strana text sekce Článek týdne. Tato sekce je ohraničena tagy <div id="mp-tfa"> a </div>, ale obsahuje i vnořené tagy div, takže je potřeba počítat aktuální úroveň zanoření. Pomůže nastavení RS na < a FS na >. Vypisujte pouze text mezi jednotlivými tagy. Ke stažení stránky použijte wget. Zobrazit řešení
        Skript:
            #!/bin/bash
            wget 'https://cs.wikipedia.org/wiki/Hlavní_strana' -O- 2>/dev/null |
            awk -v RS="<" -v FS=">" -v ORS="" '
            /^div id="mp-tfa"/ { depth=0 }
            depth=="" { next }
            /^div/ { depth++ }
            /^\/div/ { depth-- }
            depth<=0 { print "\n"; exit }
            { print $2 }'
        Postupně procházíme tagy,
        text tagu máme v $1, text za tagem v $2.
        Do nalezení divu s daným id nic neděláme,
        pak počítáme úroveň zanoření v proměnné depth
        a vypisujeme $2 až do uzavření odpovídajícího tagu div,
        kdy vypíšeme znak nového řádku a skončíme.
  4. Napište v AWK skript, který v záznamech oddělených čárkami nahradí posloupnosti bílých znaků jedním podtržítkem a vypíše každý záznam na samostatný řádek. Využijte funkci gsub. Zobrazit řešení
        #!/usr/bin/awk -f
        BEGIN { RS="," }
        { gsub(/\s+/, "_"); print }

11. cvičení (10. 5. 2019)

Zastupoval Jirka Setnička.
  1. Napište vlastní wc.
  2. Vypište ze vstupu každý desátý řádek.
  3. Napište skript, který otočí pořadí slov na řádku.
  4. Napište skript lsOwner.sh využívající AWK, který bude brát jako parametry názvy souborů/složek stejně jako ls a pro soubory, které by ls s danými parametry vypsal, vypíše řádky ve formátu JMENO_UZIVATELE NAZEV_SOUBORU s oběma sloupci zarovnanými. Jménem uživatele je zde myšleno jeho skutečné jméno, ne login. V části BEGIN si pomocí cyklu s getline načtěte výstup příkazu getent passwd a uložte si jména uživatelů podle loginů do asociativního pole; bude užitečné na tuto část skriptu změnit proměnnou FS. Zobrazit řešení
        #!/bin/bash
        ls -l "$@" |
        awk '
        BEGIN {
        FS=":"
        while ("getent passwd" | getline == 1) {
            users[$1]=$5
        }
        FS=" "
        }
        $9 { printf "%-25s %s\n", users[$3], $9 }'

12. cvičení (17. 5. 2019)

  1. Napište vlastní skript which, který bude hledat spustitelný soubor s daným názvem ve složkách uvedených v $PATH. Pro snadné procházení cest v $PATH nastavte IFS=:. Zobrazit řešení
        #!/bin/bash
        IFS=:
        for name; do
            for dir in $PATH; do
                if [ -x "$dir/$name" ]; then
                    echo "$dir/$name";
                    break;
                fi
            done
        done
        Od nastavení IFS dále se bude text po expanzi dělit na slova dvojtečkami
        namísto bílých znaků.
  2. Vytvořte soubor s nějakým obsahem a pomocí příkazu ln vytvořte symlink směřujicí na tento soubor. Změňte obsah jednoho z nich a zkontrolujte obsah druhého. Smažte prvně vytvořený soubor, co se stalo s druhým? Zkuste totéž pomocí hardlinku. Zkuste si také zjistit počet hardlinků vedoucích na tyto soubory příkazy ls nebo stat. Zobrazit řešení
        echo text > prvni.txt
        ln -s prvni.txt druhy.txt (bez -s pro hardlink)
        echo druhy radek >> druhy.txt
        cat prvni.txt
        Oba soubory budou mít stejný obsah.
        ls -l
        U symlinku zde uvidíme, kam vede;
        u hardlinku uvidíme počet odkazů - 2 namísto 1.
        rm prvni.txt
        cat druhy.txt
        U hardlinku stále funkční,
        u symlinku chyba, cíl neexistuje.
  3. Vytvořte si pojmenovanou rouru příkazem mkfifo. V jednom okně použijte příkaz cat na zápis do této roury a ve druhém na čtení, poté něco napište do prvního okna a uzavřete vstup. Zobrazit řešení
        V prvním okně:
            mkfifo mypipe
            cat mypipe
        Ve druhém okně:
            cat > mypipe
            nejaky text
            ^D
  4. Napište skript lockTest.sh, který po spuštění vypíše text locking by PID, kde PID je číslo procesu $$, vytvoří adresář /tmp/mylock jako zámek, vypíše text locked by PID, vyčká 1s a při ukončování (i pomocí signálu) vypíše text unlocking by PID a složku smaže. Spusťte paralelně 10 instancí tohoto skriptu (for i in `seq 10`; do ./lockTest.sh & done) a ověřte, že zámek vždy vlastní jen jedna z nich. Zobrazit řešení
        Skript:
            #!/bin/bash
            echo "locking by $$"
            while ! mkdir /tmp/mylock 2>/dev/null; do
                sleep 0.1
            done
            trap "echo unlocking by $$; rmdir /tmp/mylock" EXIT
            echo locked by $$
            sleep 1
        Po paralelním spuštění všechny instance vypíší locking...,
        jedna vypíše locked...,
        poté vypíše unlocked... a další vypíše locked..., ...
  5. Vyzkoušejte si getopts. Napište skript hello.sh, který bude mezi parametry přijímat přepínač -e pro angličtinu, -c pro češtinu (příp. jiné jazyky) a -n pro určení jména -- tento přepínač tedy obsahuje jméno jako parametr. Skript vypíše pozdrav v daném jazyce vč. příp. oslovení (bylo-li zadáno jméno). V případě zadání více vzájemně se vylučujících přepínačů (jazyků) se použije poslední z nich. Zobrazit řešení
        #!/bin/bash
        czech=false
        name=""
        while getopts cen: opt; do
            case $opt in
                c) czech=true;;
                e) czech=false;;
                n) name=$OPTARG;;
            esac
        done
        if $czech; then
            echo -n Ahoj
        else
            echo -n Hello
        fi
        if [ -n "$name" ]; then
            echo ", $name!"
        else
            echo "!"
        fi

13. cvičení (24. 5. 2019)

  1. Napište skript editLines, který bude přidávat a mazat řádky zadaného souboru. Prvním parametrem bude název souboru k editaci, druhým název operace insert/delete, třetím regulární výraz určující řádek, za nějž se mají připisovat řádky ze vstupu nebo který má být smazán. Zobrazit řešení
        #!/bin/bash
        [ $# = 3 ] || exit 1
        {
            echo "/$3/"
            case "$2" in
                insert)
                    echo a
                    while IFS='' read line; do
                        if [ "$line" != "." ]; then
                            echo "$line"
                        else
                            echo -e "..\n.\ns/.//\na"
                        fi
                    done
                    echo .;;
                delete)
                    echo d;;
            esac
            echo -e "w\nq"
        } | ed "$1" >/dev/null
  2. Napište skript mailReader, který zpracuje email ze svého vstupu. Skript ze zprávy přečte adresu odesílatele (bez zobrazovaného jména) a předmět; do souboru pojmenovaném po adrese odesílatele poté připíše řádek === předmět === s předmětem zprávy následovaný textem zprávy. Příklady zpráv najdete ve složce u-pl3:/tmp/unix/mails/. Zobrazit řešení
        #!/bin/bash
        while IFS=: read name value; do
            case "$name" in
                From) from=`sed -r 's/.*<(.*)>/\1/; s/^\s*|\s*$//g' <<< "$value"`;;
                Subject) subj=`sed -r 's/^\s*|\s*$//g' <<< "$value"`;;
                '') break;;
            esac
        done
        [ -n "$from" ] || exit 1
        {
            echo "=== $subj ==="
            cat
            echo
        } >> "$from"
  3. Vytvořte adresář users a v něm adresáře pro prvních 20 uživatelů pojmenované jejich uživatelskými jmény; použijte k tomu xargs. Zobrazit řešení
        mkdir users
        cd users
        getent passwd | head -n20 | cut -d: -f1 | xargs mkdir