/*************************************************************************
** ^FILE: unix_args.c - parse Unix argument vectors
**
** ^DESCRIPTION:
**    This file contains the routines used to parse Unix argument
**    vectors and to print Unix usage messages.
**
** ^HISTORY:
**    12/05/91 	Brad Appleton 	<brad@ssd.csd.harris.com>
**    - added #ifdef POSIX_SOURCE to use "--" instead of "+" as
**      GNU conformant prefix for long options.
**
**    08/27/91 	Earl Chew 	<cechew@bruce.cs.monash.edu.au>
**    - Use ProgNameLen when accessing ProgName
**    - Use get_argdesc() to access description
**
**    01/02/91 	Brad Appleton 	<brad@ssd.csd.harris.com>
**    - Added structured block comments
**    - Added optional arguments to keywords and options
**
**    --/--/--	Peter da Silva	<peter@ferranti.com>	
**
**    --/--/--	Eric P. Allman	<eric@Berkeley.EDU> 	Created
***^^**********************************************************************/

#include <ctype.h>
#include <useful.h>
#include "strfuncs.h"
#include "pgopen.h"
#include "exit_codes.h"

#define PARSEARGS_PRIVATE   /* include private definitions */
#include "parseargs.h"

EXTERN  VOID  syserr       ARGS((const char *, ...));
EXTERN  VOID  usrerr       ARGS((const char *, ...));
EXTERN  char *getenv       ARGS((const char *));
EXTERN  VOID  get_winsize  ARGS((int, int *, int *));

VERSIONID("$Header: parseargs.c,v 2.1 89/12/30 20:59:48 eric Exp $");


/***************************************************************************
** ^GLOBAL-VARIABLE: Usage_Requested
**
** ^VISIBILITY:
**    static-global (visible to all functions in this file).
**
** ^DESCRIPTION:
**    Indicates whether a usage message was requested by the user
**    (as opposed to triggered by a syntax error).  If the message
**    is requested by the user then it is always printed in verbose
**    mode and does not return an error-status-code.
***^^**********************************************************************/
static  BOOL  Usage_Requested = (BOOL) FALSE;


   /* macros to detect an option/keyword -- watch out for side effects!! */
#define isOPT(s)  \
   ( !BTEST(cmd_flags(cmd), pa_KWDSONLY)  && \
     !BTEST(cmd_state(cmd), ps_NOFLAGS)  && \
     (*s == c_OPT_PFX)  &&  *(s+1) \
   )

#ifndef POSIX_SOURCE
#define isKWD(s)  \
   ( !BTEST(cmd_flags(cmd), pa_OPTSONLY)  && \
     !BTEST(cmd_state(cmd), ps_NOFLAGS)  && \
     (*s == c_KWD_PFX)  &&  *(s+1) \
   )
#else
#define isKWD(s)  \
   ( !BTEST(cmd_flags(cmd), pa_OPTSONLY)  && \
     !BTEST(cmd_state(cmd), ps_NOFLAGS)  && \
     (*s == c_OPT_PFX)  &&  (*(s+1) == c_OPT_PFX)  &&  *(s+2) \
   )
#endif


/***************************************************************************
** ^FUNCTION: unix_parse - parse Unix arg-vectors
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   int unix_parse( argv, argd )
/*  
** ^PARAMETERS:
*/
   char *argv[];
/*    -- the vector of string arguments from the command-line
*/
   ARGDESC argd[];
/*    -- the programmer description of the command and its args
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Unix_parse will parse the arguments in the given vector of strings,
**    assign the corresponding values to the command-line arguments specified
**    in argd, and check the syntax of the command-line.
**
** ^REQUIREMENTS:
**    The final element in argv must be a NULL pointer.
**
** ^SIDE-EFFECTS:
**    argd is modified according to the command-line description and parameters
**
** ^RETURN-VALUE:
**    pe_SUCCESS (0) if no errors are encountered
**    pe_SYSTEM (-1) if a system error is encountered
**    pe_SYNTAX if a syntax error is encountered
**
** ^ALGORITHM:
**    - for each command-line argument
**       - attempt to match the argument as a keyword
**       - if it is a keyword argument
**          - record and convert its value (if any)
**         else attempt to match the argument as an option
**         if it is an option
**          - record and convert its value (if any)
**         else it is a positional parameter
**          - record and convert its value (if any)
**         else there are too many arguments
**          - return pe_SYNTAX
**         end-if
**       end-for
***^^**********************************************************************/
#ifdef __ANSI_C__
   int unix_parse( char *argv[], ARGDESC argd[] )
#endif
{
   register ARGDESC *ad, *args, *cmd;
   register char **av = argv;
   register char *p;
   argName_t  name;
   argMask_t  flags;
   int  parse_error = pe_SUCCESS;
   BOOL  ad_okay, is_match = FALSE;

   if ( !argd )  return  parse_error;

      /* initialize command-structure */
   if ( !CMD_isINIT(argd) )  init_args( argd );
   cmd = argd;

   while ( av  &&  (p = *av++) ) {
      if ( isKWD(p) ) {  /* we have a keyword here */
         char *s, c = '\0';

#ifndef POSIX_SOURCE
         /* check for `++' to end flags */
         if ( *(p+1) == c_KWD_PFX  &&  !*(p+2) ) {
            BSET( cmd_state(cmd), ps_NOFLAGS );
            cmd_list(cmd) = ARGDESCNULL;
            continue;
         }
#endif

            /* get past prefix and look for possible argument */
#ifdef POSIX_SOURCE
         ++p;
#endif
         s = strpbrk(++p, s_ARG_SEP);
         if(s) {
            c = *s;
            *s++ = '\0';
         }

         is_match = FALSE;
         for ( args = argd ; args  &&  !is_match ; args = cmd_defargs(args) ) {
            for ( ad = ARG_FIRST(args) ; !ARG_isEND(ad) ; ARG_ADVANCE(ad) ) {
               if ( arg_type(ad) == argDummy )  continue;

               if ( !ARG_isPOSONLY(ad)  &&  match(p, arg_sname(ad)) == 0 ) {
                  is_match = TRUE;
                  break;
               }/*if*/
            }
         }

         if ( c )  *(s-1) = c;  /* restore the equal sign */

         if ( !is_match ) {
#ifndef POSIX_SOURCE
            usrerr("option %c%s unknown", c_KWD_PFX, p);
#else
            usrerr("option %c%c%s unknown", c_OPT_PFX, c_OPT_PFX, p);
#endif
            parse_error = pe_SYNTAX;
            cmd_list(cmd) = ARGDESCNULL;
            continue;
         }

         /* reset the argument flags - if this arg was already given, some
         ** of its flags may be set to indicate how it was given before. 
         ** we need to know how it was given now (but save the old ones
         ** just in case the new one fails).
         */
         flags = arg_flags(ad);
         if ( ARG_isGIVEN(ad) ) {
            BCLEAR( arg_flags(ad), ARGVALSEP | ARGKEYWORD );
            if ( !ARG_isMULTIVAL(ad) )  BCLEAR( arg_flags(ad), ARGVALGIVEN );
         }

         BSET( arg_flags(ad), ARGKEYWORD );

         if( ARG_isMULTIVAL(ad) ) { /* we matched a list (or a vector) */
            cmd_list(cmd) = ad;
         }
         else {
            cmd_list(cmd) = ARGDESCNULL;
         }

            /* if usage - just print usage and exit */
         if ( arg_type(ad) == argUsage ) {
            Usage_Requested = TRUE;
            usage(argd);
            exit(exit_USAGE);
         }

            /* ARGNOVALs are special, having no value */
         if ( ! ARG_isVALTAKEN(ad) ) {
            ad_okay = HANDLE(ad, s, cmd_flags(cmd));
            if ( !ad_okay ) {
               arg_flags(ad) = flags;
               parse_error = pe_SYNTAX;
            }
            else {
               BSET( arg_flags(ad), ARGGIVEN );
               ad = ARGDESCNULL;
            }
            continue;
         }/*if ARGNOVAL*/

            /* now get the real value */
         if (!s) {
            s = *av++;
            if ( !s  ||  isOPT(s)  ||  isKWD(s) ) {
               if ( ARG_isVALOPTIONAL(ad) ) {
                  BSET( arg_flags(ad), ARGGIVEN );
               }
               else {
                  (VOID) get_kwdname( arg_sname(ad), name );
                  usrerr("option %c%s requires an argument", c_KWD_PFX, name);
                  arg_flags(ad) = flags;
                  parse_error = pe_SYNTAX;
               }

               av--;
               continue;
            }/*if arg*/
            BSET( arg_flags(ad), ARGVALSEP );
         }/*if empty*/

            /* try to convert the type */
         ad_okay = HANDLE(ad, s, cmd_flags(cmd));
         if ( !ad_okay ) {
            arg_flags(ad) = flags;
            parse_error = pe_SYNTAX;
         }
         else {
            BSET( arg_flags(ad), ARGGIVEN | ARGVALGIVEN );
         }

         continue;
      }/*if keyword*/

      else if ( isOPT(p) ) {
         p++;  /* skip over option prefix */

            /* check for `--' to end flags */
         if ( *p == c_OPT_PFX  &&  !*(p+1) ) {
            BSET( cmd_state(cmd), ps_NOFLAGS );
            cmd_list(cmd) = ARGDESCNULL;
            continue;
         }

         /* We have a flag argument;
         ** remember that in the case of single character keywords,
         ** the conversion function (ad_type) tells us how many characters
         ** were used. We need that information to decide how many 
         ** characters to skip before the next iteration of the while loop.
         */
         while (*p) {

               /* find the flag in the list */
            is_match = FALSE;
            for (args = argd; args  &&  !is_match ; args = cmd_defargs(args)) {
               for ( ad = ARG_FIRST(args) ; !ARG_isEND(ad) ; ARG_ADVANCE(ad) ) {
                  register char c1 = arg_cname(ad);
                  register char c2 = *p;

                  if ( arg_type(ad) == argDummy )   continue;
                  if ( ARG_isPOSONLY(ad) )   continue;

                  if ( BTEST(cmd_flags(cmd), pa_ANYCASE) ) {
                     c1 = TOUPPER( c1 );
                     c2 = TOUPPER( c2 );
                  }/*if*/

                  if ( c1 == c2 ) {
                     is_match = TRUE;
                     break;
                  }/*if*/
               }
            }
            if ( !is_match ) {
                  usrerr("option %c%c unknown", c_OPT_PFX, *p++);
                  parse_error = pe_SYNTAX;
                  cmd_list(cmd) = ARGDESCNULL;
                  continue;
            }/* if unknown-option */

            /* reset the argument flags - if this arg was already given, some
            ** of its flags may be set to indicate how it was given before.
            ** we need to know how it was given now (but save the old ones
            ** just in case the new one fails).
            */
            flags = arg_flags(ad);
            if ( ARG_isGIVEN(ad) ) {
               BCLEAR( arg_flags(ad), ARGVALSEP | ARGKEYWORD );
               if ( !ARG_isMULTIVAL(ad) )  BCLEAR( arg_flags(ad), ARGVALGIVEN );
            }

            if( ARG_isMULTIVAL(ad) ) {
               cmd_list(cmd) = ad;  /* we matched a list (or a vector) */
            }
            else {
               cmd_list(cmd) = ARGDESCNULL;
            }

               /* move p up to point to the (possible) value */
            p++;

            /* if usage - just print usage and exit */
            if (arg_type(ad) == argUsage) {
               Usage_Requested = TRUE;
               usage(argd);
               exit(exit_USAGE);
            }

               /* ARGNOVALs are special, having no value */
            if (! ARG_isVALTAKEN(ad)) {
               ad_okay = HANDLE(ad, p, cmd_flags(cmd));

               if ( !ad_okay ) {
                  arg_flags(ad) = flags;
                  parse_error = pe_SYNTAX;
               }/*if*/
               else {
                  BSET( arg_flags(ad), ARGGIVEN );
                  ad = ARGDESCNULL;
                  if ( ad_okay < 0 )  p -= ad_okay;
               }/*else*/

               continue;
            }/*if*/

               /* now get the real value */
            if ( !(*p) ) {
               p = *av++;
               if ( !p  ||  isOPT(p)  ||  isKWD(p) ) {
                  if ( ARG_isVALOPTIONAL(ad) ) {
                     BSET( arg_flags(ad), ARGGIVEN );
                  }
                  else {
                     (VOID) get_argname(arg_sname(ad), name);
                     usrerr( "%s required for %c%c flag",
                             name, c_OPT_PFX, arg_cname(ad) );
                     arg_flags(ad) = flags;
                     parse_error = pe_SYNTAX;
                  }/*else*/

                  av--;
                  break;
               }/*if arg*/
               BSET( arg_flags(ad), ARGVALSEP );
            }/*if empty*/

               /* try to convert the type */
            ad_okay = HANDLE(ad, p, cmd_flags(cmd));
            if ( !ad_okay ) {
               arg_flags(ad) = flags;
               parse_error = pe_SYNTAX;
               p += strlen(p);
            }/*if*/
            else {
               BSET( arg_flags(ad), ARGGIVEN | ARGVALGIVEN );
               if ( ad_okay < 0  &&  !ARG_isVALSEPARATE(ad) ) {
                  p -= ad_okay;
               }
               else {
                  p += strlen(p);
               }
            }/*else*/

         }/*while*/
      }/*elif option*/
      else {
            /* parsing a list of arguments */
         if ( cmd_list(cmd) ) {  /* we're in the middle of a list */
            ad = cmd_list(cmd);

            /* reset the argument flags - if this arg was already given, some
            ** of its flags may be set to indicate how it was given before.
            ** we need to know how it was given now (but save the old ones
            ** just in case the new one fails).
            */
            flags = arg_flags(ad);
            if ( ARG_isGIVEN(ad) ) {
               BCLEAR( arg_flags(ad), ARGVALSEP | ARGKEYWORD );
            }

            BSET( arg_flags(ad), ARGVALSEP );

            ad_okay = HANDLE(ad, p, cmd_flags(cmd));
            if ( !ad_okay ) {
               arg_flags(ad) = flags;
               parse_error = pe_SYNTAX;
            }

            continue;
         }
            /* positional argument */
         is_match = FALSE;
         for (args = argd; args  &&  !is_match ; args = cmd_defargs(args)) {
            for ( ad = ARG_FIRST(args) ; !ARG_isEND(ad) ; ARG_ADVANCE(ad) ) {
               if (arg_type(ad) == argDummy)  continue;

               if ( ARG_isPOSITIONAL(ad)  &&
                    (!ARG_isGIVEN(ad) ||  ARG_isMULTIVAL(ad)) ) {
                  is_match = TRUE;
                  break;
               }/*if*/
            }
         }

         if ( !is_match ) {
            usrerr("too many arguments");
            parse_error = pe_SYNTAX;
            continue;
         }

         /* reset the argument flags - if this arg was already given, some
         ** of its flags may be set to indicate how it was given before.
         ** we need to know how it was given now (but save the old ones
         ** just in case the new one fails).
         */
         flags = arg_flags(ad);
         if ( ARG_isGIVEN(ad) ) {
            BCLEAR( arg_flags(ad), ARGVALSEP | ARGKEYWORD );
            if ( !ARG_isMULTIVAL(ad) )  BCLEAR( arg_flags(ad), ARGVALGIVEN );
         }

         if ( ARG_isMULTIVAL(ad) ) {  /* we positionally matched a list */
            cmd_list(cmd) = ad;
         }

         /* if FLAGS1ST is set then first positional marks end-of-flags */
         if ( BTEST(cmd_flags(cmd), pa_FLAGS1ST) ) {
            BSET( cmd_state(cmd), ps_NOFLAGS );
         }

         BSET( arg_flags(ad), ARGVALSEP );

            /* try to convert */
         ad_okay = HANDLE(ad, p, cmd_flags(cmd));
         if ( !ad_okay ) {
            arg_flags(ad) = flags;
            parse_error = pe_SYNTAX;
         }
         else {
            BSET( arg_flags(ad), ARGGIVEN | ARGVALGIVEN );
         }
      }/*else*/
   }/*while*/

   return  parse_error;
}


/***************************************************************************
** ^FUNCTION: fmtarg - format command-argument syntax
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   static int fmtarg( ad, buf, usgflags )
/*
** ^PARAMETERS:
*/
   ARGDESC *ad;
/*    -- pointer to the argument to format
*/
   char *buf;
/*    -- character buffer to hold the formatted result
*/
   argMask_t usgflags;
/*    -- set of bitmasks corresponding to the value of the user's USAGECNTL
**       environment variable
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Fmtarg will determine the proper command-line syntax for the
**    given argument and write the result to the given buffer.
**
** ^REQUIREMENTS:
**    buf must be large enough to hold the formatted result (100 characters
**    should do the trick).
**
** ^SIDE-EFFECTS:
**    buf is overwritten.
**
** ^RETURN-VALUE:
**    The number of printable characters in the argument-syntax-string
**
** ^ALGORITHM:
**    Print argument usage based on whether or not the argument is
**    positional, hidden, multi-valued (list or vector), etc ....
**    Optional arguments and values are enclosed in square braces.
**
**    Any syntax biases reflected in usgflags will be used.
***^^**********************************************************************/
#ifdef __ANSI_C__
   static int fmtarg ( const ARGDESC *ad, char *buf, argMask_t usgflags )
#endif
{
   /* buf must already be large enough */
   char *pos;
   argName_t   name, keyword;

   (VOID) get_argname( arg_sname(ad), name );

   if (ARG_isPOSITIONAL(ad)) {
      sprintf( buf, "<%s>", name );
   }
   else {
      (VOID) get_kwdname( arg_sname(ad), keyword );

      if ( isupper(arg_cname(ad))  &&  toupper(*keyword) == arg_cname(ad) ) {
         *keyword = toupper(*keyword);
      }

      if ( !(usgflags & usg_LONGOPTS) ) {
         sprintf( buf, "%c%c", c_OPT_PFX, arg_cname(ad) );
      }
      else if ( !(usgflags & usg_OPTS) ) {
#ifndef POSIX_SOURCE
         sprintf( buf, "%c%s", c_KWD_PFX, keyword );
#else
         sprintf( buf, "%c%c%s", c_OPT_PFX, c_OPT_PFX, keyword );
#endif
      }
      else  {  /* use both */
#ifndef POSIX_SOURCE
         sprintf( buf, "%c%c|%c%s", c_OPT_PFX, arg_cname(ad),
                                    c_KWD_PFX, keyword );
#else
         sprintf( buf, "%c%c|%c%c%s", c_OPT_PFX, arg_cname(ad),
                                      c_OPT_PFX, c_OPT_PFX, keyword );
#endif
      }

      pos = buf + strlen(buf);

      if ( ARG_isVALTAKEN(ad)  &&  !ARG_isBOOLEAN(ad) &&  !ARG_isPSEUDOARG(ad) )
      {
         *(pos++) = ' ';

         if  (ARG_isVALOPTIONAL(ad)) {
            sprintf( pos, "[<%s>]", name);
         }
         else {
            sprintf( pos, "<%s>", name );
         }
      }/*if*/
   }/*else*/

   return  strlen(buf);
}


/***************************************************************************
** ^FUNCTION: unix_usage - print a usage message
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   VOID unix_usage( argd, usage_flags )
/*
** ^PARAMETERS:
*/
   ARGDESC *argd;
/*    -- the command-descriptor array
*/
   argMask_t usage_flags;
/*    -- flags set by $USAGECNTL
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Unix_usage will print the Unix command-line usage of the given
**    command on standard diagnostic output (stderr). The content of the
**    usage message is controlled by the bitmasks in usage_flags which
**    correspond to the settings in the user's USAGECNTL variable.
**
** ^REQUIREMENTS:
**    argd should be a non-null command-line argument-descriptor array
**
** ^SIDE-EFFECTS:
**    Prints on stderr.
**
** ^RETURN-VALUE:
**    None.
**
** ^ALGORITHM:
**    - if no usage is desired then exit
**    - if paging is requested print to the pager instead of stderr
**    - print the command-line syntax
**    - if the description is requested print it
**    - if verbose mode is requested, print the description of each argument
***^^**********************************************************************/
#ifdef __ANSI_C__
   void unix_usage ( const ARGDESC *argd, argMask_t usage_flags )
#endif
{
   register CONST ARGDESC *ad, *args, *cmd;
   int  max_cols = 80, max_lines  = 24;
   int  ll, margin, options, longest, positionals;
   BOOL first = TRUE;
   FILE *fp;

   if ( !argd )  return;

      /* initialize command-structure */
   if ( !CMD_isINIT(argd) )  init_args( (ARGDESC *)argd );
   cmd = argd;

      /* force verbose-mode if requested */
   if ( Usage_Requested )   BSET( usage_flags, usg_VERBOSE );

   if ( BTEST(usage_flags, usg_NONE) )  return;

   fp = ( BTEST(usage_flags, usg_PAGED) )
      ? pgopen( stderr, getenv("USAGE_PAGER") )
      : stderr;

      /* get screen size */
   get_winsize( fileno(fp), &max_lines, &max_cols );

   fprintf(fp, "Usage: %.*s", ProgNameLen, (ProgName) ? ProgName : "");

   ll = ProgNameLen + 7;
   margin = ll + 1;
   longest = 0;

      /* print Synopsis */
   for ( positionals = 0 ; positionals < 2 ; positionals++ ) {
      for ( args = argd ; args ; args = cmd_defargs(args) ) {
         for ( ad = ARG_FIRST(args) ; !ARG_isEND(ad) ; ARG_ADVANCE(ad) ) {
            argName_t  buf;
            int pl;

               /* don't display hidden arguments */
            if ( ARG_isHIDDEN(ad) )  continue;
            if ( !positionals  &&  ARG_isPOSITIONAL(ad) )  continue;
            if ( positionals  &&  !ARG_isPOSITIONAL(ad) )  continue;

               /* figure out how wide this parameter is (for printing) */
            pl = fmtarg(ad, buf, usage_flags);

            if ( pl > longest)  longest = pl;

            if  ( ARG_isMULTIVAL(ad) ) {
               strcat( buf, "..." );
               pl += 3;
            }
            if ( !ARG_isREQUIRED(ad) ) {
               pl += 2;
            }

            /* see if this will fit */
            if ( (ll + pl + 1) > (max_cols - first) ) {
                  /* no... start a new line */
               fprintf(fp, "\n%*s", margin, "");
               ll = margin;
            }
            else {
                  /* yes... just throw in a space */
               fputc(' ', fp);
               ++ll;
            }
            ll += pl;

               /* show the argument */
            if ( !ARG_isREQUIRED(ad) )  fputc('[', fp);
            fprintf(fp, buf);
            if ( !ARG_isREQUIRED(ad) )  fputc(']', fp);

            first = FALSE;  /* not first line anymore */
         }/*for each ad */
      }/* for each argd */
   }/* for each parm-type */

   fputc('\n', fp);

   if ( BTEST(usage_flags, usg_DESCRIPTION) ) {
      CONST char *description = cmd_description(cmd);

      if ( description  &&  *description ) {
         fprintf( fp, "Description:\n" );
         indent_para(fp, max_cols, 8, "", 0, description, 0);
         fputc( '\n', fp );
      }
   }/*if*/

   if ( !BTEST(usage_flags, usg_VERBOSE) )  {
      if ( pgactive(fp) )  (VOID) pgclose( fp );
      return;
   }

   options = 0;

      /* print Argument descriptions */
   for ( positionals = 0 ; positionals < 2 ; positionals++ ) {
      for ( args = argd ; args ; args = cmd_defargs(args) ) {
         for ( ad = ARG_FIRST(args) ; !ARG_isEND(ad) ; ARG_ADVANCE(ad) ) {
            argName_t  buf;
            char  *desc;
            int  desclen;

               /* don't display hidden arguments */
            if ( ARG_isHIDDEN(ad) )  continue;
            if ( !positionals  &&  ARG_isPOSITIONAL(ad) )  continue;
            if ( positionals  &&  !ARG_isPOSITIONAL(ad) )  continue;

            if ( !options++ )   fprintf(fp, "Options/Arguments:\n");
            fmtarg(ad, buf, usage_flags);
            desc = get_argdesc(arg_description(ad), &desclen);
            indent_para( fp, max_cols, 8, buf, longest+2, desc, desclen );
         }/*for each ad */
      }/* for each argd */
   }/* for each parm-type */

   if ( pgactive(fp) )  (VOID) pgclose( fp );
}

