Drobnosti ze shellového zápisníku (2)

15. 4. 2002
Doba čtení: 10 minut

Sdílet

Dnes dokončíme téma hromadného přejmenování, tentokrát pomocí příkazů find a awk. Pak jednoduše převedeme češtinu na „cestinu“. Nakonec se podíváme na hromadnou záměnou textů. Přitom si ukážeme sílu i úskalí regulárních výrazů a cestu k vytváření dlouhých a komplikovaných regulárních výrazů.

Použití příkazu find pro přejmenování

Příkaz find umožňuje vyhledávat soubory podle určitých pravidel a provádět s nimi různé akce.

Mějme časopis v adresářích 2000, 2001, 2002, v nich jsou podadresáře 01, 02, 03 atd. a v nich soubory obsah.pdf a obal.pdf, dále jsou v nich podadresáře s jednotlivými články a v nich jsou další PDF soubory. Nyní jsme došli k závěru, že bude vhodnější mít soubory ve tvaru 2002_02_obal.pdf či 2002_02_clanek1_tis­k.pdf. Pokud nechceme celou pracovní dobu přejmenovávat, stačí jednořádkový program. Ten převede všechna lomítka na podtržítka a poté první lomítko opět vrátí (find totiž vrací jména souborů ve tvaru ./soubor):

# zploštění adresářové struktury (souborů .pdf)
find -name ‚
.pdf‘ -type f -exec sh -c „mv -i -v ‚{}‘ \“\$(echo ‚{}‘ | sed ‚s%/%_%g;s%_%­/%‘)\"" ‚;‘

Museli jsme použít drobný trik s opakovaným voláním, protože find expanduje své závorky {} až po expanzi shellem.

Ani obrácené přejmenování nemusí představovat problém. Použijeme zde další zajímavou funkci programu find – -printf a následné přesměrování rourou do nového shellu:

# obnovení zploštěné adresářové struktury (souborů .pdf)
find -name ‚
.pdf‘ -maxdepth 1 -type f -printf „NEW=\“\$(echo ‚%P‘ | sed ‚s@_@/@g‘)\" ; mkdir -p \„\${NEW%%/*}\“ ; mv -i ‚%P‘ \„\$NEW\“\n" | sh

Poznámka: Příkaz selže, narazí-li na jméno souboru bez znaku _.

Důležitá poznámka k použití find: Jeho použití obecně není odolné proti neobvyklým jménům souborů, obsahujícím mezery, uvozovky, apostrofy či znaky konce řádku. Tento problém nelze dokonale ošetřit.

Přejmenování pomocí AWKu

AWK je výborným nástrojem pro práci s textovými soubory. Přejmenování mu proto svěříme, když máme nová jména uložena v textovém souboru.

Představme si, že máme fotografie psů pes1.jpg až pes6.jpg a soubor psi.txt, který popisuje vyobrazené psy:

1 Rex
2 Fifinka
3 Azor
4 Alík
5 Kikina
6 Fidík

Využijeme toho, že standardním oddělovačem polí v AWKu je mezera, a napíšeme jednoduchý program:

# přejmenování podle souboru
gawk ‚{ system („mv pes“ $1 „.jpg " $2 ".jpg“) }‘ <psi.txt

Česká písmena na „ceska“

Mohli bychom být rozladěni, že jména souborů budou obsahovat velká písmena a znaky s háčky a čárkami. Můžeme to sice řešit i v AWKu, ale zde si ukážeme jednoduchý postup pomocí programu tr (pro převod množin znaků). První příkaz v koloně (gawk) vypíše příkazy. Druhý převede česká písmena na „ceska“. A třetí velká na malá. Oboje by šlo zařídit jediným příkazem tr, ale první tr již je v shellovém zápisníku uschovaný. Je totiž velmi užitečný i jinak, potřebuji-li poslat mail českému čtenáři v cizině, kde by češtinu nepřečetl.

# česky na cesky
# Pozor! V řetězci ‚ -ÿ‘ není mezera, ale znak NBSP (160).
tr ‚ -ÿ‘ ' A L LS „SSTZ\-ZZ a l'\''ls sstz“zzRAAAAL­CCCEEEEIIDDNNO­OOO.RUUUUYT raaaalccceeee­iiddnnoooo/ru­uuuyt ' <česky.txt >cesky.txt

