/*
 *	libHX/opt.c
 *	Copyright Jan Engelhardt, 2002-2011
 *
 *	This file is part of libHX. libHX is free software; you can
 *	redistribute it and/or modify it under the terms of the GNU Lesser
 *	General Public License as published by the Free Software Foundation;
 *	either version 2.1 or (at your option) any later version.
 */
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libHX/ctype_helper.h>
#include <libHX/deque.h>
#include <libHX/map.h>
#include <libHX/misc.h>
#include <libHX/option.h>
#include <libHX/string.h>
#include "internal.h"

/* Definitions */
#define C_OPEN  '('
#define C_CLOSE ')'
#define NTYPE_S(con, tpx) NTYPE((con), tpx, strtol)
#define NTYPE_U(con, tpx) NTYPE((con), tpx, strtoul)

#define NTYPE(con, tpx, func) case (con): { \
	tpx *p, v = (func)(cbi->data, NULL, 0); \
	if ((p = opt->ptr) != NULL) { \
		if (opt->type & HXOPT_NOT) \
			v = ~v; \
		switch (opt->type & HXOPT_LOPMASK2) { \
			case 0:         *p  = v; break; \
			case HXOPT_OR:  *p |= v; break; \
			case HXOPT_AND: *p &= v; break; \
			case HXOPT_XOR: *p ^= v; break; \
			default: \
				fprintf(stderr, "libHX-opt: illegal " \
				        "combination of logical op mask\n"); \
				break; \
		} \
	} \
	cbi->data_long = v; \
	break; \
}

#define SCREEN_WIDTH 80 /* fine, popt also has it hardcoded */

enum {

	W_NONE    = 0,
	W_SPACE   = 1 << 0,
	W_BRACKET = 1 << 1,
	W_ALT     = 1 << 2,
	W_EQUAL   = 1 << 3,

	HXOPT_LOPMASK2 = HXOPT_OR | HXOPT_AND | HXOPT_XOR,
	HXOPT_LOPMASK  = HXOPT_LOPMASK2 | HXOPT_NOT,
	HXOPT_TYPEMASK = 0x1F, /* 5 bits */
};

/**
 * HX_getopt_error - internal option parser error codes
 * %HXOPT_E_LONG_UNKNOWN:	unknown long option
 * %HXOPT_E_LONG_TAKESVOID:	long option was used with an arg (--long=arg)
 * %HXOPT_E_LONG_MISSING:	long option requires an argument
 * %HXOPT_E_SHORT_UNKNOWN:	unknown short option
 * %HXOPT_E_SHORT_MISSING:	short option requires an argument
 * %HXOPT_E_AMBIG_PREFIX:	used abbreviated long option but had multiple results
 */
enum {
	HXOPT_E_LONG_UNKNOWN = 1,
	HXOPT_E_LONG_TAKESVOID,
	HXOPT_E_LONG_MISSING,
	HXOPT_E_SHORT_UNKNOWN,
	HXOPT_E_SHORT_MISSING,
	HXOPT_E_AMBIG_PREFIX,
};

/**
 * HX_getopt_state - internal option parser states
 * %HXOPT_S_NORMAL:	base state, options accepted
 * %HXOPT_S_SHORT:	a short option has been seen
 * %HXOPT_S_TWOLONG:	a long option has been seen
 * %HXOPT_S_LONG:	a long option and its argument have been seen
 * %HXOPT_S_TERMINATED:	options closed, all remaining args are to be copied
 */
enum HX_getopt_state {
	HXOPT_S_NORMAL = 0,
	HXOPT_S_SHORT,
	HXOPT_S_TWOLONG,
	HXOPT_S_LONG,
	HXOPT_S_TERMINATED,
};

/**
 * %HXOPT_I_ASSIGN:	call do_assign
 * %HXOPT_I_ADVARG:	advance to next argument in @opt
 * %HXOPT_I_ADVARG2:	advance by two arguments in @opt
 * %HXOPT_I_ADVCHAR:	advance to next character in @cur
 * %HXOPT_I_ERROR:	HXoption error
 */
enum {
	HXOPT_I_ASSIGN  = 1 << 3,
	HXOPT_I_ADVARG  = 1 << 4,
	HXOPT_I_ADVARG2 = 1 << 5,
	HXOPT_I_ADVCHAR = 1 << 6,
	HXOPT_I_ERROR   = 1 << (sizeof(int) * CHAR_BIT - 2),

