Skip to content

Latest commit

 

History

History
418 lines (299 loc) · 21.7 KB

vstup-výstup.md

File metadata and controls

418 lines (299 loc) · 21.7 KB

Vstup, výstup a přesměrování

!Štítky: {program}{bash} !OblíbenáZaklínadla: !ÚzkýRežim: zap

Úvod

Tato kapitola pokrývá nástroje interpretu Bash k ovládání vstupů a výstupů spouštěných příkazů i vstupů a výstupů samotného interpretu. Rovněž pokrývá nástroje ke čtení textových řetězců ze souborů, terminálu či výstupu spoustěného programu a nástroje k zápisu textových řetězců do souboru, na terminál, do souboru nebo na vstup spouštěného programu.

Interpret Bash je vyvíjen v rámci projektu GNU.

Definice

  • Deskriptor je (nejen v Bashi, ale pro každý jednotlivý proces v systému) číslovaný vstup nebo výstup. Základní deskriptory jsou „standardní vstup“ (číslo 0, „stdin“, budu zkracovat jako „s.vstup“), „standardní výstup“ (číslo 1, „stdout“, budu zkracovat jako „s.výstup“) a „standardní chybový výstup“ (číslo 2, „stderr“, budu zkracovat jako „s.ch. výstup“). Deskriptory 3 až 9 jsou určeny pro libovolné použití, deskriptory 10 až 255 pro vnitřní použití interpretem.
  • Vstup je deskriptor, ze kterého může proces číst data.
  • Výstup je deskriptor, do kterého může proces zapisovat data.
  • Roura („pipeline“, v některých příručkách také nazývaná „kolona“) je spojení dvou nebo více jednoduchých příkazů operátorem „|“. Bash pak tyto příkazy spustí paralelně a připojí standardní výstup příkazu nalevo od | na standardní vstup příkazu napravo od |. Díky tomu pak data „protékají“ přímo z jednoho procesu do druhého. Návratovým kódem roury je ve výchozím nastavení návratový kód posledního uvedeného jednoduchého příkazu.
  • Přesměrování deskriptorů (či jen přesměrování) je úkon, při kterém Bash něco provede s deskriptory vznikajícího procesu (nebo svými vlastními). Přesměrování se provádí jedno po druhém zleva doprava, jak jsou zadána na příkazové řádce.

Užitečná poznámka ke spouštění příkazů: kdykoliv z Bashe spustíte jakýkoliv nový proces, ten nejprve zdědí všechny deskriptory od interpretu (ne jen ty tři základní), pak pro něj Bash provede přesměrování deskriptorů specifikovaná na příkazovém řádku (včetně propojení procesů rour) a pak se teprve pokusí program spustit. To znamená, že vedlejší účinky přesměrování (např. vytvoření souborů) se projeví i v případě, že se Bashi program spustit nepodaří (např. proto, že program takového názvu neexistuje).

!ÚzkýRežim: vyp

Zaklínadla

Roura

# přesměrovat s.výstup příkazu A na s.vstup příkazu B
// Varianta „|&“ přesměruje navíc i s.ch. výstup.
{příkaz-A včetně přesměrování} [|[&] {příkaz-B včetně přesměrování}]...

# totéž, ale příkaz B přitom neuzavřít do podprostředí
{příkaz-B včetně přesměrování} < <({příkaz-A včetně přesměrování})

Přesměrování vstupu (čtení odněkud)

# čtení z existujícího souboru (obecně/příklad)
[{deskriptor}]< {cesta}
< "../můj soubor.txt"

# čtení ze zadaného textu (obecně/příklady)
// Poznámka: Bash za konec zadaného textu vždy připojí znak „\n“, i v případě, že už tam je!
[{deskriptor}]<<< {parametr}
sort <<< $'zoo\nahoj\nseminář'
sort <<< "A$(echo BC)D"

# čtení z prázdného vstupu
[{deskriptor}]< /dev/null

# čtení ze s.výstupu bloku příkazů (obecně/příklad)
[{deskriptor}]<␣<({příkazy...})
sort <␣<(echo Zoo; echo Abeceda)

