/[Biblio-Isis]/trunk/lib/Biblio/Isis.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/lib/Biblio/Isis.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 65 - (show annotations)
Thu Jul 13 13:34:30 2006 UTC (17 years, 8 months ago) by dpavlin
File size: 19030 byte(s)
documented that hash filter gets also a field number [0.22]
1 package Biblio::Isis;
2 use strict;
3
4 use Carp;
5 use File::Glob qw(:globally :nocase);
6
7 BEGIN {
8 use Exporter ();
9 use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
10 $VERSION = 0.22;
11 @ISA = qw (Exporter);
12 #Give a hoot don't pollute, do not export more than needed by default
13 @EXPORT = qw ();
14 @EXPORT_OK = qw ();
15 %EXPORT_TAGS = ();
16
17 }
18
19 =head1 NAME
20
21 Biblio::Isis - Read CDS/ISIS, WinISIS and IsisMarc database
22
23 =head1 SYNOPSIS
24
25 use Biblio::Isis;
26
27 my $isis = new Biblio::Isis(
28 isisdb => './cds/cds',
29 );
30
31 for(my $mfn = 1; $mfn <= $isis->count; $mfn++) {
32 print $isis->to_ascii($mfn),"\n";
33 }
34
35 =head1 DESCRIPTION
36
37 This module will read ISIS databases created by DOS CDS/ISIS, WinIsis or
38 IsisMarc. It can be used as perl-only alternative to OpenIsis module which
39 seems to depriciate it's old C<XS> bindings for perl.
40
41 It can create hash values from data in ISIS database (using C<to_hash>),
42 ASCII dump (using C<to_ascii>) or just hash with field names and packed
43 values (like C<^asomething^belse>).
44
45 Unique feature of this module is ability to C<include_deleted> records.
46 It will also skip zero sized fields (OpenIsis has a bug in XS bindings, so
47 fields which are zero sized will be filled with random junk from memory).
48
49 It also has support for identifiers (only if ISIS database is created by
50 IsisMarc), see C<to_hash>.
51
52 This module will always be slower than OpenIsis module which use C
53 library. However, since it's written in perl, it's platform independent (so
54 you don't need C compiler), and can be easily modified. I hope that it
55 creates data structures which are easier to use than ones created by
56 OpenIsis, so reduced time in other parts of the code should compensate for
57 slower performance of this module (speed of reading ISIS database is
58 rarely an issue).
59
60 =head1 METHODS
61
62 =cut
63
64 # my $ORDN; # Nodes Order
65 # my $ORDF; # Leafs Order
66 # my $N; # Number of Memory buffers for nodes
67 # my $K; # Number of buffers for first level index
68 # my $LIV; # Current number of Index Levels
69 # my $POSRX; # Pointer to Root Record in N0x
70 # my $NMAXPOS; # Next Available position in N0x
71 # my $FMAXPOS; # Next available position in L0x
72 # my $ABNORMAL; # Formal BTree normality indicator
73
74 #
75 # some binary reads
76 #
77
78 =head2 new
79
80 Open ISIS database
81
82 my $isis = new Biblio::Isis(
83 isisdb => './cds/cds',
84 read_fdt => 1,
85 include_deleted => 1,
86 hash_filter => sub {
87 my ($v,$field_number) = @_;
88 $v =~ s#foo#bar#g;
89 },
90 debug => 1,
91 join_subfields_with => ' ; ',
92 );
93
94 Options are described below:
95
96 =over 5
97
98 =item isisdb
99
100 This is full or relative path to ISIS database files which include
101 common prefix of C<.MST>, and C<.XRF> and optionally C<.FDT> (if using
102 C<read_fdt> option) files.
103
104 In this example it uses C<./cds/cds.MST> and related files.
105
106 =item read_fdt
107
108 Boolean flag to specify if field definition table should be read. It's off
109 by default.
110
111 =item include_deleted
112
113 Don't skip logically deleted records in ISIS.
114
115 =item hash_filter
116
117 Filter code ref which will be used before data is converted to hash. It will
118 receive two arguments, whole line from current field (in C<< $_[0] >>) and
119 field number (in C<< $_[1] >>).
120
121 =item debug
122
123 Dump a B<lot> of debugging output even at level 1. For even more increase level.
124
125 =item join_subfields_with
126
127 Define delimiter which will be used to join repeatable subfields. This
128 option is included to support lagacy application written against version
129 older than 0.21 of this module. By default, it disabled. See L</to_hash>.
130
131 =back
132
133 =cut
134
135 sub new {
136 my $class = shift;
137 my $self = {};
138 bless($self, $class);
139
140 croak "new needs database name (isisdb) as argument!" unless ({@_}->{isisdb});
141
142 foreach my $v (qw{isisdb debug include_deleted hash_filter}) {
143 $self->{$v} = {@_}->{$v};
144 }
145
146 my @isis_files = grep(/\.(FDT|MST|XRF|CNT)$/i,glob($self->{isisdb}."*"));
147
148 foreach my $f (@isis_files) {
149 my $ext = $1 if ($f =~ m/\.(\w\w\w)$/);
150 $self->{lc($ext)."_file"} = $f;
151 }
152
153 my @must_exist = qw(mst xrf);
154 push @must_exist, "fdt" if ($self->{read_fdt});
155
156 foreach my $ext (@must_exist) {
157 unless ($self->{$ext."_file"}) {
158 carp "missing ",uc($ext)," file in ",$self->{isisdb};
159 return;
160 }
161 }
162
163 if ($self->{debug}) {
164 print STDERR "## using files: ",join(" ",@isis_files),"\n";
165 eval "use Data::Dump";
166
167 if (! $@) {
168 *Dumper = *Data::Dump::dump;
169 } else {
170 use Data::Dumper;
171 }
172 }
173
174 # if you want to read .FDT file use read_fdt argument when creating class!
175 if ($self->{read_fdt} && -e $self->{fdt_file}) {
176
177 # read the $db.FDT file for tags
178 my $fieldzone=0;
179
180 open(my $fileFDT, $self->{fdt_file}) || croak "can't read '$self->{fdt_file}': $!";
181 binmode($fileFDT);
182
183 while (<$fileFDT>) {
184 chomp;
185 if ($fieldzone) {
186 my $name=substr($_,0,30);
187 my $tag=substr($_,50,3);
188
189 $name =~ s/\s+$//;
190 $tag =~ s/\s+$//;
191
192 $self->{'TagName'}->{$tag}=$name;
193 }
194
195 if (/^\*\*\*/) {
196 $fieldzone=1;
197 }
198 }
199
200 close($fileFDT);
201 }
202
203 # Get the Maximum MFN from $db.MST
204
205 open($self->{'fileMST'}, $self->{mst_file}) || croak "can't open '$self->{mst_file}': $!";
206 binmode($self->{'fileMST'});
207
208 # MST format: (* = 32 bit signed)
209 # CTLMFN* always 0
210 # NXTMFN* MFN to be assigned to the next record created
211 # NXTMFB* last block allocated to master file
212 # NXTMFP offset to next available position in last block
213 # MFTYPE always 0 for user db file (1 for system)
214 seek($self->{'fileMST'},4,0) || croak "can't seek to offset 0 in MST: $!";
215
216 my $buff;
217
218 read($self->{'fileMST'}, $buff, 4) || croak "can't read NXTMFN from MST: $!";
219 $self->{'NXTMFN'}=unpack("V",$buff) || croak "NXTNFN is zero";
220
221 print STDERR "## self ",Dumper($self),"\n" if ($self->{debug});
222
223 # open files for later
224 open($self->{'fileXRF'}, $self->{xrf_file}) || croak "can't open '$self->{xrf_file}': $!";
225 binmode($self->{'fileXRF'});
226
227 $self ? return $self : return undef;
228 }
229
230 =head2 count
231
232 Return number of records in database
233
234 print $isis->count;
235
236 =cut
237
238 sub count {
239 my $self = shift;
240 return $self->{'NXTMFN'} - 1;
241 }
242
243 =head2 fetch
244
245 Read record with selected MFN
246
247 my $rec = $isis->fetch(55);
248
249 Returns hash with keys which are field names and values are unpacked values
250 for that field like this:
251
252 $rec = {
253 '210' => [ '^aNew York^cNew York University press^dcop. 1988' ],
254 '990' => [ '2140', '88', 'HAY' ],
255 };
256
257 =cut
258
259 sub fetch {
260 my $self = shift;
261
262 my $mfn = shift || croak "fetch needs MFN as argument!";
263
264 # is mfn allready in memory?
265 my $old_mfn = $self->{'current_mfn'} || -1;
266 return $self->{record} if ($mfn == $old_mfn);
267
268 print STDERR "## fetch: $mfn\n" if ($self->{debug});
269
270 # XXX check this?
271 my $mfnpos=($mfn+int(($mfn-1)/127))*4;
272
273 print STDERR "## seeking to $mfnpos in file '$self->{xrf_file}'\n" if ($self->{debug});
274 seek($self->{'fileXRF'},$mfnpos,0);
275
276 my $buff;
277
278 # delete old record
279 delete $self->{record};
280
281 # read XRFMFB abd XRFMFP
282 read($self->{'fileXRF'}, $buff, 4);
283 my $pointer=unpack("V",$buff);
284 if (! $pointer) {
285 if ($self->{include_deleted}) {
286 return;
287 } else {
288 warn "pointer for MFN $mfn is null\n";
289 return;
290 }
291 }
292
293 # check for logically deleted record
294 if ($pointer & 0x80000000) {
295 print STDERR "## record $mfn is logically deleted\n" if ($self->{debug});
296 $self->{deleted} = $mfn;
297
298 return unless $self->{include_deleted};
299
300 # abs
301 $pointer = ($pointer ^ 0xffffffff) + 1;
302 }
303
304 my $XRFMFB = int($pointer/2048);
305 my $XRFMFP = $pointer - ($XRFMFB*2048);
306
307 # (XRFMFB - 1) * 512 + XRFMFP
308 # why do i have to do XRFMFP % 1024 ?
309
310 my $blk_off = (($XRFMFB - 1) * 512) + ($XRFMFP % 512);
311
312 print STDERR "## pointer: $pointer XRFMFB: $XRFMFB XRFMFP: $XRFMFP offset: $blk_off\n" if ($self->{'debug'});
313
314 # Get Record Information
315
316 seek($self->{'fileMST'},$blk_off,0) || croak "can't seek to $blk_off: $!";
317
318 read($self->{'fileMST'}, $buff, 4) || croak "can't read 4 bytes at offset $blk_off from MST file: $!";
319 my $value=unpack("V",$buff);
320
321 print STDERR "## offset for rowid $value is $blk_off (blk $XRFMFB off $XRFMFP)\n" if ($self->{debug});
322
323 if ($value!=$mfn) {
324 if ($value == 0) {
325 print STDERR "## record $mfn is physically deleted\n" if ($self->{debug});
326 $self->{deleted} = $mfn;
327 return;
328 }
329
330 carp "Error: MFN ".$mfn." not found in MST file, found $value";
331 return;
332 }
333
334 read($self->{'fileMST'}, $buff, 14);
335
336 my ($MFRL,$MFBWB,$MFBWP,$BASE,$NVF,$STATUS) = unpack("vVvvvv", $buff);
337
338 print STDERR "## MFRL: $MFRL MFBWB: $MFBWB MFBWP: $MFBWP BASE: $BASE NVF: $NVF STATUS: $STATUS\n" if ($self->{debug});
339
340 warn "MFRL $MFRL is not even number" unless ($MFRL % 2 == 0);
341
342 warn "BASE is not 18+6*NVF" unless ($BASE == 18 + 6 * $NVF);
343
344 # Get Directory Format
345
346 my @FieldPOS;
347 my @FieldLEN;
348 my @FieldTAG;
349
350 read($self->{'fileMST'}, $buff, 6 * $NVF);
351
352 my $rec_len = 0;
353
354 for (my $i = 0 ; $i < $NVF ; $i++) {
355
356 my ($TAG,$POS,$LEN) = unpack("vvv", substr($buff,$i * 6, 6));
357
358 print STDERR "## TAG: $TAG POS: $POS LEN: $LEN\n" if ($self->{debug});
359
360 # The TAG does not exists in .FDT so we set it to 0.
361 #
362 # XXX This is removed from perl version; .FDT file is updated manually, so
363 # you will often have fields in .MST file which aren't in .FDT. On the other
364 # hand, IsisMarc doesn't use .FDT files at all!
365
366 #if (! $self->{TagName}->{$TAG}) {
367 # $TAG=0;
368 #}
369
370 push @FieldTAG,$TAG;
371 push @FieldPOS,$POS;
372 push @FieldLEN,$LEN;
373
374 $rec_len += $LEN;
375 }
376
377 # Get Variable Fields
378
379 read($self->{'fileMST'},$buff,$rec_len);
380
381 print STDERR "## rec_len: $rec_len poc: ",tell($self->{'fileMST'})."\n" if ($self->{debug});
382
383 for (my $i = 0 ; $i < $NVF ; $i++) {
384 # skip zero-sized fields
385 next if ($FieldLEN[$i] == 0);
386
387 push @{$self->{record}->{$FieldTAG[$i]}}, substr($buff,$FieldPOS[$i],$FieldLEN[$i]);
388 }
389
390 $self->{'current_mfn'} = $mfn;
391
392 print STDERR Dumper($self),"\n" if ($self->{debug});
393
394 return $self->{'record'};
395 }
396
397 =head2 mfn
398
399 Returns current MFN position
400
401 my $mfn = $isis->mfn;
402
403 =cut
404
405 # This function should be simple return $self->{current_mfn},
406 # but if new is called with _hack_mfn it becomes setter.
407 # It's useful in tests when setting $isis->{record} directly
408
409 sub mfn {
410 my $self = shift;
411 return $self->{current_mfn};
412 };
413
414
415 =head2 to_ascii
416
417 Returns ASCII output of record with specified MFN
418
419 print $isis->to_ascii(42);
420
421 This outputs something like this:
422
423 210 ^aNew York^cNew York University press^dcop. 1988
424 990 2140
425 990 88
426 990 HAY
427
428 If C<read_fdt> is specified when calling C<new> it will display field names
429 from C<.FDT> file instead of numeric tags.
430
431 =cut
432
433 sub to_ascii {
434 my $self = shift;
435
436 my $mfn = shift || croak "need MFN";
437
438 my $rec = $self->fetch($mfn) || return;
439
440 my $out = "0\t$mfn";
441
442 foreach my $f (sort keys %{$rec}) {
443 my $fn = $self->tag_name($f);
444 $out .= "\n$fn\t".join("\n$fn\t",@{$self->{record}->{$f}});
445 }
446
447 $out .= "\n";
448
449 return $out;
450 }
451
452 =head2 to_hash
453
454 Read record with specified MFN and convert it to hash
455
456 my $hash = $isis->to_hash($mfn);
457
458 It has ability to convert characters (using C<hash_filter>) from ISIS
459 database before creating structures enabling character re-mapping or quick
460 fix-up of data.
461
462 This function returns hash which is like this:
463
464 $hash = {
465 '210' => [
466 {
467 'c' => 'New York University press',
468 'a' => 'New York',
469 'd' => 'cop. 1988'
470 }
471 ],
472 '990' => [
473 '2140',
474 '88',
475 'HAY'
476 ],
477 };
478
479 You can later use that hash to produce any output from ISIS data.
480
481 If database is created using IsisMarc, it will also have to special fields
482 which will be used for identifiers, C<i1> and C<i2> like this:
483
484 '200' => [
485 {
486 'i1' => '1',
487 'i2' => ' '
488 'a' => 'Goa',
489 'f' => 'Valdo D\'Arienzo',
490 'e' => 'tipografie e tipografi nel XVI secolo',
491 }
492 ],
493
494 In case there are repeatable subfields in record, this will create
495 following structure:
496
497 '900' => [ {
498 'a' => [ 'foo', 'bar', 'baz' ],
499 }]
500
501 Or in more complex example of
502
503 902 ^aa1^aa2^aa3^bb1^aa4^bb2^cc1^aa5
504
505 it will create
506
507 902 => [
508 { a => ["a1", "a2", "a3", "a4", "a5"], b => ["b1", "b2"], c => "c1" },
509 ],
510
511 This behaviour can be changed using C<join_subfields_with> option to L</new>,
512 in which case C<to_hash> will always create single value for each subfield.
513 This will change result to:
514
515
516
517 This method will also create additional field C<000> with MFN.
518
519 There is also more elaborative way to call C<to_hash> like this:
520
521 my $hash = $isis->to_hash({
522 mfn => 42,
523 include_subfields => 1,
524 });
525
526 Each option controll creation of hash:
527
528 =over 4
529
530 =item mfn
531
532 Specify MFN number of record
533
534 =item include_subfields
535
536 This option will create additional key in hash called C<subfields> which will
537 have original record subfield order and index to that subfield like this:
538
539 902 => [ {
540 a => ["a1", "a2", "a3", "a4", "a5"],
541 b => ["b1", "b2"],
542 c => "c1",
543 subfields => ["a", 0, "a", 1, "a", 2, "b", 0, "a", 3, "b", 1, "c", 0, "a", 4],
544 } ],
545
546 =item join_subfields_with
547
548 Define delimiter which will be used to join repeatable subfields. You can
549 specify option here instead in L</new> if you want to have per-record control.
550
551 =back
552
553 =cut
554
555 sub to_hash {
556 my $self = shift;
557
558
559 my $mfn = shift || confess "need mfn!";
560 my $arg;
561
562 if (ref($mfn) eq 'HASH') {
563 $arg = $mfn;
564 $mfn = $arg->{mfn} || confess "need mfn in arguments";
565 }
566
567 # init record to include MFN as field 000
568 my $rec = { '000' => [ $mfn ] };
569
570 my $row = $self->fetch($mfn) || return;
571
572 my $j_rs = $arg->{join_subfields_with};
573 $j_rs = $self->{join_subfields_with} unless(defined($j_rs));
574 my $i_sf = $arg->{include_subfields};
575
576 foreach my $f_nr (keys %{$row}) {
577 foreach my $l (@{$row->{$f_nr}}) {
578
579 # filter output
580 if ($self->{'hash_filter'}) {
581 $l = $self->{'hash_filter'}->($l, $f_nr);
582 next unless defined($l);
583 }
584
585 my $val;
586 my $r_sf; # repeatable subfields in this record
587
588 # has identifiers?
589 ($val->{'i1'},$val->{'i2'}) = ($1,$2) if ($l =~ s/^([01 #])([01 #])\^/\^/);
590
591 # has subfields?
592 if ($l =~ m/\^/) {
593 foreach my $t (split(/\^/,$l)) {
594 next if (! $t);
595 my ($sf,$v) = (substr($t,0,1), substr($t,1));
596 # XXX this might be option, but why?
597 next unless ($v);
598 # warn "### $f_nr^$sf:$v",$/ if ($self->{debug} > 1);
599
600 if (ref( $val->{$sf} ) eq 'ARRAY') {
601
602 push @{ $val->{$sf} }, $v;
603
604 # record repeatable subfield it it's offset
605 push @{ $val->{subfields} }, ( $sf, $#{ $val->{$sf} } ) if (! $j_rs && $i_sf);
606 $r_sf->{$sf}++;
607
608 } elsif (defined( $val->{$sf} )) {
609
610 # convert scalar field to array
611 $val->{$sf} = [ $val->{$sf}, $v ];
612
613 push @{ $val->{subfields} }, ( $sf, 1 ) if (! $j_rs && $i_sf);
614 $r_sf->{$sf}++;
615
616 } else {
617 $val->{$sf} = $v;
618 push @{ $val->{subfields} }, ( $sf, 0 ) if ($i_sf);
619 }
620 }
621 } else {
622 $val = $l;
623 }
624
625 if ($j_rs) {
626 map {
627 $val->{$_} = join($j_rs, @{ $val->{$_} });
628 } keys %$r_sf
629 }
630
631 push @{$rec->{$f_nr}}, $val;
632 }
633 }
634
635 return $rec;
636 }
637
638 =head2 tag_name
639
640 Return name of selected tag
641
642 print $isis->tag_name('200');
643
644 =cut
645
646 sub tag_name {
647 my $self = shift;
648 my $tag = shift || return;
649 return $self->{'TagName'}->{$tag} || $tag;
650 }
651
652
653 =head2 read_cnt
654
655 Read content of C<.CNT> file and return hash containing it.
656
657 print Dumper($isis->read_cnt);
658
659 This function is not used by module (C<.CNT> files are not required for this
660 module to work), but it can be useful to examine your index (while debugging
661 for example).
662
663 =cut
664
665 sub read_cnt {
666 my $self = shift;
667
668 croak "missing CNT file in ",$self->{isisdb} unless ($self->{cnt_file});
669
670 # Get the index information from $db.CNT
671
672 open(my $fileCNT, $self->{cnt_file}) || croak "can't read '$self->{cnt_file}': $!";
673 binmode($fileCNT);
674
675 my $buff;
676
677 read($fileCNT, $buff, 26) || croak "can't read first table from CNT: $!";
678 $self->unpack_cnt($buff);
679
680 read($fileCNT, $buff, 26) || croak "can't read second table from CNT: $!";
681 $self->unpack_cnt($buff);
682
683 close($fileCNT);
684
685 return $self->{cnt};
686 }
687
688 =head2 unpack_cnt
689
690 Unpack one of two 26 bytes fixed length record in C<.CNT> file.
691
692 Here is definition of record:
693
694 off key description size
695 0: IDTYPE BTree type s
696 2: ORDN Nodes Order s
697 4: ORDF Leafs Order s
698 6: N Number of Memory buffers for nodes s
699 8: K Number of buffers for first level index s
700 10: LIV Current number of Index Levels s
701 12: POSRX Pointer to Root Record in N0x l
702 16: NMAXPOS Next Available position in N0x l
703 20: FMAXPOS Next available position in L0x l
704 24: ABNORMAL Formal BTree normality indicator s
705 length: 26 bytes
706
707 This will fill C<$self> object under C<cnt> with hash. It's used by C<read_cnt>.
708
709 =cut
710
711 sub unpack_cnt {
712 my $self = shift;
713
714 my @flds = qw(ORDN ORDF N K LIV POSRX NMAXPOS FMAXPOS ABNORMAL);
715
716 my $buff = shift || return;
717 my @arr = unpack("vvvvvvVVVv", $buff);
718
719 print STDERR "unpack_cnt: ",join(" ",@arr),"\n" if ($self->{'debug'});
720
721 my $IDTYPE = shift @arr;
722 foreach (@flds) {
723 $self->{cnt}->{$IDTYPE}->{$_} = abs(shift @arr);
724 }
725 }
726
727 1;
728
729 =head1 BUGS
730
731 Some parts of CDS/ISIS documentation are not detailed enough to exmplain
732 some variations in input databases which has been tested with this module.
733 When I was in doubt, I assumed that OpenIsis's implementation was right
734 (except for obvious bugs).
735
736 However, every effort has been made to test this module with as much
737 databases (and programs that create them) as possible.
738
739 I would be very greatful for success or failure reports about usage of this
740 module with databases from programs other than WinIsis and IsisMarc. I had
741 tested this against ouput of one C<isis.dll>-based application, but I don't
742 know any details about it's version.
743
744 =head1 VERSIONS
745
746 As this is young module, new features are added in subsequent version. It's
747 a good idea to specify version when using this module like this:
748
749 use Biblio::Isis 0.21
750
751 Below is list of changes in specific version of module (so you can target
752 older versions if you really have to):
753
754 =over 8
755
756 =item 0.22
757
758 Added field number when calling C<hash_filter>
759
760 =item 0.21
761
762 Added C<join_subfields_with> to L</new> and L</to_hash>.
763
764 Added C<include_subfields> to L</to_hash>.
765
766 =item 0.20
767
768 Added C<< $isis->mfn >>, support for repeatable subfields and
769 C<< $isis->to_hash({ mfn => 42, ... }) >> calling convention
770
771 =back
772
773 =head1 AUTHOR
774
775 Dobrica Pavlinusic
776 CPAN ID: DPAVLIN
777 dpavlin@rot13.org
778 http://www.rot13.org/~dpavlin/
779
780 This module is based heavily on code from C<LIBISIS.PHP> library to read ISIS files V0.1.1
781 written in php and (c) 2000 Franck Martin <franck@sopac.org> and released under LGPL.
782
783 =head1 COPYRIGHT
784
785 This program is free software; you can redistribute
786 it and/or modify it under the same terms as Perl itself.
787
788 The full text of the license can be found in the
789 LICENSE file included with this module.
790
791
792 =head1 SEE ALSO
793
794 L<Biblio::Isis::Manual> for CDS/ISIS manual appendix F, G and H which describe file format
795
796 OpenIsis web site L<http://www.openisis.org>
797
798 perl4lib site L<http://perl4lib.perl.org>
799

  ViewVC Help
Powered by ViewVC 1.1.26