Veselice s `xargs`

Upozornenie

Článok má novšiu verziu.

Úvod

Každý druhý článok o xargs sa začína v duchu „… jedným z najpodceňovanejších príkazov Unixu je…”. Tento nebude iný.

Mnohokrát sa stáva, že výstupom programu je niekoľko slov oddelených bielym miestom (napr. slová na samostatných riadkoch), ktoré chceme postupne jeden za druhým spracovávať a posielať ako parameter do iného programu. Niečo v duchu:

pre každé slovo R
    spracuj R

Priamo for cyklus! V shellscriptingu je však for prekérny: vyžaduje totiž podivnú viacriadkovú syntax… a komu sa chce kvôli jednoduchým jednorazovým veciam zakladať skripty, písať shebangy, chmodovať a patlať sa s editorom.

Túto rolu vie mnohokrát zastúpiť xargs.

O xargs

Stručne povedané:

xargs zostavuje zo štandardného vstupu príkazový riadok a vykonáva ho.

Povedané inými slovami: ak máte na štandardnom vstupe slová oddelené bielym miestom, a v inom príkaze ich potrebujete nasekať do parametrov, xargs vás zachráni. A naozaj nemusíte forovať.

Ukážková úloha: vypíšte počty riadkov všetkých shellskriptov v aktuálnom adresári

Všetci vieme, že sa to dá spraviť jednoduchým (ehm) findom:

find . *.sh -maxdepth 1 -exec wc -l {} \;

Syntax findu je záludná: treba si pamätať chlpaté zátvorky a najmä nezabudnúť ukončiť -exec escapovanou bodkočiarkou. Nehovoriac o nutnosti obmedziť hľadanie na jedno vnorenie cez -maxdepth.

S xargs by to šlo jednoduchšie:

echo *.sh | xargs wc -l

Využijeme tu klasickú shellovskú pathname expansion (expanzia hviezdičky), kde echo vráti zoznam všetkých súborov (a adresárov) končiacich sa na *.sh.

Výsledok napr:

mena.sh premenuj.sh velkosti.sh vyrobsubory.sh

Vieme, že wc vie zobrať ako parameter názov súboru, pre ktorý spočíta riadky/znaky/bajty:

novotnyr@s:~$ wc -l mena.sh
14 mena.sh

Príkaz wc samozrejme podporuje aj viacero súborov:

novotnyr@s:~$ wc -l mena.sh premenuj.sh
 14 mena.sh
  8 premenuj.sh
 22 total

(Všimnite si bonusový riadok s total uvádzajúci sumarizované dáta.)

Ak by sme vedeli nasekať všetky súbory ako parametre pre wc, získali by sme všetko, čo potrebujeme. A presne toto urobí xargs.

V štandardnom správaní vyzbiera slová zo štandardného vstupu (slovom sa teraz rozumie reťazec oddelený medzerami) a rovno ich vykydne do parametrov pre príkaz uvedený za ním. V našom príklade sa totiž v skutočnosti vykoná toto:

wc -l mena.sh premenuj.sh velkosti.sh vyrobsubory.sh

Uvedené použitie je nielen jednoduchšie na zápis, ale dokonca pobeží rýchlejšie než find/exec. Vykoná sa totiž len jedno spustenie wc -l, na rozdiel od prvého prípadu, kde sa wc spúšťa pre každý nájdený súbor zvlášť.

Limity operačného systému

Operačný systém obvykle kladie limity na maximálnu dĺžku riadku, ktorý sa dá vykonať. V súčasných systémoch to už nie je až také kritické (napr. náš debianovský server má limit 2094774 znakov, teda cca 2 MB dát.) Ak by aj napriek tomu k niečomu takému došlo, xargs sa s tým vie vysporiadať — to totiž bol jeden z jeho hlavných účelov.

Pomocou parametra -n vieme povedať, koľko slov zo štandardného vstupu sa má použiť v jednej várke spustenia.

echo *.sh | xargs -n2 wc -l

Pre -n2 to znamená, že xargs bude brať dvojice slov a použije ich ako parametre pre wc. Keďže máme dve dvojice, wc sa spustí dvakrát. Aha, výsledok:

 14 mena.sh
  8 premenuj.sh
 22 total
 18 velkosti.sh
  8 vyrobsubory.sh
 26 total

Dôsledkom je dvojnásobný výpis totalov, čo môžeme vyriešiť nahrubo: jednoducho spustíme wc pre každý súbor zvlášť:

echo *.sh | xargs -n2 wc -l

Medzery a xargs

Pre xargs platí základná zásada: berú sa slová oddelené bielym miestom (medzery, tabulátory…). To sa prejaví hlavne v prípade, ak máme súbory s medzerami v názve — v takom prípade začne xargs a wc robiť psie kusy. Napríklad súbor subor 1.txt v kombinácii s xargs spôsobí volanie:

wc -l subor 1.txt

Čo znamená, že sa úbohý wc pokúsi zrátať riadky v súbore subor a potom v súbore 1.txt, čo nie je rozhodne to, čo chceme.

Vylepšiť to môžeme tým, že do xargs budeme posielať názvy súborov na riadkoch, ale nepomôže to…

ls -1 *.bin | xargs wc -l

