Moje odkazy
Obsah článku:
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.
Č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.
Asi všichni víme, že ~
znamená náš domovský adresář. Kromě toho ale existuje ještě ~+
, ~-
a ~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í
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ů find
a xargs
.
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 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).
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.
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
.
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.
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í.
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.
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.
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ů:
$PATH
~/.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'
read_nullbyte()
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
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í.
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
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/&/\&/g' -e 's/</\</g' \
-e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/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/&/\&/g' -e 's/</\</g' \
-e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/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.
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ý.
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/
a /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.
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
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).
Doplnil jsem kapitolu „Vícevláknové skripty resp. více procesů“.
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í.
Doplnil jsem kapitolu „Víceřádkové hodnoty“.