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

Cílem tohoto cvičení je představit vám klienta Gitu na příkazové řádce, aby naše komunikace s GitLabem byla mnohem efektivnější. Také si trochu rozšíříme znalosti o skriptování v shellu.

Předstartovní kontrola

  • Rozumíte konceptu standardního vstupu a výstupu a tomu, jak je lze v shellu přesměrovat (a zřetězit).
  • Pro zjednodušení spouštění skriptů můžete nastavit správný shebang a spustitelný (executable) bit.

Základy Gitu

Dosud byla naše práce s GitLabem výhradně skrze GUI. Teď se přesuneme do příkazové řádky, abychom pracovali trochu efektivněji.

Nezapomeňte, že GitLab, je postaven nad Gitem, což je vlastní verzovací systém.

Git má také klienta na příkazové řádce, který umožní stáhnout celý projekt na váš stroj, sledovat v něm změny a ty pak nahrát zpátky na server (v našem případě GitLab, ale není to jediný takový produkt).

Přestože je možné upravovat soubory on-line v GitLabu, bývá mnohem jednodušší mít je lokálně a používat nějaký lepší editor (příp. IDE). Navíc, ne všechny nástroje mají své on-line protějšky a tak je budete muset spouštět lokálně.

Než se pustíme do samotného Gitu, musíme si na něj trochu připravit prostředí.

Výběr editoru

Git bude často potřebovat spouštět váš editor, základem proto je zajistit, že použije ten, který jste si vybrali.

Následující kroky vysvětlíme podrobněji později, prozatím jen zajistěte, že přidáte následující řádek na konec souboru ~/.bashrc (mcedit v něm nahraďte za vámi vybraný editor):

export EDITOR=mcedit

Nyní otevřete nový terminál a spusťte (včetně znaku dolaru):

$EDITOR ~/.bashrc

Pokud jste soubor upravili správně, měli byste jej opět vidět otevřený ve vašem oblíbeném textovém editoru.

Pokud ne, ujistěte se, že jste skutečně upravili svůj soubor .bashrc (ve svém domovském adresáři) tak, aby obsahoval to, co je uvedeno výše (bez mezer kolem = atd.).

Aby se změny projevily, je potřeba předem zavřít všechny terminály (tedy před použitím Gitových příkazů zmíněných níže).
Nikdy nepoužívejte grafický editor pro $EDITOR, pokud opravdu nevíte, co děláte. Git očekává od editoru určité chování, které grafické editory splňují jen zřídka, zatímco TUI editory tak fungují vždy.
Pokud chcete vědět, proč jsou grafické editory špatnou volbou, vysvětlení je poměrně jednoduché: Git spustí nový editor pro úpravu zprávy o commitu (viz níže) a bude předpokládat, že zpráva je hotová, jakmile editor skončí. Mnoho editorů s grafickým uživatelským rozhraním však pracuje v režimu, kdy je spuštěna jediná instance a vy pouze otevíráte nové karty. V takovém případě se editor spuštěný Gitem ve skutečnosti okamžitě ukončí - pouze řekne stávajícímu editoru, aby otevřel nový soubor - a Git tak vidí pouze prázdnou commit zprávu.

Pokud to vypadá, že váš ~/.bashrc je pořád ignorován, možná není doopravdy přečten při startu (naschvál se tu snažíme vyhnout technickým detailům ohledně načítání .bashrc).

Přidání následujícího do ~/.bash_profile by mohlo pomoci (nebo se nám ozvěte).

if [ -f ~/.bashrc ] ; then
    . ~/.bashrc
fi

Příkaz git

V podstatě všechno okolo Gitu je řízeno příkazem git. Prvním argumentem je vždy akce – občas se jí říká také podpříkaz –, kterou chceme provést. Například, git config nastavuje Git a git commit vytváří nový commit (verzi).

Vždy je k dispozici nápověda pomocí následujícího příkazu:

git PODPRIKAZ --help

Manuálové stránky jsou dostupné pomocí man git-PODPRIKAZ.

Git má přes 100 podpříkazů. Nepropadejte panice. Začneme s méně než 10 a i poměrně pokročilé funkce nevyžadují znát více než 20 z nich.

Nastavení Gitu

Jedním z klíčových konceptů v Gitu je, že každý commit (změna) má autora – tedy je známo, kdo jej vytvořil. (Git zvládá i kryptografické podepisováni commitů, takže autorství je nezpochybnitelné, ale tím si teď nebudeme látku komplikovat.)