	HXOPT_I_MASK    = HXOPT_I_ADVARG | HXOPT_I_ADVARG2 | HXOPT_I_ADVCHAR |
	                  HXOPT_I_ASSIGN | HXOPT_I_ERROR,
};

/**
 * struct HX_getopt_vars - option parser working variable set
 * @arg0:	saved program name
 * @remaining:	list of extracted non-options
 * @cbi:	callback info
 * @flags:	flags setting the behavior for HX_getopt
 */
struct HX_getopt_vars {
	const char *arg0;
	struct HXdeque *remaining;
	struct HXoptcb cbi;
	unsigned int flags;
};

struct HXoption HXopt_ambig_prefix;

static bool posix_me_harder(void)
{
	const char *s;
	char *end;
	int res;

	s = getenv("POSIXLY_CORRECT");
	if (s == NULL || *s == '\0')
		return false;
	res = strtol(s, &end, 0);
	if (end != s)
		/* number */
		return res;
	return true; /* non-empty string */
}

static void do_assign(struct HXoptcb *cbi, const char *arg0)
{
	const struct HXoption *opt = cbi->current;

	switch (opt->type & HXOPT_TYPEMASK) {
	case HXTYPE_NONE: {
		if (opt->ptr != NULL) {
			int *p = opt->ptr;
			if (opt->type & HXOPT_INC)      ++*p;
			else if (opt->type & HXOPT_DEC) --*p;
			else                            *p = 1;
		}
		cbi->data_long = 1;
		break;
	}
	case HXTYPE_VAL:
		*static_cast(int *, opt->ptr) = cbi->data_long = opt->val;
		break;
	case HXTYPE_SVAL:
		*static_cast(const char **, opt->ptr) = cbi->data = opt->uptr;
		break;
	case HXTYPE_BOOL: {
		int *p;
		if ((p = opt->ptr) != NULL)
			*p = strcasecmp(cbi->data, "yes") == 0 ||
			     strcasecmp(cbi->data, "on") == 0 ||
			     strcasecmp(cbi->data, "true") == 0 ||
			     (HX_isdigit(*cbi->data) &&
			     strtoul(cbi->data, NULL, 0) != 0);
		break;
	}
	case HXTYPE_BYTE:
		*static_cast(unsigned char *, opt->ptr) = *cbi->data;
		break;

	NTYPE_U(HXTYPE_UCHAR,  unsigned char);
	NTYPE_S(HXTYPE_CHAR,   char);
	NTYPE_U(HXTYPE_USHORT, unsigned short);
	NTYPE_S(HXTYPE_SHORT,  short);
	NTYPE_U(HXTYPE_UINT,   unsigned int);
	NTYPE_S(HXTYPE_INT,    int);
	NTYPE_U(HXTYPE_ULONG,  unsigned long);
	NTYPE_S(HXTYPE_LONG,   long);
	NTYPE_U(HXTYPE_UINT8,  uint8_t);
	NTYPE_S(HXTYPE_INT8,   int8_t);
	NTYPE_U(HXTYPE_UINT16, uint16_t);
	NTYPE_S(HXTYPE_INT16,  int16_t);
	NTYPE_U(HXTYPE_UINT32, uint32_t);
	NTYPE_S(HXTYPE_INT32,  int32_t);
#ifndef _MSC_VER
	NTYPE(HXTYPE_ULLONG,   unsigned long long, strtoull);
	NTYPE(HXTYPE_LLONG,    long long, strtoll);
	NTYPE(HXTYPE_UINT64,   uint64_t, strtoull);
	NTYPE(HXTYPE_INT64,    int64_t, strtoll);
#endif
	NTYPE(HXTYPE_SIZE_T,   size_t, strtoull);
	case HXTYPE_FLOAT:
		cbi->data_dbl = strtod(cbi->data, NULL);
		if (opt->ptr != NULL)
			*static_cast(float *, opt->ptr) = cbi->data_dbl;
		break;
	case HXTYPE_DOUBLE:
		cbi->data_dbl = strtod(cbi->data, NULL);
		if (opt->ptr != NULL)
			*static_cast(double *, opt->ptr) = cbi->data_dbl;
		break;
	case HXTYPE_STRING:
		if (opt->ptr != NULL)
			*static_cast(char **, opt->ptr) = HX_strdup(cbi->data);
		break;
	case HXTYPE_STRDQ:
		HXdeque_push(opt->ptr, HX_strdup(cbi->data));
		break;
	case HXTYPE_MCSTR:
		if (opt->ptr != NULL)
			HXmc_strcpy(opt->ptr, cbi->data);
		break;
	case HXTYPE_XHELP:
		cbi->data = arg0;
		break;
	default:
		fprintf(stderr, "libHX-opt: illegal type %d\n",
		        opt->type & HXOPT_TYPEMASK);
		break;
	} /* switch */
	if (opt->cb != NULL)
		opt->cb(cbi);
}

