Grep och regular expressions

Från Unix.se, den fria unixresursen.

Grep är ett av de allra mest användbara verktygen man har tillgång till i en unixmiljö, och inte minst så öppnar grep-kunskap dörrarna till mer avancerade saker som sed, awk och perl. Reguljära uttryck (regexps) är mycket flitigt utnyttjade i Unix och kan förmodligen användas i din favorittexteditor, osv. Det är lite pilligt i början, men tar man lite i taget ska nog alla kunna lära sig det här, så svårt är det inte.

Vad är då grep? Manualen ('man grep') säger oss:

grep (1) - print lines matching a pattern

Kort sagt: grep söker. Vad har grep för syntax då? Vi konsulterar än en gång manualen:

grep [options] PATTERN [FILE...]

Skälet till att FILE står inom hakparenteser tar vi senare. Låt oss nu bara konstatera att:

grep 'sträng' fil(er)

används för att söka i filer.

Några exempel:

Söker igenom /etc/passwd, dvs filen som innehåller information om alla användare på datorn, och skriver ut alla rader som innehåller /bash, i praktiken de som har valt bash som shell:

grep '/bash' /etc/passwd

Söker igenom Kalles avhandling efter ordet zebra:

grep 'zebra' /home/kalle/avhandling.doc

Här bör vi dock lägga märke till att Kalle kanske lät ordet zebra stå först i en mening någonstans i dokumentet. Om så är fallet hittar inte sökningen efter 'zebra' alla zebror. Vi behöver växeln (eng. option) -i, ignore-case (behandla stora och små bokstäver som lika). Nu ger oss:

grep -i 'zebra' /home/kalle/avhandling.doc

alla rader med zebror i kalles text.

Det här är ju fint och användbart, men bara att få radnumret och raden utskriven kanske inte alltid hjälper oss så mycket. Vi vill ibland se sammanhanget i vilket kalle använde ordet 'zebra'. Då använder vi växlarna -A och -B (after resp. before), som visar ett antal rader före resp. efter varje träff (i det här fallet för ordet 'zebra'). Låtom oss ge ett exempel:

Jag har nyss installerat nån sorts UNIX och konfigurerat X, när jag upptäcker att scrollhjulet verkar scrolla tvärtemot hur det ska i X. Eftersom jag inte riktigt vet vad som är felet beslutar jag att grep:a efter 'mouse' i X:s konfigurationsfil, som på min burk heter /etc/X11/XF86Config. Eftersom jag vill ha sammanhang, och jag inser att 'mouse' kan stå med stora bokstäver på några ställen använder jag båda växlarna vi gått igenom:

grep -A 2  -B 2  -i   'mouse'   /etc/X11/XF86Config

och får svaret:

Section "InputDevice"
Identifier  "Mouse1"
Driver      "mouse"
Option "Protocol"    "IMPS/2"
Option \"Device\"      "/dev/mouse"
Option "ZAxisMapping" "5 4"
EndSection
--
# "SendCoreEvents".
InputDevice "Mouse1" "CorePointer"
InputDevice "Keyboard1" "CoreKeyboard"

och nu ser jag naturligtvis att jag skrivit fel, så jag går in i min texteditor och ändrar "5 4" till "4 5" (eller använder sed, men det blir i en senare artikel).

Ett till exempel:

grep -A 2 '^kalle' /etc/passwd

för att se vilka users som lades till precis efter kalle (om inte någon sorterat eller på annat sätt ändrat ordningen på raderna, såklart) (circumflexet förklaras senare)

Att använda grep på det här sättet (söka igenom filer) är bara ett av de två sätt man kan använda det. Det andra är att grep:a igenom outputen från ett annat program. För att göra det länkar vi på kommandoraden ihop programmen med ett | (pipe), och skriver naturligtvis inget filnamn efter grep (däremot kan jag naturligtvis använda växlarna -B, -A och -i (med flera)). Exempel:

Jag vill veta var alla partitioner på min primära slave IDE disk är monterade, så jag använder kommandot mount tillsammans med grep:

mount | grep '/dev/hdb'

och får svaret:

/dev/hdb4 on / type ext3 (rw,noatime)
/dev/hdb1 on /stuf type ext2 (rw)

I det här fallet (på min hemdator) hade 'mount' inte gett mig mer än ett par raders output, så det var onödigt att grep:a egentligen, men ni förstår principen.