# zduplikovat/přečíslovat deskriptor pro čtení

[{cílový-deskriptor}]<&{zdrojový-deskriptor}
[{cílový-deskriptor}]<&{zdrojový-deskriptor}-

# zduplikovat pojmenovaný deskriptor pro čtení
{příkaz a parametry} [{cílový-deskriptor}]<&${identifikator-pojm-desk}

# čtení z bloku řádků
// Zadaný ukončovač musí být neprázdný řetězec. Bash na přesměrovávaný vstup zapíše všechny řádky počínaje prvním řádkem po aktuálním příkazu a konče poslední řádkou, která se přesně neshoduje s ukončovačem (není dovolena odlišnost ani v bílých znacích na začátku či konci řádky!).
[{deskriptor}]<< '{ukončovač}'
{řádky textu}
{ukončovač}

# zavřít deskriptor
{deskriptor}<&-

# čtení z bloku řádků po rozvoji
// Pozor! Při použití tohoto zaklínadla nesmí být žádný znak v identifikátoru odzvláštněn (tzn. používejte jen znaky dovolené v názvu proměnné). Bash na přesměrovávaný vstup zapíše všechny řádky počínaje prvním řádkem po aktuálním příkazu a konče poslední řádkou, která se přesně neshoduje se zadaným identifikátorem (shoda musí být přesná, není dovolena odlišnost ani v bílých znacích na začátku či konci). Před použitím Bash celý blok řádek intepretuje, přičemž za zvláštní považuje pouze znaky „$“, „\“ a „`“ (s výjimkou apostrofů s dolarem, ty tam přímo použít nelze), můžete tam tedy dosazovat hodnoty proměnných a výstupy příkazů.
[{deskriptor}]<<- {identifikátor}
{řádky textu}
{identifikátor}

Přesměrování výstupu (zápis někam)

# zápis do souboru; existuje-li: zkrátit na prázdný/připojit za konec
// Varianta „>|“ dovolí přepsání existujícího souboru i v případě, že je nastavena volba „noclobber“.
[{deskriptor}]>[|] {cesta}
[{deskriptor}]>> {cesta}

# zápis nikam
[{deskriptor}]> /dev/null

# zápis na s.vstup bloku příkazů (obecně/příklad)
[{deskriptor}]>␣>({příkaz}...)
df -h >␣>(sed -u 1q; sort)

# zduplikovat/přečíslovat deskriptor pro zápis
[{cílový-deskriptor}]>&{zdrojový-deskriptor}
[{cílový-deskriptor}]>&{zdrojový-deskriptor}-

# zduplikovat pojmenovaný deskriptor pro zápis
{příkaz a parametry} [{cílový-deskriptor}]>&${identifikator-pojm-desk}

# zavřít deskriptor
{deskriptor}>&-

# přepis souboru (čtení i zápis, bez zkrácení) (obecně/příklad příkazu)
// Vyžaduje právo soubor číst i do něj zapisovat; neexistující soubor bude vytvořen, existující soubor nebude zkrácen a zápis začne bajt po bajtu přepisovat jeho obsah od začátku souboru.
{cílový-deskriptor}<>{cesta}
echo ABC 2>&1 1<> můj-soubor.txt

Některá obvyklá přesměrování

# zahodit s.výstup i s.ch. výstup (alternativy)
&> /dev/null
>/dev/null 2>/dev/null

# s.ch. výstup nasměrovat na s.výstup (alternativy)
2>&1
2> /dev/stdout

# s.výstup nasměrovat na s.ch. výstup (alternativy)
> /dev/stderr
>&2

# s.výstup a s.ch. výstup připojit za konec souboru (pokud neexistuje, vytvořit prázdný)(alternativy)
>> {cesta} 2>&1
&>> {cesta}

Pojmenované deskriptory

# otevřít (obecně/příklad)
// „Režim“ může být pro čtení „<“, pro zápis „>“, „>|“, „>>“ a pro přepis „<>“ s významem jako u odpovídajícího přesměrování.
exec {{identifikator}}{režim} {cesta/k/souboru}
exec {mujdesk}< můj-soubor.txt

