torsdag den 13. august 2009

En lille Perle

Tidligere kom jeg til at nævne, at jeg havde forsøgt mig med Perl, og der blev jeg så af Lasse opfordret til at vise min kode frem. Well, Lasse, man må sige, at du selv af ude om det!

   1:use feature ':5.10';
2:
3:my ($tag) = @ARGV;
4:my $line;
5:my $file;
6:my $headrev;
7:my $taggedrev;
8:
9:say "filename - head.rev / rev. tagged with $tag";
10:
11:sub output {
12: if (defined($taggedrev)) {
13: say "$file - $headrev / $taggedrev" if ($taggedrev ne $headrev);
14: $taggedrev = undef;
15: } else {
16: say "$file - $headrev";
17: }
18: $file = undef
19: $taggedrev = undef
20:}
21:
22:while(defined($line = <STDIN>)) {
23: if( $line =~ /RCS file: (.*),v/ ) {
24: output if (defined($file));
25: $file = $1 unless $1 =~ /:*\/Attic\/.*/;
26: }
27:
28: $headrev = $1 if ($line =~ /head: (\S*)/);
29: $taggedrev = $1 if ($line =~ /\s*$tag: (\S*)/);
30:}
31:
32:output if (defined($file));
33:


Formålet med denne stump kode er, at parse outputtet fra en "cvs log"-kommando, og finde de steder, hvor head-revision ikke svarer til revision for et givent tag. Udfordringen er, at filnavnet, head-revision og revision for tags kommer på separate linier.

Jeg er ikke sikker på, at der er noget i ovenstående, som ikke også kunne ses af en passende "cvs diff", men det kunne jeg først indse, da jeg var nået frem til ovenstående.

1 kommentar:

Lasse Hillerøe Petersen sagde ...

Jamen, tak Bjarne. Det er altid en fornøjelse at læse kode du har skrevet.

Du har ret i at det er en udfordring, at outputtet fra cvs log ikke er særligt "venligt" struktureret. Og din - må jeg sige "traditionelle"? - linieorienterede løsning er nydelig og effektiv. Men det tog mig lidt tid at vikle min hjerne omkring logikken, bl.a. fordi du kalder output når du tager fat i en ny fil, og bruger globale variable og sideeffekter fra output til at styre tilstanden.

I linie 25 er der vist i øvrigt en trykfejl - :* skulle nok have været .*, ikke? (Begge .* er dog overflødige, og det virker alligevel, fordi :* jo heldigvis også matcher nul koloner.)

Hvis man kunne løse udfordringen mere "naturligt", og behandle hver fils data under et, kunne koden blive både kortere og mere læselig. Og det kan man heldigvis med Perl (indrykning med _ fordi jeg ikke må bruge >pre> i kommentaren):

$/ = undef;
scalar @ARGV == 1 and my($tag) = @ARGV or die "need a tag as argument";
print map {
_______ # $1 -> file, $2 -> headrev, $3 -> taggedrev
_______ m/\nRCS file: (.*?),v\n.*\nhead: (.*?)\n.*\s$tag: (.*?)\n/s ?
_______________ ($2 ne $3 ? sprintf "%s - %s / %s\n",$1,$2,$3 : ())
_______ : m/\nRCS file: (.*?),v\n.*\nhead: (.*?)\n/s ?
_______________ ($1 =~ m%/Attic/% ? () : sprintf "%s - %s\n",$1,$2)
_______ : ()
} map { s/\nkeyword subst.*//s; $_ } split/\n=+\n/, <STDIN>;

Koden, som til dels skal læses bagfra, er en simpel transformation vha map.
Første linie er et Perl-trick, som bare gør det muligt at sluge STDIN i en stor mundfuld. Anden linie er kommandolinieargumentet (jeg har tilladt mig at lave lidt checks.) Herefter læses næste statement bagfra.

Først splitter jeg input fra cvs log på separatorlinien - en linie bestående af kun "=". Resultatet er en array (eller liste) af strenge, som den første map lige renser lidt ud i. Denne map er for så vidt overflødig, hvis man bruger output fra cvs log -h i stedet.

Men logikken i den væsentlige map er helt enkel:
hvis taggen forekommer og ikke er samme rev som head, udskrives navnet og begge revisionsnumre.
hvis filen ikke er tagget, men kun er på head, så udskrives revisionsnummeret, med mindre filen er i Attic.

Tricket er - som altid, vil jeg tro - at kende sit værktøj. /s optionen til matchoperationerne gør at newline behandles som ethvert andet tegn. Nongreedy *? matching sikrer at revisionerne kun spises frem til den første newline - ellers skulle jeg have lavet et udtryk der kun matchede revisionsnumre, og jeg er jo doven.

Resultatet er kortere og - i mine øjne i hvert fald - mere læselig kode. Men der er jo altid mere end en måde at gøre tingene på. (En passende grep+sed eller awk ovenpå cvs diff ville sikkert være endnu bedre.)

/Lasse