package Getopt::Tabular;
# Getopt/Tabular.pm
#
# Perl5 package for table-driven argument parsing, somewhat like Tk's
# ParseArgv. To use the package, you just have to set up an argument
# table (a list of array references), and call &GetOptions (the name
# is exported from the module). &GetOptions takes two arguments:
# a reference to your argument table (which is not modified), and
# a reference to the list of command line arguments, e.g. @ARGV (or
# a copy of it). Both arrays are unmodified, but a new version of
# @ARGV (or whatever) is returned with options and their arguments
# removed, so you can easily pluck out the leftover arguments.
#
# The argument table consists of one element per valid command-line option;
# each element should be a reference to a list of the form:
#
# ( option_name, type, num_values, option_data, help_string, arg_desc )
#
# See Getopt/Tabular.pod for complete information.
#
# originally by Greg Ward 1995/07/06-07/09 as ParseArgs.pm
# renamed to Getopt::Tabular and somewhat reorganized/reworked,
# 1996/11/08-11/10
#
# $Id: Tabular.pm,v 1.2 1996/11/11 16:28:28 greg Exp $
# Copyright (c) 1995-96 Greg Ward. All rights reserved. This program is
# free software; you can redistribute it and/or modify it under the same
# terms as Perl itself.
require Exporter;
use Carp;
use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
use vars qw/%Patterns %OptionHandlers %TypeDescriptions @OptionPatterns
$OptionTerminator $AllowTermination $HelpOption
$LongHelp $Usage $ErrorClass $ErrorMessage/;
$VERSION = 0.1;
@ISA = qw/Exporter/;
@EXPORT = qw/GetOptions/;
@EXPORT_OK = qw/SetHelp SetHelpOption SetError GetError/;
# -------------------------------------------------------------------- #
# Private global variables #
# -------------------------------------------------------------------- #
# The regexp for floating point numbers here is a little more permissive
# than the C standard -- it recognizes "0", "0.", ".0", and "0.0" (where 0
# can be substituted by any string of one or more digits), preceded by an
# optional sign, and followed by an optional exponent.
%Patterns = ('integer' => '[+-]?\d+',
'float' => '[+-]? ( \d+(\.\d*)? | \.\d+ ) ([Ee][+-]?\d+)?',
'string' => '.*');
# This hash defines the allowable option types, and what to do when we
# see an argument of a given type in the argument list. New types
# can be added by calling AddType, as long as you supply an option
# handler that acts like one of the existing handlers. (Ie. takes
# the same three arguments, returns 1 for success and 0 for failure,
# and calls SetError appropriately.)
%OptionHandlers = ("string", \&process_pattern_option,
"integer", \&process_pattern_option,
"float", \&process_pattern_option,
"boolean", \&process_boolean_option,
"const", \&process_constant_option,
"copy", \&process_constant_option,
"arrayconst",\&process_constant_option,
"hashconst", \&process_constant_option,
"call", \&process_call_option,
"eval", \&process_eval_option,
"section", undef);
# This hash is used for building error messages for pattern types. A
# subtle point is that the description should be such that it can be
# pluralized by adding an "s". OK, OK, you can supply an alternate
# plural form by making the description a reference to a two-element list,
# singular and plural forms. I18N fanatics should be happy.
%TypeDescriptions = ("integer" => "integer",
"float" => "floating-point number",
"string" => "string");
@OptionPatterns = ('(-)(\w+)'); # two parts: "prefix" and "body"
$OptionTerminator = "--";
$HelpOption = "-help";
$ErrorClass = ""; # can be "bad_option", "bad_value",
# "bad_eval", or "help"
$ErrorMessage = ""; # can be anything
# -------------------------------------------------------------------- #
# Public (but not exported) subroutines used to set options before #
# calling GetOptions. #
# -------------------------------------------------------------------- #
sub SetHelp
{
$LongHelp = shift;
$Usage = shift;
}
sub SetOptionPatterns
{
@OptionPatterns = @_;
}
sub SetHelpOption
{
$HelpOption = shift;
}
sub SetTerminator
{
$OptionTerminator = shift;
}
sub UnsetTerminator
{
undef $OptionTerminator;
}
sub AddType
{
my ($type, $handler) = @_;
$OptionHandlers{$type} = $handler;
}
sub AddPatternType
{
my ($type, $pattern, $description) = @_;
$OptionHandlers{$type} = \&process_pattern_option;
$Patterns{$type} = $pattern;
$TypeDescriptions{$type} = ($description || $type);
}
sub GetPattern
{
my ($type) = @_;
$Patterns{$type};
}
sub SetError
{
$ErrorClass = shift;
$ErrorMessage = shift;
}
sub GetError
{
($ErrorClass, $ErrorMessage);
}
# --------------------------------------------------------------------
# Private utility subroutines:
# print_help
# scan_table
# match_abbreviation
# option_error
# check_value
# split_option
# find_calling_package
# --------------------------------------------------------------------
#
# &print_help
#
# walks through an argument table and prints out nicely-formatted
# option help for all entries that provide it. Also does the Right Thing
# (trust me) if you supply "argument description" text after the help.
#
# Don't read this code if you can possibly avoid it. It's pretty gross.
#
sub print_help
{
confess ("internal error, wrong number of input args to &print_help")
if (scalar (@_) != 1);
my ($argtable) = @_;
my ($maxoption, $maxargdesc, $numcols, $opt, $breakers);
my ($textlength, $std_format, $alt_format);
my ($option, $type, $help, $argdesc);
$maxoption = 0;
$maxargdesc = 0;
foreach $opt (@$argtable)
{
my ($optlen, $argdesclen);
my ($option, $type, $help, $argdesc) = @{$opt} [0,1,4,5];
next if $type eq "section" or ! defined $help;
($option) = &split_option ($opt) if $type eq "boolean";
$optlen = length ($option);
$maxoption = $optlen if ($optlen > $maxoption);
if (defined $argdesc)
{
$argdesclen = length ($argdesc);
$maxargdesc = $argdesclen if ($argdesclen > $maxargdesc);
}
}
# We need to construct and eval code that looks something like this:
# format STANDARD =
# @<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# $option, $help
# ~~ ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# $help
# .
#
# with an alternative format like this:
# format ALTERNATIVE =
# @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# $option, $argdesc
# ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# $help
# ~~ ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# $help
# .
# in order to nicely print out the help. Can't hardcode a format,
# though, because we don't know until now how much space to allocate
# for the option (ie. $maxoption).
$breakers = $:;
$: = " \n";
chop ($numcols = `tput cols`); # should use Term::Cap here, I s'pose
$numcols = 80 if $? || !$numcols;
$textlength = $numcols - 3 - $maxoption - 2;
$std_format = "format STANDARD =\n" .
" @" . ("<" x $maxoption) . " ^" . ("<" x ($textlength-1)) . "\n".
"\$option, \$help\n" .
"~~ " . (" " x $maxoption) . " ^" . ("<" x ($textlength-1)) . "\n" .
"\$help\n.";
$alt_format = "format ALTERNATIVE =\n" .
" @" . ("<" x ($maxoption + $maxargdesc)) . "\n" .
"\$option\n" .
" " . (" " x $maxoption) . " ^" . ("<" x ($textlength-1)) . "\n" .
"\$help\n" .
"~~ " . (" " x $maxoption) . " ^" . ("<" x ($textlength-1)) . "\n" .
"\$help\n.";
eval $std_format;
confess ("internal error with format \"$std_format\": $@") if $@;
eval $alt_format;
confess ("internal error with format \"$alt_format\": $@") if $@;
my $save_fmt = $~;
print $LongHelp . "\n" if defined $LongHelp;
print "Summary of options:\n";
foreach $opt (@$argtable)
{
($option, $type, $help, $argdesc) = @{$opt}[0,1,4,5];
if ($type eq "section")
{
printf "\n-- %s %s\n", $option, "-" x ($numcols-4-length($option));
next;
}
next unless defined $help;
$argdesc = "" unless defined $argdesc;
($option) = &split_option ($opt) if $type eq "boolean";
$~ = 'STANDARD';
if ($argdesc)
{
my $expanded_option = $option . " " . $argdesc if $argdesc;
$option = $expanded_option;
if (length ($expanded_option) > $maxoption+1)
{
$~ = 'ALTERNATIVE';
}
}
write;
}
$: = $breakers;
$~ = $save_fmt;
print "\n";
print $Usage if defined $Usage;
}
#
# &scan_table
#
# walks through an argument table, building a hash that lets us quickly
# and painlessly look up an option.
#
sub scan_table
{
my ($argtable, $arghash) = @_;
my ($opt, $option, $type, $value);
my $i;
for $i (0 .. $#$argtable)
{
$opt = $argtable->[$i];
($option, $type, $value) = @$opt;
unless (exists $OptionHandlers{$type})
{
croak "Unknown option type \"$type\" supplied for option $option";
}
if ($type eq "boolean")
{
my ($pos,$neg) = &split_option($opt);
$arghash->{$pos} = $i;
$arghash->{$neg} = $i if defined $neg;
}
elsif ($type ne "section")
{
$arghash->{$option} = $i;
}
}
}
#
# &match_abbreviation
#
# Given a string $s and a list of words @$words, finds the word for which
# $s is a non-ambiguous abbreviation. If $s is found to be ambiguous or
# doesn't match, a clear and concise error message is printed, using
# $err_format as a format for sprintf. Suggested form for $err_format is
# "%s option: %s"; the first %s will be substituted with either "ambiguous"
# or "unknown" (depending on the problem), and the second will be
# substituted with $s. Thus, with this format, the error message will look
# something like "unknown option: -foo" or "ambiguous option: -f".
#
sub match_abbreviation
{
my ($s, $words, $err_format) = @_;
my ($match);
my $word;
foreach $word (@$words)
{
# If $s is a prefix of $word, it's at least an approximate match,
# so try to do better
next unless ($s eq substr ($word, 0, length ($s)));
# We have an exact match, so return it now
return $word if ($s eq $word);
# We have an approx. match, and already had one before
if ($match)
{
&SetError ("bad_option", sprintf ("$err_format", "ambiguous", $s));
return 0;
}
$match = $word;
}
&SetError ("bad_option", sprintf ("$err_format", "unknown", $s))
if !$match;
$match;
}
#
# &option_error
#
# Constructs a useful error message to deal with an option that expects
# a certain number of values of certain types, but a command-line that
# falls short of this mark. $option should be the option that triggers
# the situation; $type should be the expected type; $n should be the
# number of values expected.
#
# The error message (returned by the function) will look something like
# "-foo option must be followed by an integer" (yes, it does pick "a"
# or "an", depending on whether the description of the type starts
# with a vowel) or "-bar option must be followed by 3 strings".
#
# The error message is put in the global $ErrorMessage, as well as returned
# by the function. Also, the global $ErrorClass is set to "bad_value".
#
sub option_error
{
my ($option, $type, $n) = @_;
my ($typedesc, $singular, $plural, $article, $desc);
$typedesc = $TypeDescriptions{$type};
($singular,$plural) = (ref $typedesc eq 'ARRAY')
? @$typedesc
: ($typedesc, $typedesc . "s");
$article = ($typedesc =~ /^[aeiou]/) ? "an" : "a";
$desc = ($n > 1) ?
"$n $plural" :
"$article $singular";
&SetError ("bad_value", "$option option must be followed by $desc");
}
#
# &check_value
#
# Verifies that a value (presumably from the command line) satisfies
# the requirements for the expected type.
#
# Calls &option_error (to set $ErrorClass and $ErrorMessage globals) and returns
# 0 if the value isn't up to scratch.
#
sub check_value
{
my ($val, $option, $type, $n) = @_;
unless (defined $val && $val =~ /^$Patterns{$type}$/x)
{
&option_error ($option, $type, $n);
return 0;
}
}
#
# &split_option
#
# Splits a boolean option into positive and negative alternatives. The
# two alternatives are returned as a two-element array.
#
# Croaks if it can't figure out the alternatives, or if there appear to be
# more than 2 alternatives specified.
#
sub split_option
{
my ($opt_desc) = @_;
my ($option, @options);
$option = $opt_desc->[0];
return ($option) if $opt_desc->[1] ne "boolean";
@options = split ('\|', $option);
if (@options == 2)
{
return @options;
}
elsif (@options == 1)
{
my ($pattern, $prefix, $positive_alt, $negative_alt);
for $pattern (@OptionPatterns)
{
my ($prefix, $body);
if (($prefix, $body) = $option =~ /^$pattern$/)
{
$negative_alt = $prefix . "no" . $body;
return ($option, $negative_alt);
}
}
croak "Boolean option \"$option\" did not match " .
"any option prefixes - unable to guess negative alternative";
return ($option);
}
else
{
croak "Too many alternatives supplied for boolean option \"$option\"";
}
}
#
# &find_calling_package
#
# walks up the call stack until we find a caller in a different package
# from the current one. (Handy for `eval' options, when we want to
# eval a chunk of code in the package that called GetOptions.)
#
sub find_calling_package
{
my ($i, $this_pkg, $up_pkg, @caller);
$i = 0;
$this_pkg = (caller(0))[0];
while (@caller = caller($i++))
{
$up_pkg = $caller[0];
last if $up_pkg ne $this_pkg;
}
$up_pkg;
}
# ----------------------------------------------------------------------
# Option-handling routines:
# process_constant_option
# process_boolean_option
# process_call_option
# process_eval_option
# ----------------------------------------------------------------------
# General description of these routines:
# * each one is passed exactly three options:
# - $arg - the argument that triggered this routine, expanded
# into unabbreviated form
# - $arglist - reference to list containing rest of command line
# - $opt_desc - reference to an option descriptor list
# * they are called from GetOptions, through code references in the
# %OptionHandlers hash
# * if they return a false value, then GetOptions immediately returns
# 0 to its caller, with no error message -- thus, the option handlers
# should print out enough of an error message for the end user to
# figure out what went wrong; also, the option handlers should be
# careful to explicitly return 1 if everything went well!
sub process_constant_option
{
my ($arg, $arglist, $opt_desc) = @_;
my ($type, $n, $value) = @$opt_desc[1,2,3];
if ($type eq "const")
{
$$value = $n;
}
elsif ($type eq "copy")
{
$$value = (defined $n) ? ($n) : ($arg);
}
elsif ($type eq "arrayconst")
{
@$value = @$n;
}
elsif ($type eq "hashconst")
{
%$value = %$n;
}
else
{
confess ("internal error: can't handle option type \"$type\"");
}
1;
}
sub process_boolean_option
{
my ($arg, $arglist, $opt_desc) = @_;
my ($value) = $$opt_desc[3];
my ($pos,$neg) = &split_option ($opt_desc);
confess ("internal error: option $arg not found in argument hash")
if ($arg ne $pos && $arg ne $neg);
$$value = ($arg eq $pos) ? 1 : 0;
1;
}
sub process_call_option
{
my ($arg, $arglist, $opt_desc) = @_;
my ($option, $args, $value) = @$opt_desc[0,2,3];
croak "Invalid option table entry for option \"$option\" -- \"value\" " .
"field must be a code reference"
unless (ref $value eq 'CODE');
my @args = (ref $args eq 'ARRAY') ? (@$args) : ();
my $result = &$value ($arg, $arglist, @args);
if (!$result)
{
# Gee, wouldn't it be neat if we get get the sub name from the code ref?
&SetError
($ErrorClass || "bad_call",
$ErrorMessage || "subroutine call from option \"$arg\" failed");
}
return $result;
} # &process_call_option
sub process_eval_option
{
my ($arg, $arglist, $opt_desc) = @_;
my ($value) = $$opt_desc[3];
my $up_pkg = &find_calling_package ();
# print "package $up_pkg; $value"; # DEBUG ONLY
my $result = eval "package $up_pkg; no strict; $value";
if ($@) # any error string set?
{
&SetError ("bad_eval",
"error evaluating \"$value\" (from $arg option): $@");
return 0;
}
if (!$result)
{
&SetError
($ErrorClass || "bad_call",
$ErrorMessage || "code eval'd for option \"$arg\" failed");
}
return $result;
}
sub process_pattern_option
{
my ($arg, $arglist, $opt_desc) = @_;
my ($type, $n, $value) = @$opt_desc[1,2,3];
# This code looks a little more complicated than you might at first
# think necessary. But the ugliness is necessary because $value might
# reference a scalar or an array, depending on whether $n is 1 (scalar)
# or not (array). Thus, we can't just assume that either @$value or
# $$value is valid -- we always have to check which of the two it should
# be.
if ($n == 1) # scalar-valued option (one argument)
{
$$value = shift @$arglist;
return 0 unless &check_value ($$value, $arg, $type, $n);
}
else # it's a "vector-valued" option
{ # (fixed number of arguments)
@$value = splice (@$arglist, 0, $n);
if (scalar @$value != $n)
{
&option_error ($arg, $type, $n);
return 0;
}
my $val;
foreach $val (@$value)
{
return 0 unless &check_value ($val, $arg, $type, $n);
}
} # else
return 1;
} # &process_pattern_option
# --------------------------------------------------------------------
# The main public subroutine: GetOptions
# --------------------------------------------------------------------
sub GetOptions
{
my ($opt_table, $arglist, $new_arglist) = @_;
my (%argpos, $arg, $pos, $opt_ref);
my ($option_re, @option_list);
$new_arglist = $arglist if !defined $new_arglist;
&SetError ("", "");
# Build a hash mapping option -> position in option table
&scan_table ($opt_table, \%argpos);
# Regexp to let us recognize options on the command line
$option_re = join ("|", @OptionPatterns);
# Build a list of all acceptable options -- used to match abbreviations
my $opt_desc;
foreach $opt_desc (@$opt_table)
{
push (@option_list, &split_option ($opt_desc))
unless $opt_desc->[1] eq "section";
}
push (@option_list, $HelpOption) if $HelpOption;
# Now walk over the argument list
my @tmp_arglist = @$arglist;
@$new_arglist = ();
while (defined ($arg = shift @tmp_arglist))
{
# print "arg: $arg\n";
# If this argument is the option terminator (usually "--") then
# transfer all remaining arguments to the new arg list and stop
# processing immediately.
if (defined $OptionTerminator && $arg eq $OptionTerminator)
{
push (@$new_arglist, @tmp_arglist);
last;
}
# If this argument isn't an option at all, just append it to
# @$new_arglist and go to the next one.
if ($arg !~ /^($option_re)/o)
{
push (@$new_arglist, $arg);
next;
}
# We know we have something that looks like an option; see if it
# matches or is an abbreviation for one of the strings in
# @option_list
$arg = &match_abbreviation ($arg, \@option_list, "%s option: %s");
if (! $arg)
{
warn $Usage if defined $Usage;
warn "$ErrorMessage\n";
return 0;
}
# If it's the help option, print out the help and return
if ($arg eq $HelpOption)
{
&print_help ($opt_table);
&SetError ("help", "");
return 0;
}
# Now we know it's a valid option, and it's not the help option --
# so it must be in the caller's option table. Look up its
# entry there, and use that for the actual option processing.
$pos = $argpos{$arg};
confess ("internal error: didn't find arg in arg hash even " .
"after resolving abbreviation")
unless defined $pos;
my $opt_desc = $opt_table->[$pos];
my $type = $opt_desc->[1];
if (exists $OptionHandlers{$type} &&
ref ($OptionHandlers{$type}) eq 'CODE')
{
if (! &{$OptionHandlers{$type}} ($arg, \@tmp_arglist, $opt_desc))
{
warn $Usage if defined $Usage;
warn "$ErrorMessage\n";
return 0;
}
}
else
{
croak "Unknown option type \"$type\" (found for arg $arg)";
}
} # while ($arg = shift @$arglist)
return 1;
} # GetOptions
1;