AWK

Från Unix.se, den fria unixresursen.

AWK är ett tolkat (interpreterat) programspråk för att behandla textdata och har fått sitt namn efter de ursprungliga upphovsmännen Alfred V. Aho, Peter J. Weinberger och Brian W. Kernighan. Det karaktäriseras av en C-lik syntax, en associativ datatyp och reguljära uttryck. Se även Perl.

Innehåll

Introduktion

Vad är Awk? Manualen (man awk) säger oss:

awk - pattern scanning and processing language

Awk är alltså ett skriptspråk som används för att hämta data från strukturerade textfiler och presentera dem på ett snyggt sätt. Först som sig bör lite historia: Det hela började 1977AT&T:s Bell Labs, samma ställe som Unix skapades på, med Alfred Aho, Peter Weinberger och Brian Kernighan, programmerarna vars initialer gav språket awk dess namn. Enligt dem själva är det en blandning av egrep, snobol, ed och c, vilket jag tycker sammanfattar det hela ganska bra.

Verktygen de använt för att skapa språken är de för C-programmerarna så välkända lex och yacc. Den ursprungliga versionen hade inte användardefinierade funktioner, multipla indataströmmar eller reguljära uttryck, vilket lades till 1985. Andra milstolpar i awks historia är 1986, då GNUs version, gawk, färdigställdes, samt 1997, då nätverksmöjligheter introducerades i gawk. Användningen av språket idag har förmodligen minskat ganska mycket i och med att Perl vuxit otroligt starkt. Perl är ett annat skriptspråk som ursprungligen utformades för att lösa ungefär samma problem som awk.

Varför använda Awk istället för Perl?

Det här var för bra för att avstå från att klistra in: (från comp.lang.awk)

  • awk is simpler (especially important if deciding which to learn first)
  • awk syntax is far more regular (another advantage for the beginner, even without considering syntax-highlighting editors)
  • you may already know awk well enough for the task at hand
  • you may have only awk installed
  • awk can be smaller, thus much quicker to execute for small programs
  • awk variables don't have `$' in front of them :-)
  • clear perl code is better than unclear awk code; but NOTHING comes close to unclear perl code

Framförallt är det viktigt att inse att Awk ingalunda är det mest kraftfulla verktyget för sådana här uppgifter, men det är förmodligen ett av dem som det går snabbast att hacka fram en lösning med. Det finns många saker som är smidigare att göra i Awk än i Perl, så det lönar sig förmodligen att kunna lite om det. Vidare finns (faktiskt) Perl inte på alla datorer (floppybaserade linuxdistributioner kommer jag att tänka på genast), emedan Awk i princip finns överallt (Awk krävs till och med för att kompilera Linuxkärnan).

Syntax

En vanlig Awk-sats ser ut så här:

villkor {satser som ska utföras om raden matchar villkoret}

Där villkoret kan vara ett reguljärt uttryck (läs även grep och regular expressions) som raden som läses in ska överensstämma med, eller i princip vilka andra vanliga programmeringskonstruktioner som helst (en genomgång av Awkkommandon och fördefinierade variabler kommer). Vill man ha flera satser separerar man dem med semikolon (Awk lånar mycket från C). Satserna som ska utföras innefattar ofta nån variant av print eller printf. Ett enkelt exempel är:

/foo/ {print}

som skriver ut alla rader som innehåller texten "foo".

Awk går igenom indatafilerna (eller STDIN om ingen anges, detta innebär att man kan pipe:a utdatat av andra program till Awk precis som vi gjorde med grep) rad för rad och kollar på alla villkor i scriptet, kollar om någon av dem stämmer och utför i sådana fall de efterföljande satserna.

Anrop

awk -f «skriptfil» «indatafil»

eller

awk 'programsatser' «indatafil»

Istället för en indatafil kan vi använda STDIN, då oftast genom att pipe:a STDOUT från något annat program till Awk, precis som vi gjorde med Grep i förra artikeln.

Fältuppdelning

En av de saker som är så bra med Awk är att det automatiskt delar upp raderna den läser in i fält. Låt oss ta som exempel password-filen, på mitt systems /etc/ passwd (bör funka på de flesta system folk faktiskt använder, men man vet aldrig). Den ser ut så här:

guest:x:405:100:guest:/dev/null:/dev/null
nobody:x:65534:100:nobody:/:
peanut:x:1000:100::/home/peanut:

Vi ser att fälten är separerade med kolon (:). Detta anger vi genom att sätta den variabeln FS till : medelst:

FS=':'

eller på kommandoraden:

awk -F ':' «resten av kommandoraden»

För att nu komma åt de där fälten definierar awk speciella variabler åt oss vid namn $1, </code>$2, ..., </code>$9</code>. För att lista alla användare på datorn använder vi alltså:

awk -F : '{print $1}' /etc/passwd

Vi kan givetvis använda dessa variabler i villkoret också. Exempelvis visar det här alla roots rad i passwd-filen:

awk -F : '$1 == "root" {print $0}' /etc/passwd

($0 betyder hela raden)

