/***************************************************************************
 *
 * $Id$ 
 *
 ************************************************************************
 *
 * Licensed to the Apache Software  Foundation (ASF) under one or more
 * contributor  license agreements.  See  the NOTICE  file distributed
 * with  this  work  for  additional information  regarding  copyright
 * ownership.   The ASF  licenses this  file to  you under  the Apache
 * License, Version  2.0 (the  "License"); you may  not use  this file
 * except in  compliance with the License.   You may obtain  a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the  License is distributed on an  "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY  KIND, either  express or
 * implied.   See  the License  for  the  specific language  governing
 * permissions and limitations under the License.
 *
 * Copyright 2005-2008 Rogue Wave Software, Inc.
 * 
 **************************************************************************/

// expand _TEST_EXPORT macros
#define _RWSTD_TEST_SRC

#include <rw_cmdopt.h>

#include <ctype.h>    // isdigit(), isspace()
#include <errno.h>    // for errno
#include <stdarg.h>   // for va_arg, ...
#include <stdio.h>    // for fprintf
#include <stdlib.h>   // for atexit, free, malloc
#include <string.h>   // for memcpy, strcpy, strcmp, ...

#ifndef EINVAL
#  define EINVAL   22   /* e.g., HP-UX, Linux, Solaris */
#endif   // EINVAL

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

typedef int (optcallback_t)(int, char*[]);

struct cmdopts_t
{
    char           loptbuf_ [32];   // buffer for long option name
    optcallback_t *callback_;       // function to call to process option

    // counter to increment for each occurrence of an option
    // or to set to the numeric argument (when specified)
    int           *pcntr_;

    char          *lopt_;           // long option name
    char           sopt_;           // short option name

    int            minval_;         // minimum value of a numerical argument
    int            maxval_;         // maximum value of a numerical argument
    size_t         maxcount_;       // how many times option can be invoked
    size_t         count_;          // how many times it has been invoked

    unsigned       arg_ : 1;        // option takes an argument
    unsigned       inv_ : 1;        // callback invocation inverted
    unsigned       tristate_ : 1;   // option is a tristate
    unsigned       envseen_ : 1;    // environment option already processed
};


// total number of registered options
static size_t _rw_ncmdopts;

// number of default (always defined) options
static size_t     _rw_ndefopts;
static cmdopts_t  _rw_cmdoptbuf [32];
static cmdopts_t *_rw_cmdopts = _rw_cmdoptbuf;
static size_t     _rw_optbufsize = sizeof _rw_cmdoptbuf / sizeof *_rw_cmdoptbuf;

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

