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
neboranger
. - K procházení souborového systému a kontrole souborů můžete použít
cd
,pwd
,ls
,cat
(ahexdump
).
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ů.
Naším úkolem je napsat program, který vypíše stručný přehled těchto dat:
- Vytiskne 3 nejčastěji navštěvované adresy URL.
- Vytiskne celkového množství přenesených dat.
- 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.
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.
#!/usr/bin/env python3
. #!/usr/bin/env python
nebo #!/usr/bin/python3
jsou nesprávné a
mohou způsobit různá překvapení.
#
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).
#!/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.
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
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
.
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.
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:
...
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.
<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ě.
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
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.
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ů
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 cut
u 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
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
Příklady s automatizovanými testy
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
acat <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