Eller på båda ställena.. vi ser att användarens skal är i fält 7. Det här visar roots skal:

awk -F : '$1 == "root" {print $7}' /etc/passwd

Ytterligare en speciell variabel som definieras av awk är NR, som innehåller radnumret för raden den håller på att processera. De speciella villkoren BEGIN och END definieras också för att utföra saker innan respektive efter processeringen av texten börjar. Man kan också ange två mönster separerade av kommatecken för att matcha alla rader mellan den första förekomsten av det första och den sista förekomsten av det andra. Ett exempel klargör:

awk -F : '$1 == "guest", NR==10 {print}' /etc/passwd

(printar alla rader mellan den vars första fält är "guest\" och rad 10.)

awk -F : 'BEGIN {print "Det här är password-filen:"}; {print}' /etc/passwd

(printar först (BEGIN) ett kort meddelande, sen varje rad i inputfilen)

Vi kan givetvis pipe:a outputen av awk vidare till andra program. Betrakta följande:

ps x | awk '$5~/mozilla/ {print $1}' | xargs kill

där operatorerna ~ och !~ betyder matchar respektive inte matchar. Den där awk:en betyder alltså: om fält 5 (kommandoraden i min version av ps) matchar /mozilla/ printa fält 1 (PID i min version av ps) och skicka utdata till xargs, ett användbart litet program som gör om STDIN till en command line och kör den. I det här fallet utförs alltså kommandot kill (som dödar en process) med mozillas pid som argument. Resultatet blir att alla mozillor du startat dör. Xargs var naturligtvis inte nödvändigt att använda; vi kunde använt en funktion i bash och skrivit:

kill $(ps x| awk '$5~/mozilla/ {print $1}')

istället, men det är mindre lättläst i min mening. (kill `ps x| awk '$5~/mozilla/ {print $1}'` hade också gått, men det är ännu mindre lättläst)

Fler operatorer

I villkoren kan vi booleska operatorer med ungefär samma syntax och betydelse som i C/Perl:

&& betyder och
|| betyder eller
! betyder inte
> större än
< mindre än
<:= mindre än eller lika med
>= större än eller lika med
== lika med

Till exempel, för att visa alla användare med UID över eller lika med 1000:

awk -F : '($3 >= 1000) {print $1}' /etc/passwd

printf()

För mer precist definierad output kan man använda printf() med samma syntax som i C, d.v.s

printf("format-sträng", uttryck1, uttryck2, ...);

Där formatsträngen består av vanlig text plus speciella former där nåt värde ska sättas in. Dessa har formen:

%«flaggor»«min fältvidd»«precision (för flyttal)»«längd»typ

där typ oftast är d (heltal), f (flyttal) eller s (sträng). Några exempel:

awk -F : '{printf("%20s har user-id %dn",$1,$3);}' /etc/passwd

skriver först ut $1 (användarnamn) i ett 20 rutor brett fält, sedan strängen "har user-id" följt av $3 (UID). Notera att det här till skillnad mot vid användning av "print" är viktigt att inte glömma n (newline, radbyte) i slutet, då allt annars kommer på samma rad. Detta kan vi använda för att printa en fil med dubbla radbytningar så här:

awk '{printf("%snn\",$0);}' /path/to/fil

För mer info om hur printf fungerar refererar jag er till printf(3) (skriv man 3 printf). I de flesta fall klarar man sig fint med de ovan nyttjade simpla fallen eller "print".

Flödeskontroll (Flow Control)

Vi börjar med grundläggande if-satser:

if (villkor)
«satser som ska utföras om villkoret stämmer»
else
«satser som ska utföras annars»

Ska flera satser in på samma rad separeras de med semikolon. Vill man få plats med flera satser i ett block omger man dem med { och } (precis som i C).

Exempel:

if ($0 ~ /^(foo)*/)
{
  print "foo" NR;
}
else
  print "nofoo"

Iteration (loopar)

Inget programmeringsspråk skulle vara komplett utan ett sätt att upprepa satser (antingen det är iteration eller rekursion). Ni som har programmerat förut (framförallt ni som kommit i kontakt med C) kommer finna att:

while (villkor)
{
satser
}

fungerar precis som man förväntar sig (utför satserna inom { } om villkoret är uppfyllt, så länge det är uppfyllt) , liksom

do
{
satser
} while (villkor)

(Upprepar satserna och kollar så att vilkoret är uppfyllt efter varje iteration. Utför satserna minst en gång.) Ett exempel:

{   i = 1
    while (i <= 10) {
    print $0      
    i++
    }
}

Det här printar varje rad i inputfilen 10 gånger. För sådana här simpla upprepningar finns naturligtvis precis som i C for:

for («init»; «villkor»; «ökning»)
    satser

där init-delen normalt används till att initialisera loop-räknaren (dvs är väldigt ofta nåt i stil med "i=0"), villkoret är till för att loopen ska avslutas nån gång (är väldigt ofta nåt i stil med i<=10) och ökningsdelen normalt används till att öka loopräknaren (i++, vilket är kort form för <code>"i = i + 1"). Så här ser loopen ovan ut med en for:

{
    for (i = 1; i <= 10; i++)
        print $0
}

Andra användbara saker man kan göra med loopar:

{
    for (i=NF; i>0; i--)
        printf("%s ",$i)
    printf("n")
}

Det här exemplet stegar igenom alla fält i varje rad och skriver ut dem omvänd ordning. Vi hade naturligtvis kunnat skriva det som en while-loop istället, men for är smidigast i det här fallet (liksom de flesta andra där man vill stega igenom nåt med känd längd). Vi ser också att man inte behöver använda tal för fältindex, utan kan använda vilka uttryck som helst. Man kan alltså göra konstruktioner som $(i+2*a) och så vidare..

Kombinerar vi exemplet ovan med FS får vi en one-liner som skriver ut alla rader baklänges:

awk -F  '{for(i=NF;i>0;i=i-1) printf("%s",$i); printf("n")}' /etc/passwd<

(Sätter man FS till (tom sträng) får man som ni ser ett fält för varje tecken. Exprerimentera med det här om det inte är helt klart.)

Variabler

Någonstans i det förra stycket smög sig visst ett par variabler in, så det är väl bäst att dedikera det här stycket åt dem.

Awk är dynamiskt löst typat, vilket innebär att variabler varken behöver deklareras till en specifik typ innan de används eller att variabler måste behålla samma typ under hela dess livslängd. Alla variabler kan tolkas både som strängar och tal, och hurvida operatorer som == jämför strängar eller tal beror på sammanhanget. Kortfattat kan man säga att variabler fungerar precis som i Perl, förutom att man inte behöver prefixera dem med dollartecken.

Det finns också ett antal predefinierade variabler. Några av dem har ni redan stött på, som NR och FS. Här kommer en lista på de användbaraste:

Variabler du ändrar för att påverka awks beteende:

  • FS (field separator) - det som separerar fälten. Sätter man FS till (tom sträng) så kommer varje tecken i ett eget fält. Kan till skillnad från perls motsvarighet vara ett regexp, vilket kan vara väldigt praktiskt ibland.
  • RS (record separator) - det som separerar varje record. Normalt (och allra, allra oftast) newline (n), men det går att ändra.
  • IGNORE-CASE - kan vara bra ibland, men vanligast är nog att använda tolower() för att konvertera strängar som ska undersökas till små bokstäver.

Variabler awk ändrar:

  • ARGC - antalet argument som skickades till awk på kommandoraden
  • ARGV - ett array innehållande de argument som skickades till awk på kommandoraden
  • FILENAME - namnet på filen awk läser för tillfället
  • (F)NR - nuvarande record respektive antalet records inlästa hittills. Enda skillnaden mellan de två är att FNR nollställs när du öppnar en ny fil, vilket NR inte gör.
  • NF - antalet fält i raden (recordet egentligen) awk processerar just nu.

TODO: Fixa exempel.

Arrays

De enda arrays som finns i awk är associative, vilket innebär att både tal och strängar kan användas som index, samt att talen inte behöver vara i ordning. Man kan till exempel ha värden för a[1]<code> och <code>a[5] utan att ha definierat nåt värde för a[3]. Nya element kan skapas dynamiskt genom att bara tilldela ett värde till ett dittills orört element. Precis som i Perl och C kommer man åt elementen i en array med arr[index], där index i awk kan vara en sträng eller ett tal. Exempel:

{ stort_array[NR] = $0 }
END {
«gör nåt med arrayen»
}

..som läser in hela inputfilen i ett array, för manipulation senare. Eftersom man till skillnad från i C eller Perl inte vet vilka indexvärden i arrayen som har värden finns det en speciell for-konstruktion som stegar igenom alla definierade värden i ett array (ni som meckat med nåt annat skriptspråk kommer känna igen det här):

for («loop-variabel» in «array»)
    satser
Multidimensionella arrayer fungerar likadant:
for ((«loopvariabel 1,2,3») in «array»)
    satser

Ett exempel på multidimensionella matriser. Det här lilla scriptet tar en tvådimensionell matris från inputfilen och roterar den 90 grader medsols:

     {
          if (max_nf < NF)
               max_nf = NF
          max_nr = NR
          for (x = 1; x <= NF; x++)
               matris[x, NR] = $x
     }
    
     END {
          for (x = 1; x <= max_nf; x++) {
               for (y = max_nr; y >= 1; --y)
                    printf("%s ", matris[x, y])
               printf("n")
          }
     }

Hur fungerar då detta?

Först stegas varje rad i inputfilen igenom (första blocket) och det maximala antalet fält i varje rad samt antalet rader noteras. Hela inputfilen läses också in i en matris.

När hela inputfilen stegats igenom loopas hela matrisen igenom och alla element skrivs ut med rad- och kolonnindex ombytta. Resultatet är att matrisen roteras 90 grader i negativ riktning.

Man kan även helt ta bort element ur matriser med

delete «array[index]»

eller hela matriser:

delete «array»

(Delete fungerar även för skalärer, men där är det väl lite mer tveksamt om det är användbart.)

Externa länkar

Personliga verktyg