static int
_rw_print_help (int argc, char *argv[])
{
    if (1 == argc && argv && 0 == argv [0]) {
        static const char helpstr[] = {
            "Without an argument, prints this help to stdout and exits with\n"
            "a status of 0 without further processing.\n"
            "With the optional argument prints help on the option with that\n"
            "name, if one exists, and exits with a status of 0. If an option\n"
            "with the specified name does not exist, prints an error message\n"
            "to stderr and exits with a status of 1.\n"
            "The leading underscores in the name of an option are optional.\n"
        };

        argv [0] = _RWSTD_CONST_CAST (char*, helpstr);

        return 0;
    }

    // the option name to get help on, if any
    const char* opthelp = 1 < argc ? argv [1] : 0;

    // remove the optional one or two leading underscores
    if (opthelp && '-' == opthelp [0] && opthelp [1])
        opthelp += 1 + ('-' == opthelp [1]);

    if (0 == opthelp)
        printf ("OPTIONS\n");

    // set to a non-zero when the specified option is found
    int option_found = 0;

    for (size_t i = 0; i != _rw_ncmdopts; ++i) {

        // for convenience
        const cmdopts_t* const opt = _rw_cmdopts + i;

        // get a pointer to the name of the long option, if any
        const char* const lopt = opt->lopt_ ? opt->lopt_ : opt->loptbuf_;

        if (opthelp && *opthelp) {

            if (   (opt->sopt_ == opthelp [0] && '\0' == opthelp [1])
                || (*lopt && 0 == strcmp (lopt + 1, opthelp))) {

                // remember that we found the option whose (short
                // or long) name we're to give help on; after printing
                // the help text on the option keep looping in case
                // there is another option and callback with the same
                // name (unlikely but possible)
                option_found = 1;
            }
            else {
                // the option doesn't match, continue searching
                continue;
            }
        }

        // separate options with help functionality (which typically
        // produce multiline output) from previous options without
        // it (and thus very brief output)
        if (i && 0 == _rw_cmdopts [i - 1].callback_ && opt->callback_)
            puts ("");

        printf ("     ");

        if (opt->sopt_) {
            printf ("-%c", opt->sopt_);

            if (lopt)
                printf (" | ");
        }

        const char *pfx = "";
        const char *sfx = pfx;

        if (lopt) {
            printf ("-%s", lopt);
            if (opt->arg_ && '=' != lopt [strlen (lopt) - 1]) {
                pfx = " [ ";
                sfx = " ]";
            }
        }

        const char* const arg =
               _RWSTD_INT_MIN < opt->minval_
            || opt->maxval_ < _RWSTD_INT_MAX ? "int" : "arg";

        if (opt->arg_) {
            // print argument (in brackets when it's optional)
            printf ("%s<%s>%s", pfx, arg, sfx);
        }

        if (opt->pcntr_)
            printf (" | -%s=<%s>", lopt, arg);

        if ('i' == *arg) {
            printf (", with ");

            if (_RWSTD_INT_MIN < opt->minval_)
                printf ("%d <= ", opt->minval_);

            printf ("<%s>", arg);

            if (opt->maxval_ < _RWSTD_INT_MAX)
                printf (" <= %d", opt->maxval_);

            printf ("\n      ");
        }

        if (_RWSTD_SIZE_MAX == opt->maxcount_)
            printf (" (each occurrence evaluated)\n");
        else if (1 < opt->maxcount_)
            printf (" (at most %u occurrences evaluated)\n",
                    unsigned (opt->maxcount_));
        else
            printf (" (only the first occurrence evaluated)\n");

        // invoke callback with the "--help" option
        if (opt->callback_) {

            char* help [2] = { 0, 0 };

            opt->callback_ (1, help);

            for (const char *line = help [0]; line; ) {

                const char* const nl = strchr (line, '\n');
                const int len = nl ? int (nl - line) : int (strlen (line));

                printf ("       %.*s\n", len, line);

                line = nl;
                if (nl)
                    ++line;
            }
        }
    }

    if (opthelp && !option_found) {
        fprintf (stderr, "Unknown option \"%s\".\n", opthelp);
        exit (1);
    }

    exit (0);

    return 0;
}

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

static int
_rw_set_ignenv (int argc, char *argv[])
{
    if (1 == argc && argv && 0 == argv [0]) {
        static const char helpstr[] = {
            "Prevents options specified in the RWSTD_TESTOPTS environment\n"
            "variable from taking effect.\n"
            "Unless this option is specified, the RWSTD_TESTOPTS environment\n"
            "variable is processed as if its value were specified on the \n"
            "command line.\n"
            "For example, setting the value of the variable to the string\n"
            "\"--verbose --no-wchar\" and invoking this program with no\n"
            "command line arguments will have the same effect as invoking\n"
            "it with the two arguments on the command line.\n"
        };

        argv [0] = _RWSTD_CONST_CAST (char*, helpstr);

        return 0;
    }

    return 0;
}

extern "C" {

static void
_rw_clear_opts ()
{
    // reset all options, deallocating dynamically allocated storage

    for (size_t i = 0; i != _rw_ncmdopts; ++i) {

        // free any storage allocated for the option name
        free (_rw_cmdopts [i].lopt_);
    }

    if (_rw_cmdopts != _rw_cmdoptbuf) {
        // free the storage allocated for all the options
        free (_rw_cmdopts);
    }

    // reset the options pointer to point at the statically
    // allocated buffer and the count back to 0
    _rw_ncmdopts   = 0;
    _rw_cmdopts    = _rw_cmdoptbuf;
    _rw_optbufsize = sizeof _rw_cmdoptbuf / sizeof *_rw_cmdoptbuf;
}

}

