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

Skript je v Linuxovém prostředí libovolný program, který je interpretován (tj. program je distribuován jako zdrojový kód). V tomto směru pak rozlišujeme shellové skripty (shell je jazyk příkazové řádky), skript v Pythonu, Ruby nebo třeba PHP.

Výhodou tzv. skriptovacích jazyků je že potřebujete pouze textový editor na vývoj a jsou lehce přenositelné. Nevýhodou pak je, že potřebujete nainstalovat i interpretr. Naštěstí, Linux je často dodáván se sadou interpretrů a začít tak s dalším jazykem je vlastně velmi jednoduché.

Toto cvičení bude postaveno na jednom příkladu, který budeme postupně rozvíjet, abyste se naučili základní koncepty na praktickém příkladu (samozřejmě existují konkrétní nástroje, které by se daly použít rovnou, ale doufáme, že je to lepší než zcela umělý příklad).

Předstartovní kontrola

  • Vybrali jste si hezký emulátor terminálu. Jak na školním počítači, tak na své instalaci doma.
  • Vybrali jste si pěkný textový editor s TUI, který umíte ovládat. Ujistěte se, že je k dispozici v učebně během cvičení i na vašem počítači a že je správně nakonfigurován.
  • Na základní operace se soubory můžete používat mc nebo ranger.
  • K procházení souborového systému a kontrole souborů můžete použít cd, pwd, ls, cat (a hexdump).

Průběžný příklad

Data pro náš příklad lze stáhnout z tohoto projektu (které se nachází uvnitř adresáře 03). Neváhejte si stáhnout celý repozitář jako tarball/ZIP (odkazy jsou pod modrým tlačítkem Code) a dekomprimujte je v mc.

Simulují zjednodušené logy webového serveru, kde webový server zaznamenává, ke kterým souborům (URL) bylo přistupováno v jakém čase.

Každý soubor obsahuje provoz za jeden den ve zjednodušeném formátu CSV.

Pole jsou oddělena čárkou, není zde žádná hlavička a u každého záznamu si pamatujeme datum, IP adresu klienta, adresu URL, která byla vyžádána, a množství přenesených bajtů.

Ve skutečnosti by data byla komprimována a pravděpodobně by obsahovala více podrobností o klientovi (např. použitý prohlížeč), ale jinak zaznamenaná data představují poměrně typický formát logů webového serveru.

Naším úkolem je napsat program, který vypíše stručný přehled těchto dat:

  1. Vytiskne 3 nejčastěji navštěvované adresy URL.
  2. Vytiskne celkového množství přenesených dat.
  3. Vytiskne 3 dny s největším objemem provozu (tj. součtem přenesených bajtů).

Než ale toto řešení vytvoříme, musíme položit určité základy. A protože jich bude hodně, dokončíme třetí podúkol během příštího cvičení.

Skripty příkazové řádky

Abychom vytvořili shellový skript, stačí psát příkazy do souboru (namísto jejich přímého psaní do terminálu).

Takže, jednoduchý skript, který vypíše informace o systému může vypadat takto.

cat /proc/cpuinfo
cat /proc/meminfo

Pokud tohle uložíte do souboru first.sh, můžete skript spustit následujícím příkazem.

bash first.sh

Všimněte si, že spouštíme bash, což je program s interpretrem, a jméno vstupního souboru (skriptu).

Udělá to, že spustí cat s těmito soubory (mohli bychom samozřejmě pustit cat jen jednou a dát mu dva argumenty).

Vzpomeňte si, že váš skript 01/dayname.py lze spustit následujícím příkazem (opět musíme uvést ten správný interpretr).

python3 dayname.py

Shebang a executable (spustitelný) bit

Spouštění skriptů uvedením interpretru (tj. příkazu, který vlastně skript provede) není úplně elegantní. Ale existuje snazší cesta: označíme soubor jako spustitelný a Linux se postará o zbytek.

Ve skutečnosti, kdy spustíme cat nebo mc, existuje soubor (typicky v adresáři /bin nebo /usr/bin), který se jmenuje cat či mc a který je označen jako spustitelný. (Prozatím si představte, že spustitelnost je speciálním atributem souboru). Všimněte si, že soubor nemá žádnou příponu.

Nicméně, označení souboru jako spustitelného je jen první půlka řešení. Představte si, že vytvoříme následující program a uložíme ho do spustitelného souboru hello.py.

print("Hello")

A teď ho chceme spustit.

Ale počkat! Jak systém pozná, který interpretr má použít? Pro binární aplikace (např. přeložené z Céčkových zdrojáků) je to snadné, protože binárka je (v podstatě) rovnou strojový kód. Ale tady potřebujeme ten správný interpretr.

V Linuxu je interpretr specifikován pomocí tzv. shebangu nebo hashbangu. Už jste se s ním dokonce několikrát potkali: pokud je první řádka skriptu označená pomocí #! (odtud také název: hash a vykřičník), Linux očekává, že tam najde cestu k interpretru, který se má spustit.

Pokud není uveden žádný shebang, chování není dobře definováno.
Linuxový kernel dokonce odmítne spustit skripty bez shebangu. Pokud je ale spustíte z shellu, shell je zkusí interpretovat jako shellové skripty. V praxi na to ale příliš nespoléhejte.

