Getopt::LL
Index
Code Index:
# Local Variables:
# mode: cperl
# cperl-indent-level: 4
# fill-column: 78
# End:
# vim: expandtab tabstop=4 shiftwidth=4 shiftround
# $Id: LL.pm,v 1.17 2007/07/13 00:00:13 ask Exp $
# $Source: /opt/CVS/Getopt-LL/lib/Getopt/LL.pm,v $
# $Author: ask $
# $HeadURL$
# $Revision: 1.17 $
# $Date: 2007/07/13 00:00:13 $
package Getopt::LL;
use strict;
use warnings;
use Carp qw(croak);
use Getopt::LL::DLList;
use English qw($PROGRAM_NAME);
use version qw(qv); our $VERSION = qv('1.0.0');
use 5.006_001;
{
use Getopt::LL::SimpleExporter qw(getoptions opt_String opt_Digit opt_Flag);
use Class::Dot 1.0 qw( property isa_Hash isa_Array isa_Object );
#========================================================================
# - CLASS PROPERTIES -
#========================================================================
property rules => isa_Hash;
property aliases => isa_Hash;
property options => isa_Hash;
property help => isa_Hash;
property result => isa_Hash;
property leftovers => isa_Array;
property dll => isa_Object('Getopt::LL::DLList');
my $RE_SHORT_ARGUMENT = qr{
\A # starts with...
- # single dash.
(?!-) # with no dash after that.
.
}xms;
my $RE_LONG_ARGUMENT = qr{
\A # starts with...
-- [^-]? # two dashes.
(?!-) # with no dash after that.
.
}xms;
my $RE_ASSIGNMENT = qr{
(?<! \\ ) # equal sign that does not have backslash
= # in front of it.
}xms;
my %TYPE_CHECK = (
digit => \&is_digit,
string => \&is_string,
);
my %RULE_ACTION = (
digit => \&get_next_arg,
string => \&get_next_arg,
flag => sub {
return 1;
},
);
my %DEFAULT_OPTIONS = (
allow_unspecified => 0,
die_on_type_mismatch => 0,
silent => 0,
end_on_dashdash => 0,
split_multiple_shorts => 0,
style => 'default',
long_option => 'flag',
short_option => 'string',
);
my %DEFAULT_OPTIONS_GNU = (
# GNU-style arguments ends argument processing on empty '--'
end_on_dashdash => 1,
split_multiple_shorts => 1,
);
my $EXIT_FAILURE = 1;
# When set to true, parseopts stop processing options.
my $end_processing;
#========================================================================
# - CONSTRUCTOR -
#========================================================================
sub new {
my ($class, $rules_ref, $options_ref, $argv_ref) = @_;
$argv_ref ||= \@ARGV;
my $self = {};
bless $self, $class;
# If there are no rules, we must allowed unspecified
# arguments. (also check if we a have a reference to an empty hash).
if (!$rules_ref || (ref $rules_ref && !scalar keys %{$rules_ref})) {
$options_ref->{allow_unspecified} = 1;
}
while (my ($option, $default_value) = each %DEFAULT_OPTIONS) {
if (!defined $options_ref->{$option}) {
$options_ref->{$option} = $default_value;
}
}
if ($options_ref->{style} eq 'GNU') {
while (my ($option, $value) = each %DEFAULT_OPTIONS_GNU) {
$options_ref->{$option} = $value;
}
}
$self->set_options($options_ref);
$self->rules_prepare($rules_ref);
if (scalar @{ $argv_ref }) {
$self->set_dll( Getopt::LL::DLList->new($argv_ref) );
$self->_init();
}
$self->rules_postactions( );
return $self;
}
#========================================================================
# - INSTANCE METHODS -
#========================================================================
sub _init {
my ($self) = @_;
my $dll = $self->dll;
$end_processing = 0;
$dll->traverse($self, 'parseoption');
return $self->result;
}
sub rules_prepare {
my ($self, $rules_ref) = @_;
my $options_ref = $self->options;
my $help_ref = $self->help;
my %final_rules = ();
my %aliases = ();
RULE:
while (my ($rule_name, $rule_spec) = each %{$rules_ref}) {
# User can type:
# '-arg' => 'string',
# instead of:
# '-arg' => { type => 'string' }
# and we will convert it here.
if (ref $rule_spec ne 'HASH') {
$rule_spec = {type => $rule_spec};
}
# If the rule has a help field; save it into help.
if ($rule_spec->{help}) {
$help_ref->{$rule_name} = $rule_spec->{help};
}
my($rule_name_final, @aliases)
= split m/\|/xms, $rule_name;
# Split out the aliases (which are delimited by |)
# Aliases can also be inside the spec, like this:
# '-arg' => { alias => '-gra' };
# or a list of aliases:
# '-arg' => { alias => ['-gra', '-rag', '-rga'] };
#
my $aliases_inside_spec = $rule_spec->{alias};
if ($aliases_inside_spec) {
@aliases =
ref $aliases_inside_spec eq 'ARRAY'
? (@aliases, @{$aliases_inside_spec})
: (@aliases, $aliases_inside_spec);
}
# if the name of the rule ends with !, remove the !
# and set it as required.
if ($rule_name_final =~ s/!\z//xms) {
$rule_spec->{required} = 1;
}
# a default value can be defined inside parentheses.
# i.e:
# '-arg(defaultValue)' => 'string';
if ($rule_name_final =~ s/\( (.+?) \)//xms) {
$rule_spec->{default} = $1;
}
# Remove leading and trailing whitespace.
$rule_name_final =~ s/\A \s+ //xms;
$rule_name_final =~ s/ \s+ \z//xms;
# Save the final version of the rule.
$final_rules{$rule_name_final} = $rule_spec;
# Save aliases to this rule.
for my $alias (@aliases) {
$aliases{$alias} = $rule_name_final;
}
}
$self->set_aliases( \%aliases );
$self->set_rules( \%final_rules );
return;
}
sub rules_postactions {
my ($self) = @_;
my $rules_ref = $self->rules;
my $result = $self->result;
while (my ($rule_name, $rule_spec) = each %{ $rules_ref }) {
# Die if this is a required argument that we don't have.
if ($rule_spec->{required} && !$result->{$rule_name}) {
die "Missing required argument: $rule_name\n";
}
# Set this argument to the default if it doesn't exist
# and a default value for this rule exists.
if ($rule_spec->{default} && !$result->{$rule_name}) {
$result->{$rule_name} = $rule_spec->{default};
}
}
return;
}
sub parseoption {
my ($self, $argument, $node) = @_;
my $result_argv = $self->result;
my $leftovers = $self->leftovers;
my $rules = $self->rules;
my $options_ref = $self->options;
my $aliases = $self->aliases;
my $is_arg_of_type = $self->find_arg_type($argument);
# We stop processing options if this is a naked long option, ( '^--$' )
# and the 'end_on_dashdash' option is set.
if ($argument eq q{--} && $options_ref->{end_on_dashdash}) {
$end_processing++;
}
# if find_arg_type said we have a special argument, start processing
# it (as long as processing is not stopped).
elsif ($is_arg_of_type && !$end_processing) {
my @arguments = ($argument);
if ($is_arg_of_type eq 'short' && $options_ref->{split_multiple_shorts}) {
$argument =~ s/^-//xms;
@arguments = map { "-$_" } split m//xms, $argument;
};
for my $argument (@arguments) {
my $argument_name = $argument;
my $argument_value = q{};
# ###
# case: --argument_name=value
# if argument name contains an equal sign, the value is embedded in the
# argument. an example of inline assignement could be:
# --input-filename=/Users/ask/tmplog.txt
if ($argument =~ $RE_ASSIGNMENT) {
my @fields = split $RE_ASSIGNMENT, $argument;
($argument_name, $argument_value) = @fields;
}
# Try to find the rule for this argument...
my $opt_has_rule = $rules->{$argument_name};
# if we can't find this rule, check if it's an alias.
if (!$opt_has_rule && $aliases->{$argument_name}) {
# set the argument name to the name of the original.
# and set the rule to the rule of the original.
$argument_name = $aliases->{$argument_name};
$opt_has_rule = $rules->{$argument_name};
}
if (!$opt_has_rule && !$options_ref->{allow_unspecified}) {
$self->unknown_argument_error($argument);
}
$result_argv->{$argument_name} =
$opt_has_rule
? $self->handle_rule($argument_name, $opt_has_rule, $node,
$argument_value)
: (
$argument_value || 1
);
}
}
else {
push @{$leftovers}, $argument;
}
return;
}
sub find_arg_type {
my ($self, $argument) = @_;
if ($argument =~ $RE_LONG_ARGUMENT) {
return 'long';
}
if ($argument =~ $RE_SHORT_ARGUMENT) {
return 'short';
}
# return nothing if this is not a special argument/option.
return;
}
sub is_string {
# we have no limits for what a string can be.
return 1;
}
sub is_digit {
my ($self, $value, $option_name, $value_ref) = @_;
return 0 if $value eq q{};
my $is_digit = 0;
my $first_two_chars = substr $value, 0, 2;
if ($first_two_chars eq '0x') {
# starts with 0x: is hexadecimal number
$value = substr $value, 2, length $value;
if ($value =~ m/\A [\dA-Fa-f]+ \z/xms) {
# We get a reference to the value as argument #4.
# convert the value to hex.
${$value_ref} = hex $value;
$is_digit = 1;
}
}
elsif ($value =~ m/\A [-+]? \d+ \z/xms) {
$is_digit = 1;
}
if (!$is_digit) {
return $self->type_mismatch_error('digit',
"$option_name must be a digit (0-9).");
}
return 1;
}
sub type_mismatch_error {
my ($self, $type, $message) = @_;
my $options_ref = $self->options;
$options_ref->{die_on_type_mismatch}
? croak $message, "\n"
: warn $message, "\n"
;
return 0;
}
sub unknown_argument_error {
my ($self, $argument) = @_;
croak "Unknown argument: $argument.\n";
}
sub handle_rule {
my ($self, $arg_name, $rule_ref, $node, $arg_value) = @_;
my $rule_data;
my $rule_type = $rule_ref->{type};
if (ref $rule_type eq 'CODE') {
return $rule_type->($self, $node, $arg_name, $arg_value);
}
elsif (ref $rule_type eq 'Regexp') {
no warnings 'uninitialized'; ## no critic
my $next_arg = $self->get_next_arg($node);
if ($next_arg !~ $rule_type) {
if (! defined $next_arg) {
$next_arg = '<no-value>';
}
croak sprintf('Argument %s [%s] does not match %s', ## no critic
$arg_name, $next_arg, _regex_as_text($rule_type)
);
}
return $next_arg;
}
if ($RULE_ACTION{$rule_type}) {
$arg_value ||= $RULE_ACTION{$rule_type}->($self, $node);
if ($TYPE_CHECK{$rule_type}) {
$TYPE_CHECK{$rule_type}
->($self,$arg_value, $arg_name, \$arg_value);
}
}
else {
$Carp::CarpLevel = 2; ## no critic;
croak "Unknown rule type [$rule_type] for argument [$arg_name]";
}
return $arg_value;
}
sub get_next_arg {
my ($self, $node) = @_;
return $self->delete_arg( $node->next );
}
sub get_prev_arg {
my ($self, $node) = @_;
return $self->delete_arg( $node->prev );
}
sub peek_next_arg {
my ($self, $node) = @_;
if ($node->next) {
return $node->next->data;
}
return;
}
sub peek_prev_arg {
my ($self, $node) = @_;
if ($node->prev) {
return $node->prev->data;
}
return;
}
sub delete_arg {
my ($self, $node) = @_;
my $dll = $self->dll;
return $dll->delete_node($node);
}
# XXX this is not very complete.
sub show_help {
my ($self) = @_;
while (my ($arg, $help) = each %{ $self->help }) {
my $ret = print {*STDERR} "$arg\t\t\t$help\n";
croak 'I/O Error. Cannot print to terminal' if !$ret;
}
return;
}
# XXX this is not very complete.
sub show_usage {
my ($self) = @_;
my $program_name = $self->options->{program_name};
if (! $program_name) {
$program_name = $PROGRAM_NAME;
}
require File::Basename;
$program_name = File::Basename::basename($program_name);
my @arguments;
while (my ($arg, $spec) = each %{ $self->rules }) {
if ($spec->{type} eq 'string' || $spec->{type} eq 'digit') {
push @arguments, "$arg <n>";
}
else {
push @arguments, $arg;
}
}
my $arguments = join q{|}, @arguments;
my $ret = print {*STDERR} "Usage: $program_name [$arguments]\n";
croak 'I/O Error. Cannot print to terminal' if !$ret;
return;
}
#========================================================================
# - CLASS METHODS -
#========================================================================
#------------------------------------------------------------------------
# getoptions(\%rules, \%options, \@opt_argv)
#
#------------------------------------------------------------------------
sub getoptions {
my ($rules_ref, $options_ref, $argv_ref) = @_;
my $getopts =
__PACKAGE__->new($rules_ref, $options_ref, $argv_ref); ## no critic;
my $result = $getopts->result();
# ARGV should be set to what is left of the argument vector.
@ARGV = @{ $getopts->leftovers };
return $result;
}
sub opt_String { ## no critic
my ($help) = @_;
return {
type => 'string',
help => $help,
};
}
sub opt_Digit { ## no critic
my ($help) = @_;
return {
type => 'digit',
help => $help,
};
}
sub opt_Flag { ## no critic
my ($help) = @_;
return {
type => 'flag',
help => $help,
};
}
sub _regex_as_text {
my $regex_as_text = scalar shift;
my $regex_modifiers;
# The quoted regex (?xmsi:hello) should look something like this
# /hello/xmsi
# The job is to remove the (?: and capture xmsi into $1.
my $ret = $regex_as_text =~ s{
\A # beginning of string.
\(\? # a paren start and a question mark.
([\w-]+)? # none or more word characters captured to $1
: # ends with a colon.
}{}xms;
if ($ret) {
$regex_modifiers = $1;
}
# remove the closing paren at the end.
$regex_as_text =~ s/\) \z//xms;
# The final text we return should be:
# /hello/xmsi
# if the regex we got was:
# (?xmsi:hello)
$regex_as_text = "/$regex_as_text/";
if ($regex_modifiers) {
$regex_as_text .= $regex_modifiers;
}
return $regex_as_text;
}
}
1;
__END__