Hlavní navigace

Pythonize (pz): když chcete na řádce Python místo Bashe

22. 3. 2021
Doba čtení: 6 minut

Sdílet

 Autor: Python
Nástrojů pro zpracování vstupů existuje v linuxových distribucích celá hromada. Chtěli byste pro tuhle činnost používat syntaxi Pythonu? Představíme vám užitečnou utilitu pz, jako Pythonize.

Chtěl bych vám představit utilitu pz, jako Pythonize, určenou pro vás, pokud jste uživatelem příkazové řádky se znalostí Pythonu. Současné linuxové distribuce mají k dispozici mnoho efektivních nástrojů pro zpracování vstupu. Přáli jste si někdy, abyste místo nich mohli použít syntaxi Pythonu? Listujete často manuálem, jak že se chovají ty přepínače na zpracovávání vstupu? Připadá vám zdrojový kód Bashe obtížně čitelný? Právě pro vás je tento článek. Dozvíte se, jak si napsat maličký program a jak je vyhodnocován, které proměnné v něm máte k dispozici, pár slov k auto-importu, přepínačům a ukážeme si některé příklady použití.

Příkazová řádka je vynikající rozhraní, jež se vyznačuje ohromujícím globálním… když ne přímo nadšením, pak dosahem. Do terminálu se lze připojit málem už na poslední pračce. Někteří uživatelé se příkazové řádky obávají, nicméně doposud neexistují dokonalé grafické aplikace umožňující nám vyrovnat se s šíří úkolů, jimž člověk před klávesnicí čelí. Možná jste guru GNU coreutils a znáte všechny přepínače programů tr a cut, možná děláte jazykovou korekturu v  sed u místo ve Wordu a nákupní seznamy píšete v awk, pak utilitu pz nepotřebujete. Pokud jste ale strávili tři hodiny hledáním zapomenuté mezery v podmínce Bash skriptu a umíte si představit lepší využití pro své odpoledne, možná váš život bude už večer jednodušší. Netřeba již více umět rozličné syntaxe, umíte-li syntaxi Python.

Proč ale nepoužít přímo Python na příkazové řádce, copak nelze použít přepínač -c a vyhodnotit kód?

python -c "print(1)" # 1

Ano, samozřejmě lze. Ale zkuste si zpracovat složitější úkol a brzy utonete při zpracovávání vstupu a výstupu. Přestože je výpočet na jednu řádku, už jenom import knihoven na zacházení s rourami způsobí pořádné bolení hlavy. Zvlášť když přihlédneme k tomu, že Python nevhodný na jednořádkové příkazy, které tu působí monstrózně; na ty je tu například PERL. Utilita pz umožňuje zanedbat všechny ty řádky jako shebang #!/usr/bin/env python3, loggování, konverzi mezi byty a stringem a tak dále; všechno to, co byste kvůli jednořádkové funkcionalitě psali stále dokola.

Při navrhování rozhraní jsme se pokusili použít pravidlo intuice – pokud by existoval ideální program, jak přesně by ho uživatel chtěl použít? Základem jsou proměnné, přepínače a klauzule. Některé proměnné se automaticky doplňují, některé jsou předpřipraveny pro vás, abyste je nemuseli inicializovat a další řídí výstup. Klauzule obsahují váš příkaz nebo jejich sekvence a vyhodnocují se před, během a po zpracování vstupu a přepínače mění jejich chování.

Nechme tedy Python dělat ty věci, pro které nám přirostl k srdci, jako vykrajování podřetězců pomocí hranatých závorek [start:stop:step] nebo generátorová notace. Utilita pz vám přesně toto umožňuje. K dispozici máte proměnnou s, která vždy obsahuje právě zpracovávaný řádek. Změníte-li ji, změní se výstup. Pošleme nyní na vstup slovíčko ‚ahoj‘ a nechme vyříznout podřetězec mezi 1. a 3. znak, tedy chtějme dostat ‚ho‘. (Za mřížkou značím návratovou hodnotu programu.)

# vykrojíme řetězec
echo "ahoj" | pz s[1:3] # "ho"

Kdybychom použili přepínač --verbose, k zobrazení detailních informací, zjistili bychom, že utilita na pozadí vyhodnotila, že se proměnná s při vykonávání příkazu nijak nemění. A upravila tedy výraz o zpětné přiřazením do proměnné s tímto způsobem: s = s[1:3]. Proto není třeba do s zpátky explicitně přiřazovat. Mezi další automaticky doplňované proměnné patří n, která obsahuje aktuální řádku konvertovanou na číslo (lze-li). V následujícím příkladu přičteme k prvku na indexu 1 sedmičku. Zároveň je vidět, že volání utility lze libovolně řetězit.

# rozdělíme na čárce a přičteme sedmičku
echo "ahoj,5" | pz 's.split(",")[1]' | pz n+7 # 12

K dalším automaticky doplňovaným proměnným patří counter (číslo aktuální řádky), text (celý text v jediném řetězci), lines (celý text v listu řádků), numbers (celý text jako list, přičemž každý řádek je změněn na číslo). Utilita myslí na přetékání (automatické doplňování proměnných lze vypnout) i průtok – proměnná text je k dispozici implicitně až po zpracování celého textu, abychom bezpečně mohli zpracovávat nekonečný vstup libovolně dlouho. Podívejme se například na proměnnou numbers. Na vstupu dostáváme postupně čtyři čísla. Naše klauzule určuje, že přes proměnnou s se na výstup dostane entice ve formátu: číslo aktuální řádky, aktuální řádka a aritmetický průměr.