# zavřít pro vstup/pro výstup/pro přepis
exec {{identifikator}}<&-
exec {{identifikator}}>&-
exec {{identifikator}}<&- {{identifikator}}>&-

# příklad použití
exec {mujd}>>můj-log.txt
date "+%F %T" >&$mujd
printf 'Skript spuštěn (deskriptor %d)\n' "$mujd" >&$mujd
exec {mujd}>&-

Ostatní aplikace přesměrování

# aplikovat přesměrování na skupinu příkazů
// Složené závorky můžete umístit i na stejný řádek jako příkazy uvnitř, ale v takovém případě je musíte oddělit mezerami a poslední příkaz musí být ukončen operátorem „;“ nebo „&“! Proto doporučuji raději oddělovat složené závorky od příkazů koncem řádku.
{
{příkazy}...
} {přesměrování}...

# aplikovat přesměrování permanentně (na všechny následující příkazy)
exec {přesměrování}...

Zaklínadla: Čtení a zápis z Bashe

Zápis (výstup)

# formátovaný výstup
printf [--] {'formátovací řetězec'} [{parametry}]... [{přesměrování}]

# zapsat text
printf %s[\\n] {"text"} [{přesměrování}]

# zapsat bajty
// AB a CD reprezentují dvoumístný hexadecimální zápis bajtů k zapsání
printf '\x{AB}[\x{CD}]...' [{přesměrování}]

# zapsat „\0“ (nulový bajt)
printf \\0 [{přesměrování}]

Čtení (vstup)

Před použitím zaklínadel z této podsekce si prosím přečtěte podsekci „Jak funguje příkaz read“!

# přečíst do proměnné záznam ukončený znakem „\n“/„\0“/zadaným ASCII znakem
IFS= read -r[u {deskriptor}] {promenna}
IFS= read -r[u {deskriptor}] -d "" {promenna}
IFS= read -r[u {deskriptor}] -d {"ASCII-znak"} {promenna}

# přečíst do proměnné N znaků/1 znak
IFS= read -r[u {deskriptor}] -N {N} {promenna}
IFS= read -r[u {deskriptor}] -N 1 {promenna}

# načíst záznam a rozložit ho do pole
IFS={"oddělovače"} read -r[u {deskriptor}] [-d {"ASCII-ukončovač-záznamu"}] -a {pole}...
IFS=":" read -r -a data < /etc/passwd && printf %s\\n "${data[4]}"

# načíst záznam a rozložit ho do proměnných (obecně/příklad)
IFS={"oddělovače"} read -r[u {deskriptor}] [-d {"ASCII-ukončovač-záznamu"}] {promenna}...
IFS=":" read -r a b c d e < /etc/passwd

# načíst všechny záznamy do nového pole (alternativy)
readarray -t [-d {"ukončovač"}] [-s {kolik-z-přeskočit}] [-n {kolik-max-načíst}] [-u {deskriptor}] {pole}
IFS={"ukončovače"} read -r[u {deskriptor}] -d "" -a {pole} || true

# načíst všechny záznamy do existujícího pole
// „index“ je index v poli, kam má začít příkaz zapisovat. Pro zápis od začátku pole uveďte index „0“.
readarray -t -O {index} [-d {"ukončovač"}] [-s {kolik-z-přeskočit}] [-n {kolik-max-načíst}] [-u {deskriptor}] {pole}

# načíst celý zbytek vstupu do proměnné
IFS= read -rd "" {promenna} <␣<(tr -d \\0)

Interakce s uživatelem (jen při čtení z terminálu)

# načíst heslo
// Parametr „-s“ potlačí výstup psaných znaků na terminál. Při načítání hesel si dejte velký pozor na to, aby použitá proměnná nebyla exportovaná, jinak se totiž heslo objeví v prostředí nově spuštěných procesů, odkud může být šikovnými hackery odposlechnuto.
[if] IFS= read -rs [-p {"Text výzvy:"}] {promenna} && echo [then {...} fi]

