FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

GNU Bash: Vánoční tipy

vydáno: 24. 12. 2018 13:37, aktualizováno: 2. 5. 2020 20:14

Bash je nejpoužívanějším shellem, přes něj nejčastěji ovládáme systém z příkazové řádky a píšeme v něm skripty. Nahromadilo se mi tu pár poznámek týkajících se Bashe, tak tady jsou. Doufám, že to přispěje k pohodě vašich Vánoc.

Vánoční strom 2018

Zkratky pro poslední parametr a poslední příkaz

Často provádíme více operací s jedním souborem, např. mu nastavujeme práva a upravujeme ho. Abychom nemuseli opakovaně psát jeho název, hodí se znát zkratku Alt+. – ta nám vloží poslední parametr předchozího příkazu, což bývá typicky ten název souboru. Podobnou funkci má !$:

$ echo ahoj
ahoj
$ echo !$
echo ahoj
ahoj

Bash nám nejprve vypíše doplněný příkaz (v tomto případě echo ahoj) a následně příkaz spustí (zde výstup: ahoj). Oproti Alt+. ale nemůžeme parametr upravit, což se někdy hodí (např. chceme změnit příponu). Navíc opakovaným stiskem Alt+. dostaneme i starší parametry z historie.

Pomocí !! můžeme dosadit celý předešlý příkaz se všemi jeho parametry:

apt install gimp # tohle selže, protože nejsme root
sudo !!          # zeptá se na heslo a spustí pod rootem apt install gimp

Někdy se to hodí, i když častěji používám šipku nahoru (a Ctr+R) pro hledání v historii a Alt+. pro doplnění parametru předchozího příkazu.

Domovský, aktuální a předchozí adresář

Asi všichni víme, že ~ znamená náš domovský adresář. Kromě toho ale existuje ještě ~+, ~-~uživatel.

echo ~+    # dosadí aktuální adresář
echo $PWD  # totéž ale s proměnnou, kterou lze používat v uvozovkách
echo ~-    # dosadí předchozí adresář
echo ~root # dosadí domovský adresář jiného uživatele

Tyto zkratky můžeme použít např. při přesunech souborů nebo kopírování

Hledání souboru v podadresářích pomocí **

Kromě známého otazníku (jeden libovolný znak) a hvězdičky (libovolný počet libovolných znaků) podporuje Bash i dvě hvězdičky. Tato možnost ale bývá většinou vypnutá a lze ji povolit pomocí shopt:

ll */soubor.txt    # vypíše soubory s tímto názvem v přímých podadresářích
ll **/soubor.txt   # vypíše totéž
shopt -s globstar
ll **/soubor.txt   # tentokrát vypíše i soubory libovolně hluboko

Tímto způsobem lze nahradit jednodušší vyhledávání, která bychom jinak dělali pomocí příkazů findxargs.

mkdir + cd v jednom příkazu

Velmi častou operací je vytvoření adresáře a následné přepnutí se do něj. Proč ale zadávat dva příkazy, když stačí jeden? Můžeme si napsat funkci, která vytvoří adresář a hned do něj vstoupí:

mkcdir() { mkdir "$1" && cd "$1"; }

Na rozdíl od mkdir ale podporuje jen jeden adresář a nelze jich vytvořit víc. Přidáním for cyklu by šlo funkci rozšířit tak, aby vytvořila všechny a vstoupila např. do toho posledního.

Víceřádkové hodnoty

V Bashi můžeme zadávat víceřádkové hodnoty jednoduše tak, že začneme uvozovky nebo apostrofy a píšeme text včetně zalomení řádků. Tímto způsobem můžeme plnit proměnné nebo zadávat parametry příkazů:

$ echo "a
> b";
a
b

Středník je tam schválně, aby bylo lépe vidět, kde končí příkaz a kde začíná jeho výstup. A mimochodem, to > je prompt, který se nastavuje v proměnné $PS2.