$ echo -e "20\n40\n25\n28" | pz 's = counter, s, sum(numbers)/counter'
1, 20, 20.0
2, 40, 30.0
3, 25, 28.333333333333332
4, 28, 28.25

Zatím jsme viděli pouze klauzuli main, která se spouští pro každou řádku vstupu. Existují i klauzule --setup, která se vyhodnocuje úplně na začátku (pro případ, že je třeba iniciovat některé proměnné) a zrcadlově k tomu --end, která se spustí jedinkrát na závěr. Pro jednoduchý příklad chtějme spočítat délku celého textu.

echo -e "hello\nworld" | pz --end 'len(text)' # 11

Filtrování lze snadno realizovat pomocí přepínače --filter  – řádka se pošle dále podle booleanu, na který se přetypuje výsledek klauzule main. Obdobně pracuje proměnná skip, kterou stačí nastavit pravdivostní hodnotu. Pošleme dál například jen čísla, která jsou větší než tři.

$ echo -e "1\n2\n3\n4\n5" | pz "skip = not n > 3"
4
5

Pro užití regulární výrazů máme speciální přepínače. Není tedy třeba importovat z modulu re dané funkce a ztratit se v lese uvozovek a apostrofů, stačí využít --search, --match, --findall či --sub. Pokud se vám do textu zatoulala URL, přičemž na jedné řádce jich může být i více, prožeňte každou řádku přepínačem findall. Klauzule main se pak automaticky doplní na  s = re.findall(výraz, vstup).

echo "Lorem http://example.com ipsum http://example.cz dolor" |  pz --findall "(https?://[^\s]+)"
http://example.com
http://example.cz

Když se zaměříte na výstup, zjistíte, že pro jednu řádku vstupu (tu jedinou), se nám vrátilo více řádek. Ano, i to je možné. Výstup se upraví podle toho, co v proměnné s zbude po procesování řádku. Je-li to n-tice či generátor, vypíše se jako jeden řádek oddělený čárkami. Zbude-li list, dostaneme řádků více. Najdeme-li volatelný výraz, zavoláme ho. To je důvod, proč můžeme napsat pouhé s.lower bez závorek, abychom snížili text.

echo "HEllO" | pz s.lower # s = s.lower() → "hello"
echo "HEllO" | pz len # s = len(s) → "5"
echo "25" | pz sqrt | pz round # s = math.sqrt(n)s = round(n) → "5"

Jak pracuje automatický import? Bylo by velmi nepraktické, kdyby uživatel musel používat klauzuli --setup používat i pro import těch nejobyčejnějších funkcí. Zároveň načítat všechny dostupné moduly naráz by utilitu zpomalilo na nesnesitelnou úroveň, nemluvím-li ani o problémech, které by vyvstaly při duplicitním jménu funkce u různých modulů. Utilita přistupuje na kompromis: Část funkcí je importována od začátku, například všechny symboly z knihovny math, aby uživatel mohl sečíst všechna čísla na vstupu pouhým pz --end sum (které se interně přeloží jako pz --end "s=sum(numbers)". Další množství funkcí je připraveno v záloze, aby se importovaly, jakmile zpracovávání vstupu selže kvůli jejich nepřítomnosti. Například modul requests, aby uživatel mohl načíst webovou stránku pouhým  echo "http://example.com" | pz 'requests.get(s).content'.

Jisté množství symbolů je importováno, funkce sleep z modulu time, funkce datetime z modulu datetime, dříve viděný randint z modulu random nebo celý modul random, aby uživatel mohl snadno použít náhodný výběr přes funkce  random.choice.

Na závěr si předložme ještě jednu komplikovanější výzvu. Stojíme před úkolem, kdy chceme ověřit, k čemu budou tíhnout náhodná čísla, generovaná funkcí random.randint. Nezajímá nás celý průběh, ale dejme tomu každá desetitisící hodnota. Jak na to? Za prvé zde nepotřebujeme žádný vstup. Použijeme přepínač --generate s hodnotou nula, která zajistí nekonečný vstup. Zahodíme všechny řádky, jež nejsou dělitelné deseti tisíci, zbylé vypišme na konzoli ve formátu číslo aktuálního řádku a aktuální průměr hodnot. Hodnoty se drží okolo padesátky.

Hacking tip

pz "i+=randint(1,100); s = (counter,i/counter) if not counter % 10000 else None" --generate 0
10000, 50.4153
20000, 50.35645

Utilita nemá žádné závislosti, pouze standardní Python ve verzi alespoň 3.6 (která je s námi už čtyři roky). Nainstaluje se balíčkovačem pip, případně stačí stáhnout a spustit jediný soubor. Její kód je hostován jako GPLv3 na adrese GitHub.com/CZ-NIC/pz , kde také naleznete kompletní dokumentaci a množství příkladů.

(Původně napsáno pro blog CZ.NIC.)

Autor článku

Edvard Rejthar má v týmu CSIRT.CZ a CZ.NIC-CSIRT na starosti především analýzu škodlivého kódu na webových stránkách v české doméně, rozvoj systémů na automatické zpracovaní dat a analýzy pro projekt Turris.