Ett måhända mer användbart exempel:

ls -lF|grep /

Växeln -F gör så att en slash (/) appenderas till alla katalognamn i listan. Kommandot visar alltså alla underkataloger till katalogen du står i.

xdpyinfo är också ett användbart program som genererar lång output (för er som använder X), här är ett exempel:

xdpyinfo | grep 'dimensions'
dimensions:   1280x1024 pixels (433x347 millimeters)

Regexps

Nu undrar ni kanske om det här är allt grep kan användas till, och var Regular Expressions (från nu 'regexps' (RE kort ibland)) kommer in i bilden. Jag kommer till det, men först lite historia.

Regexps har en lång historia, som börjar med neurofysiologer på 40-talet som behövde ett sätt att beskriva mönster av elektiska signaler i neurala nätverk. Sedan dess har det naturligtvis insetts att inte bara AI-forskare, utan även vanliga människor, behöver mönstermatchningsfunktioner. Därför anammade UNIX-världen regexpen, och integrerade dem i sina verktyg. Du kan till exempel knappt hitta en vettig texteditor idag som du inte kan söka igenom text i med regexps. Det främsta, mest avancerade exemplet på användningen av regexps idag är förmodligen scriptspråket Perl, men även C(++), TCL, Python, Java, JavaScript, VBScript och PHP använder dem. Kort sagt, lever du i Unixvärlden kommer du utan tvekan komma i kontakt med regexps, och de kan göra din vardag lite lättare.

Det finns dock även problem med att regexps är så utbrett. Då de till stora delar utvecklats separat i anslutning till verktygen som använder dem finns det idag många olika sorters regexps. Ett som fungerar i Perl behöver ingalunda fungera i sed eller vim. Därför tänker jag här endast gå igenom det grundläggande, som är gemensamt för alla (eller åtminstone de flesta) sorterna, samt en del specifikt för grep.

Det finns specifika växlar i de flesta implementationer av grep för olika sorters regexps. Så har jag exempelvis i min version (GNU grep 2.5):

Regexp selection and interpretation:
-E, --extended-regexp     PATTERN is an extended regular expression
-F, --fixed-strings       PATTERN is a set of newline-separated strings
-G, --basic-regexp        PATTERN is a basic regular expression
-P, --perl-regexp         PATTERN is a Perl regular expression

Jag använder så kallade extended regular expressions. I en del unixmiljöer fungerar exemplen nedan, i andra kan "grep -E" eller \"egrep\" behöva användas (skillnaden är framförallt att (){} måste escape:as om ni vill ha deras speciella betydelser när ni använder basic regexps. Extended regexps är standard i egrep och perl, basic regexps är standard i sed, grep och awk. Lär er båda syntaxarna.).).

Då så, hur fungerar nu regexps? Först och främst konstaterar vi att alla strängar är regexps. \'baa\' är ett regexp som matchar allt som innehåller de tre bokstäverna i den ordningen, osv.. Det finns också specialtecken i regexps, som inte matchar de tecken de ser ut som, utan ger ökad funktionalitet och flexibilitet till uttrycket.

Första specialtecknet: '.'. Punkten står i RE-sammanhang för "vilket enskilt tecken som helst" Så grep 'h.j' /home/kalle/avhandling.doc matchar alla rader med orden 'hej', 'haj', 'hoj', eller till och med 'hqj', men inte 'haaj' (vilket _enskilt_ tecken som helst) eller 'hab' (matchar inte 'h.j' alls). Ett exempel:

grep '/bin/.sh' /etc/passwd

visar alla som använder /bin/ash, csh, ksh, zsh, osv som shell, men inte de som använder bash (. matchar som sagt bara ett tecken). Nästa specialtecken: '^'. Circumflexet står i (vissa) RE-sammanhang för 'inte'. Exempel:

grep '[^a]' /etc/passwd

(hakparenteserna förklaras strax) visar alla rader i /etc/passwd som matchar "inte a", dvs alla rader som innehåller nåt som inte är ett a. Det gör sannolikt de flesta, så det var inte riktigt vad vi ville. Därför introducerar jag nu en till grep-växel, nämligen -v, som står för inVert match, dvs matcha de rader som _inte_ matchar. Det lät lite invecklat, så vi tar ett exempel:

grep -v 'a' /etc/passwd

visar alla rader som inte innehåller något a.