static void
_rw_set_myopts ()
{
    static int cleanup_handler_registered;

    if (0 == cleanup_handler_registered) {
        atexit (_rw_clear_opts);
        cleanup_handler_registered = 1;
    }

    if (0 != _rw_ncmdopts)
        return;

    static int recursive;

    if (recursive)
        return;

    ++recursive;

    rw_setopts (""
                "|-help: "       // argument optional
                "|-ignenv ",
                _rw_print_help,
                _rw_set_ignenv);

    _rw_ndefopts = _rw_ncmdopts;

    recursive = 0;
}

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

#ifndef _RWSTD_NO_VA_LIST_ARRAY
   // va_list is an array type
#  define RW_VA_LIST_ARG_PTR                 va_list
#  define RW_VA_LIST_ARG_TO_PTR(va)          va
#  define RW_VA_LIST_PTR_ARG_TO_VA_LIST(va)  va
#else   // if defined (_RWSTD_NO_VA_LIST_ARRAY)
   // va_list is an object type
#  define RW_VA_LIST_ARG_PTR                 va_list*
#  define RW_VA_LIST_ARG_TO_PTR(va)          &va
#  define RW_VA_LIST_PTR_ARG_TO_VA_LIST(pva) *pva
#endif   // _RWSTD_NO_VA_LIST_ARRAY


static const char*
_rw_getbounds (const char *next, char sep, RW_VA_LIST_ARG_PTR pva)
{
    ++next;

    if (   '*' == *next || '+' == *next || '-' == *next || sep == *next
        || isdigit (*next)) {

        bool have_maxval = false;

        if (*next == sep && '-' != sep) {
            have_maxval = true;
            ++next;
        }
        
        char *end = 0;

        // '*' designates an int va_arg argument
        const long val = '*' == *next ?
              long (va_arg (RW_VA_LIST_PTR_ARG_TO_VA_LIST (pva), int))
            : strtol (next, &end, 10);

#if _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE
        // validate
        if (val < _RWSTD_INT_MIN || _RWSTD_INT_MAX < val) {
            fprintf (stderr,
                     "lower bound %ld out of range [%d, %d]: "
                     "%s\n", val, _RWSTD_INT_MIN, _RWSTD_INT_MAX,
                     next);
            return 0;
        }
#endif   // INT_SIZE < LONG_SIZE

        next = end ? end : next + 1;

        if (have_maxval) {
            _rw_cmdopts [_rw_ncmdopts].minval_ = 0;
            _rw_cmdopts [_rw_ncmdopts].maxval_ = int (val);
        }
        else
            _rw_cmdopts [_rw_ncmdopts].minval_ = int (val);

        if (sep == *next && !have_maxval) {
            ++next;

            if (   '*' == *next
                || '+' == *next
                || (val < 0 && '-' == *next)
                || isdigit (*next)) {

                end = 0;

                // '*' designates an int va_arg argument
                const long maxval = '*' == *next ?
                      long (va_arg (RW_VA_LIST_PTR_ARG_TO_VA_LIST (pva), int))
                    : strtol (next, &end, 10);

#if _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE
                // validate
                if (maxval < _RWSTD_INT_MIN || _RWSTD_INT_MAX < maxval) {

                    fprintf (stderr,
                             "upper bound %ld out of range [%ld, %d]: "
                             "%s\n", maxval, val, _RWSTD_INT_MAX,
                             next);
                    return 0;
                }
#endif   // INT_SIZE < LONG_SIZE

                if ('-' == sep && maxval < val) {
                    // invalid range (upper bound < lower bound
                    fprintf (stderr,
                             "invalid range [%ld, %ld]: %s\n",
                             val, maxval, next);
                    return 0;
                }

                next = end ? end : next + 1;
                _rw_cmdopts [_rw_ncmdopts].maxval_ = int (maxval);
            }
            else {
                // syntax error in range
                fprintf (stderr,
                         "syntax error in range specification: %s\n",
                         next);
                return 0;
            }
        }
        else if (*next && !isspace (*next)) {
            // syntax error in numeric argument
            fprintf (stderr,
                     "syntax error numeric argument: %s\n",
                     next);
            return 0;
        }
        else if (!have_maxval) {
            // no upper bound on the value of the option argument
            _rw_cmdopts [_rw_ncmdopts].maxval_ = _RWSTD_INT_MAX;
        }
    }
    else if (':' == sep) {
        // special value indicating no bits specified for a tristate
        _rw_cmdopts [_rw_ncmdopts].minval_ = 0;
        _rw_cmdopts [_rw_ncmdopts].maxval_ = 0;
    }
    else {
        // no minimum/maximum value for this option is set
        _rw_cmdopts [_rw_ncmdopts].minval_ = _RWSTD_INT_MIN;
        _rw_cmdopts [_rw_ncmdopts].maxval_ = _RWSTD_INT_MAX;
    }

    // an unlimited number of occurrences of the option
    // are allowed and will be counted
    _rw_cmdopts [_rw_ncmdopts].maxcount_ = _RWSTD_SIZE_MAX;

    return next;
}


