diff options
Diffstat (limited to 'lib/Devscripts/Config.pm')
-rw-r--r-- | lib/Devscripts/Config.pm | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/lib/Devscripts/Config.pm b/lib/Devscripts/Config.pm new file mode 100644 index 0000000..e13a5a1 --- /dev/null +++ b/lib/Devscripts/Config.pm @@ -0,0 +1,418 @@ + +=head1 NAME + +Devscripts::Config - devscripts Perl scripts configuration object + +=head1 SYNOPSIS + + # Configuration module + package Devscripts::My::Config; + use Moo; + extends 'Devscripts::Config'; + + use constant keys => [ + [ 'text1=s', 'MY_TEXT', qr/^\S/, 'Default_text' ], + # ... + ]; + + has text1 => ( is => 'rw' ); + + # Main package or script + package Devscripts::My; + + use Moo; + my $config = Devscripts::My::Config->new->parse; + 1; + +=head1 DESCRIPTION + +Devscripts Perl scripts configuration object. It can scan configuration files +(B</etc/devscripts.conf> and B<~/.devscripts>) and command line arguments. + +A devscripts configuration package has just to declare: + +=over + +=item B<keys> constant: array ref I<(see below)> + +=item B<rules> constant: hash ref I<(see below)> + +=back + +=head1 KEYS + +Each element of B<keys> constant is an array containing four elements which can +be undefined: + +=over + +=item the string to give to L<Getopt::Long> + +=item the name of the B<devscripts.conf> key + +=item the rule to check value. It can be: + +=over + +=item B<regexp> ref: will be applied to the value. If it fails against the +devscripts.conf value, Devscripts::Config will warn. If it fails against the +command line argument, Devscripts::Config will die. + +=item B<sub> ref: function will be called with 2 arguments: current config +object and proposed value. Function must return a true value to continue or +0 to stop. This is not simply a "check" function: Devscripts::Config will not +do anything else than read the result to continue with next argument or stop. + +=item B<"bool"> string: means that value is a boolean. devscripts.conf value +can be either "yes", 1, "no", 0. + +=back + +=item the default value + +=back + +=head2 RULES + +It is possible to declare some additional rules to check the logic between +options: + + use constant rules => [ + sub { + my($self)=@_; + # OK + return 1 if( $self->a < $self->b ); + # OK with warning + return ( 1, 'a should be lower than b ) if( $self->a > $self->b ); + # NOK with an error + return ( 0, 'a must not be equal to b !' ); + }, + sub { + my($self)=@_; + # ... + return 1; + }, + ]; + +=head1 METHODS + +=head2 new() + +Constructor + +=cut + +package Devscripts::Config; + +use strict; +use Devscripts::Output; +use Dpkg::IPC; +use File::HomeDir; +use Getopt::Long qw(:config bundling permute no_getopt_compat); +use Moo; + +# Common options +has common_opts => ( + is => 'ro', + default => sub { + [[ + 'help', undef, + sub { + if ($_[1]) { $_[0]->usage; exit 0 } + } + ]] + }); + +# Internal attributes + +has modified_conf_msg => (is => 'rw', default => sub { '' }); + +$ENV{HOME} = File::HomeDir->my_home; + +our @config_files + = ('/etc/devscripts.conf', ($ENV{HOME} ? "$ENV{HOME}/.devscripts" : ())); + +sub keys { + die "conffile_keys() must be defined in sub classes"; +} + +=head2 parse() + +Launches B<parse_conf_files()>, B<parse_command_line()> and B<check_rules> + +=cut + +sub BUILD { + my ($self) = @_; + $self->set_default; +} + +sub parse { + my ($self) = @_; + + # 1 - Parse /etc/devscripts.conf and ~/.devscripts + $self->parse_conf_files; + + # 2 - Parse command line + $self->parse_command_line; + + # 3 - Check rules + $self->check_rules; + return $self; +} + +# I - Parse /etc/devscripts.conf and ~/.devscripts + +=head2 parse_conf_files() + +Reads values in B</etc/devscripts.conf> and B<~/.devscripts> + +=cut + +sub set_default { + my ($self) = @_; + my $keys = $self->keys; + foreach my $key (@$keys) { + my ($kname, $name, $check, $default) = @$key; + next unless (defined $default); + $kname =~ s/^\-\-//; + $kname =~ s/-/_/g; + $kname =~ s/[!\|=].*$//; + if (ref $default) { + unless (ref $default eq 'CODE') { + die "Default value must be a sub ($kname)"; + } + $self->{$kname} = $default->(); + } else { + $self->{$kname} = $default; + } + } +} + +sub parse_conf_files { + my ($self) = @_; + + my @cfg_files = @config_files; + if (@ARGV) { + if ($ARGV[0] =~ /^--no-?conf$/) { + $self->modified_conf_msg(" (no configuration files read)"); + shift @ARGV; + return $self; + } + my @tmp; + while ($ARGV[0] and $ARGV[0] =~ s/^--conf-?file(?:=(.+))?//) { + shift @ARGV; + my $file = $1 || shift(@ARGV); + if ($file) { + unless ($file =~ s/^\+//) { + @cfg_files = (); + } + push @tmp, $file; + } else { + return ds_die + "Unable to parse --conf-file option, aborting parsing"; + } + } + push @cfg_files, @tmp; + } + + @cfg_files = grep { -r $_ } @cfg_files; + my $keys = $self->keys; + if (@cfg_files) { + my @key_names = map { $_->[1] ? $_->[1] : () } @$keys; + my %config_vars; + + my $shell_cmd = q{for file ; do . "$file"; done ;}; + + # Read back values + $shell_cmd .= q{ printf '%s\0' }; + my @shell_key_names = map { qq{"\$$_"} } @key_names; + $shell_cmd .= join(' ', @shell_key_names); + my $shell_out; + spawn( + exec => [ + '/bin/bash', '-c', + $shell_cmd, 'devscripts-config-loader', + @cfg_files + ], + wait_child => 1, + to_string => \$shell_out + ); + @config_vars{@key_names} = map { s/^\s*(.*?)\s*/$1/ ? $_ : undef } + split(/\0/, $shell_out, -1); + + # Check validity and set value + foreach my $key (@$keys) { + my ($kname, $name, $check, $default) = @$key; + next unless ($name); + $kname //= ''; + $kname =~ s/^\-\-//; + $kname =~ s/-/_/g; + $kname =~ s/[!|=+].*$//; + # Case 1: nothing in conf files, set default + next unless (length $config_vars{$name}); + if (defined $check) { + if (not(ref $check)) { + $check + = $self->_subs_check($check, $kname, $name, $default); + } + if (ref $check eq 'CODE') { + my ($res, $msg) + = $check->($self, $config_vars{$name}, $kname); + ds_warn $msg unless ($res); + next; + } elsif (ref $check eq 'Regexp') { + unless ($config_vars{$name} =~ $check) { + ds_warn("Bad $name value $config_vars{$name}"); + next; + } + } else { + ds_die("Unknown check type for $name"); + return undef; + } + } + $self->{$kname} = $config_vars{$name}; + $self->{modified_conf_msg} .= " $name=$config_vars{$name}\n"; + if (ref $default) { + my $ref = ref $default->(); + my @tmp = ($config_vars{$name} =~ /\s+"([^"]*)"(?>\s+)/g); + $config_vars{$name} =~ s/\s+"([^"]*)"\s+/ /g; + push @tmp, split(/\s+/, $config_vars{$name}); + if ($ref eq 'ARRAY') { + $self->{$kname} = \@tmp; + } elsif ($ref eq 'HASH') { + $self->{$kname} + = { map { /^(.*?)=(.*)$/ ? ($1 => $2) : ($_ => 1) } + @tmp }; + } + } + } + } + return $self; +} + +# II - Parse command line + +=head2 parse_command_line() + +Parse command line arguments + +=cut + +sub parse_command_line { + my ($self, @arrays) = @_; + my $opts = {}; + my $keys = [@{ $self->common_opts }, @{ $self->keys }]; + # If default value is set to [], we must prepare hash ref to be able to + # receive more than one value + foreach (@$keys) { + if ($_->[3] and ref($_->[3])) { + my $kname = $_->[0]; + $kname =~ s/[!\|=].*$//; + $opts->{$kname} = $_->[3]->(); + } + } + unless (GetOptions($opts, map { $_->[0] ? ($_->[0]) : () } @$keys)) { + $_[0]->usage; + exit 1; + } + foreach my $key (@$keys) { + my ($kname, $tmp, $check, $default) = @$key; + next unless ($kname); + $kname =~ s/[!|=+].*$//; + my $name = $kname; + $kname =~ s/-/_/g; + if (defined $opts->{$name}) { + next if (ref $opts->{$name} eq 'ARRAY' and !@{ $opts->{$name} }); + next if (ref $opts->{$name} eq 'HASH' and !%{ $opts->{$name} }); + if (defined $check) { + if (not(ref $check)) { + $check + = $self->_subs_check($check, $kname, $name, $default); + } + if (ref $check eq 'CODE') { + my ($res, $msg) = $check->($self, $opts->{$name}, $kname); + ds_die "Bad value for $name: $msg" unless ($res); + } elsif (ref $check eq 'Regexp') { + if ($opts->{$name} =~ $check) { + $self->{$kname} = $opts->{$name}; + } else { + ds_die("Bad $name value in command line"); + } + } else { + ds_die("Unknown check type for $name"); + } + } else { + $self->{$kname} = $opts->{$name}; + } + } + } + return $self; +} + +sub check_rules { + my ($self) = @_; + if ($self->can('rules')) { + if (my $rules = $self->rules) { + my $i = 0; + foreach my $sub (@$rules) { + $i++; + my ($res, $msg) = $sub->($self); + if ($res) { + ds_warn($msg) if ($msg); + } else { + ds_error($msg || "config rule $i"); + # ds_error may not die if $Devscripts::Output::die_on_error + # is set to 0 + next; + } + } + } + } + return $self; +} + +sub _subs_check { + my ($self, $check, $kname, $name, $default) = @_; + if ($check eq 'bool') { + $check = sub { + $_[0]->{$kname} = ( + $_[1] =~ /^(?:1|yes)$/i ? 1 + : $_[1] =~ /^(?:0|no)$/i ? 0 + : $default ? $default + : undef + ); + return 1; + }; + } else { + $self->die("Unknown check type for $name"); + } + return $check; +} + +# Default usage: switch to manpage +sub usage { + $progname =~ s/\.pl//; + exec("man", '-P', '/bin/cat', $progname); +} + +1; +__END__ +=head1 SEE ALSO + +L<devscripts> + +=head1 AUTHOR + +Xavier Guimard E<lt>yadd@debian.orgE<gt> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2018 by Xavier Guimard <yadd@debian.org> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +=cut |