Potřebujeme tedy Gitu říct, kdo jsme. Následující dva příkazy jsou absolutní minimum, co potřebujete spustit na počítači (nebo účtu), kde chcete pracovat s Gitem.

git config --global user.name "Moje skutečné jméno"
git config --global user.email "můj-email"

Přepínač --global říká, že nastavení má být platné pro všechny Gitové projekty. Pro lokální změnu můžete spustit stejný příkaz bez tohoto přepínače uvnitř konkrétního projektu. To může být například užitečné pro odlišení vaší soukromé a firemní identity.

Poznamenejme, že Git neověřuje správnost e-mailové adresy ani vašeho jména (stejně ani není, jak by mohl). Může zde být tedy cokoliv. Pokud ale použijete svou skutečnou e-mailovou adresu, GitLab dokáže spárovat commity s vaším účtem, atp., což může být užitečné. Rozhodnutí je ale na vás.

Pracovní kopie čili working copy (neboli práce s Gitem lokálně)

Úplně první operací, kterou je třeba provést, je tzv. klon (clone). Během klonování se vytvoří kopie zdrojáků ze serveru (GitLab) na místní stroj. Server může pro tuto operaci vyžadovat nějaký druh autentikace (ověření uživatele).

Můžete také vytvořit prázdný repozitář Git pouze lokálně (a později jej propojit s nějakým vzdáleným). Pokud vás to zajímá, příkaz, který hledáte, je git init.

Klonování zkopíruje kompletní historii projektu. Všechny commity jsou vidět i na naklonovaném projektu. Bez potřeby internetového připojení.

Klon se často nazývá pracovní kopií (working copy). Klon je skutečně kopií 1:1 – pokud by někdo projekt vymazal, zdrojové kódy půjde obnovit z klonu (včetně kompletní historie). (To neplatí o Wiki nebo Issues protože ty nejsou Gitem verzované.)

Jak uvidíte, celý projekt z GitLabu se naklonuje jako adresář na vašem disku. Jako obvykle, existují i GUI alternativy k příkazům, které tu budeme ukazovat. My se ale zaměříme jen na CLI variantu.

První klonování (git clone)

Pro následující příklad použijeme váš repozitář s řešeními ve skupině teaching/nswi177/2024.

Otevřete svůj projekt (v prohlížeči) a klikněte na modré tlačítko Clone. Měli byste vidět adresy pro Klonování přes SSH a Klonování přes HTTPS.

Zkopírujte adresu HTTPS a použijte ji v příkazu clone:

git clone https://gitlab.mff.cuni.cz/teaching/nswi177/2024/student-LOGIN.git

Příkaz se vás zeptá na uživatelské jméno a heslo. Jako obvykle v našem GitLabu, použijte prosím přihlašovací údaje do SISu.

Některá prostředí můžou nabízet k použití tzv. klíčenky nebo podobné nástroje na ukládání přihlašovacích údajů. Klidně je používejte. Později se podíváme na to, jak používat SSH a asymetrickou kryptografii pro bezproblémovou práci s projekty Gitu bez potřeby starat se o uživatelská jména a hesla.

Zdá se, že některá prostředí jsou poměrně “vlezlá” co se týče propagace svých klíčenek (a pokud zadáte heslo špatně, neexistuje snadná cesta, jak uložené heslo vygumovat).

Pokud narazíte na chybu HTTP Basic: Access denied. a nezobrazí se žádný prompt, zkuste spustit nejdříve následující.

export GIT_ASKPASS=""
export SSH_ASKPASS=""
git clone ...

Nyní byste měli mít na svém počítači adresář student-LOGIN. Přesuňte se do něj a podívejte se, jaké soubory tam jsou. A co skryté soubory? Odpověď.

Neřekneme-li jinak, všechny příkazy budeme vykonávat uvnitř adresáře student-LOGIN.

Jakmile je projekt naklonován, můžete začít upravovat soubory. To je naprosto nezávislé na Gitu – dokud Gitu neřeknete, že má něco udělat, vašich souborů se nijak nedotkne.

Je také důležité zmínit, že Git nestáhne aktualizace ze serveru automaticky. Takže poté co si projekt naklonujete a změníte nějaké soubory přímo na GitLabu, tyto změny se neprojeví na vaší pracovní kopii (dokud si o ně explicitně neřeknete).