_TEST_EXPORT int
rw_vsetopts (const char *opts, va_list va)
{
    if (0 == opts) {

        _rw_clear_opts ();
        return 0;
    }

    _rw_set_myopts ();

    const char *next = opts;

    for ( ; ; ++_rw_ncmdopts) {

        while (isspace (*next))
            ++next;

        if ('\0' == *next) {
            break;
        }

        if (_rw_ncmdopts == _rw_optbufsize) {

            const size_t newbufsize = 2 * _rw_ncmdopts + 1;
            
            cmdopts_t* const newopts =
                (cmdopts_t*)malloc (newbufsize * sizeof (cmdopts_t));

            if (0 == newopts) {
                fprintf (stderr, "%s%d: failed to allocate memory\n",
                         __FILE__, __LINE__);
                abort ();
            }

            memcpy (newopts, _rw_cmdopts, _rw_ncmdopts * sizeof (cmdopts_t));

            if (_rw_cmdopts != _rw_cmdoptbuf)
                free (_rw_cmdopts);

            _rw_cmdopts    = newopts;
            _rw_optbufsize = newbufsize;
        }

        // clear the next option info
        memset (_rw_cmdopts + _rw_ncmdopts, 0, sizeof *_rw_cmdopts);

        // for convenience
        cmdopts_t* const lastopt = _rw_cmdopts + _rw_ncmdopts;

        lastopt->minval_ = _RWSTD_INT_MIN;
        lastopt->maxval_ = _RWSTD_INT_MAX;

        if ('|' != *next)
            lastopt->sopt_ = *next++;

        if ('|' == *next) {
            const char* end = strpbrk (++next, "|@:=*!#~ ");
            if (0 == end)
                end = next + strlen (next);

            // copy the option name up to but not including the delimiter
            // (except when the delimiter is the equals sign ('='), which
            // becomes the last character of the option name
            const size_t optlen = size_t (end - next) + ('=' == *end);

            char *lopt = 0;

            if (optlen < sizeof lastopt->loptbuf_)
                lopt = lastopt->loptbuf_;
            else {
                lopt = (char*)malloc (optlen + 1);
                if (0 == lopt)
                    return -1;   // error
                lastopt->lopt_ = lopt;
            }

            memcpy (lopt, next, optlen);
            lopt [optlen] = '\0';

            next = end;
        }

        // only the first occurrence of each command line option
        // causes an invocation of the callback, all subsequent
        // ones will be ignored by default
        lastopt->maxcount_ = 1;

        int arg_is_callback = true;

        RW_ASSERT (0 != next);

        if ('#' == *next) {
            // examples of option specification:
            //   --foo#     option takes an optional numeric argument
            //   --foo#3    numeric argument must be equal to 3
            //   --foo#1-5  numeric argument must be in the range [1,5]

            // instead of a pointer to a callback, the argument is a pointer
            // to an int counter that is to be incremented for each occurrence
            // of the option during processing
            // when the option is immediately followed by the equals sign ('=')
            // and a numeric argument N, the value of N will be stored instead
            next = _rw_getbounds (next, '-', RW_VA_LIST_ARG_TO_PTR (va));

            if (0 == next)
                return -1;   // error

            arg_is_callback = false;

        }
        else if ('~' == *next) {
            // examples of option specification:
            //   --foo~      word 0
            //   --foo~3     word 3
            //   --foo~:4    word 0, bit 2
            //   --foo~3:6   word 3, bits 2 and 3

            // get the optional word number and bit number
            next = _rw_getbounds (next, ':', RW_VA_LIST_ARG_TO_PTR (va));

            if (0 == next)
                return -1;   // error

            // unlimited number of tristates are allowed
            lastopt->tristate_ = 1;

            // no callback function expected
            arg_is_callback = false;
        }
        else if (':' == *next || '=' == *next) {
            // ':' : argument optional
            // '=' : argument required
            lastopt->arg_ = true;

            // check if the value of the argument is restricted
            next = _rw_getbounds (next, '-', RW_VA_LIST_ARG_TO_PTR (va));

            if (0 == next)
                return -1;   // error
        }

        if ('@' == *next) {

            ++next;

            // at most how many occurrences of an option can be processed?
            if ('*' == *next) {
                // unlimited
                lastopt->maxcount_ = _RWSTD_SIZE_MAX;
                ++next;
            }
            else {
                // at most this many
                char *end;
                lastopt->maxcount_ = strtoul (next, &end, 10);
                next = end;
            }
        }
        else if ('!' == *next) {
            lastopt->inv_ = true;
            ++next;
        }

        if (arg_is_callback) {
            // retrieve the callback and verify it's not null
            // (null callback is permitted in the special case when
            // the short option is '-', i.e., when setting up or
            // resetting an "unknown option" handler)
            lastopt->callback_ = va_arg (va, optcallback_t*);
        }
        else {
            // retrieve the address of the int counter where to keep
            // track of the number of occurrences of the option, or
            // where to store the value of the numeric argument of
            // the option
            lastopt->pcntr_ = va_arg (va, int*);
        }

        if (   '-' != lastopt->sopt_
            && 0 == lastopt->callback_
            && 0 == lastopt->pcntr_) {

            // get a pointer to the long option name
            const char* const lopt = lastopt->lopt_
                ? lastopt->lopt_ : lastopt->loptbuf_;

            if (*lopt)
                fprintf (stderr, "null handler for option -%s\n", lopt);
            else
                fprintf (stderr, "null handler for option -%c\n",
                         lastopt->sopt_);
                
            abort ();
        }
    }

    return int (_rw_ncmdopts - _rw_ndefopts);
}

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

