cvsuser 03/12/11 04:55:08
Added: App-Options/lib/App Options.pm
Log:
new
Revision Changes Path
1.1 p5ee/App-Options/lib/App/Options.pm
Index: Options.pm
===================================================================
#############################################################################
## $Id: Options.pm,v 1.1 2003/12/11 12:55:04 spadkins Exp $
#############################################################################
package App::Options;
use strict;
use Carp;
=head1 NAME
App::Options - combine command line options, environment vars, and option file values
=head1 SYNOPSIS
#!/usr/local/bin/perl
BEGIN {
use App::Options;
App::Options->init(); # reads into %App::options by default
}
print "Options:\n";
foreach $var (sort keys %App::options) {
printf " %-20s => [%s]\n", $var, $App::options{$var};
}
or a more full-featured example...
#!/usr/local/bin/perl
BEGIN {
use App::Options;
App::Options->init(
values => \%MyPackage::some_hash,
options => [ "option_file", "prefix", "app", "app_path_info",
"perlinc", "debug_options", "import", ],
option => {
option_file => "~/.app/app.conf", # set default
app => "default=app;type=string", # default & type
app_path_info => {default=>"",type=>"string"}, # as a hashref
prefix => "type=string;required",
perlinc => undef, # no default
debug_options => "type=int",
import => "type=string",
flush_imports => 1,
},
no_cmd_args => 1,
no_env_vars => 1,
no_option_file => 1,
print_usage => sub { my ($values, $init_options) = @_; print "Use it
right!\n"; },
);
}
print "Options:\n";
foreach $var (sort keys %MyPackage::some_hash) {
printf " %-20s => [%s]\n", $var, $MyPkg::my_hash{$var};
}
=head1 DESCRIPTION
App::Options combines command-line arguments, environment variables,
option files, and program defaults to produce a hash of
option values.
All of this may be done within the BEGIN block so that the @INC variable
can be modified in time to affect "use" statements within the
regular code. This is particularly important to support the installation
of multiple versions of a Perl application on the same physical computer.
App::Options supports the P5EE/App-Context variant of the Perl 5 Enterprise
Environment. See the P5EE web sites for more information.
http://www.officevision.com/pub/p5ee
http://p5ee.perl.org
App::Options is in its own distribution because it will be very stable
and can be installed in the default perl places on the system.
This is different than the App-Context, App-Repository, and App-Widget
distributions which are expected to evolve significantly.
A developer writing an application based on the P5EE/App-Context framework
will want to install App-Options in the default perl places. The other
distributions will be installed in release-specific locations.
=head1 Methods
=cut
#############################################################################
# init()
#############################################################################
=head2 init()
* Signature: App::Options->init();
* Signature: App::Options->init(%named);
* Signature: App::Options->init($myvalues);
* Signature: App::Options->init($myvalues, %named);
* Param: $myvalues HASH
* Param: values HASH
* Return: void
* Throws: <none>
* Since: 0.01
Sample Usage:
BEGIN {
use App::Options;
App::Options->init();
}
... or, to use every option available ...
BEGIN {
use App::Options;
App::Options->init(
values => \%MyPackage::options,
options => [ "option_file", "prefix", "app", "app_path_info",
"perlinc", "debug_options", "import", ],
option => {
option_file => "~/.app/app.conf", # set default
app => "default=app;type=string", # default & type
app_path_info => {default=>"",type=>"string"}, # as a hashref
prefix => "type=string;required",
perlinc => undef, # no default
debug_options => "type=int",
import => "type=string",
flush_imports => 1,
},
no_cmd_args => 1,
no_env_vars => 1,
no_option_file => 1,
print_usage => sub { my ($values, $init_options) = @_; print "Use it
right!\n"; },
);
}
The init() method reads the command line args (@ARGV),
finds an options file, and loads it, all in a way which
can be done in a BEGIN block. This is important to be able
to modify the @INC array so that normal "use" and "require"
statements will work with the configured @INC path.
The init() method stores command line options and option
file values all in the global %App::options hash.
The special keys to this hash are as follows.
option_file - specifies the exact file name of the option file useful
for command line usage (i.e. "app --option_file=/path/to/app.conf")
"option_file" is automatically set with the option file that it found
if it is not supplied at the outset as an argument.
app - specifies the tag that will be used when searching for
a option file. (i.e. "app --app=myapp" will search for "myapp.conf"
before it searches for "app.conf")
"app" is automatically set with the stem of the program file that
was run (or the first part of PATH_INFO) if it is not supplied at
the outset as an argument.
app_path_info - this is the remainder of PATH_INFO after the first
part is taken off to make the app.
prefix - This represents the base directory of the software
installation (i.e. "/usr/myproduct/1.3.12"). If it is not
set explicitly, it is detected from the following places:
1. PREFIX environment variable
2. the real path of the program with /bin or /cgi-bin stripped
3. the current directory
If it is autodetected from one of those three places, that is
only provisional, in order to find the "option_file". The "prefix"
variable should be set authoritatively in the "option_file" if it
is desired to be in the $values structure.
perlinc - a path of directories to prepend to the @INC search path.
This list of directories is separated by any combination of
[,;: ] characters. (Hmmm. Windows drive specifiers could be hurt
by this approach.)
debug_options
import
=cut
sub init {
# can call as a function (&App::Options::init()) or a static method
(App::Options->init())
shift if ($#_ > -1 && $_[0] eq "App::Options");
# can supply initial hashref to use for option values instead of global
%App::options
my $values = ($#_ > -1 && ref($_[0]) eq "HASH") ? shift : \%App::options;
($#_ % 2 == 1) || croak "App::Options->init(): must have an even number of
vars/values for named args";
my %init_options = @_;
# "values" in named arg list overrides the one supplied as an initial hashref
if (defined $init_options{values}) {
(ref($init_options{values}) eq "HASH") || croak "App::Options->init():
'values' arg must be a hash reference";
$values = $init_options{values};
}
else {
$init_options{values} = $values;
}
#################################################################
# we do all this within a BEGIN block because we want to get an
# option file and update @INC so that it will be used by
# "require" and "use".
# The global option hash (%App::options) is set from 3 sources:
# command line options, environment variables, and option files.
#################################################################
#################################################################
# 1. Read the command-line options
# (anything starting with one or two dashes is an option var
# i.e. --debugmode=record -debugmode=replay
# an option without an "=" (i.e. --help) acts as --help=1
# Put the var/value pairs in %$values
#################################################################
my ($var, $value);
if (! $init_options{no_cmd_args}) {
while ($#ARGV >= 0 && $ARGV[0] =~ /^--?([^=-][^=]*)(=?)(.*)/) {
$var = $1;
$value = ($2 eq "") ? 1 : $3;
shift @ARGV;
$values->{$var} = $value;
}
}
#################################################################
# 2. find the app.
# by default this is the basename of the program
# in a web application, this is overridden by any existing
# first part of the PATH_INFO
#################################################################
my $app = $values->{app};
my $app_path_info = $ENV{PATH_INFO} || "";
$app_path_info =~ s!/+$!!; # strip off trailing slashes ("/")
if (!$app) {
if ($app_path_info && $app_path_info =~ s!^/([^/]+)!!) {
$app = $1; # last part of PATH_INFO (without slashes)
}
else {
$app = $0; # start with the full script path
$app =~ s!.*/!!; # strip off leading path
$app =~ s/\.[^.]+$//; # strip off trailing file type (i.e. ".pl")
}
$values->{app} = $app;
}
$values->{app_path_info} = $app_path_info;
#################################################################
# 3. find the directory the program was run from.
# we will use this directory to search for the
# option file.
#################################################################
my $prog_dir = $0; # start with the full script path
if ($prog_dir =~ m!^/!) { # absolute path
# i.e. /usr/local/bin/app, /app
$prog_dir =~ s!/[^/]+$!!; # trim off the program name
}
else { # relative path
# i.e. app, ./app, ../bin/app, bin/app
$prog_dir =~ s!/?[^/]+$!!; # trim off the program name
$prog_dir = "." if (!$prog_dir); # if nothing left, directory is current dir
}
#################################################################
# 4. guess the "prefix" directory for the entire
# software installation. The program is usually in
# $prefix/bin or $prefix/cgi-bin.
#################################################################
my $prefix = $values->{prefix}; # possibly set on command line
# it can be set in environment.
# This is the preferred way for Registry and PerlRun webapps.
$prefix = $ENV{PREFIX} if (!$prefix && $ENV{PREFIX});
# Using "abs_path" gets rid of all symbolic links and gives the real path
# to the directory in which the script runs.
if (!$prefix) {
use Cwd 'abs_path';
$prefix = abs_path($prog_dir);
$prefix =~ s!/bin$!!;
$prefix =~ s!/cgi-bin.*$!!;
}
$prefix = "." if (!$prefix); # last resort: current directory
my ($env_var, @env_vars);
if (! $init_options{no_option_file}) {
#################################################################
# 5. Define the standard places to look for an option file
#################################################################
my @option_file = ();
push(@option_file, $values->{option_file}) if ($values->{option_file});
push(@option_file, "$ENV{HOME}/.app/$app.conf") if ($ENV{HOME} && $app ne
"app");
push(@option_file, "$ENV{HOME}/.app/app.conf") if ($ENV{HOME});
push(@option_file, "$prog_dir/$app.conf") if ($app ne "app");
push(@option_file, "$prog_dir/app.conf");
push(@option_file, "$prefix/etc/app/$app.conf") if ($app ne "app");
push(@option_file, "$prefix/etc/app/app.conf");
#################################################################
# 6. now actually read in the file(s)
# we read a set of standard files, and
# we may continue to read in additional files if they
# are indicated by an "import" line
#################################################################
local(*App::FILE);
my ($option_file, $exclude_section);
my ($regexp, $expr, @expr, $exclude);
while ($#option_file > -1) {
$option_file = shift(@option_file);
$exclude_section = 0;
print STDERR "Looking for option file [$option_file]\n" if
($values->{debug_options});
if (open(App::FILE, "< $option_file")) {
print STDERR "Found option file [$option_file]\n" if
($values->{debug_options});
while (<App::FILE>) {
chomp;
# for lines that are like "[regexp]" or even "[regexp] var =
value"
# or "[regexp;var=value]" or "[regexp;var1=value1;var2=value2]"
if (s|^ *\[([^\[\]]*)\] *||) {
@expr = split(/;/,$1);
$regexp = undef;
$exclude = 0;
foreach $expr (@expr) {
if ($expr =~ /^([^=]+)=(.*)$/) { # a variable-based
condition
$exclude = ((defined $values->{$1} ? $values->{$1} :
"") ne $2);
}
else { # a program name regexp
$regexp = $expr;
$exclude = ($regexp ne "" && $regexp ne "ALL" &&
$app !~ m!^$regexp$!);
$exclude = (!defined $regexp && $#expr > -1 &&
$exclude_section) if (!$exclude);
}
last if ($exclude);
}
if ($_) {
# this is a single-line condition, don't change the
$exclude_section flag
next if ($exclude);
}
else {
# this condition pertains to all lines after it
$exclude_section = $exclude;
next;
}
}
else {
next if ($exclude_section);
}
s/#.*$//; # delete comments
s/^ +//; # delete leading spaces
s/ +$//; # delete trailing spaces
next if (/^$/); # skip blank lines
# look for "var = value" (ignore other lines)
if (/^([a-zA-Z0-9_.-]+) *= *(.*)/) { # untainting also happens
$var = $1;
$value = $2;
# TODO: quoting, var = " hello world "
# TODO: here documents, var = <<EOF
# only add values which have never been defined before
if (!defined $values->{$var}) {
if (! $init_options{no_env_vars}) {
$env_var = "APP_" . uc($var);
if (defined $ENV{$env_var}) {
$value = $ENV{$env_var};
}
}
# do variable substitutions, var = ${prefix}/bin
$value =~ s/\$\{([a-zA-Z0-9_\.-]+)\}/(defined
$values->{$1} ? $values->{$1} : "")/eg;
$values->{$var} = $value; # save all in %App::options
}
}
}
close(App::FILE);
if ($values->{flush_imports}) {
@option_file = (); # throw out other files to look for
delete $values->{flush_imports};
}
if ($values->{import}) {
unshift(@option_file, split(/[,:; ]+/, $values->{import}));
delete $values->{import};
}
}
}
}
#################################################################
# 6b. convert $init_options{option} to deep hash
#################################################################
my (@vars, $option);
$option = $init_options{option};
if ($option) {
croak "App::Options->init(): 'option' arg must be a hash reference"
if (ref($option) ne "HASH");
my (@args, $hash, $arg);
foreach $var (keys %$option) {
$value = $option->{$var};
if (ref($value) eq "") {
$hash = {};
$option->{$var} = $hash;
@args = split(/ *; */,$value);
foreach $arg (@args) {
if ($arg =~ /^([^=]+)=(.*)$/) {
$hash->{$1} = $2;
}
elsif (! defined $hash->{default}) {
$hash->{default} = $arg;
}
else {
$hash->{$arg} = 1;
}
}
}
}
}
#################################################################
# 6c. fill in ENV vars and defaults
#################################################################
@vars = ();
if ($init_options{options}) {
croak "App::Options->init(): 'options' arg must be an array reference"
if (ref($init_options{options}) ne "ARRAY");
push(@vars, @{$init_options{options}});
}
if ($option) {
push(@vars, (sort keys %$option));
}
foreach $var (@vars) {
if (!defined $values->{$var}) {
$value = $option ? $option->{$var}{default} : undef;
if (! $init_options{no_env_vars}) {
$env_var = "APP_" . uc($var);
if (defined $ENV{$env_var}) {
$value = $ENV{$env_var};
}
}
# do variable substitutions, var = ${prefix}/bin
$value =~ s/\$\{([a-zA-Z0-9_\.-]+)\}/(defined $values->{$1} ?
$values->{$1} : "")/eg;
$values->{$var} = $value; # save all in %App::options
}
}
#################################################################
# 7. establish the definitive (not inferred) $prefix
#################################################################
if ($values->{prefix}) {
$prefix = $values->{prefix};
}
else {
$values->{prefix} = $prefix;
}
#################################################################
# 8. add "perlinc" directories to @INC, OR
# automatically include (if not already) the directories
# $PREFIX/lib/$^V and $PREFIX/lib/site_perl/$^V
# i.e. /usr/mycompany/lib/5.6.1 and /usr/mycompany/lib/site_perl/5.6.1
#################################################################
if (defined $values->{perlinc}) { # add perlinc entries
unshift(@INC, split(/[,:; ]+/,$values->{perlinc}));
}
else {
my $libdir = "$prefix/lib";
my $libdir_found = 0;
foreach my $incdir (@INC) {
if ($incdir =~ /^$libdir/) {
$libdir_found = 1;
}
}
if (!$libdir_found) {
unshift(@INC, "$libdir");
if ($^V) {
my $perlversion = sprintf("%vd", $^V);
unshift(@INC, "$libdir/perl5/site_perl/$perlversion"); # site_perl
goes first!
unshift(@INC, "$libdir/perl5/$perlversion");
}
}
}
#################################################################
# 9. print stuff out for options debugging
#################################################################
if ($values->{debug_options}) {
print STDERR "%App::options (or other) =\n";
foreach $var (sort keys %$values) {
print STDERR " $var = [$values->{$var}]\n";
}
print STDERR "[EMAIL PROTECTED] =\n";
foreach $var (@INC) {
print STDERR " $var\n";
}
}
#################################################################
# 10. perform validations, print help, and exit
#################################################################
my $exit_status = -1;
if ($values->{"?"} || $values->{help}) {
$exit_status = 0;
}
my ($type);
if ($option) {
@vars = (sort keys %$option);
foreach $var (@vars) {
$type = $option->{$var}{type};
next if (!$type); # nothing to validate against
$value = $values->{$var};
next if (! defined $value);
if ($type eq "integer") {
if ($value !~ /^-?[0-9]+$/) {
$exit_status = 1;
print "Error: \"$var\" must be of type \"$type\" (not
\"$value\")\n";
}
}
elsif ($type eq "float") {
if ($value !~ /^-?[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?$/ &&
$value !~ /^-?[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?$/) {
$exit_status = 1;
print "Error: \"$var\" must be of type \"$type\" (not
\"$value\")\n";
}
}
elsif ($type eq "date") {
if ($value !~ /^[0-9]{4}-[01][0-9]-[0-3][0-9]$/) {
$exit_status = 1;
print "Error: \"$var\" must be of type \"$type\" (format
\"YYYY-MM-DD\") (not \"$value\")\n";
}
}
elsif ($type eq "datetime") {
if ($value !~ /^[0-9]{4}-[01][0-9]-[0-3][0-9]
[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/) {
$exit_status = 1;
print "Error: \"$var\" must be of type \"$type\" (format
\"YYYY-MM-DD HH:MM::SS\") (not \"$value\")\n";
}
}
elsif ($type eq "time") {
if ($value !~ /^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/) {
$exit_status = 1;
print "Error: \"$var\" must be of type \"$type\" (format
\"HH:MM::SS\") (not \"$value\")\n";
}
}
else {
if ($value !~ /$type/) {
$exit_status = 1;
print "Error: \"$var\" must match \"$type\" (not \"$value\")\n";
}
}
}
foreach $var (@vars) {
next if (!$option->{$var}{required} || defined $values->{$var});
$exit_status = 1;
print "Error: \"$var\" is a required option but is not defined\n";
}
}
if ($exit_status >= 0) {
if ($init_options{print_usage}) {
&{$init_options{print_usage}}($values, \%init_options);
}
else {
App::Options->print_usage($values, \%init_options);
}
exit($exit_status);
}
}
sub print_usage {
shift if ($#_ > -1 && $_[0] eq "App::Options");
my ($values, $init_options) = @_;
print STDERR "Usage: $0 [options]\n";
printf STDERR " --%-32s print this message (also -?)\n", "help";
my (@vars);
if ($init_options->{options}) {
@vars = @{$init_options->{options}};
}
elsif ($init_options->{option}) {
@vars = (sort keys %{$init_options->{option}});
}
else {
@vars = (sort keys %$values);
}
my ($var, $value, $type, $desc, $option);
$option = $init_options->{option} || {};
foreach $var (@vars) {
next if ($var eq "?" || $var eq "help");
$value = $values->{$var};
$type = $option->{$var}{type} || "";
$desc = $option->{$var}{desc} || "";
$type = " ($type)" if ($type);
$desc = " $desc" if ($desc);
printf STDERR " --%-32s [%s]$type$desc\n", "$var=<$var>", (defined
$value) ? $value : "undef";
}
}
=head1 ACKNOWLEDGEMENTS
* Author: Stephen Adkins <[EMAIL PROTECTED]>
* License: This is free software. It is licensed under the same terms as Perl
itself.
=head1 SEE ALSO
=cut
1;