3 # The content of the following variable is configuration data:
4 @DiscoverMacro::datafile_path = ('program','.');
8 discovermacro.pl - Discover which LaTeX package contains a missing macro
12 B<discovermacro.pl> \I<themissingmacro>
14 B<discovermacro.pl> I<mydocument.log>
20 The ucs package uses many macros from many packages which have to be
21 included with B<\usepackage> before the inclusion of F<ucs.sty>.
23 The missing macro can be given directly as parameter. Note that the
24 backslash must be included, so with most shells you either need to
25 quote it or write a double backslash (e. g. \\cyrc or '\cyrc').
27 An alternative way is to give a LaTeX log file as parameter in which
28 case the missing macros are parsed from it.
30 If no argument is given, the newest LaTeX log file in the current
31 directory is used as default.
35 B<discovermacro.pl> uses the data file F<ltxmacrs.txt> which is human
36 readable, thus can be used directly.
38 F<http://www.unruh.de/DniQ/cgi/discovermacro.cgi> provides an online
39 version of this script.
45 package DiscoverMacro::H;
59 while ($line =~ /\\\n?$/s) {
69 my ($format,$text) = @_;
70 if ($format eq 'text/plain') {
71 $text =~ s/\[\[\[.*?\]\]\]//g;
72 } elsif ($format eq 'text/html') {
73 $text =~ s{[<>&]|(\n[ \t]*)}{ my $x=$&;
79 sprintf "&#%d;", ord $& }}egs;
80 $text =~ s{\[\[\[.+?\]\]\]}{
81 my ($t) = ($& =~ /\[\[\[(.+?)\]\]\]/);
84 if ($t[0] eq 'NAME') {
85 $r = "<a name=\"$t[1]\">";
86 } elsif ($t[0] eq 'HREF') {
87 $r = "<a href=\"$t[1]\">";
88 } elsif ($t[0] eq '/NAME') {
90 } elsif ($t[0] eq '/HREF') {
93 warn "escape($format,[[[$t[0]...]]])";
97 } elsif ($format eq 'identifier') {
99 sprintf "_%02X", ord $&/ge;
101 warn "Unknown format $format";
102 return escape('text/plain',$text);
108 my ($format,$header) = @_;
109 $header = escape($format,$header);
110 if ($format eq 'text/plain') {
111 my $len = length $header;
112 return "\n$header\n".('=' x $len)."\n";
113 } elsif ($format eq 'text/html') {
114 return "<p><table border=1>\n<tr><th align=left colspan=2>$header</th></tr>\n";
116 warn "Unknown format $format";
117 return header('text/plain',$header);
123 if ($format eq 'text/plain') {
125 } elsif ($format eq 'text/html') {
126 return "</table></p>\n\n";
128 warn "Unknown format $format";
129 return footer('text/plain');
134 my ($format,$line) = @_;
135 $line = escape $format, $line;
136 if ($format eq 'text/plain') {
138 } elsif ($format eq 'text/html') {
139 return "<tr><td colspan=2>$line</td></tr>\n";
141 warn "Unknown format $format";
142 return line($format,$line);
148 my ($format,$col1,$col2) = @_;
149 $col1 = escape $format, $col1;
150 $col2 = escape $format, $col2;
151 if ($format eq 'text/plain') {
152 return "$col1\t$col2\n";
153 } elsif ($format eq 'text/html') {
154 return "<tr><td>$col1 </td><td>$col2</td></tr>\n";
156 warn "Unknown format $format";
157 return twocol('text/plain',$col1,$col2);
162 package DiscoverMacro::Feature;
164 use vars qw/$obj_count/;
168 my ($proto,$db,$name) = @_;
170 my $class = ref($proto) || $proto;
173 $self->{name} = $name;
175 $self->loadfeature();
185 return $self->{fontencoding};
194 return $self->{needs};
199 return $self->{packages};
202 sub getpackages_ascode($) {
204 return join '', map {
206 $p = "{$_}" unless $p =~ /\}$/;
208 } @{$self->{packages}};
214 my $pos = $self->{db}->getfeaturepos($self->{name});
215 unless (defined $pos) {
216 $self->{undefined} = 1;
219 my $file = $self->{db}->getfileat($pos);
221 my $lastfile = undef;
222 while (defined ($line = DiscoverMacro::H::readline2($file))) {
224 $line = DiscoverMacro::H::cleanline $line;
226 my @line = split ' ',$line;
227 if ($line[0] eq 'FONTENCODING') {
228 warn "Two fontencodings in feature $self->{name}"
229 if defined $self->{fontencoding};
230 $self->{fontencoding} = $line[1];
231 } elsif ($line[0] eq 'CTAN') {
232 if (!defined $lastfile) {
233 die "CTAN not preceded by FILE in feature $self->{name}"; }
234 my $ctan = $self->{ctan};
235 warn "Two CTAN locations defined for file $lastfile ".
236 "in feature $self->{name}" if defined $$ctan{$lastfile};
237 $$ctan{$lastfile} = $line[1];
238 $self->{ctan} = $ctan;
239 } elsif ($line[0] eq 'LATEXCMD') {
240 warn "Two LATEXCMDs in feature $self->{name}"
241 if defined $self->{latexcmd};
242 $self->{latexcmd} = $line[1];
243 } elsif ($line[0] eq 'NEEDS') {
244 warn "Two NEEDS lines in feature $self->{name}"
245 if defined $self->{needs};
246 $self->{needs} = join ' ',@line[1..$#line];
247 } elsif ($line[0] eq 'END') {
248 warn "FEATURE $self->{name} ended by END $line[1]"
249 if $line[1] ne 'FEATURE';
251 } elsif ($line[0] eq 'COMMENT') {
252 my $comment = readblock($file,'COMMENT',1);
253 if (defined $self->{comment}) {
254 warn "Two COMMENT sections in feature $self->{name}.".
256 $comment = "$self->{comment}$comment"; }
257 $self->{comment} = $comment;
258 } elsif ($line[0] eq 'INSTALL') {
259 my $install = readblock($file,'INSTALL',1);
260 if (defined $self->{install}) {
261 warn "Two INSTALL sections in feature $self->{name}.".
263 $install = "$self->{install}$install"; }
264 $self->{install} = $install;
265 } elsif ($line[0] eq 'FILE') {
266 push @{$self->{files}}, $line[1];
267 $tlastfile = $line[1];
268 } elsif ($line[0] eq 'PACKAGE') {
269 push @{$self->{packages}}, $line[1];
271 warn "Unknown command in feature $self->{name}: $line[0]";
273 $lastfile = $tlastfile; $tlastfile = undef;
278 my ($file,$blocktype,$raw) = @_;
281 while (defined ($line = $raw?<$file>:DiscoverMacro::H::readline2($file))) {
282 return $str if ($line =~ /^\s*END\s+\Q$blocktype\E\s*$/);
283 $line = DiscoverMacro::H::cleanline $line unless $raw;
287 warn "EOF in $blocktype";
293 return !$self->{undefined};
298 my $link = "[[[NAME ".
299 DiscoverMacro::H::escape('identifier',$self->{name})."]]]";
300 my $text = DiscoverMacro::H::header
301 ($f,"${link}Feature: $self->{name}\[[[/NAME]]]");
302 if ($self->{undefined}) {
303 $text .= DiscoverMacro::H::line($f,"No information available");
305 if (defined $self->{fontencoding}) {
306 $text .= DiscoverMacro::H::twocol($f,"Fontencoding:",
307 $self->{fontencoding});
309 for my $i (@{$self->{packages}}) {
310 $text .= DiscoverMacro::H::twocol($f,"Package:",$i);
312 if (defined $self->{needs}) {
313 $text .= DiscoverMacro::H::twocol($f,"Needs:",$self->{needs});
315 for my $i (@{$self->{files}}) {
316 my $ctan = ${$self->{ctan}}{$i};
318 $ctan = " (CTAN: [[[HREF http://www.ctan.org/tex-archive/".
319 "$ctan]]]$ctan\[[[/HREF]]])";
320 } else { $ctan = ''; };
321 $text .= DiscoverMacro::H::twocol($f,"Needed file:",$i.$ctan);
323 if ($self->{comment}) {
324 $text .= DiscoverMacro::H::twocol($f,"Comment:",$self->{comment});
326 if ($self->{install}) {
327 $text .= DiscoverMacro::H::twocol($f,"Installation:",$self->{install});
329 $text .= DiscoverMacro::H::footer $f;
333 package DiscoverMacro::Macro;
337 use vars qw/$obj_count/;
341 my ($proto,$db,$macro) = @_;
342 my $class = ref($proto) || $proto;
346 $self->{macro} = $macro;
358 return !$self->{undefined};
363 return $self->{macro};
368 return $self->{list};
373 my $macros = $self->{db}->{macros};
374 my $macro = $self->{macro};
375 my $list = $$macros{$macro};
376 unless (defined $list) {
377 my $regex = $self->{db}->{regexmacros};
378 while (my ($re,$l) = each %$regex) {
380 if ($macro =~ /$re/) {
381 $self->{regex} = $re;
385 unless (defined $list) {
386 $self->{undefined} = 1;
389 $self->{list} = $list;
391 my @list = grep { my $bad = $_ eq 'OR' || $_ eq 'AND' || $tlist{$_};
392 $tlist{$_}=1; !$bad; } split ' ', $self->{list};
395 $features{$i} = new DiscoverMacro::Feature($self->{db},$i)
396 unless defined $features{$i};
398 $self->{features} = \%features;
402 my ($self,$name) = @_;
403 return ${$self->{features}}{$name};
409 my $link = "[[[NAME ".
410 DiscoverMacro::H::escape('identifier',$self->{macro})."]]]";
411 $text .= DiscoverMacro::H::header
412 ($f,"${link}Macro: $self->{macro}\[[[/NAME]]]");
413 if ($self->{undefined}) {
414 $text .= DiscoverMacro::H::line($f,"Macro is unknown.");
416 my $list = $self->{list};
417 my $features = $self->{features};
420 if ($$features{$w}) {
421 $w = "[[[HREF #".DiscoverMacro::H::escape('identifier',$w).
426 $text .= DiscoverMacro::H::twocol($f,"Available with:",$list);
428 $text .= DiscoverMacro::H::footer($f);
429 for my $feature (values %{$self->{features}}) {
430 $text .= $feature->as_text($f);
435 package DiscoverMacro;
440 use vars qw/$obj_count @datafile_path/;
445 my $class = ref($proto) || $proto;
449 $self->{missingmacros} = {};
458 sub skip_to_end($$$) {
459 my ($file,$type,$lnr) = @_;
461 while (defined ($line = DiscoverMacro::H::readline2($file))) {
463 if $line =~ /^\s*END\s*\Q$type\E\s*$/;
465 warn "Could not find end of $type-section started on line $lnr";
471 return "ltxmacrs.txt:$lnr: ";
477 my $regexmacros = {};
479 my $datafile = $self->{datafile};
480 unless (defined $datafile) {
481 for my $dir (@datafile_path) {
482 if ($dir eq 'program') {
483 $datafile = $0; $datafile =~ s@[^/]*$@ltxmacrs.txt@;
485 $datafile = "$dir/ltxmacrs.txt";
489 if (-e $datafile) { last; }
490 else { $datafile = undef; }
492 die "Could not find data file ltxmacrs.txt in path ".
494 ($_ eq 'program')?'script location':$_ } @datafile_path
495 unless defined $datafile;
496 $self->{datafile} = $datafile;
498 my $data = new IO::File($datafile,"r") or
499 die "Could not open $datafile for reading";
501 while (defined ($line = DiscoverMacro::H::readline2($data))) {
502 $line = DiscoverMacro::H::cleanline($line);
504 my (@line) = split ' ', $line;
505 if ($line[0] eq 'MACRO') {
506 # print Dumper(\@line);
507 warn lineinfo($.)."Macro $line[1] given twice\n"
508 if defined $$macros{$line[1]};
509 $$macros{$line[1]} = join ' ', @line[2..$#line];
510 } elsif ($line[0] eq 'REGEX') {
511 warn lineinfo($.)."Regex macro $line[1] given twice\n"
512 if defined $$regexmacros{$line[1]};
513 $$regexmacros{$line[1]} = join ' ', @line[2..$#line];
514 } elsif ($line[0] eq 'FEATURE') {
515 $$features{$line[1]} = $data->getpos;
516 skip_to_end($data,'FEATURE',$.);
518 warn lineinfo($.)."Unknown command '$line[0]'\n";
521 $self->{macros} = $macros;
522 $self->{regexmacros} = $regexmacros;
523 $self->{features} = $features;
524 $self->{datafile} = $data;
526 # print Dumper($macros);
530 my ($self,$macro) = @_;
531 return new DiscoverMacro::Macro($self,$macro);
534 sub getfeaturepos($$) {
535 my ($self,$feature) = @_;
536 my $features = $self->{features};
537 return $$features{$feature};
541 my ($self,$filename,$file) = @_;
543 if (!defined $file) {
544 $file = new IO::File($filename,"r")
545 or die "Could not open log file '$filename' for reading: $!";
548 while (defined ($line = <$file>)) {
550 if ($line =~ /^! Undefined control sequence\.$/) {
551 chomp ($line = <$file>);
552 #print "LINE: '$line'\n";
554 my ($macro) = ($line =~ /(\\.)$/);
556 ($line =~ /(...)(\\[a-zA-Z@]+) ?$/) unless defined $macro;
558 ($line =~ /(...)([a-zA-Z@]+) ?$/) unless defined $macro;
560 if ($dots eq '...') {
563 $macro = "\\\\[a-zA-Z@]*\Q$macro";
565 $macro = "\\$macro" unless $macro =~ /^\\/;
567 unless (defined $macro) {
568 warn "$filename:$.: Could not identify undefined control in:\n".
571 #print "MACRO: '$macro'\n";
574 for my $m (keys %{$self->{macros}}) {
575 if ($m =~ /$macro/) {
579 $self->addmacro("REGEX:$macro") unless $found;
581 $self->addmacro($macro);
584 } elsif ($line =~ /^! Package babel Error: You haven\'t defined the language (.+) yet.$/) {
586 $self->addmacro("\\selectlanguage{$lang}");
588 } elsif ($line =~ /^! LaTeX Error: Environment .* undefined\.$/) {
590 ($line =~ /^! LaTeX Error: Environment (.*) undefined\.$/);
591 unless (defined $env) {
592 warn "$filename:$.: Could not identify environment in:\n".
593 "\t$line\n" unless defined $env;
595 $self->addmacro("\\begin{$env}");
597 } elsif ($line =~ /^! LaTeX Error: Encoding scheme \`.*\' unknown\.$/) {
599 ($line =~ /^! LaTeX Error: Encoding scheme \`(.*)\' unknown\.$/);
600 unless (defined $fe) {
601 warn "$filename:$.: Could not identify fontencoding in:\n".
602 "\t$line\n" unless defined $fe;
604 $self->addmacro("\\fontencoding{$fe}");
606 } elsif ($line =~ /^! LaTeX Error: Command .* (not provided|unavailable)/) {
608 ($line =~ /^! LaTeX Error: Command (.*) (not provided|unavailable)/);
609 unless (defined $macro) {
610 warn "$filename:$.: Could not identify macro in:\n".
611 "\t$line\n" unless defined $macro;
613 $macro = "\\$macro" unless $macro =~ /^\\/;
614 $self->addmacro("$macro");
616 } elsif ($line =~ /^! Package ucs Error: Unknown .* tag '.*' \((.*)\)\.$/) {
618 ($line =~ /^! Package ucs Error: Unknown .* tag '.*' \((.*)\)\.$/);
619 unless (defined $macro) {
620 warn "$filename:$.: Could not identify macro in:\n".
623 $macro = "\\$macro" unless $macro =~ /^\\/;
624 $self->addmacro("$macro");
626 } elsif ($line =~ /^! Font .*=([^=]+) at .* not loadable: Metric \(TFM\)/) {
627 my ($tfm) = ($line =~ /^! Font .*=([^=]+) at .* not loadable: Metric \(TFM\)/);
628 unless (defined $tfm) {
629 warn "$filename:$.: Could not identify TFM filename in:\n".
633 $self->addmacro("$tfm.tfm");
642 my ($self,$macro) = @_;
643 my $macros = $self->{missingmacros};
644 return $$macros{$macro} if $$macros{$macro};
645 my $res = $self->getmacro($macro);
646 $$macros{$macro} = $res if defined $res;
651 my ($self,$pos) = @_;
652 my $file = $self->{datafile};
653 $file->setpos($pos) or
654 die "Could not seek in datafile to pos $pos";
658 sub getmissingmacros($) {
660 return $self->{missingmacros};
665 my $macros = $self->{missingmacros};
667 for my $m (keys %$macros) {
668 $str .= $$macros{$m}->as_text($f);
675 $self->{macros} = undef;
676 $self->{features} = undef;
677 $self->{missingmacros} = undef;
678 close $self->{datafile};
682 my ($self,$feature) = @_;
683 return new DiscoverMacro::Feature($self,$feature);
686 sub scanformacro($%) {
687 my ($self,%args) = @_;
688 my $file = $args{file};
689 my $fh = new IO::File($file,"r") or
690 die "Could not open $file for reading: $!";
692 # {\newenvironment}{\renewenvironment}{\newif}
693 my $newcommand = '\\\\newcommand|\\\\renewcommand|\\\\providecommand|'.
694 '\\\\DeclareRobustCommand|\\\\def';
695 my $newenvironment = '\\\\newenvironment|\\\\renewenvironment';
697 while (defined ($line = <$fh>)) {
700 $line =~ s{($newcommand) \s* \{? (\\[a-zA-Z@]+) \}?}{
702 my ($env) = ($2 =~ /^\\end(.*)$/);
704 print "ENV: $env\n"; }
706 $line =~ s{($newenvironment) \s* \{ ([^\}]+) \} }{
710 print Dumper [\@macros];
717 my $obj = new DiscoverMacro;
719 $obj->readlog($file);
721 $obj->addmacro($file);
723 print $obj->as_text('text/plain');
727 sort { my @a = stat $a; my @b = stat $b; $b[9] <=> $a[9] }
728 grep { $_ ne 'missfont.log' } (<*.log>);
729 die "No logfile found" if (!defined $file);
730 print "Using logfile $file.\n";
736 my $obj = new DiscoverMacro;
737 $obj->scanformacro(file => 'test.tex');
743 caller || &main(@ARGV);