Pro shellové skripty budeme používat #!/bin/bash, pro Python musíme použít #!/usr/bin/env python3. Část s env vysvětlíme později; zatím si jen, prosím, zapamatujte, že ji máte používat.

Nepoužívejte pro skripty Python nic jiného než #!/usr/bin/env python3. #!/usr/bin/env python nebo #!/usr/bin/python3 jsou nesprávné a mohou způsobit různá překvapení.
Všimněte si, že většina interpretrů bere # jako znak komentáře, takže není nijak potřeba řešit, co s první řádkou, která je vlastně přeskočena (protože interpretr jako takový ji vlastně nepotřebuje).
Často se setkáte s #!/bin/sh v shellových skriptech. Pro většinu skriptů je to vlastně jedno: jednoduché konstrukce fungují stejně, ale /bin/bash nabízí některá příjemná rozšíření. V tomto předmětu budeme používat /bin/bash, protože tato rozšíření jsou poměrně užitečná.

Pokud pracujete na starších systémech nebo potřebujete, aby byl váš skript přenositelný na různé verze unixových systémů, může být nutné použít /bin/sh.

Abychom to ještě trochu zkomplikovali, na některých systémech je /bin/sh totéž co /bin/bash, protože se ve skutečnosti jedná o nadmnožinu.

Sečteno a podtrženo: pokud nevíte, co děláte, zůstaňte prozatím u shebangu #!/bin/bash.

Zpátky k původní otázce: jak je skript tedy spuštěn. Systém vezme příkaz z shebangu, přidá k němu název souboru se skriptem jako další parametr a to spustí. Pokud uživatel přidá více argumentů (např. --version), jsou přidány také (na konec).

Například, pokud by hexdump byl shellový skript, začínal by následujícím:

#!/bin/bash

...
kod-co-jde-po-bajtech-a-tiskne-je-bude-tady
...

Spuštění hexdump -C file.gif by tedy doopravdy spustilo následující příkaz:

/bin/bash hexdump -C file.gif

Všimněte si, že vlastně jediná magie za shebangem a spustitelnými soubory je, že systém zkonstruuje delší řádek s příkazem ke spuštění.

A uživatel se nemusí starat o to, který jazyk je vlastně používán.

Vyzkoušejme si to prakticky.

Už ale víme o shebangu, takže soubor upravíme a označíme ho jako spustitelný.

Uložme následující do souboru first.sh.

#!/bin/bash

cat /proc/cpuinfo
cat /proc/meminfo

Abychom jej označili jako spustitelný, spustíme následující příkaz. Prozatím si jej prosím zapamatujte jako kouzlo, které je třeba provést, podrobnější informace o tom, proč to tak vypadá, přijdou později.

chmod +x first.sh
chmod nebude fungovat na souborových systémech, které nejsou určeny pro Unixové/Linuxové systémy. Což bohužel zahrnuje i NTFS.
Webové UI GitLabu nenabízí žádné prostředky pro nastavení executable bitu. Je nutné použít klienta Gitu na příkazové řádce (což bude na dalším cvičení).

Teď můžeme skript spustit takto:

./first.sh

Nabízí se otázka: proč to přebytečné ./ místo, aby se zavolal first.sh? ./ se přece vztahuje k aktuálnímu adresáři, ne (vzpomeňte si na předchozí cvičení)? Odkazuje tedy na stejný soubor!

Když použijeme příkaz (např. cat) bez určení cesty (tj. jen holé jméno souboru s programem), shell se podívá do tzv. cesty, která je uložená v proměnné $PATH, aby našel soubor s tímto programem (cesty obvykle bude obsahovat adresář /usr/bin kde najdeme většinu spustitelných souborů). Na rozdíl od jiných operačních systémů se shell nedívá do pracovního adresáře, když program nenajde v $PATH.

Pro spuštění programu v aktuálním adresáři proto musíme specifikovat i jeho cestu (když je soubor uveden s cestou, $PATH se ignoruje a shell se prostě pokusí soubor najít). Naštěstí nemusí být absolutní, ale stačí relativní. Proto ten magický zápis ./.

Přesuneme-li se do jiného adresáře, můžeme program také spustit pomocí relativní cesty, např. ../first.sh.

Spusťte teď ls v aktuálním adresáři. Měli byste vidět soubor first.sh vypsaný zeleně. Pokud ne, zkuste ls --color nebo ověřte, že jste správně spustili chmod.

Nemáte-li barevný terminál (neobvyklé, ale pořád možné), můžete použít ls -F na odlišení typů souborů: adresáře pak budou končit lomítkem, spustitelné soubory hvězdičkou.

Mini příklady

Vytvořte skript, který vypíše všechny obrazové soubory v aktuálním adresáři (zatím můžete s jistotou předpokládat, že tam vždy nějaké budou). Zkuste jej spustit z různých adresářů pomocí relativní a absolutní cesty.

Řešení.

Vytvořte skript, který vypíše informace o aktuálně viditelných diskových oddílech v systému. Prozatím zobrazí pouze obsah souboru /proc/partitions.

Řešení.

Změna pracovního adresáře

Trochu teď skript upravíme.

cd /proc
cat cpuinfo
cat meminfo

Spusťte skript znova.

