Cvičení z Úvodu do UNIXu, letní semestr 2017/18

Petr Kučera, KTIML MFF UK

Základní informace

Užitečné odkazy

Zápočet

Po každém cvičení zadám domácí úkoly, jejichž řešení budu očekávat do začátku dalšího cvičení. Pokud na konci budete mít alespoň 2/3 bodů z těchto úkolů, tak dostanete zápočet. Kdo bude mít méně bodů, bude muset k získání zápočtu vypracovat ještě pár domácích úkolů navíc, které zadám na konec (jejich množství a obtížnost budou přímo úměrné chybějícímu počtu bodů). Za úkoly z každého cvičení bude možno dostat 3 body, i když ze začátku bude pár jednodušších úkolů a ke konci bude třeba jen jeden, ale těžší. Kdo odevzdá méně než 1/3 úkolů, zápočet ode mne nedostane.

V úkolech je možné používat jen prostředky probrané do cvičení, ke kterému se vztahují (včetně). Pokud si všimnu, že někdo odevzdal kopii jiného řešení, nedostane za tento úkol žádné body, stane-li se to podruhé, nedostane ode mne zápočet.

Úkoly mi odevzdávejte e-mailem, nejpozději do začátku následujícího cvičení. Řešení buď pište do těla mejlu, nebo přikládejte v čistě textových souborech (nepoužívejte soubory pdf, word a podobně). Zadání úkolů jsou napsána vždy u odpovídajícího cvičení (tj. aktuální úkoly jsou vždy dole na stránce).

Úkoly budou zadány na 13 cvičení, tedy limit pro získání zápočtu je 26 bodů. Doplňkové příklady budou postupně přidány k poslednímu cvičení.

Některé vlastnosti u-labů

Je dobré znát některé vlastnosti UNIXového labu na Malé Straně. Doporučuji proto prohlédnout si stránky UNIXové laboratoře na Malé Straně. Zmíním alespoň ty rozdíly, které mi připadají nejznatelnější:

  1. Domovské adresáře jsou umístěny AFS filesystému, kde nefungují standardní UNIXová práva (viz výše uvedená stránka, případně stránka o právech v u-labech)
  2. Pokud si tedy chcete zkoušet příkaz chmod a věci související se standardním UNIXovým systémem práv, čiňte tak v adresáři /tmp na tom kterém stroji, tam fungují tím standardním způsobem.
  3. /etc/passwd neobsahuje informace o vašich uživatelských účtech, protože ty jsou skladovány v dynamické databázi jinde a jinak, pokud potřebujete informace o uživatelských účtech ve formátu /etc/passwd, dostanete je příkazem getent následovně:
    getent passwd
    případně můžete přidat parametr specifikující, co vás zajímá, viz též getent --help.
  4. Totéž platí i pro /etc/group a getent group.
  5. Příkaz find si s filesystémem afs (nejspíš stále) nerozumí, nepoužívejte jej proto raději na síťových discích. Hledat pomocí find můžete v lokálních adresářích jako /tmp, /usr, /etc a podobně, ale raději ne ve vašem domovském adresáři. Výsledek vyhledávání ve vašem domovském adresáři nelze zaručit.

Obsah cvičení

1. cvičení (20. února 2018)

Základní příkazy, ovládání shellu.

Trochu podrobněji:

Příklady

  1. Ve svém domovském adresáři vytvořte adresář ADR
  2. Do tohoto adresáře okopírujte soubory z adresáře /usr/include, jejichž názvy začínají znakem m, končí příponou .h a kromě toho v názvu obsahují číslici.
  3. V adresáři ADR vytvořte podadresář PODADRESAR.
  4. Prvních pět řádek z každého z okopírovaných souborů uložte do souboru ~/ADR/PODADRESAR/prvnichpet.
  5. Poslední řádky souborů v ~/ADR uložte do souboru ~/ADR/PODADRESAR/posledni.
  6. Soubory v ~/ADR/PODADRESAR spojte do jednoho souboru ~/ADR/PODADRESAR/prvniaposledni.
  7. Smažte soubory v ~/ADR/PODADRESAR kromě prvniaposledni.
  8. Do souboru ~/ADR/PODADRESAR/pocet uložte počet souborů a adresářů v ~/ADR.
  9. Vypište dlouhé informace o adresáři ~/ADR/PODADRESAR, ne jeho obsah.
  10. Vymažte obsah souboru ~/ADR/PODADRESAR/prvniaposledni bez toho, abyste smazali tento soubor.
  11. Do souboru ~/ADR/PODADRESAR/prvniaposledni přidejte řádku, jež obsahuje jen znak hvězdičky, tj *.
  12. Smažte ~/ADR i se všemi soubory a podadresáři v něm umístěnými.
  13. (Ukázat řešení předchozích příkladů)
    #!/bin/sh

    # Ve svém domovském adresáři vytvořte adresář ADR
    mkdir ~/ADR

    # Do tohoto adresáře okopírujte soubory z adresáře
    # /usr/include, jejichž názvy začínají znakem 'm', končí
    # příponou '.h' a kromě toho v názvu obsahují číslici.
    cp /usr/include/m*[0-9]*.h ~/ADR

    # V adresáři ADR vytvořte podadresář PODADRESAR.
    mkdir ~/ADR/PODADRESAR

    # Prvních pět řádek z každého z okopírovaných souborů uložte do
    # souboru ~/ADR/PODADRESAR/prvnichpet.
    head -n 5 ~/ADR/m*[0-9]*.h >~/ADR/PODADRESAR/prvnichpet

    # Poslední řádky souborů v ~/ADR uložte do souboru
    # ~/ADR/PODADRESAR/posledni.
    tail -n 1 ~/ADR/m*[0-9]*.h >~/ADR/PODADRESAR/posledni

    # Soubory v ~/ADR/PODADRESAR spojte do jednoho souboru
    # ~/ADR/PODADRESAR/prvniaposledni.
    cat ~/ADR/PODADRESAR/* >~/ADR/PODADRESAR/prvniaposledni

    # Smažte soubory v ~/ADR/PODADRESAR kromě prvniaposledni.
    rm ~/ADR/PODADRESAR/prvnichpet
    rm ~/ADR/PODADRESAR/posledni

    # Do souboru ~/ADR/PODADRESAR/pocet uložte počet souborů a
    # adresářů v ~/ADR.
    ls ~/ADR | wc -l >~/ADR/PODADRESAR/pocet

    # Vypište dlouhé informace o adresáři ~/ADR/PODADRESAR,
    # ne jeho obsah.
    ls -ld ~/ADR/PODADRESAR

    # Vymažte obsah souboru ~/ADR/PODADRESAR/prvniaposledni bez
    # toho, abyste smazali tento soubor.
    cp /dev/null ~/ADR/PODADRESAR/prvniaposledni

    # Do souboru ~/ADR/PODADRESAR/prvniaposledni přidejte řádku, jež obsahuje
    # jen znak hvězdičky, tj *.
    echo '*' >>~/ADR/PODADRESAR/prvniaposledni

    # Smažte ~/ADR i se všemi soubory a podadresáři v něm umístěnými.
    rm -r ~/ADR
  14. Vypište řádky číslo 11-20 ze souboru /etc/passwd.
    (Ukázat řešení)
    #!/bin/sh

    head -n20 /etc/passwd | tail -n10

Domácí úkoly

  1. Napište příkaz, který vypíše všechny názvy souborů ve vašem domovském adresáři, které začínají tečkou „.“ a obsahují alespoň 2 znaky. [1 bod]
  2. Napište příkaz, který vypíše poslední řádku z každého souboru v adresáři /usr/include, jehož název končí „.h“. [1 bod]
  3. Vytvořte adresář INCL ve vašem domovském adresáři. Napište příkaz, kterým okopírujete všechny soubory z adresáře /usr/include/, jejichž jména nezačínají číslicí, ale číslici v názvu obsahují, do tohoto adresáře. [1 bod]

2. cvičení (1. března 2018)

Ještě základní příkazy a ovládání shellu.

Příkazy: date, tee, touch, cut, paste, tr.

Příklady:

  1. Vytvořte soubor se jménem „-f“, který bude obsahovat jednu řádku s aktuálním datem. Poté tento soubor smažte.
    (Ukázat řešení)
    #!/bin/sh

    # Vytvoření souboru -f s datem:
    date >-f
    # Smazání souboru -f:
    rm -- -f
  2. Vytvořte soubor „pevne datum“ (tj. „pevne<mezera>datum“) jehož čas poslední modifikace bude nastaven na 13:30 1.2. 2009.
    (Ukázat řešení)
    #!/bin/sh

    touch -t 200902011330 "pevne datum"
    # nebo
    touch -d "2009-02-01 13:30:00" "pevne datum"
  3. Do svého domovského adresáře okopírujte soubor /etc/passwd, kopie tohoto souboru se bude jmenovat ucty a bude mít čas poslední modifikace shodný s časem poslední modifikace /etc/passwd.
    (Ukázat řešení)
    #!/bin/sh

    cp -p /etc/passwd ucty
  4. Nastavte datum poslední modifikace souboru ucty (ve vašem domovském adresáři) na čas poslední modifikace souboru /etc/group.
    (Ukázat řešení)
    #!/bin/sh

    touch -r /etc/group ucty
  5. Napište příkaz, který vypíše obsah adresáře /usr/bin podle jmen sestupně podle abecedy do souborů bina a binb a navíc jej vypíše na obrazovku stránkovaně (pomocí less nebo more)
    (Ukázat řešení)
    #!/bin/sh

    ls -r /usr/bin | tee bina binb | more
  6. Rozmyslete si, co se děje při následujících příkazech:
    1. mv soubor /dev/null
    2. cp soubor /dev/null
    3. cat soubor >/dev/null
  7. Vypište deset největších souborů a pak deset nejmenších souborů v adresáři /etc
    (Ukázat řešení)
    #!/bin/sh

    ls -S /etc | head
    ls -rS /etc | head
  8. V adresáři /usr/bin najděte soubor, který byl modifikován naposledy.
    (Ukázat řešení)
    #!/bin/sh
    # V adresáři /usr/bin najděte soubor, který byl modifikován
    # naposledy.

    ls -t /usr/bin | head -n 1
  9. Stáhněte si soubor calories.csv, jde o soubor ve formátu CSV, kde je jako oddělovače sloupců použito středníku.
  10. V souboru calories.csv nahraďte uvozovky pomocí apostrofů.
    (Ukázat řešení)
    #!/bin/sh

    tr \" \' <calories.csv >/tmp/calories.a.csv
    mv /tmp/calories.a.csv calories.csv
  11. Ze souboru calories.csv odstraňte apostrofy (stejně jako dále předpokládám, že soubor byl změněn předchozími příklady, tedy zde jsou po předchozím příkladu apostrofy na místě původních uvozovek).
    (Ukázat řešení)
    #!/bin/sh

    tr -d \' <calories.csv >/tmp/calories.a.csv
    mv /tmp/calories.a.csv calories.csv
  12. Ze souboru calories.csv vypište jen první sloupec, tj. sloupec se jmény potravin.
    (Ukázat řešení)
    #!/bin/sh

    cut -f1 -d\; calories.csv
  13. V souboru calories.csv nahraďte velká písmena malými, ale jen v prvním sloupci. (Postup: Vyříznutí prvního sloupce pomocí cut, náhrada pomocí tr, připojení zpět pomocí paste
    (Ukázat řešení)
    #!/bin/sh

    cut -f1 -d\; calories.csv >/tmp/prvni
    cut -f2- -d\; calories.csv >/tmp/ostatni
    tr '[:upper:]' '[:lower:]' /tmp/prvni | paste -d\; - /tmp/ostatni >calories.csv
    rm /tmp/prvni /tmp/ostatni
  14. Soubor calories.csv upravte tak, aby poslední tři sloupce byly v opačném pořadí (tj. ve výsledku bude nejprve množství proteinů, pak karbohydrátů a pak tuků, použijte cut a paste).
    (Ukázat řešení)
    #!/bin/sh

    cut -f5 -d\; calories.csv >/tmp/paty
    cut -f6 -d\; calories.csv >/tmp/sesty
    cut -f7 -d\; calories.csv >/tmp/sedmy
    cut -f1-4 -d\; calories.csv | paste -d\; - /tmp/sedmy /tmp/sesty /tmp/paty >/tmp/novy
    mv /tmp/novy calories.csv
    rm /tmp/paty /tmp/sesty /tmp/sedmy
  15. Z výstupu ls -l vyberte pouze 9 znaků s právy.
    (Ukázat řešení)
    #!/bin/sh

    ls -l | tail -n +2 | cut -c2-10
  16. Předpokládejte, že máte tři soubory, scitanec1, scitanec2 a soucet, každý z nich obsahuje stejný počet řádků s čísly. Sestavte tyto řádky do tvaru
    scitanec1+scitanec2=soucet.
    (Ukázat řešení)
    #!/bin/sh

    paste -d+= scitanec1 scitanec2 soucet

Domácí úkoly

  1. Napište příkaz, kterým nastavíte datum a čas poslední modifikace souboru na 11.3.1993 15:04. [1 bod]
  2. V souboru calories.csv nahraďte každý název potraviny (tj. první sloupec) jediným znakem „-“. Ostatní sloupce zůstanou beze změny. [1 bod]
  3. Vypište dvojice uid:login za každého uživatele v /etc/passwd. [1 bod]

3. cvičení (7. března 2018)

Práva, linky, komunikační utility

Příkazy: ssh, scp, who, id, mail (mailx), last (tento příkaz není ve specifikaci a není proto standardní) ln, chmod, chgrp, chown, file, df, du, umask.

Pozor, na ulabech zkoušejte práva výhradně v adresáři /tmp, na ostatních je systém afs a práva tam fungují úplně jinak.

Příklady

  1. Vypište seznam souborů v aktuálním adresáři ve tvaru „velikost jméno“, seznam vytvořte úpravou výpisu ls -l a ls.
    (Ukázat řešení)
    #!/bin/sh

    ls -l | tail -n +2 | tr -s " " | cut -d " " -f 5 >/tmp/velikosti
    ls | paste -d " " /tmp/velikosti -
    rm /tmp/velikosti
  2. Vypište loginy ze souboru /etc/passwd po pěti na řádek, na každém řádku jsou oddělené čárkami.
    (Ukázat řešení)
    #!/bin/sh

    cut -d: -f1 </etc/passwd | paste -d"," - - - - -

    # Alternativně a ekvivalentně lze:
    cut -d: -f1 </etc/passwd | paste -s -d",,,,\n" -
  3. Napište příkaz, který vypíše vaše aktuální UID.
    (Ukázat řešení)
    #!/bin/sh

    id -u
  4. Napište příkaz, který vypíše seznam skupin, v nichž se nacházíte.
    (Ukázat řešení)
    #!/bin/sh

    id -Gn
  5. Pošlete obsah souboru /etc/group mejlem příkazem mail.
    (Ukázat řešení)
    #!/bin/sh

    mail -s "Obsah /etc/group" adresa </etc/group
  6. Vyzkoušejte si příkaz df ke zjištění obsazeného místa na disku s různými parametry (-k a -P, můžete si zkusit i další, ale všimněte si, že kromě -t nejsou už další parametry ve specifikaci).
  7. Vyzkoušejte si příkaz du na adresáři /tmp nebo na vašem domovském adresáři s různými parametry (-k, -s, -x).
  8. Vytvořte ve svém domovském adresáři link na /etc/passwd, zkuste nejprve vytvořit hard link a poté soft link (rozmyslete si, proč hard link vytvořit nelze). Vyzkoušejte, jak se při ls -l zobrazí soft link.
    (Ukázat řešení)
    #!/bin/sh

    # Hard link
    ln /etc/passwd ~
    # Soft link
    ln -s /etc/passwd ~
  9. Vytvořte hard link v /tmp na jiný soubor v /tmp (případně si tam nějaký nakopírujte, hard link bude mít pochopitelně jiné jméno). Vyzkoušejte si výpis ls -l a všimněte si, jak se změnilo číslo v druhém sloupečku. Pomocí ls -i zkontrolujte, že obě kopie mají stejné číslo inode.
  10. Vytvořte jednoduchý skript a přidělte mu práva pro spuštění.
    (Ukázat řešení)
    #!/bin/sh

    cat >skript.sh <<EOF
    #!/bin/sh

    echo Ahoj světe
    EOF


    chmod a+x skript.sh
  11. Vytvořte soubor, který může vlastník spustit, číst a psát do něj, členové skupiny jej mohou číst a psát do něj, ostatní jej mohou jen spustit.
    (Ukázat řešení)
    #!/bin/sh
    cat >skript.sh <<EOF
    #!/bin/sh

    echo Ahoj světe
    EOF


    chmod 0761 /tmp/skript.sh

    # Ekvivalentně například:
    chmod u+rwx,g=u-x,o=u-rw /tmp/skript.sh

    # nebo
    chmod u=rwc,g=rw,o=x /tmp/skript.sh
  12. Zkuste vytvořit skript, který nelze číst, ale má práva na spuštění, zkuste jej spustit. Proč to u shellového skriptu nejde?
    (Ukázat řešení)
    #!/bin/sh
    cat >/tmp/skript.sh <<EOF
    #!/bin/sh

    echo Ahoj světe
    EOF


    chmod a+x-r /tmp/skript.sh
    /tmp/skript.sh
    # Aby mohl skript shell spustit, musí jej být schopen přečíst,
    # k čemuž musí mít oprávnění.
  13. Vytvořte adresář, do kterého může kdokoli přejít a přidávat soubory, ale jen vlastník smí vypsat obsah adresáře.
    (Ukázat řešení)
    #!/bin/sh

    mkdir /tmp/adresar
    chmod 0755 /tmp/adresar

    #Případně ekvivalentně
    chmod u+rwx,go=u-r /tmp/adresar
  14. Vytvořte adresář, do něhož lze přejít, nelze číst jeho obsah a lze v něm přidávat soubory. Vytvořte v tomto adresáři soubor, k němuž budete mít práva jen ke čtení a zkuste ho smazat.
    (Ukázat řešení)
    #!/bin/sh
    mkdir /tmp/adresar
    chmod 0300 /tmp/adresar
    # Případně ekvivalentně
    chmod u+wx-r,go-rwx /tmp/adresar

    touch /tmp/adresar/soubor
    chmod 0400 /tmp/adresar/soubor
    # Případně ekvivalentně
    chmod u+r-wx,go-rwx /tmp/adresar/soubor

    rm -f /tmp/adresar/soubor
  15. Okopírujte /etc/passwd do /tmp jednak pomocí cp, jednak pomocí cp -p a koukněte se pomocí ls -l, jaký je mezi těmito kopiemi rozdíl.

Domácí úkoly

  1. Napište příkaz, kterým všem souborům v podstromu adresáře /tmp/adresar přidáte právo ke čtení všem členům skupiny a současně právo ke čtení zruší ostatním (tj. těm, kdo nejsou vlastníci ani členové skupiny). Použijte jediné volání chmod. [1 bod]
  2. Vytvořte soubor a přidělte mu práva tak, aby jej mohl vlastník spustit, číst i zapisovat do něj, členové skupiny jej mohli spustit a číst, ostatní jej mohou jen číst. V příkazu chmod použijte jednak symbolický zápis práv [1 bod] a jednak octalový zápis práv [1 bod].

4. cvičení (15. března 2018)

Jednoduché utility.

Příkazy: sort, diff, comm, split.

Příklady

  1. Vypište názvy souborů, které se nacházejí /usr/bin i v /bin.
    (Ukázat řešení)
    #!/bin/sh

    ls /usr/bin | sort >/tmp/usr_bin
    ls /bin | sort | comm -13 - /tmp/usr_bin
    rm /tmp/usr_bin
  2. Vypište čísla skupin, která jsou v /etc/group, ale nejsou použita v /etc/passwd.
    (Ukázat řešení)
    #!/bin/sh

    cut -d: -f4 </etc/passwd | sort > /tmp/uid
    cut -d: -f3 </etc/group | sort | comm -23 - /tmp/uid
    rm /tmp/uid
  3. V souboru calories.csv nahraďte čárky pomocí pomlčky a pomocí diff se podívejte, které řádky byly změněny.
    (Ukázat řešení)
    #!/bin/sh

    tr "," "-" <calories.csv | diff calories.csv -
  4. Soubor calories.csv seřaďte podle množství kalorií sestupně, první řádka s hlavičkou musí zůstat na svém místě.
    (Ukázat řešení)
    #!/bin/sh

    # Uložíme hlavičku
    head -n 1 calories.csv >/tmp/hlavicka
    # Zbytek seřadíme a přidáme
    tail -n +2 calories.csv | sort -t\; -k4,4nr >>/tmp/hlavicka
    mv /tmp/hlavicka calories.csv
  5. Soubor calories.csv seřaďte podle trojice (protein, karbohydráty, tuk) vzestupně. První řádek s hlavičkou musí zůstat na svém místě.
    (Ukázat řešení)
    #!/bin/sh

    # Uložíme hlavičku
    head -n 1 calories.csv >/tmp/hlavicka
    # Zbytek seřadíme a přidáme
    tail -n +2 calories.csv | sort -t\; -k7,7n -k6,6n -k5,5n >>/tmp/hlavicka
    mv /tmp/hlavicka calories.csv
  6. Ze souboru calories.csv vypište počet různých použitých jednotek (druhý sloupec). Nejprve můžete rozlišovat i mezi dvěma jednotkami s různým množstvím, tedy „1 Cup“ a „2 Cup“ by byly různé jednotky. Potom zkuste postup upravit tak, aby se počítal skutečně počet různých jednotek, kde „Cup“ je jednotka do které se počítají „1 Cup“ i „2 Cup“.
    (Ukázat řešení)
    #!/bin/sh

    # V prvním řešení rozlišujeme i mezi dvěma jednotkami s různým
    # množstvím (tj. např. 1 Cup a 2 Cup jsou brány jako různé)
    tail -n +2 calories.csv | sort -t\; -k2,2 -u | wc -l

    # Pokud bychom chtěli brát dvě jednotky s různým množstvím jako
    # shodné (tj. nechceme například rozlišovat mezi 1 Cup a 2 Cup),
    # pak můžeme postupovat následovně:
    tail -n +2 calories.csv | cut -f2 -d\; | cut -f2- -d" " | sort -u | wc -l
  7. Je známo, že v Los Angeles je osoba, která zneužívá sociální dávky. Předpokládejte, že na vstupu jsou tři soubory, které obsahují
    1. seznam osob, které dostávají sociální dávky,
    2. seznam herců a
    3. seznam obyvatelů Beverly Hills.
    Jméno každé osoby je na zvláštní řádce. Hledaná osoba se vyskytuje ve všech třech seznamech, napište skript, který nalezne alespoň jednu takovou osobu. Příklad souborů: social.txt, actor.txt, beverly_hills.txt.
    (Ukázat řešení)
    #!/bin/sh

    sort social.txt >/tmp/s_social.txt
    sort beverly_hills.txt >/tmp/s_beverly_hills.txt
    sort actor.txt | comm -12 /tmp/s_social.txt - | comm -12 - /tmp/s_beverly_hills.txt
    rm /tmp/s_social.txt /tmp/s_beverly_hills.txt.
  8. Rozdělte soubor na kusy po pěti řádcích a pak jej zase spojte do jednoho souboru.
    (Ukázat řešení)
    #!/bin/sh

    # Budeme dělit soubor /etc/passwd
    # Předpokládáme přitom, že /etc/passwd obsahuje jen tolik záznamů,
    # aby se vešly do souborů s dvoupísmennými identifikátory názvu.
    split -l5 /etc/passwd kusypasswd
    # Spojený soubor uložíme do aktuálního adresáře, protože k zápisu do
    # /etc nemáma práva
    cat kusypasswd* >passwd
    rm kusypasswd*
  9. Vypište loginy ze souboru /etc/passwd do deseti řádků, na každém řádku jsou loginy oddělené mezerami.
    (Ukázat řešení)
    #!/bin/sh

    # Předpokládáme, že /etc/passwd obsahuje jen tolik záznamů,
    # abychom si vystačili s dvoupísmennými identifikátory souborů.
    cut -d":" -f1 /etc/passwd | split -l10 - loginy
    paste -d" " loginy*
    rm loginy*
  10. V kopii souboru /etc/group nahraďte každý login (v loginu tam není čárka, „:“ a odřádkování) v seznamu členů skupiny jediným znakem „@“. Ostatní sloupce zůstanou beze změny.
    (Ukázat řešení)
    #/bin/sh

    cut -d: -f4 /etc/group | tr -sc ',\n' '[@*]' >/tmp/memb.$$
    cut -d: -f-3 /etc/group | paste -d: - /tmp/memb.$$ >group
    rm /tmp/memb.$$
  11. Vypište název skupiny s největším počtem členů, kteří jsou u ní uvedeny v /etc/group (tedy ne nutně nejvyšším počtem členů vůbec). Navazuje na předchozí příklad, můžete tedy předpokládat, že máte k dispozici soubor z předchozího příkladu.
    (Ukázat řešení)
    #/bin/sh

    sort -t: -k4,4r group | head -n 1 | cut -d: -f1

Domácí úkoly

  1. Určete login uživatele, který má v /etc/passwd nejvyšší číslo uid. [1 bod]
  2. Určete počet různých států, které se vyskytují v souboru ip-by-country.csv. [1 bod]
  3. Na základě obsahu souborů countrycodes_en.csv a kodyzemi_cz.csv vypište názvy států, které se jmenují stejně v angličtině jako v češtině. [1 bod]

5. cvičení (22. března 2018)

Příkazy: join, find, xargs.

Příklady:

  1. Vypište dvojice oddělené dvojtečkou, kde v prvním poli je login uživatele a v druhém je jméno jeho primární skupiny. K vygenerování seznamu použijte příkaz join na vhodně předupravené soubory /etc/passwd a /etc/group.
    (Ukázat řešení)
    #!/bin/sh

    sort -t: -k3,3 /etc/group >skupiny
    sort -t: -k4,4 /etc/passwd | join -1 3 -2 4 -t: -o 2.1,1.1 skupiny -
    rm skupiny
  2. Stáhněte si soubory countrycodes_en.csv a kodyzemi_cz.csv. Obojí jsou soubory ve formátu CSV, kde je jako oddělovače sloupců použito středníku.
  3. Ze souborů countrycodes_en.csv a kodyzemi_cz.csv vytvořte výpis zemí se dvěma sloupci oddělenými rovnítkem ve tvaru
    český název=anglický název (bez uvozovek).
    (Ukázat řešení)
    #!/bin/sh

    tr -d '"' <kodyzemi_cz.csv | tail -n +2 | sort -t \; -k1,1 >/tmp/kody_s
    tr -d '"' <countrycodes_en.csv | tail -n +2 | sort -t \; -k4,4 >/tmp/codes_s
    join -t\; -1 1 -2 4 -o 1.4,2.1 /tmp/kody_s /tmp/codes_s | tr \; = >preklad
    rm /tmp/kody_s /tmp/codes_s
  4. Napište příkaz, který vypíše všechny soubory nebo adresáře, které mají ve jménu podřetězec 'bin' v podstromu aktuálního adresáře.
    (Ukázat řešení)
    #!/bin/sh

    find . -name "*bin*" "(" -type d -o -type f ")
  5. Napište příkaz, který vypíše počet všech adresářů v podstromu /etc.
    (Ukázat řešení)
    #!/bin/sh

    find /etc -type d | wc -l
  6. Vypište všechny symbolické linky z adresáře /etc, nezahrnujte celý podstrom, jen to, co je přímo v adresáři /etc.
    (Ukázat řešení)
    #!/bin/sh

    find /etc -type l '!' -path "/etc/*/*"

    # nebo

    find /etc/* -prune -type l
  7. Z podstromu adresáře /usr/bin vypište soubory, na které ukazují alespoň tři hardlinky.
    (Ukázat řešení)
    #!/bin/sh

    find /usr/bin -links +3
  8. V podstromu adresáře /tmp najděte všechny soubory, které jsou větší než sto kilobyte a jsou čitelné pro všechny.
    (Ukázat řešení)
    #!/bin/sh

    find /tmp -type f -size +200 -perm -a+r
  9. V podstromu adresáře /usr/include najděte soubory, které jsou v podstromu adresářů v hloubce alespoň 2 a nejvýš 3.
    (Ukázat řešení)
    #!/bin/sh

    find /usr/include -path "/usr/include/*/*" ! -path "/usr/include/*/*/*/*"

    # Efektivnější řešení s pomocí prune:
    find /usr/include -path "/usr/*/*/*" -prune -o -path "/usr/*/*"
  10. V adresáři /etc najděte soubory, které jsou novější než /etc/passwd.
    (Ukázat řešení)
    #!/bin/sh

    find /etc -newer /etc/passwd
  11. V podstromu adresáře /bin najděte soubory, které vlastní root a jsou spustitelné jen pro vlastníka a členy skupiny, nikoli pro ostatní.
    (Ukázat řešení)
    #!/bin/sh

    find /bin -user root -perm -ug+x ! -perm -o+x
  12. V adresáři /etc najděte soubory, které patří skupině stunnel a pro každý vypište dlouhé informace pomocí ls -l.
    (Ukázat řešení)
    #!/bin/sh

    find /etc -group stunnel -exec ls -l {} +
  13. Zkuste rozdíl mezi echo $PATH; echo "$PATH"; echo '$PATH';
  14. Zkuste rozdíl mezi echo *; echo "*"; echo '*';
  15. Vypište seznam cest obsažených v $PATH s tím, že bude každá na samostatném řádku a ve výpisu nebudou obsaženy oddělovací znaky „:“.
    (Ukázat řešení)
    #!/bin/sh

    echo $PATH | tr ":" "\n"
  16. Vypište dlouhý výpis adresářů obsažených v $PATH, lépe řečeno dlouhé informace o nich, ne jejich obsah.
    (Ukázat řešení)
    #!/bin/sh

    echo $PATH | tr ":" "\n" | xargs -I{} ls -ld {}
  17. Vytvořte soubor, který obsahuje 3 řádky:
    a b c
    d e f
    g h i
    
    a na něm zkuste následující příkazy:
    cat soubor | xargs echo
    cat soubor | xargs echo -
    cat soubor | xargs -n 2 echo -
    cat soubor | xargs -I{} echo "-{}-" "-{}-"
    cat soubor | xargs -I{} -n 2 echo "-{}-" "-{}-"
    
  18. Rozdělte soubor /etc/passwd na části po pěti řádcích a potom tyto části poskládejte v opačném pořadí do jiného souboru, (to jest nejprve jde posledních pět řádků v pořadí jako v /etc/passwd, potom předposledních pět atd.)
    (Ukázat řešení)
    #!/bin/sh

    split -l5 /etc/passwd kusypasswd
    ls -r kusypasswd* | xargs cat >passwd
    rm kusypasswd*
  19. Zjistěte, ve kterých adresářích uložených v proměnné $PATH se vyskytuje program awk.
    (Ukázat řešení)
    #!/bin/sh

    echo $PATH | tr ':' '\n' | xargs -I{} find {}/awk -prune 2>/dev/null

Domácí úkoly

  1. V podstromu aktuálního adresáře najděte soubory, které mají nulovou velikost. [1 bod]
  2. V podstromu adresáře /usr/include najděte soubory, jejichž jméno začíná řetězcem "std" a nekončí příponou ".h". [1 bod]
  3. Napište příkaz, který vymaže z podstromu adresáře /tmp všechny soft linky.

6. cvičení (28. března 2018)

Příkazy: grep, sed, nl.

Příklady:

  1. Ze souboru calories.csv vyberte řádky, v nichž název potraviny obsahuje znak, který není alfanumerický (tj. nepatří do třídy [:alnum:]).
    (Ukázat řešení)
    #!/bin/sh

    grep '^"[^;]*[^[:alnum:];][^;]*";' calories.csv

    # Případně

    grep -v '^"[[:alnum:]]*";' calories.csv
  2. Ze souboru calories.csv vyberte řádky, jejichž množství je měřeno jednotkou „Piece“ nebo „Cake“.
    (Ukázat řešení)
    #!/bin/sh

    grep -e '^[^;]*;[[:digit:].]* Piece;' \
         -e '^[^;]*;[[:digit:].]* Cake;' calories.csv

    # Případně

    grep -E '^[^;]*;[[:digit:].]* (Piece|Cake);' calories.csv
  3. Z podstromu adresáře /usr/include vyberte soubory, jejichž názvy obsahují 5-7 znaků, nepočítáme-li příponu, která má být „.h“. (Použijte grep na výstup příkazu find.)
    (Ukázat řešení)
    #!/bin/sh

    find /usr/include -name "*.h" | grep '/[^/]\{5,7\}\.h'
  4. Z výpisu ls -l vyberte řádky, u nichž má vlastník nastavena stejná práva jako skupina i jako ostatní. (Případně si odpovídající soubor například v /tmp vytvořte.)
    (Ukázat řešení)
    #!/bin/sh

    ls -l | grep '.\(...\)\1\1'
  5. Vypište soubory, které se vyskytují v podstromu adresáře /usr/include a obsahují řetězec „printf“.
    (Ukázat řešení)
    #!/bin/sh

    find /usr/include -exec grep -q printf {} \; -print
  6. Pomocí sedu vyberte z výpisu ls -l jen sloupec s velikostí souboru.
    (Ukázat řešení v sedu)
    #!/bin/sh

    ls -l | sed '1d; s/^\([^ ]* *\)\{5\} .*$/\1/'

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    ls -l > /tmp/lsl.$$
    ed /tmp/lsl.$$<<\KONEC
    2,$g/^/s/^\([^ ]* *\)\{5\} .*$/\1/
    %p
    Q
    KONEC
    rm /tmp/lsl.$$
  7. Pomocí sedu přetvořte řádky /etc/passwd do podoby uid (login).
    (Ukázat řešení v sedu)
    #!/bin/sh

    sed 's/^\([^:]*\):[^:]*:\([^:]*\).*$/\2 (\1)/' /etc/passwd

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    # Pokud chceme na konci výsledek uložit do souboru, pak tam bude w a q.

    cp /etc/passwd /tmp/passwd.$$
    ed /tmp/passwd.$$<<\KONEC
    g/^/s/^\([^:]*\):[^:]*:\([^:]*\).*$/\2 (\1)/
    %p
    Q
    KONEC
    rm /tmp/passwd.$$
  8. Ze souboru /etc/passwd vypište jen sudé řádky.
    (Ukázat řešení v sedu)
    sed -n 'n; p' /etc/passwd

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    # Příkaz g provádí následující příkazy na všechny řádky.
    # V rámci něj:
    #    1) +1p vypíše následující řádku a
    #    2) .s/^// odznačí (následující) řádku tak, že ji g vynechá.
    # poslední řádku vynecháváme (předpokládáme, že soubor obsahuje aspoň
    # dvě řádky), protože u ní by +1 lezlo ven ze souboru.
    #
    # Kdybychom použili s///, tak místo prázdného regexpu by g doplnilo
    # svůj regexp a došlo by k vymazání části sudých řádek. Pokud se neukládají
    # změny, tak to nevadí.

    nl /etc/passwd > /tmp/passwd.$$
    ed /tmp/passwd<<\KONEC
    1,$-1g/^/+1p\
    .s/^//
    Q
    KONEC
    rm /tmp/passwd.$$
  9. Na začátky lichých řádek souboru /etc/passwd vložte znak „l“, na začátky sudých řádek vložte znak „s“.
    (Ukázat řešení v sedu)
    #!/usr/bin/sed -f
    # Toto je skript pro sed, použití pomocí sed -f
    # Volání sed -f <skript> /etc/passwd

    s/^/l/
    n
    s/^/s/

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.
    #
    # První příkaz přidá znak „l“ na začátek každé řádky.
    # Druhý příkaz změní znak „s“ na začátku každé sudé řádky.
    # Poté je vypsán výsledek a ed ukončen bez uložení změn.

    ed /etc/passwd <<\KONEC
    g/^/s/^/l/
    1,$-1g/^/+1s/^l/s/
    %p
    Q
    KONEC
  10. Na začátky desáté až dvacáté řádky souboru /etc/passwd přidejte znak #.
    (Ukázat řešení v sedu)
    #!/bin/sh

    sed '10,20s/^/#/' /etc/passwd

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.
    #
    # Změna se neukládá (do /etc/passwd by to nebyl dobrý nápad) a
    # výsledek se jenom vypíše na standardní výstup.

    ed /etc/passwd <<\KONEC
    10,20s/^/#/
    %p
    Q
    KONEC
  11. Před první řádku vstupního souboru vložte řádku s #!/bin/sh.
    (Ukázat řešení v sedu)
    #!/usr/bin/sed -f
    # Toto je skript pro sed, použití pomocí sed -f

    1i\
    #!/bin/sh

    (Ukázat řešení v edu)
    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    ed vstupnisoubor <<\KONEC
    1i
    #!/bin/sh
    .
    w
    q
    KONEC
  12. Napište skript pro sed, který odstraní ze shellového skriptu komentáře, přičemž
    1. Pokud jde o první řádku souboru a pokud tato řádka má formát ^#!.*, pak tento komentář neodstraňujte.
    2. Řádky, kde před # není nic než mezery, odstraňte úplně.
    3. Pokud se před # vyskytují nějaké jiné znaky, odstraňte z řádky vše od prvního výskytu znaku # (včetně) do konce řádky.
    4. Pro jednoduchost předpokládejte, že každý znak # ve skriptu skutečně uvozuje komentář, tedy není v uvozovkách, apostrofech ani uvozený zpětným lomítkem. (Pokud bychom měli ošetřit i tento případ, bylo by to složitější.)
    (Ukázat řešení v sedu)
    #!/usr/bin/sed -f
    # Toto je skript pro sed, použití pomocí sed -f

    # Přeskočení první řádky, pokud jde o komentář typu #!
    1!b neniprvni
    /^#!/n
    :neniprvni

    # Smazání řádek, kde před # nic není
    /^[[:space:]]*#/d

    # Odstranění komentáře z ostatních řádek (ignoruje fakt, že # se může
    # vyskytovat i v rámci řetězce v uvozovkách a pod., kdybychom toto
    # chtěli kontrolovat, bylo by to výrazně komplikovanější).
    s/#.*$//

    (Ukázat řešení v edu)
    #!/bin/sh

    # Nejprve vyřídíme vše kromě prvního řádku.
    # - První příkaz smaže řádky, které před # nic nemají.
    # - Druhý příkaz odstraní z ostatních řádků vše od # dál.
    # Poté vyřešíme první řádek.
    # - Nejprve vymažeme 1. řádek, pokud obsahuje komentář, který před # nic
    #   nemá a který napak za # má nějaký znak, který není !.
    # - Poté smažeme 1. řádek, pokud obsahuje jen #, za nímž ani před nímž
    #   nic není. Pokud předchozí příkaz proběhl úspěšně, dostane se sem 2.
    #   řádek, ale z něj jsme již komentáře odstranili, a tak se nám to
    #   nemůže splést a další příkazy se pro něj nevykonaly. Z tohoto
    #   důvodu jsme také nejprve řešili řádky od 2. do posledního.
    # - Nakonec z prvního řádku odstraníme vše od prvního # do konce, pokud
    #   nemá #! na začátku.
    # Všude je nutné používat g nebo v, protože jinak by s při neúspěšné
    # substituci hlásilo chybu.

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    ed vstupnisoubor.sh <<\KONEC
    2,$g/^[[:space:]]*#/d
    2,$g/#/s/#.*//
    1g/^[[:space:]]*#[^!]/d
    1g/^[[:space:]]*#$/d
    1v/^#!/s/#.*$//
    w
    q
    KONEC
  13. Napište skript pro sed, který otočí pořadí řádek na vstupu.
    (Ukázat řešení v sedu)
    #!/bin/sh

    # Vyzkoušíme na nl /etc/passwd, aby bylo vidět opačné pořadí.
    nl /etc/passwd | sed -n 'G; $p; h'

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    # Zpětné lomítko je za příkazem i nutné proto, že jde o příkaz v rámci
    # příkazu g, jinak by tam nebylo potřeba.

    ed /etc/passwd <<\KONEC
    g/^/m0
    %p
    Q
    KONEC
  14. Mezi každé dvě řádky souboru /etc/passwd vložte prázdnou řádku.
    (Ukázat řešení v sedu)
    #!/bin/sh

    # $b zařídí, že se nebude nic přidávat za poslední řádku (při
    # poslední řádce se skočí na konec skriptu bez vykonání příkazu a).
    # Příkaz a pak provádí samotné přidání nové řádky za aktuální.

    sed '$b; a\
    '
    /etc/passwd

    # Ekvivalentně lze i pomocí i (adresace zabezpečuje, že se nic nepřidá
    # před první řádku):

    sed '2,$i\
    '
    /etc/passwd

    (Ukázat řešení v edu)
    #!/bin/sh

    # Všimněte si, že řetězec KONEC je uvozen zpětným lomítkem, to
    # proto, aby shell neprováděl expanzi v textu, který za ním píšeme.

    # Zpětné lomítko je za příkazem i nutné proto, že jde o příkaz v rámci
    # příkazu g, jinak by tam nebylo potřeba.

    ed /etc/passwd <<\KONEC
    2,$g/^/i\
    \
    .
    %p
    Q
    KONEC

