http://www.perlmonks.org?node_id=139970

kvh009 has asked for the wisdom of the Perl Monks concerning the following question:

I would like options given on the command line to have priority over all other options (i.e. those in the perl script and those in option files).

There is a option to given a option file on the command line (i.e. --conf=file).

I need a way to parse the command line options looking for --conf=file, if so then do file and then parse the rest of the command line options (minus the --conf=file). In summary, I need to handle the --conf=file before looking at the other command line options to keep the priority correct.
my %help_options = ( "color!" => sub { $options{USE_COLOR} = $_[1] }, "conf=s" => sub { $options{CONF} = $_[1] }, ); GetOptions( %help_options ); do $options{CONF} if defined $options{CONF};
Obviously, this doesn't do what I want as the 'do' will be executed after parsing the command line options.
script.pl --conf=file --color should always set $options{USE_COLOR} = 1 regardless of what's in file.

Replies are listed 'Best First'.
Re: Prioritizing command line options
by grinder (Bishop) on Jan 19, 2002 at 02:55 UTC
    I know that Getopt::Mixed will let you do this, as it uses a serial approach to parsing the command line. On the other hand, a module such as Getopt::Long will not, as you can't intervene (Although I expect to be corrected if am I wrong on this point) in GetOptions, since there is no callback interface.

    Getopt::Mixed turns the problem on its head, by the fact that you specify a template string for what arguments are to be recognised, and then you repeatedly call a function until it returns no more arguments. Consider:

    Getopt::Mixed::init( 'c=s f=s conf>c foo>f'); while( my( $option, $value, $pretty ) = Getopt::Mixed::nextOption() ) +{ OPTION: { $option eq 'c' and do { # you want to do more error checking that this... eval { do $value }; die "Cannot load config file $value: $@\n"; last OPTION; }; $option eq 'f' and do { $Foo = $value; last OPTION; }; # ... } } Getopt::Mixed::cleanup();

    In this way, the config file specified by --conf will be done at the point at which it is encountered on the command line.

    The only (minor) drawback is that this module is not bundled with the core distribution, you have to download it from CPAN. But it is a pure-perl module and so installs with a minimum of fuss.

    --
    g r i n d e r
    print@_{sort keys %_},$/if%_=split//,'= & *a?b:e\f/h^h!j+n,o@o;r$s-t%t#u';
(RhetTbull) Re: Prioritizing command line options
by RhetTbull (Curate) on Jan 19, 2002 at 02:52 UTC
    I had this same issue recently. I rolled my own solution and it's been working well. I used Getopt::Long to parse the options on the command line and my own wrapper around Config::IniFiles to parse the INI file. Basically, what I do is create an empty hash to hold the options. Then I pass that hash to a sub that processes the command line, setting any elements in the hash that are specified there. Then I pass the same hash to another sub to process the INI file which will set options ONLY if they haven't already been set on the command line (e.g. the options are not already defined in the hash). In the main program I have this:
    use ConfigIni; #my own wrapper use Getopt::Long; my %config; get_options(\%config); init_config(\%config); #now %config holds all your parameters and they're properly initialize +d -- the command line overrides anything in the ini file
    These two subs are defined like this (there are probably cleaner ways to process the args but this works fine for my simple prog.)
    sub get_options { #process any command line options my $config = shift; #hash ref my ($debug,$test,$ini_file,$max_files, $data_dir,$log_dir,$user,$pass,$verbose, $no_ini); GetOptions('debug=i' => \$debug, 'test=i' => \$test, 'ini_file=s' => \$ini_file, 'max_files=i' => \$max_files, 'data_dir=s' => \$data_dir, 'log_dir=s' => \$log_dir, 'user=s' => \$user, 'pass=s' => \$pass, 'verbose=i' => \$verbose, 'no_ini' => \$no_ini, ); $config->{debug} = $debug if defined $debug; $config->{test} = $test if defined $test; $config->{ini_file} = $ini_file if defined $ini_file; $config->{max_files} = $max_files if defined $max_files; $config->{data_dir} = $data_dir if defined $data_dir; $config->{log_dir} = $log_dir if defined $log_dir; $config->{user} = $user if defined $user; $config->{password} = $pass if defined $pass; $config->{verbose} = $verbose if defined $verbose; $config->{no_ini} = $no_ini if defined $no_ini; } sub init_config { #initialize the config parameters for the program my $config = shift; #hash ref my $bindir = $FindBin::Bin; #these get set no matter what $config->{version} = $VERSION; #these first two are just program + constants $config->{revision} = ($REVISION =~ /(\d+\.\d+)/) ? $1 : '?'; #if command line option -no_ini specified, then don't try to load +the ini file unless ($config->{no_ini}) { #process the ini file #if we already have an ini_file, use that one, otherwise use t +he default file my $config_file = $config{ini_file} || "$bindir/configfile.ini +"; #default name for ini file my $ini = new ConfigIni($config_file) || croak "could not load + ini file $config_file"; $config->{ini_file} = $config_file if not defined $config->{in +i_file}; #now load and set the parameters $config->{debug} = $ini->get_ini_val('Debug','Debug',0) if not + defined $config->{debug}; $config->{test} = $ini->get_ini_val('Debug','Test',0) if not +defined $config->{test}; $config->{max_files} = $ini->get_ini_val('Debug','Max_files',0 +) if not defined $config->{max_files}; $config->{data_dir} = $ini->get_ini_val('General','Data_dir') +if not defined $config->{data_dir}; $config->{log_dir} = $ini->get_ini_val('General','Log_dir') if + not defined $config->{log_dir}; $config->{user} = $ini->get_ini_val('General','User') if not d +efined $config->{user}; $config->{password} = $ini->get_ini_val('General','Password') +if not defined $config->{password}; $config->{verbose} = $ini->get_ini_val('General','Verbose',1) +if not defined $config->{verbose}; } #unless ($config->{no_ini}) #if log_dir isn't set, put logs in the same directory as the progr +am $config->{log_dir} ||= $bindir; $config->{log_file} = $config{log_dir} . "/log_file_" . strftime(" +%Y%m%d_%H%M%S_",localtime) . "$$.log"; #init anything that needs to be and might not have been done on ei +ther command line or ini_file $config->{debug} = 0 if not defined $config->{debug}; $config->{test} = 0 if not defined $config->{test}; $config->{verbose} = 1 if not defined $config->{verbose}; $config->{max_files} = 0 if not defined $config->{max_files}; #check to make sure info is complete croak "User must be specified in [General] section of ini file" if + !defined $config->{user}; croak "Password must be specified in [General] ini file" if !defin +ed $config->{password}; croak "Data_dir must be specified in [General] section of ini file +" if !defined $config->{data_dir}; } #sub init_config
    For various reasons I wrote a wrapper around Config::IniFiles. One reason was to allow multivalued parameters (which Config::Ini did but had other issues). Here's that custom wrapper:
    package ConfigIni; use strict; use warnings; use Config::IniFiles; use Text::CSV; use Carp; our $VERSION = '0.01'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; $self->{file} = shift || ""; $self->{ini} = new Config::IniFiles( -file => $self->{file}, -noca +se => 1); return undef if not defined $self->{ini}; bless $self, $class; return $self; } sub get_ini_val { #return a single valued parameter my $self = shift; my $section = shift || croak "must provide section to get_ini_val" +; my $param = shift || croak "must provide param to get_ini_val"; my $default = shift; #optional default value my $val = $self->{ini}->val($section,$param); if (!defined $val) { $val = $default; #undef if default no provided } return $val; } sub get_ini_val_multi { #return a list of a multi-valued parameter my $self = shift; my $section = shift || croak "must provide section to get_ini_val" +; my $param = shift || croak "must provide param to get_ini_val"; my $default = shift; #array ref to optional default values my $values = $self->{ini}->val($section,$param); my @values = (); if (defined $values) { my $csv = Text::CSV->new(); my $status = $csv->parse($values); croak "error parsing $section, $param, with value = $values" u +nless $status; @values = $csv->fields(); } else { @values = $default ? @{$default} : (); } return @values; } 1;
Re: Prioritizing command line options
by VSarkiss (Monsignor) on Jan 19, 2002 at 02:54 UTC

    You don't show what's in file, but why not just use another option variable? (Note, this is untested.)

    my %help_options = ( "color!" => sub { $options{USE_COLOR_CMDLINE} = $_[1] }, "conf=s" => sub { $options{CONF} = $_[1] }, ); GetOptions( %help_options ); do $options{CONF} if defined $options{CONF}; # Override it if specified on the command line. $options{USE_COLOR} = $options{USE_COLOR_CMDLINE} if exists $options{USE_COLOR_CMDLINE};
    That's not necessarily the easiest or best way to accomplish it, but it's probably the smallest change to your example code that would do it.

    On a separate note, I cringe at the idea that your program variables are spread among its own and the external file. (In other words, the external file knows the name of the %options hash and the particular key values, so if you change one you have to change the other.) I'd much rather use a "ini" style file with var = value pairs or some such, where I can map the keys to my variables through a hash or something. Of course, I don't know what limitations you're working under, so maybe this isn't practical for you, but if it is, I'd really recommend the change.