Pro zápis znaku konce řádku se často používá \n. Pokud ho ale napíšeme jen tak, vypíše se doslovně:

$ echo 'a\nb';
a\nb

Zalomení řádku můžeme dosáhnout buď přidáním parametru -e:

$ echo -e 'a\nb';
a
b

nebo pomocí konstrukce $'…':

$ echo $'a\nb';
a
b

Výsledek je zdánlivě stejný, ale tyto zápisy se liší v tom, že zatímco v prvním případě se \n interpretuje příkazem echo, ve druhém se o jeho interpretaci postará už Bash – tzn. můžeme tento zápis použít i u jiných příkazů než echo – příkaz (spouštěný proces) totiž pak dostává jako parametr textový řetězec obsahující přímo znak konce řádku (zatímco v prvním případě dostane textový řetězec obsahující \n a je na něm, jak si tuto dvojici znaků interpretuje).

Dosazení procesu místo cesty k souboru

Většina slušných nástrojů umí pracovat jako filtr tzn. číst ze standardního vstupu a zapisovat na standardní výstup, a lze je tak řetězit pomocí | rour. Některé programy to ale neumí a pracují pouze se soubory. Někdy to ani jinak nejde, když má mít program víc vstupů/výstupů. I v takovém případě existuje šance vyhnout se dočasným souborům a poslat výstup jednoho programu na vstup druhého. Bash podporuje tzv. process substitution. Díky tomu můžeme na místo parametru, kde měla být cesta k souboru, dosadit program nebo i několik dalších programů spojených rourami.

Např. příkaz paste slouží k vypsání dvou souborů ve sloupcích vedle sebe. A díky process substitution to nemusí být jen soubory:

paste <(echo -e "A\nB\nC") <(echo -e "a\nb\nc")
# nám vypíše:
# A       a
# B       b
# C       c

Téhož výsledku dosáhneme pomocí:

echo -e "A\nB\nC" | paste - <(echo -e "a\nb\nc")

Konvence, kterou mnoho programů dodržuje, je, že pokud jako název souboru uvedeme - bere se to jako standardní vstup nebo výstup. Pokud bychom chtěli pracovat se souborem, který se jmenuje -, zadáme ho jako ./-

Že process substitution není žádná magie, si ověříme takto:

echo <(echo "a") <(echo "b")
# vypíše něco jako:
# /dev/fd/63 /dev/fd/62

Vidíme, že příkaz echo dostal jako dva parametry cesty k souborovým popisovačům, přes které mu Bash napojil vstupy či výstupy jednotlivých příkazů uvedených v závorkách.

Příkladem praktického využití je porovnání dvou adresářů:

diff <(ls "adresář_1") <(ls "adresář_2")

Tím zjistíme, zda se v adresářích nacházejí soubory se stejnými názvy, případně jaké soubory kde přebývají. Neřešíme zde velikosti nebo obsah souborů. Složitější porovnání můžeme dělat pomocí find, sort, xargs atd.

Výstup (zápis do virtuálního souboru) funguje obdobně. Např. příkaz strace vypisuje systémová volání buď na standardní chybový výstup nebo do souboru a umožňuje různým způsobem filtrovat a formátovat svůj výstup. Co když ale potřebujeme filtrovat jinak nebo se jedná o program, který žádné filtrování neumožňuje a chce zapisovat jen do nějakého souboru? Díky Bashi můžeme programu podstrčit virtuální soubor, který ve skutečnosti vede na vstup nějakého procesu a dále se nějak zpracovává, aniž by se data ukládala na disk:

strace -o >(grep ahoj >&2) echo ahoj
# vypíše:
# execve("/bin/echo", ["echo", "ahoj"], 0x7fff71d98d98 /* 60 vars */) = 0
# ahoj
# write(1, "ahoj\n", 5)                   = 5

První a třetí řádek procházejí od příkazu strace a do našeho terminálu se dostaly přes STDERR. Druhý řádek pochází z příkazu echo a do terminálu přišel normálně přes STDOUT.