Všimněte si, že navzdory tomu, že skript změnil adresář na /proc, jsme po jeho ukončení stále v původním adresáři.

Zkuste vložit pwd na ověření, že skript je opravdu uvnitř /proc.

Je důležité si odnést, že každý proces (běžící program; vč. skriptů) má svůj vlastní pracovní adresář. Po spuštění zdědí adresář od toho, kdo jej zavolal (např. shellu, v němž byl spuštěn). Následné změny adresáře ale nijak neovlivní ostatní procesy, takže je po ukončení volající stále ve stejném adresáři.

To také znamená, že samotné cd nemůže být normální program. Protože kdyby to byl normální program (např. v Pythonu), jakákoli změna uvnitř něj by byla po jeho ukončení k ničemu (ztracena).

Proto je cd takzvaný builtin, který je implementován uvnitř samotného shellu.

Ladění skriptů

Chcete-li vidět, co se děje, spusťte skript pomocí bash -x first.sh. Zkuste si to. Pro delší skripty je ale lepší vypisovat vlastní hlášky, protože -x bývá až příliš podrobné.

Pro vypsání hlášky na terminál lze použít příkaz echo. Až na pár výjimek (více o nich později), jsou všechny jeho argumenty jen vypsány.

Vytvořte skript echos.sh s následujícím obsahem a vysvětlete rozdíly:

#!/bin/bash

echo alpha bravo charlie
echo alpha  bravo   charlie
echo "alpha   bravo"   charlie
Odpověď.

Rozšíření průběžného příkladu

Nyní začneme pracovat na našem průběžném příkladu a připravíme jej.

Pro začátek vytvořte verzi, která jednoduše echuje seznam souborů, se kterými budeme pracovat.

Předpokládejte, že program bude číst soubory z podadresáře logs. Nezapomeňte nastavit svůj skript jako a přidat správný shebang.

Řešení.

Argumenty na příkazové řádce

Argumenty příkazové řádky (jako například -l pro ls nebo -C pro hexdump) jsou obvyklým způsobem, jak ovládat chování CLI nástrojů v Linuxu. Pro nás, jako vývojáře, je důležité naučit se, jak s nimi uvnitř našich programů pracovat.

O práci s argumenty v shellových skriptech budeme mluvit později, dnes si ukážeme jejich použití v Pythonu.

Přístup k těmto argumentům v Pythonu je jednoduchý. Potřebujeme do programu přidat import sys a poté k nim můžeme přistupovat přes seznam sys.argv.

Proto tedy následující program vypíše své argumenty.

#!/usr/bin/env python3

import sys

def main():
    for arg in sys.argv:
        print("'{}'".format(arg))

if __name__ == '__main__':
    main()

Při jeho spuštění (samozřejmě poté, co na něj zavoláme chmod +x) uvidíme následující (řádky prefixované $ značí příkaz, zbytek je jeho výstup).

$ ./args.py
'./args.py'
$ ./args.py one two
'./args.py'
'one'
'two'
$ ./args.py "one  two"
'./args.py'
'one  two'

Všimněme si, že nultá položka představuje vlastní název příkazu (ten teď nevyužijeme, ale může být užitečný pro některé chytré triky) a také jak se volání druhého a třetího příkazu liší uvnitř Pythonu.

To by nás ale nemělo překvapit, vzpomeňme si na předchozí cvičení a práci se jmény souborů s mezerami.

Spusťte výše uvedený příkaz a zadejte mu jako parametr zástupný znak. Za předpokladu, že již máte nějaké shellové skripty s příponou .sh, se podívejte na chování následujících volání.

./args.py *.py
./args.py *.sh
./args.py *.shhhhhh

Pokud si nejste jisti, co se stalo, připomeňte si předchozí cvičení. Nápověda.

Standardní vstup a výstup

Následující pojmy již pravděpodobně znáte, ale možná ne přesně pod těmito názvy, proto se pokusíme osvěžit vaše znalosti o nich.

Standardní výstup

Standardní výstup (obvykle se mu říká stdout, což je zkratka ze “standard output”) se použije, když zavoláte print("Hello") třeba v Pythonu - je to cíl, kam se vytiskne výstup vašeho programu. Stdout používají prakticky všechny základní výstupní funkce (funkce, které se používají třeba pro vytištění textu na obrazovku) v téměř každém programovacím jazyce.

Rychlá kontrola: jak se v shellu tiskne na stdout? Odpověď.

V zásadě se stdout chová jako soubor - říkáme, že má stejné API pro zápis, jako obyčejný soubor. Proto se na něj zapisuje stejně bez ohledu na to, z jakého jazyka - print z Pythonu, System.out.print v Javě i printf v Céčku (kde existuje pár funkcí print a printf kvůli vlastnostem C, do kterých nemá smysl zde zabíhat) - všechna tahle volání se k stdoutu chovají stejně. Dokonce se k němu chovají v zásadě stejně bez ohledu na to, zda stdout míří do souboru, je to terminál (takže “píše na obrazovku”) nebo je vstupem jiného programu.

Když se v Linuxu spustí program, dostane stdout už připravený. O to se postará shell ve spolupráci s operačním systémem, někdy se toho účastní i runtime jazyka, ve kterém byl program napsán. Ani zde nemá smysl zabíhat do podrobností; v praxi je pouze důležité vědět, že stdout dostane program už připravený a co napíšeme na stdout, objeví se na obrazovce v terminálu (a pokud jsme aplikaci spustili graficky, většinou o data zapsaná na stdout přijdeme).

V Pythonu můžete k otevřenému souboru, který reprezentuje stdout, přistoupit pomocí sys.stdout. Jedná se o stejný objekt, jaký vrací volání open.

Standardní vstup

Podobně jako můžete zapisovat na stdout v téměř každém jazyce, můžete číst ze standardního vstupu (kterému se říká stdin ze standard input). Obvykle lze ze stdin přečíst to, co napíšete na klávesnici do terminálu (a pouze do terminálu, grafické aplikace se ke klávesnici chovají jinak).

Všimněte si, že funkce input(), kterou jste asi v Pythonu používali je nadstavbou stadnardního vstupu, protože umožní vstup upravovat. Obyčejný stdin nepodporuje žádné úpravy (resp. umí mazat znaky od konce řádku).

Pokud chcete v jazyce Python přistupovat ke standardnímu vstupu, musíte explicitně použít sys.stdin. Jak se dalo očekávat, používá souborové API, a proto je možné z něj přečíst řádek voláním .readline() nebo iterovat přes všechny řádky.

Iterace přes všechny řádky vstupu je velmi běžné chování, které je společné mnoha Linuxovým programům (ty jsou obvykle napsané v Céčku, ale chovají se stejně).

for line in sys.stdin:
    ...
Všimněte si, že tahle smyčka funguje stejně dobře s libovolným otevřeným textovým souborem v Pythonu, a textový vstup se takhle skutečně obvykle zpracovává.

Pro mnoho nástrojů je ve skutečnosti čtení ze stdin výchozím chováním. Například cut -d : -f 1 vypíše pouze první sloupec dat z každého řádku (a očekává, že sloupce budou ohraničeny znakem :).

Spusťte jej a napište na klávesnici následující, přičemž každý řádek ukončete znakem <Enter>.

cut -d : -f 1
one:two
alpha:bravo
uno:dos

Pod zadaným vstupem by se měl zobrazit první sloupec.

Co dělat, když jste hotovi? Zadání exit zde nepomůže, ale <Ctrl>-D zafunguje.

Stisk <Ctrl>-D na prázdné řádce způsobí ukončení standardního vstupu. Program cut si všimne, že již nemůže zpracovávat žádný vstup a ukončí se. Všimněte si, že se jedná o něco jiného, než kombinace kláves <Ctrl>-C, která násilně ukončí běžící proces. Z uživatelského pohledu to při použití nástroje cut vypadá podobně, ale jedná se o jiné chování a rozdíl je zásadní (a u jiných utilit může být i viditelný).

Přesměrování vstupu a výstupu (“I/O redirection”)

Jako technický detail jsme již zmínili, že standardní vstup a výstup připravuje (částečně) operační systém. To také znamená, že je lze změnit (tj. jinak zinicializovat), aniž by se změnil program. A program o tom ani nemusí “vědět”.

Když změníme standardní vstup nebo výstup našeho programu, říká se tomu přesměrování (standardního vstupu resp. standardního výstupu). Přesměrování nám například umožňuje vyjádřit, že výstup se nemá objevit na obrazovce, ale má být uložen do souboru.

Přesměrování musí proběhnout předtím, než se program spustí, a musí se o něj postarat ten, kdo program spouští - tedy shell, což je pro nás v tuto chvíli podstatné.

Zařídit přesměrování je jednoduché: na konec příkazu můžeme napsat třeba > output.txt a všechno, co by se normálně objevilo na obrazovce v terminálu, se teď objeví v souboru output.txt.

Než začnete experimentovat: přesměrování výstupu je nízkoúrovňová operace a nemá žádnou formu “kroku zpět” (ve smyslu undo). Pokud tedy soubor, na který přesměrováváte, již existuje, bude bez dalšího ptaní přepsán. A to bez jakékoliv snadné možnosti obnovení původního obsahu (a u malých souborů je obnovení u většiny souborových systémů používaných v Linuxu technicky nemožné).

Pro jistotu si zvykněte po zadání názvu souboru stisknout klávesu <Tab>. Pokud soubor neexistuje, kurzor se nepohne. Pokud soubor již existuje, doplnění tabulátorem vloží mezeru.

Jako nejjednodušší příklad můžeme spustit následující dva příkazy, které vytvoří soubory one.txt a two.txt, ve kterých bude po řadě napsáno ONE resp. TWO (a znak konce řádku jako poslední znak).

echo ONE > one.txt
echo TWO >two.txt

Syntaxe shellu je poměrně volná, takže je možné vynechat mezeru před one.txt a napsat >one.txt. Mezera za znakem > tedy není součástí názvu souboru, do kterého přesměrováváme.

Z pohledu implementace: echo dostalo jediný argument. Část s přesměrováním (> filename) mu nebyla nijak předána a > filename budete v sys.argv tudíž hledat marně.

Pokud znáte volání popen nebo podobné volání z jazyka Python, možná víte, že umožňují specifikovat, co se má použít jako stdout pokud chcete v programu provést přesměrování (ale pro nově spuštěný program, ne uvnitř již běžícího programu).

