1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22 23 24#************************************************************************* 25# 26# SourceConfig - Perl extension for parsing general info databases 27# 28# usage: see below 29# 30#************************************************************************* 31 32package SourceConfig; 33 34use strict; 35 36use constant SOURCE_CONFIG_FILE_NAME => 'source_config'; 37use constant SOURCE_CONFIG_VERSION => 3; 38 39use Carp; 40use Cwd; 41use RepositoryHelper; 42use File::Basename; 43use File::Temp qw(tmpnam); 44 45my $debug = 0; 46 47##### profiling ##### 48 49##### ctor ##### 50 51sub new { 52 my $proto = shift; 53 my $class = ref($proto) || $proto; 54 my $source_root = shift; 55 my @additional_repositories = @_; 56 57 my $self = {}; 58 $self->{USER_SOURCE_ROOT} = undef; 59 $self->{SOURCE_CONFIG_FILE} = undef; 60 if (defined $source_root) { 61 $source_root = Cwd::realpath($source_root); 62 $source_root =~ s/\\|\/$//; 63 if (-f $source_root) { 64 # We have path to source_config 65 if (File::Basename::basename($source_root) eq 'source_config') { 66 # We have path to source_config 67 $self->{SOURCE_CONFIG_FILE} = $source_root; 68 $source_root = File::Basename::dirname($source_root); 69 } else { 70 croak("$source_root is not a source_config file"); 71 }; 72 } else { 73 $self->{USER_SOURCE_ROOT} = $source_root; 74 $source_root .= '/..'; 75 } 76 } else { 77 $source_root = $ENV{SOURCE_ROOT_DIR}; 78 }; 79 $source_root = Cwd::realpath($source_root); 80 $self->{SOURCE_ROOT} = $source_root; 81 $self->{DEBUG} = 0; 82 $self->{VERBOSE} = 0; 83 $self->{REPOSITORIES} = {}; 84 $self->{ACTIVATED_REPOSITORIES} = {}; 85 $self->{MODULE_PATHS} = {}; 86 $self->{MODULE_BUILD_LIST_PATHS} = {}; 87 $self->{ACTIVATED_MODULES} = {}; 88 $self->{MODULE_REPOSITORY} = {}; 89 $self->{REAL_MODULES} = {}; 90 $self->{NEW_MODULES} = []; 91 $self->{REMOVE_MODULES} = {}; 92 $self->{REMOVE_REPOSITORIES} = {}; 93 $self->{NEW_REPOSITORIES} = []; 94 $self->{WARNINGS} = []; 95 $self->{REPORT_MESSAGES} = []; 96 $self->{CONFIG_FILE_CONTENT} = []; 97 if (defined $self->{USER_SOURCE_ROOT}) { 98 ${$self->{REPOSITORIES}}{File::Basename::basename($self->{USER_SOURCE_ROOT})} = $self->{USER_SOURCE_ROOT}; 99 }; 100 $self->{SOURCE_CONFIG_FILE} = get_config_file($self->{SOURCE_ROOT}) if (!defined $self->{SOURCE_CONFIG_FILE}); 101 $self->{SOURCE_CONFIG_DEFAULT} = $self->{SOURCE_ROOT} .'/'.SOURCE_CONFIG_FILE_NAME; 102 if (defined $self->{USER_SOURCE_ROOT}) { 103 ${$self->{REPOSITORIES}}{File::Basename::basename($self->{USER_SOURCE_ROOT})} = $self->{USER_SOURCE_ROOT}; 104 }; 105 foreach my $additional_repository (@additional_repositories) 106 { 107 ${$self->{REPOSITORIES}}{File::Basename::basename($additional_repository)} = $additional_repository; 108 } 109 110 read_config_file($self); 111 get_module_paths($self); 112 bless($self, $class); 113 return $self; 114} 115 116##### methods ##### 117 118sub get_version { 119 return SOURCE_CONFIG_VERSION; 120}; 121 122sub get_repositories 123{ 124 my $self = shift; 125 return sort keys %{$self->{REPOSITORIES}}; 126} 127 128sub add_repository 129{ 130 my $self = shift; 131 my $new_rep_path = shift; 132 $new_rep_path = Cwd::realpath($new_rep_path); 133 my $new_rep_name = File::Basename::basename($new_rep_path); 134 if (defined ${$self->{REPOSITORIES}}{$new_rep_name}) { 135 croak("Repository $new_rep_name is already defined!!"); 136 }; 137 ${$self->{REPOSITORIES}}{$new_rep_name} = $new_rep_path; 138 $self -> get_repository_module_paths($new_rep_name); 139} 140 141sub get_config_file_default_path { 142 my $self = shift; 143 return $self->{SOURCE_CONFIG_DEFAULT}; 144} 145 146sub get_config_file_path { 147 my $self = shift; 148 return $self->{SOURCE_CONFIG_FILE}; 149} 150 151sub get_module_repository { 152 my $self = shift; 153 my $module = shift; 154 if (defined ${$self->{MODULE_REPOSITORY}}{$module}) { 155 return ${$self->{MODULE_REPOSITORY}}{$module}; 156 } else { 157 Carp::cluck("No such module $module in active repositories!!\n"); 158 return undef; 159 }; 160} 161 162sub get_module_path { 163 my $self = shift; 164 my $module = shift; 165 if (defined ${$self->{MODULE_PATHS}}{$module}) { 166 return ${$self->{MODULE_PATHS}}{$module}; 167 } else { 168 Carp::cluck("No path for module $module in active repositories!!\n") if ($debug); 169 return undef; 170 }; 171} 172 173sub get_module_build_list { 174 my $self = shift; 175 my $module = shift; 176 if (defined ${$self->{MODULE_BUILD_LIST_PATHS}}{$module}) { 177 return ${$self->{MODULE_BUILD_LIST_PATHS}}{$module}; 178 } else { 179 my @possible_build_lists = ('build.lst', 'build.xlist'); # build lists names 180 foreach (@possible_build_lists) { 181 my $possible_path = ${$self->{MODULE_PATHS}}{$module} . "/prj/$_"; 182 if (-e $possible_path) { 183 ${$self->{MODULE_BUILD_LIST_PATHS}}{$module} = $possible_path; 184 return $possible_path; 185 }; 186 }; 187 Carp::cluck("No build list in module $module found!!\n") if ($self->{DEBUG}); 188 return undef; 189 }; 190} 191 192sub get_all_modules 193{ 194 my $self = shift; 195 my $module = shift; 196 return sort keys %{$self->{MODULE_PATHS}}; 197}; 198 199sub get_active_modules 200{ 201 my $self = shift; 202 if (scalar keys %{$self->{ACTIVATED_MODULES}}) { 203 return sort keys %{$self->{ACTIVATED_MODULES}}; 204 } 205 return sort keys %{$self->{REAL_MODULES}}; 206} 207 208sub is_active 209{ 210 my $self = shift; 211 my $module = shift; 212 if (scalar keys %{$self->{ACTIVATED_MODULES}}) { 213 return exists ($self->{ACTIVATED_MODULES}{$module}); 214 } 215 return exists ($self->{REAL_MODULES}{$module}); 216} 217 218##### private methods ##### 219 220sub get_repository_module_paths { 221 my $self = shift; 222 my $repository = shift; 223 my $repository_path = ${$self->{REPOSITORIES}}{$repository}; 224 if (opendir DIRHANDLE, $repository_path) { 225 foreach my $module (readdir(DIRHANDLE)) { 226 next if (($module =~ /^\.+/) || (!-d "$repository_path/$module")); 227 my $module_entry = $module; 228 if (($module !~ s/\.lnk$//) && ($module !~ s/\.link$//)) { 229 $self->{REAL_MODULES}{$module}++; 230 } 231 my $possible_path = "$repository_path/$module_entry"; 232 if (-d $possible_path) { 233 if (defined ${$self->{MODULE_PATHS}}{$module}) { 234 close DIRHANDLE; 235 croak("Ambiguous paths for module $module: $possible_path and " . ${$self->{MODULE_PATHS}}{$module}); 236 }; 237 ${$self->{MODULE_PATHS}}{$module} = $possible_path; 238 ${$self->{MODULE_REPOSITORY}}{$module} = $repository; 239 } 240 }; 241 close DIRHANDLE; 242 } else { 243 croak("Cannot read $repository_path repository content"); 244 }; 245}; 246 247sub get_module_paths { 248 my $self = shift; 249 foreach my $repository (keys %{$self->{REPOSITORIES}}) { 250 get_repository_module_paths($self, $repository); 251 }; 252 my @false_actives = (); 253 foreach (keys %{$self->{ACTIVATED_MODULES}}) { 254 push(@false_actives, $_) if (!defined ${$self->{MODULE_PATHS}}{$_}); 255 }; 256 croak("Error!! Activated module(s): @false_actives\nnot found in the active repositories!! Please check your " . $self->{SOURCE_CONFIG_FILE} . "\n") if (scalar @false_actives); 257 croak("No modules found!") if (!scalar keys %{$self->{MODULE_PATHS}}); 258}; 259 260sub get_config_file { 261 my $source_root = shift; 262 my $possible_path = $source_root . '/' . SOURCE_CONFIG_FILE_NAME; 263 return $possible_path if (-f $possible_path); 264 return ''; 265}; 266 267# 268# Fallback - fallback repository is based on RepositoryHelper educated guess 269# 270sub get_fallback_repository { 271 my $self = shift; 272 my $repository_root = RepositoryHelper->new()->get_repository_root(); 273 ${$self->{REPOSITORIES}}{File::Basename::basename($repository_root)} = $repository_root; 274}; 275 276sub read_config_file { 277 my $self = shift; 278 if (!$self->{SOURCE_CONFIG_FILE}) { 279 if (!defined $self->{USER_SOURCE_ROOT}) { 280 get_fallback_repository($self); 281 }; 282 return; 283 }; 284 my $repository_section = 0; 285 my $module_section = 0; 286 my $line = 0; 287 my @file_content = (); 288 289 if (open(SOURCE_CONFIG_FILE, $self->{SOURCE_CONFIG_FILE})) { 290 foreach (<SOURCE_CONFIG_FILE>) { 291 push (@{$self->{CONFIG_FILE_CONTENT}}, $_); 292 $line++; 293 chomp; 294 next if (!/^\S+/); 295 next if (/^\s*#+/); 296 s/\r\n//; 297 if (/^\[repositories\]\s*(\s+#)*/) { 298 $module_section = 0; 299 $repository_section = 1; 300 next; 301 }; 302 if (/^\[modules\]\s*(\s+#)*/) { 303 $module_section = 1; 304 $repository_section = 0; 305 next; 306 }; 307 next if (!$repository_section && !$module_section); 308 if (/\s*(\S+)=active\s*(\s+#)*/) { 309 if ($repository_section) { 310 my $repository_source_path = $self->{SOURCE_ROOT} . "/$1"; 311 if (defined $ENV{UPDMINOREXT}) { 312 $repository_source_path .= $ENV{UPDMINOREXT}; 313 if (defined ${$self->{REPOSITORIES}}{$1.$ENV{UPDMINOREXT}}) { 314 delete ${$self->{REPOSITORIES}}{$1.$ENV{UPDMINOREXT}}; 315 }; 316 }; 317 ${$self->{REPOSITORIES}}{$1} = $repository_source_path; 318 ${$self->{ACTIVATED_REPOSITORIES}}{$1}++; 319 next; 320 } 321 if ($module_section) { 322 ${$self->{ACTIVATED_MODULES}}{$1}++; 323 next; 324 }; 325 }; 326 croak("Line $line in " . $self->{SOURCE_CONFIG_FILE} . ' violates format. Please make your checks!'); 327 }; 328 close SOURCE_CONFIG_FILE; 329 if (!scalar keys %{$self->{REPOSITORIES}}) { 330 get_fallback_repository($self); 331 }; 332 } else { 333 croak('Cannot open ' . $self->{SOURCE_CONFIG_FILE} . ' for reading'); 334 }; 335}; 336 337sub remove_all_activated_repositories { 338 my $self = shift; 339 $self->remove_activated_repositories([keys %{$self->{ACTIVATED_REPOSITORIES}}]); 340}; 341 342sub remove_activated_repositories { 343 my $self = shift; 344 my $new_repositories_ref = shift; 345 push(@{$self->{WARNINGS}}, "\nWARNING: Empty repository list passed for removing from source_config\n") if (!scalar @$new_repositories_ref); 346 $self->{VERBOSE} = shift; 347 $self->{REMOVE_REPOSITORIES} = {}; 348 foreach (@$new_repositories_ref) { 349 if (!defined ${$self->{ACTIVATED_REPOSITORIES}}{$_}) { 350 push (@{$self->{WARNINGS}}, "\nWARNING: repository $_ is not activated in ". $self->get_config_file_default_path()."\n"); 351 } else { 352 ${$self->{REMOVE_REPOSITORIES}}{$_}++; 353 delete ${$self->{ACTIVATED_REPOSITORIES}}{$_}; 354 }; 355 }; 356 generate_config_file($self); 357}; 358 359sub remove_all_activated_modules { 360 my $self = shift; 361 $self->remove_activated_modules([keys %{$self->{ACTIVATED_MODULES}}]); 362}; 363 364sub remove_activated_modules { 365 my $self = shift; 366 my $new_modules_ref = shift; 367 push(@{$self->{WARNINGS}}, "\nWARNING: Empty module list passed for removing from source_config\n") if (!scalar @$new_modules_ref); 368 $self->{VERBOSE} = shift; 369 $self->{REMOVE_MODULES} = {}; 370 foreach (@$new_modules_ref) { 371 if (!defined ${$self->{ACTIVATED_MODULES}}{$_}) { 372 push (@{$self->{WARNINGS}}, "\nWARNING: module $_ is not activated in ". $self->get_config_file_default_path()."\n"); 373 } else { 374 ${$self->{REMOVE_MODULES}}{$_}++; 375 delete ${$self->{ACTIVATED_MODULES}}{$_}; 376 }; 377 }; 378 generate_config_file($self); 379}; 380 381sub add_active_repositories { 382 my $self = shift; 383 $self->{NEW_REPOSITORIES} = shift; 384 croak('Empty repository list passed for addition to source_config') if (!scalar @{$self->{NEW_REPOSITORIES}}); 385 $self->{VERBOSE} = shift; 386 foreach (@{$self->{NEW_REPOSITORIES}}) { 387 $self->add_repository($_); 388 }; 389 generate_config_file($self); 390}; 391 392sub add_active_modules { 393 my $self = shift; 394 my $module_list_ref = shift; 395 my $ignored_modules_string = ''; 396 my @real_modules = (); 397 foreach my $module (sort @$module_list_ref) { 398 if ($self->get_module_path($module)) { 399 push(@real_modules, $module); 400 } else { 401 $ignored_modules_string .= " $module"; 402 }; 403 }; 404 push (@{$self->{WARNINGS}}, "\nWARNING: following modules are not found in active repositories, and have not been added to the " . $self->get_config_file_default_path() . ":$ignored_modules_string\n") if ($ignored_modules_string); 405 $self->{NEW_MODULES} = \@real_modules; 406 croak('Empty module list passed for addition to source_config') if (!scalar @{$self->{NEW_MODULES}}); 407 $self->{VERBOSE} = shift; 408 generate_config_file($self); 409}; 410 411sub add_content { 412 my $self = shift; 413 my $content = shift; 414 my $entries_to_add = shift; 415 return if (!scalar @$entries_to_add); 416 my $message; 417 my $message_part1; 418 my $warning_message; 419 my $activated_entries; 420 421 if ($entries_to_add == $self->{NEW_MODULES}) { 422 $self->{NEW_MODULES} = []; 423 $message_part1 = "Module(s):\n"; 424 $activated_entries = $self->{ACTIVATED_MODULES}; 425 } elsif ($entries_to_add == $self->{NEW_REPOSITORIES}) { 426 $self->{NEW_REPOSITORIES} = []; 427 $message_part1 = "Repositories:\n"; 428 $activated_entries = $self->{ACTIVATED_REPOSITORIES}; 429 }; 430 foreach my $entry (@$entries_to_add) { 431 if (defined $$activated_entries{$entry}) { 432 $warning_message .= "$entry " 433 } else { 434 push(@$content, "$entry=active\n"); 435 ${$activated_entries}{$entry}++; 436 $message .= "$entry " 437 }; 438 }; 439 440 push(@{$self->{REPORT_MESSAGES}}, "\n$message_part1 $message\nhave been added to the ". $self->get_config_file_default_path()."\n") if ($message); 441 push (@{$self->{WARNINGS}}, "\nWARNING: $message_part1 $warning_message\nare already added to the ". $self->get_config_file_default_path()."\n") if ($warning_message); 442}; 443 444sub generate_config_file { 445 my $self = shift; 446 my @config_content_new = (); 447 my ($module_section, $repository_section); 448 my %removed_modules = (); 449 my %removed_repositories = (); 450 foreach (@{$self->{CONFIG_FILE_CONTENT}}) { 451 if (/^\[repositories\]\s*(\s+#)*/) { 452 if ($module_section) { 453 $self->add_content(\@config_content_new, $self->{NEW_MODULES}); 454 }; 455 $module_section = 0; 456 $repository_section = 1; 457 }; 458 if (/^\[modules\]\s*(\s+#)*/) { 459 if ($repository_section) { 460 $self->add_content(\@config_content_new, $self->{NEW_REPOSITORIES}); 461 }; 462 $module_section = 1; 463 $repository_section = 0; 464 }; 465 if ($module_section && /\s*(\S+)=active\s*(\s+#)*/) { 466 if (defined ${$self->{REMOVE_MODULES}}{$1}) { 467 $removed_modules{$1}++; 468 next; 469 }; 470 } 471 if ($repository_section && /\s*(\S+)=active\s*(\s+#)*/) { 472 if (defined ${$self->{REMOVE_REPOSITORIES}}{$1}) { 473 $removed_repositories{$1}++; 474 next; 475 }; 476 } 477 push(@config_content_new, $_); 478 }; 479 if (scalar @{$self->{NEW_MODULES}}) { 480 push(@config_content_new, "[modules]\n") if (!$module_section); 481 $self->add_content(\@config_content_new, $self->{NEW_MODULES}); 482 }; 483 if (scalar @{$self->{NEW_REPOSITORIES}}) { 484 push(@config_content_new, "[repositories]\n") if (!$repository_section); 485 $self->add_content(\@config_content_new, $self->{NEW_REPOSITORIES}); 486 }; 487 if (scalar keys %removed_modules) { 488 my @deleted_modules = keys %removed_modules; 489 push(@{$self->{REPORT_MESSAGES}}, "\nModules: @deleted_modules\nhave been removed from the ". $self->get_config_file_default_path()."\n"); 490 491 }; 492 if (scalar keys %removed_repositories) { 493 my @deleted_repositories = keys %removed_repositories; 494 push(@{$self->{REPORT_MESSAGES}}, "\nRepositories: @deleted_repositories\nhave been removed from the ". $self->get_config_file_default_path()."\n"); 495 496 }; 497 498 # Writing file, printing warnings and reports 499 500 #check if we need to write a new file 501 my $write_needed = 0; 502 if ((scalar @{$self->{CONFIG_FILE_CONTENT}}) != (scalar @config_content_new)) { 503 $write_needed++; 504 } else { 505 foreach my $i (0 .. $#{$self->{CONFIG_FILE_CONTENT}}) { 506 if (${$self->{CONFIG_FILE_CONTENT}}[$i] ne $config_content_new[$i]) { 507 $write_needed++; 508 last; 509 }; 510 }; 511 }; 512 if ($write_needed) { 513 my $temp_config_file = File::Temp::tmpnam($ENV{TMP}); 514 die("Cannot open $temp_config_file") if (!open(NEW_CONFIG, ">$temp_config_file")); 515 print NEW_CONFIG $_ foreach (@config_content_new); 516 close NEW_CONFIG; 517 rename($temp_config_file, $self->get_config_file_default_path()) or system("mv", $temp_config_file, $self->get_config_file_default_path()); 518 if (-e $temp_config_file) { 519 system("rm -rf $temp_config_file") if (!unlink $temp_config_file); 520 }; 521 $self->{CONFIG_FILE_CONTENT} = \@config_content_new; 522 }; 523 if ($self->{VERBOSE}) { 524 print $_ foreach (@{$self->{WARNINGS}}); 525 $self->{VERBOSE} = 0; 526 }; 527 $self->{WARNINGS} = []; 528 print $_ foreach (@{$self->{REPORT_MESSAGES}}); 529 $self->{REPORT_MESSAGES} = []; 530}; 531 532##### finish ##### 533 5341; # needed by use or require 535 536__END__ 537 538=head1 NAME 539 540SourceConfig - Perl extension for parsing general info databases 541 542=head1 SYNOPSIS 543 544 # example that will read source_config file and return the active repositories 545 546 use SourceConfig; 547 548 # Create a new instance of the parser: 549 $a = SourceConfig->new(); 550 551 # Get repositories for the actual workspace: 552 $a->get_repositories(); 553 554 # Add a repository new_repository for the actual workspace (via full path): 555 $a->add_repository(/DEV300/new_repository); 556 557=head1 DESCRIPTION 558 559SourceConfig is a perl extension to load and parse General Info Databses. 560It uses a simple object oriented interface to retrieve the information stored 561in the database. 562 563Methods: 564 565SourceConfig::new() 566 567Creates a new instance of SourceConfig. Can be initialized by: path to the default repository, path to the source_config, default - empty, the source_config will be taken from the environment 568 569 570SourceConfig::get_version() 571 572Returns version number of the module. Can't fail. 573 574 575SourceConfig::get_repositories() 576 577Returns sorted list of active repositories for the actual workspace 578 579 580SourceConfig::add_repository(REPOSITORY_PATH) 581 582Adds a repository to the list of active repositories 583 584 585SourceConfig::get_active_modules() 586 587Returns a sorted list of active modules 588 589SourceConfig::get_all_modules() 590 591Returns sorted list of all modules in active repositories. 592 593SourceConfig::get_module_path($module) 594 595Returns absolute module path 596 597SourceConfig::get_module_build_list($module) 598 599Returns absolute module build list path 600 601SourceConfig::get_module_repository($module) 602 603Returns the module's repository 604 605SourceConfig::get_config_file_path() 606 607Returns absolute module to the source configuration file 608 609SourceConfig::get_config_file_default_path() 610 611Returns default path for source configuration file 612 613SourceConfig::is_active() 614 615Returns 1 (TRUE) if a module is active 616Returns 0 (FALSE) if a module is not active 617 618SourceConfig::add_active_modules($module_array_ref) 619 620Adds modules from the @$module_array_ref as active to the source_config file 621 622SourceConfig::add_active_repositories($repository_array_ref) 623 624Adds repositories from the @$repository_array_ref as active to the source_config file 625 626SourceConfig::remove_activated_modules($module_array_ref) 627 628Removes modules from the @$module_array_ref from the source_config file 629 630SourceConfig::remove_all_activated_modules() 631 632Removes all activated modules from the source_config file 633 634SourceConfig::remove_activated_repositories($repository_array_ref) 635 636Removes repositories from the @$repository_array_ref from the source_config file 637 638SourceConfig::remove_all_activated_repositories() 639 640Removes all activated repositories from the source_config file 641 642 643=head2 EXPORT 644 645SourceConfig::new() 646SourceConfig::get_version() 647SourceConfig::get_repositories() 648SourceConfig::add_repository() 649SourceConfig::get_active_modules() 650SourceConfig::get_all_modules() 651SourceConfig::get_module_path($module) 652SourceConfig::get_module_build_list($module) 653SourceConfig::get_module_repository($module) 654SourceConfig::get_config_file_path() 655SourceConfig::get_config_file_default_path() 656SourceConfig::is_active($module) 657SourceConfig::add_active_modules($module_array_ref) 658SourceConfig::add_active_repositories($repository_array_ref) 659SourceConfig::remove_activated_modules($module_array_ref) 660SourceConfig::remove_all_activated_modules() 661SourceConfig::remove_activated_repositories($repository_array_ref) 662SourceConfig::remove_all_activated_repositories() 663 664=head1 AUTHOR 665 666Vladimir Glazunov, vg@openoffice.org 667 668=head1 SEE ALSO 669 670perl(1). 671 672=cut 673