Nu till specialtecknen [ och ]. De indikerar att det finns flera alternativ för vad som matchar mönstret. Exempelvis [ab] matchar a eller b, [aouåeiyäö] matchar alla vokaler (små bokstäver enbart) och så vidare. Man kan också använda '-' för att ange hela skalor tecken, exempelvis [a-z] (alla små bokstäver), [A-Za-z] (alla bokstäver) osv.

Exempel:

grep '/bin/[cz]sh' /etc/passwd

visar sannolikt de användare som använder csh eller zsh som shell, men inte de som använder bash eller sh. Här kunde man alltså använda tildetecknet för "inte" som jag nämnde tidigare, vi tar några exempel:

grep '/bin/[^cz]sh' /etc/passwd

Visar alla users som inte använder csh eller zsh.

mount|grep 'hd[ab]'

Visar var partitionerna på de primära IDE diskarna i min burk är mountade

mount|grep 'hd[ab][^6]'

Samma sak, men jag vill inte se var hda6 eller hdb6 är mountade..

Nästa specialkombination: {a,b}. Måsvingarna (skyll inte på mig, inte jag som kom på ordet) signifikerar att ett mönster upprepas. Ex: [abc]{6} matchar en kombination på 6 tecken av bokstäverna a, b och c, exempelvis aabcab, cababc, men inte abcdab eller abcba (otillåtet tecken resp. för kort). Man kan även ange ett minimi- och ett maximiantal. Exempelvis så här: ba{1,5} matchar (notera att det inte är några hakparenteser runt 'ba', så upprepningen gäller bara 'a'.) baa, baaaaa och ba, men inte b.

grep 'ba{1,5}b' fil.txt

Skulle alltså matcha bab, baab och baaaaab, men inte baaaaaaaaaaab, eller bb. Vissa kombinationer av {a,b}-värden är såklart mycket vanligare än andra, exempelvis "matcha ett eller fler x" och dylikt.. för dessa infördes ytterligare specialtecken:

? motsvarar {0,1} + motsvarar {1,} (utelämnas andra värdet antas att det är positiva oändligheten, utelämnas första värdet antas att det är 0, så det här blir "1 eller fler tecken")

  • motsvarar {0,} (det här blir analogt "0 eller fler tecken")

Den observante läsaren ser att man kan använda regexpet '.*' för att matcha "noll eller flera vilka tecken som helst", dvs vad som helst. Lägg det på minnet, det kommer ni ha nytta av framöver. (Man kan givetvis också analogt med {a,} syntaxen skriva {,b} och mena "färre än b tecken")

Exempel på detta:

grep '/bin/t?csh' /etc/passwd

Matchar alla som antingen använder csh eller tcsh. (? är samma sak som {0,1})

grep '/bin/.*sh' /etc/passwd

Matchar alla som använder ett shell som slutar på sh, dvs de flesta populära shells (och en hel del opopulära).

Två byggstenar kvar, ( ) och |. Parenteserna används för att gruppera ihop flera deluttryck, och kommer till störst användning tillsammans med |, som betyder 'eller '. '(en/nos)hörning' matchar både enhörningar och noshörningar. 'blå(klocka|klint)' matchar båda de uppenbara alternativen.

grep '/bin/(ba|tc|z)sh' /etc/passwd

Visar alla users som använder någon av de populära shellen bash, tcsh och zsh. Sen har vi specialtecknen '$' och '^' (som betyder nåt helt annat när det står utanför []). Dessa står för början respektive slutet av en rad. Man kan alltså t.ex. göra:

grep '^p' /etc/passwd

För att hitta alla users som börjar på 'p'. Som så ofta annars i unixvärlden, vill ni matcha nåt av de här specialtecknen (exempelvis $) får ni escape:a det genom att prefixera det med en backslash () (för att söka efter en backslash används så klart ).

grep '$' /home/kalle/avhandling.doc

Hittar alla rader med dollartecken på i Kalles avhandling. Ytterligare specialtecken:

b tom sträng vid kanten av ett ord
B tom sträng som inte är vid kanten av ett ord
< tom sträng vid början av ett ord
> tom sträng vid slutet av ett ord
w tecken som kan ingå i ord (synonym för [[:alnum:]]
W tecken som _inte_ kan ingå i ord (synonym för [^[:alnum:]])

Exempel:

'bko' matchar "ko" och "kossa", men inte "kaffekok"
'koB' matchar "kossa" och "kaffekok", men inte "ko"
'kob' matchar "ko" och "inlandsko", men inte "kaffekok" eller "kossa"

Till sist då till det som av många uppfattas som bland det knövligaste när det gäller regexps, bakåtreferenser. Varje gång grep matchar någonting mot en del av ett regexp som är inneslutet i parenteser sparas texten som matchade i en speciell variabel, # (där # är siffran som står för vilket deluttryck i ordningen av regexpet som matchades). Exempelvis i:

grep '([abc])' fil

så kanske grep hittar ett b i filen. Då sparas 'b' i 1. Detta kan vi utnyttja. Studera följande:

grep '([abc])1' fil

Det här matchar någon av bokstäverna a, b eller c, förutsatt att den följs direkt av 1, dvs samma bokstav igen. Vi använder alltså här bakåtreferenser för att hitta dubblerade bokstäver.

Ytterligare ett par växlar till grep som kan vara bra att känna till:

-C x gör samma sak som -A x -B x, visar x rader i varje matchs omgivning -x, -w gör att enbart regexps som matchar en hel rad respektive ett helt ord matchar -r (recursive) gör att grep gräver igenom alla underkataloger också. Detta är användbart i konstruktioner som: grep -r 'sshd' /etc/*

För fullständighetens skull, POSIX Teckenklasser:

Metoden att uttrycka teckenskalor [a-z] är inte felfri. Vissa internationella (icke-amerikanska) teckenuppsättningar sorterar tecknen i alfabetisk ordning.. AaBbCcDd, osv.. Om du på ett sådant system skriver [a-d] kommer det vara motsvarigt med "aBbCcDd", vilket inte är vad du vill. För att lösa detta problem introducerades i POSIX-standarden teckenklasser som bör användas därhelst det är möjligt. Dessa är:

[:alnum:]
alfanumeriska tecken (ie. [:alpha:] och [:digit:])
[:alpha:]
alfabetiska tecken (ie. [:lower:] och [:upper:])
[:blank:]
tomrum, dvs mellanslag och tabbar
[:cntrl:]
Control-tecken, dvs de med ASCII-koder mellan 000 och 037 och 0177 (delete).
Det här är tecken som escape, backspace, delete, osv..
[:digit:]
siffror, dvs [0123456789]
[:graph:]
\"grafiska\" tecken (ie. [:alnum:] och [:punct:])
[:lower:]
små bokstäver, dvs abcdefghijklmnopqrstuvwxyz
[:print:]
utskriftsvänliga tecken (ie. [:alnum:], [:punct:] och mellanslag)
[:punct:]
punktuation, samma som !"#$%&\'()*+,-./:;=>?@[]^_`{|}~'
[:space:]
tomrum, dvs mellanslag, tab, osv..
[:upper:]
stora bokstäver, dvs i engelskan ABCDEFGHIJKLMNOPQRSTUVWXYZ
[:xdigit:]
hexadecimala siffror: 0123456789AaBbCcDdEeFf

Andra användbara växlar, som dock bara finns i GNUs mycket förbättrade version av grep (och alltså inte är standard, dvs ingenting man ska förlita sig på i script eller dylikt) är --color och -o. "--color" markerar den del av de matchande raderna som faktiskt matchade regexpet, emedan "-o" visar den matchande delen av raden. Exempel:

Givet en textfil med innehållet:

foo foo.foo
foo.foo foo

visar

grep --color '\.foo'

(punkten måste quotas för att undvika att den tolkas som "valfritt tecken") ".foo"-delen av varje rad i en från resten av texten avvikande färg.

% echo "foo foo.foo
foo.foo foo"  |  grep -o "\.foo"
.foo
.foo

Nu kommer några grep-exempel från verkliga livet. Försök komma på vad de gör!

ls -l |grep '^d........x'
ls -l |egrep '^d.{8}x'
ls -l |grep '^d.{8}x'
grep -i 'href="[^\"]* [^"]*"' *.html
grep -v \'^#\' /etc/inetd.conf
ls -F|grep /

Läs mera om grep

man grep info grep (för system med GNU Info, som GNU/Linux och GNU Hurd)

Den bästa källan till kunskap är dock att prova själv, våga experimentera och framförallt vara _medveten_ om att sådana här verktyg finns. Fråga gärna på forumet eller i IRC-kanalen om det är nåt ni undrar över eller om ni behöver hjälp med något.

Personliga verktyg