views:

322

answers:

4

I have a Perl script that sets up variables near the top for directories and files that it will use. It also requires a few variables to be set as command-line arguments. Example:

use Getopt::Long;

my ($mount_point, $sub_dir, $database_name, $database_schema);
# Populate variables from the command line:
GetOptions(
    'mount_point=s'       => \$mount_point,
    'sub_dir=s'           => \$sub_dir,
    'database_name=s'     => \$database_name,
    'database_schema=s'   => \$database_schema
);
# ...  validation of required arguments here

################################################################################
# Directory variables
################################################################################
my $input_directory    = "/${mount_point}/${sub_dir}/input";
my $output_directory   = "/${mount_point}/${sub_dir}/output";
my $log_directory      = "/${mount_point}/${sub_dir}/log";
my $database_directory = "/db/${database_name}";
my $database_scripts   = "${database_directory}/scripts";

################################################################################
# File variables
################################################################################
my $input_file       = "${input_dir}/input_file.dat";
my $output_file      = "${output_dir}/output_file.dat";
# ... etc

This works fine in my dev, test, and production environments. However, I was trying to make it easier to override certain variables (without going into the debugger) for development and testing. (For example, if I want to set my input_file = "/tmp/my_input_file.dat"). My thought was to use the GetOptions function to handle this, something like this:

GetOptions(
    'input_directory=s'      => \$input_directory,
    'output_directory=s'     => \$output_directory,
    'database_directory=s'   => \$database_directory,
    'log_directory=s'        => \$log_directory,
    'database_scripts=s'     => \$database_scripts,
    'input_file=s'           => \$input_file,
    'output_file=s'          => \$output_file
);

GetOptions can only be called once (as far as I know). The first 4 arguments in my first snippit are required, the last 7 directly above are optional. I think an ideal situation would be to setup the defaults as in my first code snippit, and then somehow override any of them that have been set if arguments were passed at the command line. I thought about storing all my options in a hash and then using that hash when setting up each variable with the default value unless an entry exists in the hash, but that seems to add a lot of additional logic. Is there a way to call GetOptions in two different places in the script?

Not sure if that makes any sense.

Thanks!

A: 

GetOptions can be called with an array as its input data. Read the documentation.

dsm
+3  A: 

I think what I would do is set input_directory et al to "undef", and then put them in the getopts, and then afterwards, test if they're still undef and if so assign them as shown. If your users are technically sophisticated enough to understand "if I give a relative path it's relative to $mount_point/$sub_dir", then I'd do additional parsing looking for an initial "/".

Paul Tomblin
+7  A: 

It sounds like you need to change your program to use configuration files rather than hard-coded configuration. I devoted an entire chapter of Mastering Perl to this. You don't want to change source code to test the program.

There are many Perl modules on CPAN that make configuration files an easy feature to add. Choose the one that works best for your input data.

Once you get a better configuration model in place, you can easily set default values, take values from multiple places (files, command-line, etc), and easily test the program with different values.

brian d foy
+6  A: 

Here's another approach. It uses arrays of names and a hash to store the options. It makes all options truly optional, but validates the required ones unless you include "--debug" on the command line. Regardless of whether you use "--debug", you can override any of the others.

You could do more explicit logic checks if that's important to you, of course. I included "--debug" as an example of how to omit the basic options like "mount_point" if you're just going to override the "input_file" and "output_file" variables anyway.

The main idea here is that by keeping sets of option names in arrays, you can include logic checks against the groups with relatively little code.

use Getopt::Long;

my @required_opts = qw(
    mount_point
    sub_dir
    database_name
    database_schema
);

my @internal_opts = qw(
    input_directory
    output_directory
    log_directory
    database_directory
    database_scripts
    input_file
    output_file
);

my @opt_spec = ("debug", map { "$_:s" } @required_opts, @internal_opts);

# Populate variables from the command line:
GetOptions( \(my %opts), @opt_spec );

# check required options unless 
my @errors = grep { ! exists $opts{$_} } @required_options;
if ( @errors && ! $opts{debug} ) {
    die "$0: missing required option(s): @errors\n";
}

################################################################################
# Directory variables
###############################################################################
my $opts{input_directory}    ||= "/$opts{mount_point}/$opts{sub_dir}/input";
my $opts{output_directory}   ||= "/$opts{mount_point}/$opts{sub_dir}/output";
my $opts{log_directory}      ||= "/$opts{mount_point}/$opts{sub_dir}/log";
my $opts{database_directory} ||= "/db/$opts{database_name}";
my $opts{database_scripts}   ||= "$opts{database_directory}/scripts";

################################################################################
# File variables
################################################################################
my $opts{input_file}    ||= "$opts{input_directory}/input_file.dat";
my $opts{output_file}   ||= "$opts{output_directory}/output_file.dat";
# ... etc
xdg
I like this - can you explain the lines like my $var ||="somevalue"? How would $var take an option from the command line? Or is there a step missing where it pulls it from the hash %opts?
BrianH
Oops. I didn't finish converting all those variables to the options hash. Those should be "$opts{varname} ||= somevalue". The "||=" operator is "or-equals" which assigns somevalue unless the left hand side variable is true. In Perl 5.10, a safer alternative is "//=" which does defined-or-equals
xdg
Okay - thanks - I figured something was left out but just wanted to make sure I wasn't missing some magical perl feature :)
BrianH
For the purpose of my question, I like this answer the best because it accomplishes what I was trying to do. It is true that there may be better ways to handle this as pointed out by brian d foy, but this one directly answers my question. Thanks xdg!
BrianH