Jakmile jste dokončili práci na změnách (např. jste opravili chybu), je na čase Gitu říct o nové verzi (revizi).

Provedení změn (git status a git diff)

Než začnete lokálně měnit jakýkoli soubor, otevřete nový terminál a spusťte git status. Měli byste vidět něco takového.

$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

Nyní provedeme triviální změnu. Otevřete soubor README.md ve svém projektu (lokálně, tj. ne v uživatelském rozhraní GitLabu v prohlížeči) a přidejte do něj odkaz na Fórum.

Všimněte si, jak jsou v Markdown vytvořeny odkazy, a přidejte jej jako poslední odstavec.

Po provedení změny spusťte git status. Pozorně si přečtěte celý výstup tohoto příkazu, abyste pochopili, co vlastně vypisuje.

Vytvořte nový soubor 04/editor.txt a vložte do něj název editoru, který jste se rozhodli používat (klidně vytvořte adresář 04 v nějakém grafickém nástroji nebo použijte mkdir 04).

Opět se podívejte, jak git status hlásí tuto změnu v adresáři projektu.

Co jste se naučili? Odpověď.

Spusťte git diff pro zobrazení toho, jak Git sleduje provedené změny.

Zobrazí se seznam změněných souborů (tj. jejich obsah se liší od poslední revize) a také tzv. diff (někdy také nazývaný záplata čili patch), který popisuje změnu.

Diff bude obvykle vypadat nějak takto:

diff --git a/README.md b/README.md
index 39abc23..61ad679 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,5 @@
 find and check your solution.

 Course homepage: https://d3s.mff.cuni.cz/teaching/nswi177/
+
+Forum is at ...

Jak tohle přečíst? Je to obyčejný text, který obsahuje následující:

  • soubor, kde ke změně došlo
  • kontext změny
    • čísla řádků (-3,3 +3,5)
    • řádky beze změny (začínají mezerou)
  • skutečné změny
    • přidané řádky (začínají +)
    • odebrané rádky (začínají -)

Proč je tento výstup vhodný pro změny ve zdrojovém kódu?

Příkaz git diff je taky extrémně užitečný pro ověření, že provedené změny jsou správné, tím že se zaměřuje jen na kontext změn namísto celých souborů.

Uložení změny natrvalo (git add a git commit)

Jakmile jste se změnami spokojeni, můžete je připravit k zapsání (staging). Což v Gitovštině znamená, že tyto soubory (jejich současný stav) budou v další verzi. Obvykle budete připravovat všechny změněné soubory. Ale občas je lepší commit rozdělit, protože jste pracovali na dvou věcech a nejdřív commitnete první část a teprve pak tu druhou.

Například jste opravovali chybu, ale také jste našli někde nějaký překlep. Můžete samozřejmě obě opravy přidat najednou do jednoho commitu, ale je mnohem lepší udržet commity malé a cílené na jednu věc. Takže první commit bude Oprava chyby XY zatímco druhý bude Oprava preklepu.

Což krásně říká, co se vlastně stalo. Je to trochu podobné tomu, jak vytváříte funkce při programování. Jedna funkce má dělat jednu věc (a dělat ji dobře). Jediný commit má zachycovat jedinou změnu.

Teď se připravte na svůj první commit (pro připomenutí: commit je v podstatě verze nebo pojmenovaný stav projektu) – spusťte git add 04/editor.txt. O rozšíření se postaráme později v souboru README.md.

Jak to změnilo výstup git stutus? Odpověď.

Poté co připravíte všechny relevantní změny (tj. git addnete všechny potřebné soubory), můžete vytvořit commit. Commit vyčistí změny připravené k zapsání a můžete začít opravovat další chybu :-).

Vytvořte svůj první commit pomocí git commit. Nezapomeňte pro něj použít výstižnou commit zprávu!

Poznamenejme, že bez přepínačů git commit otevře váš textový editor. Napište do něj commit zprávu, uložte a editor zavřete. Váš první commit je tímto dokončen.

Pro krátké commit zprávy můžete použít git commit -m "Typo fix", kde celá commit zpráva je dána argumentem přepínače -m (všimněte si uvozovek kvůli použití mezery).

Jak bude git status vypadat teď? Zamyslete se nad tím před spuštěním příkazu!