_TEST_EXPORT int
rw_setopts (const char *opts, ...)
{
    va_list va;
    va_start (va, opts);
    const int result = rw_vsetopts (opts, va);
    va_end (va);
    return result;
}

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

// validates (and optionally stores) a restricted numeric option argument
static int
_rw_getarg (cmdopts_t *optspec, const char *opt, const char *arg)
{
    RW_ASSERT (0 != optspec);
    RW_ASSERT (0 != arg);

    // obtain the numeric argument
    char *end = 0;
    const long optval = strtol (arg, &end, 0);

    int status = 0;

    if (end && '\0' != *end) {
        fprintf (stderr, "expected numeric argument: %s\n", opt);
        errno  = EINVAL;
        status = 1;
    }

#if _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE

    else if (optval < _RWSTD_INT_MIN || _RWSTD_INT_MAX < optval) {
        fprintf (stderr, "numeric argument %ld out of range"
                 " [%d, %d]: %s\n", optval,
                 _RWSTD_INT_MIN, _RWSTD_INT_MAX, opt);
        errno  = EINVAL;
        status = 1;
    }

#endif   // _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE

    else if (   optval < long (optspec->minval_)
             || long (optspec->maxval_) < optval) {
        
        // numeric argument out of range
        fprintf (stderr, "numeric argument %ld out of range"
                 " [%d, %d]: %s\n", optval,
                 optspec->minval_, optspec->maxval_, opt);
        errno  = EINVAL;
        status = 1;
    }
    else if (optspec->pcntr_) {

        // set the counter
        *optspec->pcntr_ = int (optval);
    }

    return status;
}

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