static __inline__ const struct HXoption *
lookup_short(const struct HXoption *table, char opt)
{
	for (; table->type != HXTYPE_XSNTMARK; ++table)
		if (table->sh == opt)
			return table;
	return NULL;
}

static const struct HXoption *
lookup_long_pfx(const struct HXoption *table, const char *key)
{
	const struct HXoption *cand = NULL;
	size_t klen = strlen(key);

	for (; table->type != HXTYPE_XSNTMARK; ++table) {
		if (table->ln == NULL)
			continue;
		if (strncmp(table->ln, key, klen) != 0)
			continue;
		/* Prefix match */
		if (table->ln[klen] == '\0')
			return table; /* Exact match */
		if (cand != NULL)
			return &HXopt_ambig_prefix;
		cand = table;
	}
	return cand;
}

static __inline__ const struct HXoption *
lookup_long(const struct HXoption *table, const char *key)
{
	for (; table->type != HXTYPE_XSNTMARK; ++table)
		if (table->ln != NULL && strcmp(table->ln, key) == 0)
			return table;
	return NULL;
}

static __inline__ bool takes_void(unsigned int t)
{
	t &= HXOPT_TYPEMASK;
	return t == HXTYPE_NONE || t == HXTYPE_VAL || t == HXTYPE_SVAL ||
	       t == HXTYPE_XSNTMARK || t == HXTYPE_XHELP;
}

static void opt_to_text(const struct HXoption *opt, char *buf, size_t len,
    unsigned int flags)
{
	const char *alt, *htyp = (opt->htyp != NULL) ? opt->htyp : "ARG";
	size_t i = 0;
	char equ;

	if (flags & W_SPACE)   buf[i++] = ' ';
	if (flags & W_BRACKET) buf[i++] = '['; /* ] */
	if (flags & W_ALT) {
		alt = "|";
		equ = (flags & W_EQUAL) ? '=' : ' ';
	} else {
		alt = ", ";
		equ = '=';
	}

	if (opt->ln == NULL) {
		buf[i++] = '-';
		buf[i++] = opt->sh;
		if (!takes_void(opt->type))
			i += snprintf(buf + i, len - i, " %s", htyp);
	} else if (opt->sh == '\0') {
		if (takes_void(opt->type))
			i += snprintf(buf + i, len - i,
			     "--%s", opt->ln);
		else
			i += snprintf(buf + i, len - i,
			     "--%s=%s", opt->ln, htyp);
	} else {
		if (takes_void(opt->type))
			i += snprintf(buf + i, len - i, "-%c%s--%s",
			     opt->sh, alt, opt->ln);
		else
			i += snprintf(buf + i, len - i, "-%c%s--%s%c%s",
			     opt->sh, alt, opt->ln, equ, htyp);
	}

	if (flags & W_BRACKET)
		buf[i++] = ']';
	buf[i] = '\0';
}

static void print_indent(const char *msg, unsigned int ind, FILE *fp)
{
	size_t rest = SCREEN_WIDTH - ind;
	char *p;

	while (true) {
		if (strlen(msg) < rest) {
			fprintf(fp, "%s", msg);
			break;
		}
		if ((p = HX_strbchr(msg, msg + rest, ' ')) == NULL) {
			fprintf(fp, "%s", msg);
			break;
		}
		fprintf(fp, "%.*s\n%*s", static_cast(unsigned int, p - msg),
		        msg, ind, "");
		msg  = p + 1;
		rest = SCREEN_WIDTH - ind;
	}
	fprintf(fp, "\n");
}

/**
 * HXparse_dequote_int - shell-style argument unescape
 * @o:		input/output string
 * @end:	terminating characters
 *
 * Unescapes a quoted argument, in-place.
 * Returns a pointer to one position after the termination character.
 */