# přejmenování ze souboru + zrušení akcentů (ISO-8859–2) + převod na minusky
# Pozor! V řetězci ‚ -ÿ‘ není mezera, ale znak NBSP (160).
gawk ‚{ print „mv pes“ $1 „.jpg " $2 ".jpg“ }‘ <psi.txt | tr ‚ -ÿ‘ ' A L LS „SSTZ\-ZZ a l'\''ls sstz“zzRAAAAL­CCCEEEEIIDDNNO­OOO.RUUUUYT raaaalccceeee­iiddnnoooo/ru­uuuyt ' | tr ‚A-Z‘ ‚a-z‘ | sh

Poznámky:
Při kopírování znaku NBSP z prohlížeče mohou nastat problémy!
Takové řazení příkazů je poněkud svévolné. Pokud by bylo v názvu některého příkazu nebo v původním jménu souboru velké písmeno, takto jednoduchý postup by selhal. V našem případě ale bude fungovat dobře. V ostatních případech bychom si pomohli již zmíněnou konstrukcí „$(echo "argument“ | tr ‚množina‘ ‚množina‘)" nad druhým argumentem.

Hromadné záměny textů

Tématem se na Rootu zabývaly již dva články, ale nebude na škodu tyto postupy připomenout, neboť jsou všestranné a mimořádně užitečné.

K vytvoření seznamu jmen souborů lze použít tytéž postupy jako u přejmenování.

K hromadné záměně textů s oblibou používám regulární výrazy, a tedy i sed. A protože se jedná o akci velmi častou, napsal jsem skript sedfile.sh. Pokud se zarazíte nad jeho poměrnou komplikovaností, pak je to proto, aby byl bezpečný, vytvářel zálohy, u souborů, které nemění, neměnil datum, a u souborů, které mění, neměnil práva. Kromě toho, že provede záměnu, je jeho výstupem klasická záplata v unifikovaném formátu.

Použití je jednoduché: Mějme projekt, ve kterém linkujeme spustitelný soubor s knihovnou Xinerama. Zjistili jsme, že v případě, že jde o dynamickou knihovnu, budeme muset linkovat i s knihovnou Xext. Nejrychlejší cestou bude hromadná záměna.

# hromadná záměna -lXinerama → -lXinerama -lXext
sedfile.sh ‚s/-lXinerama/& -lXext/g‘  find -name Makefile

Snadné, že? Teď se pokusíme o komplikovanější úkol. Máme soubory pes*.html, kde se mluví o psovi. Chceme všechny výskyty slova pes ve všech pádech zaměnit za odpovídající pád jména Alík:

# záměna textu (špatně!)
sedfile.sh ‚s/pes/Alík/g;s/psu/­Alíkovi/g;s/pse/A­líku/g;s/ps\(a­\|ovi\|em)/Alík\1/g‘ pes*.html

Takový regulární výraz sice vypadá hezky a je syntakticky v pořádku, jeho výsledek je však žalostný:
Pokud očekáváte, že z věty Pes přinesl psaní. dostanete Alík přinesl psaní., jste na omylu! Dostanete Pes přinesl Alíkaní.! Přehlédli jste možnost výskytu slova pes s velkým písmenem na začátku věty nebo jako podřetězce uprostřed slova.

Naštěstí sedfile.sh vytváří zálohy souborů. Přejmenujeme je zpět:

# vrátit všechny zálohy v aktuálním adresáři
for i in *~ ; do mv -vf „$i“ „${i%\~}“ ; done

Jsou-li v adresáři i starší zálohy, můžeme použít find:

# vrátit všechny zálohy v aktuálním adresáři staré právě jednu minutu
for i in find -name '*~' -maxdepth 1 -cmin 1 ; do mv -vf „$i“ „${i%\~}“ ; done

A nyní se můžeme pustit do vymýšlení správného regulárního výrazu. V jiné části zápisníku, kde jsou uloženy užitečné regulární výrazy, se nacházejí i výrazy pro rozpoznávání hranic slov (sed není Emacs a rozpoznávání písmen \w a \W nefunguje pro písmena s háčky a čárkami):

# regulární výraz pro všechna písmena ISO-8859–2
[A-Za-zĄŁĽŚŠ-ŹŽŻął´ľśš-źž-ÖŘ-öř-ţ]
# regulární výraz pro rozpoznání začátku slova
\(^\|[^A-Za-zĄŁĽŚŠ-ŹŽŻął´ľśš-źž-ÖŘ-öř-ţ])
# regulární výraz pro rozpoznání konce slova
\($\|[^A-Za-zĄŁĽŚŠ-ŹŽŻął´ľśš-źž-ÖŘ-öř-ţ])

Poznámka: Protože regulární výraz pro všechna písmena je příliš dlouhý, zkrátíme jej z důvodu omezené šířky řádků na Rootu na nepřesný zkrácený výraz A-Za-zĄ-ţ. Pokud si příkazy nakopírujete do zápisníku, jednoduchou záměnou výraz opět nahraďte přesným A-Za-zĄŁĽŚŠ-ŹŽŻął´ľśš-źž-ÖŘ-öř-ţ.

Provedeme rozpoznání hranic slov a velkého písmena a vyzkoušíme jej na krátké povídce, která obsahuje různé výskyty:

echo ‚Pes přinesl psaní. Musím odepsat a odměnit psa. Pse, k noze!‘ | sed ‚s/\(^\|[^A-Za-zĄ-ţ])[Pp]es\($\|[^A-Za-zĄ-ţ])/\1Alík\2/g; s/\(^\|[^A-Za-zĄ-ţ])[Pp]su\($\|[^A-Za-zĄ-ţ])/\1Alíkovi\2/g; s/\(^\|[^A-Za-zĄ-ţ])[Pp]se\($\|[^A-Za-zĄ-ţ])/\1Alíku\2/g; s/\(^\|[^A-Za-zĄ-ţ])[Pp]s\(a\|­ovi\|em)\($\|­[^A-Za-zĄ-ţ])/\1Alík\2\3/g‘

Poznámka: To, že některé příkazy jsou příliš dlouhé a nevejdou se na řádek Roota, jsem řešil vložením nepovinné mezery za středník.

Dostaneme přesně to, co čekáme:
Alík přinesl psaní. Musím odepsat a odměnit Alíka. Alíku, k noze!

Regulární výraz, přestože je v podstatě triviální, vypadá hrozivě a není snadné se v něm orientovat.

Můžeme jít o krok dále a generovat regulární výraz pomocí jiného regulárního výrazu. Není to tak ztřeštěné, jak to vypadá – vytvoříme si jednoduchý a srozumitelný metajazyk, ve kterém záměnu popíšeme. To, že výsledný regulární výraz bude ještě složitější, nás nemusí pálit:

sedfile.sh „$(echo ‚pes()->Alík psu()->Alíkovi pse()->Alíku ps(a|ovi|em)->Alík‘ | tr ' ' \\n | sed ‚s:p:; s:\\&:g; s:^:s/\\(^\\|[^A-Za-zĄ-ţ]\):; s:->:\\($\\|[^A-Za-zĄ-ţ]\)/\\1:; s:$:\\2\\3/g:‘)“ pes*.html

Hmm… Stále je to jen pro otrlé, ale budeme-li chtít vyrobit další sadu textů, kde se mluví třeba o Fidíkovi, tuto metodu jistě oceníme. Kromě první záměny s:p: je celý zbytek programu univerzální. Stačí drobná změna a program nebude generovat obyčejný text, ale hypertextové odkazy. A to ve všech pádech!

sedfile.sh „$(echo ‚pes()->Fidík psu()->Fidíkovi pse()->Fidíku ps(a|ovi|em)->Fidík‘ | tr ' ' \\n | sed ‚s:p:; s:\\&:g; s:^:s/\\(^\\|[^A-Za-zĄ-ţ]\):; s:->:\\($\\|[^A-Za-zĄ-ţ]\)/\\1<a href=“photos\\/gra­fika\\/fidik.jpg">:; s:$:\\2<\\/a>­\\3/g:‘)" pes*.html

Dostaneme:
Fidík přinesl psaní. Musím odepsat a odměnit Fidíka. Fidíku, k noze!

Tím jsme možná vyčerpali sebe, ale zdaleka ne možnosti regulárních výrazů.

Jak odladit složité příkazy?

Odladit takové příklady není jednoduché a nelze to provést najednou.

Ukážeme si, jak odladit skript, který bude automaticky přidávat hypertextové odkazy k názvům českých linuxových serverů:
Roota jsem přečetl hned ráno. Penguina také. A na Netem dnes nic nebylo. Lupu jsem ještě nečetl.

bitcoin školení listopad 24

  1. Nejdříve vybereme vhodné ohraničovací znaky (které ve výrazu nepoužijeme – zde @ a :) a odladíme jeden regulární výraz (pomocí příkazu echo):

    echo Roota | sed ‚s@\(^\|[^A-Za-zĄ-ţ])\(Root\(\|a\|u\|­em\|ovi))\($\|[^A-Za-zĄ-ţ])@\1<a href=„http://­www.root.cz“>\2</a>\­4@g‘

  2. Vytvoříme metapříkaz a odladíme levou část nového výrazu:

    echo ‚Root(|a|u|em|o­vi)=www.root.cz Penguin(|a|u|em|o­vi)=www.pengu­in.cz Netem()=www.ne­tem.cz Lup(a|y|u|ě|ou­)=www.lupa.cz‘ | tr ' ' \\n | sed ‚s:\\&:g; s%^\(.)=\(.)%ar­g1=\1 arg2=\2%‘

  3. Vložíme první výraz do editoru a ke každému expandovanému znaku (\, & a znaku, který použijeme jako ohraničovač) přidáme zpětné lomítko. Poté jej vložíme jako pravou stranu nového výrazu a pevné řetězce zaměníme za parametry (parametr 2 se v našem případě promění na parametr 4). Zkontrolujeme, zda generuje stejný regulární výraz, který jsme v první fázi vytvořili.

    echo ‚Root(|a|u|em|o­vi)=www.root.cz Penguin(|a|u|em|o­vi)=www.pengu­in.cz Netem()=www.ne­tem.cz Lup(a|y|u|ě|ou­)=www.lupa.cz‘ | tr ' ' \\n | sed ‚s:\\&:g; s%^\(.)=\(.)%s@\\(^\\|­[^A-Za-zĄ-ţ]\)\\(\1\)\\­($\\|[^A-Za-zĄ-ţ]\)@\\1<a href=„http://­\2“>\\2</a>\\4@g%‘

  4. Uzavřeme jej do přesměrovacích závorek a uvozovek „$()“. Pokud bychom byli nuceni použít zpětné uvozovky „``“, museli bychom ještě opatřit zpětným lomítkem všechny znaky, které by shell expandoval uvnitř uvozovek. Přidáme příkaz sed a jednoduchý testovací příklad:

    echo „Roota jsem přečetl hned ráno. Penguina také. A na Netem dnes nic nebylo. Lupu jsem ještě nečetl.“ | sed „$(echo ‚Root(|a|u|em|o­vi)=www.root.cz Penguin(|a|u|em|o­vi)=www.pengu­in.cz Netem()=www.ne­tem.cz Lup(a|y|u|ě|ou­)=www.lupa.cz‘ | tr ' ' \\n | sed ‚s:\\&:g; s%^\(.)=\(.)%s@\\(^\\|­[^A-Za-zĄ-ţ]\)\\(\1\)\\­($\\|[^A-Za-zĄ-ţ]\)@\\1<a href=“http://­\2">\\2</a>\\4@g%‘)"

    Vše je v pořádku a výsledek vypadá podle očekávání.

  5. Můžeme tedy přistoupit k ostrému nasazení vytvořeného výrazu:

    sedfile.sh „$(echo ‚Root(|a|u|em|o­vi)=www.root.cz Penguin(|a|u|em|o­vi)=www.pengu­in.cz Netem()=www.ne­tem.cz Lup(a|y|u|ě|ou­)=www.lupa.cz‘ | tr ' ' \\n | sed ‚s:\\&:g; s%^\(.)=\(.)%s@\\(^\\|­[^A-Za-zĄ-ţ]\)\\(\1\)\\­($\\|[^A-Za-zĄ-ţ]\)@\\1<a href=“http://­\2">\\2</a>\\4@g%‘)" *.html

A příkaz je funkční a odladěný… Budeme-li v budoucnu přidávat další servery, bute to již triviální: do seznamu přidáme Linuxworld(|u|em)=ww­w.linuxworld.cz AbcLinuxu()=www­.abclinuxu.cz.

Pokud vám uvedený postup připadl složitý, mohu vás ubezpečit, že lze odladit a v praxi nasadit i systém mnohem složitější: Na jedné z www stran, které jsem vytvářel, jsem použil následující postup: Regulární výraz vyhledá na stranách jejich titulky – jména hudebníků. Další regulární výraz upraví jména na regulární výrazy, odpovídající všem možným pádům. Z nich se vygeneruje obří regulární výraz na vytváření hypertextových odkazů. Ten zpracuje další regulární výraz, který vyřadí vytvoření odkazů sama na sebe. Teprve tímto výrazem se zpracuje webová stránka!

Autor článku