/* $Id: shhopt.c 916 2002-07-22 17:06:19Z schoenw $ */
/*------------------------------------------------------------------------
 |  FILE            shhopt.c
 |
 |  DESCRIPTION     Functions for parsing command line arguments. Values
 |                  of miscellaneous types may be stored in variables,
 |                  or passed to functions as specified.
 |
 |  REQUIREMENTS    Some systems lack the ANSI C -function strtoul. If your
 |                  system is one of those, you'll ned to write one yourself,
 |                  or get the GNU liberty-library (from prep.ai.mit.edu).
 |
 |  WRITTEN BY      Sverre H. Huseby <sverrehu@online.no>
 +----------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>

#include "shhopt.h"

/*-----------------------------------------------------------------------+
|  PRIVATE DATA                                                          |
+-----------------------------------------------------------------------*/

static void optFatalFunc(const char *, ...);
static void (*optFatal)(const char *format, ...) = optFatalFunc;

/*-----------------------------------------------------------------------+
|  PRIVATE FUNCTIONS                                                     |
+-----------------------------------------------------------------------*/

/*------------------------------------------------------------------------
 |  NAME          optFatalFunc
 |
 |  FUNCTION      Show given message and abort the program.
 |
 |  INPUT         format, ...
 |                        Arguments used as with printf().
 |
 |  RETURNS       Never returns. The program is aborted.
 */
void
optFatalFunc(const char *format, ...)
{
    va_list ap;

    fflush(stdout);
    va_start(ap, format);
    vfprintf(stderr, format, ap);
    va_end(ap);
    exit(99);
}

/*------------------------------------------------------------------------
 |  NAME          optStructCount
 |
 |  FUNCTION      Get number of options in a optStruct.
 |
 |  INPUT         opt     array of possible options.
 |
 |  RETURNS       Number of options in the given array.
 |
 |  DESCRIPTION   Count elements in an optStruct-array. The strcture must
 |                be ended using an element of type OPT_END.
 */
static int
optStructCount(optStruct opt[])
{
    int ret = 0;

    while (opt[ret].type != OPT_END)
        ++ret;
    return ret;
}

/*------------------------------------------------------------------------
 |  NAME          optMatch
 |
 |  FUNCTION      Find a matching option.
 |
 |  INPUT         opt     array of possible options.
 |                s       string to match, without `-' or `--'.
 |                lng     match long option, otherwise short.
 |
 |  RETURNS       Index to the option if found, -1 if not found.
 |
 |  DESCRIPTION   Short options are matched from the first character in
 |                the given string.
 */
static int
optMatch(optStruct opt[], const char *s, int lng)
{
    int        nopt, q;
    const char *p;
    size_t     matchlen = 0;

    nopt = optStructCount(opt);
    if (lng) {
	if ((p = strchr(s, '=')) != NULL)
	    matchlen = p - s;
	else
	    matchlen = strlen(s);
    }
    for (q = 0; q < nopt; q++) {
	if (lng) {
	    if (!opt[q].longName)
		continue;
	    if (strncmp(s, opt[q].longName, matchlen) == 0)
		return q;
	} else {
	    if (!opt[q].shortName)
		continue;
	    if (*s == opt[q].shortName)
		return q;
	}
    }
    return -1;
}

/*------------------------------------------------------------------------
 |  NAME          optString
 |
 |  FUNCTION      Return a (static) string with the option name.
 |
 |  INPUT         opt     the option to stringify.
 |                lng     is it a long option?
 |
 |  RETURNS       Pointer to static string.
 */
static char *
optString(optStruct *opt, int lng)
{
    static char ret[31];

    if (lng) {
	strcpy(ret, "--");
	strncpy(ret + 2, opt->longName, 28);
    } else {
	ret[0] = '-';
	ret[1] = opt->shortName;
	ret[2] = '\0';
    }
    return ret;
}

/*------------------------------------------------------------------------
 |  NAME          optNeedsArgument
 |
 |  FUNCTION      Check if an option requires an argument.
 |
 |  INPUT         opt     the option to check.
 |
 |  RETURNS       Boolean value.
 */
static int
optNeedsArgument(optStruct *opt)
{
    return opt->type == OPT_STRING
	|| opt->type == OPT_INT
	|| opt->type == OPT_UINT
	|| opt->type == OPT_LONG
	|| opt->type == OPT_ULONG;
}

/*------------------------------------------------------------------------
 |  NAME          argvRemove
 |
 |  FUNCTION      Remove an entry from an argv-array.
 |
 |  INPUT         argc    pointer to number of options.
 |                argv    array of option-/argument-strings.
 |                i       index of option to remove.
 |
 |  OUTPUT        argc    new argument count.
 |                argv    array with given argument removed.
 */
static void
argvRemove(int *argc, char *argv[], int i)
{
    if (i >= *argc)
        return;
    while (i++ < *argc)
        argv[i - 1] = argv[i];
    --*argc;
}

/*------------------------------------------------------------------------
 |  NAME          optExecute
 |
 |  FUNCTION      Perform the action of an option.
 |
 |  INPUT         opt     array of possible options.
 |                arg     argument to option, if it applies.
 |                lng     was the option given as a long option?
 |
 |  RETURNS       Nothing. Aborts in case of error.
 */
static void
optExecute(optStruct *opt, char *arg, int lng)
{
    switch (opt->type) {
      case OPT_FLAG:
	if (opt->flags & OPT_CALLFUNC)
	    ((void (*)(void)) opt->arg)();
	else
	    *((int *) opt->arg) = 1;
	break;

      case OPT_STRING:
	if (opt->flags & OPT_CALLFUNC)
	    ((void (*)(char *)) opt->arg)(arg);
	else
	    *((char **) opt->arg) = arg;
	break;

      case OPT_INT:
      case OPT_LONG: {
	  long tmp;
	  char *e;
	  
	  tmp = strtol(arg, &e, 10);
	  if (*e)
	      optFatal("invalid number `%s'\n", arg);
	  if (errno == ERANGE
	      || (opt->type == OPT_INT && (tmp > INT_MAX || tmp < INT_MIN)))
	      optFatal("number `%s' to `%s' out of range\n",
		       arg, optString(opt, lng));
	  if (opt->type == OPT_INT) {
	      if (opt->flags & OPT_CALLFUNC)
		  ((void (*)(int)) opt->arg)((int) tmp);
	      else
		  *((int *) opt->arg) = (int) tmp;
	  } else /* OPT_LONG */ {
	      if (opt->flags & OPT_CALLFUNC)
		  ((void (*)(long)) opt->arg)(tmp);
	      else
		  *((long *) opt->arg) = tmp;
	  }
	  break;
      }
	
      case OPT_UINT:
      case OPT_ULONG: {
	  unsigned long tmp;
	  char *e;
	  
	  tmp = strtoul(arg, &e, 10);
	  if (*e)
	      optFatal("invalid number `%s'\n", arg);
	  if (errno == ERANGE
	      || (opt->type == OPT_UINT && tmp > UINT_MAX))
	      optFatal("number `%s' to `%s' out of range\n",
		       arg, optString(opt, lng));
	  if (opt->type == OPT_UINT) {
	      if (opt->flags & OPT_CALLFUNC)
		  ((void (*)(unsigned)) opt->arg)((unsigned) tmp);
	      else
		  *((unsigned *) opt->arg) = (unsigned) tmp;
	  } else /* OPT_ULONG */ {
	      if (opt->flags & OPT_CALLFUNC)
		  ((void (*)(unsigned long)) opt->arg)(tmp);
	      else
		  *((unsigned long *) opt->arg) = tmp;
	  }
	  break;
      }

      default:
	break;
    }
}

/*-----------------------------------------------------------------------+
|  PUBLIC FUNCTIONS                                                      |
+-----------------------------------------------------------------------*/

/*------------------------------------------------------------------------
 |  NAME          optSetFatalFunc
 |
 |  FUNCTION      Set function used to display error message and exit.
 |
 |  SYNOPSIS      #include "shhopt.h"
 |                void optSetFatalFunc(void (*f)(const char *, ...));
 |
 |  INPUT         f       function accepting printf()'like parameters,
 |                        that _must_ abort the program.
 */
void
optSetFatalFunc(void (*f)(const char *, ...))
{
    optFatal = f;
}

/*------------------------------------------------------------------------
 |  NAME          optParseOptions
 |
 |  FUNCTION      Parse commandline options.
 |
 |  SYNOPSIS      #include "shhopt.h"
 |                void optParseOptions(int *argc, char *argv[],
 |                                     optStruct opt[], int allowNegNum);
 |
 |  INPUT         argc    Pointer to number of options.
 |                argv    Array of option-/argument-strings.
 |                opt     Array of possible options.
 |                allowNegNum
 |                        a negative number is not to be taken as
 |                        an option.
 |
 |  OUTPUT        argc    new argument count.
 |                argv    array with arguments removed.
 |
 |  RETURNS       Nothing. Aborts in case of error.
 |
 |  DESCRIPTION   This function checks each option in the argv-array
 |                against strings in the opt-array, and `executes' any
 |                matching action. Any arguments to the options are
 |                extracted and stored in the variables or passed to
 |                functions pointed to by entries in opt.
 |
 |                Options and arguments used are removed from the argv-
 |                array, and argc is decreased accordingly.
 |
 |                Any error leads to program abortion.
 */
void
optParseOptions(int *argc, char *argv[], optStruct opt[], int allowNegNum)
{
    int  ai,        /* argv index. */
         optarg,    /* argv index of option argument, or -1 if none. */
         mi,        /* Match index in opt. */
         done;
    char *arg,      /* Pointer to argument to an option. */
         *o,        /* pointer to an option character */
         *p;

    /*
     *  Loop through all arguments.
     */
    for (ai = 0; ai < *argc; ) {
	/*
	 *  "--" indicates that the rest of the argv-array does not
	 *  contain options.
	 */
	if (strcmp(argv[ai], "--") == 0) {
            argvRemove(argc, argv, ai);
	    break;
	}

	if (allowNegNum && argv[ai][0] == '-' && isdigit(argv[ai][1])) {
	    ++ai;
	    continue;
	} else if (strncmp(argv[ai], "--", 2) == 0) {
	    /* long option */
	    /* find matching option */
	    if ((mi = optMatch(opt, argv[ai] + 2, 1)) < 0)
		optFatal("unrecognized option `%s'\n", argv[ai]);

	    /* possibly locate the argument to this option. */
	    arg = NULL;
	    if ((p = strchr(argv[ai], '=')) != NULL)
		arg = p + 1;
	    
	    /* does this option take an argument? */
	    optarg = -1;
	    if (optNeedsArgument(&opt[mi])) {
		/* option needs an argument. find it. */
		if (!arg) {
		    if ((optarg = ai + 1) == *argc)
			optFatal("option `%s' requires an argument\n",
				 optString(&opt[mi], 1));
		    arg = argv[optarg];
		}
	    } else {
		if (arg)
		    optFatal("option `%s' doesn't allow an argument\n",
			     optString(&opt[mi], 1));
	    }
	    /* perform the action of this option. */
	    optExecute(&opt[mi], arg, 1);
	    /* remove option and any argument from the argv-array. */
            if (optarg >= 0)
                argvRemove(argc, argv, ai);
            argvRemove(argc, argv, ai);
	} else if (*argv[ai] == '-') {
	    /* A dash by itself is not considered an option. */
	    if (argv[ai][1] == '\0') {
		++ai;
		continue;
	    }
	    /* Short option(s) following */
	    o = argv[ai] + 1;
	    done = 0;
	    optarg = -1;
	    while (*o && !done) {
		/* find matching option */
		if ((mi = optMatch(opt, o, 0)) < 0)
		    optFatal("unrecognized option `-%c'\n", *o);

		/* does this option take an argument? */
		optarg = -1;
		arg = NULL;
		if (optNeedsArgument(&opt[mi])) {
		    /* option needs an argument. find it. */
		    arg = o + 1;
		    if (!*arg) {
			if ((optarg = ai + 1) == *argc)
			    optFatal("option `%s' requires an argument\n",
				     optString(&opt[mi], 0));
			arg = argv[optarg];
		    }
		    done = 1;
		}
		/* perform the action of this option. */
		optExecute(&opt[mi], arg, 0);
		++o;
	    }
	    /* remove option and any argument from the argv-array. */
            if (optarg >= 0)
                argvRemove(argc, argv, ai);
            argvRemove(argc, argv, ai);
	} else {
	    /* a non-option argument */
	    ++ai;
	}
    }
}
