4.4. Jokertegn, omdirigering og kanaler

Vi vil i dette afsnit se på et par af systemets mere smarte funktioner, som gør livet lettere for dig som bruger.

4.4.1. Jokertegnene

Forestil dig, at du har mange filer, som du gerne vil slette. Du kan naturligvis skrive rm den_ene_fil den_næste_fil den tredje_fil (osv.) med hvert enkelt navn, men bare med 5-10 filer bliver du hurtigt træt af det. Redningen er jokertegnene * og ?.

Lad os antage, at du har tre filer i et katalog, og at du gerne vil slette dem alle. For øvelsens skyld opretter vi først filerne med touch-kommandoen og sletter dem derefter. Nedenfor ser du hvordan:

[tyge@hven MitKatalog]$ touch aaa abc ccc
[tyge@hven MitKatalog]$ ls
 aaa  abc   ccc  
[tyge@hven MitKatalog]$ rm *
[tyge@hven MitKatalog]$ ls
[tyge@hven MitKatalog]$ 
Asterisken (*) er et jokertegn som her betyder alle filer. Hvis du i stedet havde skrevet a*, ville det betyde alle filer som begynder med a.
[tyge@hven MitKatalog]$ touch aaa abc ccc
[tyge@hven MitKatalog]$ ls
 aaa  abc   ccc
[tyge@hven MitKatalog]$ rm a*
[tyge@hven MitKatalog]$ ls
 ccc
[tyge@hven NytKatalog]$ 

Asterisken (*) betyder helt generelt nul eller flere tegn i et filnavn. For eksempel betyder a*b alle filer, der begynder med a og slutter på b, hvilket vil inkludere filerne ab, aDuErGodb, men ikke en fil med navnet abe.

Ligesom asterisken fungerer spørgsmålstegn (?) også som jokertegn. Et spørgsmålstegn står for et (og netop et) vilkårligt tegn i et filnavn.

Eksempel 4-1. Kombination af flere jokertegn

Som et eksempel på hvordan jokertegnene kan kombineres med hinanden beder vi ls om at give os en liste med alle de filer i kataloget /usr/bin, hvis tredje bogstav er et z:

[tyge@hven MitKatalog]$ ls /usr/bin/??z*
/usr/bin/mozilla  /usr/bin/size86  /usr/bin/unzip
/usr/bin/size     /usr/bin/tgz     /usr/bin/unzipsfx

Da det er kommandofortolkeren og ikke de enkelte kommandoer der tager sig af at fortolke jokertegnene, kan man bruge præcist de samme mønstre til at angive samlinger af filer til alle programmer på systemet.

Et meget simpelt eksempel: Et katalog indeholder 3 filer: fil1, fil2 og ccc. Skriver du: ls f*, vil kommandofortolkeren først ekspandere f* og derefter kalde ls med: ls fil1 fil2 . Output fra ls f* er blot:

[tyge@hven MitKatalog]$ touch fil1 fil2
[tyge@hven MitKatalog]$ ls
ccc   fil1   fil2
[tyge@hven MitKatalog]$ ls f*
fil1   fil2
og ls laver således et yderst banalt arbejde.

Men der er flere muligheder i dette. Et eksempel: For at se forskellen mellem fil1 og fil2 kan du skrive: diff fil1 fil2. Men du kan også nøjes med at skrive: diff fil? og så lade kommandofortolkeren regne ud at f* betyder fil1 fil2. Da de to filer er ens (de er begge tomme), så er der ingen forskel, og derfor intet at vise:

[tyge@hven MitKatalog]$ diff f*
[tyge@hven MitKatalog]$ 

Somme tider kan denne konsekvente ekspansion af jokertegn dog være en ulempe. For eksempel: Et katalog indeholder 2 filer:

[tyge@hven film]$ ls
dogme95.zip      film_index.html

Du kan også bruge zip-filer under Unix, og du kan se indholdet af zip-filen ved f.eks. at skrive:

[tyge@hven film]$ unzip -v d*
Archive:  dogme95.zip
Length Method Size Ratio Date    Time   CRC-32   Name
------ ------ ---- ----- ----    ----   ------   ----
 14853 Defl:N 5224 65%  10-27-98 16:12 944a4af4 festen.html
 14844 Defl:N 5401 64%  11-14-98 19:53 e55c1636 idioterne.html
  1941 Defl:N 1024 47%  03-12-99 22:12 5ecb7d23 mifune.html
