CfgFile.pm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::CfgFile
  4. #
  5. # DESCRIPTION
  6. # This class is used for reading and writing FCM config files. A FCM config
  7. # file is a line-based text file that provides information on how to perform
  8. # a particular task using the FCM system.
  9. #
  10. # COPYRIGHT
  11. # (C) Crown copyright Met Office. All rights reserved.
  12. # For further details please refer to the file COPYRIGHT.txt
  13. # which you should have received as part of this distribution.
  14. # ------------------------------------------------------------------------------
  15. package Fcm::CfgFile;
  16. @ISA = qw(Fcm::Base);
  17. # Standard pragma
  18. use warnings;
  19. use strict;
  20. # Standard modules
  21. use Carp;
  22. use File::Basename;
  23. use File::Path;
  24. use File::Spec;
  25. # FCM component modules
  26. use Fcm::Base;
  27. use Fcm::CfgLine;
  28. use Fcm::Config;
  29. use Fcm::Keyword;
  30. use Fcm::Util;
  31. # List of property methods for this class
  32. my @scalar_properties = (
  33. 'actual_src', # actual source of configuration file
  34. 'lines', # list of lines, Fcm::CfgLine objects
  35. 'pegrev', # peg revision of configuration file
  36. 'src', # source of configuration file
  37. 'type', # type of configuration file
  38. 'version', # version of configuration file
  39. );
  40. # Local module variables
  41. my $expand_type = 'bld|ext'; # config file type that needs variable expansions
  42. # ------------------------------------------------------------------------------
  43. # SYNOPSIS
  44. # $obj = Fcm::CfgFile->new (%args);
  45. #
  46. # DESCRIPTION
  47. # This method constructs a new instance of the Fcm::CfgFile class. See above
  48. # for allowed list of properties. (KEYS should be in uppercase.)
  49. # ------------------------------------------------------------------------------
  50. sub new {
  51. my $this = shift;
  52. my %args = @_;
  53. my $class = ref $this || $this;
  54. my $self = Fcm::Base->new (%args);
  55. bless $self, $class;
  56. for (@scalar_properties) {
  57. $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
  58. }
  59. return $self;
  60. }
  61. # ------------------------------------------------------------------------------
  62. # SYNOPSIS
  63. # $value = $obj->X;
  64. # $obj->X ($value);
  65. #
  66. # DESCRIPTION
  67. # Details of these properties are explained in @scalar_properties.
  68. # ------------------------------------------------------------------------------
  69. for my $name (@scalar_properties) {
  70. no strict 'refs';
  71. *$name = sub {
  72. my $self = shift;
  73. if (@_) {
  74. $self->{$name} = $_[0];
  75. }
  76. if (not defined $self->{$name}) {
  77. if ($name eq 'lines') {
  78. $self->{$name} = [];
  79. }
  80. }
  81. return $self->{$name};
  82. }
  83. }
  84. # ------------------------------------------------------------------------------
  85. # SYNOPSIS
  86. # $mtime = $obj->mtime ();
  87. #
  88. # DESCRIPTION
  89. # This method returns the modified time of the configuration file source.
  90. # ------------------------------------------------------------------------------
  91. sub mtime {
  92. my $self = shift;
  93. my $mtime = undef;
  94. if (-f $self->src) {
  95. $mtime = (stat $self->src)[9];
  96. }
  97. return $mtime;
  98. }
  99. # ------------------------------------------------------------------------------
  100. # SYNOPSIS
  101. # $read = $obj->read_cfg ();
  102. #
  103. # DESCRIPTION
  104. # This method reads the current configuration file. It returns the number of
  105. # lines read from the config file, or "undef" if it fails. The result is
  106. # placed in the LINES array of the current instance, and can be accessed via
  107. # the "lines" method.
  108. # ------------------------------------------------------------------------------
  109. sub read_cfg {
  110. my $self = shift;
  111. my @lines = $self->_get_cfg_lines;
  112. # List of CFG types that need INC declarations expansion
  113. my %exp_inc = ();
  114. for (split (/$Fcm::Config::DELIMITER_LIST/, $self->setting ('CFG_EXP_INC'))) {
  115. $exp_inc{uc ($_)} = 1;
  116. }
  117. # List of CFG labels that are reserved keywords
  118. my %cfg_keywords = ();
  119. for (split (/$Fcm::Config::DELIMITER_LIST/, $self->setting ('CFG_KEYWORD'))) {
  120. $cfg_keywords{$self->cfglabel ($_)} = 1;
  121. }
  122. # Loop each line, to separate lines into label : value pairs
  123. my $cont = undef;
  124. my $here = undef;
  125. for my $line_num (1 .. @lines) {
  126. my $line = $lines[$line_num - 1];
  127. chomp $line;
  128. my $label = '';
  129. my $value = '';
  130. my $comment = '';
  131. # If this line is a continuation, set $start to point to the line that
  132. # starts this continuation. Otherwise, set $start to undef
  133. my $start = defined ($cont) ? $self->lines->[$cont] : undef;
  134. my $warning = undef;
  135. if ($line =~ /^(\s*#.*)$/) { # comment line
  136. $comment = $1;
  137. } elsif ($line =~ /\S/) { # non-blank line
  138. if (defined $cont) {
  139. # Previous line has a continuation mark
  140. $value = $line;
  141. # Separate value and comment
  142. if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
  143. $comment = $1;
  144. }
  145. # Remove leading spaces
  146. $value =~ s/^\s*\\?//;
  147. # Expand environment variables
  148. my $warn;
  149. ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
  150. $warning .= ($warning ? ', ' : '') . $warn if $warn;
  151. # Expand internal variables
  152. ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
  153. $warning .= ($warning ? ', ' : '') . $warn if $warn;
  154. # Get "line" that begins the current continuation
  155. my $v = $start->value . $value;
  156. $v =~ s/\\$//;
  157. $start->value ($v);
  158. } else {
  159. # Previous line does not have a continuation mark
  160. if ($line =~ /^\s*(\S+)(?:\s+(.*))?$/) {
  161. # Check line contains a valid label:value pair
  162. $label = $1;
  163. $value = defined ($2) ? $2 : '';
  164. # Separate value and comment
  165. if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
  166. $comment = $1;
  167. }
  168. # Remove trailing spaces
  169. $value =~ s/\s+$//;
  170. # Value begins with $HERE?
  171. $here = ($value =~ /\$\{?HERE\}?(?:[^A-Z_]|$)/);
  172. # Expand environment variables
  173. my $warn;
  174. ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
  175. $warning .= ($warning ? ', ' : '') . $warn if $warn;
  176. # Expand internal variables
  177. ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
  178. $warning .= ($warning ? ', ' : '') . $warn if $warn;
  179. }
  180. }
  181. # Determine whether current line ends with a continuation mark
  182. if ($value =~ s/\\$//) {
  183. $cont = scalar (@{ $self->lines }) unless defined $cont;
  184. } else {
  185. $cont = undef;
  186. }
  187. }
  188. if (exists $exp_inc{uc ($self->type)} and
  189. uc ($start ? $start->label : $label) eq $self->cfglabel ('INC') and
  190. not defined $cont) {
  191. # Current configuration file requires expansion of INC declarations
  192. # The start/current line is an INC declaration
  193. # The current line is not a continuation or is the end of the continuation
  194. # Get lines from an "include" configuration file
  195. my $src = ($start ? $start->value : $value);
  196. $src .= '@' . $self->pegrev if $here and $self->pegrev;
  197. if ($src) {
  198. # Invoke a new instance to read the source
  199. my $cfg = Fcm::CfgFile->new (
  200. SRC => expand_tilde ($src), TYPE => $self->type,
  201. );
  202. $cfg->read_cfg;
  203. # Add lines to the lines array in the current configuration file
  204. $comment = 'INC ' . $src . ' ';
  205. push @{$self->lines}, Fcm::CfgLine->new (
  206. comment => $comment . '# Start',
  207. number => ($start ? $start->number : $line_num),
  208. src => $self->actual_src,
  209. warning => $warning,
  210. );
  211. push @{ $self->lines }, @{ $cfg->lines };
  212. push @{$self->lines}, Fcm::CfgLine->new (
  213. comment => $comment . '# End',
  214. src => $self->actual_src,
  215. );
  216. } else {
  217. push @{$self->lines}, Fcm::CfgLine->new (
  218. number => $line_num,
  219. src => $self->actual_src,
  220. warning => 'empty INC declaration.'
  221. );
  222. }
  223. } else {
  224. # Push label:value pair into lines array
  225. push @{$self->lines}, Fcm::CfgLine->new (
  226. label => $label,
  227. value => ($label ? $value : ''),
  228. comment => $comment,
  229. number => $line_num,
  230. src => $self->actual_src,
  231. warning => $warning,
  232. );
  233. }
  234. next if defined $cont; # current line not a continuation
  235. my $slabel = ($start ? $start->label : $label);
  236. my $svalue = ($start ? $start->value : $value);
  237. next unless $slabel;
  238. # Check config file type and version
  239. if (index (uc ($slabel), $self->cfglabel ('CFGFILE')) == 0) {
  240. my @words = split /$Fcm::Config::DELIMITER_PATTERN/, $slabel;
  241. shift @words;
  242. my $name = @words ? lc ($words[0]) : 'type';
  243. if ($self->can ($name)) {
  244. $self->$name ($svalue);
  245. }
  246. }
  247. # Set internal variable
  248. $slabel =~ s/^\%//; # Remove leading "%" from label
  249. $self->config->variable ($slabel, $svalue)
  250. unless exists $cfg_keywords{$slabel};
  251. }
  252. # Report and reset warnings
  253. # ----------------------------------------------------------------------------
  254. for my $line (@{ $self->lines }) {
  255. w_report $line->format_warning if $line->warning;
  256. $line->warning (undef);
  257. }
  258. return @{ $self->lines };
  259. }
  260. # ------------------------------------------------------------------------------
  261. # SYNOPSIS
  262. # $rc = $obj->print_cfg ($file, [$force]);
  263. #
  264. # DESCRIPTION
  265. # This method prints the content of current configuration file. If no
  266. # argument is specified, it prints output to the standard output. If $file is
  267. # specified, and is a writable file name, the output is sent to the file. If
  268. # the file already exists, its content is compared to the current output.
  269. # Nothing will be written if the content is unchanged unless $force is
  270. # specified. Otherwise, for typed configuration files, the existing file is
  271. # renamed using a prefix that contains its last modified time. The method
  272. # returns 1 if there is no error.
  273. # ------------------------------------------------------------------------------
  274. sub print_cfg {
  275. my ($self, $file, $force) = @_;
  276. # Count maximum number of characters in the labels, (for pretty printing)
  277. my $max_label_len = 0;
  278. for my $line (@{ $self->lines }) {
  279. next unless $line->label;
  280. my $label_len = length $line->label;
  281. $max_label_len = $label_len if $label_len > $max_label_len;
  282. }
  283. # Output string
  284. my $out = '';
  285. # Append each line of the config file to the output string
  286. for my $line (@{ $self->lines }) {
  287. $out .= $line->print_line ($max_label_len - length ($line->label) + 1);
  288. $out .= "\n";
  289. }
  290. if ($out) {
  291. my $old_select = select;
  292. # Open file if necessary
  293. if ($file) {
  294. # Make sure the host directory exists and is writable
  295. my $dirname = dirname $file;
  296. if (not -d $dirname) {
  297. print 'Make directory: ', $dirname, "\n" if $self->verbose;
  298. mkpath $dirname;
  299. }
  300. croak $dirname, ': cannot write to config file directory, abort'
  301. unless -d $dirname and -w $dirname;
  302. if (-f $file and not $force) {
  303. if (-r $file) {
  304. # Read old config file to see if content has changed
  305. open IN, '<', $file or croak $file, ': cannot open (', $!, '), abort';
  306. my $in_lines = '';
  307. while (my $line = <IN>) {
  308. $in_lines .= $line;
  309. }
  310. close IN or croak $file, ': cannot close (', $!, '), abort';
  311. # Return if content is up-to-date
  312. if ($in_lines eq $out) {
  313. print 'No change in ', lc ($self->type), ' cfg: ', $file, "\n"
  314. if $self->verbose > 1 and $self->type;
  315. return 1;
  316. }
  317. }
  318. # If config file already exists, make sure it is writable
  319. if (-w $file) {
  320. if ($self->type) {
  321. # Existing config file writable, rename it using its time stamp
  322. my $mtime = (stat $file)[9];
  323. my ($sec, $min, $hour, $mday, $mon, $year) = (gmtime $mtime)[0 .. 5];
  324. my $timestamp = sprintf '%4d%2.2d%2.2d_%2.2d%2.2d%2.2d_',
  325. $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
  326. my $oldfile = File::Spec->catfile (
  327. $dirname, $timestamp . basename ($file)
  328. );
  329. rename $file, $oldfile;
  330. print 'Rename existing ', lc ($self->type), ' cfg: ',
  331. $oldfile, "\n" if $self->verbose > 1;
  332. }
  333. } else {
  334. # Existing config file not writable, throw an error
  335. croak $file, ': config file not writable, abort';
  336. }
  337. }
  338. # Open file and select file handle
  339. open OUT, '>', $file
  340. or croak $file, ': cannot open config file (', $!, '), abort';
  341. select OUT;
  342. }
  343. # Print output
  344. print $out;
  345. # Close file if necessary
  346. if ($file) {
  347. select $old_select;
  348. close OUT or croak $file, ': cannot close config file (', $!, '), abort';
  349. if ($self->type and $self->verbose > 1) {
  350. print 'Generated ', lc ($self->type), ' cfg: ', $file, "\n";
  351. } elsif ($self->verbose > 2) {
  352. print 'Generated cfg: ', $file, "\n";
  353. }
  354. }
  355. } else {
  356. # Warn if nothing to print
  357. my $warning = 'Empty configuration';
  358. $warning .= ' - nothing written to file: ' . $file if $file;
  359. carp $warning if $self->type;
  360. }
  361. return 1;
  362. }
  363. # ------------------------------------------------------------------------------
  364. # SYNOPSIS
  365. # @lines = $self->_get_cfg_lines ();
  366. #
  367. # DESCRIPTION
  368. # This internal method reads from a configuration file residing in a
  369. # Subversion repository or in the normal file system.
  370. # ------------------------------------------------------------------------------
  371. sub _get_cfg_lines {
  372. my $self = shift;
  373. my @lines = ();
  374. my $verbose = $self->verbose;
  375. my ($src) = $self->src();
  376. if ($src =~ qr{\A([A-Za-z][\w\+-\.]*):}xms) { # is a URI
  377. $src = Fcm::Keyword::expand($src);
  378. # Config file resides in a SVN repository
  379. # --------------------------------------------------------------------------
  380. # Set URL source and version
  381. my $rev = 'HEAD';
  382. # Extract version from source if it exists
  383. if ($src =~ s{\@ ([^\@]+) \z}{}xms) {
  384. $rev = $1;
  385. }
  386. $src = Fcm::Util::tidy_url($src);
  387. # Check whether URL is a config file
  388. my $rc;
  389. my @cmd = (qw/svn cat/, $src . '@' . $rev);
  390. @lines = &run_command (
  391. \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
  392. );
  393. # Error in "svn cat" command
  394. if ($rc) {
  395. # See whether specified config file is a known type
  396. my %cfgname = %{ $self->setting ('CFG_NAME') };
  397. my $key = uc $self->type;
  398. my $file = exists $cfgname{$key} ? $cfgname{$key} : '';
  399. # If config file is a known type, specified URL may be a directory
  400. if ($file) {
  401. # Check whether a config file with a default name exists in the URL
  402. my $path = $src . '/' . $file;
  403. my @cmd = (qw/svn cat/, $path . '@' . $rev);
  404. @lines = &run_command (
  405. \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
  406. );
  407. # Check whether a config file with a default name exists under the "cfg"
  408. # sub-directory of the URL
  409. if ($rc) {
  410. my $cfgdir = $self->setting (qw/DIR CFG/);
  411. $path = $src . '/' . $cfgdir . '/' . $file;
  412. my @cmd = (qw/svn cat/, $path . '@' . $rev);
  413. @lines = &run_command (
  414. \@cmd, METHOD => 'qx', DEVNULL => 1, RC => \$rc, ERROR => 'ignore',
  415. );
  416. }
  417. $src = $path unless $rc;
  418. }
  419. }
  420. if ($rc) {
  421. # Error in "svn cat"
  422. croak 'Unable to locate config file from "', $self->src, '", abort';
  423. } else {
  424. # Print diagnostic, if necessary
  425. if ($verbose and $self->type and $self->type =~ /$expand_type/) {
  426. print 'Config file (', $self->type, '): ', $src;
  427. print '@', $rev if $rev;
  428. print "\n";
  429. }
  430. }
  431. # Record the actual source location
  432. $self->pegrev ($rev);
  433. $self->actual_src ($src);
  434. } else {
  435. # Config file resides in the normal file system
  436. # --------------------------------------------------------------------------
  437. my $src = $self->src;
  438. if (-d $src) { # Source is a directory
  439. croak 'Config file "', $src, '" is a directory, abort' if not $self->type;
  440. # Get name of the config file by looking at the type
  441. my %cfgname = %{ $self->setting ('CFG_NAME') };
  442. my $key = uc $self->type;
  443. my $file = exists $cfgname{$key} ? $cfgname{$key} : '';
  444. if ($file) {
  445. my $cfgdir = $self->setting (qw/DIR CFG/);
  446. # Check whether a config file with a default name exists in the
  447. # specified path, then check whether a config file with a default name
  448. # exists under the "cfg" sub-directory of the specified path
  449. if (-f File::Spec->catfile ($self->src, $file)) {
  450. $src = File::Spec->catfile ($self->src, $file);
  451. } elsif (-f File::Spec->catfile ($self->src, $cfgdir, $file)) {
  452. $src = File::Spec->catfile ($self->src, $cfgdir, $file);
  453. } else {
  454. croak 'Unable to locate config file from "', $self->src, '", abort';
  455. }
  456. } else {
  457. croak 'Unknown config file type "', $self->type, '", abort';
  458. }
  459. }
  460. if (-r $src) {
  461. open FILE, '<', $src;
  462. print 'Config file (', $self->type, '): ', $src, "\n"
  463. if $verbose and $self->type and $self->type =~ /$expand_type/;
  464. @lines = readline 'FILE';
  465. close FILE;
  466. } else {
  467. croak 'Unable to read config file "', $src, '", abort';
  468. }
  469. # Record the actual source location
  470. $self->actual_src ($src);
  471. }
  472. return @lines;
  473. }
  474. # ------------------------------------------------------------------------------
  475. # SYNOPSIS
  476. # $string = $self->_expand_variable ($string, $env[, \%recursive]);
  477. #
  478. # DESCRIPTION
  479. # This internal method expands variables in $string. If $env is true, it
  480. # expands environment variables. Otherwise, it expands local variables. If
  481. # %recursive is set, it indicates that this method is being called
  482. # recursively. In which case, it must not attempt to expand a variable that
  483. # exists in the keys of %recursive.
  484. # ------------------------------------------------------------------------------
  485. sub _expand_variable {
  486. my ($self, $string, $env, $recursive) = @_;
  487. # Pattern for environment/local variable
  488. my @patterns = $env
  489. ? (qr#\$([A-Z][A-Z0-9_]+)#, qr#\$\{([A-Z][A-Z0-9_]+)\}#)
  490. : (qr#%(\w+(?:::[\w\.-]+)*)#, qr#%\{(\w+(?:(?:::|/)[\w\.-]+)*)\}#);
  491. my $ret = '';
  492. my $warning = undef;
  493. while ($string) {
  494. # Find the first match in $string
  495. my ($prematch, $match, $postmatch, $var_label);
  496. for my $pattern (@patterns) {
  497. next unless $string =~ /$pattern/;
  498. if ((not defined $prematch) or length ($`) < length ($prematch)) {
  499. $prematch = $`;
  500. $match = $&;
  501. $var_label = $1;
  502. $postmatch = $';
  503. }
  504. }
  505. if ($match) {
  506. $ret .= $prematch;
  507. $string = $postmatch;
  508. # Get variable value from environment or local configuration
  509. my $variable = $env
  510. ? (exists $ENV{$var_label} ? $ENV{$var_label} : undef)
  511. : $self->config->variable ($var_label);
  512. if ($env and $var_label eq 'HERE' and not defined $variable) {
  513. $variable = dirname ($self->actual_src);
  514. $variable = File::Spec->rel2abs ($variable) if not &is_url ($variable);
  515. }
  516. # Substitute match with value of variable
  517. if (defined $variable) {
  518. my $cyclic = 0;
  519. if ($recursive) {
  520. if (exists $recursive->{$var_label}) {
  521. $cyclic = 1;
  522. } else {
  523. $recursive->{$var_label} = 1;
  524. }
  525. } else {
  526. $recursive = {$var_label => 1};
  527. }
  528. if ($cyclic) {
  529. $warning .= ', ' if $warning;
  530. $warning .= $match . ': cyclic dependency, variable not expanded';
  531. $ret .= $variable;
  532. } else {
  533. my ($r, $w) = $self->_expand_variable ($variable, $env, $recursive);
  534. $ret .= $r;
  535. if ($w) {
  536. $warning .= ', ' if $warning;
  537. $warning .= $w;
  538. }
  539. }
  540. } else {
  541. $warning .= ', ' if $warning;
  542. $warning .= $match . ': variable not expanded';
  543. $ret .= $match;
  544. }
  545. } else {
  546. $ret .= $string;
  547. $string = "";
  548. }
  549. }
  550. return ($ret, $warning);
  551. }
  552. 1;
  553. __END__