static char *HXparse_dequote_int(char *o, const char *end)
{
	char *i, quot = '\0';
	for (i = o; *i != '\0'; ) {
		if (quot == '\0') {
			switch (*i) {
				case '"':
				case '\'':
					quot = *i++;
					continue;
				case '\\':
					if (*++i != '\0')
						*o++ = *i++;
					continue;
			}
			if (end != NULL && strchr(end, *i) != NULL) {
				*o = '\0';
				return i + 1;
			}
			*o++ = *i++;
			continue;
		}
		if (*i == quot) {
			quot = 0;
			++i;
			continue;
		} else if (*i == '\\') {
			if (*++i != '\0')
				*o++ = *i++;
			continue;
		}
		*o++ = *i++;
	}
	*o = '\0';
	return NULL;
}

/**
 * HXparse_dequote_fmt
 * @s:		Input string
 * @end:	Terminating characters. May be %NULL.
 * @pptr:	Return pointer
 *
 * Dequote a string @s until @end, and return an allocated string that will
 * contain the result, or %NULL on error. @*pptr will then point to the
 * terminating character.
 * Nested %() are honored.
 *
 * (This function is used from format.c. It is here in opt.c to call
 * HXparse_dequote_int.)
 */
hxmc_t *HXparse_dequote_fmt(const char *s, const char *end, const char **pptr)
{
	unsigned int level = 0; /* nesting */
	const char *i;
	char quot = '\0';
	hxmc_t *tmp;

	/* Search for end */
	for (i = s; *i != '\0'; ) {
		if (quot == '\0') {
			switch (*i) {
				case '"':
				case '\'':
					quot = *i++;
					continue;
				case '\\':
					if (i[1] != '\0')
						i += 2;
					continue;
				case C_OPEN:
					++level;
					++i;
					continue;
			}
			if (level == 0 && end != NULL &&
			    strchr(end, *i) != NULL)
				break;
			if (i[0] == C_CLOSE && level > 0)
				--level;
			++i;
			continue;
		}
		if (*i == quot) {
			quot = 0;
			++i;
			continue;
		} else if (*i == '\\') {
			if (*++i != '\0')
				++i;
			continue;
		}
		++i;
	}

	if (pptr != NULL)
		*pptr = i;
	tmp = HXmc_meminit(s, i - s);
	if (tmp == NULL)
		return NULL;
	HXparse_dequote_int(tmp, NULL);
	return tmp;
}

static int HX_getopt_error(int err, const char *key, unsigned int flags)
{
	switch (err) {
	case HXOPT_E_LONG_UNKNOWN:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Unknown option: %s\n", key);
		return HXOPT_I_ERROR | HXOPT_ERR_UNKN;
	case HXOPT_E_LONG_TAKESVOID:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Option %s does not take "
			        "any argument\n", key);
		return HXOPT_I_ERROR | HXOPT_ERR_VOID;
	case HXOPT_E_LONG_MISSING:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Option %s requires an "
			        "argument\n", key);
		return HXOPT_I_ERROR | HXOPT_ERR_MIS;
	case HXOPT_E_SHORT_UNKNOWN:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Unknown option: -%c\n", *key);
		return HXOPT_I_ERROR | HXOPT_ERR_UNKN;
	case HXOPT_E_SHORT_MISSING:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Option -%c requires an "
			        "argument\n", *key);
		return HXOPT_I_ERROR | HXOPT_ERR_MIS;
	case HXOPT_E_AMBIG_PREFIX:
		if (!(flags & HXOPT_QUIET))
			fprintf(stderr, "Option %s is ambiguous\n", key);
		return HXOPT_I_ERROR | HXOPT_ERR_AMBIG;
	}
	return HXOPT_I_ERROR;
}

static int HX_getopt_twolong(const char *const *opt,
    struct HX_getopt_vars *par)
{
	const char *key = opt[0], *value = opt[1];

	par->cbi.current = lookup_long_pfx(par->cbi.table, key + 2);
	if (par->cbi.current == &HXopt_ambig_prefix)
		return HX_getopt_error(HXOPT_E_AMBIG_PREFIX, key, par->flags);
	if (par->cbi.current == NULL) {
		if (par->flags & HXOPT_PTHRU) {
			char *tmp = HX_strdup(key);
			if (tmp == NULL)
				return -errno;
			if (HXdeque_push(par->remaining, tmp) == NULL) {
				free(tmp);
				return -errno;
			}
			return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
		}
		return HX_getopt_error(HXOPT_E_LONG_UNKNOWN, key, par->flags);
	}