A tohle v podstatě opakujete tak dlouho, dokud máte co měnit. Nezapomeňte, že každý commit by měl zachytit nějaký rozumný stav projektu, ke kterému dává smysl se vracet.

Kdykoli vytvoříte novou verzi, zůstane commit lokální. Není nijak automaticky poslána zpět na server.

Odeslání změn na server

Chcete-li nahrát commity (revize) zpět na server, musíte iniciovat takzvaný push. Ten nahraje všechny nové verze (tj. ty, které byly provedeny mezi vaším naklonováním a nyní) zpět na server. Příkaz je poměrně jednoduchý.

git push

Znovu vás požádá o heslo a poté byste měli vidět své změny v GitLabu.

Které změny jsou na GitLabu? Odpověď.

Cvičení

Přidejte odkaz na Fórum jako druhý commit provedený z příkazového řádku.

Jako třetí commit vytvořte skript 04/architecture.sh, který obsahuje správný shebang, je spustitelný a vypíše aktuální architekturu (pokud jste tento úkol v předchozím cvičení přeskočili, jednoduše tam spusťte pouze uname nebo si teď najděte správný přepínač v manuálu).

Pošlete změny do GitLabu. Všimněte si, že všechny commity byly poslány najednou.

Prohlížení commitů (git log)

Prozkoumejte, co je v menu Repository -> Commits v GitLabu. Porovnejte to s výstupem git log a git log --oneline.

Ano, příkazy mohou být i takto jednoduché.

Získání změn ze serveru

Ujistěte se, že jste nejprve odevzdali a odeslali všechny změny v lokální (pracovní) kopii do GitLabu.

Změňte nadpis v README.md aby také obsahoval pro VAŠE JMÉNO. Tentokrát ale proveďte změnu na GitLabu.

Pro aktualizaci lokálního klonu projektu použijte git pull.

Jak nejsnadněji zjistíte, jestli máte i změnu v souboru README.md? na vašem počítači po provedení git pull? Odpověď.

Příkaz git pull je opravdu mocný, dokáže začlenit změny, které vznikly v podstatě ve stejnou dobu na GitLabu a ve vašem lokálním klonu. Porozumění tomuto procesu ale vyžaduje také znalosti větví, což je mimo rámec tohoto cvičení.

Prozatím si tak pamatujte nemíchat lokální změny s těmi v GitLabu (nebo na jiném stroji) aniž byste vždy ukončili práci pomocí git push a začali pomocí git pull.

Práce na více strojích

Věci se trochu zkomplikují pokud pracujete na více počítačích (např. ráno na školním počítači a večer na vašem notebooku).

Git je dost šikovný na to, aby dokázal sloučit vaše změny z různých míst.

Prozatím je však nejlepší zajistit následující postup, aby se minimalizovalo množství nekompatibilních úprav.

Uvědomte si, že pokud se něco hodně pokazí, můžete vždy provést nový klon do jiného adresáře, zkopírovat soubory ručně a odstranit poškozený klon.

Dokud budete pracovat následujícím způsobem, nic se nemůže rozbít:

  1. Naklonujete práci na stroji A.
  2. Pracujete na stroji A (a commitujete).
  3. Push ze stroje A (na server).
  4. Přesunete se na počítač B a tam projekt naklonujete.
  5. Pracujete na stroji B (a commitujete).
  6. Push ze stroje B (na server).
  7. Přesunete se zpět na A a pullnete (ze serveru).
  8. Pracujete na A (commitujete).
  9. Push z A.
  10. Pull na B.
  11. Práce na B.
  12. Atd. (zpátky na bod 5).

Pokud zapomenete na nějaký synchronizační push/pull při přesunu mezi stroji, tak se mohou objevit problémy. Jsou poměrně jednoduše řešitelné, ale my je budeme probírat až později.

Zatím můžete vždy udělat čistý klon a prostě překopírovat změněné soubory a znovu je commitnout (není to to pravé ořechové, tedy Gitové, ale bude to fungovat).

Něco navíc

Příkaz git log zobrazuje spoustu informací, obvykle nás ale zajímají jen ty nejnovější. Použijeme je pro připomenutí, na čem jsme pracovali, atp.

Následující příkaz tak bude dávat větší smysl:

git log --max-count=20 --oneline

Je to ale delší a obtížně zapamatovatelné. Zkuste toto:

git config --global alias.ls 'log --max-count=20 --oneline'

