1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 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
|