# nabídnout uživateli řádku k úpravě (obecně/příklad)
// Pozor! V případě selhání příkazu (např. vypršení časového limitu) zůstane v cílové proměnné prázdný řetězec, ať už uživatel mezitím nějaké úpravy provedl nebo ne.
IFS= [TMOUT=[{časový-limit-sek}]] read -r [-p {"Text výzvy"}] -ei {"Výchozí text řádky"} {promenna}

Je deskriptor připojený na terminál?

# je s.vstup připojený na terminál?
test -t 0

# je s.výstup/s.ch. výstup připojený na terminál?
test -t 1
test -t 2

# je deskriptor N připojený na terminál?
test -t {N}

Nastavení Bashe související s deskriptory a rourami

# návratový kód vícenásobné roury se vezme: z prvního příkazu, který selhal/vždy z posledního příkazu roury (výchozí nastavení)
set -o pipefail
set +o pipefail

# zkrácení existujícího souboru obyčejným přesměrováním výstupu (zakázat/povolit)
set -C
set +C

Další poznámky

Použití přesměrování deskriptorů

Přesměrování deskriptorů se obvykle uvádějí na konec příkazu, za poslední parametr; např. takto:

*# *
printf %s\\n "Chyba!" >&2

Ale Bash je dovoluje uvést kamkoliv, dokonce i před příkaz nebo mezi parametry, např.:

*# *
>&2 LC_ALL=C sort <<< $'P\nC\nA' -

Přesměrování je možno aplikovat i na celý blok příkazů, na cykly (např. „while“) apod.

Zvláštní pozornost je potřeba věnovat použití „bloku řádků“ jako vstupu; definice ukončovače se totiž uvádí za značku <<, ale číst vstup začne Bash až od následující řádky, což umožňuje tento druh vstupu skombinovat např. s rourou:

*# *
LC_ALL=C sort << "KONEC" | tr A _
ZAHRADA
Abeceda
KONEC

Jak funguje příkaz read

#
IFS={
"oddělovače"
} read -r[u {deskriptor}] [-d {"ukončovač"}] {promenna}...
IFS={"oddělovače"} read -r[u {deskriptor}] [-d {"ukončovač"}] -a {pole}
read -r[u {deskriptor}] -N {počet-znaků} {promenna}

  • Před zahájením čtení „read“ do všech uvedených proměnných přiřadí prázdný řetězec. (Jde-li o čtení do pole, pole se vyprázdní.)
  • Čte ze zadaného deskriptoru (výchozí je s.vstup, tedy 0) znak po znaku a přidává je na konec první zadané proměnné (resp. prvního prvku pole).
  • Když na vstupu narazí na oddělovač (kterýkoliv znak z hodnoty proměnné IFS), přeskočí na další proměnnou/další prvek pole, s výjimkou případu, kdy se oddělovač nachází bezprostředně před ukončovačem záznamu; v takovém případu je oddělovač ignorován. Do poslední zadané proměnné se pak načte celý zbytek záznamu (tam už je hodnota proměnné IFS ignorována).
  • Když „read“ na vstupu narazí na ukončovač záznamu (výchozí je „\n“), úspěšně tím skončí čtení (tzn. návratový kód 0).
  • Když „read“ narazí na konec vstupu (nebo hardwarovou chybu), skončí čtení s návratovým kódem 1. Již načtené znaky budou v proměnných ponechány.

Postřehy:

  • Ne-ASCII znaky lze použít v proměnné IFS (tzn. jako oddělovače záznamů), ale ne v parametru „-d“ (tzn. nemohou sloužit jako ukončovače záznamů).
  • Nulový bajt lze použít jako ukončovač záznamu (tvar parametru je pak „-d ""“), ale ne jako oddělovač záznamů (nelze ho uložit do proměnné IFS).
  • V případě, že „read“ narazí na konec vstupu, skončí s návratovým kódem 1, ale již přečtené znaky ponechá v příslušných proměnných.
  • Nastavíte-li časový limit, v případě jeho vypršení skončí „read“ s kódem 142 a načítané proměnné budou vyprázdněny.