Ve druhém cvičení jsme se zmínili, že program cat se používá na “zřetězování” souborů (vytištění několika souborů za sebou). Teď, když umíme přesměrovat výstup, začíná to dávat smysl: výstup cat, tedy několik souborů vytištěných po řadě za sebou, se dá jednoduše uložit do nového souboru.

cat one.txt two.txt >merged.txt

Připojování výstup na konec při přesměrování

Shell také nabízí možnost připojit výstup k existujícímu souboru pomocí operátoru >>. Následující příkaz tedy přidá UNO jako další řádek do souboru one.txt.

echo UNO >>one.txt

Pokud soubor neexistuje, bude vytvořen.

Pro následující příklad budeme potřebovat program tac, který obrací pořadí jednotlivých řádků, ale jinak funguje jako cat (všimněte si, že tac je cat, ale obráceně, což je vážně skvělý název). Nejprve si vyzkoušejte tento postup.

tac one.txt two.txt

Pokud jste provedli výše uvedené příkazy, měli byste vidět následující:

UNO
ONE
TWO

Vyzkoušejte následující příkaz a vysvětlete, co se stane (a proč), když spustíte

tac one.txt two.txt >two.txt
Odpověď.

Přesměrování vstupu

Podobně shell nabízí < pro přesměrování stdin. Pak místo čtení vstupu zadaného uživatelem na klávesnici program čte vstup ze souboru.

Pythonovské programy, které používají input() moc dobře s přesměrovaným vstup nefungují. Prakticky je input() vhodný pouze pro interaktivní programy. Spíše asi budete chtít použít sys.stdin.readline() nebo for line in sys.stdin.

Když je vstup přesměrován, nemusíme zadávat <Ctrl>-D, abychom uzavřeli vstup, protože vstup se uzavře automaticky po dosažení konce souboru.

Standardní vstup a výstup: zkontrolujte si, že rozumíte základům

Vyberte všechna pravdivá tvrzení. You need to have enabled JavaScript for the quiz to work.

Filtry

Mnoho příkazů v Linuxu funguje jako filtry. Čtou svůj vstup ze stdin a (modifikovaný) výstup zapisují na stdout.

Jeden takový příklad je cut, příkaz, který vytiskne jenom určité sloupce vstupu a ostatní zahodí. Například když spustíme cut -d : -f 1 a standardní vstup bude /etc/passwd, dostaneme seznam účtů (uživatelských jmen) na aktuálním stroji.

Zkuste spustit následující dva příkazy (a všimněte si rozdílu).

cut -d : -f 1 </etc/passwd
cut -d : -f 1 /etc/passwd

Filtry se často chovají přesně takhle: pokud nespecifikujete jméno souboru, filtr pracuje se standardním vstupem. Jestliže specifikujete soubor jako argument, bude filtr pracovat s ním.

Jaký je rozdíl mezi oběma výše uvedenými voláními? Vypíšou přece stejný výsledek.

V prvním případě (s přesměrováním vstupu) je vstupní soubor otevřen shellem a otevřený soubor je předán programu cut. Problém s otevřením ohlásí shell a cut nemusí být vůbec spuštěn. Ve druhém případě je soubor otevřen cut (tj. cut provede volání open a musí také zpracovat případné chyby).

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

S těmito znalostmi můžeme konečně vyřešit první část našeho příkladu. Připomeňme si, že máme soubory, které zaznamenávají provoz každého dne, a chceme najít adresy URL, které se ve všech souborech dohromady vyskytují nejčastěji.

To znamená, že musíme spojit všechny soubory dohromady, zachovat pouze adresu URL a najít tři nejčastěji se vyskytující řádky.

A to už můžeme udělat. Připomeňme, že cat lze použít ke spojování souborů a cut k zachování pouze některých sloupců. Nalezení nejčastěji se vyskytující adresy URL uděláme už za chvíli.

Tak, co třeba takhle?

#!/bin/bash

echo "Podíváme se na následující soubory:" logs/20[0-9][0-9]-[01][0-9]-[0-3][0-9].csv

cat logs/20[0-9][0-9]-[01][0-9]-[0-3][0-9].csv >_logs_merged.csv
cut -d , -f 5 <_logs_merged.csv