static bool
_rw_runopt_recursive = false;


// processes a single command line option and its arguments
static int
_rw_runopt (cmdopts_t *optspec, int argc, char *argv[])
{
    RW_ASSERT (0 != optspec);
    RW_ASSERT (0 != argv);

    // ignore the option if invoked recursively (by processing options
    // set in the environment) and the option has already been seen
    // (this prevents duplicate processing of options that are set both
    // on the command line and in the environment and allows option with
    // an argument set on the command line to override those set in the
    // environment)

    if (optspec->count_ && _rw_runopt_recursive)
        return 0;

    // if the option has been evaluated the maximum number
    // of times, avoid evaluating it and continue processing
    if (optspec->maxcount_ <= optspec->count_)
        return 0;

    int status = 0;

    char* equals = strchr (argv [0], '=');

    if (optspec->callback_) {
        // option matched the specification

        if (!optspec->inv_) {
            // option is not inverted (i.e., is processed when seen
            // on the command line as opposed processed when missing)

            const char* const lopt = optspec->lopt_ ?
                optspec->lopt_ : optspec->loptbuf_;

            if (   optspec->arg_
                && (   _RWSTD_INT_MIN < optspec->minval_
                    || optspec->maxval_ < _RWSTD_INT_MAX)) {

                if (equals) {
                    if (strchr (lopt, '=')) {
                        // option requires a restricted numeric argument
                        // retrieve and validate it before invoking the
                        // callback

                        status = _rw_getarg (optspec, argv [0], equals + 1);
                    }
                }
                else if (1 < argc && '-' != argv [1][0]) {

                    const char *arg = argv [1];

                    // handle escaped minus (necessary to disambiguate
                    // it from a leading dash that introduces an option)
                    if ('\\' == arg [0] && '-' == arg [1])
                        ++arg;

                    status = _rw_getarg (optspec, argv [0], arg);
                }
            }

            if (0 == status)
                status = optspec->callback_ (argc, argv);
        }
    }
    else if (equals) {
        // option takes an optional numeric argument

        RW_ASSERT (0 != optspec->pcntr_);

        status = _rw_getarg (optspec, argv [0], equals + 1);
    }
    else {
        // option must not be a tristate (those are handled elsewhere)
        RW_ASSERT (0 == optspec->tristate_);
        RW_ASSERT (0 != optspec->pcntr_);
        ++*optspec->pcntr_;
    }

    ++optspec->count_;

    if (_rw_runopt_recursive)
        optspec->envseen_ = true;

    return status;
}

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

// tries to match the option named by optname with the option
// specification opt and returns a non-zero value on success,
// 0 otherwise; the returned value is negative when the matched
// option is being disabled and positive when it's being
// enabled
static int
_rw_match_tristate (const cmdopts_t *opt, const char *optname)
{
    RW_ASSERT (0 != opt);
    RW_ASSERT (0 != optname);

    static const char* const prefix[] = {
        "+enable", "+use", "+with", 
        "-disable", "-no", "-without",
        0
    };

    int tristate = 0;

    if ('-' == optname [0] && '-' != optname [2]) {

        ++optname;

        const size_t optlen = strlen (optname);

        for (size_t i = 0; prefix [i]; ++i) {
            const char* const pfx    = prefix [i] + 1;
            const size_t      pfxlen = strlen (pfx);

            if (pfxlen < optlen && 0 == memcmp (optname, pfx, pfxlen)) {
                const char* const name = optname + pfxlen;

                // return -1 to disable, +1 to enable, 0 when not found
                const char* const lopt =
                    opt->lopt_ ? opt->lopt_ : opt->loptbuf_;

                if (0 == strcmp (lopt, name)) {
                    tristate = '+' == prefix [i][0] ? 1 : -1;
                    break;
                }
            }
        }
    }

    return tristate;
}

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

// returns a bitmap all of whose bits are set starting with the most
// significant non-zero bit corresponding to first such bit in val
// e.g., _rw_set_bits(4) --> 7
static int
_rw_set_bits (int val)
{
    int res = 0;

    for (unsigned uval = val; uval; uval >>= 1) {
        res = (res << 1) | 1;
    }

    return res;
}


_TEST_EXPORT int
rw_runopts (int argc, char *argv[])
{
    _rw_set_myopts ();

    // ignore options set in the environment?
    int ignenv = _rw_runopt_recursive;

    // return status
    int status = 0;

    // number of options processed
    int nopts = 0;

    // pointer to the option whose callback will be invoked
    // for command line options that do not match any other
    cmdopts_t* not_found_opt = 0;

    // iterate over the command line arguments until a callback
    // returns a non-zero value or until all options have been
    // successfully processed
    for (int i = 0; i < argc && argv [i] && 0 == status; ++i) {

        if (0 == strcmp ("--ignore-environment", argv [i])) {
            // ignore options set in the environment
            ignenv = true;
            continue;
        }

        if (0 == strcmp ("--", argv [i])) {
            // "--" terminates options, everything
            // after it is treated as an argument
            break;
        }

        // the name of the option without the leading dash
        const char* const optname = argv [i] + 1;

        // the argc and argv to pass to the option handler
        const int    opt_argc = argc - i;
        char** const opt_argv = argv + i;

        // look for the first equals sign
        const char* const eq = strchr (optname, '=');

        // compute the length of the option including the equals sign (if any)
        const size_t optlen = eq ? size_t (eq - optname + 1) : strlen (optname);

        int found = false;

        // look up each command line option (i.e., a string that starts
        // with a dash ('-')) and invoke the callback associated with it
        for (size_t j = 0; j != _rw_ncmdopts; ++j) {

            // for convenience
            cmdopts_t* const opt = _rw_cmdopts + j;

            if ('-' == opt->sopt_)
                not_found_opt = opt;

            if ('-' == argv [i][0]) {

                const size_t cmplen =
                    eq && opt->pcntr_ ? optlen - 1 : optlen;

                // get a pointer to the (possibly empty) name
                // of the long option
                const char* const lopt = opt->lopt_ ? 
                    opt->lopt_ : opt->loptbuf_;

                if (opt->tristate_) {
                    // option specification denotes a tristate, see if it
                    // matches and if so, whether it's being disabled or
                    // enabled (-1 or +1, respectively)
                    const int tristate = _rw_match_tristate (opt, optname);

                    if (tristate) {
                        RW_ASSERT (0 != opt->pcntr_);

                        const int word_bits = _RWSTD_CHAR_BIT * sizeof (int);

                        // the optional minval_ specifies the bit number
                        // of the first of a sequence of bits in an array
                        // stored at pcntr_
                        const int word_inx = opt->minval_ / word_bits;
                        const int bit_inx  = opt->minval_ % word_bits;

                        const int bit_val = _RWSTD_INT_MAX == opt->maxval_
                            ? 1 : opt->maxval_;

                        if (bit_val) {
                            const int bit_mask =
                                _rw_set_bits (bit_val) << bit_inx;

                            // clear the bit field
                            opt->pcntr_ [word_inx] &= ~bit_mask;

                            if (tristate < 0)
                                opt->pcntr_ [word_inx] = ~(bit_val << bit_inx);
                            else
                                opt->pcntr_ [word_inx] |= bit_val << bit_inx;
                        }
                        else {
                            // special case
                            opt->pcntr_ [word_inx] = tristate < 0 ? -1 : +1;
                        }

                        // matching option has been found
                        found = true;

                        // increment the number of options processed
                        ++nopts;
                    }

                    // avoid ordinary option processing below
                    continue;
                }

                // try to match the long option first, and only if it
                // doesn't match try the short single-character option
                if (   (   cmplen == strlen (lopt)
                        && 0 == memcmp (optname, lopt, cmplen))
                    || (   opt->sopt_
                        && optname [0] == opt->sopt_
                        && (1 == optlen || opt->arg_))) {

                    // matching option has been found
                    found = true;

                    // process it and its arguments, if any
                    status = _rw_runopt (opt, opt_argc, opt_argv);

                    // increment the number of options processed
                    // (whether successfully or otherwise)
                    ++nopts;

                    if (status) {
                        // when the status returned from the last callback
                        // is non-0 stop further processing (including
                        // any options set in the environment) and return
                        // status to the caller
                        ignenv = true;
                        break;
                    }
                }
            }
        }

        if (!found && '-' == argv [i][0]) {

            // invoke the appropriate error handler for an option
            // that was not found
            if (0 != not_found_opt) {

                // invoke the error handler set up through rw_setopts()
                // and let the handler decide whether to go on processing
                // other options or whether to abort
                status = not_found_opt->callback_ (opt_argc, opt_argv);
                if (status) {
                    // no further processing done
                    ignenv = true;
                    break;
                }
            }
            else {
                // print an error message to stderr when no error
                // handler has been set up
                fprintf (stderr, "unknown option: %s\n", argv [i]);
                ignenv = true;
                errno  = EINVAL;
                status = 1;
                break;
            }
        }
    }

    if (!ignenv) {
        // process options from the environment
        const char* const envar = getenv ("RWSTD_TESTOPTS");
        if (envar) {
            _rw_runopt_recursive = true;
            rw_runopts (envar);
            _rw_runopt_recursive = false;
        }
    }

    // invoke any inverted callbacks or bump their user-specified counters,
    // and reset internal counters indicating if/how many times each option
    // has been processed
    for (size_t j = 0; j != _rw_ncmdopts; ++j) {

        // for convenience
        cmdopts_t* const opt = _rw_cmdopts + j;

        if (opt->inv_ && 0 == opt->count_ && 0 == status) {

            if (opt->callback_)
                status = opt->callback_ (0, 0);
            else {
                RW_ASSERT (0 != opt->pcntr_);
                ++*opt->pcntr_;
            }
        }

        opt->count_   = 0;
        opt->envseen_ = false;
    }

    return status;
}

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