------       ----- ---                           -----
 31638       11649 63%                          3 files

Nu vil vi pakke festen.html ud af zip-filen. Det er den eneste fil i zip-filen, der starter med f, så under DOS kunne man blot skrive: unzip d* f*. Men prøver man det under Unix, går det galt:

[tyge@hven film]$ unzip d* f*
Archive:  dogme95.zip
caution: filename not matched:  film_index.html

Hvorfor det? Fordi der i kataloget i forvejen ligger en fil, der matcher f*, så vil kommandofortolkeren ekspandere både d* og f* og kalde unzip med: unzip dogme95.zip film_index.html.

Du bliver som minimum nødt til at skrive: unzip d* fe*. Der er ingen fil i kataloget, der matcher fe*, så kommandofortolkeren vil kun ekspandere d* og kalde unzip med: unzip dogme95.zip fe*. Det overlades nu til unzip at ekspandere fe*.

Alternativt kan man sætte en '\' foran stjernen og derved fortælle kommandofortolkeren at stjernen ikke skal ekspanderes. Altså: unzip d* f\*

Se også næste afsnit om regulære udtryk.

Det skal også nævnes, at hvis du kun vil finde et tegn, så brug et spørgsmålstegn. Vil du f.eks. finde alle filer aaa, baa, caa osv. men ikke caaa, så kan du bruge følgende

[tyge@hven MitKatalog]$ touch aaa baa caa caaa
[tyge@hven MitKatalog]$ ls 
aaa   baa   caa   caaa   ccc   fil1   fil2
[tyge@hven MitKatalog]$ ls ?aa
aaa   baa   caa
Bemærk "Globbing" og regulære udtryk
 

Jokertegnene ? og * er en lille del af et større system til at danne mønstre for filnavne. Dette system kaldes "globbing". Du kan finde en detaljeret gennemgang af hvordan man bruger "globbing" i bøgerne »Unix in a Nutshell« og »Linux in a Nutshell«.

Det er vigtigt ikke at komme til at blande "globbing" sammen med det system man i Unix bruger til at danne mønstre for tekst i al almindelighed – regulære udtryk (eng. "regular expressions"). Regulære udtryk er et væsentligt stærkere system end "globbing", men de bruges altså sædvanligvis ikke til mønstre for filnavne.

4.4.2. "Globbing"

Jokertegnene ? og * (omtalt i Afsnit 4.4.1) er de grundlæggende elementer i "globbing".

På kommandolinjen i for eksempel Bash kan man bruge noget der minder om regulære udtryk der kaldes "globbing". Med "globbing" kan du f.eks. liste alle dine filer, der slutter med bogstavet a ved at skrive ls *[a]. For at liste filer der ikke slutter på a skrives ls *[^a]. Det er vigtigt at skelne regex og globbing fra hinanden.

Tabel 4-2. Oversigt over "globbing"

Syntaks Beskrivelse
* Svarer til nul eller flere vilkårlige tegn.
? Svarer til netop ét vilkårligt tegn
[a-d] Svarer til et af tegnene a, b, c eller d. Men pas på. Nogle systemer kan ignorere forskelle mellem store og små bogstaver og regne med enten det danske eller det latinske alfabet.
[^a-d] Svarer til alt andet end tegnene a, b, c eller d.
{Skåne,Sjælland,Hven} Svarer til netop én af strengene Skåne, Sjælland og Hven.
*foo??ba[rz]{fubar,qux}*quux

Svarer til nul eller flere vilkårlige tegn, efterfulgt af strengen foo. Så to vilkårlige tegn efterfulgt af strengen ba og et af tegnene r eller z. Dernæst en af strengene fubar eller qux efterfulgt af ingen eller flere vilkårlige tegn og så strengen quux til sidst. Med dette udtryk ses det (let) at man kan finde filnavnene foo++barquxquux og XYZfoo77bazfubarYESquux i én og samme søgning.

4.4.3. Regulære udtryk

Mange programmer (blandt andre ed, egrep, Emacs, Perl, PHP, sed, slocate og vi) understøtter et meget effektivt system til søg-og-erstat af tekst efter angivne mønstre. Dette system kaldes regulære udtryk (eng. "regular expressions").

Tabel 4-3. Oversigt over regulære udtryk