To je ještě horší! S touto magii ale Git náhle začne rozpoznávat následující podpříkaz:

git ls

A to může ušetřit čas.

Naše oblíbené aliasy jsou pro následující příkazy.

st = status
ci = commit
ll = log --format='tformat:%C(yellow)%h%Creset %an (%cr) %C(yellow)%s%Creset' --max-count=20 --first-parent

Zkuste si je nejprve spustit před přidáním do svého Gitu.

Nepropadejte panice

Může se zdát, že Gitových příkazů je strašně moc a celý prostup je hrozně složitý. Může to tak vypadat prvních pár dní. Ale jakmile je začnete používat pravidelně, práce s Gitem ustoupí na pozadí a stane se z ní přirozenou součást vývojového procesu.

Níže je malý diagram základních operací a jejich pořadí a vztahů.

Basic Git Workflow / Operations

Git: zkontrolujte si, že si pamatujete základní příkazy

Vyberte všechna pravdivá tvrzení.

You need to have enabled JavaScript for the quiz to work.

Více o shebangs

V této části cvičení se trochu vrátíme k tomu předchozímu a rozšíříme si znalosti o shebangs.

Použití jiných interpretrů

Vytvořte absolutní (!) cestu (nápověda: man 1 realpath) k args.py, který jsme používali předtím. Použijte ji jako shebang v jinak prázdném souboru (např. use-args) a označte jej jako spustitelný. Nápověda.

Poté jej spusťte takto:

./use-args
./use-args first second

Zjistíte, že argument nula teď obsahuje cestu k vašemu skriptu, argument na pozici jedna obsahuje vnější skript – use-args a až za nimi jsou vlastní argumenty z příkazové řádky (first a second).

Tohle je zásadní – když přidáme shebang, interpretr obdrží vstupní název souboru jako první argument. Jinými slovy – každý Linuxový interpretr musí začít vykonávat program, který dostane jako jméno souboru v prvním argumentu.

Přestože to může vypadat jako cvičení ve zbytečnostech, jde o demonstraci důležitého principu: Linux je extrémně nakloněn vytváření minijazyků. Potřebujete-li vytvořit interpretr pro svůj vlastní minijazyk, stačí jen zařídit, aby přijímal vstupní jméno souboru jako první argument a voilà, uživatelé v něm můžou psát své vlastní spustitelné soubory.

Jako další příklad si připravte následující soubor, uložte jej jako experiment (bez přípony) a označte jej jako spustitelný:

#!/bin/bash

echo Hello

Poznamenejme, že jsme se rozhodli znova úplně zahodit příponu, protože uživatele ve skutečnosti nezajímá jaký jazyk jsme použili. A ono je to stejně zachyceno pomocí shebangu.

Nyní změňte shebang na #!/bin/cat a spusťte program znova. Co se stalo? Spusťte jej s nějakým argumentem (např. jako ./experiment experiment). Co se stalo? Odpověď.

Změňte shebang na /bin/echo. Co se stalo?

Shebang: zkontrolujte si, zda této části rozumíte

Budeme předpokládat, že my-cat i my-echo jsou spustitelné skripty v aktuálním adresáři.

my-cat obsahuje jako svůj jediný obsah následující shebang #!/bin/cat a my-echo obsahuje pouze #!/bin/echo.

Vyberte všechna pravdivá tvrzení.

You need to have enabled JavaScript for the quiz to work.

Spouštění testů lokálně

Protože už víte, co jsou shebang, executable bit, a skripty obecně, máte dostatek znalostí, abyste spustili naše testy lokálně bez potřeby GitLabu.

Mělo by to učinit váš vývoj rychlejší a přirozenější, když nebudete muset čekat na GitLab.

Jednoduše spusťte ./bin/run_tests.sh v kořenovém adresáři vašeho projektu a prohlédněte si výsledky.

Můžete také spustit jen konkrétní podmnožinu testů.

./bin/run_tests.sh 04
./bin/run_tests.sh 02/architecture

Poznámka: Používate-li vlastní instalaci Linuxu, může být potřeba předem nainstalovat balíček bats (či bash-bats nebo bats-core).

Psaní vlastních filtrů

Dokončeme další část průběžného příkladu z předchozího cvičení: chceme vypočítat přenesená data pro každý den a vypsat dny s největším provozem.

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). Odpověď.

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

Základy jsme probrali v minulém cvičení. V tomto textu téma rozšíříme.

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. Nápověda. Odpověď.

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 předmět 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ů).

S každým otevřeným souborem je asociovaný takzvaný file descriptor. File descriptory se používají jako identifikátory otevřených souborů při komunikaci s operačním systémem (říkali jsme, že vlastní souborové operace vykonává operační systém). File descriptor je celé číslo, které slouží jako index do tabulky aktuálně otevřených souborů, kterou si operační systém udržuje pro každý proces (proces je pro nás běžící instance programu).

Toto číslo – file deskriptor – je pak předán systémovýmu volání (syscall), které s ním pracuje. Například syscall write dostane dva argumenty: file descriptor a buffer s daty, která má operační systém zapsat (v příkladech je teď budeme pro jednoduchost předávat přímo jako string). Takže když váš program zavolá print("Message", file=some_file), ve skutečnosti se “někde vespod” provede volání write(3, "Message\n"), kde 3 je file descriptor asociovaný s otevřeným souborem some_file.

Tohle je trochu technické, ale pomůže vám to pochopit, proč přesměrování standardního chybového výstupu vypadá tak, jak vypadá, a proč většina operací se soubory vyžaduje, aby byl příslušný soubor nejprve otevřen (tedy proč write_to_file(filename, contents) není elementární operace).

V unixovém prostředí jsou file descriptory 0, 1 a 2 po řadě přiřazeny standardnímu vstupu, výstupu a chybovému výstupu. Proto print("Message") v Pythonu nakonec zavolá write(1, "Message\n") a volání print("Error", file=sys.stderr) volá write(2, "Error\n").

Při spuštění nového procesu získá tyto tři deskriptory souborů od svého volajícího (např. shellu). Ve výchozím nastavení ukazují na terminál, ale volající je může jednoduše otevřít tak, aby ukazovaly na jiný soubor. Takto také funguje přesměrování.

Skutečnost, že stdout a stderr jsou logicky odlišné proudy (soubory), také vysvětluje slovo pravděpodobně v jednom z výše uvedených příkladů. Přestože oba končí ve stejném fyzickém zařízení (terminálu), mohou používat odlišnou konfiguraci: standardní výstup je typicky bufferovaný, tj. výstup vaší aplikace jde na obrazovku až když je ho dostatek, zatímco standardní chybový výstup bufferovaný není - je vypisován okamžitě. Důvod je pravděpodobně zřejmý – chybová hlášení by měla být viditelná co nejdříve, zatímco normální výstup může být kvůli zvýšení výkonu přeci jen o chvíli odložen.