_TEST_EXPORT int
rw_runopts (const char *str)
{
    RW_ASSERT (0 != str);

    _rw_set_myopts ();

    char buf [80];      // fixed size buffer to copy `str' into
    char *pbuf = buf;   // a modifiable copy of `str'

    size_t len = strlen (str);
    if (len >= sizeof buf) {
        // allocate if necessary
        pbuf = (char*)malloc (len + 1);
        if (!pbuf)
            return -1;
    }

    // copy `str' to modifiable buffer
    memcpy (pbuf, str, len + 1);

    char *tmp_argv_buf [32] = { 0 };   // fixed size argv buffer
    char **argv = tmp_argv_buf;        // array of arguments

    // initial size of argument array (will grow as necessary)
    size_t argv_size = sizeof tmp_argv_buf / sizeof *tmp_argv_buf;
    size_t argc = 0;   // number of arguments in array

    int in_quotes = 0;   // quoted argument being processed

    for (char *s = pbuf; *s; ++s) {
        if ('"' == *s) {
            in_quotes = !in_quotes;
            continue;
        }

        if (in_quotes)
            continue;

        // split up unquoted space-separated arguments
        if (argc == 0 || isspace (*s)) {
            if (argc > 0) 
                *s = 0;

            // skip over leading spaces
            if (argc > 0 || isspace (*s))
                while (isspace (*++s));

            if (*s) {
                if (argc == argv_size) {
                    // grow `argv' as necessary
                    char **tmp = (char**)malloc (sizeof *tmp * argv_size * 2);
                    if (!tmp) {
                        if (argv != tmp_argv_buf)
                            free (argv);
                        return -1;
                    }

                    // copy existing elementes and zero out any new entries
                    memcpy (tmp, argv, sizeof *tmp * argv_size);
                    memset (tmp + argv_size, 0, sizeof *tmp * argv_size);

                    // free existing buffer if necessary
                    if (argv != tmp_argv_buf)
                        free (argv);

                    // reassign buffer and increase size
                    argv       = tmp;
                    argv_size *= 2;
                }

                // add argument to array
                argv [argc++] = s;
            }
        }
    }

    // process `argc' options pointed to by `argv'
    const int status = rw_runopts (int (argc), argv);

    // free buffers if necessary
    if (argv != tmp_argv_buf)
        free (argv);

    if (pbuf != buf)
        free (pbuf);

    return status;
}