	par->cbi.flags = HXOPTCB_BY_LONG;
	if (takes_void(par->cbi.current->type)) {
		par->cbi.data = NULL;
		return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG;
	} else if (par->cbi.current->type & HXOPT_OPTIONAL) {
		/* Rule: take arg if next thing is not-null, not-option. */
		if (value == NULL || *value != '-' ||
		    (value[0] == '-' && value[1] == '\0')) {
			/* --file -, --file bla */
			par->cbi.data = value;
			return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG2;
		} else {
			/* --file --another --file -- endofoptions */
			par->cbi.data = NULL;
			return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG;
		}
	} else {
		if (value == NULL)
			return HX_getopt_error(HXOPT_E_LONG_MISSING, key, par->flags);
		par->cbi.data = value;
		return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG2;
	}
}

static int HX_getopt_long(const char *cur, struct HX_getopt_vars *par)
{
	int ret;
	char *key, *value;

	key = HX_strdup(cur);
	if (key == NULL)
		return -errno;
	value = strchr(key, '=');
	if (value == nullptr) {
		/* Cannot happen because state is always !S_TWOLONG */
		free(key);
		return -EINVAL;
	}
	*value++ = '\0';
	par->cbi.current = lookup_long_pfx(par->cbi.table, key + 2);
	if (par->cbi.current == &HXopt_ambig_prefix) {
		ret = HX_getopt_error(HXOPT_E_AMBIG_PREFIX, key, par->flags);
		free(key);
		return ret;
	}
	if (par->cbi.current == NULL) {
		if (par->flags & HXOPT_PTHRU) {
			/* Undo nuke of '=' and reuse alloc */
			value[-1] = '=';
			if (HXdeque_push(par->remaining, key) == NULL) {
				free(key);
				return -errno;
			}
			return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
		}
		ret = HX_getopt_error(HXOPT_E_LONG_UNKNOWN, key, par->flags);
		free(key);
		return ret;
	}
	/*
	 * @value is always non-NULL when entering
	 * %HXOPT_S_LONG, so no need to check for !takes_void.
	 */
	if (takes_void(par->cbi.current->type)) {
		ret = HX_getopt_error(HXOPT_E_LONG_TAKESVOID, key, par->flags);
		free(key);
		return ret;
	}

	par->cbi.flags    = HXOPTCB_BY_LONG;
	par->cbi.data     = value;
	/* Not possible to use %HXOPT_I_ASSIGN due to transience of @key. */
	do_assign(&par->cbi, par->arg0);
	free(key);
	return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
}

static int HX_getopt_short(const char *const *opt, const char *cur,
    struct HX_getopt_vars *par)
{
	char op = *cur;

	if (op == '\0')
		return HXOPT_S_NORMAL | HXOPT_I_ADVARG;

	par->cbi.current = lookup_short(par->cbi.table, op);
	if (par->cbi.current == NULL) {
		if (par->flags & HXOPT_PTHRU) {
			/*
			 * @cur-1 is always valid: it is either the previous
			 * char, or it is '-'.
			 */
			char *buf = HX_strdup(cur - 1);
			if (buf != NULL)
				*buf = '-';
			if (HXdeque_push(par->remaining, buf) == NULL) {
				free(buf);
				return -errno;
			}
			return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
		}
		return HX_getopt_error(HXOPT_E_SHORT_UNKNOWN, &op, par->flags);
	}

	par->cbi.flags = HXOPTCB_BY_SHORT;
	if (takes_void(par->cbi.current->type)) {
		/* -A */
		par->cbi.data = NULL;
		return HXOPT_S_SHORT | HXOPT_I_ASSIGN | HXOPT_I_ADVCHAR;
	} else if (cur[1] != '\0') {
		/* -Avalue */
		par->cbi.data = cur + 1;
		return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG;
	}

	cur = *++opt;
	if (par->cbi.current->type & HXOPT_OPTIONAL) {
		if (cur == NULL || *cur != '-' ||
		    (cur[0] == '-' && cur[1] == '\0')) {
			/* -f - -f bla */
			par->cbi.data = cur;
			return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG2;
		} else {
			/* -f -a-file --another --file -- endofoptions */
			par->cbi.data = NULL;
			return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG;
		}
	} else {
		/* -A value */
		if (cur == NULL)
			return HX_getopt_error(HXOPT_E_SHORT_MISSING, &op, par->flags);
		par->cbi.data = cur;
		return HXOPT_S_NORMAL | HXOPT_I_ASSIGN | HXOPT_I_ADVARG2;
	}
}

