Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
- Průběžný příklad
- Standardní vstup a výstup
- Filtry
- Roury čili pipes (skládání proudových dat)
- Psaní vlastních filtrů
- Standardní chybový výstup
- Pohled pod kapotu (aneb o deskriptorech souborů)
- Pokročilé přesměrování vstupu/výstupu
- Návratová hodnota programu (exit code)
- Přizpůsobení shellu
- Další příklady
- Úlohy před cvičením (deadline: začátek vašeho cvičení, týden 6. března - 10. března)
- Úlohy po cvičení (deadline: 26. března)
- Učební výstupy
- Seznam změn na této stránce
Cílem tohoto cvičení je definovat a do hloubky pochopit, co je to standardní
vstup (stdin), standardní výstup (stdout) a standardní chybový výstup
(stderr). To nám umožní porozumět tomu, jak funguje přesměrování vstupu a
výstupu (I/O redirection) a propojování programů pomocí pipes (český
ekvivalent “pipe” je doslova “roura”, ale tento pojem se v praxi téměř
nepoužívá). Také si přizpůsobíme chování našeho shellu - prozkoumáme, jak
fungují aliasy a .bashrc
.
Průběžný příklad
Toto cvičení bude postaveno na jednom příkladu, který budeme postupně rozvíjet, abyste se základní koncepty naučili na praktickém příkladu (samozřejmě existují specifické nástroje, které by se daly použít rovnou, ale doufáme, že je to lepší než zcela umělý příklad).
Data pro náš příklad lze stáhnout (tj. git clone
ovat) z tohoto
repozitáře,
kde se nacházejí v podadresáři 04/
.
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 3 dny s největším objemem provozu (tj. součtem přenesených bajtů).
- Vytiskne celkového množství přenesených dat.
Než začneme úlohu řešit, musíme se naučit úplné základy.
Standardní vstup a výstup
Cvičení začneme několika definicemi, které ale již pravděpodobně znáte (ale možná ne přesně pod těmito názvy).
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.
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.
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ě.
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
Answer.
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 vysvětlit rozdíl mezi těmito voláními:
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.
K předchozí otázce: rozdíl je v tom, že v prvním případě (s přesměrováním
vstupu) otevře /etc/passwd
shell a otevřený soubor (nějak) předá programu
cut
, který spustí. Pokud se soubor nepodaří otevřít, selhání ohlásí shell
a cut
se nejspíš ani nespustí. Ve druhém případě otevírá /etc/passwd
sám
běžící cut
, který dostane jméno souboru jako argument (tj. cut
zavolá
open
a obslouží případné chyby při otevírání souboru sám).
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
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 už ve svých skriptech nikdy nedělejte.
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. (Přesně vzato jsou
data 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ší.
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.
Náš program se tedy 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 (jen 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 unikátní položky (uniq je sám nesetřídi)
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c
# Setřídíme výstup uniq podle čísel
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c | sort -n
# A vytiskneme jen poslední řádky
cat logs/*.csv | cut -d , -f 5 | sort | uniq -c | sort -n | tail -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í.
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í.
Rychlá kontrola znalosti filtrů
Psaní vlastních filtrů
Dokončeme další část průběžného příkladu. Chceme vypočítat návštěvnost (ve smyslu množství přenesených dat) pro každý den a vypsat dny s největší návštěvností.
Vzhledem k tomu, jak jsme zatím vše poskládali, nám chybí pouze prostřední část roury. Součet velikostí pro jednotlivé dny.
Pro tento účel neexistuje žádné hotové řešení (pokročilí uživatelé mohou
zvážit instalaci termsql
), ale my
si vytvoříme vlastní v Pythonu a zapojíme ho do naší pipeline.
Budeme se snažit, aby byl jednoduchý, ale zároveň dostatečně univerzální.
Připomeňme si, že chceme seskupit provoz podle datumů, a proto by náš program měl být schopen provést následující transformaci.
# Vstup
day1 1
day1 2
day2 4
day1 3
day2 1
# Výstup
day1 6
day2 5
Zde je naše verze programu. Všimněte si, že jsme (prozatím) ignorovali ošetření chyb, ale umožnili jsme, aby program mohl být použit jako filtr uprostřed pipeline (tj. číst ze stdin, když nejsou uvedeny žádné argumenty), ale také snadno použitelný pro více souborů.
Ve vlastních filtrech byste měli tento přístup také dodržovat: množství zdrojového kódu, které je třeba napsat, je zanedbatelné, ale uživateli poskytuje flexibilitu při používání.
#!/usr/bin/env python3
import sys
def sum_file(inp, results):
for line in inp:
(key, number) = line.split(maxsplit=1)
results[key] = results.get(key, 0) + int(number)
def main():
sums = {}
if len(sys.argv) == 1:
sum_file(sys.stdin, sums)
else:
for filename in sys.argv[1:]:
with open(filename, "r") as inp:
sum_file(inp, sums)
for key, sum in sums.items():
print(f"{key} {sum}")
if __name__ == "__main__":
main()
S takovým programem můžeme náš skript webové statistiky rozšířit následujícím způsobem.
cat logs/*.csv | cut -d , -f 1,4 | tr ',' ' ' | ./group_sum.py
Pomocí man
zjistíte, co dělá tr
.
Sami rozšiřte řešení tak, aby se vytiskly pouze 3 nejlepší dny
(sort
může řadit řádky i pomocí jiných sloupečků než přes celý řádek).
Answer.
Standardní chybový výstup
Zatímco výstup programu chceme často přesměrovat někam jinam, chyby při běhu programu bychom rádi viděli na obrazovce.
Dejme tomu, že existují soubory one.txt
a two.txt
, ale nonexistent.txt
v aktuálním adresáři není. Spustíme následující příkaz.
Ne, nepředstavujte si to. Vytvořte soubory one.txt
a two.txt
, které budou obsahovat slova ONE
a two.txt
TWO
sami v příkazovém řádku.
Hint.
Answer.
cat one.txt nonexistent.txt two.txt >merged.txt
cat
celkem pochopitelně ohlásí chybu - neexistující soubor přečíst
nejde. Kdyby se ale tahle chybová hláška tiskla na stdout, přesměrovala by
se do merged.txt
společně s výstupem. To by nebylo úplně praktické.
A proto má každý Linuxový program ještě jeden výstup, standardní chybový výstup, kterému se říká stderr (ze standard error [output]). Stderr je obvykle inicializovaný stejně jako stdout, ale logicky je odlišný. Když přesměrujeme stdout pomocí >
, stderr to neovlivní.
V Pythonu je stderr dostupný jako sys.stderr
. Opět se chová jako otevřený
soubor.
Naši implementaci můžeme rozšířit tak, aby zpracovávala chyby I/O:
try:
with open(filename, "r") as inp:
sum_file(inp, sums)
except IOError as e:
print(f"Error reading file {filename}: {e}", file=sys.stderr)
Pohled pod kapotu (aneb o deskriptorech souborů)
Následující text poskytuje přehled souborových deskriptorů, což je abstrakce používaná operačním systémem a aplikacemi při práci s otevřenými soubory. Pochopení tohoto konceptu není pro tento kurz nezbytné, ale jedná se o obecný princip, který je (do určité míry) přítomen ve většině operačních systémů a aplikací (nebo programovacích jazyků).
Pokročilé přesměrování vstupu/výstupu
Ujistěte se, že máte k dispozici skript group_sum.py
.
Připravte si soubory one.txt
a two.txt
:
echo ONE 1 > one.txt
echo ONE 1 > two.txt
echo TWO 2 >> two.txt
Nyní proveďte následující příkazy.
./group_sum.py <one.txt
./group_sum.py one.txt
./group_sum.py one.txt two.txt
./group_sum.py one.txt <two.txt
Chovalo se to podle vašich očekávání?
Sledujte, jakými cestami (tj. přes které řádky) program prošel při výše uvedených spuštěních.
Přesměrování standardního chybového výstupu
Chcete-li přesměrovat standardní chybový výstup, můžete opět použít >
, ale tentokrát s číslem
2
(které označuje deskriptor souboru stderr).
Proto lze náš příklad cat
transformovat do následující podoby, kdy
err.txt
bude obsahovat chybovou zprávu a na obrazovku se nic nevypíše.
cat one.txt nonexistent.txt two.txt >merged.txt 2>err.txt
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
).
#!/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
Výše uvedený příklad je poněkud umělý, ale dobře demonstruje, že stdin je přirozeně dostupný i uvnitř skriptů bez ohledu na přesměrování z vnějšku.
Obecné přesměrování
Shell nám umožňuje přesměrovat výstupy zcela libovolně pomocí čísel deskriptorů souborů před a za znaménkem větší než.
Například >&2
určuje, že standardní výstup bude přesměrován na standardní chybový výstup.
Může to znít divně, ale vezměme si následující miniskript.
Zde se wget
používá ke stažení souboru ze zadané adresy URL.
echo "Downloading tarball for lab 02..." >&2
wget https://d3s.mff.cuni.cz/f/teaching/nswi177/202122/labs/nswi177-lab02.tar.gz 2>/dev/null
Ve skutečnosti chceme skrýt zprávy o průběhu wget
a místo nich vypsat naše
zprávy.
Berte to jako ilustraci konceptu, protože wget
lze ztišit i pomocí
argumentů příkazového řádku (--quiet
).
Někdy chceme přesměrovat stdout a stderr do jednoho souboru.
V těchto situacích by prosté >output.txt 2>output.txt
nefungovalo
a musíme použít >output.txt 2>&1
nebo &>output.txt
(pro přesměrování obou najednou).
Můžeme použít také 2>&1 >output.txt
?
Vyzkoušejte si to sami!
Hint.
Důležité speciální soubory
Již jsme se zmínili, že prakticky vše v Linuxu je soubor. Mnoho speciálních
souborů reprezentujících zařízení se nachází v podadresáři /dev/
.
Některé z nich jsou velmi užitečné pro přesměrování výstupu.
Spusťte cat one.txt
a přesměrujte výstup na /dev/full
a poté na
/dev/null
. Co se stalo?
Zejména soubor /dev/null
je velmi užitečný, protože jej lze použít v každé
situaci, kdy nás výstup programu nezajímá.
U mnoha programů můžete explicitně určit použití stdin pomocí -
(pomlčka)
jako názvu vstupního souboru.
Další možností je explicitně použít /dev/stdin
: s tímto názvem můžeme
zprovoznit příklad s group_sum.py
:
./group_sum.py /dev/stdin one.txt <two.txt
Pak Python otevře soubor /dev/stdin
jako soubor a operační systém (spolu s
shellem) jej skutečně spojí se souborem two.txt
.
/dev/stdout
lze použít pokud chceme explicitně zadat standardní výstup (to
je užitečné hlavně pro programy pocházející z jiných prostředí, kde není
kladen takový důraz na použití stdout).
Návratová hodnota programu (exit code)
Doposud naše programy chybu ohlašovaly tak, že zapsaly chybovou zprávu na standardní chybový výstup. To je celkem užitečné pro interaktivní programy, protože uživatel chce vědět, co se pokazilo.
Nicméně pro neinteraktivní použití by bylo nepohodlné rozpoznávat chyby tak, že bychom hledali chybové hlášky na standardním chybovém výstupu. Chybové hlášky se mění, mohou být lokalizované atd. Existuje proto jiný způsob, jak můžeme poznat, zda běh programu skončil chybou či nikoli.
Úspěch či neúspěch poznáme podle exit code. Exit code je celé číslo a na rozdíl od jiných programovacích jazyků indikuje 0 úspěch a libovolný nenulový kód indikuje chybu.
Uhodnete, proč autoři zvolili nulu pro indikaci úspěchu (zatímco v jiných
jazycích je logická hodnota nuly false
), zatímco nenulové kódy (jejichž
logická hodnota je obvykle true
) byly použity pro chyby? Nápověda: kolika
způsoby může program uspět?
Není-li řečeno jinak, když váš program úspěšně doběhne (například v Pythonu doběhne main a nedojde k vyhození výjimky), exit code by měl být nula.
Chcete-li změnit toho chování, můžete program ukončit s jiným chybovým kódem
jako argumentem funkce exit.V Pythonu je to
sys.exit`.
Následující příklad je modifikace výše uvedeného souboru group_sum.py
,
tentokrát se správným zpracováním exit kódu.
def main():
sums = {}
exit_code = 0
if len(sys.argv) == 1:
sum_file(sys.stdin, sums)
else:
for filename in sys.argv[1:]:
try:
with open(filename, "r") as inp:
sum_file(inp, sums)
except IOError as e:
print(f"Error reading file {filename}: {e}", file=sys.stderr)
exit_code = 1
for key, sum in sums.items():
print(f"{key} {sum}")
sys.exit(exit_code)
Později uvidíme, že různé control-flow konstrukce v shellu (podmínky a smyčky) se řídí právě tím, jaký byl exit code jednotlivých příkazů.
Rychlé selhávání
Dosud jsme očekávali, že naše shellové skripty nikdy neselžou. Na žádné chyby jsme je ani nepřipravovali.
Časem se podíváme, jak lze exit kódy testovat a používat k lepšímu řízení našich shellových skriptů, ale zatím chceme jen zastavit, kdykoli dojde k nějaké chybě.
To je vlastně docela rozumné chování: obvykle chcete, aby se celý program ukončil, pokud dojde k neočekávanému selhání (místo aby pokračoval s nekonzistentními daty). Podobně jako nezachycená výjimka v Pythonu.
Chcete-li povolit ukončení při chybě, musíte zavolat set -e
. V případě
neúspěchu shell ukončí provádění skriptu a skončí se stejným exit kódem jako
neúspěšný příkaz.
Kromě toho obvykle chcete skript ukončit, pokud je použita neinicializovaná
proměnná: to umožňuje set -u
. O proměnných si povíme později, ale -e
a
-u
se obvykle nastavují společně.
A ještě jedno upozornění týkající se roury a úspěšnosti příkazů: úspěšnost
roury je určena jejím posledním příkazem. Příkaz sort /nonexistent | head
je tedy úspěšný příkaz. Chcete-li, aby neúspěch některého příkazu způsobil
neúspěch (celé) pipeline, musíte ve skriptu (nebo shellu) před pipeline
spustit příkaz set -o pipefail
.
Obvykle tedy chcete skript začít následující trojicí:
set -o pipefail
set -e
set -u
Mnoho příkazů umožňuje takto slučovat krátké volby (jako -l
nebo -h
,
které znáte z ls
) (všimněte si, že -o pipefail
musí být na posledním
místě):
set -ueo pipefail
Navykněte si začínat každý váš skript tímto příkazem.
Pipeline GitLabu budou od této chvíle kontrolovat, zda je tento příkaz součástí vašich skriptů.
Úskalí rour (alias SIGPIPE)
Návratové kódy: zkontrolujte si, zda této části rozumíte
Přizpůsobení shellu
Již jsme se zmínili, že byste si měli emulátor terminálu přizpůsobit tak, aby se vám pohodlně používal. Koneckonců s ním strávíte minimálně tento semestr a jeho používání by vás mělo bavit.
V tomto cvičení si ukážeme některé další možnosti, jak si zpříjemnit používání shellu.
Aliasy příkazů
Pravděpodobně jste si všimli, že některé příkazy se stejnými argumenty
voláte často. Jedním z takových příkladů může být ls -l -h
, který vypíše
podrobný výpis souborů s použitím velikostí čitelných pro člověka. Nebo
třeba ls -F
, který k adresářům připojí lomítko. A pravděpodobně také ls --color
.
Shell nabízí vytvoření takzvaných aliasů, kde můžete snadno přidávat nové příkazy, aniž byste museli někde vytvářet plnohodnotné skripty.
Zkuste provést následující příkazy, abyste zjistili, jak lze definovat nový
příkaz l
.
alias l='ls -l -h`
l
Můžeme dokonce přepsat původní příkaz, shell zajistí, aby přepisování nebylo rekurzivní.
alias ls='ls -F --color=auto'
Všimněte si, že tyto dva aliasy společně zajišťují, že l
bude zobrazovat
názvy souborů barevně.
Kolem znaménka rovná se mezery nepíšeme.
Mezi typické aliasy, které pravděpodobně budete chtít vyzkoušet, patří
následující. Pokud si nejste jisti, k čemu alias slouží, použijte
manuálovou stránku. Všimněte si, že curl
se používá k načtení obsahu z
adresy URL a wttr.in
je skutečně adresa URL. Mimochodem, tento příkaz
vyzkoušejte, i když nemáte v plánu tento alias používat :-).
alias ls='ls -F --color=auto'
alias ll='ls -l'
alias l='ls -l -h'
alias cp='cp -i'
alias mv='mv -i'
alias rm='rm -i'
alias man='man -a'
alias weather='curl wttr.in'
~/.bashrc
Výše uvedené aliasy jsou pěkné, ale pravděpodobně je nechcete definovat při
každém spuštění shellu. Většina shellů v Linuxu však má nějaký soubor,
který spustí před vstupem do interaktivního režimu. Obvykle se tento soubor
nachází přímo ve vašem domovském adresáři a je pojmenován podle shellu a
končí na rc
(což si můžete zapamatovat jako runtime configuration, čili
běhové nastavení).
Pro Bash, který nyní používáme (pokud používáte jiný shell, pravděpodobně
již víte, kde najdete jeho konfigurační soubory), se tento soubor nazývá
~/.bashrc
.
Již jste jej použili při nastavení EDITOR
u pro Git, ale můžete tam také
přidávat aliasy. V závislosti na vaší distribuci se tam již mohou
zobrazovat existující aliasy nebo jiné příkazy.
Přidejte tam aliasy, které se vám líbí, uložte soubor a spusťte nový terminál. Zkontrolujte, zda aliasy fungují.
Soubor .bashrc
se chová jako skript shellu a nejste omezeni na to, abyste
v něm měli pouze aliasy. Můžete v něm mít prakticky libovolné příkazy,
které chcete spouštět v každém terminálu, který spustíte.
Změna promptu ($PS1
)
Můžete také upravit vzhled promptu. Výchozí nastavení je obvykle rozumné, ale někteří lidé chtějí vidět více informací. Pokud patříte mezi ně, zde jsou podrobnosti (berte je jako přehled, protože přizpůsobení promptu je téma na celou knihu).
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
.
Úlohy před cvičením (deadline: začátek vašeho cvičení, týden 6. března - 10. března)
Následující úlohy musí být vyřešeny a odevzdány před příchodem na vaše cvičení. Pokud máte cvičení ve středu v 10.40, soubory musí být nahrány do vašeho projektu (repozitáře) na GitLabu nejpozději ve středu v 10.39.
Pro virtuální cvičení je deadline úterý 9:00 (každý týden, vždy ráno, bez ohledu na možné státní svátky apod.).
Všechny úlohy (pokud není explicitně uvedeno jinak) musí být odevzdány přes váš repozitář na úkoly. Pro většinu úloh existují automatické testy, které vám mohou pomoci zkontrolovat úplnost vašeho řešení (zde je popsáno jak číst jejich výsledky).
04/line_count.sh
(30 bodů, skupina shell
)
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.
04/users.sh
(40 bodů, skupina admin
)
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 | 04/users.sh
.
04/fastest.sh
(30 bodů, skupina shell
)
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é).
Úlohy po cvičení (deadline: 26. března)
Očekáváme, že následující úlohy vyřešíte po cvičení, tj. poté, co budete mít zpětnou vazbu k vašim řešením úloh před cvičením.
Všechny úlohy (pokud není explicitně uvedeno jinak) musí být odevzdány přes váš repozitář na úkoly. Pro většinu úloh existují automatické testy, které vám mohou pomoci zkontrolovat úplnost vašeho řešení (zde je popsáno jak číst jejich výsledky).
04/row_sum.sh
(50 bodů, skupina shell
)
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).
04/day_of_week.py
(50 bodů, skupina devel
)
Napište filtr v Pythonu, který převede datum na den v týdnu.
Program převede data pouze v prvním sloupci (s použitím bílých znaků pro rozdělení do sloupečků), neplatná data budou ignorována (a řádek bude zachován v nezměněné podobě). Zbytek sloupce bude zkopírován do výstupu.
2023-02-20 Rest of the line
Some other line
2023-02-21 Line contents
Monday Rest of the line
Some other line
Tuesday Line contents
Program musí být možné spustit jako:
04/day_of_week.py <input.txt
04/day_of_week.py input.txt
cat one.txt two.txt | 04/day_of_week.py
Pokud se soubor nepodaří otevřít, program vypíše chybovou zprávu na stderr (přesné znění je definováno v testech) a ukončí se s kódem 1.
Můžete očekávat, že program nebude spouštěn jako 04/day_of_week.py one.txt two.txt
.
Předpokládáme, že budete používat funkce z modulu datetime.
Učební výstupy
Učební výstupy podávají zhuštěný souhrn základních konceptů a dovedností, které byste měli umět vysvětlit a/nebo použít po každém cvičení. Také obsahují absolutní minimum, které je potřebné pro pochopení navazujících cvičení (a dalších předmětů).
Znalosti konceptů
Znalost konceptů znamená, že rozumíte významu a kontextu daného tématu a jste schopni témata zasadit do většího rámce. Takže, jste schopni …
-
vysvětlit, co je 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, proč existují dva druhy výstupu (výstupních proudů): stdout a stderr
-
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
-
vysvětlit co je návratový kód programu (exit code)
-
vysvětlit rozdíly a typické využití pro pět hlavních rozhraní, které může využít CLI program: argumenty, stdin, stdout, stderr a návratová hodnota (exit code)
-
volitelné: vysvětlit, co je to deskriptor souboru (z pohledu aplikace, nikoliv OS/kernelu)
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 …
-
přesměrovat standardní (chybový) výstup a vstup CLI programů v shellu
-
změnit návratovou hodnotu (exit code) pro Pythoní skripty
-
používat speciální soubor
/dev/null
-
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 -
volitelné: upravit si chování shellu pomocí aliasů
-
volitelné: upravit si konfiguraci shellu pomocí
.bashrc
a.profile
skriptů -
volitelné: upravit si vzhled promptu pomocí proměnné
PS1
Seznam změn na této stránce
-
2023-02-25: Úloha
04/users.sh
přesunuta do skupinyadmin
. -
2023-03-03: Zdůraznění jak je stdin předáván dovnitř skriptu.