Jak funguje příkaz printf

#
printf [-v {promenna}] [--] {
'formátovací řetězec'
} [{parametr}]...

Ve formátovacím řetězci jsou interpretovány formátovací značky (uvozené znakem „%“) a lomítkové sekvence (začínající zpětným lomítkem „\“), oba případy lze odzvláštnit zdvojením (tzn. „%%“ se interpretuje jako obyčejný znak % a „\\“ jako obyčejné zpětné lomítko). Netriviální formátovací řetězce doporučuji uzavřít do apostrofů, aby nedocházelo ke konfliktům se zvláštním významem některých znaků na příkazovém řádku.

Algoritmus příkazu printf:

    1. Vzít formátovací řetězec a nahradit v něm všechny lomítkové sekvence odpovídajícími znaky či řetězci.
    1. Projít všechny formátovací značky ve formátovacím řetězci zleva doprava; pro každou načíst jeden parametr z parametrů za formátovacím řetězcem (pokud chybí, použít místo něj prázdný řetězec), zformátovat podle značky a dosadit místo ní.
    1. Výsledný řetězec vypsat na standardní výstup.
    1. Pokud zbyl alespoň jeden parametr, vzít řetězec, který byl výstupem kroku 1 a jít na krok 2 se zbylými parametry.

V případě použití parametru „-v“ se řetězce místo vypsání ukládají do zadané proměnné a nesmí obsahovat nulový bajt (jinak může být chování nepředvídatelné).

Instalace na Ubuntu

Bash a všechny příkazy použité v této kapitole jsou základními součástmi Ubuntu přítomnými i v minimální instalaci.

!ÚzkýRežim: zap

Tipy a zkušenosti

  • Při práci s deskriptory je třeba mít na paměti, že „pozice“ čtení či zápisu není vlastností samotného deskriptoru. Otevřením souboru přesměrováním deskriptoru vznikne nová „čtecí pozice“ i v případě, že stejný soubor je již otevřen přes jiný deskriptor; pokud ale stávající deskriptor zduplikujete nebo ho zdědí nový proces, nová pozice nevznikne a čtení z obou deskriptorů bude posouvat tutéž čtecí pozici (existující pravděpodobně někde v jádře).

Časté chyby s rourou

Pozor na implicitní vznik podprostředí v některých situacích! Bash automaticky obklopí podprostředím každý jednoduchý příkaz roury a také i jednoduchý příkaz spouštěný na pozadí nebo v operátoru „$()“. To znamená, že např. tento blok kódu vypíše „19“, protože přiřazení z konstrukce „:=“ zůstalo izolované v podprostředí:

*# *
unset a
true "${a:=12}" &
wait $!
printf %s\\n "${a:=19}"

Nejčastěji se tato chyba vyskytuje ve formě pokusu o použití příkazu „read“ s rourou:

*# *
unset a
printf 99\\n | IFS= read -r a
printf %s\\n "$a"

V uvedeném příkladu zůstane hodnota „a“ nedefinovaná, protože Bash uzavře příkaz „read“ do samostatného podprostředí.

Problémy s nulovým bajtem

Bash obecně neumí pracovat s nulovým bajtem „\0“; umí ho však vygenerovat (příkazem printf) a přečíst jako ukončovač záznamu (příkazy read a readarray) a rovněž může být předáván rourou z příkazu do příkazu (což probíhá mimo Bash, ten rouru jen vytváří). Celkově je třeba při jakémkoliv pokusu o prácí s nulovým bajtem nástroji Bashe dát velký pozor, zejména ho nelze uložit do proměnných ani použít uvnitř textu parametru jakéhokoliv příkazu.

Další zdroje informací

Zákulisí kapitoly

V této verzi kapitoly chybí:

!KompaktníSeznam:

  • koprocesy
  • více „dalších zdrojů informací“
  • podrobnější výklad o „printf“

Tato kapitola záměrně nepokrývá:

!KompaktníSeznam:

  • nic

!ÚzkýRežim: vyp