
/*
 * \file usage.c
 *
 *  This module implements the default usage procedure for
 *  Automated Options.  It may be overridden, of course.
 *
 * @addtogroup autoopts
 * @{
 */
/*
 *  Sort options:
    --start=END-[S]TATIC-FORWARD --patt='^/\*($|[^:])' \
    --out=xx.c key='^[a-zA-Z0-9_]+\(' --trail='^/\*:' \
    --spac=2 --input=usage.c
 */

/*
 *  This file is part of AutoOpts, a companion to AutoGen.
 *  AutoOpts is free software.
 *  AutoOpts is Copyright (C) 1992-2018 by Bruce Korb - all rights reserved
 *
 *  AutoOpts is available under any one of two licenses.  The license
 *  in use must be one of these two and the choice is under the control
 *  of the user of the license.
 *
 *   The GNU Lesser General Public License, version 3 or later
 *      See the files "COPYING.lgplv3" and "COPYING.gplv3"
 *
 *   The Modified Berkeley Software Distribution License
 *      See the file "COPYING.mbsd"
 *
 *  These files have the following sha256 sums:
 *
 *  8584710e9b04216a394078dc156b781d0b47e1729104d666658aecef8ee32e95  COPYING.gplv3
 *  4379e7444a0e2ce2b12dd6f5a52a27a4d02d39d247901d3285c88cf0d37f477b  COPYING.lgplv3
 *  13aa749a5b0a454917a944ed8fffc530b784f5ead522b1aacaf4ec8aa55a6239  COPYING.mbsd
 */

#define GRAPH_CH(_ch) \
    ((((unsigned)_ch) <= 0x7E) && (((unsigned)_ch) > ' '))

/**
 * Parse the option usage flags string.  Any parsing problems yield
 * a zero (no flags set) result.  This function is internal to
 * set_usage_flags().
 *
 * @param[in] fnt   Flag Name Table - maps a name to a mask
 * @param[in] txt   the text to process.  If NULL, then
 *                  getenv("AUTOOPTS_USAGE") is used.
 * @returns a bit mask indicating which \a fnt entries were found.
 */
static unsigned int
parse_usage_flags(ao_flag_names_t const * fnt, char const * txt)
{
    unsigned int res = 0;

    /*
     * The text may be passed in.  If not, use the environment variable.
     */
    if (txt == NULL) {
        txt = getenv("AUTOOPTS_USAGE");
        if (txt == NULL)
            return 0;
    }

    txt = SPN_WHITESPACE_CHARS(txt);
    if (*txt == NUL)
        return 0;

    /*
     * search the string for table entries.  We must understand everything
     * we see in the string, or we give up on it.
     */
    for (;;) {
        int ix = 0;

        for (;;) {
            if (strneqvcmp(txt, fnt[ix].fnm_name, (int)fnt[ix].fnm_len) == 0)
                break;
            if (++ix >= AOUF_COUNT)
                return 0;
        }

        /*
         *  Make sure we have a full match.  Look for whitespace,
         *  a comma, or a NUL byte.
         */
        if (! IS_END_LIST_ENTRY_CHAR(txt[fnt[ix].fnm_len]))
            return 0;

        res |= 1U << ix;
        txt = SPN_WHITESPACE_CHARS(txt + fnt[ix].fnm_len);

        switch (*txt) {
        case NUL:
            return res;

        case ',':
            txt = SPN_WHITESPACE_CHARS(txt + 1);
            /* Something must follow the comma */
            /* FALLTHROUGH */

        default:
            continue;
        }
    }
}

/**
 * Set option usage flags.  Any parsing problems yield no changes to options.
 * Three different bits may be fiddled: \a OPTPROC_GNUUSAGE, \a OPTPROC_MISUSE
 * and \a OPTPROC_COMPUTE.
 *
 * @param[in] flg_txt   text to parse.  If NULL, then the AUTOOPTS_USAGE
 *                      environment variable is parsed.
 * @param[in,out] opts  the program option descriptor
 */
static void
set_usage_flags(tOptions * opts, char const * flg_txt)
{
#   define _aof_(_n, _f)   { sizeof(#_n)-1, _f, #_n },
    static ao_flag_names_t const fn_table[AOUF_COUNT] = {
        AOFLAG_TABLE
    };
#   undef  _aof_

    /*
     * the flag word holds a bit for each selected table entry.
     */
    unsigned int flg = parse_usage_flags(fn_table, flg_txt);
    if (flg == 0) return;

    /*
     * Ensure we do not have conflicting selections
     */
    {
        static unsigned int const form_mask =
            AOUF_gnu | AOUF_autoopts;
        static unsigned int const misuse_mask =
            AOUF_no_misuse_usage | AOUF_misuse_usage;
        if (  ((flg & form_mask)   == form_mask)
           || ((flg & misuse_mask) == misuse_mask) )
            return;
    }

    /*
     * Now fiddle the fOptSet bits, based on settings.
     * The OPTPROC_LONGOPT bit is immutable, thus if it is set,
     * then fnm points to a mask off mask.
     */
    {
        ao_flag_names_t const * fnm = fn_table;
        for (;;) {
            if ((flg & 1) != 0) {
                if ((fnm->fnm_mask & OPTPROC_LONGOPT) != 0)
                     opts->fOptSet &= fnm->fnm_mask;
                else opts->fOptSet |= fnm->fnm_mask;
            }
            flg >>= 1;
            if (flg == 0)
                break;
            fnm++;
        }
    }
}