static int HX_getopt_term(const char *cur, const struct HX_getopt_vars *par)
{
	char *tmp = HX_strdup(cur);
	if (tmp == NULL)
		return -errno;
	if (HXdeque_push(par->remaining, tmp) == NULL) {
		free(tmp);
		return -errno;
	}
	return HXOPT_S_TERMINATED | HXOPT_I_ADVARG;
}

static int HX_getopt_normal(const char *cur, const struct HX_getopt_vars *par)
{
	if (cur[0] == '-' && cur[1] == '\0') {
		/* Note to popt developers: A single dash is NOT an option! */
		HXdeque_push(par->remaining, HX_strdup(cur));
		return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
	}
	if (cur[0] == '-' && cur[1] == '-' && cur[2] == '\0') {
		/*
		 * Double dash. If passthrough is on, "--" must be copied into
		 * @remaining. This is done in the next round.
		 */
		if (!(par->flags & HXOPT_PTHRU))
			return HXOPT_S_TERMINATED | HXOPT_I_ADVARG;
		return HXOPT_S_TERMINATED;
	}
	if (cur[0] == '-' && cur[1] == '-') { /* long option */
		if (strchr(cur + 2, '=') == NULL)
			return HXOPT_S_TWOLONG;
		/* Single argument long option: --long=arg */
		return HXOPT_S_LONG;
	}
	if (cur[0] == '-')
		/* Short option(s) - one or more(!) */
		return HXOPT_S_SHORT | HXOPT_I_ADVCHAR;
	if (par->flags & HXOPT_RQ_ORDER)
		/* POSIX: first non-option implies option termination */
		return HXOPT_S_TERMINATED;
	cur = HX_strdup(cur);
	if (cur == NULL || HXdeque_push(par->remaining, cur) == NULL)
		return -errno;
	return HXOPT_S_NORMAL | HXOPT_I_ADVARG;
}

EXPORT_SYMBOL int HX_getopt(const struct HXoption *table, int *argc,
    const char ***argv, unsigned int flags)
{
	struct HX_getopt_vars ps;
	const char **opt = *argv;
	int state = HXOPT_S_NORMAL;
	int ret = -ENOMEM;
	unsigned int argk;
	const char *cur;

	memset(&ps, 0, sizeof(ps));
	ps.remaining = HXdeque_init();
	if (ps.remaining == NULL) {
		ret = -errno;
		goto out;
	}
	ps.flags = flags;
	ps.arg0  = **argv;
	ps.cbi.table = table;

	if (*opt != NULL) {
		/* put argv[0] back */
		char *arg = HX_strdup(*opt++);
		if (arg == NULL) {
			ret = -errno;
			goto out;
		}
		if (HXdeque_push(ps.remaining, arg) == NULL) {
			free(arg);
			ret = -errno;
			goto out;
		}
	}

	if (posix_me_harder())
		ps.flags |= HXOPT_RQ_ORDER;
	for (cur = *opt; cur != NULL; ) {
		if (state == HXOPT_S_TWOLONG)
			state = HX_getopt_twolong(opt, &ps);
		else if (state == HXOPT_S_LONG)
			state = HX_getopt_long(cur, &ps);
		else if (state == HXOPT_S_SHORT)
			state = HX_getopt_short(opt, cur, &ps);
		else if (state == HXOPT_S_TERMINATED)
			state = HX_getopt_term(cur, &ps);
		else if (state == HXOPT_S_NORMAL)
			state = HX_getopt_normal(cur, &ps);

		if (state < 0) {
			ret = state;
			goto out;
		}
		if (state & HXOPT_I_ERROR) {
			ret = state & ~HXOPT_I_ERROR;
			goto out;
		}
		if (state & HXOPT_I_ASSIGN)
			do_assign(&ps.cbi, ps.arg0);
		if (state & HXOPT_I_ADVARG)
			cur = *++opt;
		else if (state & HXOPT_I_ADVARG2)
			cur = *(opt += 2);
		else if (state & HXOPT_I_ADVCHAR)
			++cur;
		state &= ~HXOPT_I_MASK;
	}

	if (!(ps.flags & HXOPT_KEEP_ARGV)) {
		const char **nvec = reinterpret_cast(const char **,
		                    HXdeque_to_vec(ps.remaining, &argk));
		if (nvec == NULL) {
			ret = -errno;
			goto out;
		}
		if (ps.flags & HXOPT_DESTROY_OLD)
			/*
			 * Only the "true, original" argv is stored on the
			 * stack - the argv that HX_getopt() produces is on
			 * the heap, so the %HXOPT_DESTROY_OLD flag should be
			 * passed when you use passthrough chaining, i.e. all
			 * but the first call to HX_getopt() should have this
			 * set.
			 */
			HX_zvecfree(const_cast2(char **, *argv));

		*argv = nvec;
		if (argc != NULL)
			*argc = argk;
		/* pointers are owned by nvec/argv now */
		HXdeque_free(ps.remaining);
		ps.remaining = nullptr;
	}
	ret = HXOPT_ERR_SUCCESS;

 out:
	if (ret == HXOPT_ERR_SUCCESS) {
	} else if (ret < 0) {
		if (!(ps.flags & HXOPT_QUIET))
			fprintf(stderr, "%s: %s\n", __func__, strerror(errno));
	} else {
		ps.cbi.data = ps.arg0;
		if (ps.flags & HXOPT_HELPONERR)
			HX_getopt_help(&ps.cbi, stderr);
		else if (ps.flags & HXOPT_USAGEONERR)
			HX_getopt_usage(&ps.cbi, stderr);
	}
	if (ps.remaining != nullptr)
		HXdeque_genocide2(ps.remaining, free);
	return ret;
}

