/*****************************************************************************/
/*
                                  calogs.c

Consolidate Access LOGS (pronounced the same as the breakfast cereal brand :^)
merges multiple HTTP server common and combined format access logs into a
single log file with records in time-order.  Although specifically intended for
per-instance logging (where one log per server process is generated) it can be
used to consolidate any set of access logs.  Due to the granularity of HTTP
server entry timestamps (one second) the records are sorted to the one second
but not within the one second.

It uses RMS and the VMS sort-merge routines to provide it's basic consolidation
functionality.  An RMS search matches a wildcard log file specification as the
first CLI parameter.  Matching files are opened and each record read.  The
date/time field is parsed and the textual timestamp converted to a binary time
in seconds (Unix time) which is prepended to the log record and passed to
sort-merge input.  When all files have been processed the sort/merge is
performed using the integer time value as an efficient sort key.  The sorted
records are then written to the output file specified as the second CLI
parameter (defaults to SYS$OUTPUT) stripping the leading binary time.

To allow some limited filtering of records during the merge the /NOWASD
qualifier excludes WASD server status records (startup, shutdown, timestamp,
etc.), the /NOPROXY qualifier excludes proxy records (producing a log
containing only non-proxy request processing), and the /PROXY qualifier
excludes non-proxy records (producing a log containing only proxy request
processing).


USAGE
-----
  $ CALOGS == "$HT_EXE:CALOGS"
  $ CALOGS <log-file-spec> [<output-file-name>] [qualifiers]

For example:

  $ CALOGS HT_LOGS:*200205*.LOG 2002_MAY.LOG
  $ CALOGS /VERBOSE HT_LOGS:
  $ CALOGS /NOWASD HT_LOGS:*200206*.LOG_* 2002_JUNE.LOG
  $ CALOGS /PROXY /NOWASD HT_LOGS:*2002*.LOG 2002_PROXY.LOG

                                      
QUALIFIERS
----------
/DBUG                 turns on all "if (Debug)" statements
/HELP                 provide some basic usage information
/NOPROXY              only merge records that are not of proxy access
/NOWASD               filter out the WASD server status messages
/OUTPUT=<filename>    same as second parameter (for habitual users of /OUT= :^)
/PROXY                only merge records of proxy access
/QUIET                no output except error messages
/SOFTWAREID           display the utility version
/VERBOSE              provide file-by-file statistics
/VERSION              display the utility version


BUILD DETAILS
-------------
See BUILD_CALOGS.COM procedure.


COPYRIGHT
---------
Copyright (C) 2002,2003 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
23-DEC-2003  MGD  v1.0.1, minor conditional mods to support IA64
28-MAY-2002  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.0.1"
#define SOFTWARENM "CALOGS"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* VMS-related header files */
#include <descrip.h>
#include <lib$routines.h>
#include <rms.h>
#include <sordef.h>
#include <sor$routines.h>
#include <ssdef.h>
#include <stsdef.h>
#include <starlet.h>

#define BOOL int
#define true 1
#define false 0

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
/* maximum log line/record length */
#define MAX_LINE_LENGTH 2048

char  CopyrightInfo [] =
"Copyright (C) 2002,2003 Mark G.Daniel.\n\
This software comes with ABSOLUTELY NO WARRANTY.\n\
This is free software, and you are welcome to redistribute it\n\
under the conditions of the GNU GENERAL PUBLIC LICENSE, version 2.";

char  Utility [] = "CALOGS";

BOOL  Debug,
      DoNoWasd,
      DoNoProxy,
      DoProxyOnly,
      DoQuiet,
      DoVerbose;

int  DiscardRecordCount,
     FileCount,
     InputRecordCount,
     NonProxyRecordCount,
     OutputRecordCount,
     ProxyRecordCount,
     ReadRecordCount,
     WasdRecordCount;

char  LogFileSpec [256],
      MergedFileName [256];

typedef struct
{
   unsigned short  type;
   unsigned short  order;
   unsigned short  offset;
   unsigned short  len;
} KEY_INFO;

struct
{
   unsigned short  num;
   KEY_INFO  key [1];
} SortKeys;

globalvalue int  SOR$M_STABLE;

/* required function prototypes */
int GetParameters ();
int ShowHelp ();
int ReadLogFiles ();
int ProcessLogFile (char*);
int WriteMergedLog ();
int strsame (char*, char*, int);
char* SysGetMsg (int);

/*****************************************************************************/
/*
*/

main ()

{
   int  status,
        SortLrl,
        SortOptions;

   /*********/
   /* begin */
   /*********/

   if (getenv ("CALOGS$DBUG")) Debug = true;
   GetParameters ();

   if (DoVerbose)
      fprintf (stdout, "%%%s-I-VERBOSE, version %s\n", Utility, SOFTWAREID);

   /* time (Unix seconds) */
   SortKeys.key[0].type = DSC$K_DTYPE_LU;
   SortKeys.key[0].order = 0;
   SortKeys.key[0].offset = 0;
   SortKeys.key[0].len = 4;
   SortKeys.num = 1;

   SortOptions = SOR$M_STABLE;
   SortLrl = MAX_LINE_LENGTH + 4;

   if (VMSnok (status =
        sor$begin_sort (&SortKeys, &SortLrl, &SortOptions,
                        0, 0, 0, 0, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-SORT, begin\n-%s\n",
               Utility, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   if (DoVerbose) fprintf (stdout, "READING ...\n");

   ReadLogFiles ();

   if (DoVerbose) fprintf (stdout, "MERGING ...\n");

   if (VMSnok (status = sor$sort_merge (0)))
   {
      fprintf (stdout, "%%%s-E-SORT, merge\n-%s\n",
               Utility, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   if (InputRecordCount)
   {
      if (DoVerbose) fprintf (stdout, "WRITING ...\n");
      if (!MergedFileName[0]) strcpy (MergedFileName, "SYS$OUTPUT:");
      WriteMergedLog ();
   }

   if (VMSnok (status = sor$end_sort (0)))
   {
      fprintf (stdout, "%%%s-E-SORT, end\n-%s\n",
               Utility, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   if (!DoQuiet || DoVerbose)
   {
      fprintf (stdout,
"%%%s-%s-STATISTICS, for log consolidation\n\
Log Files:         %d\n\
Records Read:      %d\n\
        Discarded: %d\n",
            Utility,
            InputRecordCount == OutputRecordCount ? "I" : "W",
            FileCount, ReadRecordCount, DiscardRecordCount);

      if (DoNoProxy || DoProxyOnly)
         fprintf (stdout,
"        Proxy:     %d\n\
        Non-Proxy: %d\n",
            ProxyRecordCount, NonProxyRecordCount);

      if (DoNoWasd)
         fprintf (stdout,
"        WASD:      %d\n",
            WasdRecordCount);

      fprintf (stdout,
"        Merged:    %d\n\
        Output:    %d\n",
            InputRecordCount, OutputRecordCount);

      if (InputRecordCount != OutputRecordCount)
         fprintf (stdout, "-%s-W-COUNT, input and output record count?\n",
                  Utility);
   }

   if (InputRecordCount != OutputRecordCount) exit (SS$_BUGCHECK);

   exit (SS$_NORMAL);
}

/****************************************************************************/
/*
Search against the command line file specification, passing each file found to
be processed.
*/ 

ReadLogFiles ()

{
   int  status,
        FileNameLength,
        Length;
   char  FileName [256],
         ExpFileName [256];
   struct FAB  SearchFab;
   struct NAM  SearchNam;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ReadLogFiles()\n");

   /* initialize the file access block */
   SearchFab = cc$rms_fab;
   SearchFab.fab$l_dna = "*.LOG*;";
   SearchFab.fab$b_dns = 7;
   SearchFab.fab$l_fna = LogFileSpec;
   SearchFab.fab$b_fns = strlen(LogFileSpec);
   SearchFab.fab$l_nam = &SearchNam;

   SearchNam = cc$rms_nam;
   SearchNam.nam$l_esa = ExpFileName;
   SearchNam.nam$b_ess = sizeof(ExpFileName)-1;
   SearchNam.nam$l_rsa = FileName;
   SearchNam.nam$b_rss = sizeof(FileName)-1;

   if (VMSnok (status = sys$parse (&SearchFab, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-PARSE, %s\n-%s\n",
               Utility, LogFileSpec, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   while (VMSok (status = sys$search (&SearchFab, 0, 0)))
   {
      FileCount++;
      *SearchNam.nam$l_ver = '\0';
      ProcessLogFile (FileName);
      *SearchNam.nam$l_ver = ';';
   }

   if (VMSnok (status) && status != RMS$_NMF)
   {
      SearchNam.nam$l_ver[SearchNam.nam$b_ver] = '\0';
      fprintf (stdout, "%%%s-E-SEARCH, %s\n-%s\n",
               Utility, FileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }
}

/****************************************************************************/
/*
Open the log file, read and process each record from it, close the file!
Parse the date/time components from each record, converting them into a binary
integer value (Unix time in seconds).  Prepend this this to the textual
component of the record passing this to the sort-merge utility routines.  To
reduce unnecessary copying of record contents the buffer is organized so the
first four bytes are left available for prepending the integer time.

The common and combined formats ...
'client rfc891 authuser [date/time] "request" status bytes'
'client rfc891 authuser [date/time] "request" status bytes referer agent'
*/ 

int ProcessLogFile (char *FileName)

{
   static char  *MonthAbbrev [] = { NULL,
                                    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
   static $DESCRIPTOR (RecordDsc, "");

   int  status,
        day, month, year, hour, min, sec,
        FileDiscardRecordCount,
        FileInputRecordCount,
        FileNonProxyRecordCount,
        FileProxyRecordCount,
        FileRecordCount,
        FileWasdRecordCount,
        UnixTime;
   char  *cptr, *mptr, *sptr;
   /* plus four allows for the binary timestamp */
   char  Line [MAX_LINE_LENGTH+4];
   struct tm  utm;
   struct FAB  FileFab;
   struct RAB  FileRab;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessLogFile()\n");

   if (DoVerbose) fprintf (stdout, "%s\n", FileName);

   FileFab = cc$rms_fab;
   FileFab.fab$b_fac = FAB$M_GET;
   FileFab.fab$l_fna = FileName;
   FileFab.fab$b_fns = strlen(FileName);
   FileFab.fab$b_shr = FAB$M_SHRGET | FAB$M_SHRPUT | FAB$M_SHRUPD;

   if (VMSnok (status = sys$open (&FileFab, 0, 0)))
   {
      if (status == RMS$_FLK)
      {
         /* convert it into a warning */
         status &= 0xfffffff8;
         fprintf (stdout, "%%%s-W-OPEN, %s\n-%s\n",
                  Utility, FileName, SysGetMsg(status)+1);
         return (status);
      }
      fprintf (stdout, "%%%s-E-OPEN, %s\n-%s\n",
               Utility, FileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   FileRab = cc$rms_rab;
   FileRab.rab$l_fab = &FileFab;
   /* 2 buffers and read ahead performance option */
   FileRab.rab$b_mbf = 2;
   FileRab.rab$l_rop = RAB$M_RAH;
   /* plus four, minus five allows for the binary timestamp */
   FileRab.rab$l_ubf = (char*)Line + 4;
   FileRab.rab$w_usz = sizeof(Line) - 5;

   if (VMSnok (status = sys$connect (&FileRab, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-CONNECT, %s\n-%s\n",
               Utility, FileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   FileDiscardRecordCount = FileInputRecordCount =
      FileNonProxyRecordCount = FileProxyRecordCount =
      FileRecordCount = FileWasdRecordCount = 0;

   while (VMSok (status = sys$get (&FileRab, 0, 0)))
   {
      ReadRecordCount++;
      FileRecordCount++;

      cptr = Line + 4;
      cptr[FileRab.rab$w_rsz] = '\0';

      if (DoNoProxy || DoProxyOnly || DoNoWasd)
      {
         /* skip over client and remote ident fields */
         while (*cptr && !isspace(*cptr)) cptr++;
         while (*cptr && isspace(*cptr)) cptr++;
         while (*cptr && !isspace(*cptr)) cptr++;
         while (*cptr && isspace(*cptr)) cptr++;
         sptr = cptr;
      }

      if (DoNoProxy || DoProxyOnly)
      {
         /* skip straight on to the request field */
         while (*cptr && *cptr != '\"') cptr++;
         if (*cptr) *cptr++;
         /* skip over method */
         mptr = cptr;
         while (*cptr && !isspace(*cptr)) cptr++;
         if (*cptr) *cptr++;
         if (*cptr == '/')
         {
            NonProxyRecordCount++;
            FileNonProxyRecordCount++;
            if (DoProxyOnly) continue;
         }
         else
         if (*mptr == 'C' && !memcmp (mptr, "CONNECT", 7))
         {
            ProxyRecordCount++;
            FileProxyRecordCount++;
            if (DoNoProxy) continue;
         }
         else
         {
            /* look for what passes for a URL scheme */
            while (*cptr && *cptr != ':' && *cptr != '/' && *cptr != '\"')
               cptr++;
            if (cptr[0] == ':' && cptr[1] == '/' && cptr[2] == '/')
            {
               ProxyRecordCount++;
               FileProxyRecordCount++;
               if (DoNoProxy) continue;
            }
         }
         /* go back to the 'authorized user' field */
         cptr = sptr;
      }

      if (DoNoWasd)
      {
         if (!memcmp (cptr, "HTTPd ", 6))
         {
            /* authorized user matches, quite possible, look at request */
            cptr += 6;
            /* skip to the start of the request field */
            while (*cptr && *cptr != '\"') cptr++;
            if (*cptr == '\"')
            {
               /* looking for:
                   pre-8.2 '"POST <node>::HTTP<char>:<port>-<whatever>"'
                  post-8.2 '"POST /<node>::HTTP<char>:<port>-<whatever>"'
               */
               cptr++;
               if (!memcmp (cptr, "POST ", 5))
               {
                  /* looking more likely */
                  cptr += 5;
                  while (*cptr && *cptr != ':' && !isspace(*cptr)) cptr++;
                  if (*(unsigned short*)cptr == '::')
                  {
                     /* more and more likely :^) */
                     cptr += 2;
                     while (*cptr && *cptr != ':' && !isspace(*cptr)) cptr++;
                     if (*cptr == ':')
                     {
                        /* almost certain */
                        cptr++;
                        if (isdigit(*cptr))
                        {
                           while (*cptr && isdigit(*cptr)) cptr++;
                           if (*cptr == '-')
                           {
                              /* ok, I'm convinced! */
                              WasdRecordCount++;
                              FileWasdRecordCount++;
                              continue;
                           }
                        }
                     }
                  }
               }
            }
         }
         /* go back to the 'authorized user' field */
         cptr = sptr;
      }

      /* straight to the date field */
      while (*cptr && *cptr != '[') cptr++;
      if (*cptr != '[')
      {
         /* doesn't look like the start of a date field to me */
         DiscardRecordCount++;
         FileDiscardRecordCount++;
         continue;
      }
      cptr++;

      /* date/time */
      year = month = day = hour = min = sec = 0;
      day = atoi(cptr);
      while (*cptr && *cptr != '/') cptr++;
      if (*cptr) cptr++;
      for (month = 1; month <= 12; month++)
         if (strsame (MonthAbbrev[month], cptr, 3)) break;
      while (*cptr && *cptr != '/') cptr++;
      if (*cptr) cptr++;
      year = atoi(cptr);
      while (*cptr && *cptr != ':') cptr++;
      if (*cptr) cptr++;
      hour = atoi(cptr);
      while (*cptr && *cptr != ':') cptr++;
      if (*cptr) cptr++;
      min = atoi(cptr);
      while (*cptr && *cptr != ':') cptr++;
      if (*cptr) cptr++;
      sec = atoi(cptr);

      memset (&utm, 0, sizeof(utm));
      utm.tm_sec = sec;
      utm.tm_min = min;
      utm.tm_hour = hour;
      utm.tm_mday = day;
      utm.tm_mon = month - 1;
      utm.tm_year = year - 1900;
      UnixTime = mktime (&utm);

      if (Debug)
         fprintf (stdout, "%d %d %d %d %d %d = %d\n",
                  year, month, day, hour, min, sec, UnixTime);

      if (UnixTime == -1)
      {
         DiscardRecordCount++;
         FileDiscardRecordCount++;
         continue;
      }

      /* prepend the binary, longword time to the original buffer */
      *(int*)Line = UnixTime;

      /* plus four allows for the binary timestamp */
      RecordDsc.dsc$a_pointer = Line;
      RecordDsc.dsc$w_length = FileRab.rab$w_rsz + 4;

      if (VMSnok (status = sor$release_rec (&RecordDsc, 0)))
      {
         fprintf (stdout, "%%%s-E-SORT, release record\n-%s\n",
                  Utility, SysGetMsg(status)+1);
         exit (status | STS$M_INHIB_MSG);
      }
      InputRecordCount++;
      FileInputRecordCount++;
   }

   if (status != RMS$_EOF)
   {
      fprintf (stdout, "%%%s-E-GET, %s\n-%s\n",
               Utility, FileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   sys$close (&FileFab, 0, 0);

   if (DoVerbose)
   {
      fprintf (stdout, "records:%d discarded:%d",
               FileRecordCount, FileDiscardRecordCount);
      if (DoNoWasd) fprintf (stdout, " WASD:%d", FileWasdRecordCount);
      if (DoNoProxy || DoProxyOnly)
         fprintf (stdout, " proxy:%d non-proxy:%d",
                  FileProxyRecordCount, FileNonProxyRecordCount);
      fprintf (stdout, " input:%d\n", FileInputRecordCount);
   }

   return (SS$_NORMAL);
}

/****************************************************************************/
/*
Open an output file for write.  Retrieve each record from the sort-merge
utility routines.  Strip the leading binary time writing the textual component 
of the record to the output file.
*/ 

int WriteMergedLog ()

{
   int  status;
   unsigned short  ShortLength;
   char  ExpFileName [256],
         Line [MAX_LINE_LENGTH+4];
   struct FAB  FileFab;
   struct NAM  FileNam;
   struct RAB  FileRab;
   $DESCRIPTOR (RecordDsc, Line);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "WriteMergedLog()\n");

   FileFab = cc$rms_fab;
   FileFab.fab$l_dna = ".LOG";
   FileFab.fab$b_dns = 4;
   FileFab.fab$l_fop = FAB$M_DFW | FAB$M_SQO | FAB$M_TEF;
   FileFab.fab$l_fna = MergedFileName;
   FileFab.fab$b_fns = strlen(MergedFileName);
   FileFab.fab$b_fac = FAB$M_PUT;
   FileFab.fab$l_nam = &FileNam;
   FileFab.fab$b_rfm = FAB$C_STMLF;
   FileFab.fab$b_org = FAB$C_SEQ;
   FileFab.fab$b_rat = FAB$M_CR;
   
   FileNam = cc$rms_nam;
   FileNam.nam$l_esa = ExpFileName;
   FileNam.nam$b_ess = sizeof(ExpFileName)-1;

   if (VMSnok (status = sys$create (&FileFab, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-CREATE, %s\n-%s\n",
               Utility, MergedFileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   FileRab = cc$rms_rab;
   FileRab.rab$l_fab = &FileFab;
   FileRab.rab$b_mbc = 127;
   FileRab.rab$b_mbf = 2;
   FileRab.rab$l_rop = RAB$M_WBH;

   if (VMSnok (status = sys$connect (&FileRab, 0, 0)))
   {
      fprintf (stdout, "%%%s-E-CONNECT, %s\n-%s\n",
               Utility, MergedFileName, SysGetMsg(status)+1);
      exit (status | STS$M_INHIB_MSG);
   }

   *FileNam.nam$l_ver = '\0';

   for (;;)
   {
      if (VMSnok (status = sor$return_rec (&RecordDsc, &ShortLength, 0)))
      {
         if (status == SS$_ENDOFFILE) break;
         fprintf (stdout, "%%%s-E-SORT, return record\n-%s\n",
                  Utility, SysGetMsg(status)+1);
         exit (status | STS$M_INHIB_MSG);
      }
      OutputRecordCount++;

      FileRab.rab$l_rbf = Line + 4;
      FileRab.rab$w_rsz = ShortLength - 4;

      if (VMSnok (status = sys$put (&FileRab, 0, 0)))
      {
         fprintf (stdout, "%%%s-E-PUT, %s\n-%s\n",
                  Utility, ExpFileName, SysGetMsg(status)+1);
         exit (status | STS$M_INHIB_MSG);
      }
   }

   sys$close (&FileFab, 0, 0);

   if (DoVerbose)
      fprintf (stdout, "%s\nrecords: %d\n", ExpFileName, OutputRecordCount);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  status;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetParameters()\n");

   if ((clptr = getenv ("CALOGS$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/HELP", -1))
      {
         ShowHelp ();
         exit (SS$_NORMAL);
      }
      if (strsame (aptr, "/PROXY", 5))
      {
         DoProxyOnly = true;
         continue;
      }
      if (strsame (aptr, "/NOPROXY", 5))
      {
         DoNoProxy = true;
         continue;
      }
      if (strsame (aptr, "/NOWASD", 5))
      {
         DoNoWasd = true;
         continue;
      }
      if (strsame (aptr, "/OUTPUT=", 4))
      {
         while (*aptr && *aptr != '=') aptr++;
         if (*aptr) aptr++;
         strcpy (MergedFileName, aptr);
         continue;
      }
      if (strsame (aptr, "/QUIET", 4))
      {
         DoQuiet = true;
         continue;
      }
      if (strsame (aptr, "/SOFTWAREID", 4) ||
          strsame (aptr, "/VERSION", 5))
      {
         fprintf (stdout, "%%%s-I-SOFTWAREID, %s\n%s\n",
                  Utility, SOFTWAREID, CopyrightInfo);
         exit (SS$_NORMAL);
      }
      if (strsame (aptr, "/VERBOSE", 5))
      {
         DoVerbose = true;
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!LogFileSpec[0])
      {
         sptr = LogFileSpec;
         for (cptr = aptr; *cptr; *sptr++ = toupper(*cptr++));
         *sptr = '\0';
         continue;
      }

      if (!MergedFileName[0])
      {
         sptr = MergedFileName;
         for (cptr = aptr; *cptr; *sptr++ = toupper(*cptr++));
         *sptr = '\0';
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

BOOL strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

/****************************************************************************/
/*
Return a pointer to the textual meaning of the specific VMS status value.
*/

char* SysGetMsg (int StatusValue)

{
   static char  Message [256];
   static $DESCRIPTOR (MessageDsc, Message);

   int  status;
   short int  Length;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SysGetMsg() %%X%08.08X\n", StatusValue);

   status = sys$getmsg (StatusValue, &Length, &MessageDsc, 0, 0); 
   if (VMSnok (status)) exit (status);
   Message[Length] = '\0';
   return (Message);
}

/*****************************************************************************/
/*
*/

ShowHelp ()

{
   fprintf (stdout,
"Usage for Consolidate Access LOGS (%s)\n\
\n\
Consolidate Access LOGs merges multiple HTTP server common and combined format\n\
access logs into a single log file with records in time-order.  Due to the\n\
granularity of HTTP server entry timestamps (one second) the records are sorted\n\
to the one second but not within the one second.  The /NOPROXY, /PROXY and\n\
/NOWASD qualifiers allow the particular class of record to be excluded from the\n\
merged file.\n\
\n\
$ CALOGS <log-file-spec> [<output-file-name>] [<qualifiers>]\n\
\n\
/HELP /NOPROXY /NOWASD /OUTPUT=<filename> /PROXY /QUIET /VERBOSE /VERSION\n\
\n\
Usage examples:\n\
\n\
$ CALOGS == \"$dir:CALOGS\"\n\
$ CALOGS HT_LOGS:*200205*.LOG 2002_MAY.LOG\n\
$ CALOGS /VERBOSE HT_LOGS:\n\
$ CALOGS /NOWASD HT_LOGS:*200206*.LOG_* 2002_JUNE.LOG\n\
$ CALOGS /PROXY /NOWASD HT_LOGS:*2002*.LOG 2002_PROXY.LOG\n\
\n",
      SOFTWAREID);
}

/*****************************************************************************/

