#! /usr/bin/env perl use strict; use FileHandle; use Fcntl qw(SEEK_SET :mode); use DirHandle; use Locale::gettext; use Getopt::Std qw(getopts); use Config; use constant { MENU_ENTRY_SECTION => 0, MENU_ENTRY_TAG => 1, MENU_ENTRY_TARGET_FILE => 2, MENU_ENTRY_TARGET_NODE => 3, MENU_ENTRY_DESCRIPTION => 4, }; use constant { STATE_INITIAL => 0, STATE_MENU => 1, STATE_ENTRY => 2, }; use constant { STAT_DEV => 0, STAT_INODE => 1, STAT_MODE => 2, STAT_SIZE => 7, }; use vars qw(@menu_entries @dir_queue %inodes %tags $td $opt_l $opt_r $opt_u $opt_d); sub split_menu { my ($section, $menu) = @_; return () unless $menu =~ m(^\* \s* ([^:\s]+): \s* [\(] ([^\)]+) [\)] ([^.]*) \. (\s+ (.*))?$)sx ; return [ $section, $1, $2, $3, $5 ]; } sub read_info_file { my $fh = shift; my ($section, $text, $line, $entry); my $state = STATE_INITIAL; while ($line = $fh->getline ()) { if ($state == STATE_INITIAL) { if ($line =~ /^INFO-DIR-SECTION\s+(.*)/) { $section = $1; chomp $section; } elsif ($line =~ /^START-INFO-DIR-ENTRY/) { $state = STATE_ENTRY; } } elsif ($state == STATE_ENTRY) { if ($line =~ /^END-INFO-DIR-ENTRY/) { $state = STATE_INITIAL; } elsif ($line =~ /^\*/) { $text = $line; $state = STATE_MENU; } } elsif ($state == STATE_MENU) { if ($line =~ /^END-INFO-DIR-ENTRY/) { $section ||= $td->get ("Miscellaneous"); $state = STATE_INITIAL; $entry = split_menu ($section, $text); next unless $entry; next if $opt_u && $tags{$entry->[MENU_ENTRY_TAG]}; push @menu_entries, $entry; $tags{$entry->[MENU_ENTRY_TAG]} = 1; } elsif ($line =~ /^\*/) { $section ||= $td->get ("Miscellaneous"); $entry = split_menu ($section, $text); $text = $line, next unless $entry; $text = $line, next if $opt_u && $tags{$entry->[MENU_ENTRY_TAG]}; push @menu_entries, $entry; $tags{$entry->[MENU_ENTRY_TAG]} = 1; $text = $line; } elsif ($line =~ /^\s+/) { $text .= $line; } } } die "$!" if $fh->error; } sub open_info_file { my $name = shift; my ($fh, $prog, @magic); $fh = FileHandle->new; $fh->open ("<$name") or die "$!"; $fh->binmode (); for (0..3) { $fh->read ($magic [$_], 1) or die "$!"; } $fh->close (); $prog = 'gzip' if $magic [0] eq chr 0x1f && $magic [1] eq chr 0x8b; $prog = 'bzip2' if $magic [0] eq 'B' && $magic [1] eq 'Z' && $magic [2] eq 'h'; $prog = 'bzip' if $magic [0] eq 'B' && $magic [1] eq 'Z' && $magic [2] eq '0'; if ($prog) { my $cmd = "$prog -cd < $name |"; $fh->open ($cmd) or die "$!"; } else { $fh->open ("<$name") or die "$!"; } return $fh; } sub read_info_byname { my $name = shift; my $fh = open_info_file ($name); read_info_file ($fh); $fh->close () or die "$?"; } sub is_subsidiary { my $name = shift; return $name =~ /-[0-9]+(\.(gz|bz|bz2))?$/; } sub scan_infos { my $name = shift; my $d = DirHandle->new; $d->open ($name) or die "$!"; my $entry; while ($entry = $d->read ()) { next if $entry eq '.' || $entry eq '..' || $entry =~ /^dir([~.].*)?$/ ; my $fullentry = "$name/$entry"; my @stats = stat ($fullentry) or die "$!"; my $is_plain = not S_ISDIR ($stats [STAT_MODE]); next if $is_plain && is_subsidiary ($entry); my $fid = "$stats[STAT_DEV]:$stats[STAT_INODE]"; next if $inodes{$fid}; if ($is_plain) { read_info_byname ($fullentry); } else { push @dir_queue, $fullentry; } $inodes{$fid} = 1; } $d->close (); } sub compare_menu_entries ($$) { return ($_[0]->[MENU_ENTRY_SECTION] cmp $_[1]->[MENU_ENTRY_SECTION] || $_[0]->[MENU_ENTRY_TAG] cmp $_[1]->[MENU_ENTRY_TAG]); } sub write_items { my $name = shift; my $have_autoformat = eval { require Text::Autoformat }; my $fh = FileHandle->new; $fh->open (">$name") or die "$!"; $fh->printf ($td->get ("%s\nThis is the file .../info/dir, which contains the\ topmost node of the Info hierarchy, called (dir)Top.\ The first time you invoke Info you start off looking at this node.\ \x1f\ %s\tThis is the top of the INFO tree\ \ This (the Directory node) gives a menu of major topics.\ Typing \"q\" exits, \"?\" lists all Info commands, \"d\" returns here,\ \"h\" gives a primer for first-timers,\ \"mEmacs\" visits the Emacs manual, etc.\ \ In Emacs, you can click mouse button 2 on a menu item or cross reference\ to select it.\ \ %s\ "), "-*- Text -*-", "File: dir,\tNode: Top", "* Menu:"); my $section = ''; foreach my $menu_entry (sort compare_menu_entries @menu_entries) { if (($section cmp $menu_entry->[MENU_ENTRY_SECTION]) != 0) { $section = $menu_entry->[MENU_ENTRY_SECTION]; $fh->print ("\n"); $fh->print ($section); $fh->print ("\n"); } $fh->printf ("* %s: (%s)%s.\n", $menu_entry->[MENU_ENTRY_TAG], $menu_entry->[MENU_ENTRY_TARGET_FILE], $menu_entry->[MENU_ENTRY_TARGET_NODE]); if ($have_autoformat) { $fh->print (Text::Autoformat::autoformat ($menu_entry->[MENU_ENTRY_DESCRIPTION], { left => $opt_l, right => $opt_r })) if $menu_entry->[MENU_ENTRY_DESCRIPTION]; } else { map { $fh->printf ("%s%s\n", ' ' x $opt_l, $_); } (split /\n/, $menu_entry->[MENU_ENTRY_DESCRIPTION]) if $menu_entry->[MENU_ENTRY_DESCRIPTION]; } } $fh->close (); } sub usage { print $td->get ( <<'EOF' usage: generate-info-dir [-l LEFT] [-r RIGHT] [-d FILE] [-u] DIR ... DIR: defaults to contents of INFOPATH, if that is not set, "/usr/share/info" LEFT: left margin for description formatting; defaults to 12 RIGHT: right margin for description formatting; defaults to 72 FILE: the dirfile to write; defaults to DIR/dir -u: ensure each menu entry tag is unique EOF ); exit $_[0]; } sub main { $td = Locale::gettext->domain ('texinfo'); getopts ('l:r:d:u') or usage (1); $opt_l ||= 12; $opt_r ||= 72; if (@ARGV) { push @dir_queue, @ARGV; } elsif ($ENV{'INFOPATH'}) { push @dir_queue, split ($Config{'path_sep'}, $ENV{'INFOPATH'}); } else { push @dir_queue, '/usr/share/info'; } $opt_d ||= $dir_queue [0] . '/dir'; while (my $info_dir = shift @dir_queue) { scan_infos ($info_dir); } write_items ("$opt_d~~"); rename ($opt_d, "$opt_d~") || die "$!" if -r $opt_d; rename ("$opt_d~~", $opt_d) || die "$!"; 1; } main ();