Syntaks Beskrivelse
.* Svarer til nul eller flere vilkårlige tegn.
. Svarer til netop ét vilkårligt tegn
[a-d] Svarer til et af tegnene a, b, c eller d. Men pas på. Nogle systemer kan ignorere forskelle mellem store og små bogstaver og regne med enten det danske eller det latinske alfabet.
[^a-d] Svarer til alt andet end tegnene a, b, c eller d.
(Skåne|Sjælland|Hven) Svarer til netop én strengene Skåne, Sjælland og Hven.
.*foo..ba[rz](fubar|qux).*quux

Svarer til nul eller flere vilkårlige tegn, efterfulgt af strengen foo. Så to vilkårlige tegn efterfulgt af strengen ba og et af tegnene r eller z. Dernæst strengen en af strengene fubar eller qux efterfulgt af nul eller flere vilkårlige tegn og så strengen quux til sidst. Med dette udtryk ses det (let) at man kan finde filnavnene foo++barquxquux og XYZfoo77bazfubarYESquux i én og samme søgning.

Tabel 4-4. Definition af regulære udtryk

RegEx Beskrivelse
[tegn] Tegn der skal matches
RE1RE2 To forskellige regulære udtryk efter hinanden
RE* Regulære udtryk skal passe nul eller flere gange
RE? Regulære udtryk skal passe nul eller én gang. De regulære udtryk som følger POSIX-standarden benyttes dog "RE\?". Se f.eks. nedenstående eksempel med slocate.
RE+ Regulære udtryk skal passe én eller flere gange
RE1|RE2 Det ene eller det andet udtryk
(RE) Gruppe som bl.a. kan benyttes som variabel. I vi, emacs og sed er det dog "\(RE\)".
^ Start af linje
$ Slutning af linje

For at gøre det nemmere at skrive og læse regulære udtryk, har man indført nogle forkortelser. Den "rigtige" syntax for at matche bogstavet 'a', er ved at skrive '[a]', men her kan man i stedet blot skrive 'a' (uden anførselstegn).

Tabel 4-5. Forkortelser af regulære udtryk

RegEx Beskrivelse
a = a{1} = [a] = [a]{1} Matcher tegnet 'a' én gang
[\0-\m\o-\377] = . Matcher alle tegn undtagen \n
[*] = \* Matcher tegnet '*'
[abcd] = [a-d] Matcher en række tegn
[\0-ac-\377] = [^b] Undlader at matche et tegn 'b'
\d = [0-9] Matcher tallene 0-9
[a][a][a] = aaa = a{3} Matcher serien af tre ens tegn 'aaa'
[a][a][a]? = aaa? = a{2,3} Matcher serien af to til tre ens tegn 'a'
a* = a{0,} Matcher ingen eller mange tegn 'a'
a+ = aa* = a{1,} Matcher et eller mange tegn 'a'
a? = a{0,1} Matcher ingen eller et tegn 'a'

Et simpelt eksempel på brug af ovenstående, kunne være at matche en datoer i filen regex. Herunder ses en del af fil, hvor nogle linjer kun har en dato, og andre dato og en tekst. Datoen er skrevet i ISO-8601 formatet, som også kan udtrykkes som YYYY-MM-DD (ÅÅÅÅ-MM-DD).

# En kommentar 2002-12-01
2002-12-27
2002-12-28 Linus Torvalds 33

For at få listen over alle linjer der ikke indeholder en kommentar, skrives så:

[tyge@hven ~]$  egrep "^[^#]" regex
2002-12-27
2002-12-28 Linus Torvalds 33

Ovenstående giver nok ikke helt det ønskede resultat, i tilfælde af at der er skrevet noget i filen der er en ugyldig dato. Mere rigtigt vil det derfor være at matche datoerne. Da der kan forekomme datoer i kommentarer, så skal et match starte først på linjen, hvilket matches med '^'. Dernæst skal første tegn være et tal.

[tyge@hven ~]$  egrep "^[0-9]" regex
2002-12-27
2002-12-28 Linus Torvalds 33

Match af fire tal, kan enten skrives som:

[tyge@hven ~]$  egrep "^[0-9][0-9][0-9][0-9]" regex

eller lidt mere overskueligt:

[tyge@hven ~]$  egrep "^[0-9]{4}" regex

Et lidt mere præcist dato-match der både tjekker år, måned og dag bliver så:

[tyge@hven ~]$  egrep "^[0-9]{4}-[0-9]{2}-[0-9]{2}" regex

En mere løs form, hvor der kun matches tegn og bindestreger, der er nem at læse, kunne være:

[tyge@hven ~]$  egrep "^....-..-.." regex

Vil man kun have de linjer hvor der alene står en dato, indsættes et '$'-tegn der angiver slutning af linje.

[tyge@hven ~]$  egrep "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" regex
2002-12-27

Det modsatte match hvor der kun vises linjer med dato og kommentar, kan så fås ved at angive at der skal være et eller flere tegn efter datoen. Det kan gøres ved at indsætte '.+'. Ikke alle programmer forstår '+', så man kan også skrive '..*' for at få det samme resultat.

[tyge@hven ~]$  egrep "^[0-9]{4}-[0-9]{2}-[0-9]{2}.+$" regex
2002-12-28 Linus Torvalds 33

I datoer anvendes ret få tal, da der for eksempel kun er 12 måneder, og maksimum 31 dage i en måned. Hvis man dertil også kun ønsker årstal imellem 2000 og 2099, kan dette skrives som:

[tyge@hven ~]$  egrep "^20[0-9]{2}-[01][0-9]-[0-3][0-9]" regex
2002-12-27
2002-12-28 Linus Torvalds 33

Da der kun er 12 måneder, kan dette matches lidt mere præcist ved at tage højde for at andet tal kun kan være 0-9 hvis første tal er et 0. Er første tal 1, kan andet tal kun være 0-2.

[tyge@hven ~]$  egrep "^20[0-9]{2}-(0[0-9]|1[0-2])-[0-3][0-9]" regex
2002-12-27
2002-12-28 Linus Torvalds 33

De ovenstående eksempler viser så dels hvordan man kan skrive forskelligt og få det samme output, og dels at man meget nemt kan komme til at matche upræcist. Netop med datoer er det meget komplekst at lave det fuldstændige match, der også tager højde for skudår, så mindre kan i mange tilfælde gøre det.

Et meget brugt regulært udtryk i tekst-programmer, er udtrykket der bytter om på to ord. Som eksempel kunne opgaven være at bytte "Linus Torvalds" om til "Torvalds, Linus" i hele dokumentet. Der skal så laves en søgning der dels matcher hvert ord, men også 'fanger' ordene og lægger dem ind i variable. Her bruges () til at 'fange' ordet og efterfølgende kan 'fangsterne' findes i nummererede variable 1, 2, 3 osv.

Eksempel 4-2. Notation af regulære udtryk i forskellige programmer

Perl:

s/(Linus) (Torvalds)/$2, $1/

Emacs:

M-x replace-regexp RET
\(Linus\) \(Torvalds\) RET
\2, \1 RET

vi:

:%s/\(Linus\) \(Torvalds\)/\2, \1/g

PHP:

$a = preg_replace("/(Linus) (Torvalds)/", "\\2, \\1", $a);

sed:

sed 's/\(Linus\) \(Torvalds\)/\2, \1/'

Der er altså en lille forskel i de forskellige programmer på brugen af regulære udtryk, men princippet er det samme.

Et lille praktisk og anvendeligt eksempel er at finde alle de filer på dine diske som enten ender på '.htm' eller '.html'. I regulære udtryk betyder '.' alle tegn undtaget \n så der skal en '\' foran. Med 'l\?' angives det, at der enten skal være et 'l' eller intet tegn. '$' angiver slutning på filnavnet (altså linjen). Med slocate gøres dette således:

[tyge@hven MitKatalog]$ slocate -r '\.html\?$'

Et eksempel på et regulært udtryk kunne være at finde alle brugernavne på systemet, som har brugergruppe-privilegier under 100. /etc/passwd indeholder oplysningerne.

..
halt:*:7:0:halt:/sbin:/sbin/halt
..

Brugernavn 'halt' tilhører gruppe '0' som angivet i fjerde kolonne. I Perl kunne kommandoen se således ud:

[tyge@hven MitKatalog]$  perl -ne '/^[^:]+:[^:]+:[^:]+:..?:/ and print' /etc/passwd

Idéen i ovenstående er at gå tre kolonner frem, og se om der står et eller to tegn imellem kolon. Hvis udtrykket matches skal det printes ud. '/' er skille-tegnet. Der skal matches fra første tegn på linjen '^'. '[^:]+:' betyder match et eller flere tegn som ikke er kolon, frem til næste kolon. Første match vil så være 'halt:'. Dette kolonne-match er gentaget tre gange. '..?:' matcher et eller to tegn, efterfulgt af kolon. Da der kun står tal i denne kolonne, er det kun '0'-'9' og '10'-'99' der vil blive listet.

Skal man matche det samme mange gange, måske flere end tre, kan man bruge gentagelsesoperatoren {n}, hvor n er et range. I førnævnte eksempel blev blokken '[^:]+:' gentaget tre gange. Da det er en blok skal der parenteser omkring før det virker: '([^:]+:){3}'. Den færdige linje ser nu således ud:

[tyge@hven MitKatalog]$  perl -ne '/^([^:]+:){3}..?:/ and print' /etc/passwd

'.' matcher ethvert tegn og vil man være sikker på at det kun er tal der bliver matchet, skal man skrive '[0-9]' i stedet for '.'. I Perl er '[0-9]' også forkortet til '\d' (eng: decimal number) så udtrykket kunne også se således ud:

[tyge@hven MitKatalog]$  perl -ne '/^([^:]+:){3}\d\d?:/ and print' /etc/passwd

Hjemmeopgaven er nu at forklare hvad '[^#]+#' og '[^;]+;' matcher. Ekstraopgaven er '[^:]?:' og '[^:]*:'.

Beskrivelse af regulære udtryk kan findes i manualsiderne med kommandoen man 7 regex.

Læs mere om regulære udtryk på http://zez.org/article/articleview/11/1/

4.4.4. Omdirigering

Det er ikke altid hensigtsmæssigt at få outputtet fra en kommando skrevet direkte på skærmen. Hvis du skriver ls /usr/bin, vil du forstå hvorfor. Derfor er det muligt at omdirigere output (eng. "redirect"). Til det formål bruger du tegnet > til at omdirigere outputtet til en fil.

[tyge@hven MitKatalog]$  ls /usr/bin > usrbin.dir
[tyge@hven MitKatalog]$  less usrbin.dir

Andre gange har man brug for at tilføje nye linjer til en fil, hvilket sker ved at benytte to større-end-tegn efter hinanden >>. Dette bruges især til logfiler.

[tyge@hven MitKatalog]$  date >> tidsstempel
[tyge@hven MitKatalog]$  date >> tidsstempel
[tyge@hven MitKatalog]$  cat tidsstempel
lør jul 29 17:03:29 CEST 2000
lør jul 29 17:03:31 CEST 2000

Tilsvarende kan nogle programmer og kommandoer fødes med indholdet af en fil ved at bruge <

[tyge@hven MitKatalog]$  cat < usrbin.dir

Hvilket dog i tilfældet med cat er det samme som cat usrbin.dir.

Du kan også sende skreven tekst ind til et program som forventer tastetryk. Nedenstående lille program svarer "y" på et spørgsmål som "Interaktivkommando", beder brugeren at svare på.

#!/bin/sh
Interaktivkommando <<EOF
y
EOF

4.4.5. Kanaler

Som du så i det foregående afsnit, kan du omdirigere uddata til en fil. Nu kan du med rette spørge hvorfor du skal omdirigere til en fil, hvis du kun skal se på den én gang? Svaret på dit spørgsmål er ligefremt: Det behøver du heller ikke. Du kan nemlig anvende en kanal. I Linux betyder det, at uddata fra et program bruges som inddata til et andet. Lad os give et lille eksempel:

[tyge@hven MitKatalog]$ ls /usr/bin | less

Den lodrette streg (|) sætter en kanal op, så uddata fra ls /usr/bin kanaliseres videre og bruges som inddata til less. Det er naturligvis muligt at sætte en hel række kanaler op efter hinanden og på den måde slippe for en masse midlertidige filer.

På amerikansk kaldes denne slags kanaler for »pipes«.

4.4.6. Programmet grep

Ofte vil du fra kommandoer som cat, ls og find få alt for meget information, og oftest ved du godt, at det kun er en begrænset del af de viste linjer tekst, du er interesseret i. Hvis du f.eks. kun skal bruge de linjer af /etc/passwd, der indeholder navnet "tyge", kan du bruge grep til at begrænse outputtet med.

[tyge@hven MitKatalog]$  cat /etc/passwd | grep tyge 
tyge:x:501:501:Tyge Brahe,,,,:/home/tyge:/bin/bash

Du kan også bruge de forskellige i kombination. Eksemplet her finder filer i /usr/bin som begynder med p og indeholder ordet mail i filnavnet.

[tyge@hven MitKatalog]$  ls /usr/bin | grep ^p | grep mail
patch-metamail
pilot-mail
printmail
procmail

Hvilket også kunne have være gjort med kommandoen ls /usr/bin | grep ^p.*mail