Použili jsme poměrně explicitní wildcard, abychom zajistili, že nebudeme vypisovat náhodné CSV, i když cat logs/*.csv by mohlo fungovat stejně dobře.

Uvědomte si, kolik času by zabralo napsat toto v Pythonu.

Skript má jednu velkou chybu (brzy ji vyřešíme, ale přesto je třeba se o ní zmínit).

Skript zapisuje do souboru s názvem _logs_merged.csv. Názvu souboru jsme předřadili podtržítko, abychom ho označili jako poněkud speciální, ale přesto: co kdyby uživatel takový soubor vytvořil ručně?

Tento soubor bychom bez dalšího ptaní přepsali. A bez možnosti obnovy.

Tohle ve svých skriptech nikdy nedělejte.

Můžete se také setkat s variantou, kdy je cut voláno jako cut -d, -f3. Většina programů je dostatečně chytrá na to, aby rozpoznala obě varianty, ale je důležité si uvědomit, že s tím si musí poradit každý program sám.

To znamená, že program musí být schopen pracovat s sys.argv[1] == '-d,' a s (sys.argv[1] == '-d') a (sys.argv[2] == ',').

Roury čili pipes (skládání proudových dat)

Konečně se přesuneme do oblasti, ve které Linux vyniká: ke skládání programů. V podstatě celá myšlenka operačních systémů rodiny Unix spočívá v tom, že umožňují snadné skládání různých malých prográmků dohromady.

Většinou se jedná o programy, které se skládají dohromady jako filtry pracujícími s textovými vstupy. Tyto programy nepředpokládají žádný specifický formát textu a jsou velmi obecné. Pokud je vstup více strukturovaný, například XML nebo JSON, jsou potřeba speciální nástroje (které jsou nicméně součástí repozitářů softwaru pro Linux).

Výhodou je, že skládání programů je velmi snadné a lze je také velmi snadno skládat postupně (tj. přidávat další filtr, až když výstup z předchozích vypadá rozumně). Takové inkrementální skládání je obtížnější v běžných jazycích, kde vypisování dat vyžaduje další příkazy (zde se vypisují data na stdout bez práce navíc).

Nevýhodou je, že složité kompozice mohou být obtížně čitelné. Je na vývojáři, aby se rozhodl, kdy je čas přejít na lepší jazyk a zpracovávat data v něm. Typická dělba práce spočívá v tom, že se k předzpracování dat používají shellové skripty: ty jsou nejlepší, když potřebujete spojit data z více souborů (např. stovky různých logů apod.) nebo když je třeba data převést do rozumného formátu (např. nestrukturované logy z webového serveru do CSV načitatelného do vašeho oblíbeného tabulkového procesoru nebo jazyka R). Výpočet statistik a podobné úlohy je pak nejlepší přenechat specializovaným nástrojům.

Netřeba dodávat, že Linux nabízí spoustu nástrojů pro statistické výpočty nebo nástroje pro kreslení grafů, které lze ovládat pomocí CLI. Zvládnutí těchto nástrojů je bohužel mimo téma tohoto kurzu.

Vraťme se znovu k našemu průběžnému příkladu.

Již jsme se zmínili, že použitý dočasného souboru je špatně, protože jsme mohli přepsat cizí data.

Vyžaduje však také místo na disku pro další kopii (pravděpodobně obrovského objemu) dat.

Trochu nenápadnější, ale mnohem nebezpečnější problém spočívá v tom, že cesta k dočasnému souboru je pevně daná. Představte si, co se stane, když skript spustíte ve dvou terminálech současně. Nenechte se zmást pocitem, že skript je tak krátký, že pravděpodobnost souběžného spuštění je zanedbatelná. Je to past, která jen čeká na sklapnutí. O správném použití mktemp(1) si povíme později, ale v tomto příkladu není dočasný soubor vlastně vůbec potřeba.

Učili jsme se o skladbě programů, že? A můžeme ji použít i zde.

cat logs/20[0-9][0-9]-[01][0-9]-[0-3][0-9].csv | cut -d , -f 5

Symbol | znamená rouru (česky též pajpu čili pipe), který spojuje standardní výstup cat se standardním vstupem cut. Roura předává data mezi oběma procesy, aniž by je vůbec zapisovala na disk. (Data jsou předávána pomocí paměťových bufferů, ale to je technický detail.)

Výsledek je stejný, ale vyhnuli jsme se nástrahám používání dočasných souborů a výsledek je ve skutečnosti ještě čitelnější.

Roura | spojuje standardní výstup programu na levé straně se standardním vstupem programu na pravé straně a shell/OS zajišťuje tok dat mezi oběma programy.

Programy obvykle nevědí, že jsou součástí roury: stdout a stdin jsou transparentně připraveny systémem a programy (nebo jejich vývojáři) se o to nemusí starat.

Pro případy, kdy první příkaz čte také ze standardního vstupu, je k dispozici jiná syntaxe. Například následující vypíše setříděný seznam místních uživatelských účtů (uživatelských jmen).

cut -d : -f 1 </etc/passwd | sort

Můžeme dokonce přesunout první < před cut, aby bylo možné skript přečíst zleva doprava jako “vezmi /etc/passwd, vezmi první sloupec a pak jej seřaď”:

</etc/passwd cut -d : -f 1 | sort

Rodina unixových systémů je v podstatě postavena na možnosti vytvářet pipelines, které řetězí posloupnost programů pomocí rour. Každý program v rouře označuje typ transformace. Tyto transformace se skládají dohromady a vytvářejí tím konečný výsledek.

Další doplnění našeho průběžného příkladu

Nejprve jsme chtěli vytisknout tři nejnavštěvovanější URL adresy.

Pomocí výše uvedené roury můžeme vypsat všechny adresy URL v jednom seznamu.

Pro zjištění nejčastěji navštěvovaných řádků použijeme typický trik, kdy nejprve seřadíme řádky podle abecedy a poté pomocí programu uniq s -c spočítáme unikátní řádky (takže v podstatě spočítáme, kolikrát byla která URL navštívena). Tento výstup pak seřadíme podle čísel a vypíšeme první 3 řádky.

V pythonovském řešení byste pravděpodobně vytvořili slovník, jehož klíčem by byla adresa URL, hodnotou by bylo počítadlo (kolikrát byla adresa URL navštívena). A pak byste vypsali klíče s nejvyšší hodnotou. Ošklivé řešení, které by se dalo hacknout, aby vše fungovalo, by mohlo vypadat takto (a to předpokládá, že všechny soubory jsou již spojeny):

import sys

urls = {}
for line in map(lambda x: x.rstrip().split(',')[4], sys.stdin):
    urls[line] = urls.get(line, 0) + 1
how_many = 3
for url, count in sorted(urls.items(), key=lambda item: item[1], reverse=True):
    print("{:7} {}".format(count, url))
    how_many = how_many - 1
    if how_many <= 0:
        break

Náš program se tedy v shellu bude vyvíjet takto (řádky začínající # jsou samozřejmě komentáře).

# Získáme všechny URL
cat logs/20[0-9][0-9]-[01][0-9]-[0-3][0-9].csv | cut -d , -f 5

# Zkrátíme si název souboru kvůli ušetření místa
cat logs/*.csv | cut -d , -f 5

# Setřídíme URL a dostaneme je tak k sobě
cat logs/*.csv | cut -d , -f 5 | sort

# Spočítáme počet výskytů (uniq je sám nesetřídi)
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c

# Setřídíme výstup uniq podle čísel (a v opačném pořadí)
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c | sort -n

# Vytiskneme jen první řádky
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c | sort -n -r | head -n 3

Nebojte se. Postupovali jsme po malých krůčcích na každém řádku. Spouštějte jednotlivé příkazy sami a sledujte, jak se výstup mění.

Všimněte si, že řešení v shellu je jednodušší na ladění (jakmile znáte jazyk): sestavujete ho postupně, zatímco ve skriptu Pythonu vyžaduje dodatečné výpisy (které pak musíte odstranit) a řešení je mnohem pevněji zauzlované než řešení v shellu.

Cvičení

Vypište celkové množství přenesených bajtů z logů z našeho příkladu (tj. poslední část úlohy).

Nápověda: budete potřebovat cat, cut, paste a bc.

První část by měla být snadná: zajímá nás pouze poslední sloupec.

cat logs/*.csv | cut -d , -f 4

Pro sčítání řádků čísel použijeme paste, který umí sloučit řádky z více souborů nebo spojit řádky do jednoho souboru. Dáme mu oddělovač + a vytvoříme tak obrovský výraz VELIKOST1+VELIKOST2+VELIKOST3+....

cat logs/*.csv | cut -d , -f 4 | paste -s -d +

Nakonec to pomocí bc sečteme.

cat logs/*.csv | cut -d , -f 4 | paste -s -d + | bc

bc je poměrně výkonný kalkulátor, který lze používat i interaktivně (připomeňme, že <Ctrl>-D ukončí zadávání v interaktivním režimu).

Další příklady jsou uvedeny na konci tohoto cvičení.

Nyní víte o ‘pipes’ v podstatě vše. Zbytek kouzla spočívá ve znalosti dostupných filtrů (a několika hraničních případů).

Je to jako s API v jazyce Python: čím více ho znáte, tím snadněji vytváříte nové programy.

Rychlá kontrola znalosti filtrů

Vyberte všechna pravdivá tvrzení. You need to have enabled JavaScript for the quiz to work.

Přesměrování uvnitř a dovnitř skriptu

Uvažujme následující malý skript (first-column.sh) který vezme a setřídí první sloupec (z dat oddělených dvojtečkou, tj. například soubor /etc/passwd). Všimněte si, že není uveden žádný vstupní soubor.

#!/bin/bash

cut -d : -f 1 | sort

Potom může uživatel skript použít takto a standardní vstup cutu bude vždy správně propojen se standardním vstupem shellu nebo skrz rouru.

cat /etc/passwd | ./first-column.sh
./first-column.sh </etc/passwd
head /etc/passwd | ./first-column.sh | tail -n 3
Výše uvedený příklad je poněkud umělý, ale dobře demonstruje důležitý princip, že stdin je přirozeně dostupný i uvnitř skriptů, pokud je přesměrován z “venku”.

Další příklady

Následující příklady lze vyřešit buď provedením několika příkazů nebo spojením základních příkazů shellu. K nalezení správného programu vám mohou pomoci manuálové stránky. Jako výchozí bod můžete také použít náš manuál.

Všimněte si, že žádné z řešení nevyžaduje nic jiného než použití několika rour. Pro pokročilé uživatele: rozhodně nepotřebujete if nebo while nebo read nebo dokonce používat PERL nebo AWK.

První várka příkladů obsahuje také naše řešení, abyste je mohli porovnat se svým.

Druhá várka neobsahuje řešení, ale jsou k dispozici automatizované testy.

Příklady s kompletními řešeními

Použijte následující CSV s údaji o tom, jak dlouho trvalo zkopírování obrazu disku USB na flash disky v knihovně. První sloupec představuje zařízení, druhý dobu trvání kopírování.

Ve skutečnosti první sloupec nepřímo představuje také port rozbočovače USB (je to spíše náhoda, ale vyplývá to ze způsobu, jakým jsme kopírování uspořádali). Poznámka na okraj: je zajímavé, že některé porty, které mají být stejné, jsou ve skutečnosti systematicky pomalejší.

Chceme vědět, jaká byla nejdelší doba trvání kopírování: jinými slovy, maximum ze sloupce dva.

Řešení.

Vytvořte adresář a a v něm textový soubor --help obsahující Lorem Ipsum. Vypište obsah tohoto souboru a poté jej smažte. Řešení.
Vytvořte adresář b a v něm soubory s názvem alpha.txt a *. Poté smažte soubor s názvem * a sledujte, co se stalo se souborem alpha.txt. Řešení.
Vypište obsah souboru /etc/passwd seřazený podle řádků. Řešení.
Vypište první a třetí sloupec souboru /etc/group. Řešení.
Spočítejte řádky souboru /etc/services. Řešení.
Vypište poslední dva řádky souborů /etc/passwd a /etc/group pomocí jediného příkazu. Řešení.
Připomeňte si soubor disk-speeds-data.csv s údaji o délce kopírování disku. Vypočítejte celkovou dobu trvání všech kopírování dohromady. Řešení.

Předpokládejme následující formát souboru.

Alpha     8  4  5  0
Bravo    12  5  3  2
Charlie   1  0 11  4

Ke každému řádku přičtěte součet jeho řádku. Nemusíte zachovávat původní zarovnání (tj. klidně si zmačkejte mezery).

Nápověda.

Řešení.

Vypište obsah souborů /etc/passwd a /etc/group oddělený textem Ha ha ha (tj. obsah /etc/passwd, řádek s Ha ha ha a obsah /etc/group). Řešení.
Vytiskněte výrobce vašeho CPU. Jako výchozí bod použijte soubor /proc/cpuinfo. Řešení.

Příklady s automatizovanými testy

Spočtěte celkový počet řádků všech textových souborů (tj. *.txt) v aktuálním adresáři. Skript vypíše pouze jedno číslo.

Můžete předpokládat, že vždy alespoň jeden takový soubor bude přítomen.

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

Vytiskněte skutečná jména uživatelů, která obsahují system kdekoli v jejich záznamu (tj. slovo system se objeví kdekoli na řádku).

Seznam uživatelů je uložen buď v /etc/passwd, nebo pomocí getent passwd. Váš skript bude předpokládat, že seznam uživatelů bude přicházet přes standardní vstup.

Proto jej otestujte pomocí getent passwd | 03/users.sh.

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

Předpokládejme následující vstupní formát (doby trvání jsou celá čísla) obsahující doby běhu programu spolu s jejich autory.

name1,duration_in_seconds_1
name2,duration_in_seconds_2

Napište autora nejrychlejšího řešení (můžete předpokládat, že doby trvání jsou různé).

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

Předpokládejme, že mám matici zapsané v takovémhle “krásném” formátu. Můžete se spolehnout, že formát je pevně daný (s ohledem na mezery, maximálně trojciferné číslo a symbol pipe) ale může se lišit počet řádků i sloupců.

Napište skript, který sečte čísla v každém řádku.

Počítáme, že pro následující matici dostaneme tento výstup.

| 106 179 |
| 188  50 |
|   5 125 |
285
238
130

Skript bude vstup číst na stdinu, počet sloupců a řádků není nijak omezen (kromě celkového formátu).

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

Učební výstupy

Učební výstupy podávají zhuštěný souhrn základních konceptů a dovedností, které byste měli umět vysvětlit a/nebo použít po každém cvičení. Také obsahují absolutní minimum, které je potřebné pro pochopení navazujících cvičení (a dalších předmětů).

Znalosti konceptů

Znalost konceptů znamená, že rozumíte významu a kontextu daného tématu a jste schopni témata zasadit do většího rámce. Takže, jste schopni …

  • vysvětlit, co znamená skript (v kontextu Linuxového prostředí)

  • vysvětlit, co je to shebang a jak ovlivní spuštění skriptu

  • chápat rozdíl mezi tím, zda skript má nebo nemá nastavený spustitelný bit

  • vysvětlit, co je pracovní adresář (working directory)

  • vysvětlit proč je pracovní adresář soukromou “vlastností” běžícího programu

  • vysvětlit, jak jsou argumenty (parametry) předané skriptu s shebangem

  • vysvětlit, co je standardní výstup a vstup

  • vysvětlit, proč přesměrování standardního vstupu/výstupu není (přímo) viditelné uvnitř programu

  • vysvětlit, jak se liší cat foo.txt a cat <foo.txt

  • vysvětlit, jak může být více programů používajících standardní vstup/výstup složeno (propojeno) dohromady

  • volitelné: vysvětlit, proč cd nemůže být normální spustitelný soubor jako /usr/bin/ls

Praktické dovednosti

Praktické dovednosti se obvykle týkají použití daných programů pro vyřešení různých úloh. Takže, dokážete …

  • vytvořit Linuxový skript se správným shebangem

  • nastavit executable bit skriptu pomocí utility chmod

  • přistupovat k argumentům příkazové řádky v Pythonovém skriptu

  • přesměrovat standardní vstup a výstup programů v shellu

  • používat standardní vstup a výstup v Pythonu

  • použít rouru (pipe) | pro řetězení více programů dohromady

  • používat základní filtry: cut, sort, …

  • použít grep -F pro filtrování řádků odpovídajících zadanému vzoru