EXPORT_SYMBOL void HX_getopt_help(const struct HXoptcb *cbi, FILE *nfp)
{
	FILE *fp = (nfp == NULL) ? stderr : nfp;
	const struct HXoption *travp;
	char tmp[84] = {'\0'};
	unsigned int tw = 0;

	HX_getopt_usage(cbi, nfp);

	/* Find maximum indent */
	for (travp = cbi->table; travp->type != HXTYPE_XSNTMARK; ++travp) {
		size_t tl;

		opt_to_text(travp, tmp, sizeof(tmp), W_EQUAL);
		if ((tl = strlen(tmp)) > tw)
			tw = tl;
	}

	/* Print table */
	for (travp = cbi->table; travp->type != HXTYPE_XSNTMARK; ++travp) {
		opt_to_text(travp, tmp, sizeof(tmp), W_NONE);
		fprintf(fp, "  %-*s    ", static_cast(int, tw), tmp);
		if (travp->help == NULL)
			fprintf(fp, "\n");
		else
			print_indent(travp->help, tw + 6, fp);
	}
}

EXPORT_SYMBOL void HX_getopt_help_cb(const struct HXoptcb *cbi)
{
	HX_getopt_help(cbi, stdout);
	exit(EXIT_SUCCESS);
}

EXPORT_SYMBOL void HX_getopt_usage(const struct HXoptcb *cbi, FILE *nfp)
{
	size_t wd, tw = 0;
	FILE *fp = (nfp == NULL) ? stderr : nfp;
	const struct HXoption *travp;
	char tmp[84] = {};
	/* Program name now expected in .data */
	const char *arg0 = cbi->data;

	if (arg0 == NULL || *arg0 == '\0')
		arg0 = "($0)";

	wd = sizeof("Usage:") + strlen(arg0);
	fprintf(fp, "Usage: %s", arg0);

	/* Short-only flags */
	if (wd + 5 > SCREEN_WIDTH) {
		/* 5 is the minimum size for a new starting option, " [-X]" */
		fprintf(fp, "\n     ");
		wd = 6;
	}
	for (travp = cbi->table; travp->type != HXTYPE_XSNTMARK; ++travp) {
		if (!(travp->ln == NULL && travp->sh != '\0' &&
		    takes_void(travp->type)))
			continue;
		if (*tmp == '\0') {
			snprintf(tmp, sizeof(tmp), " [-"); /* ] */
			tw = 3;
		}
		tmp[tw++] = travp->sh;
		if (wd + tw + 1 > SCREEN_WIDTH) {
			tmp[tw++] = /* [ */ ']';
			tmp[tw]   = '\0';
			fprintf(fp, "%s\n      ", tmp);
			wd   = 6;
			*tmp = '\0';
		}
	}
	if (*tmp != '\0') {
		tmp[tw++] = ']';
		tmp[tw]   = '\0';
		wd += fprintf(fp, "%s", tmp);
	}

	/* Any other args */
	for (travp = cbi->table; travp->type != HXTYPE_XSNTMARK; ++travp) {
		if (travp->ln == NULL && travp->sh != '\0' &&
		    takes_void(travp->type))
			continue;

		opt_to_text(travp, tmp, sizeof(tmp),
		            W_SPACE | W_BRACKET | W_ALT);
		if (wd + strlen(tmp) > SCREEN_WIDTH) {
			fprintf(fp, "\n      ");
			wd = 6;
		}
		wd += fprintf(fp, "%s", tmp);
	}

	fprintf(fp, "\n");
}