Rozdíl oproti rouře

Pro řetězení programů používáme obvykle rouru, což je mj. čitelnější protože čteme zleva doprava:

echo -e "a\nb\nc" | while read x; do echo ">$x<"; XXX=$x; done
# vypíše:
# >a<
# >b<
# >c<

Téhož výstupu dosáhneme i tímto zápisem:

while read x; do echo ">$x<"; XXX=$x; done < <(echo -e "a\nb\nc")

Ovšem rozdíl je v tom, že while cyklus se teď spustil v rámci našeho původního shellu, a po dokončení tohoto cyklu tak máme k dispozici proměnnou XXX, ze které si můžeme přečíst hodnotu c.

Složitější příklad

Tímto způsobem lze stavět i poměrně složitá potrubí (pipeline, česky také nazývané kolony), ve kterých máme několik vstupů, data tečou nejdřív část cesty samostatně a různě se transformují, pak se v jednom bodě spojí a následně se mohou zase rozdvojit, projít dvěma různými transformacemi, aby se nakonec zase cesty spojily v našem terminálu. Hypotetický příklad:

echo a b c | tr ' ' '\n' | paste - <(echo -e "x\ny\nz") \
    | tee >(tr '[:lower:]' '[:upper:]' >&2) | sort -r

nám vypíše:

  A       X # ─╮
  B       Y # ─┼─ data zpracovaná první výstupní transformací
  C       Z # ─╯
  c       z # ─╮
  b       y # ─┼─ data zpracovaná druhou výstupní transformací
  a       x # ─╯
# │       │ 
# │       ╰─ data z druhého vstupu
# ╰─ data z prvního vstupu

Graficky znázorněno:

echo ─── tr ───╮           ╭─── tr ───── STDERR ───╮
               ├── paste ──┤                       ├── terminál
echo ──────────╯           ╰─── sort ─── STDOUT ───╯

Nicméně poskládat data z více vstupů dohromady není tak jednoduché, jak to tady vypadá. A každopádně, pokud se do něčeho podobného pustíte, doporučuji kód patřičně okomentovat a členit ho na menší funkce nebo skripty.

Proměnná proměnná

Bash dokáže být i hodně dynamický, což se mj. projevuje tím, že název proměnné v kódu může být proměnná. Takže máme proměnnou proměnnou. Dá se to použít jako ukazatele nebo reference v jiných programovacích jazycích, ale na rozdíl od nich neukazují proměnné proměnné na nějaké místo v paměti nebo objekt, ale jedná se o obyčejný text, který se až nakonec interpretuje jako název proměnné. To znamená, že s proměnnou proměnnou můžeme jako s textem pracovat (prohledávat, nahrazovat, porovnávat atd.).

zzz=PWD
echo ${!zzz}   # vypíše aktuální adresář
echo $PWD      # vypíše totéž
zzz=HOME
echo ${!zzz}   # vypíše domovský adresář
echo $HOME     # vypíše totéž

Pokud tuhle vlastnost použijeme neuváženě, je to zaručený způsob jak znepřehlednit naše skripty, tak abychom se v nich nevyznali ani my sami. Delší skript prolezlý proměnnými proměnnými bude často lepší smazat a napsat znovu než se ho pokoušet upravit.

Smysluplné použití existuje (ukážeme si níže), ale v takovém případě je vhodné proměnné proměnné uzavřít do nějaké malé funkce nebo skriptu a schovat tuto magii za nějaké srozumitelné rozhraní.

Nastavení proměnné z funkce

Výhodou funkcí je to, že se kvůli nim nespouští nový proces, a kód se vykoná v rámci stávajícího shellu. Což má mj. za následek to, že z funkce můžeme měnit hodnoty proměnných v shellu, ze kterého jsme funkci zavolali.

generuj_uuid() { export uuid=`uuidgen`; }
generuj_uuid   # zavoláme funkci definovanou výše
echo $uuid     # vypíšeme si proměnnou nastavenou při volání funkce