/*
 *  Figure out if we should try to format usage text sort-of like
 *  the way many GNU programs do.
 */
static inline bool
do_gnu_usage(tOptions * pOpts)
{
    return (pOpts->fOptSet & OPTPROC_GNUUSAGE) ? true : false;
}

/*
 *  Figure out if we should try to format usage text sort-of like
 *  the way many GNU programs do.
 */
static inline bool
skip_misuse_usage(tOptions * pOpts)
{
    return (pOpts->fOptSet & OPTPROC_MISUSE) ? true : false;
}


/*=export_func  optionOnlyUsage
 *
 * what:  Print usage text for just the options
 * arg:   + tOptions *  + pOpts    + program options descriptor +
 * arg:   + int         + ex_code  + exit code for calling exit(3) +
 *
 * doc:
 *  This routine will print only the usage for each option.
 *  This function may be used when the emitted usage must incorporate
 *  information not available to AutoOpts.
=*/
void
optionOnlyUsage(tOptions * pOpts, int ex_code)
{
    char const * pOptTitle = NULL;

    set_usage_flags(pOpts, NULL);
    if ((ex_code != EXIT_SUCCESS) &&
        skip_misuse_usage(pOpts))
        return;

    /*
     *  Determine which header and which option formatting strings to use
     */
    if (do_gnu_usage(pOpts))
        (void)setGnuOptFmts(pOpts, &pOptTitle);
    else
        (void)setStdOptFmts(pOpts, &pOptTitle);

    prt_opt_usage(pOpts, ex_code, pOptTitle);

    fflush(option_usage_fp);
    if (ferror(option_usage_fp) != 0)
        fserr_exit(pOpts->pzProgName, zwriting, (option_usage_fp == stderr)
                   ? zstderr_name : zstdout_name);
}

/**
 * Print a message suggesting how to get help.
 *
 * @param[in] opts      the program options
 */
static void
print_offer_usage(tOptions * opts)
{
    char help[24];

    if (HAS_opt_usage_t(opts)) {
        int ix = opts->presetOptCt;
        tOptDesc * od = opts->pOptDesc + ix;
        while (od->optUsage != AOUSE_HELP) {
            if (++ix >= opts->optCt)
                ao_bug(zmissing_help_msg);
            od++;
        }
        switch (opts->fOptSet & (OPTPROC_LONGOPT | OPTPROC_SHORTOPT)) {
        case OPTPROC_SHORTOPT:
            help[0] = '-';
            help[1] = od->optValue;
            help[2] = NUL;
            break;

        case OPTPROC_LONGOPT:
        case (OPTPROC_LONGOPT | OPTPROC_SHORTOPT):
            help[0] = help[1] = '-';
            strncpy(help + 2, od->pz_Name, 20);
            break;

        case 0:
            strncpy(help, od->pz_Name, 20);
            break;
        }

    } else {
        switch (opts->fOptSet & (OPTPROC_LONGOPT | OPTPROC_SHORTOPT)) {
        case OPTPROC_SHORTOPT:
            strcpy(help, "-h");
            break;

        case OPTPROC_LONGOPT:
        case (OPTPROC_LONGOPT | OPTPROC_SHORTOPT):
            strcpy(help, "--help");
            break;

        case 0:
            strcpy(help, "help");
            break;
        }
    }

    fprintf(option_usage_fp, zoffer_usage_fmt, opts->pzProgName, help);
}

/**
 * Print information about each option.
 *
 * @param[in] opts      the program options
 * @param[in] exit_code whether or not there was a usage error reported.
 *                      used to select full usage versus abbreviated.
 */
static void
print_usage_details(tOptions * opts, int exit_code)
{
    {
        char const * pOptTitle = NULL;
        int flen;

        /*
         *  Determine which header and which option formatting strings to use
         */
        if (do_gnu_usage(opts)) {
            flen = setGnuOptFmts(opts, &pOptTitle);
            sprintf(line_fmt_buf, zFmtFmt, flen);
            fputc(NL, option_usage_fp);

        } else {
            flen = setStdOptFmts(opts, &pOptTitle);
            sprintf(line_fmt_buf, zFmtFmt, flen);

            /*
             *  When we exit with EXIT_SUCCESS and the first option is a doc
             *  option, we do *NOT* want to emit the column headers.
             *  Otherwise, we do.
             */
            if (  (exit_code != EXIT_SUCCESS)
               || ((opts->pOptDesc->fOptState & OPTST_DOCUMENT) == 0) )

                fputs(pOptTitle, option_usage_fp);
        }

        flen = 4 - ((flen + 15) / 8);
        if (flen > 0)
            tab_skip_ct = flen;
        prt_opt_usage(opts, exit_code, pOptTitle);
    }

    /*
     *  Describe the mechanics of denoting the options
     */
    switch (opts->fOptSet & OPTPROC_L_N_S) {
    case OPTPROC_L_N_S:     fputs(zFlagOkay, option_usage_fp); break;
    case OPTPROC_SHORTOPT:  break;
    case OPTPROC_LONGOPT:   fputs(zNoFlags,  option_usage_fp); break;
    case 0:                 fputs(zOptsOnly, option_usage_fp); break;
    }

    if ((opts->fOptSet & OPTPROC_NUM_OPT) != 0)
        fputs(zNumberOpt, option_usage_fp);

    if ((opts->fOptSet & OPTPROC_REORDER) != 0)
        fputs(zReorder, option_usage_fp);

    if (opts->pzExplain != NULL)
        fputs(opts->pzExplain, option_usage_fp);

    /*
     *  IF the user is asking for help (thus exiting with SUCCESS),
     *  THEN see what additional information we can provide.
     */
    if (exit_code == EXIT_SUCCESS)
        prt_prog_detail(opts);

    /*
     * Give bug notification preference to the packager information
     */
    if (HAS_pzPkgDataDir(opts) && (opts->pzPackager != NULL))
        fputs(opts->pzPackager, option_usage_fp);

    else if (opts->pzBugAddr != NULL)
        fprintf(option_usage_fp, zPlsSendBugs, opts->pzBugAddr);

    fflush(option_usage_fp);

    if (ferror(option_usage_fp) != 0)
        fserr_exit(opts->pzProgName, zwriting, (option_usage_fp == stderr)
                   ? zstderr_name : zstdout_name);
}

static void
print_one_paragraph(char const * text, bool plain, FILE * fp)
{
    if (plain) {
#ifdef ENABLE_NLS
#ifdef HAVE_LIBINTL_H
#ifdef DEBUG_ENABLED
#undef gettext
#endif
        char * buf = dgettext("libopts", text);
        if (buf == text)
            text = gettext(text);
#endif /* HAVE_LIBINTL_H */
#endif /* ENABLE_NLS */
        fputs(text, fp);
    }

    else {
        char const * t = optionQuoteString(text, LINE_SPLICE);
        fprintf(fp, PUTS_FMT, t);
        AGFREE(t);
    }
}

/*=export_func  optionPrintParagraphs
 * private:
 *
 * what:  Print a paragraph of usage text
 * arg:   + char const * + text  + a block of text that has bee i18n-ed +
 * arg:   + bool         + plain + false -> wrap text in fputs()        +
 * arg:   + FILE *       + fp    + the stream file pointer for output   +
 *
 * doc:
 *  This procedure is called in two contexts: when a full or short usage text
 *  has been provided for display, and when autogen is assembling a list of
 *  translatable texts in the optmain.tlib template.  In the former case, \a
 *  plain is set to \a true, otherwise \a false.
 *
 *  Anything less than 256 characters in size is printed as a single unit.
 *  Otherwise, paragraphs are detected.  A paragraph break is defined as just
 *  before a non-empty line preceded by two newlines or a line that starts
 *  with at least one space character but fewer than 8 space characters.
 *  Lines indented with tabs or more than 7 spaces are considered continuation
 *  lines.
 *
 *  If 'plain' is true, we are emitting text for a user to see.  So, if it is
 *  true and NLS is not enabled, then just write the whole thing at once.
=*/
void
optionPrintParagraphs(char const * text, bool plain, FILE * fp)
{
    size_t len = strlen(text);
    char * buf;
#ifndef ENABLE_NLS
    if (plain || (len < 256))
#else
    if (len < 256)
#endif
    {
        print_one_paragraph(text, plain, fp);
        return;
    }

    AGDUPSTR(buf, text, "ppara");
    text = buf;

    for (;;) {
        char * scan;

        if (len < 256) {
        done:
            print_one_paragraph(buf, plain, fp);
            break;
        }
        scan = buf;

    try_longer:
        scan = strchr(scan, NL);
        if (scan == NULL)
            goto done;

        if ((scan - buf) < 40) {
            scan++;
            goto try_longer;
        }

        scan++;
        if ((! isspace((int)*scan)) || (*scan == HT))
            /*
             * line starts with tab or non-whitespace --> continuation
             */
            goto try_longer;

        if (*scan == NL) {
            /*
             * Double newline -> paragraph break
             * Include all newlines in current paragraph.
             */
            while (*++scan == NL)  /*continue*/;

        } else {
            char * p = scan;
            int   sp_ct = 0;

            while (*p == ' ') {
                if (++sp_ct >= 8) {
                    /*
                     * Too many spaces --> continuation line
                     */
                    scan = p;
                    goto try_longer;
                }
                p++;
            }
        }

        /*
         * "scan" points to the first character of a paragraph or the
         * terminating NUL byte.
         */
        {
            char svch = *scan;
            *scan = NUL;
            print_one_paragraph(buf, plain, fp);
            len -= scan - buf;
            if (len <= 0)
                break;
            *scan = svch;
            buf = scan;
        }
    }
    AGFREE(text);
}