Bufferování se obecně může chovat složitěji. Stačí ale myslet na to, že zatímco standardní chybový výstup obvykle vidíme hned, standardní výstup může být zpožděný.

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

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/202324/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! Nápověda.

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`.

U programů v jazyce C vrací funkce main ve skutečnosti hodnotu int, jejíž hodnotou je návratový kód programu. Používejte ji správně.

Plná signatura je vlastně int main(int argc, char *argv[]), takže můžete přistupovat k volbám příkazového řádku jako k argumentům funkce (většina prostředí vám ve skutečnosti umožní použít prosté void main(void), ale nedoporučuje se to).

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)

set -ueo pipefail může někdy způsobit nežádoucí a zcela neočekávané chování.

Následující skript skončí s těžko vysvětlitelnou chybou, tj. nikdy se nedostaneme k závěrečnému echo. Všimněte si, že závěrečný hexdump je zde pouze proto, abychom zajistili, že nevypíšeme smetí z /dev/urandom přímo na terminál.

#!/bin/bash

set -ueo pipefail

cat /dev/urandom | head -n 1 | hexdump

echo OKAY NOT PRINTED

Přestože vše vypadá v pořádku.

Důvodem je příkaz head. příkaz head má velmi chytrou implementaci, která ukončí činnost po vypsání prvních -n řádků. Rozumné, že? Ale to znamená, že první cat najednou zapisuje do roury, kterou nikdo nečte. Je to jako zápis do souboru, který už byl uzavřen. To generuje výjimku (no, tak trochu) a cat se ukončí s chybou. Kvůli set -o pipefail selže celá pipeline.

Pravdou je, že rozlišit, zda je uzavřená roura platnou situací, kterou je třeba vůbec řešit, nebo zda indikuje problém, je nemožné. Proto se cat ukončí s chybou (někdo přeci jen uzavřel jeho výstup, aniž by mu to dal předtím vědět), a shell tak musí označit celou pipeline za neúspěšnou.

Řešení tohoto problému není vždy snadné a je k dispozici několik možností. Každá z nich má své výhody a nevýhody.

Pokud víte, proč k tomu může dojít, přidáním || true označíte rouru jako v pořádku (o || se však dozvíme později).

Návratové kódy: zkontrolujte si, zda této části rozumíte

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

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í EDITORu 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).

Prompt se mění pomocí proměnné PS1. O proměnných budeme podrobněji hovořit později, nyní se naučíme pouze syntaxi.

Při nastavování proměnné ji můžeme přímo změnit v shellu a ihned sledovat výsledek.

Nyní proveďte následující příkazy.

PS1=''

Prompt je pryč. Nastavili jsme jej na prázdný řetězec.

PS1='Zadejte sve prikazy: '

Tohle je mnohem lepší, že?

A co třeba:

PS1='\w '

Zde nastavíme, aby vypisoval aktuální adresář a mezeru. Speciální sekvence \w bude automaticky nahrazena názvem pracovního adresáře.

Mnoho uživatelů chce vědět, jako který uživatel jsou přihlášeni.

PS1='\u: \w '

Obvyklou tradicí je končit prompt znakem dolaru.

PS1='\u \w\$ '

Pomocí speciální sekvence \[\033[01;32m\] a \[\033[0m\] můžeme změnit i barvu promptu.

PS1='\[\033[01;32m\]\u \w\[\033[0m\]\$ '

Chcete-li jinou barvu, použijte místo 32 jiná čísla. Speciální hodnota 0m přepne zpět na výchozí barvu terminálu.

Je také možné přidat vlastní příkazy, které se mají spustit, nebo dokonce vytvořit víceřádkový prompt.

PS1='$( date ) \u \w\$ '

Zde speciální část $( date ) označuje, že výstup z programu date se stane součástí promptu (o konstrukci $( ) si povíme později, zde ji berte jen jako takovou ochutnávku).

Použití \n nám umožňuje rozdělit prompt na více řádků.

PS1='\n$( date )\n\u \w\$ '

A samozřejmě lze vše kombinovat.

PS1='\n\[\033[01;32m\]$( date )\[\033[0m\]\n\[\033[01;34m\]\u\[\033[0m\] \[\033[01;35m\]\w\[\033[0m\]\$ '

Úlohy k ověření vašich znalostí

Očekáváme, že následující úlohy vyřešíte ještě před příchodem na cvičení, takže se budeme moci o vašich řešeních na cvičení pobavit.

Pro následující úkoly budete muset použít klienta Git, protože uživatelské rozhraní GitLabu nenabízí žádnou možnost nastavení spustitelného bitu. Berte to jako přípravu na test v příštím cvičení (5. týden semestru), který budete odevzdávat do GitLabu (budete moci použít i webové rozhraní, ale CLI je prostě rychlejší).

Rozšiřte své řešení ze cvičení 02 a napište skript, který vypíše, jakou hardwarovou architekturu má váš počítač.

Ujistěte se, že váš skript má správný shebang a je označen jako spustitelný.

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

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.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 04/day_of_week.py 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 je pracovní kopie Gitu (klon, working copy)

  • vysvětlit, proč existují dva druhy výstupu (výstupních proudů): stdout a stderr

  • 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 …

  • nastavit informace o autorovi v Gitu

  • nastavení výchozího editoru v shellu (nastavení EDITOR v ~/.bashrc)

  • naklonovat Gitový repozitář v shellu přes HTTPS

  • prohlédnout změny v Gitové pracovní kopii (příkaz git status)

  • vytvoření commitu v Gitu z příkazové řádky (příkazy git add a git commit)

  • nahrát nové commity na Git server nebo stáhnout nové do pracovní kopie (za předpokladu projektu s jedním uživatelem, příkazy git push a git pull)

  • prohlédnout si souhrnné informace o předchozích commitech pomocí git log

  • volitelné: upravit si chování Gitu pomocí aliasů

  • přesměrovat standardní výstup a standardní chybový výstup programu v shellu

  • změnit návratovou hodnotu (exit code) pro Pythoní skripty

  • používat speciální soubor /dev/null

  • používat standardní chybový výstup v Pythonu

  • 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

  • 2024-06-24: Poznámka o .bash_profile.