#! /usr/bin/perl ################################################################################## # # # expand_macros.pl -- recursively expand greenstone macros / 1080805 - 1230106 # # # # Copyright (C) 2005,2006 Jens Wille # # # # This program is free software; you can redistribute it and/or # # modify it under the terms of the GNU General Public License # # as published by the Free Software Foundation; either version 2 # # of the License, or (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the Free Software # # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # # ################################################################################## # # expand_macros.pl reads in specified greenstone macro files and prints the # definitions of requested macros and recursively the definitions of macros # used within these definitions. # # # FEATURES: # # - generic: # # - additional/collection-specific macro files can be included # # - macros set within the server can be included (though this might not be # of much help without reading the respective source file) # # - "interactive browse mode" where you can select which macro (and from which # package) to display next. # # - readline support and persistent history # # - interactive commands # # - read files from within interactive browse mode # # - batch mode only: # # - search for macros that use certain macros ("reverse" search) # # - search for strings (regular expressions) in macro definitions # # # TODO: # # - add "reverse search" and "string search" for browse mode # # - handle macro options better (v, c, l, ...?) # # - implement some kind of "persistent" macro db (so that it doesn't need to be # built each and every time) # # # KNOWN ISSUES: # # - for a sufficiently large file (> 12288 bytes == 12 k) (paged-)read will quit # the program if not scrolled until the end of the file => SIGPIPE: broken pipe! # SOLVED: the PIPE signal will simply be IGNOREd # use strict; use warnings; use Getopt::Long qw(GetOptions); use File::Basename qw(basename dirname); use File::Spec::Functions qw(catdir catfile curdir); use IO::Handle qw(autoflush); STDOUT->autoflush(1); use Term::ReadLine; ### progname and version my $NAME = basename $0; my $VERSION = '0.22'; ### global patterns # my understanding of greenstone macro names: # - enclosed in underscores ('_') # - starts with a letter ([:alpha:]) # - followed by alphanumeric characters ([:alnum:]) # (consequently, it doesn't start with a number and # particularly isn't a macro parameter, i.e. something like _1_) # - also we might need to take care of escaped underscores ('\_') # # => does this fit??? (see '/src/lib/display.cpp' for details, # in particular the 'displayclass::loadparammacros' method) my $MACRO_PATTERN = '[[:alpha:]][[:alnum:]]*'; # package names: letters my $PACKAGE_NAME = '[[:alpha:]]+'; # package specification: package name followed by a colon (':') my $PACKAGE_PATTERN = $PACKAGE_NAME . ':'; # beginning/end of macro specification: underscore ('_'), not escaped, # i.e. not preceded by a backslash ('\') # (need to double-escape backslash here!) my $MACRO_AFFIX = '(? 0, 'version' => 0, 'depth' => 0, 'short' => 0, 'reverse' => 0, 'interactive' => 0, 'paged' => 0, 'pager' => $ENV{'PAGER'} || 'less', 'histfile' => catfile($ENV{'HOME'} || curdir, '.expand_macros.history'), 'histsize' => 100, 'macro_dirs' => [catdir($ENV{'GSDLHOME'}, 'macros')], 'source_dir' => $ENV{'GSDLSOURCE'} || '', 'args' => [] ); # global vars my $TERM = ''; my $CURRENT_FILE = ''; # usage information and help text my $USAGE = <] [-r] {_[:]_|} ... $NAME [generic-options] {-b|-i} [-p] $NAME [-h|-?|--help] generic options are: [-v] [-e ,...] [-n ] HERE_USAGE my $HELP = <,... paths to extra macro directories, comma-separated list [default directory: '$ARG{'macro_dirs'}[0]'] --source path to greenstone source directory, so that macros which are set within the server will be included [default: '$ARG{'source_dir'}'] NOTE: you can set an environment variable GSDLSOURCE instead {-n|--show-version} print only macro definitions for specified version (0=graphic/1=text) [default: '$ARG{'version'}', set to '-1' for 'all'] -s, --short short output, i.e. only macro names, no content batch mode: {-d|--depth} how deep to recurse through macros [default: '$ARG{'depth'}', set to '-1' for 'unlimited'] -r, --reverse reverse search, recursively outputs the macros which use the specified macro all non-option arguments will be treated as - macro names (denoted by surrounding underscores '_') or - regular expressions to search for in macro definitions (otherwise) (you can restrict your macro name query to a certain package by prepending the macro name with ':') EXAMPLES: get definition of the 'pagescriptextra' macro > $NAME _pagescriptextra_ get definition of the 'pagescriptextra' macro, package 'query' only > $NAME _query:pagescriptextra_ get definition of the 'pagescriptextra' macro, package 'query' only -- and recursively get definitions of macros used within that definition (up to level 2) > $NAME -d 2 _query:pagescriptextra_ get all the macros that use the 'pagescriptextra' macro (names only) > $NAME -r -s _pagescriptextra_ interactive browse mode: -b, -i, --browse interactive browse mode, allows you to select what to display next -p, --paged data output will be passed to a pager [default: '$ARG{'pager'}'] --pager pass paged output to specified pager instead of above named default --histfile path to history file to keep history between sessions [default: '$ARG{'histfile'}'] --histsize maximum number of lines to keep in histfile [default: '$ARG{'histsize'}'] NOTE: in case you don\'t want the history to be stored you may set to '' or to '0' (however, this does not remove any existing history files) NOTE: for this script to run your greenstone environment needs to be set up (GSDLHOME set) HERE_HELP my $HELP_INTERACTIVE = <+d (+z on windows), or by typing '.q', or '.quit' commands: .h, .?, .help display this help .q, .quit exit program . redisplay current stage .. return to previous stage (same as leaving empty) ..., .t, .top return to top to enter new macro name {.n|.show-version} [] print only macro definitions for specified version (0=graphic/1=text) [default: '0', set to '-1' for 'all'] .s, .short short output, i.e. only macro names, no content .p, .paged data output will be passed to a pager [default: '$ARG{'pager'}'] .pager [] pass paged output to specified pager instead of above named default .r, .read [] display the contents of the specified file (by default the last file we came across) .pr, .paged-read [] same, but paged (without turning on paged mode permanently) .c, .config display current configuration HERE_HELP # allow for bundling of options Getopt::Long::Configure ("bundling"); # parse arguments GetOptions( 'help|h|?' => sub { print "$HELP\n"; exit 0 }, 'verbose|v' => \$ARG{'verbose'}, 'only-version|n=i' => \$ARG{'version'}, 'extra|e=s' => \@{$ARG{'macro_dirs'}}, 'source=s' => \$ARG{'source_dir'}, 'depth|d=i' => \$ARG{'depth'}, 'short|s' => \$ARG{'short'}, 'reverse|r' => \$ARG{'reverse'}, 'browse|b|i' => \$ARG{'interactive'}, 'paged|p' => \$ARG{'paged'}, 'pager=s' => \$ARG{'pager'}, 'histfile=s' => \$ARG{'histfile'}, 'histsize=i' => \$ARG{'histsize'}, '<>' => sub { push(@{$ARG{'args'}} => @_) } ) or die "$USAGE\n"; ### some sanity checks (dunno which one to check first ;-) # need one of our "actions": batch, query or interactive # ("batch" requiring at least one macro name or regexp specified) unless (@{$ARG{'args'}} || $ARG{'interactive'}) { warn "$USAGE"; warn <= @{$ARG{'args'}}; } } else { # interactive browse mode # ignore 'broken pipe' error $SIG{'PIPE'} = 'IGNORE'; # get the pager executable (no need to test if not in "paged" mode) get_pager() if $ARG{'paged'}; # create new Term::ReadLine object $TERM = Term::ReadLine->new($NAME); # don't want the prompt underlined $TERM->ornaments(0); # don't want autohistory (can't set autohistory explicitly, so use this "workaround") $TERM->MinLine(undef); # restore history readhist(); # print help hint print < "); # remove surrounding '_' $macro =~ s/^_//; $macro =~ s/_$//; exit 0 unless length $macro; # normal exit next if $macro eq '0' || $macro eq '-1'; # a command was executed # now get all packages for given macro, and begin recursion... recurse_packages($macro); } # can't expect anything down here to be executed } END { if ($ARG{'interactive'}) { # save history savehist(); } } ### that's it ;-) exit 0; ### subroutines # # build hash of macro information ("macro db") # # hash structure: # macro # -> package # -> {'0=graphic'|'1=text'} # -> 'file' # -> 'line' # -> 'content' # # usage: # %macro_db = build_db() # # => macro_db: returned hash ("macro db") # sub build_db { my %macro_db = (); my @dm_list = (); my ($n, $m) = (0, 0); # get all macro files (*.dm) from specified directories foreach my $dir (@{$ARG{'macro_dirs'}}) { opendir(DIR, "$dir") or die "can't read macro directory '$dir': $!\n"; push(@dm_list => map { $_ = catfile($dir, $_) } grep { /\.dm$/ } readdir(DIR)); closedir DIR; } # now parse each macro file and build hash foreach my $dm (sort @dm_list) { open(DM, "< $dm") or die "can't open macro file '$dm': $!\n"; my ($name, $content, $version, $curpkg, $contd) = ('', '', '0', '', 0); while (my $line = ) { chomp($line); next unless length $line; # skip empty lines next if $line =~ /^\s*$/; # skip "empty" lines next if $line =~ /^\s*#/; # skip comments (hope this doesn't affect # cases we actually wanted to keep) if ($line =~ /^$PACKAGE_DECLARATION\s*($PACKAGE_NAME)/) { # remember the current package we are in $curpkg = $1; } elsif ($line =~ /$MACRO_AFFIX($MACRO_PATTERN)$MACRO_AFFIX\s*(\[v=1\])?\s*$DEFINITION_START\s*(.*)/) { # start of macro definition $n++; $name = $1; $version = (defined $2 && $2 eq '[v=1]') ? '1' : '0'; $content = $3 || ''; # don't include unnecessary version, unless we're interactive (where version may change during session) next if $ARG{'version'} ne '-1' && $version ne $ARG{'version'} && ! $ARG{'interactive'}; if (exists $macro_db{$name}->{$curpkg}->{$version}) { # everytime a macro definition already exists, it's simply # overwritten - but we can give a warning # (this might also serve debugging purposes) $m++; warn <{$curpkg}->{$version}->{'file'}, line $macro_db{$name}->{$curpkg}->{$version}->{'line'}) HERE_WARN } # store the information we got so far $macro_db{$name}->{$curpkg}->{$version}->{'file'} = $dm; $macro_db{$name}->{$curpkg}->{$version}->{'line'} = $.; $macro_db{$name}->{$curpkg}->{$version}->{'content'} = [$content] if length $content; # is the macro definition already finished? $contd = ($content =~ s/\s*$DEFINITION_END.*//) ? 0 : 1; } elsif ($contd) { # continuation of macro definition # store additional content push(@{$macro_db{$name}->{$curpkg}->{$version}->{'content'}} => $line); # is the macro definition already finished? $contd = ($line =~ s/\s*$DEFINITION_END.*//) ? 0 : 1; } else { # something else... ($name, $content) = ('', ''); } } close DM; } # get server macros (overwriting already read macros) if (length $ARG{'source_dir'}) { if (-r $ARG{'source_dir'}) { my $recpt_dir = catdir($ARG{'source_dir'}, 'src', 'src', 'recpt'); my @cpp_list = (); opendir(DIR, "$recpt_dir") or die "can't read receptionist's source directory '$recpt_dir': $!\n"; push(@cpp_list => map { $_ = catfile($recpt_dir, $_) } grep { /\.cpp$/ } readdir(DIR)); close DIR; foreach my $cpp (@cpp_list) { open(CPP, "< $cpp") or die "can't open source file '$cpp': $!\n"; my $args = ''; my $contd = 0; while (my $line = ) { next unless $line =~ /disp\.setmacro\s*\((.*)/ || $contd; unless (defined $1) { $contd = 1; next; } my $string = $1; if ($string =~ s/\);\s*$//) { $args .= $string; my ($name, $package, $value) = split(/\s*,\s*/ => $args, 3); $name =~ s/^\s*["']?//; $name =~ s/["']?\s*$//; $package =~ s/^\s*["']?//; $package =~ s/["']?\s*$//; $package = 'Global' if $package eq 'displayclass::defaultpackage'; $macro_db{$name}->{$package}->{'0'}->{'file'} = 'SERVER: ' . $cpp; $macro_db{$name}->{$package}->{'0'}->{'line'} = $.; $macro_db{$name}->{$package}->{'0'}->{'content'} = [$value]; $args = ''; ++$n; $contd = 0; } elsif ($contd) { $args .= ' ' . $string; } else { $contd = 1; } } close CPP; } } else { warn "can't find source directory '$ARG{'source_dir'}'! server macros will not be included\n"; } } # print some statistics print "$n total macro definitions, $m duplicates\n" if $ARG{'verbose'}; # we stored all information there is so we can return it return %macro_db; } # # # recursively print macro information # # usage: # get_macro($macro[, $level]) # # macro: macro name (optionally including package specification) # level: recursion level (optional) # # => VOID CONTEXT # sub get_macro { my ($macro, $level) = @_; $level ||= 0; # indent output according to recursion level my $indent = ' ' x $level; # get all the packages which our macro is defined in ($macro, my @packages) = get_packages($macro, $indent); return unless @packages; # macro definitions may occur in several packages so we display them all # (unless a certain package was explicitly specified) foreach my $pkg (@packages) { foreach my $version (sort keys %{$macro_db{$macro}->{$pkg}}) { print "$indent* $pkg:$macro [v=$version] ($macro_db{$macro}->{$pkg}->{$version}->{'file'}, line $macro_db{$macro}->{$pkg}->{$version}->{'line'})\n"; my $content = ''; # some macros are defined, but don't have any content if (defined $macro_db{$macro}->{$pkg}->{$version}->{'content'}) { # for batch display we condense the output a little bit... map { s/^\s*//; s/\s*$// } @{$macro_db{$macro}->{$pkg}->{$version}->{'content'}}; # ...and put it on a single line $content = join(' ' => @{$macro_db{$macro}->{$pkg}->{$version}->{'content'}}); } print "$indent { $content }\n\n" unless $ARG{'short'}; # short display only, i.e. no content # of the macro's definition # only go (deeper) into referenced macros if we # haven't reached the specified recursion level if ($ARG{'depth'} eq '-1' || $level < $ARG{'depth'}) { # get (referencing|referenced) macros... my @refs = $ARG{'reverse'} ? get_r_macros($macro) : get_macros($content); # ...and recurse above them (with increased recursion level) foreach my $ref (@refs) { get_macro($ref, $level + 1); } } } } } # # # returns a list of macros extracted from a content string # or a boolean value if a macro name was specified # # usage: # @macros = get_macros($content) # $boolean = get_macros($content, $macro) # # content: content string # macro: macro name # # => macros: list of macros # => boolean: boolean value (true = 1 / false = empty list) # sub get_macros { my ($content, $macro) = @_; my @macro_list = (); my %seen = (); # get each macro reference in the string # (for macro name considerations see above) while ($content =~ /$MACRO_AFFIX((?:$PACKAGE_PATTERN)?$MACRO_PATTERN)$MACRO_AFFIX/g) { my $m = $1; # we want to skip some macros that have no content anyway (defined # from within the server) - unless we're doing a "reverse" search next if $seen{$m}++ || ($m =~ /^(cgiarg.*|histvalue\d+|if|httpimg|httpweb|gwcgi|(decoded)?compressedoptions)$/i && ! $ARG{'reverse'}); if (defined $macro) { # is this the macro we wanted? then the current # macro uses it => return true return 1 if $m =~ /^(?:$PACKAGE_PATTERN)?$macro$/; } else { # add macro to our list push(@macro_list => $m); } } # return the list of used macros # (this evaluates to false (empty list) if there are no further # macro calls or if this macro doesn't use the sought-after macro) return sort @macro_list; } # # # returns a list of macro names which reference ("use") the # specified macro or match the query # # usage: # @macros = get_r_macros($macro) # @macros = get_r_macros($query, $is_query) # # macro: macro name # query: query string (regular expression) # is_query: boolean value to indicate whether arg is a query or a macro # # => macros: list of macros # sub get_r_macros { my ($arg, $query) = @_; $query ||= 0; my %refs = (); # need to test each single macro's... foreach my $m (sort keys %macro_db) { # ...each single package foreach my $p (sort keys %{$macro_db{$m}}) { foreach my $v (sort keys %{$macro_db{$m}->{$p}}) { my $pm = "$p:$m"; # include package information in the macro name! # does this macro have any content? if (defined $macro_db{$m}->{$p}->{$v}->{'content'}) { # stringify content! my $content = join(' ' => @{$macro_db{$m}->{$p}->{$v}->{'content'}}); if ($query) { # search regexp $refs{$pm}++ if $content =~ /$arg/; } else { # search macro $refs{$pm}++ if get_macros($content, $arg); } } } } } # now we have all the macros which use our sought-after return sort keys %refs; } # # # recurse all packages for a given macro # # usage: # recurse_packages($macro) # # macro: macro name (any package specification will be dropped) # # => VOID CONTEXT # sub recurse_packages { my ($macro) = @_; # repeat until explicit break/exit while (1) { # get all the packages which our macro is defined in #($macro, my @packages) = get_packages($macro); #return unless @packages; my @packages = (); my $n = 0; my $package = ''; # ask for user's selection... do { # get all the packages which our macro is defined in ($macro, @packages) = get_packages($macro); return unless @packages; # ask for user's selection... print "select package for macro '$macro' [leave empty to return]\n"; foreach my $pkg (@packages) { printf " [%d]%s %s\n", ++$n, " " x (4 - length $n), $pkg; } $package = prompt(); $n = 0; # ...until we return... } until ($package eq '' || $package eq '-1' # ...or a valid number is provided || ($package =~ /^\d+$/ && $package > 0 && $package <= @packages)); return unless length $package; # return to previous stage return '-1' if $package eq '-1'; # return to top # set selected package $package = $packages[$package - 1]; foreach my $version (sort keys %{$macro_db{$macro}->{$package}}) { # all versions next unless $ARG{'version'} eq '-1' # desired version || $version eq $ARG{'version'} # fallback to 'graphic' || ($version eq '0' && ! exists $macro_db{$macro}->{$package}->{'1'}); # some macros are defined, but don't have any content my $content = defined $macro_db{$macro}->{$package}->{$version}->{'content'} # now we want to retain the original structure ? join("\n" => @{$macro_db{$macro}->{$package}->{$version}->{'content'}}) : ''; ($CURRENT_FILE = $macro_db{$macro}->{$package}->{$version}->{'file'}) =~ s/^SERVER: //; my $content_string = "* $package:$macro [v=$version] ($macro_db{$macro}->{$package}->{$version}->{'file'}, line $macro_db{$macro}->{$package}->{$version}->{'line'})\n"; $content_string .= "{ $content }\n" unless $ARG{'short'}; print_output($content_string); # now on to the macros referenced within this one my $return = recurse_macros($content); return $return if defined $return; } } } # # # returns list of packages for specified macro, also returns # modified macro name (without surrounding '_' and package specification) # # usage: # ($macro, @packages) = get_packages($macro) # # macro: macro name # # => macro: modified macro name # => packages: list of packages # sub get_packages { my ($macro, $indent) = @_; $indent ||= ''; # save original macro name (including package specification) my $omacro = $macro; my @packages = (); if ($macro =~ /^($PACKAGE_PATTERN)?$MACRO_PATTERN$/) { # valid macro name # strip off package specification my $package = ($macro =~ s/^($PACKAGE_NAME)://) ? $1 : ''; if (exists $macro_db{$macro}) { # valid/existing macro unless ($ARG{'interactive'}) { if (length $package) { # account for package specification @packages = ($package) if exists $macro_db{$macro}->{$package}; } else { # get all packages otherwise @packages = sort keys %{$macro_db{$macro}}; } } else { foreach my $pkg (sort keys %{$macro_db{$macro}}) { push(@packages => $pkg) # all versions if $ARG{'version'} eq '-1' # desired version || exists $macro_db{$macro}->{$pkg}->{$ARG{'version'}} # fallback to 'graphic' || exists $macro_db{$macro}->{$pkg}->{'0'}; } } } } else { # invalid macro name warn "invalid macro name '$macro'!\n"; return; # skip it } # no packages - no definition unless (@packages) { print "$indent- $omacro\n$indent no definition for macro!\n\n"; return; # skip it } # return modified macro name and packages found return $macro, sort @packages; } # # # recurse all macros for a given content string # # usage: # recurse_macros($content) # # content: content string # # => VOID CONTEXT # sub recurse_macros { my ($content) = @_; # repeat until explicit break/exit while (1) { # get all the macros referenced within the current one my @macros = get_macros($content); return unless @macros; my $n = 0; my $macro = ''; # ask for user's selection... do { print "select macro [leave empty to return]\n"; foreach my $m (@macros) { printf " [%d]%s %s\n", ++$n, " " x (4 - length $n), $m; } $macro = prompt(); $n = 0; # ...until we return... } until ($macro eq '' || $macro eq '-1' # ...or a valid number is provided || ($macro =~ /^\d+$/ && $macro > 0 && $macro <= @macros)); return unless length $macro; # return to previous stage return '-1' if $macro eq '-1'; # return to top # set selected macro $macro = $macros[$macro - 1]; # now we want all the macro's packages again my $return = recurse_packages($macro); return $return if defined $return; } } # # # prompt for user input # # usage: # $reply = prompt([$prompt]) # # prompt: optional prompt (default: '> ') # # => reply: user input # sub prompt { my $prompt = shift || '> '; my $term = $TERM; # read user input my $reply = $term->readline($prompt); if (defined $reply) { # add input to history, unless it's just a number $term->addhistory($reply) if $reply =~ /[[:alpha:]]/; if ($reply =~ s/^\s*["']*\s*\././) { # execute command my $return = parse_command($reply); return $return if defined $return; } else { return $reply; } } # allow for exiting by hitting +d, # or quitting by command (.q, .quit) die "\n"; } # # # print output, paged or not # # usage: # print_output($output) # print_output(@output) # # output: text to print # # => VOID CONTEXT # sub print_output { my $output = join('' => @_); if ($ARG{'paged'}) { # pass output to pager open(PAGER, "| $ARG{'pager'}") or die "can't open pipe to '$ARG{'pager'}': $!"; print PAGER "$output"; close PAGER; } else { # print to standard out... print "\n$output\n"; # ...and wait for user reaction to continue wait_for_user(); } } # # # wait for user reaction to continue # # usage: # wait_for_user() # # => VOID CONTEXT # sub wait_for_user { print "[press key to continue]"; print "\n" if ; } # # # prompt for user input # # usage: # parse_command($command_line) # # command_line: command string # # => VOID CONTEXT # sub parse_command { my $command_line = shift; my @commands = split(/\s*;\s*/ => $command_line); my $return = 0; foreach my $command (@commands) { my $msg = "command executed: '$command'"; $command =~ s/^\.//; $command =~ s/^(\w+)["']*/$1/; $command =~ s/\s*$//; if ($command =~ /^(h|\?|help)$/) { print "$HELP_INTERACTIVE\n"; # wait for user reaction to continue wait_for_user(); next; } elsif ($command =~ /^(q|quit)$/) { return undef; } elsif ($command =~ /^(\.)$/) { $return = ''; next; } elsif ($command =~ /^(\..|t|top)$/) { $return = '-1'; next; } elsif ($command =~ /^(n|show-version)(?:\s+["']*(0|1|-1)["']*)?$/) { $ARG{'version'} = $2 if defined $2; $msg = "'version' " . (defined $2 ? '' : 'is currently ') . "set to: '$ARG{'version'}'"; } elsif ($command =~ /^(s|short)$/) { $ARG{'short'} = ! $ARG{'short'}; $msg = "'short' output " . ($ARG{'short'} ? 'en' : 'dis') . "abled"; } elsif ($command =~ /^(p|paged)$/) { $ARG{'pager'} = get_pager(); $ARG{'paged'} = ! $ARG{'paged'} if -x $ARG{'pager'}; $msg = "'paged' output " . ($ARG{'paged'} ? 'en' : 'dis') . "abled"; } elsif ($command =~ /^(pager)(?:\s+["']*(\w+)["']*)?$/) { $ARG{'pager'} = get_pager($2) if defined $2; $msg = "'pager' " . (defined $2 ? '' : 'is currently ') . "set to: '$ARG{'pager'}'"; } elsif ($command =~ /^(p|paged-)?(r|read)(?:\s+(["']?.+["']?))?$/) { my $paged = $1 || ''; my $file = $3 || $CURRENT_FILE; $CURRENT_FILE = $file; if (-r $file) { open(FILE, "< $file") or die "can't open file '$file': $!\n"; my @lines = ; close FILE; my $previous_paged = $ARG{'paged'}; $ARG{'paged'} = 1 if $paged; #print_output("$file:\n\n", @lines); print_output(@lines); $ARG{'paged'} = $previous_paged; next; } $msg = "can't find file '$file'"; } elsif ($command =~ /^(c|config)$/) { my $short = $ARG{'short'} ? 'enabled' : 'disabled'; my $paged = $ARG{'paged'} ? 'enabled' : 'disabled'; $msg = < # # read history from histfile # # usage: # readhist(); # # => VOID CONTEXT # sub readhist { my $term = $TERM; if (-r $ARG{'histfile'}) { open(HIST, "< $ARG{'histfile'}") or die "can't open histfile '$ARG{'histfile'}': $!\n"; while () { chomp; $term->AddHistory($_); } close HIST; warn "history restored from '$ARG{'histfile'}'\n" if $ARG{'verbose'}; } else { warn "history could not be restored (maybe no/wrong history file specified)\n" if $ARG{'verbose'}; } } # # # save history to histfile # # usage: # savehist(); # # => VOID CONTEXT # sub savehist { return unless length $ARG{'histfile'} && $ARG{'histsize'}; my $term = $TERM; return unless length $term; if (-w $ARG{'histfile'} || (! -e $ARG{'histfile'} && -w dirname $ARG{'histfile'})) { my @history = $term->GetHistory; # drop (consecutive) duplicate entries my @unified = (); my $previous = ''; foreach my $element (@history) { push(@unified => $element) unless $element eq $previous; $previous = $element; } @history = @unified; # cut history to specified maximum number of entries splice(@history, 0, @history - $ARG{'histsize'}) if @history > $ARG{'histsize'}; open(HIST, "> $ARG{'histfile'}") or die "can't open history file '$ARG{'histfile'}' for writing: $!\n"; { local $, = "\n"; print HIST @history, ""; } close HIST; warn "history written to '$ARG{'histfile'}'\n" if $ARG{'verbose'}; } else { warn "history could not be written (maybe no history file specified, or history file not writable)\n" if $ARG{'verbose'}; } } # # # get pager executable # # usage: # $pager = get_pager([$candidate]); # # canidate: candidate for pager executable (defaulting to $ARG{'pager'}) # # => pager: pager executable # sub get_pager { my $candidate = shift || $ARG{'pager'}; return $candidate if -x $candidate; # get first pager executable in PATH foreach my $path (split(':' => $ENV{'PATH'})) { return catfile($path, $candidate) if -x catfile($path, $candidate); } # still no executable! warn "can't find pager '$candidate'! disabling 'paged' output\n"; $ARG{'paged'} = 0; return '-1'; } #