/*=export_func  optionUsage
 * private:
 *
 * what:  Print usage text
 * arg:   + tOptions * + opts + program options descriptor +
 * arg:   + int        + exitCode + exit code for calling exit(3) +
 *
 * doc:
 *  This routine will print usage in both GNU-standard and AutoOpts-expanded
 *  formats.  The descriptor specifies the default, but AUTOOPTS_USAGE will
 *  over-ride this, providing the value of it is set to either "gnu" or
 *  "autoopts".  This routine will @strong{not} return.
 *
 *  If "exitCode" is "AO_EXIT_REQ_USAGE" (normally 64), then output will to
 *  to stdout and the actual exit code will be "EXIT_SUCCESS".
=*/
lo_noreturn void
optionUsage(tOptions * opts, int usage_exit_code)
{
    int exit_code = (usage_exit_code == AO_EXIT_REQ_USAGE)
        ? EXIT_SUCCESS : usage_exit_code;

    displayEnum = false;
    set_usage_flags(opts, NULL);

    /*
     *  Paged usage will preset option_usage_fp to an output file.
     *  If it hasn't already been set, then set it to standard output
     *  on successful exit (help was requested), otherwise error out.
     *
     *  Test the version before obtaining pzFullUsage or pzShortUsage.
     *  These fields do not exist before revision 30.
     */
    {
        char const * pz;

        if (exit_code == EXIT_SUCCESS) {
            pz = (opts->structVersion >= 30 * 4096)
                ? opts->pzFullUsage : NULL;

            if (option_usage_fp == NULL)
                option_usage_fp = print_exit ? stderr : stdout;

        } else {
            pz = (opts->structVersion >= 30 * 4096)
                ? opts->pzShortUsage : NULL;

            if (option_usage_fp == NULL)
                option_usage_fp = stderr;
        }

        if (((opts->fOptSet & OPTPROC_COMPUTE) == 0) && (pz != NULL)) {
            if ((opts->fOptSet & OPTPROC_TRANSLATE) != 0)
                optionPrintParagraphs(pz, true, option_usage_fp);
            else
                fputs(pz, option_usage_fp);
            goto flush_and_exit;
        }
    }

    fprintf(option_usage_fp, opts->pzUsageTitle, opts->pzProgName);

    if ((exit_code == EXIT_SUCCESS) ||
        (! skip_misuse_usage(opts)))

        print_usage_details(opts, usage_exit_code);
    else
        print_offer_usage(opts);

 flush_and_exit:
    fflush(option_usage_fp);
    if (ferror(option_usage_fp) != 0)
        fserr_exit(opts->pzProgName, zwriting, (option_usage_fp == stdout)
                   ? zstdout_name : zstderr_name);

    option_exits(exit_code);
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *   PER OPTION TYPE USAGE INFORMATION
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
 * print option conflicts.
 *
 * @param opts the program option descriptor
 * @param od   the option descriptor
 */
static void
prt_conflicts(tOptions * opts, tOptDesc * od)
{
    const int * opt_no;
    fputs(zTabHyp + tab_skip_ct, option_usage_fp);

    /*
     *  REQUIRED:
     */
    if (od->pOptMust != NULL) {
        opt_no = od->pOptMust;

        if (opt_no[1] == NO_EQUIVALENT) {
            fprintf(option_usage_fp, zReqOne,
                    opts->pOptDesc[*opt_no].pz_Name);
        } else {
            fputs(zReqThese, option_usage_fp);
            for (;;) {
                fprintf(option_usage_fp, zTabout + tab_skip_ct,
                        opts->pOptDesc[*opt_no].pz_Name);
                if (*++opt_no == NO_EQUIVALENT)
                    break;
            }
        }

        if (od->pOptCant != NULL)
            fputs(zTabHypAnd + tab_skip_ct, option_usage_fp);
    }

    /*
     *  CONFLICTS:
     */
    if (od->pOptCant == NULL)
        return;

    opt_no = od->pOptCant;

    if (opt_no[1] == NO_EQUIVALENT) {
        fprintf(option_usage_fp, zProhibOne,
                opts->pOptDesc[*opt_no].pz_Name);
        return;
    }

    fputs(zProhib, option_usage_fp);
    for (;;) {
        fprintf(option_usage_fp, zTabout + tab_skip_ct,
                opts->pOptDesc[*opt_no].pz_Name);
        if (*++opt_no == NO_EQUIVALENT)
            break;
    }
}

/**
 *  Print the usage information for a single vendor option.
 *
 * @param[in] opts    the program option descriptor
 * @param[in] od      the option descriptor
 * @param[in] argtp   names of the option argument types
 * @param[in] usefmt  format for primary usage line
 */
static void
prt_one_vendor(tOptions *    opts,  tOptDesc *   od,
               arg_types_t * argtp, char const * usefmt)
{
    prt_preamble(opts, od, argtp);

    {
        char z[ 80 ];
        char const *  pzArgType;

        /*
         *  Determine the argument type string first on its usage, then,
         *  when the option argument is required, base the type string on the
         *  argument type.
         */
        if (od->fOptState & OPTST_ARG_OPTIONAL) {
            pzArgType = argtp->pzOpt;

        } else switch (OPTST_GET_ARGTYPE(od->fOptState)) {
        case OPARG_TYPE_NONE:        pzArgType = argtp->pzNo;   break;
        case OPARG_TYPE_ENUMERATION: pzArgType = argtp->pzKey;  break;
        case OPARG_TYPE_FILE:        pzArgType = argtp->pzFile; break;
        case OPARG_TYPE_MEMBERSHIP:  pzArgType = argtp->pzKeyL; break;
        case OPARG_TYPE_BOOLEAN:     pzArgType = argtp->pzBool; break;
        case OPARG_TYPE_NUMERIC:     pzArgType = argtp->pzNum;  break;
        case OPARG_TYPE_HIERARCHY:   pzArgType = argtp->pzNest; break;
        case OPARG_TYPE_STRING:      pzArgType = argtp->pzStr;  break;
        case OPARG_TYPE_TIME:        pzArgType = argtp->pzTime; break;
        default:                     goto bogus_desc;
        }

        pzArgType = SPN_WHITESPACE_CHARS(pzArgType);
        if (*pzArgType == NUL)
            snprintf(z, sizeof(z), "%s", od->pz_Name);
        else
            snprintf(z, sizeof(z), "%s=%s", od->pz_Name, pzArgType);
        fprintf(option_usage_fp, usefmt, z, od->pzText);

        switch (OPTST_GET_ARGTYPE(od->fOptState)) {
        case OPARG_TYPE_ENUMERATION:
        case OPARG_TYPE_MEMBERSHIP:
            displayEnum = (od->pOptProc != NULL) ? true : displayEnum;
        }
    }

    return;

 bogus_desc:
    fprintf(stderr, zbad_od, opts->pzProgName, od->pz_Name);
    ao_bug(zbad_arg_type_msg);
}

/**
 * Print the long options processed with "-W".  These options will be the
 * ones that do *not* have flag characters.
 *
 * @param opts  the program option descriptor
 * @param title the title for the options
 */
static void
prt_vendor_opts(tOptions * opts, char const * title)
{
    static unsigned int const not_vended_mask =
        OPTST_NO_USAGE_MASK | OPTST_DOCUMENT;

    static char const vfmtfmt[] = "%%-%us %%s\n";
    char vfmt[sizeof(vfmtfmt)];

    /*
     *  Only handle client specified options.  The "vendor option" follows
     *  "presetOptCt", so we won't loop/recurse indefinitely.
     */
    int          ct     = opts->presetOptCt;
    tOptDesc *   od     = opts->pOptDesc;
    fprintf(option_usage_fp, zTabout + tab_skip_ct, zVendOptsAre);

    {
        size_t   nmlen  = 0;
        do  {
            size_t l;
            if (  ((od->fOptState & not_vended_mask) != 0)
               || GRAPH_CH(od->optValue))
                continue;

            l = strlen(od->pz_Name);
            if (l > nmlen)  nmlen = l;
        } while (od++, (--ct > 0));

        snprintf(vfmt, sizeof(vfmt), vfmtfmt, (unsigned int)nmlen + 4);
    }

    if (tab_skip_ct > 0)
        tab_skip_ct--;

    ct    = opts->presetOptCt;
    od    = opts->pOptDesc;

    do  {
        if (  ((od->fOptState & not_vended_mask) != 0)
           || GRAPH_CH(od->optValue))
            continue;

        prt_one_vendor(opts, od, &argTypes, vfmt);
        prt_extd_usage(opts, od, title);

    } while (od++, (--ct > 0));

    /* no need to restore "tab_skip_ct" - options are done now */
}

/**
 * Print extended usage.  Usage/help was requested.
 *
 * @param opts  the program option descriptor
 * @param od   the option descriptor
 * @param title the title for the options
 */
static void
prt_extd_usage(tOptions * opts, tOptDesc * od, char const * title)
{
    if (  ((opts->fOptSet & OPTPROC_VENDOR_OPT) != 0)
       && (od->optActualValue == VENDOR_OPTION_VALUE)) {
        prt_vendor_opts(opts, title);
        return;
    }

    /*
     *  IF there are option conflicts or dependencies,
     *  THEN print them here.
     */
    if ((od->pOptMust != NULL) || (od->pOptCant != NULL))
        prt_conflicts(opts, od);

    /*
     *  IF there is a disablement string
     *  THEN print the disablement info
     */
    if (od->pz_DisableName != NULL )
        fprintf(option_usage_fp, zDis + tab_skip_ct, od->pz_DisableName);

    /*
     *  Check for argument types that have callbacks with magical properties
     */
    switch (OPTST_GET_ARGTYPE(od->fOptState)) {
    case OPARG_TYPE_NUMERIC:
        /*
         *  IF the numeric option has a special callback,
         *  THEN call it, requesting the range or other special info
         */
        if (  (od->pOptProc != NULL)
           && (od->pOptProc != optionNumericVal) ) {
            (*(od->pOptProc))(OPTPROC_EMIT_USAGE, od);
        }
        break;

    case OPARG_TYPE_FILE:
        (*(od->pOptProc))(OPTPROC_EMIT_USAGE, od);
        break;
    }

    /*
     *  IF the option defaults to being enabled,
     *  THEN print that out
     */
    if (od->fOptState & OPTST_INITENABLED)
        fputs(zEnab + tab_skip_ct, option_usage_fp);

    /*
     *  IF  the option is in an equivalence class
     *        AND not the designated lead
     *  THEN print equivalence and leave it at that.
     */
    if (  (od->optEquivIndex != NO_EQUIVALENT)
       && (od->optEquivIndex != od->optActualIndex )  )  {
        fprintf(option_usage_fp, zalt_opt + tab_skip_ct,
                 opts->pOptDesc[ od->optEquivIndex ].pz_Name);
        return;
    }

    /*
     *  IF this particular option can NOT be preset
     *    AND some form of presetting IS allowed,
     *    AND it is not an auto-managed option (e.g. --help, et al.)
     *  THEN advise that this option may not be preset.
     */
    if (  ((od->fOptState & OPTST_NO_INIT) != 0)
       && (  (opts->papzHomeList != NULL)
          || (opts->pzPROGNAME != NULL)
          )
       && (od->optIndex < opts->presetOptCt)
       )

        fputs(zNoPreset + tab_skip_ct, option_usage_fp);

    /*
     *  Print the appearance requirements.
     */
    if (OPTST_GET_ARGTYPE(od->fOptState) == OPARG_TYPE_MEMBERSHIP)
        fputs(zMembers + tab_skip_ct, option_usage_fp);

    else switch (od->optMinCt) {
    case 1:
    case 0:
        switch (od->optMaxCt) {
        case 0:       fputs(zPreset + tab_skip_ct, option_usage_fp); break;
        case NOLIMIT: fputs(zNoLim  + tab_skip_ct, option_usage_fp); break;
        case 1:       break;
            /*
             * IF the max is more than one but limited, print "UP TO" message
             */
        default:
            fprintf(option_usage_fp, zUpTo + tab_skip_ct, od->optMaxCt); break;
        }
        break;

    default:
        /*
         *  More than one is required.  Print the range.
         */
        fprintf(option_usage_fp, zMust + tab_skip_ct,
                od->optMinCt, od->optMaxCt);
    }

    if (  NAMED_OPTS(opts)
       && (opts->specOptIdx.default_opt == od->optIndex))
        fputs(zDefaultOpt + tab_skip_ct, option_usage_fp);
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
 * Figure out where all the initialization files might live.  This requires
 * translating some environment variables and testing to see if a name is a
 * directory or a file.  It's squishy, but important to tell users how to
 * find these files.
 *
 * @param[in]  papz        search path
 * @param[out] ini_file    an output buffer of AG_PATH_MAX+1 bytes
 * @param[in]  path_nm     the name of the file we're hunting for
 */
static void
prt_ini_list(char const * const * papz, char const * ini_file,
             char const * path_nm)
{
    char pth_buf[AG_PATH_MAX+1];

    fputs(zPresetIntro, option_usage_fp);

    for (;;) {
        char const * path   = *(papz++);
        char const * nm_buf = pth_buf;

        if (path == NULL)
            break;

        /*
         * Ignore any invalid paths
         */
        if (! optionMakePath(pth_buf, (int)sizeof(pth_buf), path, path_nm))
            nm_buf = path;

        /*
         * Expand paths that are relative to the executable or installation
         * directories.  Leave alone paths that use environment variables.
         */
        else if ((*path == '$')
                 && ((path[1] == '$') || (path[1] == '@')))
            path = nm_buf;

        /*
         *  Print the name of the "homerc" file.  If the "rcfile" name is
         *  not empty, we may or may not print that, too...
         */
        fprintf(option_usage_fp, zPathFmt, path);
        if (*ini_file != NUL) {
            struct stat sb;

            /*
             *  IF the "homerc" file is a directory,
             *  then append the "rcfile" name.
             */
            if ((stat(nm_buf, &sb) == 0) && S_ISDIR(sb.st_mode)) {
                fputc(DIRCH,    option_usage_fp);
                fputs(ini_file, option_usage_fp);
            }
        }

        fputc(NL, option_usage_fp);
    }
}

/**
 *  Print the usage line preamble text
 *
 * @param opts  the program option descriptor
 * @param od    the option descriptor
 * @param at    names of the option argument types
 */
static void
prt_preamble(tOptions * opts, tOptDesc * od, arg_types_t * at)
{
    /*
     *  Flag prefix: IF no flags at all, then omit it.  If not printable
     *  (not allowed for this option), then blank, else print it.
     *  Follow it with a comma if we are doing GNU usage and long
     *  opts are to be printed too.
     */
    if ((opts->fOptSet & OPTPROC_SHORTOPT) == 0)
        fputs(at->pzSpc, option_usage_fp);

    else if (! GRAPH_CH(od->optValue)) {
        if (  (opts->fOptSet & (OPTPROC_GNUUSAGE|OPTPROC_LONGOPT))
           == (OPTPROC_GNUUSAGE|OPTPROC_LONGOPT))
            fputc(' ', option_usage_fp);
        fputs(at->pzNoF, option_usage_fp);

    } else {
        fprintf(option_usage_fp, "   -%c", od->optValue);
        if (  (opts->fOptSet & (OPTPROC_GNUUSAGE|OPTPROC_LONGOPT))
           == (OPTPROC_GNUUSAGE|OPTPROC_LONGOPT))
            fputs(", ", option_usage_fp);
    }
}

/**
 *  Print the usage information for a single option.
 *
 * @param opts  the program option descriptor
 * @param od    the option descriptor
 * @param at    names of the option argument types
 */
static void
prt_one_usage(tOptions * opts, tOptDesc * od, arg_types_t * at)
{
    prt_preamble(opts, od, at);

    {
        char z[80];
        char const * atyp;

        /*
         *  Determine the argument type string first on its usage, then,
         *  when the option argument is required, base the type string on the
         *  argument type.
         */
        if (od->fOptState & OPTST_ARG_OPTIONAL) {
            atyp = at->pzOpt;

        } else switch (OPTST_GET_ARGTYPE(od->fOptState)) {
        case OPARG_TYPE_NONE:        atyp = at->pzNo;   break;
        case OPARG_TYPE_ENUMERATION: atyp = at->pzKey;  break;
        case OPARG_TYPE_FILE:        atyp = at->pzFile; break;
        case OPARG_TYPE_MEMBERSHIP:  atyp = at->pzKeyL; break;
        case OPARG_TYPE_BOOLEAN:     atyp = at->pzBool; break;
        case OPARG_TYPE_NUMERIC:     atyp = at->pzNum;  break;
        case OPARG_TYPE_HIERARCHY:   atyp = at->pzNest; break;
        case OPARG_TYPE_STRING:      atyp = at->pzStr;  break;
        case OPARG_TYPE_TIME:        atyp = at->pzTime; break;
        default:                     goto bogus_desc;
        }

#ifdef _WIN32
        if (at->pzOptFmt == zGnuOptFmt)
            snprintf(z, sizeof(z), "--%s%s", od->pz_Name, atyp);
        else if (at->pzOptFmt == zGnuOptFmt + 2)
            snprintf(z, sizeof(z), "%s%s", od->pz_Name, atyp);
        else
#endif
        snprintf(z, sizeof(z), at->pzOptFmt, atyp, od->pz_Name,
                 (od->optMinCt != 0) ? at->pzReq : at->pzOpt);

        fprintf(option_usage_fp, line_fmt_buf, z, od->pzText);

        switch (OPTST_GET_ARGTYPE(od->fOptState)) {
        case OPARG_TYPE_ENUMERATION:
        case OPARG_TYPE_MEMBERSHIP:
            displayEnum = (od->pOptProc != NULL) ? true : displayEnum;
        }
    }

    return;

 bogus_desc:
    fprintf(stderr, zbad_od, opts->pzProgName, od->pz_Name);
    option_exits(EX_SOFTWARE);
}

/**
 *  Print out the usage information for just the options.
 */
static void
prt_opt_usage(tOptions * opts, int ex_code, char const * title)
{
    int         ct     = opts->optCt;
    int         optNo  = 0;
    tOptDesc *  od     = opts->pOptDesc;
    int         docCt  = 0;

    do  {
        /*
         * no usage --> disallowed on command line (OPTST_NO_COMMAND), or
         * deprecated -- strongly discouraged (OPTST_DEPRECATED), or
         * compiled out of current object code (OPTST_OMITTED)
         */
        if ((od->fOptState & OPTST_NO_USAGE_MASK) != 0) {

            /*
             * IF      this is a compiled-out option
             *   *AND* usage was requested with "omitted-usage"
             *   *AND* this is NOT abbreviated usage
             * THEN display this option.
             */
            if (  (od->fOptState == (OPTST_OMITTED | OPTST_NO_INIT))
               && (od->pz_Name != NULL)
               && (ex_code == EXIT_SUCCESS))  {

                char const * why_pz =
                    (od->pzText == NULL) ? zDisabledWhy : od->pzText;
                prt_preamble(opts, od, &argTypes);
                fprintf(option_usage_fp, zDisabledOpt, od->pz_Name, why_pz);
            }

            continue;
        }

        if ((od->fOptState & OPTST_DOCUMENT) != 0) {
            if (ex_code == EXIT_SUCCESS) {
                fprintf(option_usage_fp, argTypes.pzBrk, od->pzText,
                        title);
                docCt++;
            }

            continue;
        }

        /* Skip name only options when we have a vendor option */
        if (  ((opts->fOptSet & OPTPROC_VENDOR_OPT) != 0)
           && (! GRAPH_CH(od->optValue)))
            continue;

        /*
         *  IF       this is the first auto-opt maintained option
         *    *AND*  we are doing a full help
         *    *AND*  there are documentation options
         *    *AND*  the last one was not a doc option,
         *  THEN document that the remaining options are not user opts
         */
        if ((docCt > 0) && (ex_code == EXIT_SUCCESS)) {
            if (opts->presetOptCt == optNo) {
                if ((od[-1].fOptState & OPTST_DOCUMENT) == 0)
                    fprintf(option_usage_fp, argTypes.pzBrk, zAuto, title);

            } else if ((ct == 1) &&
                       (opts->fOptSet & OPTPROC_VENDOR_OPT))
                fprintf(option_usage_fp, argTypes.pzBrk, zVendIntro, title);
        }

        prt_one_usage(opts, od, &argTypes);

        /*
         *  IF we were invoked because of the --help option,
         *  THEN print all the extra info
         */
        if (ex_code == EXIT_SUCCESS)
            prt_extd_usage(opts, od, title);

    } while (od++, optNo++, (--ct > 0));

    fputc(NL, option_usage_fp);
}


/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
 *  Print program details.
 * @param[in] opts  the program option descriptor
 */
static void
prt_prog_detail(tOptions * opts)
{
    bool need_intro = (opts->papzHomeList == NULL);

    /*
     *  Display all the places we look for config files, if we have
     *  a list of directories to search.
     */
    if (! need_intro)
        prt_ini_list(opts->papzHomeList, opts->pzRcName, opts->pzProgPath);

    /*
     *  Let the user know about environment variable settings
     */
    if ((opts->fOptSet & OPTPROC_ENVIRON) != 0) {
        if (need_intro)
            fputs(zPresetIntro, option_usage_fp);

        fprintf(option_usage_fp, zExamineFmt, opts->pzPROGNAME);
    }

    /*
     *  IF we found an enumeration,
     *  THEN hunt for it again.  Call the handler proc with a NULL
     *       option struct pointer.  That tells it to display the keywords.
     */
    if (displayEnum) {
        int        ct     = opts->optCt;
        int        optNo  = 0;
        tOptDesc * od     = opts->pOptDesc;

        fputc(NL, option_usage_fp);
        fflush(option_usage_fp);
        do  {
            switch (OPTST_GET_ARGTYPE(od->fOptState)) {
            case OPARG_TYPE_ENUMERATION:
            case OPARG_TYPE_MEMBERSHIP:
                (*(od->pOptProc))(OPTPROC_EMIT_USAGE, od);
            }
        } while (od++, optNo++, (--ct > 0));
    }

    /*
     *  If there is a detail string, now is the time for that.
     */
    if (opts->pzDetail != NULL)
        fputs(opts->pzDetail, option_usage_fp);
}


/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 *   OPTION LINE FORMATTING SETUP
 *
 *  The "OptFmt" formats receive three arguments:
 *  1.  the type of the option's argument
 *  2.  the long name of the option
 *  3.  "YES" or "no ", depending on whether or not the option must appear
 *      on the command line.
 *  These formats are used immediately after the option flag (if used) has
 *  been printed.
 *
 *  Set up the formatting for GNU-style output
 */
static int
setGnuOptFmts(tOptions * opts, char const ** ptxt)
{
    static char const zOneSpace[] = " ";
    int  flen = 22;
    *ptxt = zNoRq_ShrtTtl;

    argTypes.pzStr  = zGnuStrArg;
    argTypes.pzReq  = zOneSpace;
    argTypes.pzNum  = zGnuNumArg;
    argTypes.pzKey  = zGnuKeyArg;
    argTypes.pzKeyL = zGnuKeyLArg;
    argTypes.pzTime = zGnuTimeArg;
    argTypes.pzFile = zGnuFileArg;
    argTypes.pzBool = zGnuBoolArg;
    argTypes.pzNest = zGnuNestArg;
    argTypes.pzOpt  = zGnuOptArg;
    argTypes.pzNo   = zOneSpace;
    argTypes.pzBrk  = zGnuBreak;
    argTypes.pzNoF  = zSixSpaces;
    argTypes.pzSpc  = zThreeSpaces;

    switch (opts->fOptSet & OPTPROC_L_N_S) {
    case OPTPROC_L_N_S:    argTypes.pzOptFmt = zGnuOptFmt;     break;
    case OPTPROC_LONGOPT:  argTypes.pzOptFmt = zGnuOptFmt;     break;
    case 0:                argTypes.pzOptFmt = zGnuOptFmt + 2; break;
    case OPTPROC_SHORTOPT:
        argTypes.pzOptFmt = zShrtGnuOptFmt;
        zGnuStrArg[0] = zGnuNumArg[0] = zGnuKeyArg[0] = zGnuBoolArg[0] = ' ';
        argTypes.pzOpt = " [arg]";
        flen = 8;
        break;
    }

    return flen;
}


/*
 *  Standard (AutoOpts normal) option line formatting
 */
static int
setStdOptFmts(tOptions * opts, char const ** ptxt)
{
    int  flen = 0;

    argTypes.pzStr  = zStdStrArg;
    argTypes.pzReq  = zStdReqArg;
    argTypes.pzNum  = zStdNumArg;
    argTypes.pzKey  = zStdKeyArg;
    argTypes.pzKeyL = zStdKeyLArg;
    argTypes.pzTime = zStdTimeArg;
    argTypes.pzFile = zStdFileArg;
    argTypes.pzBool = zStdBoolArg;
    argTypes.pzNest = zStdNestArg;
    argTypes.pzOpt  = zStdOptArg;
    argTypes.pzNo   = zStdNoArg;
    argTypes.pzBrk  = zStdBreak;
    argTypes.pzNoF  = zFiveSpaces;
    argTypes.pzSpc  = zTwoSpaces;

    switch (opts->fOptSet & (OPTPROC_NO_REQ_OPT | OPTPROC_SHORTOPT)) {
    case (OPTPROC_NO_REQ_OPT | OPTPROC_SHORTOPT):
        *ptxt = zNoRq_ShrtTtl;
        argTypes.pzOptFmt = zNrmOptFmt;
        flen = 19;
        break;

    case OPTPROC_NO_REQ_OPT:
        *ptxt = zNoRq_NoShrtTtl;
        argTypes.pzOptFmt = zNrmOptFmt;
        flen = 19;
        break;

    case OPTPROC_SHORTOPT:
        *ptxt = zReq_ShrtTtl;
        argTypes.pzOptFmt = zReqOptFmt;
        flen = 24;
        break;

    case 0:
        *ptxt = zReq_NoShrtTtl;
        argTypes.pzOptFmt = zReqOptFmt;
        flen = 24;
    }

    return flen;
}

/** @}
 *
 * Local Variables:
 * mode: C
 * c-file-style: "stroustrup"
 * indent-tabs-mode: nil
 * End:
 * end of autoopts/usage.c */