EXPORT_SYMBOL void HX_getopt_usage_cb(const struct HXoptcb *cbi)
{
	HX_getopt_usage(cbi, stdout);
	exit(EXIT_SUCCESS);
}

static void HX_shconf_break(void *ptr, char *line,
    void (*cb)(void *, const char *, const char *))
{
	char *lp = line, *key, *val;
	HX_chomp(line);

	while (lp != NULL) {
		while (HX_isspace(*lp) || *lp == ';')
			++lp;
		/* Next entry if comment, empty line or no value */
		if (*lp == '#' || *lp == '\0')
			return;
		if (!HX_isalpha(*lp) && *lp != '_')
			/* Variables ought to start with [A-Z_] */
			return;
		key = lp;
		while (HX_isalnum(*lp) || *lp == '_')
			++lp;
		if (*lp != '=')
			/* Variable name contained something not in [A-Z0-9_] */
			return;
		*lp++ = '\0';
		val = lp;

		/* Handle escape codes and quotes, and assign to TAB entry */
		lp = HXparse_dequote_int(val, "\t\n ;");
		(*cb)(ptr, key, val);
	}
}

static void HX_shconf_assign(void *table, const char *key, const char *value)
{
	struct HXoptcb cbi = {
		.table = table,
		.flags = HXOPTCB_BY_LONG,
		.data  = value,
	};

	if ((cbi.current = lookup_long(table, key)) == NULL)
		return;
	do_assign(&cbi, NULL);
}

EXPORT_SYMBOL int HX_shconfig(const char *file, const struct HXoption *table)
{
	hxmc_t *ln = NULL;
	FILE *fp;

	if ((fp = fopen(file, "r")) == NULL)
		return -errno;

	while (HX_getl(&ln, fp) != NULL)
		HX_shconf_break(const_cast(void *,
			static_cast(const void *, table)), ln,
			HX_shconf_assign);

	HXmc_free(ln);
	fclose(fp);
	return 1;
}

static void HX_shconf_assignmp(void *map, const char *key, const char *value)
{
	HXmap_add(map, key, value);
}

EXPORT_SYMBOL struct HXmap *HX_shconfig_map(const char *file)
{
	struct HXmap *map;
	hxmc_t *ln = NULL;
	FILE *fp;

	map = HXmap_init(HXMAPT_DEFAULT, HXMAP_SCKEY | HXMAP_SCDATA);
	if (map == NULL)
		return NULL;

	if ((fp = fopen(file, "r")) == NULL) {
		int saved_errno = errno;
		HXmap_free(map);
		errno = saved_errno;
		return NULL;
	}

	while (HX_getl(&ln, fp) != NULL)
		HX_shconf_break(map, ln, HX_shconf_assignmp);

	HXmc_free(ln);
	fclose(fp);
	return map;
}

EXPORT_SYMBOL int HX_shconfig_pv(const char **path, const char *file,
    const struct HXoption *table, unsigned int flags)
{
	char buf[MAXFNLEN];
	int ret = 0;

	for (; *path != NULL; ++path) {
		int v;
		snprintf(buf, sizeof(buf), "%s/%s", *path, file);
		v = HX_shconfig(buf, table);
		if (v > 0) {
			++ret;
			if (flags & SHCONF_ONE)
				break;
		}
	}

	return ret;
}

EXPORT_SYMBOL void HX_shconfig_free(const struct HXoption *table)
{
	if (table == NULL)
		return;
	for (; table->ln != NULL; ++table) {
		char **ptr = table->ptr;
		if (table->type == HXTYPE_STRING &&
		    ptr != NULL && *ptr != NULL)
			free(*ptr);
	}
}