Když to spojíme s proměnnými proměnnými, získáme:

generuj_uuid() { export $1=`uuidgen`; }
generuj_uuid xxx
echo $xxx

Pořád je to ale poněkud neužitečné, protože místo abychom funkci říkali, jakou proměnnou nám má naplnit, stačí, když jednoduše návratovou hodnotu funkce (resp. výstup příkazu) přiřadíme do dané proměnné:

xxx=`uuidgen`
echo $xxx

Smysl to začne mít ve chvíli, kdy potřebujeme naplnit více proměnných najednou, což si ukážeme hned v následujícím příkladu.

Čtení hodnot oddělených \0 do více proměnných

Nedávno jsem řešil úkol, jak číst proud hodnot oddělených nulovým bajtem, kde hodnoty jsou členěné do záznamů tak, že známe počet atributů záznamu (sloupců) a záznamy následují hned za sebou (není mezi nimi jiný oddělovač než mezi samotnými hodnotami). Vhodným oddělovačem je nulový bajt \0, protože ten se v textových datech nevskytuje (kdybychom potřebovali přenášet binární data, která nulové bajty obsahovat mohou, už bychom si s takto jednoduchým formátem nevystačili a museli bychom hodnoty buď escapovat nebo na začátku vždy uvádět jejich délky). Data vypadají např. takto:

printf 'a\0aa\0aaa\0b\0bb\0bbb\0' | xargs -0 -n1 echo

Záznamy mají vždy tři atributy, první záznam je a,aa,aaa a druhý b,bb,bbb. Tohle je mimochodem asi nejjednodušší způsob, jak zapsat dvourozměrnou strukturu (tabulku) ve formě jednorozměrné struktury (pole). Akorát si musíme někde bokem předat informaci, kolik atributů záznam má (např. si dohodnout, že před hodnotami bude uveden počet atributů oddělený zase tím samým oddělovačem). Podobným způsobem lze zapisovat mapy – v poli budou liché prvky klíče a sudé hodnoty.

Ke čtení ze standardního vstupu do proměnné slouží příkaz read. Používá se např. když chceme, aby uživatel zadal nějakou hodnotu (pak lze nastavit i prompt a předvyplněný text), nebo když zpracováváme data ze vstupního proudu. Tento příkaz umí naplnit i více proměnných. Ovšem předpokládá jiný oddělovač hodnot a jiný oddělovač záznamů. Nepodařilo se mi ho (čistě pomocí parametrů) přimět, aby četl formát popsaný výše.

Úlohu lze ale řešit tím, že si definujeme pomocnou funkci a využijeme v ní znalosti z předchozích příkladů: proměnné proměnné a plnění proměnných z funkce. Funkci jsem pojmenoval read_nullbyte() a vypadá takto:

read_nullbyte() { local IFS=; for v in "$@"; do export "$v"; read -r -d '' "$v"; done }

A používá se následujícím způsobem:

printf 'a\0aa\0aaa\0b\0bb\0bbb\0' \
    | while read_nullbyte p1 p2 p3; do echo "záznam: p1=$p1 p2=$p2 p3=$p3"; done

# vypíše nám:
# záznam: p1=a p2=aa p3=aaa
# záznam: p1=b p2=bb p3=bbb

Je tedy podobná původnímu příkazu read, kde jsme rovněž zadávali jako parametry názvy proměnných, které chceme naplnit, akorát zde nemusíme definvoat oddělovač, protože ten je vždy \0.

V diskusi na AbcLinuxu přišel Andrej s alternativním řešením pomocí readarray:

printf 'a\0b\0c\0d' | for ((;;)); do
    readarray -d $'\0' -n 2 -t array
    ((! ${#array[@]})) && break
    echo "${array[@]@A}"
done

Výhodou je, že nepotřebujeme definovat pomocnou funkci, ale na druhou stranu musíme pracovat s pozicemi atributů v poli a nemáme je naplněné v pojmenovaných proměnných.

Typy příkazů

V shellu zadáváme příkazy a pracujeme s nimi jednotným způsobem – zadáme název příkazu a pak volitelně jeho parametry oddělené mezerami (pokud parametr obsahuje mezery, dáme ho do uvozovek nebo apostrofů). Funguje u nich napovídání tabulátorem, historie, lze je používat v rourách atd. Ovšem tyto příkazy – přestože se navenek chovají stejně – mohou být různých typů:

  • programy – jsou uložené v souboru na disku, mohou to být binárky nebo skripty, případně symbolické odkazy na ně; hledají se v tzv. cestě – proměnná $PATH
  • aliasy – zkratky pro jiné příkazy a jejich parametry; jsou definované typicky v ~/.bashrc, nějakém z něj načítaném souboru nebo ad-hoc v rámci aktuálního shellu; příklad: alias ll='ls -alF'
  • funkce – definují se na stejných místech jako aliasy; ukázky jsou výše, např. read_nullbyte()
  • vestavěné příkazy shellu – tzv. built-in – jsou buď zakompilované v Bashi nebo se načítají z dynamických knihoven; můžeme si psát i vlastní (viz níže)
  • klíčová slova shellu – v některých případech se používají podobně jako příkazy (např. time), ale většinou se chovají dost odlišně – je to součást jazyka a Bash je parsuje/interpretuje dříve

Někdy si ani nemusíme všimnout, že nepracujeme s programem, ale s vestavěným příkazem. Např. echo je jak binárka v souboru /bin/echo, tak vestavěný příkaz. Ostatně řada často používaných příkazů byla postupně přepsána z původních externích programů na vestavěné příkazy Bashe. Jejich spouštění je pak výrazně efektivnější, protože se nevytváří nový proces, ale pouze se zavolá nějaká céčkovská funkce.

Typ příkazu zjistíme takto:

builtin type -type ll       # alias
builtin type -type uname    # file
builtin type -type time     # keyword
builtin type -type ls       # alias
builtin type -type sleep    # file
builtin type -type echo     # builtin
builtin type -type kill     # builtin
builtin type -type for      # keyword
builtin type -type '[['     # keyword
builtin type -type '['      # builtin

případně:

type uname                  # uname je /bin/uname
type time                   # time je klíčové slovo shellu

Pokud chceme spustit binárku místo vestavěného příkazu, uvedeme celou její absolutní nebo relativní cestu:

echo --version
/bin/echo --version

Výstup je v tomto případě odlišný, i když se jinak obě implementace chovají celkem podobně.

Zatímco dokumentaci k programům hledáme obvykle v manuálových stránkách (např. man echo), nápovědu k vestavěným příkazům získáme pomocí příkazu help např. help echo.

Všechny příkazy určitého typu si můžeme vypsat pomocí compgen (používá se pro napovídání tabulátorem, Bash completion):

compgen -c                  # binárky
compgen -a                  # aliasy
compgen -k                  # klíčová slova
compgen -A function         # funkce
compgen -A function -abck   # všechno dohromady

Built-in: vestavěné příkazy shellu

Aby vestavěné příkazy nebyly taková magie, zkusíme si nějaký zkompilovat a načíst. Odrazit se můžeme od příkladů, které jsou součástí zdrojových kódů Bashe.

tar xvzf bash-4.4.18.tar.gz
cd bash-4.4.18/
./configure
make -j8
cd examples/loadables/
make -j8 all others
find -type f -executable | xargs file

Poslední příkaz nám vypíše seznam příkladů, které se nám zkompilovaly, a z výpisu vidíme, že se jedná o sdílené knihovny (ELF ... shared object ... dynamically linked). Jeden příklad si načteme, prozkoumáme, spustíme a zase dáme pryč:

type hello                 # -bash: type: hello: nenalezeno
enable -f ./hello hello    # hello builtin loaded
type hello                 # hello je součást shellu
hello                      # hello world
enable -d hello            # hello builtin unloaded
type hello                 # -bash: type: hello: nenalezeno

Pro opakované ladění a různé experimentování je lepší pouštět příkaz v novém shellu:

bash -c "enable -f ./hello hello; help hello; hello"

Na příkladu hello.c vidíme i to, že dokumentace k příkazu je jeho součástí a načte se automaticky s ním. Pak je dostupná přes help hello a dokonce je i lokalizovaná. Viz ../../po/cs.po (pokud překlad neexistuje, zobrazí se původní text uvedený ve zdrojáku).

Pro programátory v jazyce C bude triviální napsat si vlastní vestavěný příkaz, a rozšířit si tak možnosti Bashe. Výhodou oproti programům psaným v C je vyšší efektivita (nemusí se spouštět nový proces) a to, že jsme uvnitř dané instance Bashe a můžeme s ní pracovat lépe, než kdyby to byl náš rodičovský proces. Nicméně jak se říká:

There is more Unix-nature in one line of shell script than there is in ten thousand lines of C.

Nevýhodou je také to, že když uděláme chybu v programu, tak nám sletí celý Bash a ne jen jeden příkaz (z čehož by se šlo ve skriptu zotavit a nějak na to zareagovat, zatímco v případě vestavěné funkce nám skript okamžitě skončí).

Většinou k rozšiřování Bashe bohatě stačí napsat si vlastní funkci nebo vytvořit alias. A ani spouštění externích binárek nebo skriptů není problém, pokud je nevoláme v cyklu pořád dokola. Zajímavá by možná byla implementace ovládání GPIO, která by takto byla efektivnější než zápisy do virtuálních souborů nebo spouštění externích binárek. Případně nějaké hackování Bashe, kde bychom mohli využít toho, že náš céčkový kód běží jako součást daného shellu a může s ním komunikovat zevnitř pomocí céčkových funkcí.

Formátování a zvýrazňování syntaxe

Když stáhneme nějaká strukturovaná data přes wget nebo curl, často dnes bývají ve formátech XML nebo JSON, ale protože jsou určena pro strojové zpracování, bývají dost nečitelná. To můžeme snadno napravit vhodným odsazením a obarvením syntaxe. K tomu si definujeme pomocné funkce nebo skripty:

formátuj-xml()  { xmllint --format --encode utf8 - | pygmentize -l xml;  }
formátuj-json() { python3 -mjson.tool              | pygmentize -l json; }

Příkazy pak používáme v rouře jako filtr. Delší texty je lepší číst pomocí less:

curl https://blog.frantovo.cz/agregace/c/ | formátuj-xml | less -RSi

Escapování XML

Přestože Bash (nebo obecně shell) není zrovna prostředí, ve kterém bychom chtěli konstruovat XML, můžeme se někdy do takové situace dostat. Pokud jde jen o generování (nikoli čtení), je to jednoduchý úkol, který se dá snadno zvládnout. Akorát je třeba správně ošetřit všechny vkládané hodnoty, aby nám nenarušily strukturu XML dokumentu. Toho dosáhneme tímto příkazem:

sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' \
    -e 's/>/\&gt;/g'  -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"

Příkaz funguje jako filtr a je vhodné si ho uložit do nějakého skriptu nebo funkce pro opakované použití. Případně tento nástroj můžeme udělat obojetný, aby uměl pracovat jak se standardním vstupem, tak s daty zadanými ve formě argumentů na příkazové řádce:

#!/bin/bash

if [ $# = 0 ]; then
    sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' \
        -e 's/>/\&gt;/g'  -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
else
    echo -n "${@}" | $0;
fi

Pak nám budou fungovat obě varianty:

XXX='nějaká <ošklivá> hodnot& obsahující speciální <<<znaky>>>'
echo "<moje-xml>$(escapuj-xml $XXX)</moje-xml>"              | xmllint -
echo $XXX | escapuj-xml | echo "<moje-xml>$(cat)</moje-xml>" | xmllint -

Validitu XML dokumentu si zkontrolujeme nástrojem xmllint. Příkaz ošetřuje i apostrofy a uvozovky, takže jde použít i pro hodnoty atributů a to bez ohledu na to, zda je atribut v uvozovkách nebo apostrofech.

Vícevláknové skripty resp. více procesů

Běžný skript se provádí jako posloupnost příkazů v jednom vlákně. V Bashi ale můžeme psát i vícevláknové programy pro paralelní zpracování. Resp. nejsou to vlákna, ale procesy, kterých můžeme spustit víc.

Příkaz/proces spustíme na pozadí jednoduše tak, že na jeho konec místo ; napíšeme &. Takto můžeme spustit libovolné množství paralelně běžících procesů a pokud nás nezajímá, jak a kdy doběhnou, máme hotovo. Pokud nás to zajímá, tak si zjistíme PID procesu spuštěného na pozadí a později si počkáme na jeho dokončení. Příklad:

#!/bin/bash

(sleep 1; echo "ahoj" | while read x; do echo "přijato: $x"; exit 123; done)&

pid=$!

# Tady budeme něco dělat a druhý proces zatím běží na pozadí…

# sleep 2;
echo "1: čekám na PID $pid";
wait $pid;
echo "2: proces $pid doběhl s výsledkem $?";

Proces běžící na pozadí nemusí být jen externí program, může to být i část našeho skriptu (viz obsah závorky na prvním řádku). Nemůžeme z něj nastavovat proměnné, ale můžeme s rodičovským procesem komunikovat pomocí návratového kódu – v příkladu: 123 – ten se k nám totiž dostane jako návratový kód příkazu wait a přečteme si ho z proměnné $?. Tím si můžeme zpátky předat informaci, zda proces doběhl v pořádku (0), nebo zda a k jakému výjimečnému stavu došlo (různé nenulové hodnoty). Pokud bychom potřebovali předat více dat nebo nějaká strukturovaná data, tak bychom je museli uložit do souboru nebo použít nějakou formu IPC (meziprocesové komunikace).

Operační systém poskytuje řadu možností, jak IPC řešit – System V Message Queues (MQ), Semafory, Sdílená paměť, POSIX MQ, POSIX Semafory, POSIX Sdílená paměť a různé formy soketů (jako TCP, UDP, SCTP nebo unixové doménové sokety). Dobrý přehled poskytuje kniha The Linux Programming Interface (Michael Kerrisk). Kromě toho existuje spousta nadstaveb/abstrakcí a příslušných knihoven postavených nad těmito základními formami IPC.

Zde už se dostáváme poněkud nad rámec běžného skriptování v shellu – pokud řešená úloha vyžaduje paralelizaci a koordinaci více vláken/procesů, pravděpodobně sáhneme po nějakém programovacím jazyku. Nicméně tyto věci lze řešit i v Bashi. Můžeme např. napsat asynchronní skript založený na posílání zpráv mezi více procesy. K jejich koordinaci se dají použít unixové doménové sokety (UDS) konkrétně jejich datagramová varianta. Pro vytváření těchto soketů a posílání zpráv použijeme příkaz socat:

# V jednom procesu budeme čekat na zprávu:
zprava=$(socat -u unix-recvfrom:./můj-soket -);
echo "Přijata zpráva: $zprava";

# A z druhého ji odešleme:
echo "Ahoj, jak se máš?" | socat -u - unix-send:./můj-soket

Program socat při použití recvfrom skončí po přijetí prvního datagramu. To se hodí např. v případech, kdy nám stačí notifikace o tom, že něco doběhlo (a jak), a pak pokračujeme dál. Pokud ale chceme zpracovávat více událostí/zpráv stejného typu, použijeme volbu recv:

# Přijímáme a průběžně zpracováváme události:
socat -u unix-recv:./můj-soket - | while read_nullbyte zprava; do
    echo $(date --iso-8601=s) "Přijata zpráva: $zprava";
done

# A z jiného vlákna posíláme zprávy:
printf "ahoj\0" | socat -u - unix-send:./můj-soket

Nulový bajt \0 použijeme k vyznačení hranic mezi zprávami. Ty jsou u datagramů sice dané, ale při následném zpracování (výstup příkazu socat předáváme rourou dál) by se nám mohly ztratit a nevěděli bychom, kde jedna zpráva končí a kde začíná druhá. Zpráva se může skládat i z více částí, které si načteme do více proměnných – viz kapitola o funkci read_nullbyte().

Místo UDS bychom také mohli použít některý druh síťových soketů (TCP, UDP, SCTP…) a distribuovat běh našeho skriptu napříč několika počítači. Ovšem tam už je většinou potřeba řešit bezpečnost a šifrování… ale hlavně při takto složitém návrhu už budeme pravděpodobně narážet na hranice možností Bashe při ošetřování různých výjimečných stavů a ten kód nemusí být už moc hezký a přehledný.

Síťová komunikace přímo z Bashe

Ano, můžeme použít wget, curl nebo socat, ale věděli jste, že lze navázat síťové spojení přímo z Bashe bez použití dalších programů? Podle hesla „vše je soubor“ nám Bash zpřístupňuje tato rozhraní ve formě virtuálních souborů v /dev/tcp//dev/udp/. Následujícím příkazem např. pošleme datagram protokolem UDP na localhost a port 9999:

echo "ahoj, jak se máš?" > /dev/udp/localhost/9999

Aby to k něčemu bylo, je potřeba, aby na druhé straně někdo naslouchal. Např. socat:

socat UDP-LISTEN:9999,fork STDOUT

Soubory, do kterých zapisujeme, ve skutečnosti neexistují a v jiném shellu nebo programu to fungovat nebude:

bash -c 'echo ahoj > /dev/udp/localhost/9999' # odešle datagram
sh   -c 'echo ahoj > /dev/udp/localhost/9999' # vypíše chybu:
# sh: 1: cannot create /dev/udp/localhost/9999: Directory nonexistent

Komunikace přes TCP je trochu složitější, protože je stavová:

# navážeme TCP spojení
# a napojíme ho na souborový popisovač (vybereme si nějaké číslo vyšší než 2):
exec 3<>/dev/tcp/frantovo.cz/80

# odešleme požadavek
# (v tomto případě HTTP, ale bude to fungovat pro libovolný protokol):
echo -e "GET / HTTP/1.0\nHost: frantovo.cz\n" >&3

# vyzvedneme si odpověď:
cat <&3

Pokud jsme masochisti, můžeme jen pomocí Bashe implementovat celkem jakýkoli (i binární) protokol nad TCP nebo UDP.

Barevný výstup: lolcat

Pokud by nám výstup nějakého příkazu připadal moc nudný, můžeme si ho obarvit pomocí nástroje lolcat. Funguje podobně jako klasický cat, ale přidává barvy a umí dokonce i animace :-)

ls -l /bin/ | head | lolcat -a

Závěr

Dnes to byl takový trochu náhodný výběr, ale snad tam najdete aspoň něco zajímavého. A budu rád, když se v komentářích podělíte o svoje tipy pro Bash (případně jiný shell).

Odkazy a zdroje:

Témata: [GNU/Linux] [XML] [taháky] [Bash]

Komentáře čtenářů


Franta, 22. 4. 2019 16:55, Vícevláknové skripty resp. více procesů [odpovědět]

Doplnil jsem kapitolu „Vícevláknové skripty resp. více procesů“.


jiwopene, 28. 4. 2019 19:14, Zjednodušení zápisu sedu [odpovědět]

Místo sed -e příkaz1 -e příkaz2 … stačí sed -e příkaz1;příkaz2;….

Nevím jak moc přenositelné to je, ale GNU sed to umí.


Franta, 29. 4. 2019 20:36, Víceřádkové hodnoty [odpovědět]

Doplnil jsem kapitolu „Víceřádkové hodnoty“.

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře