123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- # ------------------------------------------------------------------------------
- # NAME
- # Fcm::CfgFile
- #
- # DESCRIPTION
- # This class is used for reading and writing FCM config files. A FCM config
- # file is a line-based text file that provides information on how to perform
- # a particular task using the FCM system.
- #
- # COPYRIGHT
- # (C) Crown copyright Met Office. All rights reserved.
- # For further details please refer to the file COPYRIGHT.txt
- # which you should have received as part of this distribution.
- # ------------------------------------------------------------------------------
- package Fcm::CfgFile;
- @ISA = qw(Fcm::Base);
- # Standard pragma
- use warnings;
- use strict;
- # Standard modules
- use Carp;
- use File::Basename;
- use File::Path;
- use File::Spec;
- # FCM component modules
- use Fcm::Base;
- use Fcm::CfgLine;
- use Fcm::Config;
- use Fcm::Keyword;
- use Fcm::Util;
- # List of property methods for this class
- my @scalar_properties = (
- 'actual_src', # actual source of configuration file
- 'lines', # list of lines, Fcm::CfgLine objects
- 'pegrev', # peg revision of configuration file
- 'src', # source of configuration file
- 'type', # type of configuration file
- 'version', # version of configuration file
- );
- # Local module variables
- my $expand_type = 'bld|ext'; # config file type that needs variable expansions
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $obj = Fcm::CfgFile->new (%args);
- #
- # DESCRIPTION
- # This method constructs a new instance of the Fcm::CfgFile class. See above
- # for allowed list of properties. (KEYS should be in uppercase.)
- # ------------------------------------------------------------------------------
- sub new {
- my $this = shift;
- my %args = @_;
- my $class = ref $this || $this;
- my $self = Fcm::Base->new (%args);
- bless $self, $class;
- for (@scalar_properties) {
- $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
- }
- return $self;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $value = $obj->X;
- # $obj->X ($value);
- #
- # DESCRIPTION
- # Details of these properties are explained in @scalar_properties.
- # ------------------------------------------------------------------------------
- for my $name (@scalar_properties) {
- no strict 'refs';
- *$name = sub {
- my $self = shift;
- if (@_) {
- $self->{$name} = $_[0];
- }
- if (not defined $self->{$name}) {
- if ($name eq 'lines') {
- $self->{$name} = [];
- }
- }
- return $self->{$name};
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $mtime = $obj->mtime ();
- #
- # DESCRIPTION
- # This method returns the modified time of the configuration file source.
- # ------------------------------------------------------------------------------
- sub mtime {
- my $self = shift;
- my $mtime = undef;
- if (-f $self->src) {
- $mtime = (stat $self->src)[9];
- }
- return $mtime;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $read = $obj->read_cfg ();
- #
- # DESCRIPTION
- # This method reads the current configuration file. It returns the number of
- # lines read from the config file, or "undef" if it fails. The result is
- # placed in the LINES array of the current instance, and can be accessed via
- # the "lines" method.
- # ------------------------------------------------------------------------------
- sub read_cfg {
- my $self = shift;
- my @lines = $self->_get_cfg_lines;
- # List of CFG types that need INC declarations expansion
- my %exp_inc = ();
- for (split (/$Fcm::Config::DELIMITER_LIST/, $self->setting ('CFG_EXP_INC'))) {
- $exp_inc{uc ($_)} = 1;
- }
- # List of CFG labels that are reserved keywords
- my %cfg_keywords = ();
- for (split (/$Fcm::Config::DELIMITER_LIST/, $self->setting ('CFG_KEYWORD'))) {
- $cfg_keywords{$self->cfglabel ($_)} = 1;
- }
- # Loop each line, to separate lines into label : value pairs
- my $cont = undef;
- my $here = undef;
- for my $line_num (1 .. @lines) {
- my $line = $lines[$line_num - 1];
- chomp $line;
- my $label = '';
- my $value = '';
- my $comment = '';
- # If this line is a continuation, set $start to point to the line that
- # starts this continuation. Otherwise, set $start to undef
- my $start = defined ($cont) ? $self->lines->[$cont] : undef;
- my $warning = undef;
- if ($line =~ /^(\s*#.*)$/) { # comment line
- $comment = $1;
- } elsif ($line =~ /\S/) { # non-blank line
- if (defined $cont) {
- # Previous line has a continuation mark
- $value = $line;
- # Separate value and comment
- if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
- $comment = $1;
- }
- # Remove leading spaces
- $value =~ s/^\s*\\?//;
- # Expand environment variables
- my $warn;
- ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
- $warning .= ($warning ? ', ' : '') . $warn if $warn;
- # Expand internal variables
- ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
- $warning .= ($warning ? ', ' : '') . $warn if $warn;
- # Get "line" that begins the current continuation
- my $v = $start->value . $value;
- $v =~ s/\\$//;
- $start->value ($v);
- } else {
- # Previous line does not have a continuation mark
- if ($line =~ /^\s*(\S+)(?:\s+(.*))?$/) {
- # Check line contains a valid label:value pair
- $label = $1;
- $value = defined ($2) ? $2 : '';
- # Separate value and comment
- if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
- $comment = $1;
- }
- # Remove trailing spaces
- $value =~ s/\s+$//;
- # Value begins with $HERE?
- $here = ($value =~ /\$\{?HERE\}?(?:[^A-Z_]|$)/);
- # Expand environment variables
- my $warn;
- ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
- $warning .= ($warning ? ', ' : '') . $warn if $warn;
- # Expand internal variables
- ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
- $warning .= ($warning ? ', ' : '') . $warn if $warn;
- }
- }
- # Determine whether current line ends with a continuation mark
- if ($value =~ s/\\$//) {
- $cont = scalar (@{ $self->lines }) unless defined $cont;
- } else {
- $cont = undef;
- }
- }
- if (exists $exp_inc{uc ($self->type)} and
- uc ($start ? $start->label : $label) eq $self->cfglabel ('INC') and
- not defined $cont) {
- # Current configuration file requires expansion of INC declarations
- # The start/current line is an INC declaration
- # The current line is not a continuation or is the end of the continuation
- # Get lines from an "include" configuration file
- my $src = ($start ? $start->value : $value);
- $src .= '@' . $self->pegrev if $here and $self->pegrev;
- if ($src) {
- # Invoke a new instance to read the source
- my $cfg = Fcm::CfgFile->new (
- SRC => expand_tilde ($src), TYPE => $self->type,
- );
- $cfg->read_cfg;
- # Add lines to the lines array in the current configuration file
- $comment = 'INC ' . $src . ' ';
- push @{$self->lines}, Fcm::CfgLine->new (
- comment => $comment . '# Start',
- number => ($start ? $start->number : $line_num),
- src => $self->actual_src,
- warning => $warning,
- );
- push @{ $self->lines }, @{ $cfg->lines };
- push @{$self->lines}, Fcm::CfgLine->new (
- comment => $comment . '# End',
- src => $self->actual_src,
- );
- } else {
- push @{$self->lines}, Fcm::CfgLine->new (
- number => $line_num,
- src => $self->actual_src,
- warning => 'empty INC declaration.'
- );
- }
- } else {
- # Push label:value pair into lines array
- push @{$self->lines}, Fcm::CfgLine->new (
- label => $label,
- value => ($label ? $value : ''),
- comment => $comment,
- number => $line_num,
- src => $self->actual_src,
- warning => $warning,
- );
- }
- next if defined $cont; # current line not a continuation
- my $slabel = ($start ? $start->label : $label);
- my $svalue = ($start ? $start->value : $value);
- next unless $slabel;
- # Check config file type and version
- if (index (uc ($slabel), $self->cfglabel ('CFGFILE')) == 0) {
- my @words = split /$Fcm::Config::DELIMITER_PATTERN/, $slabel;
- shift @words;
- my $name = @words ? lc ($words[0]) : 'type';
- if ($self->can ($name)) {
- $self->$name ($svalue);
- }
- }
- # Set internal variable
- $slabel =~ s/^\%//; # Remove leading "%" from label
- $self->config->variable ($slabel, $svalue)
- unless exists $cfg_keywords{$slabel};
- }
- # Report and reset warnings
- # ----------------------------------------------------------------------------
- for my $line (@{ $self->lines }) {
- w_report $line->format_warning if $line->warning;
- $line->warning (undef);
- }
- return @{ $self->lines };
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $rc = $obj->print_cfg ($file, [$force]);
- #
- # DESCRIPTION
- # This method prints the content of current configuration file. If no
- # argument is specified, it prints output to the standard output. If $file is
- # specified, and is a writable file name, the output is sent to the file. If
- # the file already exists, its content is compared to the current output.
- # Nothing will be written if the content is unchanged unless $force is
- # specified. Otherwise, for typed configuration files, the existing file is
- # renamed using a prefix that contains its last modified time. The method
- # returns 1 if there is no error.
- # ------------------------------------------------------------------------------
- sub print_cfg {
- my ($self, $file, $force) = @_;
- # Count maximum number of characters in the labels, (for pretty printing)
- my $max_label_len = 0;
- for my $line (@{ $self->lines }) {
- next unless $line->label;
- my $label_len = length $line->label;
- $max_label_len = $label_len if $label_len > $max_label_len;
- }
- # Output string
- my $out = '';
- # Append each line of the config file to the output string
- for my $line (@{ $self->lines }) {
- $out .= $line->print_line ($max_label_len - length ($line->label) + 1);
- $out .= "\n";
- }
- if ($out) {
- my $old_select = select;
- # Open file if necessary
- if ($file) {
- # Make sure the host directory exists and is writable
- my $dirname = dirname $file;
- if (not -d $dirname) {
- print 'Make directory: ', $dirname, "\n" if $self->verbose;
- mkpath $dirname;
- }
- croak $dirname, ': cannot write to config file directory, abort'
- unless -d $dirname and -w $dirname;
- if (-f $file and not $force) {
- if (-r $file) {
- # Read old config file to see if content has changed
- open IN, '<', $file or croak $file, ': cannot open (', $!, '), abort';
- my $in_lines = '';
- while (my $line = <IN>) {
- $in_lines .= $line;
- }
- close IN or croak $file, ': cannot close (', $!, '), abort';
- # Return if content is up-to-date
- if ($in_lines eq $out) {
- print 'No change in ', lc ($self->type), ' cfg: ', $file, "\n"
- if $self->verbose > 1 and $self->type;
- return 1;
- }
- }
- # If config file already exists, make sure it is writable
- if (-w $file) {
- if ($self->type) {
- # Existing config file writable, rename it using its time stamp
- my $mtime = (stat $file)[9];
- my ($sec, $min, $hour, $mday, $mon, $year) = (gmtime $mtime)[0 .. 5];
- my $timestamp = sprintf '%4d%2.2d%2.2d_%2.2d%2.2d%2.2d_',
- $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
- my $oldfile = File::Spec->catfile (
- $dirname, $timestamp . basename ($file)
- );
- rename $file, $oldfile;
- print 'Rename existing ', lc ($self->type), ' cfg: ',
- $oldfile, "\n" if $self->verbose > 1;
- }
- } else {
- # Existing config file not writable, throw an error
- croak $file, ': config file not writable, abort';
- }
- }
- # Open file and select file handle
- open OUT, '>', $file
- or croak $file, ': cannot open config file (', $!, '), abort';
- select OUT;
- }
- # Print output
- print $out;
- # Close file if necessary
- if ($file) {
- select $old_select;
- close OUT or croak $file, ': cannot close config file (', $!, '), abort';
- if ($self->type and $self->verbose > 1) {
- print 'Generated ', lc ($self->type), ' cfg: ', $file, "\n";
- } elsif ($self->verbose > 2) {
- print 'Generated cfg: ', $file, "\n";
- }
- }
- } else {
- # Warn if nothing to print
- my $warning = 'Empty configuration';
- $warning .= ' - nothing written to file: ' . $file if $file;
- carp $warning if $self->type;
- }
- return 1;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # @lines = $self->_get_cfg_lines ();
- #
- # DESCRIPTION
- # This internal method reads from a configuration file residing in a
- # Subversion repository or in the normal file system.
- # ------------------------------------------------------------------------------
- sub _get_cfg_lines {
- my $self = shift;
- my @lines = ();
- my $verbose = $self->verbose;
- my ($src) = $self->src();
- if ($src =~ qr{\A([A-Za-z][\w\+-\.]*):}xms) { # is a URI
- $src = Fcm::Keyword::expand($src);
- # Config file resides in a SVN repository
- # --------------------------------------------------------------------------
- # Set URL source and version
- my $rev = 'HEAD';
- # Extract version from source if it exists
- if ($src =~ s{\@ ([^\@]+) \z}{}xms) {
- $rev = $1;
- }
- $src = Fcm::Util::tidy_url($src);
- # Check whether URL is a config file
- my $rc;
- my @cmd = (qw/svn cat/, $src . '@' . $rev);
- @lines = &run_command (
- \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
- );
- # Error in "svn cat" command
- if ($rc) {
- # See whether specified config file is a known type
- my %cfgname = %{ $self->setting ('CFG_NAME') };
- my $key = uc $self->type;
- my $file = exists $cfgname{$key} ? $cfgname{$key} : '';
- # If config file is a known type, specified URL may be a directory
- if ($file) {
- # Check whether a config file with a default name exists in the URL
- my $path = $src . '/' . $file;
- my @cmd = (qw/svn cat/, $path . '@' . $rev);
- @lines = &run_command (
- \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
- );
- # Check whether a config file with a default name exists under the "cfg"
- # sub-directory of the URL
- if ($rc) {
- my $cfgdir = $self->setting (qw/DIR CFG/);
- $path = $src . '/' . $cfgdir . '/' . $file;
- my @cmd = (qw/svn cat/, $path . '@' . $rev);
- @lines = &run_command (
- \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
- );
- }
- $src = $path unless $rc;
- }
- }
- if ($rc) {
- # Error in "svn cat"
- croak 'Unable to locate config file from "', $self->src, '", abort';
- } else {
- # Print diagnostic, if necessary
- if ($verbose and $self->type and $self->type =~ /$expand_type/) {
- print 'Config file (', $self->type, '): ', $src;
- print '@', $rev if $rev;
- print "\n";
- }
- }
- # Record the actual source location
- $self->pegrev ($rev);
- $self->actual_src ($src);
- } else {
- # Config file resides in the normal file system
- # --------------------------------------------------------------------------
- my $src = $self->src;
- if (-d $src) { # Source is a directory
- croak 'Config file "', $src, '" is a directory, abort' if not $self->type;
- # Get name of the config file by looking at the type
- my %cfgname = %{ $self->setting ('CFG_NAME') };
- my $key = uc $self->type;
- my $file = exists $cfgname{$key} ? $cfgname{$key} : '';
- if ($file) {
- my $cfgdir = $self->setting (qw/DIR CFG/);
- # Check whether a config file with a default name exists in the
- # specified path, then check whether a config file with a default name
- # exists under the "cfg" sub-directory of the specified path
- if (-f File::Spec->catfile ($self->src, $file)) {
- $src = File::Spec->catfile ($self->src, $file);
- } elsif (-f File::Spec->catfile ($self->src, $cfgdir, $file)) {
- $src = File::Spec->catfile ($self->src, $cfgdir, $file);
- } else {
- croak 'Unable to locate config file from "', $self->src, '", abort';
- }
- } else {
- croak 'Unknown config file type "', $self->type, '", abort';
- }
- }
- if (-r $src) {
- open FILE, '<', $src;
- print 'Config file (', $self->type, '): ', $src, "\n"
- if $verbose and $self->type and $self->type =~ /$expand_type/;
- @lines = readline 'FILE';
- close FILE;
- } else {
- croak 'Unable to read config file "', $src, '", abort';
- }
- # Record the actual source location
- $self->actual_src ($src);
- }
- return @lines;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $string = $self->_expand_variable ($string, $env[, \%recursive]);
- #
- # DESCRIPTION
- # This internal method expands variables in $string. If $env is true, it
- # expands environment variables. Otherwise, it expands local variables. If
- # %recursive is set, it indicates that this method is being called
- # recursively. In which case, it must not attempt to expand a variable that
- # exists in the keys of %recursive.
- # ------------------------------------------------------------------------------
- sub _expand_variable {
- my ($self, $string, $env, $recursive) = @_;
- # Pattern for environment/local variable
- my @patterns = $env
- ? (qr#\$([A-Z][A-Z0-9_]+)#, qr#\$\{([A-Z][A-Z0-9_]+)\}#)
- : (qr#%(\w+(?:::[\w\.-]+)*)#, qr#%\{(\w+(?:(?:::|/)[\w\.-]+)*)\}#);
- my $ret = '';
- my $warning = undef;
- while ($string) {
- # Find the first match in $string
- my ($prematch, $match, $postmatch, $var_label);
- for my $pattern (@patterns) {
- next unless $string =~ /$pattern/;
- if ((not defined $prematch) or length ($`) < length ($prematch)) {
- $prematch = $`;
- $match = $&;
- $var_label = $1;
- $postmatch = $';
- }
- }
- if ($match) {
- $ret .= $prematch;
- $string = $postmatch;
- # Get variable value from environment or local configuration
- my $variable = $env
- ? (exists $ENV{$var_label} ? $ENV{$var_label} : undef)
- : $self->config->variable ($var_label);
- if ($env and $var_label eq 'HERE' and not defined $variable) {
- $variable = dirname ($self->actual_src);
- $variable = File::Spec->rel2abs ($variable) if not &is_url ($variable);
- }
- # Substitute match with value of variable
- if (defined $variable) {
- my $cyclic = 0;
- if ($recursive) {
- if (exists $recursive->{$var_label}) {
- $cyclic = 1;
- } else {
- $recursive->{$var_label} = 1;
- }
- } else {
- $recursive = {$var_label => 1};
- }
- if ($cyclic) {
- $warning .= ', ' if $warning;
- $warning .= $match . ': cyclic dependency, variable not expanded';
- $ret .= $variable;
- } else {
- my ($r, $w) = $self->_expand_variable ($variable, $env, $recursive);
- $ret .= $r;
- if ($w) {
- $warning .= ', ' if $warning;
- $warning .= $w;
- }
- }
- } else {
- $warning .= ', ' if $warning;
- $warning .= $match . ': variable not expanded';
- $ret .= $match;
- }
- } else {
- $ret .= $string;
- $string = "";
- }
- }
- return ($ret, $warning);
- }
- 1;
- __END__
|