Abstrakt: Práce v příkazové řádce se neobejde bez znalosti některých základních utit, které dokáží značně zjednodušit práci. V textovém prostředí často potřebujeme vyhledávat, vybírat část textu, sloupec, řadit či data dále zpracovávat. Přednáška vás seznámí se základními utilitami a úlohami, se kterými se v prostředí CLI setkáváme každý den.
Spousta úloh se dá velice efektivně řešit v příkazové řádce, od těch jednoduchý, až po ty komplexnější. GNU/Linux, a samozřejmě i další unixy, nám poskytují soubor utilit, které jsou léty prověřené, dobře zdokumentované, existují k nim tutoriály na internetu a zkušení uživatelé se bez nich neobejdou.
Příklad z praxe: dostal jsem za úkol monitorovat disková zařízení v systému HP-UX a po jednom dni provozu zjistit jaké je jejich zatížení a to porovnat s jiným monitorovacím nástrojem. Data se sbírala utilitou SAR a jejich formát byl následující:
14:31:25 device %busy avque r+w/s blks/s avwait avserv 14:31:45 disk1 4.40 2.33 17 274 5.50 58.52 disk50 0.30 0.50 12 208 0.00 0.51 disk51 0.05 0.50 0 0 0.00 8.80 disk52 2.80 0.50 21 286 0.00 1.44 disk53 2.45 0.50 16 297 0.00 1.64 disk54 2.35 0.50 14 313 0.00 1.76 14:32:05 disk1 0.75 0.50 2 43 0.00 12.53 disk50 0.05 0.50 3 33 0.00 0.29 disk51 0.05 0.50 0 0 0.00 15.69 disk52 2.60 0.50 15 227 0.00 1.82 disk53 2.80 0.50 19 401 0.00 1.60
Na úkolu jsem pracoval s kolegou, který je znalcem Excelu a ten se rozhodl výsledky a grafy měření zpracovat právě v něm. Já jako odpůrce Excelu jsem mu řekl, že mnohem jednodušší bude malý skriptík v perlu, trocha shellu, grepování a gnuplot.
První problém bylo nějak hezky zformátovat data, protože časová značka je vždy na jednom řádku. To se skriptem v perlu udělá poměrně snadno a i zbytek nebyl problém. Za asi 40 minut jsem měl vytvořenou automaticky vygenerovanou statickou HTML stránku i s grafy.
Jak se s úkolem vypořádal odborník na Excel: nejprve si vylámal zuby už na vstupním formátu a poté co jsem mu vypomohl s perlem vytvořený nový formát po několika hodinách dospěl k závěru. Vytvořil XSL dokument, kde se muselo poměrně značnou posloupností klikání filtrovat podle jednotlivých zařízení a generovat grafy pro každý disk zvlášť. Protože mi chtěl dokázat, že danou věc také zvládne, strávil nad tím cca 4 hodiny.
Závěr: pochlubil jsem se, že jsem fakt dobrej :), ale hlavně tím chci naznačit, že CLI přístup k řešení problému má také něco do sebe a mnohdy přináší značné ulehčení práce oproti drahým a mainstreamově rozšířeným nástrojům.
Přístup unixové filozofie, kdy veškeré konfigurační soubory a dokumenty jsou uloženy v obyčejném textu má něco do sebe. Kromě toho, že se s takovými soubory snadno pracuje, jsou přenositelné, snadno porovnatelné a editovatelné všude je také obrovskou výhodou, že při ztrátě dat, např. chybě na disku se obyčejný text dá ještě zachránit pomocí lowlevel utilit, soubor jehož formát je pro nás lidi jen binární smetí, už většinou nezachráníme. Také si uvědomte, že binární formát, vašeho momentálně oblíbeného programu může mít omezenou životnost a za pár let už si tato data nebudete schopni přečíst, protože hardware už nemáte a výrobce SW zkrachoval a váš nejnovější hyper-OS už dávno není podporován.
S některými z nich jste se pravděpodobně už setkali, nebo denně setkáváte, jiné jsou spíše okrajovou záležitostí.
Standardní anglická abeceda má 26 základních písmen, v našich končinách se navíc objevují znaku s diakritickými znaménky, ať přímo v češtině a slovenštině, tak např. různé přehlásky v cizích jménech pocházejících z němčiny nebo maďarštiny. Už od doby rozšíření počítačů tak narážíme na to, zapsat text hezky česky občas nejde bez problémů. A to je naše situace ještě mnohem jednodušší než třeba v Izraeli, arabských zemích nebo některých asijských zemí nebo Rusku.
Protože byte má jen 8 bitů, a může tak popsat jen 256 kombinací bylo nutné vytvořit vícebytové kódování: ISO/IEC 10646, které má několik variant, z nichž asi nejčastější se kterým se setkáme je UTF-8.
Příklady:
Podívejte se na reprezentaci následujícího pangramu: echo -n "Příliš žluťoučký kůň úpěl ďábelské ódy" | hexdump -C Příklady převodu: cat ANSI.ANS | recode IBM437..UTF-8 recode iso-8859-2..utf-8 soubor.txt recode utf-8..iso-8859-2 soubor.txt
Zde je příklad znaku– malé řecké písmenko alfa (α). V normě znaků Unicode je v databázi všech znaků uvedeno ke každému mnoho informací: název, kód v UTF-8, UTF-16, UTF-32, zápis jako HTML entity, případně jako kódu v HTML, do bloku jaké abecedy daný znak patří (řecká a koptská), dále že se jedná o malé písmenko a jaká je jeho varianta pro velké a zda je znak použitý při čtení z leva do prava (nebo obráceně).
Při vytváření programu nebo databáze u kterého předpokládáme mezinárodní použití je potřeba myslet na spoustu věcí, které by pak mohli způsobovat problémy.
Příklady jakým způsobem je možné pracovat s Unicode sekvencemi v různých jazycích:
stty -a # je nastaven příznak utf8? echo -e "\u03B1"; # -e pro escape sekvence echo -e "\xce\xb1" printf "\u03B1\xce\xb1\n" awk 'BEGIN { print "\xce\xb1"; }' perl -e 'print "\xce\xb1\n";' perl -e 'binmode STDOUT, ":utf8"; print "\N{U+03b1}\n";' perl -Mcharnames=:full -e 'binmode STDOUT, ":utf8"; print "\N{GREEK SMALL LETTER ALPHA}\n";'
Kódování UTF-8 je kompatibilní s ASCII. Soubory zobrazíme, i když národní znaky se zobrazí chybně, funkce pro práci s textem, jako zobrazení, spojování, přesuny, které se spoléhají na to, že řetězec je ukončen nulovým znakem pracují téměř bez potíží (pozor na přetečení!). Kde mohou vícebytové znaky způsobovat potíže? Např. funkce které pracují s délkou řetězce počítají vícebytový znak jako několik bytů.
Vyzkoušejte si:
TEXT="Příliš žluťoučký kůň úpěl ďábelské ódy" echo ${TEXT:7:9} # výběr podřetězce v BASHi žluťoučký printf "%.8s\n" "$TEXT" # výpis prvních 8 znaků (chyba!, Příli = 50 c5 99 c3 ad 6c 69 c5) Příli echo $TEXT | gawk '{ printf "%.8s\n", $0 }' Příliš ž
A nyní praktická ukázka, chceme vypsat na terminál tabulku koptských a řeckých znaků pomocí BASHe:
for i in `printf "%x " $(eval echo {$((0x370))..$((0x3FF))})` do echo -e "\u$i " done | fmt -w 30
Podívejme se na předchozí skript podrobněji. Potřebujeme zobrazit znaky, jejichž kód je v rozsahu od 0x370 do 0x3FF a potřebujeme je příkazu echo -e zadat tak, aby je byl schopen interpretovat na obrazovku, tedy \u370, \u371,...
Využijeme matematické funkce interpretované v těchto závorkách: $((...)):
echo $((1+1)) $((0xFFFF)) $((2**4)) $((10/(2+1))) 2 65535 16 3
BASH umožňuje převádět z různých soustav do desítkové. Další užitečnou vlastností jsou sekvence, které lze zadat např. takto: {1..5} nebo {a..e} nebo {1..10..2}:
echo -e {1..5} "\n" {a..e} "\n" {1..10..2} 1 2 3 4 5 a b c d e 1 3 5 7 9
Takže, jak se zachová výše uvedené echo {$((0x370))..$((0x3FF))}, vypíše na obrazovku:
{880..1023}
Tedy bez toho, aby se daná sekvence rozvinula. Zde nám pomůže příkaz eval, který nejdříve zadaný kód rozvine a pak provede. Protože sekvence je ovšem v desítkové soustavě a my jí potřebujeme v šestnáctkové, je nutné rozvinutá čísla znovu převést zpět, tady nám vypomůže printf "%x ":
printf "%x " $(eval echo {$((0x370))..$((0x3FF))})
Tyto čísla jsou pak vstupními argumenty pro for cyklus. Tak jak je to ve skriptu zadáno, tedy `printf "%x " $(eval echo {$((0x370))..$((0x3FF))})`, shell spustí subshell a vrátí vypsaná data o úroveň výše, vyzkoušejte si následující příklady:
echo Dnes je date +"%d. %m. %Y" Dnes je date +%d. %m. %Y echo Dnes je $(date +"%d. %m. %Y") Dnes je 02. 03. 2012 echo Dnes je `date +"%d. %m. %Y"` Dnes je 02. 03. 2012
Je jedno zda použijete závorky `...` nebo $(...), ovšem pokud potřebujete shelly vnořovat, je nutné použít druhý způsob.
Samotný for cykluje je jednoduchý, jeho syntaxe je: for proměnná in argumenty; do příkazy; done, také pamatujte na to, že shell při inicializaci zapisuje proměnnou bez prefixu, tedy TEXT="Ahoj", pokud chceme přistupovat k hodnotě proměnné musíme zadávat $TEXT. Zkuste si např. tento for cyklus:
for i in jedna dva tři; do echo $i; done jedna dva tři
Jako argument můžete zadat slova, čísla oddělená mezerou nebo bílým znakem. Můžete zde zadat např: * (hvězdička se rozvina na všechny soubory v aktuálním adresáři) nebo *.jpg, výše popsané sekvence či subshell, který vypisuje nějaké položky. Náš skript je tímto vlastně hotov, jeho výstup pomocí roury přesměřujeme do fmt -w 30, tato utilita zformátuje svůj vstup tak, aby měl maximálně 30 znaků na řádek.
Poznámka k načítání argumentů for cyklem: jako oddělovač jsou standardně použité bílé znaky, tj. mezera, tabulátor \t, návrat vozíku \r a nový řádek \n, pokud chceme použít jiný oddělovač, nastavíme proměnnou prostředí IFS (Internal Field Separator). Např.: IFS=$'\n', pokud chceme jako oddělovač použít pouze nový řádek.
Začneme jednou ze základních utilit pro náhradu a mazání znaků a tou je tr. Bohužel tr pracuje po bytech a tak úvodní příklad se změnou malých písmen na velká nedopadne dobře:
TEXT="Příliš žluťoučký kůň úpěl ďábelské ódy" echo $TEXT | tr '[:lower:]' '[:upper:]' PříLIš žLUťOUčKý Kůň úPěL ďáBELSKé óDY echo $TEXT | tr ž z Pz�íliz� zzluz�oučký kz�z� úpěl ďábelské ódy
Příklad z praxe. Občas se mi stává, že vykopíruji nějaký text např. z knihy v PDF nebo chci vysázet soubor původně vytvořený ve Wordu pomocí TeXu. Soubor vykopírovaný z knihy zdrojak.c. Problematické v těchto příkladech je to, že v PDF nebo Word u jednoznakových předložek, doplnily jako mezery znak nedělitelné mezery (U+00A0). Pouhým pohledem na text v editoru samozřejmě nic nepoznáme, k problémům dochází až při překladu.
gcc zdrojak.c zdrojak.c: In function ‘main’: zdrojak.c:8:1: error: stray ‘\302’ in program zdrojak.c:8:1: error: stray ‘\240’ in program zdrojak.c:8:1: error: stray ‘\302’ in program zdrojak.c:8:1: error: stray ‘\240’ in program
Kompilátor nalezl neplatné znaky, vypsal je v osmičkové notaci a skončil s chybou. Soubor opravíme následovně:
tr '\302\240' ' ' < zdrojak.c > zdrojak2.c
printf "%x %x\n" 0302 0240
sed -e 's/\xc2\xa0/ /g' zdrojak.c > zdrojak2.c
Postup pomocí sedu je podobný, ale sed vyžaduje zadat byty jako hexadecimální čísla, ty si snadno převedeme pomocí printf, pozor na to, aby printf/BASH považoval čísla za osmičková je nutné je zapsat s nulou na začátku.
Pro prohledávání obsahu souborů se nejčastěji používá program grep. Odtud pochází podstatné jméno grepování, případně sloveso grepni si :) Jako hledaný výraz můžeme zapsat přímo vzor a nebo regulární výraz (regex). Následující příklady provádí grepování, ačkoliv jsou použité nejrůznější programy:
grep 'regex' soubor awk '/regex/ { print $0 }' soubor perl -ne 'print if /regex/' soubor perl -ne '/regex/ && print' soubor sed -e '/regex/!d' soubor
Co když chceme v GNU/Linuxu najít soubor, který obsahuje nějaký vzor?
find . -name '*.txt' -exec grep -H 'regex' {}\;
Použijeme program find, zde find prohledává rekurzivně pracovní adresář, a pro každý nalezený soubor spustí příkaz zadaný za parametrem -exec, nalezené jméno souboru se zapisuje jako {} a vložený příkaz se ukončí \;. Parametr -H nutí grep vypisovat název soboru, kde nalezl vzor.
Častým problémem bývá získat z nějakého výpisu pouze sloupec s daty, která jsou pro nás důležitá. K tomu slouží několik utilit:
cut -d: -f 1,3 /etc/passwd # login a UID ls -l | cut -d' ' -f 6 # velikosti souborů ls -l | cut -b 27-35,48- | sort -rn
První příklad je jednoduchý, sloupce /etc/passwd jsou oddělena znakem dvojtečky, pro login a UID vybereme 1. a 3. sloupec. V druhém příkladu chceme získat velikost souboru z výpisu ls -l, tento příklad je chybně, víc mezer vedle sebe je považováno za jednotlivé sloupce, takže 6 sloupec má podle šířky záznamů různé pozice. Poradíme si tak, že z výpisu vybereme jednotlivé byty (třetí příkaz). Mimochodem, pro zjištění velikosti souboru není nutné parsovat výstup ls -l, ale použijeme utilitu stat, která zjisťuje potřebné informace o souboru.
Pro soubory s proměnným formátováním je vhodnější použít následující:
awk -F: '{print $1 " " $3}' /etc/passwd perl -F: -ane 'print "@F[0] @F[3]\n"' /etc/passwd ls -l | awk '{print $5 " " $9}' # problém?
Poslední příkaz je problematický v tom, že v názvu souboru se může objevit mezera, vše za mezerou se pak nezobrazí, jelikož jsme si vyžádali pouze 9. sloupec.
Jeden posluchač navrhoval použít na výběr sloupce regulární výraz a sed, řešení, které mě napadlo je následující:
ls -l | sed -e 's/.* [0-9]\+ \([[:alpha:]]\+ \)\{2\} *\([[:digit:]]\+\).*/\2/'
Jak vidíte regulární výraz je značně krkolomný, sice svůj účel plní, ale doporučuji využít jednoduší způsoby.
Jako oddělovač je možné použít i množinu znaků, zde chceme získat pouze čísla z HTML tabulky:
echo "<tr><td>120.12 Volts</td><td>3.0 Ampers</td></tr>" | awk -F"[> ]" '{ print $3 " " $6}' echo "<tr><td>120.12 Volts</td><td>3.0 Ampers</td></tr>" | perl -F"[>\s]" -ane 'print "@F[2] @F[5]\n"'
Pozor na perl, ten nebere mezeru jako oddělovač v parametru -F, musíme jí nahradit metaznakem \s, který zastupuje i další bílé znaky [ \t\n\r\f] (bude to v našem konkrétním skriptu dělat nějaké problémy?). Případně můžeme použít [:blank:] pro [ \t]: -F"[>[:blank:]]".
Neinteraktivní proudový editor slouží k dávkovému zpracování souborů, nejčastěji ho využijeme při nahrazování vzorů, ale je možné v něm připravit i další složitější editační skripty. Sed načítá příkazy z příkazového řádku -e nebo ze souboru -f. Sed pracuje tak, že nejdříve načte první řádek ze vstupu do bufferu a provede zadané příkazy v daném pořadí a vypíše výsledek na standardní výstup. Poté nahradí obsah bufferu dalším řádkem. Sed obvykle vypisuje vše co načte, také na výstup, parametr -n potlačí automatický výpis a sed pak vypisuje jen explicitně zadané. Sed nemá problém se zpracováním textů v kódování UTF-8.
Příklad, předchozí tabulku znaků převedeme do HTML formátu. Skript tbl2html.sed pro sed, který provede převod je následující:
#před 1. řádek zapiš 1i\\<table>\n<tbody> #za poslední řádek připoj $a\<\/tbody>\n<\/table> s/^/\t<tr><td>/ #začátek řádku nahraď = před řádek přidá s/$/<\/td><\/tr>/ #na konec řádku přidej s/ /<\/td><td>/g #všechny mezery nahraď oddělovačem sloupců
Poté připojíme do roury příkaz:sed -f tbl2html.sed.
Další užitečné příkazy sedu:
V této části se zaměříme na informace, které nám předává GNU/Linux prostřednictvím speciálního oddílu procfs. Zde můžeme nalézt informace o hardware, běhu OS, detailní informace o všech procesech, atd. Informace v této části nemusí být všechny podporované vaší verzí jádra, aktuální dokumentaci si zobrazíte příkazem man 5 proc.
Pozor: některé dále uvedené soubory obsahují jako oddělovač řetězců nulový znak \000, který se na terminálu nezobrazí, využijte tr, např.: cat /proc/$$/cmdline | tr '\0' '\n' | nl -v 0 zobrazí příkazový řádek právě běžícího shellu a u každého řádku pozici parametru.
Praktický příklad pro měření rychlosti síťového rozhraní. Pustíme stahování nějakého velkého souboru ze vzdáleného síťového úložiště a budeme sledovat vytížení síťové karty (kdysi jsem použil na testování silně nestabilní ADSL linky od poskytovatele SkyNet, a. s.)
#!/bin/sh INT_SEC=5 ETH_DEV=p5p1 get_packets() { awk "/$ETH_DEV/ { print \$2 }" /proc/net/dev } while true do I1=$(get_packets) sleep $INT_SEC I2=$(get_packets) echo -e $(date +%Y-%m-%d-%H:%M:%S)"\t"$(((I2-I1)/INT_SEC)) done
Pro načítání stavu síťového rozhraní využijeme /proc/net/dev, soubor obsahuje stav síťových rozhraní:
Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1026421140 7797753 0 0 0 0 0 0 1026421140 7797753 0 0 0 0 0 0 p5p1: 109098939400 107475239 0 0 0 0 0 0 58997257910 93492007 0 0 0 0 0 0
V mé distribuci Fedora Core 15 je síťové rozhraní označené p5p1, jiných distribucích to může být obvyklejší eth0, podobný výpis, ale čitelnější s údaji navíc získáte pomocí /sbin/ifconfig. V našem případě potřebujeme znát počet stažených bytů v závislosti na čase, což je 2. sloupec (bashová funkce get_packets). Všimněte si, že příkaz awk obsahuje ve svém zápisu \$2, je to proto, že potřebujeme awk předat obsah proměnné $ETH_DEV, takže kód awk není uzavřený v jednoduchých 'závorkách', ale "dvojitých", text $2 by se tak rozvinul na druhý poziční parametr z příkazové řádky, mi ho ale potřebujeme předat awk, aby vědělo, že načítáme 2. sloupec.
V hlavní smyčce skriptu se pak načítají dva údaje, mezi načteními je pauza dlouhá $INT_SEC. Obě dvě tyto proměnné jsou nastavené na začátku skriptu na jednom místě, při použití skriptu na jiném počítači je pak nutné změnit jenom jeden údaj (označení síťovky). Výpis naměřených dat a časovou značku pak provádí řádek:
echo -e $(date +%Y-%m-%d-%H:%M:%S)"\t"$(((I2-I1)/INT_SEC))
Vypisujeme datum ve formátu %Y-%m-%d-%H:%M:%S, tedy např. 2012-03-03-10:15:29. Výpočet $(((I2-I1)/INT_SEC)) je proveden v matematickém režímu shellu $((...)), všimněte si, že proměnné zde nemusí mít prefix dolar. Po spuštění skriptu získám tato data:
2012-03-03-10:22:42 254 2012-03-03-10:22:47 551 2012-03-03-10:22:52 642 2012-03-03-10:22:57 235 2012-03-03-10:23:02 165 2012-03-03-10:23:07 137 2012-03-03-10:23:12 321 2012-03-03-10:23:17 526 2012-03-03-10:23:22 265 2012-03-03-10:23:27 365 2012-03-03-10:23:32 114 2012-03-03-10:23:37 329 2012-03-03-10:23:42 292 2012-03-03-10:23:47 714 2012-03-03-10:23:52 298 2012-03-03-10:23:57 275 2012-03-03-10:24:02 2557990 2012-03-03-10:24:07 3417734 2012-03-03-10:24:12 3121275 2012-03-03-10:24:17 3471264 2012-03-03-10:24:22 597316 2012-03-03-10:24:27 135 2012-03-03-10:24:32 157
V jeden moment jsem dal stahovat:
wget ftp://ftp.sh.cvut.cz/debian-cd/debian-cd/current/amd64/iso-dvd/debian-6.0.4-amd64-DVD-1.iso
A nyní příklad, jak si zobrazit tato data do grafu, pomocí Gnuplotu:
set term dumb set xdata time set timefmt "%Y-%m-%d-%H:%M:%S" plot "data2.log" using 1:($2/1024);
3500 ++-+--+--+-+--+--+--+--+--+-+--+--+--+--+--+-+--+--+--+--+--+-+--+-++ + + + + + + "data2.log" using 1:($2/1024) +A + | A | 3000 ++ ++ | | 2500 ++ A ++ | | | | 2000 ++ ++ | | | | 1500 ++ ++ | | | | 1000 ++ ++ | | 500 ++ A ++ | | + + + + + + + + + + + + + 0 +A-+A-+A-+A+A-+A-+A-+A-+A-+A+A-+A-+A-+A-+A-+A+--+--+--+--+--+A+A-+-++ 22:40 22:5023:00 23:10 23:2023:30 23:40 23:5024:00 24:10 24:2024:30 24:40
Pro hromadné přejmenování je výhodné použít funkci BASHe pro náhradu podřetězců v proměnné, syntaxe je následující: ${proměnná/vzor/náhrada}. Vyzkoušejte si na daných příkladech.
FILE="jmeno.txt" NEW_NAME=${FILE/jmeno/name} echo $NEW_NAME name.txt
Díky tomu je přejmenování celkem jednoduché, pro jistotu použijeme mv -i, kdy se při výskytu stejného cílového názvu mv dotáže, zda chceme soubor přepsat:
for i in *; do mv -i $i ${i/jmeno/name}; done
Pokud naše názvy jsou složitější (je nutné využit složitejší regex) můžeme využít sed, zde je předchozí skript s jeho použitím:
for i in *; do new=$(echo $i | sed -e 's/jmeno/name/') mv -i $i $new done