Domácí úkoly

  1. Vypište řádky s titulky <hi> (kde i je číslice od 1 do 6), které jsou v daném html souboru a které se vejdou na jeden řádek (tj. na jednom řádku je jak počáteční tag, tak ukončující tag, mezi tím může být cokoli), předpokládejte syntakticky správný vstupní soubor. [1 bod]
  2. Pomocí sedu ve vstupním textovém souboru nahraďte výskyty znaků '&', '<', '>' pomocí jejich HTML entit ('&' pomocí '&amp;', '<' pomocí '&lt;' a '>' pomocí '&gt;'). [1 bod]
  3. Předpokládejte, že řádky vstupního souboru obsahují jen znaky '(' a ')' (tj. na každé řádce je řetězec vyhovující regulárnímu výrazu „[()]*“), vytvořte skript pro sed, který zkontroluje, zda je řetězec na řádce správně uzávorkovaný. Pokud ano, nahradí řádku řetězcem 'ano', v opačném případě řetězcem 'ne'. [1 bod]

7. cvičení (5. dubna 2018)

Příkazy: ed.

  1. Úkoly 6 až 14 ze 6. cvičení vyřešte také pomocí editoru ed. Řešení najdete u každého ze zmíněných cvičení
  2. Spusťte si vimtutor cs

Domácí úkoly

  1. Napište příkazy pro editor ed, které ze souboru /etc/passwd vypíší posledních deset řádek. [1 bod]
  2. Předpokládejme, že ve vstupním textovém souboru jsou odstavce odděleny prázdnou řádkou. Napište skript pro editor sed nebo ed, který spojí každý odstavec ze vstupu do jedné řádky.
  3. Napište skript pro editor sed, který z každé řádky na vstupu zachová jen prostřední písmeno. Pokud je řádka sudé délky, je zachované jedno ze dvou písmen uprostřed. [1 bod]

8. cvičení (12. dubna 2018)

Příkaz: ps

  1. Pomocí editoru ed nebo sed upravte ve vstupním souboru komentáře typu „/* ... */“, a to jejich jednořádkové i víceřádkové varianty, do následujícího tvaru:
    • Úvodní „/*“ i koncové „*/“ jsou na samostatných řádkách.
    • Řádky uvnitř komentáře jsou uvozeny „ *“ (tj. hvězdičkou odsazenou o jeden znak vpravo).
    Tj. například z
    aaa /* bbb */ ccc /* ddd
    eee
    fff */ ggg
    
    vznikne
    aaa
    /*
     * bbb
     */
    ccc
    /*
     * ddd
     * eee
     * fff
     */
    ggg
    
    Můžete předpokládat, že všechny komentáře jsou správně uzávorkované, tj. mají počáteční i koncovou značku. Pokud už jsou „/*“ nebo „*/“ na samostatných řádkách, neměli byste je odřádkovat znovu.
    (Ukázat řešení v edu)
    #!/bin/sh
    #
    # Soubor je daný parametrem (byť je to trochu předbíhání)
    #
    # Je-li použit příkaz s v rámci příkazu g v edu, není možné v jeho nahrazovací
    # části použít vložené odřádkování, tj. například
    # g//s_\*/\(.\)_*/\
    # \1_
    # nelze standardně použít. Proto skript nejprve na začátek souboru vloží řádek
    # se vzorovým komentářem, díky tomu všechny příkazy s (substitute) uspějí, i
    # když jsou adresovány %, tj 1,$. Tedy neohlásí chybu. Formátováním vloženého
    # řádku vznikne 5 nových řádků, a proto je potom odstraníme pomocí 1,5d

    ed "$1" <<\KONEC
    1i
    111 /* 222 */ 333
    .
    %s_\([^[:space:]]\)[[:space:]]*/\*_\1\
    /*_g
    %s_/\*[[:space:]]*\([^[:space:]]\)_/*\
    \1_g
    g/^[[:space:]]\/\*/s//\/*/
    %s_\([^[:space:]]\)[[:space:]]*\*/_\1\
    */_
    %s_\*/[[:space:]]*\([^[:space:]]\)_*/\
    \1_
    g/\/\*/.,/\*\//s/^/ * /
    g/^ \* \*\//s// *\//
    g/^ \* \/\*/s//\/*/
    1,5d
    w
    q
    KONEC
  2. Pomocí sedu ve vstupním souboru spojte vždy dvě následující řádky do jedné (tj. odstraňte odřádkování mezi nimi). Ve výsledku tedy bude spojena 1. s 2. řádkou, 3. se 4. řádkou, 5. s 6. řádkou, atd.
    (Ukázat řešení v sedu)
    #!/bin/sh

    sed '$!N; s/\n/ /'
  3. Ve všech souborech s příponou ".h" nebo ".c" v podstromu aktuálního adresáře nahraďte výskyt řetězce "counter" řetězcem "citac". Řetězec nahrazujte jen tehdy, pokud není součástí delšího slova skládajícího se z alfanumerických znaků a "_". (Tj. před ním a za ním je buď jiný znak, nebo počátek či konec řádku. Pro uložení řádky do odkládacího souboru můžete v sedu použít příkaz "w".)
    (Ukázat řešení)
    #!/bin/sh

    find . -name '*.[hc]' -exec sed -n "s/^counter$/citac/
    s/\([^[:alnum:]_]\)counter$/\1citac/
    s/^counter\([^[:alnum:]_]\)/citac\1/
    s/\([^[:alnum:]_]\)counter\([^[:alnum:]_]\)/\1citac\2/
    w /tmp/mytemp.$$"
    {} \; \
    -exec mv /tmp/mytemp.$$ {} \;
  4. Číslo v textovém souboru definujeme jako posloupnost číslic ([:digit:]), která může být uvozena znakem ‚+‘ nebo ‚-‘. Tato posloupnost je ohraničena prázdnými znaky ([:space:]), počátkem řádku nebo koncem řádku. Úvodní nuly nehrají roli pro určení hodnoty čísla. Například na řádce
    12 -31 aaaa b01 001 a-13 0-5
    
    jsou tři čísla s hodnotami 12, -31, 1. Napište příkazy shellu, jež určí počet po dvou různých čísel v absolutní hodnotě (jelikož úvodní nuly jsou ignorovány, pak všechna následující čísla se počítají jako shodná: -31, 31, 0031 +0000031). Jako příklad vstupu můžete použít soubor formula.cnf, který obsahuje popis formule v konjunktivně normální formě. První řádek tvoří hlavičku, každý další řádek (není-li uvozen znakem ‚c‘) pak popisuje klauzuli. Pokud bychom si odmysleli hlavičku a 0 ukončující každou řádku, počet po dvou různých čísel je počtem různých proměnných, které se v souboru vyskytují.
    (Ukázat řešení)
    #!/bin/sh

    echo 'xa 12 -31 aaaa b01 001 a-13 0-5' | sed '
    # Přidáme si mezeru na konec řádku
    s/$/ /

    # a na začátek řádku, to proto, abychom měli slova ohraničená mezerami.
    s/^/ /

    # Odstraníme slova, která obsahují znak, jenž není číslice, + ani -
    s/[[:space:]][^[:space:]]*[^[:digit:][:space:]+-][^[:space:]]*//g

    # Odstraníme slova, která obsahují + nebo - uprostřed už po nějakých číslicích
    # nebo úvodním znaménku.
    s/[[:space:]][[:digit:]+-]\{1,\}[+-][[:digit:]+-]*//g

    # Odstraníme úvodní nuly z každého čísla
    s/[[:space:]]0*/ /g

    # Odstraníme znaky + a -
    s/[-+]//g

    # Odstraníme mezery ze začátku řádku ...
    s/^[[:space:]]*//

    # ... a z konce řádku
    s/[[:space:]]*$//

    # Nahradíme posloupnost mezer pomocí odřádkování,
    # tak abychom mohli výsledný seznam čísel setřídit.
    s/[[:space:]]\{1,\}/\
    /g'
    | sort -u | wc -l
  5. Pomocí příkazu ps a dalších příkazů shellu určete PID (pid) a název (comm) procesu, který strávil nejvíce času na procesoru (time).
    (Ukázat řešení)
    #!/bin/sh

    ps -eo time=,pid=,comm= |\
       sed 's/^ *\([[:digit:]]*\):\([[:digit:]]*\).\([[:digit:]]*\)/\1:\2:\3/' |\
       sort -t: -k1,3 | cut -d ' ' -f2-

Domácí úkoly

  1. Napište skript pro ed, který přesune všechny řádky, jež začínají znakem '#' na konec souboru. [1 bod]
  2. V souboru morse.csv najdete přepis znaků anglické abecedy a číslic do Morseovy abecedy. Napište příkazy shellu které vytvoří na základě tohoto souboru skript pro sed, který potom může být použit k překladu textu zapsaného pomocí písmen anglické abecedy (malých i velkých) do Morseovy abecedy. Přesněji: Vaším úkolem je napsat skript, který na vstupu očekává soubor morse.csv a jeho výstupem je skript translate.sed tak, aby k překladu textu do Morseovy abecedy bylo možno použít volání
    sed -f translate.sed input >output

    [2 body]

9. cvičení (19. dubna 2018)

Příkazy: printf, read, řídící struktury (for, while, if, case, a pod.), expr, test, dirname, basename.

Další: Proměnné a expanze, přesměrování, poziční a speciální parametry ($#, $0, $n, ${n}, shift, "$@", set -, $?, $$, $!), kombinace příkazů pomocí &&, složený příkaz a vyvolání subshellu, zpětné apostrofy a $().

  1. Napište skript, který očekává dva parametry, cestu k souboru a číslo řádky. Skript potom vymaže řádku s daným číslem ze daného souboru.
    (Show solution)
    #!/bin/sh
     ed "$1" <<KONEC
     ${2}d
    w
    q
    KONEC
  2. Vytvořte skript, který dostane n+1 parametrů, první obsahuje číslo x mezi 1 a n, skript vypíše parametr s číslem (x+1). Tj. například, skript 2 a b c vypíše b, tedy ten třetí parametr (ale druhý, když budete počítat od a). (Použijte shift s parametrem.)
    (Ukázat řešení)
    #!/bin/sh

    shift $1
    echo $1
  3. Napište skript reverse, který vypíše své parametry na standardní výstup v opačném pořadí. Například reverse a b c d vypíše d c b a.
    (Ukázat řešení)
    #!/bin/sh

    for x
    do
       y="$x $y"
    done
    echo "$y"
  4. Napište skript max, jenž očekává v parametrech čísla a vypíše jejich maximum. Například max 1 15 7 19 11 vypíše 19.
    (Ukázat řešení)
    #!/bin/sh

    # Bez test, s použitím sort
    for x
    do
       echo "$x"
    done | sort -nr | head -n 1

    # S použitím sort, nyní se spoléháme na to, že parametry
    # obsahují jen čísla a neobsahují mezery.
    echo "$@" | tr " " '\n' | sort -nr | head -n 1

    # S použitím test
    m="$1"
    shift
    for x
    do
       if [ "$x" -gt "$m" ]
       then
          m="$x"
       fi
    done
    echo "$m"

    # S použitím aritmetické expanze
    m="$1"
    shift
    for x
    do
       m=$((x>m ? x : m))
    done
    echo "$m"
  5. Napište skript, který očekává jeden až dva číselné parametry. První číselný parametr pak zarovná na počet míst zadaných druhým číselným parametrem. Pokud druhý parametr není zadaný, implicitně se rozumí 5. (Použijte printf a expanzi pomocí ${proměnná:-slovo} a podobné.)
    (Ukázat řešení)
    #!/bin/sh

    printf "%${2:-5}d\n" $1
  6. Co vypíší následující příkazy:
    a="ahoj"; { echo $a ; a="cau" ; echo $a ; } ; echo $a
    b="ahoj"; ( echo $b ; b="cau" ; echo $b ; ) ; echo $b
    x="ahoj"; x="cau" sh -c 'echo $x'; echo $x
    y="ahoj"; y="cau"; sh -c 'echo $y'; echo $y
    z="ahoj"; sh -c 'echo $z'
    export u="ahoj"; sh -c 'echo $u'
  7. Rozmyslete si, co dělá následující příkaz:
    a=1;b=2; { a=LEVY; echo $a >&2; } |\
    { b=PRAVY; echo $b; tr 'A-Z' 'a-z'; }; echo "A=$a,B=$b"
    a jak se liší od
    a=1;b=2; { a=LEVY; echo $a; } |\
    { b=PRAVY; echo $b; tr 'A-Z' 'a-z'; }; echo "A=$a,B=$b"

Domácí úkoly

  1. Napište skript, který dělá totéž, co cp, ale s opačným pořadím parametrů (cíl je jako první, nezapomeňte, že obecně cp přijímá n+1 parametrů s tím, že je-li n>1, pak poslední parametr cp musí být adresář. ) [1 bod].
  2. Napište skript, který očekává tři parametry, soubor, číslo n
  3. a znak c. Skript potom provede cyklický posun doleva řádek podle položek oddělených znakem c s krokem n. Tj. například při volání
    skript /etc/passwd 2 :
    by každou řádku tvaru
    login:*:uid:gid:jméno:domovský adresář:shell
    upravil na řádku tvaru
    uid:gid:jméno:domovský adresář:shell:login:*
    Upravený soubor vypisuje skript na standardní výstup, soubor zadaný v parametru neupravuje. [2 body]

10. cvičení (26. dubna 2018)

Příkazy: read, expr, test, dirname, basename.

  1. Spočítá následující skript správně počet řádků souboru /etc/passwd?
    pocet=0
    cat /etc/passwd | while read x
    do
       pocet=`expr $pocet + 1`
    done
    echo $pocet
    A co následující skript?
    cat /etc/passwd | {
       pocet=0
       while read x
       do
          pocet=`expr $pocet + 1`
       done
       echo $pocet
    }
    A co následující skript?
    pocet=0
    while read x
    do
       pocet=`expr $pocet + 1`
    done </etc/passwd
    echo $pocet
    A konečně co následující skript?
    pocet=0
    while read x </etc/passwd
    do
       pocet=`expr $pocet + 1`
    done
    echo $pocet
    Jaký je mezi těmito skripty rozdíl?
  2. Napište skript, který dostane v parametrech seznam souborů zadaný cestami, které můžou být i relativní. Ke každému souboru vypíše řádek s absolutní cestou.
    (Ukázat řešení)
    #!/bin/sh

    # Je-li část in ... u for cyklu vynechaná, doplní se in "$@"
    for x
    do
       adresar=$(dirname "$x")
       adresar=$(cd $adresar && pwd)
       echo $adresar/$(basename $x)
    done
  3. Napište skript, který přejmenuje všechny soubory v aktuálním adresáři na soubory se týmž jménem, kde jsou velká písmena změněná na malá. (Například soubor 'ZALOHA.ZIP' by přejmenoval na 'zaloha.zip')
    (Ukázat řešení)
    #!/bin/sh

    for x in *[A-Z]*
    do
       mv "$x" "`echo $x | tr '[:upper:]' '[:lower:]'`"
    done
  4. Napište skript, který přejmenuje všechny soubory v aktuálním adresáři s příponou ".jpeg" na soubory s týmž jménem, ale příponou ".jpg". (Zkuste využít expanze ${proměnná%přípona}, sed, nebo dirname a basename.)
    (Ukázat řešení)
    #!/bin/sh

    # Varianta pomocí expanze (není ve standardu moc dlouho)
    for x in *.jpeg
    do
       mv "$x" "${x%jpeg}jpg"
    done

    # Varianta pomocí sedu:
    for x in *.jpeg
    do
       mv "$x" "$(echo $x | sed 's/\.jpeg$/.jpg/')"
    done

    # Varianta pomocí basename a dirname.
    for x in *.jpeg
    do
       mv "$x" "$(dirname "$x")/$(basename "$x" .jpeg).jpg"
    done
  5. Napište skript, který dostane tři parametry, všechny číselné, a vypíše na standardní výstup čísla oddělená mezerami počínaje $1, konče $2 a s krokem $3. Pokud skript dostane jen dva parametry, pak se implicitně použije krok 1.
    (Ukázat řešení)
    #!/bin/sh

    if [ $# -lt 2 ] || [ $# -gt 3 ]
    then
       echo Špatný počet parametrů
       exit 1
    fi

    krok=${3:-1}

    x=$1
    while [ $x -le $2 ]
    do
       echo $x
       x=`expr $x + $krok`
    done
  6. Totéž jako předchozí příklad, ale čísla budou každé na zvláštní řádce a budou zarovnaná na počet míst daný délkou nejdelšího čísla (tj. délkou $2). Čísla budou zarovnaná nulami na začátku. Tj. například
    skript 0 10 2
    by vypsal:
    00
    02
    04
    06
    08
    10
    
    (Ukázat řešení)
    #!/bin/sh
    if [ $# -lt 2 ] || [ $# -gt 3 ]
    then
       echo Špatný počet parametrů
       exit 1
    fi

    krok=${3:-1}

    x=$1
    delka=${#2}
    while [ $x -le $2 ]
    do
       printf "%0${delka}d\n" $x
       x=`expr $x + $krok`
    done
  7. Napište skript, který přejmenuje všechny soubory v aktuálním adresáři s příponou jpg na soubory s touž příponou ale s čísly místo názvů, kde čísla budou zarovnaná na tolik míst, kolik je třeba pomocí nul. (Využijte toho, co jste už udělali v jednom z předchozích příkladů.) Tj. například pokud jsou v adresáři soubory alice.jpg, bob.jpg cyril.jpg a daniel.jpg, přejmenoval by je popořadě skript na 1.jpg, 2.jpg, 3.jpg a 4.jpg. Pokud by těchto souborů bylo alespoň deset ale méně než 100, byly by pojmenovány jako 01.jpg, 02.jpg, 03.jpg, 04.jpg, ...
    (Ukázat řešení)
    #!/bin/sh
    x=1
    pocet=`ls ./*.jpg | wc -l`

    # Tohle je pro případ, že by tam byl nějaký soubor s příponou *.jpg a
    # číselným názvem, mohlo by to selhat.
    for soubor in *.jpg
    do
       mv "$soubor" "x$soubor"
    done
    for soubor in *.jpg
    do
       mv "$soubor" `printf "%0${#pocet}d\n" $x`.jpg
       x=`expr $x + 1`
    done
  8. Napište skript, který očekává na standardním vstupu řádky ve tvaru:
    a+b=c
    kde a, b a c jsou čísla, skript načte pro každou řádku tato tři čísla a zkontroluje, jestli je rovnost správná. Pokud není správná, ohlásí chybu s číslem řádky, na které k ní došlo. Na závěr vypíše celkový počet chyb. (Doporučení: Použijte shellový while cyklus s read a b c a vhodnou volbou IFS (+=)).
    (Ukázat řešení)
    #!/bin/sh
    IFS="+="

    radka=0

    while read a b c
    do
       radka=$(expr $radka + 1)
       if [ "$(expr "$a" + "$b")" -ne "$c" ]
       then
          printf "Chyba na řádce %d (%d+%d<>%d).\n" $radka $a $b $c
       fi
    done
    printf "Počet zkontrolovaných řádek: %d\n" $radka
  9. Rozmyslete si, co dělá následující příkaz, co se vypíše do out a co do err a proč.
    { { { echo 1aaa; echo 2bbb >&2;} 3>&1 1>&2 2>&3 |\
    { tr 'a-z' 'A-Z'; echo 2tr >&2; } 2>&1; } \
    3>&1 1>&2 2>&3; }>out 2>err
  10. Co vypíší následující příkazy na obrazovku?
    sh -c "exit 1" | sh -c "exit 2"
    echo $?

    { { { sh -c "exit 1"; echo $? >&3; } |\
    sh -c "exit 2" >&4; } 3>&1 | { read e; exit $e; }; } 4>&1
    echo $?

    { { { sh -c "exit 1"; chyba=$?; } |\
    sh -c "exit 2" >&4; } 3>&1 | { exit $chyba; }; } 4>&1
    echo $?

Domácí úkoly

  1. Napište skript sum, který vypíše součet parametrů, které dostane na vstupu. Například sum 4 13 112 7 vypíše 136. [1 bod]
  2. Napište skript rmexcept, který očekává n+1 parametrů, kde první parametr je adresář a další parametry jsou řetězce popisující názvy souborů (může jít o shellové masky) v tomto adresáři. Skript v zadaném adresáři vymaže všechny soubory, jejichž názvy nevyhovují maskám zadaným v parametrech. Soubory, které vyhovují zadaným maskám a názvům, skript nechá beze změny, přičemž se jich ani netkne (tedy speciálně nebude je přesouvat mimo a pak zpět). Například volání
    rmexcept . '*.jpg' '*.png'
    by v aktuálním adresáři vymazalo všechny soubory, které nemají příponu jpg ani png. [2 body]

11. cvičení (3. května 2018)

  1. Napište skript, který na vstupu očekává řádky tvaru:
    cil : z1 z2 .... zn
    kde cil, z1, z2, ..., zn jsou názvy souborů. Ze vstupu vypíše skript ty cíle, které jsou starší než některý ze souborů z1, ..., zn na témže řádku (závislosti). (Parametr -nt příkazu test není standardní, použijte find s parametrem -newer.)
    (Ukázat řešení)
    #!/bin/sh

    # Řešení by nefungovalo, kdybychom chtěli v názvech souborů povolit
    # mezery, ale pak bychom museli uvažovat nějaké backslashování mezer,
    # proto tento případ pomíjíme.

    while read cil dvojtecka soubory
    do
       set -- $soubory
       if [ "$(find "$@" -newer "$cil" -prune)" ]
       then
          echo $cil
       fi
    done

    # Alternativní řešení pomocí ls -t:
    while read cil dvojtecka soubory
    do
       if [ "$(ls -t "$cil" $soubory | head -n 1)" != "$cil" ]
       then
          echo $cil
       fi
    done
  2. Napište skript newfrom, který dostane jména dvou adresářů DIR1 a DIR2. Soubory z DIR1, které v DIR2 nejsou nebo jsou novější než v DIR2, zkopíruje do DIR2. Skript pracuje rekurzivně i v podadresářích. Pokud přidáme volitelný přepínač -n, skript nebude kopírovat, bude pouze vypisovat, které soubory by se měly zkopírovat. Pokud DIR2 neexistuje, tak by měl být vytvořen, což platí i pro podadresáře.
    (Ukázat řešení)
    #!/bin/sh

    usage() {
       echo "Použití: $0 [-n] dir1 dir2"
       exit 1
    }

    if [ "(" $# -eq 3 ")" -a "(" "$1" == "-n" ")" ]
    then
       DRY_RUN=1
       shift
    else
       DRY_RUN=0
    fi

    if [ $# -ne 2 ]
    then
       usage
    fi

    DIR1="$1"
    DIR2="$2"

    if [ ! -d "$DIR1" ]
    then
       echo "$DIR1 není adresář"
       usage
    fi

    if [ ! -d "$DIR2" ]
    then
       if [ $DRY_RUN -eq 0 ]
       then
          cp -pR "$DIR1" "$DIR2"
       else
          echo cp -pR "$DIR1" "$DIR2"
       fi
    else
       ( cd "$DIR1" && find . -type d -path "*/*" ) >/tmp/newfrom_temp.$$
       while read line
       do
          rel_path="`echo "$line" | cut -d / -f 2- `"
          if [ ! -d "$DIR2/$rel_path" ]
          then
             if [ $DRY_RUN -eq 0 ]
             then
                mkdir "$DIR2/$rel_path"
             else
                echo "mkdir $DIR2/$rel_path"
             fi
          fi
       done</tmp/newfrom_temp.$$

       ( cd "$DIR1" && find . -type f -path "*/*" ) >/tmp/newfrom_temp.$$
       while read line
       do
          rel_path="`echo "$line" | cut -d / -f 2- `"
          if [ "`find "$DIR1/$rel_path" -newer "$DIR2/$rel_path"`" ]
          then
             if [ $DRY_RUN -eq 0 ]
             then
                cp -p "$DIR1/$rel_path" "$DIR2/$rel_path"
             else
                echo "cp -p $DIR1/$rel_path $DIR2/$rel_path"
             fi
          fi
       done</tmp/newfrom_temp.$$

       rm /tmp/newfrom_temp.$$
    fi
  3. Napište skript cutreorder, který se bude chovat podobně jako cut, bude mít parametry -d a -f, které budou mít týž formát a význam jako u cut, v dalších parametrech budou názvy souborů (pokud nebudou uvedeny, čte se standardní vstup). Rozdíl je v tom, že skript cutreorder na výstupu umístí sloupce v pořadí daném parametrem -f (v cut jsou sloupce vypisované v pořadí, v jakém jsou ve vstupním souboru). Například
    • cutreorder -d : -f 3,1 /etc/passwd by vypsalo z /etc/passwd do dvou sloupců uid:login.
    • cutreorder -d : -f 3,3-5,1 /etc/passwd by vypsalo sloupce uid:uid:gid:full name:login.
    (Ukázat řešení)
    #!/bin/sh
    # Zpracování parametrů
    # defaultně je delim tabelátor
    delim=" "
    pole=

    while getopts f:d: name
    do
       case $name in
       f) pole="$OPTARG";;
       d) delim="$OPTARG";;
       ?) printf "Use: %s [-d delim] [-f field] files...\n" "$0";;
       esac
    done
    shift $((OPTIND-1))

    if [ "$pole" = "" ]
    then
       # Není co vypisovat
       exit 0
    fi
    # Test, nemáme-li číst standardní vstup
    if [ $# -eq 0 ]
    then
       set -- -
    fi

    # Upravíme si pole do tvaru vhodného pro for cyklus
    pole="`echo $pole | tr , " "`"

    # Zpracování souborů
    for soubor
    do
       cat "$soubor" | while read radek
       do
          vysledek=""
          # Zpracování jednotlivých položek vstupu a výstupu
          for f in $pole
          do
             if [ -n "$vysledek" ]
             then
                vysledek="$vysledek$delim"
             fi
             vysledek="$vysledek$(echo "$radek" | cut -d $delim -f $f)"
          done
          echo "$vysledek"
       done
    done
  4. Napište skript, který ze souborů jejichž názvy jsou zadané v parametrech, vybere n nejdelších řádek, u každé řádky je uvedeno pořadí ve vstupním souboru (ve formátu číslo řádky: řádka), řádky jsou na výstupu uspořádány podle pořadí ve vstupním souboru (tj. podle toho čísla před dvojtečkou na výstupu). Parametr n je implicitně 5, nebo může být zadán jako první jeden až dva parametry pomocí -n10, nebo -n 10 (to je pro příklad n=10, tj. syntaxe je stejná jako třeba u head). Pokud chybí parametry s názvy souborů, čte se standardní vstup. Pokud jsou zadány alespoň dva soubory, pak je výpis nejdelších řádek z každého souboru uvozen názvem souboru.
    (Ukázat řešení)
    #!/bin/sh

    n=5
    if [ "$1" == "-n" ]
    then
       n="$2"
       shift 2
    elif ( echo $1 | grep '^-n[[:digit:]]*$' >/dev/null )
    then
       n=${1#-n}
       shift
    fi

    if [ "$#" == 0 ]
    then
       set -- -
    fi

    for soubor
    do
       if [ "$#" -gt 1 ]
       then
          printf "\n%s:\n" "$soubor"
       fi
       cisloRadky=0
       cat "$soubor" | while read radka
       do
          cisloRadky=$(expr $cisloRadky + 1)
          printf '%d:%d:%s\n' $cisloRadky ${#radka} "$radka"
       done | sort -t: -k2,2nr | head -n $n | cut -d: -f1,3- | sort -k1,1n
    done

Domácí úkol

  1. Napište csvcut, který bude příjímat dva a více parametrů s následujícím významem: 1. parametr je znak oddělovače delim, 2. parametr je řetězec specifikující sloupec v CSV souboru a třetí a další parametry obsahují názvy souborů (pokud chybí, bude se číst standardní vstup). Skript předpokládá, že vstupní soubory jsou CSV soubory s oddělovačem daným prvním parametrem ($delim). Navíc předpokládá, že první řádek každého souboru obsahuje hlavičku. Skript vyřízne z každého vstupního souboru sloupec, jehož název v hlavičkovém řádku je daný druhým parametrem, přičemž ignoruje případné uvozovky kolem názvu. Například volání
    • csvcut ';' Measure calories.csv
      by ze souboru calories.csv vyřízlo druhý sloupec (tj. bylo by ekvivalentní cut -d ';' -f 2 calories.csv).
    • csvcut ';' Food calories.csv
      by z téhož souboru vyřízlo první sloupec (byť jsou v názvu v hlavičce uvozovky kolem Food).
    [3 body]

12. cvičení (10. května 2018)

Příkazy: awk.

  1. Napište awk skript, který vypíše každou desátou řádku vstupu.
    (Ukázat řešení)
    #!/bin/awk -f

    (NR % 10) == 0
  2. Napište awk skript, který v souboru /etc/passwd převede písmena v loginech na velká.
    (Ukázat řešení)
    #!/bin/awk -f

    BEGIN { FS=":"; OFS=":" }
    { $1=toupper($1); print }
  3. Napište awk skript, který otočí pořadí slov na každé řádce vstupu (napíše je v opačném pořadí)
    (Ukázat řešení)
    #!/bin/awk -f

    {
       for (i = NF / 2 ; i > 1; --i)
       {
          printf ("%s" OFS, $i);
       }
       printf ("%s" ORS, $1);
    }
  4. Napište awk skript, který očísluje řádky na vstupu a vypíše je i s čísly (jako nl).
    (Ukázat řešení)
    #!/bin/awk -f

    {
       print NR " " $0;
    }
  5. Napište awk skript, který vypíše tolik náhodných čísel mezi 0 a 1, kolik dostane zadáno parametrem.
    (Ukázat řešení)
    #!/bin/sh
    # Toto je skript, který obaluje awk skript
    # Sám dostane počet potřebných parametrů ve svém parametru.

    awk -v n="$1" 'BEGIN {
       srand();
       for (i=0; i<n; ++i)
       {
          print rand ();
       }
    }'
  6. Napíšte awk skript, který vypíše počet sekund od začátku UNIXové éry. Nápověda: Použijte srand().
    (Ukázat řešení)
    #!/bin/awk -f

    BEGIN { srand(); printf("%d\n", srand()); }
  7. Napište awk skript, který z výstupu getent passwd) vypíše plná jména uživatelů, jejichž login má tvar čtyři písmena následovaná čtyřmi číslicemi.
    (Ukázat řešení)
    #!/bin/awk -f

    BEGIN {
       FS = ":";
    }

    $1 ~ /^[[:alpha:]]{4}[[:digit:]]{4}$/ {
       print $5;
    }
  8. Napište awk skript, který vypíše počet linků, tedy tagů <a ve vstupním html souboru.
    (Ukázat řešení)
    #!/bin/awk -f
    # Jako RS zvolíme "<", protože podle toho se dělí HTML soubor
    # nejvhodněji. Specifikace ani nepřipouští víceznakové RS (v tom
    # případě je výsledek nedefinovaný), regexp může být jen ve FS, tak to
    # ani jinak nejde.
    #
    BEGIN {
       RS = "<";
       pocetAcek = 0;
    }

    ($1 ~ /^a$/) {
       ++ pocetAcek;
    }

    END {
       print pocetAcek;
    }
  9. Napište awk skript, který dostane na vstupu html soubor a vypíše adresy, na které se tento soubor odkazuje (to jest za značku <a href="adresa"> vypíše adresa). Pozor na to, že ta značka může být rozložena přes víc řádků a můžou tam být kolem a, href mezery. Volbou vhodného RS a FS si výrazně usnadníte práci.
    (Ukázat řešení)
    #!/bin/awk -f
    # Je to trochu komplikovanější, aby to fungovalo i v případě, že není
    # href hned za a, ale je mezi tím ještě něco. Jinak by stačilo rovnou
    # brát i=1.

    BEGIN {
       RS = "<";
       FS = "=";
    }

    $1 ~ /^[[:blank:]]*a[[:blank:]]+/ {
       i=1
       while (i <= NF)
       {
          if ($i ~ /[[:blank:]]+href[[:blank:]]*$/)
          {
             split ($(i+1), pole, "[[:blank:]]*\"[[:blank:]]*");
             print pole[2]
          }
          else if ($i ~ />/)
          {
             break;
          }
          ++i;
       }
    }
  10. Napište awk skript, který vypíše posledních 10 řádků souboru.
    (Ukázat řešení)
    #!/bin/awk -f

    BEGIN {
       konecBufferu = 0;
    }

    {
       buffer [konecBufferu] = $0;
       konecBufferu = (konecBufferu + 1) % 10;
    }

    END {
       if (NR > 0)
       {
          if (NR < 10)
          {
             indexVBufferu = 0;
          }
          else
          {
             indexVBufferu = konecBufferu;
          }
          do
          {
             print buffer[indexVBufferu];
             indexVBufferu = (indexVBufferu + 1) % 10;
          }
          while (indexVBufferu != konecBufferu)
       }
    }
  11. Definujme slovo jako maximální posloupnost alfanumerických znaků (tj. znaků z třídy [:alnum:]). Napište awk skript, který přečte zadaný text a po ukončení čtení, vypíše ke každému slovu, jež se vyskytlo v rámci vstupu, počet jeho výskytů.
    (Ukázat řešení)
    #!/bin/awk -f

    BEGIN {
       FS="[^[:alnum:]]"
    }

    {
       for(i=1; i<=NF; ++i)
       {
          if($i!="")
          {
             ++count[$i]
          }
       }
    }

    END {
       for(word in count)
       {
          printf("%s: %d\n", word, count[word])
       }
    }
  12. Napište awk skript, který na vstupu očekává soubor ve formátu csv, tj. na každé řádce jsou položky oddělené čárkami, přičemž je jich na každé řádce stejně. První řádka obsahuje hlavičku, tj. názvy sloupců, první sloupec následujících řádků obsahuje název řádku. Na dalších pozicích jsou čísla. Skript k tomuto vstupu přidá na výstupu nový sloupec se jménem „Součet“, který na každém řádku bude obsahovat součet číselných položek na daném řádku. Tj. například ze vstupu
    Jméno,body1,body2,body3
    Hynek,3,12,9
    Jarmila,7,34,1
    Vilém,8,27,0
    
    vytvoří na výstupu
    Jméno,body1,body2,body3,Součet
    Hynek,3,12,9,24
    Jarmila,7,34,1,42
    Vilém,8,27,0,35
    
    (Ukázat řešení)
    #!/usr/bin/awk -f

    BEGIN { FS=","; OFS="," }
    FNR == 1 { $(NF+1)="Součet"; print }
    FNR > 1 {
     $(NF+1)=0
     for (i=1; i < NF; i++) {
       $NF+=$i
     }
     print
    }

Domácí úkol:

Pokud nastavíte RS na prázdný řetězec, znamená to, že záznamy jsou odděleny prázdnou řádkou a přesně tak odpovídají odstavcům.

  1. Napište awk skript, který čte vstupní text a pokud narazí na řádku začínající znakem #, přidá na začátek každé další řádky až do konce odstavce znak # následovaný mezerou. Řádky, které již začínají znakem #, skript nemění. Odstavce jsou odděleny prázdným řádkem. [1 bod]
    Například vstup
    a b c
    d e f
    # g h i
    j k l
    m n o
    
    p q r
    s t u
    
    # v w x
    # y z 1
    2 3 4
    # 5 6 7
    8 9 0
    
    upraví skript do následující podoby:
    a b c
    d e f
    # g h i
    # j k l
    # m n o
    
    p q r
    s t u
    
    # v w x
    # y z 1
    # 2 3 4
    # 5 6 7
    # 8 9 0
    
  2. Napište awk skript, který formátuje soubor po odstavcích do zadaného počtu sloupců (=znaků na řádek). Jednotlivé odstavce jsou od sebe odděleny volným řádkem, tj. několik řádek za sebou tvoří týž odstavec, pakliže mezi nimi není žádná prázdná řádka. Program vždy přečte odstavec a vypíše jej pomocí co nejmenšího počtu řádků tak, aby k přechodu na nový řádek nedocházelo uprostřed slova. Počet sloupců bude zadaný jako parametr předaný pomocí -v. [2 body]

13. cvičení (17. května 2018)

Příkazy: trap, ps, kill, sleep, date, eval.

  1. Napište skript, který bude očekávat vstup na standardním vstupu, načtený vstup pouze zopakuje na svůj standardní výstup. Pokud skript dostane signál SIGKILL (9), tak samozřejmě skončí, ale pokud dostane signál SIGTERM (15), SIGQUIT (3), či SIGINT (2) (to je ten, který je poslán pomocí Ctrl-c), neskončí a jen vypíše číslo toho signálu, který takto obdržel. Při SIGINT navíc vypíše naposledy vypsanou řádku vstupu.
    (Ukázat řešení)
    #!/bin/sh

    trap "echo 15" TERM
    trap "echo 3" QUIT
    trap 'echo 2; echo $x' INT

    while read x
    do
       echo $x
    done
  2. Napište skript, který dostane signál a jméno programu, poté pošle signál všem procesům daného programu. Můžete předpokládat, že se program jmenuje normálně a název neobsahuje žádné divné znaky (mezeru ale obsahovat může). Něco jako killall, ale killall není standardní příkaz. Doporučuji podívat se na parametr -o příkazu ps.
    (Ukázat řešení)
    #!/bin/sh

    [ $# -eq 2 ] || {
       echo "Očekávám dva parametry, číslo signálu a název programu";
       exit 1
    }

    signal="$1"
    program="$2"

    ps -Ao pid=,comm= | while read -r pid cmd
    do
       if [ "$cmd" = "$program" ]
       then
          kill -"$signal" "$pid"
       fi
    done
  3. Napište skript, který dostane jeden číselný parametr s počtem sekund. Skript potom počká zadaný počet sekund a skončí. Pokud skript obdrží signál SIGINT (2 - Ctrl-C), tak vypíše, kolik sekund už od začátku uběhlo. Pokud obdrží signál SIGQUIT (3 - Ctrl-\), tak vypíše, kolik ještě zbývá sekund do konce, ale svůj běh nezastaví. Pokud skript obdrží signál SIGTERM (15), vypíše, kolik uběhlo sekund od začátku, kolik zbývá do konce a skončí. Návratová hodnota skriptu je buď 0, pokud skončil ve správný čas, nebo zbývající počet sekund, pokud skončil kvůli SIGTERM.
    Nepoužívejte date +%s, %s není standardní součást formátovacího řetězce pro date. Můžete použít awk funkci srand().
    (Ukázat řešení)
    #!/bin/sh

    die() {
       echo "$1"
       exit 1;
    }

    now() {
       awk 'BEGIN { srand(); print srand(); }'
    }

    [ $# -eq 1 ] || die "Špatný počet parametrů"
    expr "$1" : '^[[:digit:]]*$' >/dev/null || die "Parametr není číslo"

    start=$(now)
    konec=$((start + $1))

    trap 'echo $(($(now) - start))' 2
    trap 'echo $((konec - $(now)))' 3
    trap 'echo $(($(now) - start)); kill $! 2>/dev/null; exit' 15

    while [ "$(now)" -lt "$konec" ]
    do
       sleep $((konec - $(now) )) &
       wait $!
       [ $? -gt 128 ] && kill $! 2>/dev/null
    done

    # V uvedeném cyklu by stačilo sleep 1, ale proces by se probouzel
    # každou vteřinu.
    # sleep $(expr $konec - $(now) ) samo nestačí kvůli TERM, pokud totiž
    # dostane proces shellu se skriptem TERM ve chvíli, kdy běží sleep,
    # tak nechá nejprve sleep doběhnout a pak teprve zpracuje signál akcí
    # danou trap. (To se neprojeví nutně u ctrl-C, protože to dostane sleep
    # samo.) Proto je sleep puštěn na pozadí a poté spuštěno čekání, dokud
    # sleep nedoběhne. Po wait se zpracuje signál, aby se nekumulovaly
    # procesy sleep, je jim poté poslán signál TERM, čímž jsou průběžně
    # ukončovány. V případě obsluhy TERM je též zabit poslední sleep
    # běžící na pozadí. Test na to, zda wait s kódem větším než 128 je tu
    # proto, že wait vrací takové číslo, právě pokud došlo k přerušení
    # signálem.
  4. Napište skript, který dostane jako parametr název souboru, skript očekává poté na standardním vstupu posloupnost čísel, každé je na nové řádce a je indexem řádky, kterou má skript vypsat. Skript implementujte tak, že nejprve načte řádky souboru do pole, které implementujete pomocí eval (tj. eval '$'POLE_$INDEX), poté dotazy zodpovídejte přístupem do tohoto "pole".
    (Ukázat řešení)
    #!/bin/sh

    [ $# -eq 1 ] || { echo "Špatný počet parametrů"; exit 1; }

    pocet=0
    while read radka
    do
      pocet=$(expr $pocet + 1)
      eval RADKY_$pocet='"$radka"'
    done <"$1"

    while read index
    do
       eval echo '$'RADKY_$index
    done
  5. Napište skript pro spouštění příkazu pomocí e-mailu, na standardní vstup dostane soubor následujícího tvaru:
    From:<addr>
    Subject:<cmd>

    <input>
    ...

    Skript spustí příkaz <cmd> a na standardní vstup mu dá <input>. Návratový kód (tj. $?) pošle zpět na adresu <addr> v subjectu, v těle mejlu posílaného zpět bude obsah standardního výstupu spuštěného programu.
    Upřesnění:
    • Místo <addr> je pochopitelně skutečná adresa, místo <cmd> je skutečný příkaz i s parametry (které by měl shell zpracovat) a místo <input> je text, který se předá tomu příkazu. Třetí řádka je prázdná.
    • Skript nedostane žádné parametry, vstup dostane na standardním vstupu.
    • Parametry příkazu jsou uvedené v rámci subjectu, na standardní vstup dostane obsah mejlu od 4. řádky včetně dál.

    (Ukázat řešení)
    #!/bin/sh

    IFS=": " read f from
    IFS=": " read s subject
    read e

    eval "$subject"
    echo "$?" | mail -s "Exit code of $subject" "$from"

Domácí úkol

  1. Napište skript, který bude ve vstupním textovém souboru provádět indentaci řádků podle hloubky zanoření daném závorkami. Parametry skriptu bude název znak indentace c, počet znaků na úroveň n a seznam souborů (pokud chybí soubory, čte se standardní vstup). Skript před každou řádku vloží k*n znaků c, kde k je hloubka zanoření počátku řádku v závorkách, uvažují se kulaté, hranaté a složené závorky. Pokud jsou před začátkem textu na řádce již prázdné znaky, tak jsou nejprve odstraněny. Například vstupní soubor
    a ( b
         c d [ e ] f [
      g h { j (
                k ) } l m
         n ] o ) p
    q r
    
    by volání skriptu s parametrem c='.' a počtem n=1 upravilo do následující podoby:
    a ( b
    .c d [ e ] f [
    ..g h { j (
    ....k ) } l m
    ..n ] o ) p
    q r
    
    Můžete předpokládat, že vstup je správně uzávorkovaný (pokud není, je chování nedefinované). Dá se očekávat, že znakem může být mezera nebo tabelátor. [3 body]

14. cvičení (24. května 2018)

  1. Ukázkový zkouškový příklad s řešením

Doplňkové domácí úkoly

  1. Předpokládejme, že vstupní soubor je ve formátu CSV, kde sloupce jsou oddělené pomocí čárky. Napište shellový skript, který očekává dva parametry — název souboru a číslo sloupce. Skript potom vypočítá a vypíše medián číselných hodnot v daném sloupci, průměr těchto hodnot a směrodatnou odchylku. [3 body]
  2. Fibonacciho slovo definujeme následujícím způsobem: Slovo \(S_0=0\), slovo \(S_1=01\), dále pak pro \(n\geq 2\) definujeme \(S_n=S_{n-1}S_{n-2}\) (tj. jde o konkatenaci slov \(S_{n-1}\) a \(S_{n-2}\)). Napište shellový skript, který pro zadané číslo \(n\) vypíše \(n\)-té Fibonacciho slovo \(S_n\). Skript bude kromě \(n\) připouštět dva volitelné parametry, které budou určovat znaky (nebo řetězce), jež se mají použít místo 0 a 1. [3 body]
  3. Napište skript, který očekává soubor, kde na každé řádce je číslo. Skript soubor setřídí, přičemž pokud se nějaké číslo v souboru vypisuje vícekrát, skript je vypíše jen jednou. U každého čísla navíc bude na výstupu v dalším sloupci počet výskytů daného čísla na vstupu. Bez použití uniq. [3 body]