Activator
view release on metacpan or search on metacpan
lib/Activator/Config.pm view on Meta::CPAN
command line options
=item *
environment variables
=item *
forced overrides from config files
=item *
merged settings from YAML configuration files
=back
=head1 COMMAND LINE ARGUMENTS
This module allows you to override configuration file settings from
the command line. You can use long or short options using C<'-'> or
C<'--'> notation, allows barewords in any order, and recognizes the
arguments terminator C<'--'>. Also supported are multiple flag
arguments:
#### turn on super verbosity. sets $config->{v} = 2
myscript.pl -v -v
You can specify configured options at the command line for
override:
#### override the configuration file setting for 'foo'
myscript.pl --foo=bar
Note that while YAML configuration (and this module) support deep
structures for configuration, you can only override top level keys
that are scalars using command line arguments and/or environment
variables.
=head2 Reserved Arguments
There are a few reserved command line arguments:
--skip_env : ignore environment variables (EXCEPT $USER)
--project=<> : used to search for the C<E<lt>projectE<gt>.yml> file
--realm=<> : use C<E<lt>realmE<gt>.yml> in config file processing and
consider all command line arguments to be in this realm
--conf_path : colon separated list of directories to search for config files
=head2 Project as a Bareword Argument
There are times where a script takes the project name as a required
bareword argument. For these cases, require that project be the last
argument, and pass a flag to L</get_config()>.
That is, when your script is called like this:
myscript.pl --options <project>
get the config like this:
Activator::Config->get_config( \@ARGV, undef, 1 );
The second argument to L</get_config()> is the realm, so you pass
C<undef> (unless you know the realm you are looking for) to allow the
command line options and environment variables to take affect.
=head1 ENVIRONMENT VARIABLES
Environment variables can be used to act as a default to command line
options, and/or override any top level configuration file key which is
a scalar. The expected format is C<ACT_CONFIG_[key]>. Note that YAML is
case sensitive, so the environment variables must match. Be especially
wary of command shell senstive characters in your YAML keys (like
C<:~E<gt>E<lt>|>).
If you wish to override a key for only a particular realm, you
can insert the realm into the env variable wrapped by double
underscores:
ACT_CONFIG_foo - set 'foo' for default realm
ACT_CONFIG__bar__foo - set 'foo' only for 'bar' realm
The L</Reserved Arguments> listed in the L</COMMAND LINE ARGUMENTS>
section also have corresponding environment variables with only
C<skip_env> being slightly different:
ACT_CONFIG_skip_env : set to 1 to skip, or 0 (or don't set it at all) to
not skip
ACT_CONFIG_project : same as command line argument
ACT_CONFIG_realm : same as command line argument
ACT_CONFIG_conf_path : same as command line argument
=head2 Automatically Imported Environment Variables
Since they tend to be generally useful, the following environment
variables are automatically imported into your configuration:
=over
=item *
HOME
=item *
USER
=back
it is L</FUTURE WORK> to make these cross-platform compatible.
=head1 CONFIGURATION FILES
Currently, you can put your YAML configuration file wherever you like,
but you must set a key inside your configuration files C<conf_path>,
then set the environment variable C<ACT_CONFIG_conf_path> or use the
C<--conf_path> option. It is somewhat wonky the way this currently
works, and it'll get fixed Real Soon Now.
This path behaves the same as a bash shell C<$PATH>, in that you can
set this to one or more colon separated fully qualified path values.
Note that the leftmost path takes precedence when processing config
files.
lib/Activator/Config.pm view on Meta::CPAN
=over
=item *
All configuration files are read and merged together with higher
precedence configuration files overriding lower precedence on a realm
by realm basis.
If identically named files exist in the C<conf_path> for any level
(user, realm, project, organization), only the first discovered file
is used. Put another way, the leftmost path in the C<conf_path> takes
precedence for any file name conflict.
=item *
The C<default> realm is merged into each realm (I<realm>'s values
taking precedence).
=item *
All C<default> realm environment variables override all values for
each I<realm> (excepting the C<overrides> realm).
=item *
All specific I<realm> environment variables override that realm's values.
=item *
The C<default> realm overrides section is used to override matching
keys in each I<realm>.
=item *
The specific I<realm> overrides section is used to override matching keys
in I<realm>.
=item *
Any command line options given override ALL matching keys for ALL realms.
=item *
# TODO: NOT YET IMPLEMENTED
Perform variable substitution
=back
=head1 METHODS
=cut
sub new {
my ( $pkg ) = @_;
my $self = bless( {
REGISTRY => Activator::Registry->new(),
ARGV_EXTRA => {},
ARGV => undef,
BAREWORDS => undef,
}, $pkg);
$self->_init_StrongSingleton();
return $self;
}
=head2 get_config()
Process command line arguments, environment variables and
configuration files then return a hashref representing the merged
configuration. Recognized configuration items are removed from C<@ARGV>.
Usage:
Activator::Config->get_config( \@ARGV, $realm, $project_is_arg );
C<$realm> is optional (default is 'default'). If undefined, it will be
determined from a command line option or environment variable.
C<$project_is_arg> is optional. Use any true value for this argument
if your script requries the project name as the last bareword
argument.
Examples:
#
# get options for default realm
#
my $config = Activator::Config->get_config( \@ARGV );
#
# get options for 'some' realm, ignoring --realm and ACT_CONFIG_realm
#
my $config = Activator::Config->get_config( \@ARGV, 'some' );
#
# don't ignore --realm and ACT_CONFIG_realm, use $barewords[-1] (the
# last bareword argument) as the project
#
Activator::Config->get_config( \@ARGV, undef, 1 );
See L</get_args()> for a description of the way command line arguments
are processed.
If called repeatedly, this sub does NOT reprocess C<\@ARGV>. This
allows you to make multiple calls to get a reference to the config for
multiple realms if desired.
=cut
sub get_config {
my ( $pkg, $argv, $realm, $project_is_arg ) = @_;
my $self = &new( @_ );
# get_args sets $self->{ARGV}
$self->get_args( $argv );
DEBUG( Data::Dumper->Dump( [ $self->{ARGV} ], [ qw/ ARGV / ] ) );
DEBUG( Data::Dumper->Dump( [ $self->{BAREWORDS} ], [ qw /BAREWORDS/ ] ) );
# make sure we can use ENV vars
my $skip_env = $ENV{ACT_CONFIG_skip_env};
$realm ||=
$self->{ARGV}->{realm} ||
( $skip_env ? undef : $ENV{ACT_CONFIG_realm} ) ||
'default';
if ( ref( $realm ) ) {
Activator::Exception::Config->throw( 'realm_specified_more_than_once', Dumper( $realm ) );
}
if ( $realm ne 'default' ) {
Activator::Registry->set_default_realm( $realm );
}
# setup or get the merged YAML configuration settings from files
# into the registry
my $config = $self->{REGISTRY}->get_realm( $realm );
# first call
if ( !keys %$config ) {
# define valid config from config files
try eval {
$self->_process_config_files( $realm, $skip_env, $project_is_arg );
};
if ( catch my $e ) {
$e->rethrow;
}
# read environment variables, set any keys found
if ( !$skip_env ) {
my ( $env_key, $env_realm );
foreach my $env_key ( keys %ENV ) {
next unless $env_key =~ /^ACT_CONFIG_(.+)/;
$opt_key = $1;
$opt_realm = $realm;
my $env_opt_realm = $opt_realm;
my $env_opt_key = $opt_key;
if ( $opt_key =~ /^_(\w+)__(\w+)$/ ) {
$env_opt_realm = $1;
$env_opt_key = $2;
if ( $env_opt_realm eq $realm ) {
$opt_key = $env_opt_key;
$opt_realm = $env_opt_realm;
}
}
if ( $self->{REGISTRY}->get( $opt_key, $opt_realm ) ) {
$self->{REGISTRY}->register( $opt_key, $ENV{ $env_key }, $opt_realm );
}
elsif( $env_opt_realm ne $opt_realm &&
!grep( /$opt_key/, qw( skip_env project
realm conf_path ) ) ) {
WARN( "Skipped invalid environment variable $env_key. Key '$opt_key' for realm '$opt_realm' unchanged");
}
}
}
# forced overrides from config files
my $overrides = $self->{REGISTRY}->get_realm( 'overrides' );
DEBUG( Data::Dumper->Dump( [ $overrides ], [ 'processing overrides' ] ) );
# NOTE: bad (typo) keys could be in overrides. Someday,
# Activator::Registry will allow debug mode so we can
lib/Activator/Config.pm view on Meta::CPAN
Arguments can be barewords, '-' notation or '--' notation.
=item *
Any arguments after the arguments terminator symbol (a plain '--'
argument) are returned as barewords. Bareword order of specification
is maintained.
=item *
Values with spaces must be double-quoted, and can themselves contain quotes
--mode="sliding out of control"
--plan="pump the "brakes" vigorously"
=item *
Flag arguments are counted. That is C<-v -v> would set C<$config-E<gt>{v} = 2>
=item *
Argument bundling is not supported.
=back
Examples:
@ARGV | Value returned
----------------------+-----------------------------------------
--arg | $argv = { arg => 1 }
--arg --arg | $argv = { arg => 2 }
--arg=val | $argv = { arg => 'val' }
--arg=val --arg=val2 | $argv = { arg => [ 'val', 'val2' ] }
--arg="val val" | $argv = { arg => 'val val' }
Returns array: C<( $args_hashref, $barewords_arrayref )>
Throws C<Activator::Exception::Config> when arg is invalid (which at this
time is only when a barewod arg of '=' is detected).
=cut
sub get_args {
my ( $pkg, $argv_raw ) = @_;
my $self = &new( @_ );
# quick and dirty check for debug mode
if( grep /^--?debug$/, @$argv_raw ) {
Activator::Log->level('DEBUG');
DEBUG('Entering debug mode');
}
if ( defined( $self->{ARGV} ) || defined( $self->{BAREWORDS} ) ) {
DEBUG("skipping ARGV reprocessing");
return ( $self->{ARGV}, $self->{BAREWORDS} );
}
DEBUG("got ARGV: ". join(' ', @$argv_raw ));
# use refs to insure that that $self->{ARGV} and
# $self->{BAREWORDS} are defined, so we don't return undef.
my $argv = {};
my $barewords = [];
my $found_terminator = 0;
foreach my $arg ( @$argv_raw ) {
my ( $key, $value ) = $self->_get_arg( $arg );
if ( $found_terminator || !defined( $key ) ) {
DEBUG("'$arg' is a bareword or after the args terminator '--'");
push @$barewords, $arg;
next;
}
if( $key eq '--' ) {
DEBUG("'$arg' is the terminator");
$found_terminator = 1;
next;
}
if ( defined $value ) {
DEBUG("got key '$key' = '$value'");
# if we see an argument again, coerce this value into an
# array
if ( exists $argv->{ $key } ) {
if ( reftype ( $argv->{ $key } ) eq 'ARRAY' ) {
DEBUG("added '$value' to key list '$key'" );
push @{ $argv->{ $key } }, $value;
}
else {
DEBUG("created key list '$key' and added '$value'" );
$argv->{ $key } = [ $argv->{ $key }, $value ];
}
}
# just set it
else {
$argv->{ $key } = $value;
}
}
else {
# if we see a value again, increment the occurence count
if ( exists $argv->{ $key } ) {
DEBUG("incremented key '$key'" );
$argv->{ $key }++;
}
else {
DEBUG("set $key" );
$argv->{ $key } = 1;
}
}
}
# save these so we don't have to do it again
$self->{ARGV} = $argv;
$self->{BAREWORDS} = $barewords;
return ( $argv, $barewords );
}
# Helper to split an arg into key/value. Returns ($key, $value), where
# $value is undef if the argument is flag format (--debug), undef if
# it is a bareword ( foo ) and '--' if it is the arguments terminator
# symbol.
#
sub _get_arg {
my ( $self, $arg ) = @_;
if ( $arg !~ /^-(-)?/ ) {
return;
}
if ( $arg eq '--' ) {
return $arg;
}
my ( $key, $value ) = split /=/xms, $arg, 2;
if ( !defined $key ) {
Activator::Exception::Config->throw( 'argument',
'invalid',
$arg );
}
# clean up key
$key =~ s/^--?//;
# clean up value, if quoted
if ( defined $value ) {
$value =~ s/^"//;
$value =~ s/"$//;
}
return ( $key, $value );
}
# Merge config files into this objects Activator::Registry object
sub _process_config_files {
my ( $pkg, $realm, $skip_env, $project_is_arg ) = @_;
my $self = &new( @_ );
# figure out what project we are working on
my $project =
$self->{ARGV}->{project} ||
( $project_is_arg ? $self->{BAREWORDS}->[-1] : undef ) ||
( $skip_env ? undef : $ENV{ACT_CONFIG_project} ) ||
Activator::Exception::Config->throw( 'project', 'missing' );
# process these files:
# $ENV{USER}.yml
# <realm>.yml - realm specific settings and defaults
# <project>.yml - project specific settings and defaults
# org.yml - top level organization settings and defaults
# in one of these paths, if set
# --conf_file= : use $self->{ARGV}->{conf_file} (which could be an arrayref )
# ACT_CONFIG_conf_file= : comma separated list of files
my $conf_path = $self->{ARGV}->{conf_path};
if ( ! $conf_path ) {
$conf_path = ( $skip_env ? undef : $ENV{ACT_CONFIG_conf_path} );
if ( !$conf_path ) {
ERROR( "Neither ACT_CONFIG conf_path env var nor --conf_path set");
Activator::Exception::Config->throw( 'conf_path', 'missing' );
}
else {
INFO( "Using ACT_CONFIG_conf_path env var: $conf_path");
}
}
else {
INFO( "Using conf_path argument: $conf_path");
}
my @search_paths = split ':', $conf_path;
DEBUG( 'Searching for conf files in: ' . Data::Dumper->Dump( [ \@search_paths ], [ qw/ search_paths / ] ) );
# Search for these files, create a files lookup.
my $files = { user => { target => "$ENV{USER}.yml" },
realm => { target => "${realm}.yml" },
project => { target => "${project}.yml" },
org => { target => 'org.yml' } };
foreach my $path ( @search_paths ) {
$path =~ s|/$||;
foreach my $which ( keys %$files ) {
my $target = $files->{ $which }->{target};
if ( !opendir DIR, $path ) {
WARN( "Ignoring invalid path '$path'" );
} else {
my @found = grep { /^$target$/ && -f "$path/$_" } readdir(DIR);
if ( @found ) {
my $file = "$path/$found[0]";
if ( !exists( $files->{ $which }->{ file } ) ) {
$files->{ $which }->{file} = $file;
} else {
INFO( "Ignoring lower priority config file '$file'" );
}
}
}
}
}
DEBUG ( 'Processing config files: ' . Data::Dumper->Dump( [ $files ], [ qw/ files / ] ) );
# now that we have all the files, import 'em! This is a super long
# winded but safe "left precedence" merge of all files
my ( $user_config, $realm_config, $project_config, $org_config );
try eval {
if ( exists( $files->{user}->{file} ) ) {
$user_yml = YAML::Syck::LoadFile( $files->{user}->{file} );
}
};
if ( catch my $e ) {
Activator::Exception::Config->throw( 'user_config', 'invalid', $e );
}
try eval {
if ( exists( $files->{realm}->{file} ) ) {
$realm_yml = YAML::Syck::LoadFile( $files->{realm}->{file} );
( run in 1.692 second using v1.01-cache-2.11-cpan-d80b1682f3f )