From 687cd059a82ccce6396e5a484dd3041d512145da Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Tue, 31 Mar 2026 14:53:46 +0200 Subject: [PATCH] Add file inclusion mechanism to the config parser. --- btrbk | 87 ++++++++++++++++++++++++++++++++------- doc/btrbk.conf.5.asciidoc | 23 +++++++++++ 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/btrbk b/btrbk index ddb2eb4..f796c84 100755 --- a/btrbk +++ b/btrbk @@ -2093,7 +2093,7 @@ sub system_urandom($;$) { binmode URANDOM; my $rand; my $rlen = read(URANDOM, $rand, $size); - close(FILE); + close(URANDOM); unless(defined($rand) && ($rlen == $size)) { ERROR "Failed to read from /dev/urandom: $!"; return undef; @@ -4313,17 +4313,29 @@ sub _config_file(@) { return undef; } -sub parse_config($) +# Recursively parse a single config file, following any "include" directives. +# Returns the updated $cur context on success, undef on error. +# $seen is a hashref mapping realpath -> 1 for files currently on the include +# stack, used to detect circular includes. +sub _parse_config_file { - my $file = shift; - return undef unless($file); + my ($file, $root, $cur, $seen) = @_; + $seen //= {}; - my $root = init_config(SRC_FILE => $file); - my $cur = $root; + my $real = abs_path($file) // $file; + if($seen->{$real}) { + ERROR "Circular include detected for \"$file\""; + return undef; + } + $seen->{$real} = 1; TRACE "config: open configuration file: $file" if($do_trace); - open(FILE, '<', $file) or die $!; - while () { + open(my $fh, '<', $file) or do { + ERROR "Cannot open configuration file \"$file\": $!"; + delete $seen->{$real}; + return undef; + }; + while(<$fh>) { chomp; s/((?:[^"'#]*(?:"[^"]*"|'[^']*'))*[^"'#]*)#.*/$1/; # remove comments next if /^\s*$/; # ignore empty lines @@ -4332,19 +4344,64 @@ sub parse_config($) TRACE "config: parsing line $. with context=$cur->{CONTEXT}: \"$_\"" if($do_trace); unless(/^([a-zA-Z_]+)(?:\s+(.*))?$/) { ERROR "Parse error in \"$file\" line $."; - $root = undef; - last; + close $fh; + delete $seen->{$real}; + return undef; } - unless($cur = parse_config_line($cur, lc($1), $2 // "", error_statement => "in \"$file\" line $.")) { - $root = undef; - last; + my ($key, $value) = (lc($1), $2 // ""); + if($key eq "include") { + $value =~ s/^"(.*)"$/$1/; + $value =~ s/^'(.*)'$/$1/; + # resolve a relative pattern against the directory of the including file + unless($value =~ m{^/}) { + (my $dir = $file) =~ s{/[^/]*$}{}; + $dir = "." if($dir eq $file); # $file had no slash; use current directory + $value = "$dir/$value"; + } + my @inc_files; + for my $m (sort glob($value)) { + if(-d $m) { + push @inc_files, sort glob("$m/*.conf"); + } elsif(-e $m) { + push @inc_files, $m; + } + } + for my $inc_file (@inc_files) { + TRACE "config: including file: $inc_file (from \"$file\" line $.)" if($do_trace); + $cur = _parse_config_file($inc_file, $root, $cur, $seen); + unless(defined($cur)) { + close $fh; + delete $seen->{$real}; + return undef; + } + } + } else { + unless($cur = parse_config_line($cur, $key, $value, error_statement => "in \"$file\" line $.")) { + close $fh; + delete $seen->{$real}; + return undef; + } } TRACE "line processed: new context=$cur->{CONTEXT}" if($do_trace); } - close FILE || ERROR "Failed to close configuration file: $!"; + close $fh || ERROR "Failed to close configuration file: $!"; + + delete $seen->{$real}; + return $cur; +} + +sub parse_config($) +{ + my $file = shift; + return undef unless($file); + + my $root = init_config(SRC_FILE => $file); + + unless(_parse_config_file($file, $root, $root, {})) { + return undef; + } _config_propagate_target($root); - return $root; } diff --git a/doc/btrbk.conf.5.asciidoc b/doc/btrbk.conf.5.asciidoc index 04f8e4d..ddd74aa 100644 --- a/doc/btrbk.conf.5.asciidoc +++ b/doc/btrbk.conf.5.asciidoc @@ -37,6 +37,29 @@ Blank lines are ignored. A hash character (#) starts a comment extending until end of line. +INCLUDE FILES +------------- + +*include* :: + Include additional configuration files matching the shell glob + ''. All matching files are processed in sorted order. + If '' matches a directory, it is treated as if + +'/*.conf'+ had been specified, i.e. all files ending in + +.conf+ directly inside that directory are included. ++ +-- +A relative '' is resolved relative to the directory of the +file containing the 'include' directive. + +Circular includes are detected and reported as an error. + +Example: + + include /etc/btrbk/btrbk.d/*.conf + include /etc/btrbk/btrbk.d +-- + + SECTIONS --------