# ---------------------------------------------------------------------------
# 
# File:    QPARSE.pm
#
# Copyright (c) Programming Research, 2004.
#
# Written: C.Waddingham 20-Aug-04
#
# Version: 1.19
#
# Purpose: To provide QAC/PP input parsing capability for qaw.pl.
#
# History:
# 1.19   26-01-12  CW  - CR-14397: ProjectDataflowFile handling added.
# 1.18   22-11-11  CW  - RESOLVE_PATHS_WITH_ENCODED_SPACES(), and
#                        RESOLVE_PARAMETER_WITH_ENCODED_SPACES() reinstated to use
#                        GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS
# 1.17   21-03-11  CW  - PARSE_SASEQ_OPT(), PARSE_MASEQ_OPT(), PARSE_GLOB_PARAMS(),
#                        EXTRACT_FILTER(),
#                        RESOLVE_PATHS_WITH_ENCODED_SPACES(), and
#                        RESOLVE_PARAMETER_WITH_ENCODED_SPACES() updated to use
#                        SPLIT_COMMAND_LINE_STRING().
#                      - CR-13644 Handling for %N added.
#                      - Restructured handling of -rems,
#                      - PARSE_REM_STATEMENTS() removed.
# 1.16   17-04-08  CW  - EXECUTE_FOR_PROG_VERSION parameters updated.
# 1.15   07-02-08  CW  - CR12691. Enable RP/EV handling.
# 1.14   10-07-07  CW  - CR12409: Handle case of cmd.exe needing /c when finding version.
# 1.13   20-02-07  CW  - Handling of -maseq,-saseq,-exec,-glob  which use '\\ '
#                        modified to allow '\"' quote escaping. -rem also.
#                      - QAC[PP] workaound for stringify quotes in -d param, added.
# 1.12   24-10-06  CW  - Expansion of %P deferred to RUN.pm.
#                      - -forget handling corrected.
# 1.11   09-02-06  CW  - Incorrect handling of parameter paths with spaces 
#                        in options like -maseq, -saseq, -exec, -glob - corrected.
#                        Now can allow relative path in -file arg.
#                      - Corrected test for bad " in -saseq/maseq arg.
# 1.10   05-12-05  CW  - PARSE_UNIX_PROJECT_FILE rewritten to correct
#                        handling of inherited personalities and to
#                        remove restriction of not allowing  'Default String'
#                        in Unix project files.
# 1.9    19-10-05  CW  - -afe, -dafe, -sfba added.
# 1.8    08-07-05  CW  - -saseq and -maseq added.
#                      - GET_POST_PROCESSING_OPTS removed.
# 1.7    06-05-05  CW  - <prj><folder>:<absolute path> handling added.
#                      - Project Config File handling added.
# 1.6    20-04-05  CW  - SETTINGS_VIA -> PURE_SETTINGS_VIA,
#                        QA_FILELIST  -> QA_PURE_FILELIST.
# 1.5    17-04-05  CW  - Commented lines allowed in .prj files.
# 1.4    27-03-05  CW  - -glob handling updated for -mode implementation.
# 1.3    24-03-05  CW  - Reworked handling of %M, %E, %F and %F+ in 
#                      PARSE_EXEC_PARAMS.
#                      - Disabled QAC-6.0.1 etc handling in 
#                      GET_POST_PROCESSING_OPTS.
# 1.2    19-02-05  CW  - GET_QAW_OPTS modified to prevent command-line 
#                      QAW options: -canal,-crep,-exec & -glob being passed
#                      to project analysis.
#                      - GET_NON_ANALYSIS_OPTIONS created as rewrite of
#                      GET_FILE_SPECIAL_OPTIONS.  Rewrite gets all non
#                      analysis options (ie display options) for subsequent
#                      inclusion in parameters of project file processing.
# 1.1    12-01-05  CW  - Handling of Cygwin added, including handling of
#                      Unix or Windows progect file.
#
# ---------------------------------------------------------------------------

package QPARSE; #============================================================

require Exporter;
our @ISA = qw( Exporter );

use strict;

require GEN;
require PARAM;
require MODE;
require PARSE_CL;

#----------------------------------------------------------------------------

BEGIN
{
   our $ar_all_qaw_opts = [ "-afe", "-alias", "-cargs", "-canal",
                            "-crep", "-dafe", "-disp", "-ehtml",
                            "-etxt", "-exec", "-fdisp", "-glob",
                            "-glog", "-help", "-logerrs", "-logmsgs",
                            "-maseq", "-mode", "-pdsp", "-pdspd",
                            "-plog", "-sapf", "-saseq", "-sat",
                            "-sfba", "-stoponfail", "-trace", "-view" ];

   our $ar_propagated_qaw_opts = [ "-afe", "-dafe", "-canal", "-crep",
                                   "-disp", "-ehtml", "-etxt", 
                                   "-exec", "-fdisp", "-glob", "-glog", "-help",
                                   "-logerrs", "-logmsgs", "-maseq", "-mode",
                                   "-pdsp", "-pdspd", "-plog",
                                   "-sapf", "-saseq", "-sfba", "-stoponfail", 
                                   "-trace", "-view" ];

   our $ar_non_qa_opts =  [ "-afe", "-alias", "-cargs",
                            "-dafe", "-disp", "-ehtml",
                            "-etxt", "-fdisp",
                            "-glog", "-help", "-logerrs", "-logmsgs",
                            "-mode", "-pdsp", "-pdspd",
                            "-plog", "-sapf", "-sat",
                            "-sfba", "-stoponfail", "-trace", "-view" ];

   our $source_path_static = undef;
}

#----------------------------------------------------------------------------

sub SET_SOURCE_PATH
{
   # Usage: SET_SOURCE_PATH( $path );
   #
   my $path = shift;

   if ( ! defined( $path ) )
   {
      $QPARSE::source_path_static = undef;
   }
   else
   {
      $QPARSE::source_path_static = GEN::APPLY_FINAL_SEP( $path );
   }
}

#----------------------------------------------------------------------------

sub CLEAR_SOURCE_PATH
{
   # Usage: CLEAR_SOURCE_PATH();
   #
   SET_SOURCE_PATH( undef );
}

#----------------------------------------------------------------------------

sub GET_SOURCE_PATH
{
   # Usage: $source_path = GET_SOURCE_PATH();
   #
   return $QPARSE::source_path_static;
}

#----------------------------------------------------------------------------

sub OPTION_IS_CUMULATIVE
{
   # Usage: $opt_is_cumulative_bool = OPTION_IS_CUMULATIVE( $opt );
   #
   my $opt = shift;

   return exists $PARAM::cumulatives{ $opt };
}

#----------------------------------------------------------------------------

sub PREFERRED_FORM_OF_PARAMETER
{
   # Usage: $preferred_form = PREFERRED_FORM_OF_PARAMETER( $pref_opt, $param );
   #
   # Forces parameter of certain options to upper case.
   #
   my $pref_opt = shift;
   my $param    = shift;

   if ( exists( $PARAM::options_with_upper_case_parameter{ $pref_opt } ) )
   {
      return uc( $param );
   }
   elsif ( exists( $PARAM::options_with_lower_case_parameter{ $pref_opt } ) )
   {
      return lc( $param );
   }
   else
   {
      return $param;
   }
}

# -------------------------------------------------------------------------

sub PREFERRED_FORM_OF_OPTION
{
   # Usage: $preferred_form = PREFERRED_FORM_OF_OPTION( $option );
   #
   # For options with more than one name (for the same option), this
   # sub returns a consistent preferred form of the option.
   # Since this form is written to the .opt file, the shortest form is
   # chosen to minimise the size of the file.
   # If there is no preferred form the option is returned in lower case.
   #
   my $option = shift;

   my $opt = lc( $option );

   if ( exists( $PARAM::preferred_options{ $opt } ) )
   {
      return $PARAM::preferred_options{ $opt };
   }
   else
   {
      return $opt;
   }
}

# -------------------------------------------------------------------------

sub PARSE_WINDOWS_PROJECT_FILE
{
   # Usage: ( $ar_project_folders,
   #          $root_folder_name,
   #          $project_properties_file,
   #          $project_dataflow_file ) = PARSE_WINDOWS_PROJECT_FILE( $prj_filename );
   #
   my $prj_filename = shift;

   my $ar_project_folders = [];

   my $root_folder_name   = "";
   my $first_folder_found = 0;   # false

   open PRJ_FILE, "<$prj_filename" or GEN::DIE( ": Cannot open \"$prj_filename\".\n$!" );

   # Read first line, expect:
   # VersionTag45
   #
   my $line = READ_LINE( \*PRJ_FILE, $prj_filename );

   if ( $line !~ /^\s*VersionTag45\s*$/o )
   {
      PARSE_CL::LOG_ERROR( "\"$prj_filename\" is not a valid project file." );
 
      return ( [], "", "" );
   }

   # Get $project_properties_file
   #
   my $project_properties_file = "";

   $line = READ_LINE( \*PRJ_FILE, $prj_filename );

   if ( $line =~ /^\s*ProjectConfigFile=/o )
   {
      ( $project_properties_file = $line ) =~ s/^\s*ProjectConfigFile=//o;

      $project_properties_file = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $project_properties_file );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
   }      

   # Get $project_dataflow_file
   #
   my $project_dataflow_file = "";

   if ( $line =~ /^\s*ProjectDataflowFile=/o )
   {
      ( $project_dataflow_file = $line ) =~ s/^\s*ProjectDataflowFile=//o;

      $project_dataflow_file = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $project_dataflow_file );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
   }      

   LINE:
   while ( ! eof( PRJ_FILE ) )
   {
      next LINE if ( $line !~ /^StartProjectMarker\s*$/o );
      last LINE if ( eof( PRJ_FILE ) );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );

      # Here when at first line of project
      #
      # Get foldername
      #
      my $folder_name;

      ( $folder_name = $line ) =~ s/FolderName=//o;

      if ( ! $first_folder_found )
      {
         $first_folder_found = 1;   # true;

         $root_folder_name = $folder_name;
      }

      my $ar_file_list = [];
      my $source_path;
      my $output_path;
      my $subset_person;
      my $analyser_person;
      my $compiler_person;

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
      ( $source_path = $line ) =~ s/SourcePath=//o;
      $source_path = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $source_path );
      SET_SOURCE_PATH( $source_path );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
      ( $output_path = $line ) =~ s/OutputPath=//o;
      $output_path = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $output_path );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
      ( $subset_person = $line ) =~ s/SubsPers=//o;
      $subset_person = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $subset_person );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
      ( $analyser_person = $line ) =~ s/AnalPers=//o;
      $analyser_person = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $analyser_person );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
      ( $compiler_person = $line ) =~ s/CompPers=//o;
      $compiler_person = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $compiler_person );

      $ar_file_list = READ_FILE_LIST( \*PRJ_FILE, $prj_filename, $source_path );

      foreach my $path ( @$ar_file_list )
      {
         $path = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $path );

         if ( ! -e $path )
         {
            OPC::SHOW_PLOG();
            OPC::INITIALISE_PLOG_BUFFER();
            PARSE_CL::LOG_WARNING( "File \"$path\" does not exist in \"$prj_filename\"." );
         }
      }

      my $hr_project_folder = 
         { "<folder_name>"      => $folder_name,
           "<source_path>"      => $source_path,
           "<output_path>"      => $output_path,
           "<subset_person>"    => $subset_person,
           "<analyser_person>"  => $analyser_person,
           "<compiler_person>"  => $compiler_person,
           "<file_list>"        => $ar_file_list };

      push @$ar_project_folders, $hr_project_folder;
   }
   continue
   {
      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
   }

   close( PRJ_FILE );

   CLEAR_SOURCE_PATH();

   return ( $ar_project_folders, $root_folder_name, $project_properties_file, $project_dataflow_file );
}

# -------------------------------------------------------------------------

sub PARSE_UNIX_PROJECT_FILE
{
   # Usage: ( $ar_project_folders,
   #          $root_folder_name,
   #          $project_properties_file,
   #          $project_dataflow_file ) = PARSE_UNIX_PROJECT_FILE( $prj_filename );
   #
   my $prj_filename = shift;

   open PRJ_FILE, "<$prj_filename" or GEN::DIE( ": Cannot open \"$prj_filename\".\n$!" );

   # Get $project_properties_file
   #

   my $project_properties_file = "";

   my $line = READ_LINE( \*PRJ_FILE, $prj_filename );

   if ( ( index( $line, "ProjectConfigurationFile" ) != -1 ) ||
        ( index( $line, "ProjectConfigFile" ) != -1 )
      )
   { 
      $project_properties_file = READ_LINE( \*PRJ_FILE, $prj_filename );

      $line = READ_LINE( \*PRJ_FILE, $prj_filename );
   }      

   # As of 26-Jan-2012, Motif GUI .prj file is not affected by dataflow.
   # Dataflow settings are stored in the ProjectPropertiesFile.
   # 
   my $project_dataflow_file = "";

   # Parse root folder
   #

   # empty_person is the default or null person and is an empty
   # file.  The idea is that if the empty_person is used then no 
   # personality settings will be passed to the analyser and the 
   # analyser will use its own defaults unless overridden by other
   # command line settings.
   #
   my $empty_person = GET_EMPTY_PERSON_FILE_NAME();

   my $ar_project_folders = 
      PARSE_UNIX_FOLDER
         ( \*PRJ_FILE,
           $prj_filename,
           $line,
           $empty_person,
           $empty_person,
           $empty_person );

   close PRJ_FILE; 

   my $hr_root_folder = $$ar_project_folders[ 0 ];
   my $root_folder_name = $$hr_root_folder{ "<folder_name>" };

   return ( $ar_project_folders, $root_folder_name, $project_properties_file, $project_dataflow_file );
}

#----------------------------------------------------------------------------

sub PARSE_UNIX_FOLDER
{
   # $ar_project_folders = 
   # 	PARSE_UNIX_FOLDER
   # 	   ( \*PRJ_FILE,
   # 	     $prj_filename,
   # 	     $line,
   #         $parent_subset_person,
   #         $parent_analyser_person,
   #         $parent_compiler_person,
   # 	     $empty_person,
   # 	     $empty_person,
   # 	     $empty_person );
   #
   my $PRJ_FILE               = shift;
   my $prj_filename           = shift;
   my $line	              = shift;
   my $parent_subset_person   = shift;
   my $parent_analyser_person = shift;
   my $parent_compiler_person = shift;

   my $ar_project_folders = [];

   my $person_path = "";

   # Skip to StartProjectMarker
   #
   while ( ( ! eof( $PRJ_FILE ) ) &&
           ( $line !~ /^StartProjectMarker\s*$/o )
         )
   {
      if ( $line =~ /^StartPersonalityLocationMarker\s*$/o )
      {
         $person_path = READ_LINE( $PRJ_FILE, $prj_filename );

         $person_path = GEN::APPLY_FINAL_SEP( $person_path );
      }

      $line = READ_LINE( $PRJ_FILE, $prj_filename );
   }

   # Find each StartProjectMarker and process as a folder
   #
   while ( $line =~ /^StartProjectMarker\s*$/o )
   {
      # Here when at first line of folder
      #

      # Get foldername
      #
      my $folder_name = READ_LINE( $PRJ_FILE, $prj_filename );

      # Now expect StartProjectOutpathMarker
      #
      $line = READ_LINE( $PRJ_FILE, $prj_filename );

      if ( $line !~ /^StartProjectOutpathMarker\s*$/o )
      {
         GEN::DIE( ": Bad Unix project file" );
      }
      
      # Read the output path 
      #
      my $output_path = READ_LINE( $PRJ_FILE, $prj_filename );
      
      # Read the source path
      #
      my $source_path = READ_LINE( $PRJ_FILE, $prj_filename );
      
      # Read the file list
      #
      my $ar_file_list = READ_FILE_LIST( $PRJ_FILE, $prj_filename, $source_path );
      
      my $subset_person = READ_PERSON
           ( $PRJ_FILE,
             $prj_filename,
             $person_path,
             ".p_s",
             $parent_subset_person );

      my $analyser_person = READ_PERSON
           ( $PRJ_FILE,
             $prj_filename,
             $person_path,
             ".p_a",
             $parent_analyser_person );

      my $compiler_person = READ_PERSON
           ( $PRJ_FILE,
             $prj_filename,
             $person_path,
             ".p_c",
             $parent_compiler_person );

      # Save this folder
      #
      my $hr_project_folder = 
         { "<folder_name>"      => $folder_name,
           "<source_path>"      => $source_path,
           "<output_path>"      => $output_path,
           "<subset_person>"    => $subset_person,
           "<analyser_person>"  => $analyser_person,
           "<compiler_person>"  => $compiler_person,
           "<file_list>"        => $ar_file_list };

      push @$ar_project_folders, $hr_project_folder;

      # Get any sub folders
      #
      $line = READ_LINE( $PRJ_FILE, $prj_filename );

      if ( $line =~ /^StartSubProjectMarker\s*$/o )
      {
         my $ar_sub_project_folders =
      	    PARSE_UNIX_FOLDER
      	       ( $PRJ_FILE,
      		 $prj_filename,
      		 $line,
      		 $subset_person,
      		 $analyser_person,
      		 $compiler_person );

         $line = READ_LINE( $PRJ_FILE, $prj_filename );

         push @$ar_project_folders, @$ar_sub_project_folders;
      }
   }

   return $ar_project_folders;
}

# -------------------------------------------------------------------------

sub PARSE_WINDOWS_OR_UNIX_PROJECT_FILE
{
   # Usage: ( $ar_project_folders,
   #          $root_folder_name,
   #          $project_properties_file,
   #          $project_dataflow_file ) = PARSE_WINDOWS_OR_UNIX_PROJECT_FILE( $prj_filename );
   #
   # Checks supplied $prj_filename.  If it starts with VersionTag45,
   # then load as a Windows project file, otherwise load as a Unix
   # file.
   #
   my $prj_filename = shift;

   open PRJ_FILE, "<$prj_filename" or GEN::DIE( ": Cannot open \"$prj_filename\".\n$!" );

   LINE:
   while ( ! eof( PRJ_FILE ) )
   {
      my $line = READ_LINE( \*PRJ_FILE, $prj_filename );

      next LINE if ( $line =~ /^\s*$/o );

      close( PRJ_FILE );

      my $ar_project_folders;
      my $root_folder_name;
      my $project_properties_file;
      my $project_dataflow_file;

      if ( $line =~ /^\s*VersionTag45\s*$/o )
      {
         ( $ar_project_folders,
           $root_folder_name,
           $project_properties_file,
           $project_dataflow_file ) = PARSE_WINDOWS_PROJECT_FILE( $prj_filename );
      }
      else
      {
         ( $ar_project_folders,
           $root_folder_name,
           $project_properties_file,
           $project_dataflow_file ) = PARSE_UNIX_PROJECT_FILE( $prj_filename );
      }

      # Here with project read in.
      # Ensure path separators for paths are acceptable to the OS
      #
      $project_properties_file = GEN::NORMALISE_CYGWIN_PATH( $project_properties_file );
      $project_dataflow_file   = GEN::NORMALISE_CYGWIN_PATH( $project_dataflow_file );

      foreach my $hr_prj_folder ( @$ar_project_folders )
      {
         $$hr_prj_folder{ "<source_path>" }     = GEN::NORMALISE_CYGWIN_PATH( $$hr_prj_folder{ "<source_path>" } );  
         $$hr_prj_folder{ "<output_path>" }     = GEN::NORMALISE_CYGWIN_PATH( $$hr_prj_folder{ "<output_path>" } );
         $$hr_prj_folder{ "<subset_person>" }   = GEN::NORMALISE_CYGWIN_PATH( $$hr_prj_folder{ "<subset_person>" } );
         $$hr_prj_folder{ "<analyser_person>" } = GEN::NORMALISE_CYGWIN_PATH( $$hr_prj_folder{ "<analyser_person>" } );
         $$hr_prj_folder{ "<compiler_person>" } = GEN::NORMALISE_CYGWIN_PATH( $$hr_prj_folder{ "<compiler_person>" } );
         
         foreach my $file ( @{ $$hr_prj_folder{ "<file_list>" } } )
         {
            $file = GEN::NORMALISE_CYGWIN_PATH( $file );
         }
      }

      return ( $ar_project_folders, $root_folder_name, $project_properties_file, $project_dataflow_file );
   }

   close( PRJ_FILE );

   GEN::DIE( ": Unreadable project file \"$prj_filename\"." );
}

# -------------------------------------------------------------------------

sub READ_PERSON
{
   # Usage: $person = READ_PERSON
   #             ( \*PRJ_FILE,
   #               $prj_filename,
   #               $person_path,
   #               $extn,
   #               $person );
   #
   my $PRJ_FILE_HANDLE = shift;
   my $prj_filename    = shift;
   my $person_path     = shift;
   my $extn            = shift;
   my $person          = shift;

   my $local_person;

   $local_person = READ_LINE( $PRJ_FILE_HANDLE, $prj_filename );

   if ( $local_person eq "inherit string" )
   {
      return $person;
   }
   elsif ( $local_person eq "default string" )
   {
      # "default string" is deemed to mean that "Default Values" was
      # selected in the gui and hence default analyser settings are
      # to be used.  This implies providing no personality settings
      # whatsoever, - so point to an empty file.
      #
      return GET_EMPTY_PERSON_FILE_NAME();
   }
   elsif ( GEN::LEFT( $local_person, 1 ) eq "/" )
   {
      return $local_person;
   }
   else
   {
      return $person_path . $local_person . $extn;
   }
}

# -------------------------------------------------------------------------

sub READ_LINE
{
   # Usage: $line = READ_LINE( \*PRJ_FILE,
   #                           $prj_filename );
   #
   my $PRJ_FILE_HANDLE = shift;
   my $prj_filename    = shift;

   LINE:
   while ( ! eof( $PRJ_FILE_HANDLE ) )
   {
      my $line = <$PRJ_FILE_HANDLE>;
     
      chomp $line;

      # Skip empty lines:
      #
      next LINE if ( $line =~ /^\s*$/o );

      # Skip comment lines:
      #
      next LINE if ( $line =~ /^\s*\*/o );

      # Expand any alii
      #
      $line = PARSE_CL::EXPAND_VARS( $line );

      # Check for -alias
      #
      if ( $line =~ /^\s*-alias/oi )
      {
         # Here with alias statement
         #
         $line =~ s/.*-alias\s*//io;
   
         my ( $name, $value ) = split( "=", $line, 2 );
            
         PARSE_CL::SET_ALII( $name, $value );

         next LINE;
      }
   
      return GEN::TRIM( $line );
   }

   return "";
}

# -------------------------------------------------------------------------

sub GET_EMPTY_PERSON_FILE_NAME
{
   # Usage: $filename = GET_EMPTY_PERSON_FILE_NAME()
   #

   my $empty_person = PARAM::QA_TEMP() . "EmptyPerson";

   GEN::TRUNCATE_FILE( $empty_person );

   return $empty_person;
}

# -------------------------------------------------------------------------

sub READ_FILE_LIST
{
   # Usage: \@file_list = 
   #           READ_FILE_LIST( \*PRJ_FILE_HANDLE,
   #                           $prj_filename,
   #                           $source_path );
   #
   my $PRJ_FILE_HANDLE = shift;
   my $prj_filename    = shift;
   my $source_path     = shift;

   my $source_path_2 = GEN::APPLY_FINAL_SEP( $source_path );

   my @file_list = ();
   my $line;

   SOURCE_FILE:
   while ( ! eof( $PRJ_FILE_HANDLE ) )
   {
      $line = READ_LINE( $PRJ_FILE_HANDLE, $prj_filename );
   
      last SOURCE_FILE if ( $line eq "EndContainedFilesMarker" );

      my $filename = $line;
   
      if ( GEN::PLATFORM_IS_UNIX() )
      {
   	 if ( ! GEN::IS_FILE_PATH_ABSOLUTE( $filename ) )
   	 {
   	    $filename = GEN::EVALUATE_RELATIVE_PATHS( $source_path_2 . $filename );
   	 }

         if ( ! -e $filename )
         {
            OPC::SHOW_PLOG();
            OPC::INITIALISE_PLOG_BUFFER();
            PARSE_CL::LOG_WARNING( "File \"$line\" does not exist in \"$prj_filename\"." );
         }
      }

      push @file_list, $filename;
   }

   return \@file_list;
}

# -------------------------------------------------------------------------

sub RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE
{
   # Usage: $resolved_path = RESOLVE_PATH_IN_WINDOWS_PROJECT_FILE( $path );
   #
   my $path = shift;

   if ( $path eq "" )
   {
      return $path;
   }

   my $resolved_path = $path;
   my $source_path = GET_SOURCE_PATH();
   my $root_path = PARSE_CL::GET_ROOT_PATH();

   # Handle %SOURCEPATH% prefix
   #
   if ( $resolved_path =~ /^\%SOURCEPATH\%/o )
   {
      # Here when resolved path has %SOURCEPATH%
      #
      if ( defined( $source_path ) )
      {
         $resolved_path =~ s/^\%SOURCEPATH\%[\\\/]*/$source_path/;
      }

      # Check for remaining % var
      #
      if ( index( $resolved_path, "%" ) != -1 )
      {
         GEN::DIE( ": Illegal path in project file.  Path is \"$path\"" );
      }
   }
   elsif ( index( $resolved_path, "%" ) != -1 )
   {
      $resolved_path = PARSE_CL::EXPAND_WINDOWS_ENV_VARS( $resolved_path );

      if ( ! GEN::IS_FILE_PATH_ABSOLUTE( $resolved_path ) )
      {
   	 # Here with resultant relative path
   	 # Combination of env vars and rel path is illegal
   	 #
   	 GEN::DIE( ": Illegal combination of relative path and environment variable(s) in project file.  Path is \"$path\"" );
      }
   }
   elsif ( ! GEN::IS_FILE_PATH_ABSOLUTE( $resolved_path ) )
   {
      # Here with valid rel path, expand it.
      #
      if ( defined( $root_path ) )
      {
   	 $resolved_path = $root_path . $resolved_path;
      }
      else
      {
   	 GEN::DIE( ": Illegal relative path in project file.  Path is \"$path\"" );
      }
   }

   return $resolved_path;
}

# -------------------------------------------------------------------------

sub CHECK_EXISTS
{
   # Usage: CHECK_EXISTS( $description, $item, $prj_filename );
   #
   my $description  = shift;
   my $item         = shift;
   my $prj_filename = shift;

   if ( ! defined( $item ) || ( $item eq "" ) )
   {
      GEN::DIE( ": $description not set in \"$prj_filename\"." );
   }

   if ( ! -e $item )
   {
      GEN::DIE( ": $description \"$item\" not found in \"$prj_filename\"." );
   }
}

# -------------------------------------------------------------------------

sub CHECK_PROJECT_FOLDERS_LOOK_OK
{
   # Usage: CHECK_PROJECT_FOLDERS_LOOK_OK
   #        ( $ar_project_folders,
   #          $prj_filename );
   #
   # Check that $ar_project_folders has non-null content throughout.
   #
   my $ar_project_folders = shift;
   my $prj_filename       = shift;

   if ( $#$ar_project_folders == -1 )
   {
      GEN::DIE( ": Specified folders not found in project file \"$prj_filename\"." );
   }

   my $file_count = 0;

   foreach my $hr_prj_folder ( @$ar_project_folders )
   {
      CHECK_EXISTS( "SourcePath", $$hr_prj_folder{ "<source_path>" },     $prj_filename );

      # Allow Output Path not to exist as analysis will subsequently create
      # the Output Path directory if it doesn't already exist.
      #
      # CHECK_EXISTS( "OutputPath", $$hr_prj_folder{ "<output_path>" },     $prj_filename );

      CHECK_EXISTS( "SubsPers",   $$hr_prj_folder{ "<subset_person>" },   $prj_filename );
      CHECK_EXISTS( "AnalPers",   $$hr_prj_folder{ "<analyser_person>" }, $prj_filename );
      CHECK_EXISTS( "CompPers",   $$hr_prj_folder{ "<compiler_person>" }, $prj_filename );

      my $ar_file_list = $$hr_prj_folder{ "<file_list>" };

      $file_count += scalar( @$ar_file_list );
   }

   if ( $file_count == 0 )
   {
      GEN::DIE( ": No files listed in project file \"$prj_filename\"." );
   }
}

# -------------------------------------------------------------------------

sub PARSE_FOR_EXEC_AND_GLOB
{
   # Usage: ( $ar_options, 
   #          $ar_exec_anal_details,
   #          $ar_glob_anal_details,
   #          $saseq_found ) =
   #              PARSE_FOR_EXEC_AND_GLOB( $ar_options );
   #
   # NB This sub handles -rem, -exec, -saseq, -glob, -maseq, -crep and -canal.
   #
   # Example (Windows) -rem statements are:
   #
   # -rem "EnablePostAnalysis=1"
   # -rem "ShellExe=C:\Program Files\PRQA\QAC4.5.2\mcm\bin\qacuser"
   # -rem "ShellFile=C:\Program Files\PRQA\QAC4.5.2\bin\qac.ini"
   # -rem "ShellParams=%S %F -trace"
   #
   # Example (Unix) -rem statements are:
   # -rem "EnablePostAnalysis =1",
   # -rem "shellFile /home/Chris_Waddingham/linux01/qac-6.0/mcm/bin/qacuser",
   # -rem "shellNameFile fred",
   # -rem "shellParams '%F %S %Q %P %F %Q'"
   #
   # $post_analysis_command = "$ShellExe $shellParams"  - with %S substituted
   # (if exists) Also substitute %N, %Q, leaving only %F and %P
   # 
   # ShellParams can contain any number of the following
   #
   # %S Script name
   # %N Name rule file
   # %F Source filename
   # %Q Product identifier
   # %P Personality file (traditionally QAC[PP]TEMP/settings.via)
   #
   my $ar_options = shift;

   my @exec_anal_details = ();
   my @glob_anal_details = ();
   my @new_options       = ();

   # 14-11-04:
   # Set default $enable_post_analysis = false.
   # That is, if no EnablePostAnalysis=* records found, then Windows GUI
   # does NOT enable post-analysis.  i.e. to run post-analysis specified
   # by -rem statements, then must explicitly do EnablePostAnalysis=1.
   #    
   # Previous statement:
   # my $enable_post_analysis = 1;
   #
   my $enable_post_analysis = 0;
   my $shell_program        = "";
   my $shell_file           = "";
   my $shell_params         = "";

   my $full_rem_set_found = 0;   # false
         
   OPT:
   foreach my $opt ( @$ar_options )
   {
      if ( ( lc( substr( $opt, 0, 7 ) ) eq "-remark" ) ||
           ( lc( substr( $opt, 0, 4 ) ) eq "-rem" )
         )
      {
         # As of 14-8-04, ie QAC-6.0 and QACPP-2.0, message personality files
         # can only contain one post analysis specification.
         # Hence read all -rem specs in a given file and use the latest settings
         # when reading that file.  ie overwrite settings if duplicate entries
         # exist.
         #
         my $rem_arg = $opt;

         if ( GEN::PLATFORM_IS_WINDOWS() ||
              GEN::PLATFORM_IS_CYGWIN()
            )
         {
            if ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?EnablePostAnalysis=(\d+)/oi )
            {
               $enable_post_analysis = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?ShellExe=([^\"]*)/oi )
            {
               $shell_program = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?ShellFile=([^\"]*)/oi )
            {
               $shell_file = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?ShellParams=([^\"]*)/oi )
            {
               $shell_params = $2;
               $full_rem_set_found = 1;   # true
            }
            else
            {
               GEN::TRACE( "PARSE_REM_STATEMENTS: Ignoring $rem_arg\n" );
            }
         }
         else
         {
            # Here with Unix message personality file entries
            #
            if ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?EnablePostAnalysis.*?(\d+)/oi )
            {
               $enable_post_analysis = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?shellFile\s*([^\"]*)/oi )
            {
               $shell_program = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?shellNameFile=([^\"]*)/oi )
            {
               $shell_file = $2;
            }
            elsif ( $rem_arg =~ /^\s*-(rem|remark)\s*\"?shellParams.*?\'(.*)\'/oi )
            {
               $shell_params = $2;
               $full_rem_set_found = 1;   # true
            }
            else
            {
               GEN::TRACE( "PARSE_REM_STATEMENTS: Ignoring $rem_arg\n" );
            }
         }
         
         next OPT if ( $enable_post_analysis eq "0" );
         
         next OPT if ( $shell_program eq "" );

         if ( $full_rem_set_found )
         {
            $full_rem_set_found = 0;  # false

            # Save current set of -rem specs.
            #
            # In Windows gui shell_params is last line of a set of -rems.
            # Hence save the analyser. 
            #
            # Resolve the parameters
            #
            # NB ShellFile and ShellExe both take a single path which may have embedded 
            # spaces which are NOT quoted.  These are resolved.
            #
            $shell_program = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( "-rem", $shell_program );
            $shell_file    = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( "-rem", $shell_file );
            $shell_params  = RESOLVE_PATHS_WITH_ENCODED_SPACES( $shell_params );
            
            # Ensure correct separators if on Cygwin
            #
            my $prog    = GEN::WHICH( $shell_program );
            $shell_file = GEN::NORMALISE_CYGWIN_PATH( $shell_file );
            
            if ( $prog eq "" )
            {
               GEN::DIE( ": Post-Analysis program \"$shell_program\" not found." );
            }
            
            if ( ( $shell_file ne "" ) && ( ! -e $shell_file ) )
            {
               GEN::DIE( ": Post-Analysis script \"$shell_file\" does not exist." );
            }
            
            my $filter;
            ( $shell_params, $filter ) = PARSE_EXEC_PARAMS( $shell_params, $shell_file );
            
            # Set the program name that does the actual post-analysis.
            # For Unix this is always $shell_program.
            # For Windows this can be $shell_program, or possibly $shell_file.
            # NB If $shell_file is set, then $shell_program must also be set.
            #
            my $error_reporting_program;
            
            if ( $shell_file ne "" )
            {
               $error_reporting_program = $shell_file;
            }
            else
            {
               $error_reporting_program = $prog;
            }
            
            push @exec_anal_details, "$prog\n$shell_params\n$error_reporting_program\n$filter";

            # Have written off this set.
            # Next set won't be written until another SA program entry is found.
            #
            $shell_program = "";
         }

         next OPT;
      }         

      elsif ( lc( substr( $opt, 0, 6 ) ) eq "-saseq" )
      {
         push @exec_anal_details, PARSE_SASEQ_OPT( GEN::TRIM( substr( $opt, 6 ) ) );

         next OPT;
      }

      elsif ( lc( substr( $opt, 0, 6 ) ) eq "-maseq" )
      {
         push @glob_anal_details, PARSE_MASEQ_OPT( GEN::TRIM( substr( $opt, 6 ) ) );

         next OPT;
      }

      elsif ( lc( substr( $opt, 0, 5 ) ) eq "-exec" )
      {
         my ( $prog, $param, $error_reporting_program, $filter ) =
            PARSE_EXEC_OPT( GEN::TRIM( substr( $opt, 5 ) ) );

         push @exec_anal_details, "$prog\n$param\n$error_reporting_program\n$filter";
      }

      elsif ( lc( substr( $opt, 0, 5 ) ) eq "-glob" )
      {
         my ( $prog, $param, $renderer, $result_filename, $raw_param, $filter ) = 
            PARSE_GLOB_OPT( GEN::TRIM( substr( $opt, 5 ) ), "-glob" );

         push @glob_anal_details, "$prog\n$param\n$renderer\n$result_filename\n$raw_param\n$filter";
      }

      elsif ( lc( substr( $opt, 0, 5 ) ) eq "-crep" )
      {
         my $report_name = GEN::UNQUOTE( GEN::TRIM( substr( $opt, 5 ) ) );

         my ( $r_prog, $r_param ) = PARAM::LOOKUP_CUSTOM_REPORT_DETAILS( $report_name );

         my ( $prog, $param, $renderer, $result_filename, $raw_param, $filter ) =
            PARSE_GLOB_OPT( "$r_prog#$r_param", "-crep" );

         push @glob_anal_details, "$prog\n$param\n$renderer\n$result_filename\n$raw_param\n$filter";
      }
      elsif ( lc( substr( $opt, 0, 6 ) ) eq "-canal" )
      {
         my $analyser_name = GEN::UNQUOTE( GEN::TRIM( substr( $opt, 6 ) ) );

         my ( $a_prog, $a_param ) = PARAM::LOOKUP_PROJECT_ANALYSIS_DETAILS( $analyser_name );

         my ( $prog, $param, $renderer, $result_filename, $raw_param, $filter ) = 
            PARSE_GLOB_OPT( "$a_prog#$a_param", "-canal" );

         push @glob_anal_details, "$prog\n$param\n$renderer\n$result_filename\n$raw_param\n$filter";
      }
      else
      {
         push @new_options, $opt;
      }
   }

   return ( \@new_options,
            \@exec_anal_details,
            \@glob_anal_details );
}

# ---------------------------------------------------------------------------

sub PARSE_SASEQ_OPT
{
   # Usage: @exec_anal_details = PARSE_SASEQ_OPT( $opt );
   #
   # Where $opt is argument of a -saseq option and has structure:
   #
   # $opt = program ' '+ program_parameters { '#' program ' '+ program_parameters }
   #
   # NB program and program_parameters use '\ ' for embedded significant spaces.
   # Also, ' '+ is separator for program and program_parameters.
   # program_parameters can have any number of embedded ' ' characters which are
   # kept.  program_parameters sections with '\ ' become surrounded
   # in quotes when processed.
   #
   #
   # program_parameters can have same % terms as PARSE_EXEC_OPT, but %S will result
   # in an error being reported as the -saseq option cannot supply the script value.
   #
   my $opt = shift;

   # Cannot have embedded " anywhere in -saseq parameter.  Instead user should escape
   # spaces with '\ ' and hence have no need of quotes.
   # NB User can escape " using '\"' (this can be used with '\ ' or '\"' pairs. Will
   # make '\ ' the same as ' ', ie escaping of significant spaces becomes unnecessary).
   #
   DIE_IF_EMBEDDED_QUOTES( $opt, "-saseq" );

   # Split opt into list of program specs
   #
   my @prog_specs = SPLIT_HASH_DELIMITED_STRING( $opt, 0, "" );

   my @exec_anal_details = ();

   PROG_SPEC:
   foreach my $prog_spec ( @prog_specs )
   {
      # Handle any embedded '\ ' and '\"'
      #
      my $resolved_spec = RESOLVE_PATHS_WITH_ENCODED_SPACES( $prog_spec );

#      my @list_of_terms = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " ", $resolved_spec );
      my @list_of_terms = GEN::SPLIT_COMMAND_LINE_STRING( $resolved_spec );

      next PROG_SPEC if ( $#list_of_terms == -1 );

      # Get the program value
      #

      my $supplied_prog = $list_of_terms[ 0 ];

      my $prog = GEN::WHICH( $supplied_prog );

      if ( $prog eq "" )
      {
   	 GEN::DIE( ": -saseq parameter \"$supplied_prog\" not found." );
      }

      # Initialise remaining args to null
      #
      my $program_parameters = "";
      my $param = "";
      my $filter = "";
      my $error_prog;

      # Get the parameters value
      #
   
      my ( $ignore_prog, @param_terms ) = @list_of_terms;

      # Convert @param_terms to a list of space spearated terms which are
      # quoted if necessary.
      #

      if ( $#param_terms != -1 )
      {
         # Pop the first term...
         #
         $program_parameters = GEN::QUOTE_ARG( shift @param_terms );
         
         # Get the remaining terms
         #      
         foreach my $term ( @param_terms )
         {
            $program_parameters .= " " . GEN::QUOTE_ARG( $term );
         }

         # Parse params
         #
         ( $param, $filter ) = PARSE_EXEC_PARAMS( $program_parameters, "" );
      }
   
      # Error prog is always prog as -saseq does not allow script.
      #
      $error_prog = $prog;

      my $post_analysis_details = "$prog\n$param\n$error_prog\n$filter";

      push @exec_anal_details, $post_analysis_details;
   }

   return @exec_anal_details;
}

# ---------------------------------------------------------------------------

sub PARSE_MASEQ_OPT
{
   # Usage: @glob_anal_details = PARSE_MASEQ_OPT( $opt );
   #
   # Where $opt is argument of a -maseq option and has structure:
   #
   # $opt = program ' ' program_parameters { '#' program ' ' program_parameters }
   #
   # NB program and program_parameters use '\ ' for embedded significant spaces.
   # Also, ' ' is separator for program and program_parameters.
   # program_parameters can have any number of embedded ' ' characters which are
   # kept.  program_parameters sections with '\ ' become surrounded
   # in quotes when processed.
   #
   #
   # program_parameters can have same % terms as PARSE_GLOB_OPT, but %R will always
   # invoke the default browser (similarly to -canal and -crep) because -maseq
   # does not allow renderer to be specified.
   #
   my $opt = shift;

   # Cannot have embedded " anywhere in -maseq parameter.  Instead user should escape
   # spaces with '\ ' and hence have no need of quotes.
   # NB User can escape " by using '\"' (this can be used with '\ ' or '\"' pairs. Will
   # make '\ ' the same as ' ', ie escaping of significant spaces becomes unnecessary).
   #
   DIE_IF_EMBEDDED_QUOTES( $opt, "-maseq" );

   # Split opt into list of program specs
   #
   my @prog_specs = SPLIT_HASH_DELIMITED_STRING( $opt, 0, "" );

   my @glob_anal_details = ();

   PROG_SPEC:
   foreach my $prog_spec ( @prog_specs )
   {
      # Handle any embedded '\ ' and '\"'
      #
      my $resolved_spec = RESOLVE_PATHS_WITH_ENCODED_SPACES( $prog_spec );

#      my @list_of_terms = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " ", $resolved_spec );
      my @list_of_terms = GEN::SPLIT_COMMAND_LINE_STRING( $resolved_spec );

      next PROG_SPEC if ( $#list_of_terms == -1 );

      # Get the program value
      #

      my $supplied_prog = $list_of_terms[ 0 ];

      my $prog = GEN::WHICH( $supplied_prog );

      if ( $prog eq "" )
      {
   	 GEN::DIE( ": -maseq parameter \"$supplied_prog\" not found." );
      }

      # Initialise remaining args
      #
      my $program_parameters = "";
      my $param = "";
      my $renderer = "";
      my $result_filename = "";
      my $raw_param = "";
      my $filter = "";

      # Get the parameters value
      #
   
      my ( $ignore_prog, @param_terms ) = @list_of_terms;

      if ( $#param_terms != -1 )
      {   
         # Convert @param_terms to a list of space separated terms which are
         # quoted if necessary.
         #
   
         # Pop the first term...
         #
         $program_parameters = GEN::QUOTE_ARG( shift @param_terms );
   
         # Get the remaining terms
         #      
         foreach my $term ( @param_terms )
         {
            $program_parameters .= " " . GEN::QUOTE_ARG( $term );
         }

         # Parse params
         #
         ( $prog, $param, $renderer, $result_filename, $raw_param, $filter ) =
	     PARSE_GLOB_OPT( "$prog#$program_parameters", "-maseq" );
      }

      my $global_analysis_details = 
         "$prog\n$param\n$renderer\n$result_filename\n$raw_param\n$filter";

      push @glob_anal_details, $global_analysis_details;
   }

   return @glob_anal_details;
}

# ---------------------------------------------------------------------------

sub PARSE_EXEC_OPT
{
   # Usage: ( $prog, $param, $error_prog, $filter ) = PARSE_EXEC_OPT( $opt )
   #
   # Syntax of $opt is:
   # "<program>#<script>#<parameters>"
   #
   # NB <parameters> can contain:
   #   %P - include settings.via
   #   %Q - qac / qacpp
   #   %S - insert script file spec at this point
   #   %N - insert name rule file spec at this point
   #   %F - placeholder for file under analysis - NB this is evaluated in 
   #        EXECUTE_POST_ANALYSIS.
   #
   # NB %F allows + and :filter
   # NB %N allows +
   #
   my $opt = shift;

   my ( $supplied_prog, $supplied_script, $param ) =
       SPLIT_HASH_DELIMITED_STRING
          ( $opt,
            -3,
            ": Illegal -exec parameter \"$opt\"" );

   if ( ! defined( $supplied_prog ) )
   {
      $supplied_prog = "";
   }

   if ( ! defined( $supplied_script ) )
   {
      $supplied_script = "";
   }

   if ( ! defined( $param ) )
   {
      $param = "";
   }

   my $script = "";

   # Resolve the parameters
   #
   # NB $supplied_prog and $supplied_script both take a single path which may have 
   # embedded spaces which are NOT quoted.  Resolve these as well.
   #
   my $resolved_prog = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( "-exec", $supplied_prog );

   my $prog = GEN::WHICH( $resolved_prog );

   if ( $supplied_script ne "" )
   {
      $script = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( "-exec", $supplied_script );
   }

   # Use RESOLVE_PATHS_WITH_ENCODED_SPACES here as may have many terms.
   #
   $param = RESOLVE_PATHS_WITH_ENCODED_SPACES( $param );

   if ( $prog eq "" )
   {
      GEN::DIE( ": -exec parameter \"$supplied_prog\" not found." );
   }

   if ( ( $script ne "" ) && ( ! -e $script ) )
   {
      GEN::DIE( ": -exec parameter \"$script\" does not exist." );
   }

   my $filter;
   ( $param, $filter ) = PARSE_EXEC_PARAMS( $param, $script );

   my $error_prog;
   if ( $script ne "" )
   {
      $error_prog = $script;
   }
   else
   {
      $error_prog = $prog;
   }

   return ( $prog, $param, $error_prog, $filter );
}

# ---------------------------------------------------------------------------

sub PARSE_EXEC_PARAMS
{
   # Usage: ( $params, $filter ) = PARSE_EXEC_PARAMS( $params, $script );
   #
   # For Windows, ShellParams can contain any number of the following
   #
   # %S Script name
   # %N[+] Name rule file
   # %F Source filename
   # %Q QAC / QACPP
   # %P Personality file (traditionally QAC[PP]TEMP/settings.via)
   #
   # For Unix GUI parameters are:
   #
   # %Q - QAC / QACPP
   # %M - met file
   # %E - err file
   # %P[+] - [ -via ] settings.via
   # %F[+] - [ -op <outputpath> ] source filename
   #
   # Substitution of %M, %E, %F and %F+ is deferred until RUN::EXECUTE_POST_ANALYSIS.
   #
   # NB For Windows and Unix, %F[+]:filter is permitted.
   #
   my $params = shift;
   my $script = shift;

   # Terminate if %S present and $script is null.
   #
   if ( ( $params =~ /\%S/oi ) &&
        ( $script eq "" )
      )
   {
      GEN::DIE( ": \%S specifier expands to null value in \"$params\"" );
   }

   # Terminate if %N present and $script is null.
   #
   if ( ( $params =~ /\%N/oi ) &&
        ( $script eq "" )
      )
   {
      GEN::DIE( ": \%N specifier expands to null value in \"$params\"" );
   }

   # Get filter
   #
   my $filter;
   ( $filter, $params ) = EXTRACT_FILTER( $params, "F" );

   my $quoted_script = GEN::QUOTE_ARG( $script );
   my $qa_program    = PARAM::QA_PROGRAM();

   # Following are both Unix and Windows GUI options.  Allow all of these to be
   # applied to allow calls from command-line.
   #
   # %Q supported by Unix & Windows
   #
   $params =~ s/\%Q/$qa_program/gi;

   # %P+ is really a Unix GUI only parameter
   #
   $params =~ s/\%P\+/-via \%P/gi;

   # Despite the fact that the Unix GUI does not support %S, support it
   # here to allow -exec users to make use of the 2nd parameter of -exec.
   #
   $params =~ s/\%S/$quoted_script/gi;
   
   # Despite the fact that the Unix GUI does or does not support %N, support it
   # here to allow -exec users to make use of the 2nd parameter of -exec.
   #
   $params =~ s/\%N\+/-nrf \%N/gi;
   $params =~ s/\%N/$quoted_script/gi;
   
   return ( $params, $filter );
}

# ---------------------------------------------------------------------------

sub PARSE_GLOB_OPT
{
   # Usage: ( $prog, $param, $renderer, $result_filename, $raw_param, $filter ) =
   #           PARSE_GLOB_OPT( $opt, $opt_kwd )
   #
   # Syntax of supplied $opt is:
   #   <program>#<parameters> [ -file STDOUT ] >[ #<renderer program> ]
   #
   # NB <renderer program> defaults to PARAM::BROWSER_NAME();
   #
   # Where <parameters> is a string which may contain:
   #   %L - insert name of file listing all files for this global analysis
   #   %P - include settings.via
   #   %Q - qac / qacpp
   #   %R - placeholder for ie result file ie <$prog>.html
   #   %T - temp dir
   #
   # "+" can follow %P, %L, %R with following results:
   #   %L+ => -list <filelist.lst>
   #   %P+ => -via <settings.via>
   #   %R+ => -file <$prog>.html
   #
   # For Windows and Unix, %L[+]:filter is permitted..
   #
   # NB %J is translated in RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS()
   #
   # <renderer program> is the full name of the browser to be used, e.g.
   # "C:\Program Files\Internet Explorer\Iexplore.exe".
   # If <renderer program> is "", then the environment variable:
   # PARAM::DEFAULT_BROWSER() is used as the browser program name.
   #
   my $opt     = shift;
   my $opt_kwd = shift;

   my ( $supplied_prog, $supplied_param, $supplied_renderer ) =
      SPLIT_HASH_DELIMITED_STRING
         ( $opt, -3, ": Illegal $opt_kwd parameter $opt" );

   if ( ! defined( $supplied_renderer ) )
   {
      $supplied_renderer = "";
   }

   my $effective_renderer = $supplied_renderer;
   if ( $effective_renderer eq "" )
   {
      $effective_renderer = PARAM::BROWSER_NAME();
   }

   # Resolve the parameters
   #

   # NB $supplied_prog and $supplied_script both take a single path which may have
   # embedded spaces which are NOT quoted.  Resolve these as well.
   #

   my $resolved_prog = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( $opt_kwd, $supplied_prog );

   my $renderer = "";
   if ( $effective_renderer ne "" )
   {
      my $resolved_renderer = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( $opt_kwd, $effective_renderer );
      $renderer = GEN::WHICH( $resolved_renderer );
   }

   # Use RESOLVE_PATHS_WITH_ENCODED_SPACES here as may have many terms.
   #
   my $supplied_param_2 = RESOLVE_PATHS_WITH_ENCODED_SPACES( $supplied_param );

   my $prog = GEN::WHICH( $resolved_prog );

   if ( $prog eq "" )
   {
      GEN::DIE( ": $opt_kwd parameter \"$supplied_prog\" not found." );
   }

   if ( $effective_renderer ne "" )
   {
      if ( $renderer eq "" )
      {
         GEN::DIE( ": $opt_kwd renderer \"$effective_renderer\" not found." );
      }
   }

   #- Translate the % settings in $param ------------------------------------
   #
   my ( $param, $renderer_prog, $result_filename, $raw_param, $filter ) =
      PARSE_GLOB_PARAMS( $supplied_param_2, $supplied_renderer, $renderer, $prog, $opt_kwd, $opt );

   return ( $prog, $param, $renderer_prog, $result_filename, $raw_param, $filter );
}

#----------------------------------------------------------------------------

sub PARSE_GLOB_PARAMS
{
   # Usage: ( $param, $renderer, $renderer_param, $supplied_param, $filter ) =
   #          PARSE_GLOB_PARAMS
   #             ( $param,
   #               $user_specified_renderer,
   #               $renderer,
   #               $prog,
   #               $opt_kwd,
   #               $opt );
   #
   # For Windows and Unix, %L[+]:filter is permitted..
   #
   my $param                   = shift;
   my $user_specified_renderer = shift;
   my $renderer                = shift;
   my $prog                    = shift;
   my $opt_kwd                 = shift;
   my $opt                     = shift;

   # Get filter
   #
   my $filter;
   ( $filter, $param ) = EXTRACT_FILTER( $param, "L" );

   my $param_without_filter = $param;

   my $quoted_temp = GEN::QUOTE_ARG( PARAM::QA_RESULTS_TEMP() );

   # Calc $percent_r_result_filename as the value to replace the %R string.
   #
   my $percent_r_result_filename = PARAM::QA_RESULTS_TEMP() .
                                   GEN::STEM_NAME( $prog ) .
                                   ".html";

   $percent_r_result_filename = GEN::QUOTE_ARG( $percent_r_result_filename );

   if ( PARAM::VERSION_IS_LATER_THAN_QAC453_OR_QACPP142() )
   {
      # Here with QAC-5.0 or later, or QACPP-2.0 or later
      # In this case %L, %P and %R have special "+" qualifier as follows
      #
      $param =~ s/\%L\+/-list \%L/gi;
      $param =~ s/\%P\+/-via \%P/gi;
      $param =~ s/\%R\+/-file $percent_r_result_filename/gi;

      # Allow case where there is no qualifier
      #
      $param =~ s/\%R/$percent_r_result_filename/gi;
   }
   else
   {
      # Older versions don't have qualifier
      #
      $param =~ s/\%P/-via \%P/gi;   # nb -via is correct.
      $param =~ s/\%R/$percent_r_result_filename/gi;
   }

   my $qa_program = PARAM::QA_PROGRAM();

   # %Q and %T don't have qualifier in either version(s)
   #
   $param =~ s/\%Q/$qa_program/gi;
   $param =~ s/\%T/$quoted_temp/gi;

   #   Already done:
   #   - Replace %R+ terms throughout with -file <$prog>.html
   #   - Replace %R terms throughout with <$prog>.html 
   #
   #   Now do:
   #   - Count '-file' terms, if > 1 die. 
   #

   my $count_of_file_terms = GEN::COUNT_STRING( lc( $param ), "-file" );

   if ( $count_of_file_terms > 1 )
   {
      GEN::DIE( ": Illegal parameter in: " .
                "$opt_kwd " . GEN::QUOTE_ARG( $opt ) . "\n" .
                "Actual or implied count of $count_of_file_terms \"-file\" terms.  " .
                "Max allowed is 1." );
   }
   elsif ( ( $count_of_file_terms == 0 ) &&
           ( $user_specified_renderer ne "" )
         )
   {
      GEN::DIE( ": Useless specification of renderer in: " .
                "$opt_kwd " . GEN::QUOTE_ARG( $opt ) . "\n" .
                "as no -file parameter option present." );
   }
   elsif ( $count_of_file_terms == 0 )
   {
      # Never do rendering if -file not present.  Hence reset $renderer.
      #
      $renderer = "";
   }

   #- Handle output file specification. -------------------------------------
   #
   # Following is taken from the QAC-6.0-Win-UsersGuide.pdf:
   #
   #    %R[+] => [-file] <TEMPDIR\<exe_name>.html>
   #
   #    Formats an output file, located in the product temp
   #    directory, made up of the basename ( <exe_name> ) of the
   #    report executable with a .html extension.
   #
   #    There are two possible modes of operation for custom reports:
   #
   #    1. Run as a batch process, generate an output file, and display through
   #    an editor or browser. For example, prjdsp.exe operates in this
   #    manner.
   #
   #    2. Run as an interactive program. For example, the Message Browser
   #    ( viewer.exe ) operates in this manner.
   #
   #    The decision on which mode will apply is based on whether there is a
   #    "-file" entry in the final command line of the custom report. If this
   #    parameter is detected, it signifies that an output file will be generated,
   #    and thus, that this output file will be displayed as the report result. If
   #    this parameter entry is not found, then it is assumed that the external
   #    report program has its own display mechanism, as is the case with the
   #    Message Browser.
   #
   # Use the same rule in QAW.  i.e.:
   #
   #   Already done:
   #   - Replace %R+ terms throughout with -file <$prog>.html
   #   - Replace %R terms throughout with <$prog>.html 
   #   - Count '-file' terms, if > 1 die. 
   #   
   #   Now do:
   #   IF  '-file' is present in the expanded parameters  THEN
   #     SET $renderer_param = Parameter of '-file'
   #     ! Handling of '-file' parameter is performed in
   #     ! RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS.
   #     ! IF '-file' parameter is STDOUT THEN
   #     !   RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS -
   #     !   i. executes $prog which creates a temporary file
   #     !      which is sent to Stdout. No rendering occurs.
   #     ! ELSE
   #     !   RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS -
   #     !   i. executes $prog which creates file named as token after '-file'.
   #     !   ii. IF $renderer is specified in the -glob option  THEN
   #     !         Render file created in i. using 3rd arg specified browser.
   #     !       ELSE
   #     !         Render file created in i. using DEFAULT_BROWSER.
   #     !       ENDIF
   #     !  ENDIF
   #   ELSE
   #     ! Here with no -file hence no rendering.
   #     SET $renderer_param = ""
   #     ! RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS -
   #     !   i. Executes $prog only.
   #   ENDIF
   #
   # Hence logic is:
   #
   # <params> inc.      3rd arg       $result_filename    Effect
   # ---------------    --------------------------------------------------------------
   #                                                   
   # ""                 ""            ""                  Nothing rendered.
   #                                                      
   # ""                 <bro>                             Error. Specifying the
   #                                                      browser is meaningless.
   #                                                      
   # "-file STDOUT"     ""            STDOUT              Create temporary file.
   #                                                      Send that file to STDOUT.
   #                                                      
   # "-file fred"       ""            fred                Create file fred.
   #                                                      Render file fred with
   #                                                      DEFAULT_BROWSER.
   #                                                      
   # "-file fred"       <bro>         fred                Create file fred.
   #                                                      Render file fred with
   #                                                      browser <bro>.
   #                                                      
   # "%R"               ""            <program>.html      Run report program.
   #                                                      Do not render as no -file
   #                                                      in final params.
   #                                                      
   # "%R"               <bro>                             Error. Specifying the
   #                                                      browser is meaningless 
   #                                                      as no browsing is to be
   #                                                      done, because no '-file'
   #                                                      
   # "%R -file STDOUT"  ""            STDOUT              Create temporary file.
   #                                                      Send that file to STDOUT.
   #                                                      
   # "%R -file fred"    ""            fred                Create file fred.
   #                                                      Render file fred with
   #                                                      DEFAULT_BROWSER.
   #                                                      
   # "%R -file STDOUT"  <bro>         STDOUT              Error. Specifying the
   #                                                      browser is meaningless 
   #                                                      as no browsing is to be
   #                                                      done, because filename is
   #                                                      STDOUT.
   #                                                      
   # "%R -file fred"    <bro>         fred                Create file fred.
   #                                                      Render file fred with
   #                                                      browser <bro>.
   #                                                      
   # "%R+"              ""            <program>.html      Create <program>.html.
   #                                                      Render that with 
   #                                                      DEFAULT_BROWSER.
   #                                                      
   # "%R+"              <bro>         <program>.html      Create <program>.html.
   #                                                      Render that with 
   #                                                      browser <bro>.
   #                                                      
   # "%R+ -file STDOUT" ""            STDOUT              Error. >= 2 -file specified.
   #                    
   # "%R+ -file fred"   ""            fred                Error. >= 2 -file specified.
   #                    
   # "%R+ -file STDOUT" <bro>         STDOUT              Error. >= 2 -file specified.
   #                    
   # "%R+ -file fred"   <bro>         fred                Error. >= 2 -file specified.
   #
   # -----------------------------------------------------------------------------
   #

   #- Here with single or no -file term, get the parameter. -----------------
   # 

   my $renderer_param = "";

#   my @tokens = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " \t", $param );
   my @tokens = GEN::SPLIT_COMMAND_LINE_STRING( $param );

   # Look for token following -file
   #
   TOKEN:
   for ( my $itok = 0; $itok <= $#tokens; ++$itok )
   {
      if ( lc( $tokens[ $itok ] ) eq "-file" ) 
      {
         if ( $itok == $#tokens )
         {
            GEN::DIE( ": No parameter for option -file in:\n" .
                      "\"$param\"" );
         }
         
         $renderer_param = $tokens[ $itok + 1 ];
   
         last TOKEN;
      }
   }
   
   if ( ( $renderer_param eq "STDOUT" ) && 
        ( $user_specified_renderer ne "" )
      )
   {
      GEN::DIE( ": Useless specification of renderer in: " .
                "$opt_kwd " . GEN::QUOTE_ARG( $opt ) . "\n" .
                "as output to STDOUT specified, hence renderer not used." );
   }

   # As far as $renderer_param is concerned, here with:
   #
   #    1 - $renderer_param == "", if no actual or implied '-file'
   #        => RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS shall not render.
   #
   # or 2 - $renderer_param == e.g. ../QAC../temp/prjdsp.html
   #        if %R[+] was specified.
   #
   # or 3 - $renderer_param == arg of -file, nb:
   #        If -file arg == "STDOUT", then set $renderer_param := "STDOUT"
   #        In this case force $renderer to "". NB don't have to worry
   #        about $renderer_param == STDOUT regarding following %R translation
   #        as there cannot be a %R as well as -file.
   #
   if ( $renderer_param eq "STDOUT" )
   {
      # Return fact that renderer is not invoked by setting $renderer to "".
      #
      $renderer = "";

      # Remove the -file STDOUT parameter from $param as 
      # RUN::EXECUTE_GLOBAL_ANALYSIS_PROGRAMS will insert its own -file option.
      #
      $param =~ s/\-file\s*STDOUT\b//io;
   }

   return ( $param, $renderer, $renderer_param, $param_without_filter, $filter );
}

# ---------------------------------------------------------------------------

sub EXTRACT_FILTER
{
   # Usage: ( $filter, $params ) = EXTRACT_FILTER( $params, $code );
   #
   # Returns filter part of any %F[+]:filter or %L[+]:filter terms
   # in $params, and $params with the :filter part removed.
   #
   my $params = shift;
   my $code   = uc( shift );

   my $filter = "";
   my $uc_params = uc( $params );
   my $percent_code_count = GEN::COUNT_STRING( $uc_params, "%$code" );

   if ( $percent_code_count > 0 )
   {
      my $uc_params_less_plus;
      ( $uc_params_less_plus = $uc_params ) =~ s/\%$code\+/\%$code/g;

      # 24-10-06: This 'if' is redundant
      if ( $percent_code_count > 0 )
      {
         my $percent_code_colon_count = GEN::COUNT_STRING( $uc_params_less_plus, "%$code:" );

         if ( $percent_code_colon_count > 1 )
         {
            GEN::DIE( ": Illegal multiple filter term in \"$params\"" );
         }
         elsif ( $percent_code_colon_count == 1 )
         {       
            # Here with (single) %F[+]:filter or %L[+]:filter
            #

            # If :filter had embedded '\ ' these were processed out prior to
            # entry to this sub, and if so :filter will have surrounding "" etc
            #
            # So split params into terms (allowing quoted terms) and then
            # extract filter from the (single) term that starts '%$code[+]:...'
      	    #

#            my @list_of_terms = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " ", $params );
            my @list_of_terms = GEN::SPLIT_COMMAND_LINE_STRING( $params );
      	 
            foreach my $term ( @list_of_terms )
            {
               if ( $term =~ /^\%$code\+?:/i )
               {
                  # Set the filter
                  #
                  ( $filter = $term ) =~ s/^\%$code\+?://i;

                  # Remove filter from term
                  #
                  ( $term = $term ) =~ s/^(\%$code\+?).*$/$1/i;
               }
            }

            # Reconstitute $params without the filter
            #
            $params = join( " ", @list_of_terms );
         }
      }
   }

   return ( $filter, $params );
}

# ---------------------------------------------------------------------------

sub SPLIT_HASH_DELIMITED_STRING
{
   # Usage: @result = SPLIT_HASH_DELIMITED_STRING( $str, 
   #                                               $n_elements,
   #                                               $error_message );
   #
   # $str           = string to split
   # $n_elements    = expected number of elements.
   #                  If zero then any number of elements allowed.
   #                  If negative then expected number of elements 
   #                  is 1 .. abs( $n_elements ).
   #                  If positive then exactly the n_elements
   #                  elements is expected.
   # $error_message = die message if $n_elements test fails
   #
   my $str           = shift;
   my $n_elements    = shift;
   my $error_message = shift;

   # Remove any surrounding quotes
   #
   $str = GEN::UNQUOTE( $str );

   # Replace all instances of escaped # ie \# with new line
   #
   $str =~ s/\\#/\n/go;

   # Ensure we have the correct number of terms
   #
   my $n_el = GEN::COUNT_STRING( $str, "#" ) + 1;

   if ( ( ( $n_elements > 0 ) && ( $n_el != $n_elements ) ) ||
        ( ( $n_elements < 0 ) && ( $n_el > abs( $n_elements ) ) )
      )
   {
      GEN::DIE( $error_message );
   }

   # Now split
   #
   my @result = split( "#", $str );

   # Ensure that @result has correct number of terms
   #
   while ( scalar( @result ) < $n_el )
   {
      push @result, "";
   }

   # Now reinstate original \# throughout all terms but as just #
   #
   foreach my $term ( @result )
   {
      $term  =~ s/\n/#/go;
   }

   return @result;
}

# -------------------------------------------------------------------------

sub DIE_IF_EMBEDDED_QUOTES
{
   # Usage: DIE_IF_EMBEDDED_QUOTES( $opt, $option_name );
   #
   # Allow '\"' but not bare '"'
   #
   my $opt         = shift;
   my $option_name = shift;

   $opt = GEN::UNQUOTE( $opt );
 
   $opt =~ s/\\\"//go;

   if ( index( $opt, "\"" ) != -1 )
   {
      GEN::DIE( ": $option_name option cannot have \" anywhere in its argument - instead, escape significant spaces with '\\ '." );
   }
}

# -------------------------------------------------------------------------

sub RESOLVE_PATHS_WITH_ENCODED_SPACES
{
   # Usage: $param = RESOLVE_PATHS_WITH_ENCODED_SPACES( $param );
   #
   # Replaces all '\ ' strings with '#' and then splits $param into
   # space separated terms, and for each non-separator term converts
   # all '#' into ' ' and applies quotes to the term.
   # Finally the full $param is reassembled with '\ ' now processed out.
   #
   # NB An ENCODED SPACE is a '\ ' string.
   #
   # NB By definition of $param, there can be no bare " characters.
   # NB This rule does not affect this sub.
   #
   # NB A trailing \ character is removed.  This follows the
   # design of GEN::QUOTE_ARG.  For rationale see comments in
   # GEN::QUOTE_ARG.
   #
   my $param = shift;

   # Note $param has already been split on '#' hence this cannot
   # appear with $param.
   #
   my $non_param_char = "#";
   $param =~ s/\\ /$non_param_char/g;

   # Unescape any escaped quotes
   #
   $param =~ s/\\\"/\"/go;

   my @terms = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " ", $param );

   foreach my $term ( @terms )
   {
      if ( $term !~ /^[ ]+$/o )
      {
         $term =~ s/$non_param_char/ /g;
   
         $term = GEN::QUOTE_ARG( $term );
      }
   }

   my $resultant_param = join( " ", @terms );

   return $resultant_param;
}

# -------------------------------------------------------------------------

sub RESOLVE_PARAMETER_WITH_ENCODED_SPACES
{
   # Usage: $resolved_par = RESOLVE_PARAMETER_WITH_ENCODED_SPACES( $opt, $param );
   #
   my $opt   = shift;
   my $param = shift;

   if ( ( index( $param, "\\ " ) != -1 ) ||
        ( index( $param, "\\\"" ) != -1 )
      )
   {
      # Here when there are embedded '\ ' or '\"' so treat specially
      #
      my $resolved_param = RESOLVE_PATHS_WITH_ENCODED_SPACES( $param );
   
      my @list_of_terms = GEN::SPLIT_STRING_ALLOWING_QUOTED_TERMS( " ", $resolved_param );
   
      if ( $#list_of_terms != 0 )
      {
   	GEN::DIE( ": $opt parameter \"$param\" badly formed." );
      }
   
      return $list_of_terms[ 0 ];
   }
   else
   {
      # Here when no embedded escaped '\ ' or '\"' but there may be embedded spaces
      # return anyway.
      #
      return $param;
   }
}

# -------------------------------------------------------------------------

sub UNESCAPE_QUOTES
{
   # Usage: $ar_options = UNESCAPE_QUOTES( $ar_options );
   #
   my $ar_options = shift;

   foreach my $opt ( @$ar_options )
   {
      $opt =~ s/\\"/"/go;
   }

   return $ar_options;
}

# -------------------------------------------------------------------------

sub GET_QAW_OPTS
{
   # Usage: $hr_qaw_options = GET_QAW_OPTS( $ar_opts );
   #
   my $ar_opts = shift;

   # Filter out all options except for the QAW options.
   #
   # NB Filter out -canal, -crep and -glob as these are handled in
   # @$RUN::EXECUTE_ANALYSIS::ar_global_analysis_details and hence are
   # not included in %$RUN::EXECUTE_ANALYSIS::hr_qaw_options.
   #
   # Also filter out -exec as project analysis cannot include -exec
   # specified analysis from the command line.
   #

   my %qaw_options = ();

   my $i_opt = 0;

   OPT:
   while ( $i_opt <= $#$ar_opts )
   {
      my ( $opt, $param, $dn_opt, $error ) = PARSE_CL::GET_OPT( $i_opt, $ar_opts );

      if ( $dn_opt != 1 )
      {
         GEN::DIE( "::GET_QAW_OPTS: Error $error - Bad option \"$opt\"." );
      }

      # Skip all non-propagated QAW options
      #
      next OPT if ( grep( /^\Q$opt\E$/i, @$QPARSE::ar_propagated_qaw_opts ) == 0 );

      my $pref_opt = PREFERRED_FORM_OF_OPTION( $opt );
      $param       = PREFERRED_FORM_OF_PARAMETER( $pref_opt, $param );

      $qaw_options{ $pref_opt } = $param;
   }
   continue
   {
      ++$i_opt;
   }

   return \%qaw_options;
}

# -------------------------------------------------------------------------

sub REMOVE_QAW_OPTIONS
{
   # Usage: $ar_qa_opts = REMOVE_QAW_OPTIONS( $ar_opts );
   #
   # Strips QAW options from $ar_opts, by getting the preferred form of
   # each option and then comparing to the ar_non_qa_opts list.
   #
   # Returns qa options only in preferred form.
   #
   my $ar_opts = shift;

   # Filter out any undesired arguments for analysis
   #
   my $ar_qa_opts = [];

   # Only want QAC/++ opts, hence skip QAW options.
   #
   my $ar_non_qa_opts = $QPARSE::ar_non_qa_opts;

   my $i_opt = 0;

   OPT:
   while ( $i_opt <= $#$ar_opts )
   {
      my ( $opt, $param, $dn_opt, $error ) = PARSE_CL::GET_OPT( $i_opt, $ar_opts );

      if ( $dn_opt != 1 )
      {
	 GEN::DIE( "::REMOVE_QAW_OPTIONS: Error $error - Bad option \"$opt\"." );
      }

      my $pref_opt = PREFERRED_FORM_OF_OPTION( $opt );
      $param       = PREFERRED_FORM_OF_PARAMETER( $pref_opt, $param );

      next OPT if ( grep( /^\Q$pref_opt\E$/i, @$ar_non_qa_opts ) > 0 );

      my $option = PARSE_CL::FULL_OPTION( $pref_opt, $param );

      push @$ar_qa_opts, $option;
   }
   continue
   {
      ++$i_opt;
   }

   return $ar_qa_opts;
}

# -------------------------------------------------------------------------

sub REDUCE_PARSE_OPTIONS
{
   # Usage: $ar_reduced_options = REDUCE_PARSE_OPTIONS( $ar_qa_opts );
   #
   # Does -forget (ie -f on options)
   #
   # Make pass through all options looking for -d options with embedded quotes.
   # These are replaced with:
   #
   # -d _stringify(x)=#x      ! Once only
   # -d user_macro=_stringify(user_macro_unquoted_string)
   #
   my $ar_qa_opts = shift;

   # Now reduce $ar_qa_opts to remove duplicate simple settings,
   # apply any -forget / -forgetall options, 
   # handle the -d options, and then sort
   # the simple options alphabetically and group the 
   # cumulative options by option type.
   # NB Cannot sort the cumulative options on their parameter as
   # the order of -i options must not be changed.
   #
   my $ar_cumulative_opts      = [];
   my $hr_cumulative_opt_names = {};
   my $hr_simple_opt_params    = {};
   my $i_opt                   = 0;

   # First make pass to handle -d options
   #

   my $stringify_specified = 0;   # false
   my $ar_qa_opts_pass1 = [];

   while ( $i_opt <= $#$ar_qa_opts )
   {
      my ( $opt, $param, $dn_opt, $error ) = PARSE_CL::GET_OPT( $i_opt, $ar_qa_opts );

      if ( $dn_opt != 1 )
      {
         GEN::DIE( "::GET_QA_OPTS: Error $error - Bad option \"$opt\"." );
      }

      # 24-10-06: This has already been done(?) Leave in, but redundant
      my $pref_opt = PREFERRED_FORM_OF_OPTION( $opt );
      $param       = PREFERRED_FORM_OF_PARAMETER( $pref_opt, $param );

      # Check for -d in case parameter has embedded quotes and if so apply
      # the _stringify() workaround.  NB _stringify workaround will only work for
      # simple case of parameter surrounded in quotes!  Try anyway.
      #
      if ( $pref_opt eq "-d" )
      {
         if ( index( $param, "\"" ) != -1 )
         {
   	    # Unquote as if there are any embedded quotes, the parm will look like:
   	    # print $param =>  "LD=\"/usr/local/share/locale\""
   	    #
   	    $param = GEN::UNQUOTE( $param );

            if ( ! $stringify_specified )
            {
               $stringify_specified = 1;   # true
            }

            # Now look for user_macro= ..., ie
            # -d user_macro=_stringify(user_macro_unquoted_string)
            #
            ( my $lhs = $param ) =~ s/=.*$//o;
            ( my $rhs = $param ) =~ s/^.*?=//o;

            # If $rhs is empty or equal to $param, then we ignore as there is no real rhs.
            #
            if ( ( $rhs ne "" ) &&
                 ( $rhs ne $param )
               )
            {
               # Here with real rhs to check
               #

               # If there are any " characters then they have been escaped, so unescape.
               #
               $rhs =~ s/\\\"/\"/go;

               my $unquoted_rhs = GEN::UNQUOTE( $rhs );

               if ( $unquoted_rhs ne $rhs )
               {
                  $param = "$lhs=_stringify($unquoted_rhs)";
               }
            }
         }
      }

      push @$ar_qa_opts_pass1, PARSE_CL::FULL_OPTION( $pref_opt, $param );

      ++$i_opt;
   }

   if ( $stringify_specified )
   {
      my $stringify_opt = PARSE_CL::FULL_OPTION( "-d", "_stringify(x)=#x" );

      unshift @$ar_qa_opts_pass1, $stringify_opt;
   }

   # Pass 2.

   my $j_opt = 0;

   while ( $j_opt <= $#$ar_qa_opts_pass1 )
   {
      my ( $opt, $param, $dn_opt, $error ) = PARSE_CL::GET_OPT( $j_opt, $ar_qa_opts_pass1 );

      if ( $dn_opt != 1 )
      {
         GEN::DIE( "::GET_QA_OPTS: Error $error - Bad option \"$opt\"." );
      }

      # 24-10-06: This has already been done - by previous loop
      # my $pref_opt = PREFERRED_FORM_OF_OPTION( $opt );
      # $param       = PREFERRED_FORM_OF_PARAMETER( $pref_opt, $param );

      my $pref_opt = $opt;

      # Note -forget and -forgetall will have been folded to -f
      #
      if ( $pref_opt eq "-f" )
      {
         my $forget_param = lc( "-$param" );

         if ( OPTION_IS_CUMULATIVE( $forget_param ) )
         {
            # Remove any options called $forget_param from $ar_cumulative_opts
            #

   	    my $ar_new_cumulative_opts = [];
   
   	    CUMULATIVE_OPT:
   	    foreach my $cumulative_opt ( @$ar_cumulative_opts )
   	    {
   	       my ( $opt, $ignored_param ) = split( "\n", $cumulative_opt );
   
   	       next CUMULATIVE_OPT if ( lc( $opt ) eq $forget_param );
   
   	       push @$ar_new_cumulative_opts, $cumulative_opt;
   	    }
   
   	    $ar_cumulative_opts = $ar_new_cumulative_opts;
	 }
         else
         {
            # Remove non-cumulative option from current list.
            #
            if ( exists( $$hr_simple_opt_params{ $forget_param } ) )
            {
               delete $$hr_simple_opt_params{ $forget_param };
            }
         }
      }
      elsif ( OPTION_IS_CUMULATIVE( $pref_opt ) )
      {
         push @$ar_cumulative_opts, "$pref_opt\n$param";
         $$hr_cumulative_opt_names{ $pref_opt } = "anything";
      }
      else
      {
         $$hr_simple_opt_params{ $pref_opt } = $param;
      }

      ++$j_opt;
   }

   my $ar_reduced_options = [];

   foreach my $opt ( sort keys %$hr_simple_opt_params )
   {
      push @$ar_reduced_options, 
           PARSE_CL::FULL_OPTION( $opt, $$hr_simple_opt_params{ $opt } );
   }

   # Group the cumulative opts by opt name
   #
   foreach my $opt_name ( sort keys %$hr_cumulative_opt_names )
   {
      CUM_OPT:
      foreach my $cum_opt ( @$ar_cumulative_opts )
      {
         my ( $opt, $param ) = split( "\n", $cum_opt );

         next CUM_OPT if ( $opt ne $opt_name );

         push @$ar_reduced_options, PARSE_CL::FULL_OPTION( $opt, $param );
      }
   }

   return $ar_reduced_options;
}

# -------------------------------------------------------------------------

sub GET_NON_ANALYSIS_OPTIONS
{
   # Usage: $ar_non_analysis_options = GET_NON_ANALYSIS_OPTIONS( $ar_opts );
   #
   # Create list of display options present in $ar_opts
   #
   # This includes -file options as these must be passed to the project
   # analysis in order for the output file, specified by -file, to be
   # written to by any -glob processing.
   #
   # NB %PARAM::analysis_options contains all option forms, e.g. -f -forget and
   # -forgetall
   #
   my $ar_opts = shift;

   my $ar_non_analysis_options = [];

   foreach my $arg ( @$ar_opts )
   {
      my $opt = lc( $arg );
 
      # Strip off parameter (if present)
      #
      $opt =~ s/^(-.+?)\s.*/$1/o;

      # Strip off trailing +|- (if present)
      #
      $opt =~ s/(\+|\-)$//o;

      if ( ! exists( $PARAM::analysis_options{ $opt } ) )
      {
         push @$ar_non_analysis_options, $arg;
      }
   }

   return $ar_non_analysis_options;
}

# -------------------------------------------------------------------------

sub GET_POST_ANALYSER_SPEC
{
   # Usage: $analyser_spec = 
   #           GET_POST_ANALYSER_SPEC( $post_anal_program,
   #                                   $error_reporting_program );
   #
   my $post_anal_program       = shift;
   my $error_reporting_program = shift;

   my $prog_name;
   my $version_string;
   my $post_anal_prog = $post_anal_program;

   if ( GEN::PLATFORM_IS_WINDOWS() )
   {
      # Test for case when on Windows and $post_anal_prog is cmd.exe. 
      # If this is the case then need /c arg.
      #

      my $prog_t = lc( GEN::BASE_NAME( $post_anal_program ) );

      if ( ( $prog_t eq "cmd" ) ||
           ( $prog_t eq "cmd.exe" ) 
         )
      {
      	 $post_anal_prog .= "/C";
      }
   }

   # Get version of program / script
   #
   if ( GEN::FILES_ARE_SAME( $post_anal_program, $error_reporting_program ) )
   {
      # Here when no post-analysis script is in use.
      #
      $version_string = PARAM::EXECUTE_FOR_PROG_VERSION( $post_anal_prog, "-ver" );

      $prog_name = GEN::BASE_NAME( $post_anal_program );
   }
   else
   {
      # Here when post-analysis script in use, hence get version of script
      #
      my $error_prog = GEN::QUOTE_ARG( $error_reporting_program );

      $version_string = PARAM::EXECUTE_FOR_PROG_VERSION( $post_anal_prog, "$error_prog -ver" );

      $prog_name = GEN::BASE_NAME( $error_reporting_program );
   }

   chomp $version_string;

   $version_string =~ s/\n/ /go;

   if ( $version_string =~ /^.*?version/oi )
   {
      # Here when 'version' substring is present
      # Strip all preceding chars
      #
      $version_string =~ s/^.*?version/version/oi;
   }

   $version_string = GEN::LEFT( $version_string, 80 );

   return "$prog_name $version_string";
}

# -- eof QPARSE.pm ----------------------------------------------------------