… ale bude to krok k správnemu výsledku. xargs má totiž parameter -I, ktorým umožňuje určiť presné miesto v príkazovom riadku, na ktoré sa vložia parametre. Štandardne sa totiž parametre dolepujú za príkaz. Ak ich chceme dolepiť na iné miesto, použime toto:

ls -1 *.bin | xargs -I{} wc -l {}

Uff, funguje to, ale… prečo?

Parameter -I hovorí, že namiesto znakov {} sa vložia samotné hodnoty pre parametre do príkazového riadku na miesto, ktoré si špecifikujeme sami. (Zástupný znak si môžete zvoliť ľubovoľne, ale konvencia z findu odporúča použiť práve kombináciu {}.)

Dôležité je však druhé správanie: -I hovorí, že hodnoty pre parametre budú na samostatných riadkoch. Príkaz echo *.bin pchá názvy súborov za seba (oddelené medzerami), ale ls -1 ich naseká práve do riadkov, čo sa nám hodí.

Inak povedané, slovom do parametra sa stane všetko, čo sa nachádza na samostatnom riadku, bez ohľadu na to, či obsahuje medzery, alebo nie.

Ak sa na riadku bude nachádzať

`subor 1.txt`

xargs ho celé vezme a pošle ako parameter do wc presne tak, ako keby sa vykonalo volanie

wc -l "subor 1.txt"

Samozrejme, tento trik sa spolieha na fakt, že názov súboru neobsahuje znak nového riadka, ale treba si stanoviť isté hranice, po ktoré budeme náš program opravovať. Ak by to predsa nestačilo, tak sa využije…

find a xargs

xargs idú často ruka v ruke s findom. Hoci tento príkaz má podporu pre -exec, tá je (ako sme spomenuli vyššie) pomalá.

Mnohokrát sa používa omnoho prehľadnejšie a efektívnejšie:

find . *.sh -type f -maxdepth 1 | xargs wc -l

Porovnajte to s klasickým:

find . *.sh -type f  -maxdepth 1 -exec wc -l {} \;

Opäť platí, že to bude fungovať len pre súbory neobsahujúce medzery. GNU find však na túto možnosť myslel, pretože dáva k dispozícii možnosť vypľuť na výstup súbory oddelené znakom NUL (teda znakom s ASCII hodnotou 0). Dosiahneme to parametrom -print0.

xargsu potom môžeme povedať, že položky v štandardnom vstupe sú oddelené tým istým znakom , stačí použiť parameter -0.

find . *.sh -maxdepth 1 -print0 | xargs -0 wc -l

Táto možnosť je o niečo dlhšia ako kombinácia ls a xargs, ale je o to robustnejšia.

printf a xargs

Analogická možnosť je dosiahnuteľná cez printf, kde povieme napevno, že položky vypadnuté z hviezdičkovej expanzie sa majú oddeliť NUL znakom.

printf "%s\0" *.sh | xargs -0 wc -l

Táto finta stojí a padá na vlastnosti printfu: ak je špecifikátorov formátu menej než skutočných parametrov pre printf, posledný sa zopakuje toľkokrát, koľko je treba. V tomto prípade sa pre každý skutočný parameter použije špecifikátor %s\0, kde \0 indikuje práve NUL znak.

Ďalšia úloha: Vytvorte 10 súborov s názvom subor1.txtsubor10.txt

Namiesto for cyklu vieme použiť kombináciu seq a xargs s touch:

seq 10 | xargs -I{} touch subor{}.txt

Príkaz seq nageneruje na každý riadok jedno číslo, a kombinácia -I bude brať jednotlivé riadky a zároveň poslúži na vyformátovanie názvu súboru.

Ak by sme chceli súbory subor 1.txt atď (s medzerami), použijeme klasické zásady o úvodzovkovaní zo shellu.

seq 10 | xargs -I{} touch "subor {}.txt"

Ďalšie triky s xargs

Sekanie vstupov sa dá použiť na kozmetické efekty:

Vypíšte maticu 3 x 3 s prvkami 1..9

Stačí použiť fintu s dávkovaním parametrom po n-ticiach:

seq 9 | xargs -n3 

Výsledkom je

1 2 3
4 5 6
7 8 9

Vypíšte jednotkovú maticu 3 x 3

yes | head -n9 | xargs -n3 | tr 'y' '1'

Príkaz yes generuje y donekonečna, ale nám stačí odseknúť headom prvých 9 hodnôt (3 x 3), ktoré pošleme posekať do xargs a na záver nahradíme znaky y jednotkami.

Vypíšte čísla na jeden riadok

Príkaz xargs vie poslúžiť ako lepič riadkov:

seq 10 | xargs  

Výsledok:

1 2 3 4 5 6 7 8 9 10

Vypíšte zoznam používateľov v systéme na jeden riadok

cut -d: -f1 < /etc/passwd | sort | xargs

Vymažte súbory *.txt

Klasické riešenie stavia na rm a expanzii hviezdičky

rm *.txt

Ak nechceme naraziť na limity dĺžky cesty, pokojne môžeme použiť kombináciu xargs a rm

echo *.txt | xargs rm

Trasovanie príkazov

Ak chcete vidieť, čo sa naozaj vykoná, spustite xargs s parametrom -t.

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *