#include "internal.h"
#pragma mark Definitions
#define CTL_OUTPUT_WIDTH (80)
#define CTL_OUTPUT_OPTARG_PAD (28)
#define CTL_OUTPUT_LIST_PAD (4)
#define SUBCOMMAND_LINKER_SET "__subcommands"
#define OS_SUBCOMMAND_OPTIONS_FOREACH(_osco_i, _osc, _which, _i) \
while (((_osco_i) = &osc->osc_ ## _which[(_i)]) && \
((_i) += 1, 1) && \
!((_osco_i)->osco_flags & OS_SUBCOMMAND_OPTION_FLAG_TERMINATOR))
#pragma mark Types
OS_ENUM(os_subcommand_option_spec_fmt, uint64_t,
OS_SUBCOMMAND_OPTION_SPEC_SHORT,
OS_SUBCOMMAND_OPTION_SPEC_LONG,
OS_SUBCOMMAND_OPTION_SPEC_COMBINED,
);
#pragma mark Forward Declarations
static void _print_header(
FILE *f,
const char *hdr,
bool *already_done);
static const os_subcommand_t *_os_subcommand_find(
const char *name);
static void _os_subcommand_print_usage(
const os_subcommand_t *osc,
FILE *f);
static void _os_subcommand_print_help_line(
const os_subcommand_t *osc,
FILE *f);
#pragma mark Module Globals
static const os_subcommand_t __help_cmd;
static const os_subcommand_t *_help_cmd = &__help_cmd;
static const os_subcommand_t __main_cmd;
static const os_subcommand_t *_main_cmd = &__main_cmd;
static const os_subcommand_t *_internal_main_cmd = &__main_cmd;
#pragma mark Main Subcommand
static int _main_invoke(const os_subcommand_t *osc,
int argc,
const char *argv[]);
static const os_subcommand_option_t _main_positional[] = {
[0] = {
.osco_version = OS_SUBCOMMAND_OPTION_VERSION,
.osco_flags = 0,
.osco_option = NULL,
.osco_argument_usage = "subcommand",
.osco_argument_human = "The subcommand to invoke",
},
OS_SUBCOMMAND_OPTION_TERMINATOR,
};
static const os_subcommand_t __main_cmd = {
.osc_version = OS_SUBCOMMAND_VERSION,
.osc_flags = OS_SUBCOMMAND_FLAG_MAIN,
.osc_name = "_main",
.osc_desc = "main command",
.osc_optstring = NULL,
.osc_options = NULL,
.osc_required = NULL,
.osc_optional = NULL,
.osc_positional = _main_positional,
.osc_invoke = &_main_invoke,
};
static int
_main_invoke(const os_subcommand_t *osc, int argc, const char *argv[])
{
return 0;
}
#pragma mark Help Subcommand
static int _help_invoke(const os_subcommand_t *osc,
int argc,
const char *argv[]);
static const os_subcommand_option_t _help_positional[] = {
[0] = {
.osco_version = OS_SUBCOMMAND_OPTION_VERSION,
.osco_flags = 0,
.osco_option = NULL,
.osco_argument_usage = "subcommand",
.osco_argument_human = "The subcommand to query for help",
},
OS_SUBCOMMAND_OPTION_TERMINATOR,
};
static const os_subcommand_t __help_cmd = {
.osc_version = OS_SUBCOMMAND_VERSION,
.osc_flags = 0,
.osc_name = "help",
.osc_desc = "prints helpful information",
.osc_optstring = NULL,
.osc_options = NULL,
.osc_required = NULL,
.osc_optional = NULL,
.osc_positional = _help_positional,
.osc_invoke = &_help_invoke,
};
static int
_help_invoke(const os_subcommand_t *osc, int argc, const char *argv[])
{
int xit = -1;
const char *cmdname = NULL;
const os_subcommand_t *target = NULL;
FILE *f = stdout;
if (argc > 1) {
cmdname = argv[1];
}
if (cmdname) {
target = _os_subcommand_find(argv[1]);
if (!target) {
os_subcommand_fprintf(osc, stderr, "unrecognized subcommand: %s",
argv[1]);
xit = EX_USAGE;
} else {
xit = 0;
}
} else {
target = _main_cmd;
xit = 0;
}
if (xit) {
f = stderr;
}
_os_subcommand_print_usage(target, f);
if (target == _main_cmd) {
const os_subcommand_t **oscip = NULL;
const os_subcommand_t *osci = NULL;
bool header_printed = false;
LINKER_SET_FOREACH(oscip, const os_subcommand_t **,
SUBCOMMAND_LINKER_SET) {
osci = *oscip;
_print_header(f, "subcommands", &header_printed);
_os_subcommand_print_help_line(osci, f);
}
_os_subcommand_print_help_line(osc, f);
}
return xit;
}
#pragma mark Utilities
static void
_print_header(FILE *f, const char *hdr, bool *already_done)
{
if (*already_done) {
return;
}
crfprintf_np(f, "");
crfprintf_np(f, "%s:", hdr);
*already_done = true;
}
#pragma mark Module Routines
static char *
_os_subcommand_copy_option_spec_short(const os_subcommand_t *osc,
const os_subcommand_option_t *osco)
{
const struct option *opt = osco->osco_option;
char optbuff[64] = "";
char argbuff[64] = "";
char *final = NULL;
int ret = -1;
if (opt) {
snprintf(optbuff, sizeof(optbuff), "-%c", opt->val);
switch (opt->has_arg) {
case no_argument:
break;
case optional_argument:
snprintf(argbuff, sizeof(argbuff), "[%s]",
osco->osco_argument_usage);
break;
case required_argument:
snprintf(argbuff, sizeof(argbuff), "<%s>",
osco->osco_argument_usage);
break;
default:
__builtin_unreachable();
}
} else {
snprintf(optbuff, sizeof(optbuff), "%s", osco->osco_argument_usage);
}
ret = asprintf(&final, "%s%s", optbuff, argbuff);
if (ret < 0) {
os_assert_zero(ret);
}
return final;
}
static char *
_os_subcommand_copy_option_spec_long(const os_subcommand_t *osc,
const os_subcommand_option_t *osco)
{
const struct option *opt = osco->osco_option;
char optbuff[64] = "";
char argbuff[64] = "";
char *final = NULL;
int ret = -1;
if (opt) {
snprintf(optbuff, sizeof(optbuff), "--%s", opt->name);
switch (opt->has_arg) {
case no_argument:
break;
case optional_argument:
snprintf(argbuff, sizeof(argbuff), "[=%s]",
osco->osco_argument_usage);
break;
case required_argument:
snprintf(argbuff, sizeof(argbuff), "=<%s>",
osco->osco_argument_usage);
break;
default:
__builtin_unreachable();
}
} else {
snprintf(optbuff, sizeof(optbuff), "%s", osco->osco_argument_usage);
}
ret = asprintf(&final, "%s%s", optbuff, argbuff);
if (ret < 0) {
os_assert_zero(ret);
}
return final;
}
static char *
_os_subcommand_copy_option_spec(const os_subcommand_t *osc,
const os_subcommand_option_t *osco, os_subcommand_option_spec_fmt_t fmt)
{
int ret = -1;
char *spec = NULL;
char *__os_free spec_old = NULL;
switch (fmt) {
case OS_SUBCOMMAND_OPTION_SPEC_SHORT:
_os_subcommand_copy_option_spec_short(osc, osco);
break;
case OS_SUBCOMMAND_OPTION_SPEC_LONG:
_os_subcommand_copy_option_spec_long(osc, osco);
break;
case OS_SUBCOMMAND_OPTION_SPEC_COMBINED:
spec = _os_subcommand_copy_option_spec_long(osc, osco);
if (osco->osco_option) {
spec_old = spec;
ret = asprintf(&spec, "%s | -%c", spec, osco->osco_option->val);
if (ret < 0) {
os_assert_zero(ret);
}
}
break;
default:
__builtin_unreachable();
}
return spec;
}
static char *
_os_subcommand_copy_usage_line(const os_subcommand_t *osc)
{
char *usage_line = NULL;
size_t i = 0;
const os_subcommand_option_t *osco_i = NULL;
const char *optional_spec = "";
char subcmd_name[64];
int ret = -1;
if (osc->osc_optional) {
optional_spec = " [options]";
}
if (osc == _main_cmd) {
strlcpy(subcmd_name, "", sizeof(subcmd_name));
} else {
snprintf(subcmd_name, sizeof(subcmd_name), " %s", osc->osc_name);
}
ret = asprintf(&usage_line, "%s%s%s",
getprogname(), subcmd_name, optional_spec);
if (ret < 0) {
os_assert_zero(ret);
}
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, required, i) {
char *__os_free usage_line_old = NULL;
char *__os_free osco_spec = NULL;
usage_line_old = usage_line;
osco_spec = _os_subcommand_copy_option_spec_long(osc, osco_i);
ret = asprintf(&usage_line, "%s %s", usage_line, osco_spec);
if (ret < 0) {
os_assert_zero(ret);
}
}
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, positional, i) {
char *__os_free usage_line_old = NULL;
char *__os_free osco_spec = NULL;
const char *braces[] = {
"<",
">",
};
if (osco_i->osco_flags & OS_SUBCOMMAND_OPTION_FLAG_OPTIONAL_POS) {
braces[0] = "[";
braces[1] = "]";
}
usage_line_old = usage_line;
osco_spec = _os_subcommand_copy_option_spec_long(osc, osco_i);
ret = asprintf(&usage_line, "%s %s%s%s",
usage_line, braces[0], osco_spec, braces[1]);
if (ret < 0) {
os_assert_zero(ret);
}
}
if (osc == _main_cmd && osc != _internal_main_cmd) {
char *__os_free usage_line_old = NULL;
char *__os_free osco_spec = NULL;
usage_line_old = usage_line;
osco_spec = _os_subcommand_copy_option_spec_long(osc,
&_main_positional[0]);
ret = asprintf(&usage_line, "%s <%s>", usage_line, osco_spec);
if (ret < 0) {
os_assert_zero(ret);
}
}
return usage_line;
}
static void
_os_subcommand_print_option_usage(const os_subcommand_t *osc,
const os_subcommand_option_t *osco, FILE *f)
{
char *__os_free opt_spec = NULL;
ssize_t initpad = -CTL_OUTPUT_OPTARG_PAD;
opt_spec = _os_subcommand_copy_option_spec(osc, osco,
OS_SUBCOMMAND_OPTION_SPEC_COMBINED);
fprintf(f, " %-24s", opt_spec);
if (strlen(opt_spec) >= CTL_OUTPUT_OPTARG_PAD) {
initpad = CTL_OUTPUT_OPTARG_PAD;
crfprintf_np(f, "");
}
wfprintf_np(f, initpad, CTL_OUTPUT_OPTARG_PAD,
CTL_OUTPUT_WIDTH, "%s",
osco->osco_argument_human);
}
static void
_os_subcommand_print_help_line(const os_subcommand_t *osc, FILE *f)
{
ssize_t initpad = -CTL_OUTPUT_OPTARG_PAD;
fprintf(f, " %-24s", osc->osc_name);
if (strlen(osc->osc_name) >= CTL_OUTPUT_OPTARG_PAD) {
initpad = CTL_OUTPUT_OPTARG_PAD;
crfprintf_np(f, "");
}
wfprintf_np(f, initpad, CTL_OUTPUT_OPTARG_PAD,
CTL_OUTPUT_WIDTH, "%s",
osc->osc_desc);
}
static void
_os_subcommand_print_usage(const os_subcommand_t *osc, FILE *f)
{
size_t i = 0;
const os_subcommand_option_t *osco_i = NULL;
char *__os_free usage_line = NULL;
bool header_printed = false;
usage_line = _os_subcommand_copy_usage_line(osc);
wfprintf_np(f, 0, 0, CTL_OUTPUT_WIDTH, "usage: %s", usage_line);
if (osc->osc_required || osc->osc_positional || osc == _main_cmd) {
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, required, i) {
_print_header(f, "required", &header_printed);
crfprintf_np(f, "");
_os_subcommand_print_option_usage(osc, osco_i, f);
}
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, positional, i) {
_print_header(f, "required", &header_printed);
if (osco_i->osco_flags & OS_SUBCOMMAND_OPTION_FLAG_OPTIONAL_POS) {
continue;
}
crfprintf_np(f, "");
_os_subcommand_print_option_usage(osc, osco_i, f);
}
if (osc == _main_cmd && osc != _internal_main_cmd) {
_print_header(f, "required", &header_printed);
crfprintf_np(f, "");
_os_subcommand_print_option_usage(osc, &_main_positional[0], f);
}
}
header_printed = false;
if (osc->osc_optional || osc->osc_positional) {
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, optional, i) {
_print_header(f, "optional", &header_printed);
crfprintf_np(f, "");
_os_subcommand_print_option_usage(osc, osco_i, f);
}
i = 0;
OS_SUBCOMMAND_OPTIONS_FOREACH(osco_i, osc, positional, i) {
if (osco_i->osco_flags & OS_SUBCOMMAND_OPTION_FLAG_OPTIONAL_POS) {
_print_header(f, "optional", &header_printed);
crfprintf_np(f, "");
_os_subcommand_print_option_usage(osc, osco_i, f);
}
}
}
}
static const os_subcommand_t *
_os_subcommand_find(const char *name)
{
const os_subcommand_t **oscip = NULL;
if (strcmp(_help_cmd->osc_name, name) == 0) {
return &__help_cmd;
}
LINKER_SET_FOREACH(oscip, const os_subcommand_t **, SUBCOMMAND_LINKER_SET) {
const os_subcommand_t *osci = *oscip;
if (osci->osc_flags & OS_SUBCOMMAND_FLAG_MAIN) {
continue;
}
if (strcmp(osci->osc_name, name) == 0) {
return osci;
}
}
return NULL;
}
#pragma mark API
int
os_subcommand_main(int argc, const char *argv[],
os_subcommand_main_flags_t flags)
{
int xit = -1;
const char *cmdname = NULL;
const os_subcommand_t *osc = NULL;
const os_subcommand_t **oscip = NULL;
if (argc < 2) {
os_subcommand_fprintf(NULL, stderr, "please provide a subcommand");
xit = EX_USAGE;
goto __out;
}
LINKER_SET_FOREACH(oscip, const os_subcommand_t **, SUBCOMMAND_LINKER_SET) {
osc = *oscip;
if (osc->osc_flags & OS_SUBCOMMAND_FLAG_MAIN) {
_main_cmd = osc;
break;
}
}
osc = NULL;
xit = _main_cmd->osc_invoke(_main_cmd, argc, argv);
if (xit) {
goto __out;
}
argc -= optind;
argv += optind;
cmdname = argv[0];
if (argc < 1) {
os_subcommand_fprintf(NULL, stderr, "please provide a subcommand");
xit = EX_USAGE;
goto __out;
}
osc = _os_subcommand_find(cmdname);
if (osc) {
if (osc->osc_flags & OS_SUBCOMMAND_FLAG_REQUIRE_ROOT) {
if (geteuid()) {
os_subcommand_fprintf(osc, stderr,
"subcommand requires root: %s",
cmdname);
xit = EX_NOPERM;
goto __out;
}
}
if (osc->osc_flags & OS_SUBCOMMAND_FLAG_TTYONLY) {
if (!isatty(STDOUT_FILENO) || !isatty(STDIN_FILENO)) {
os_subcommand_fprintf(osc, stderr,
"subcommand requires a tty: %s",
cmdname);
xit = EX_UNAVAILABLE;
goto __out;
}
}
xit = osc->osc_invoke(osc, argc, argv);
} else {
os_subcommand_fprintf(NULL, stderr, "unknonwn subcommand: %s", cmdname);
xit = EX_USAGE;
}
__out:
if (xit == EX_USAGE) {
if (!osc) {
osc = _main_cmd;
}
_os_subcommand_print_usage(osc, stderr);
}
return xit;
}
void
os_subcommand_fprintf(const os_subcommand_t *osc, FILE *f,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vcrfprintf_np(f, fmt, ap);
va_end(ap);
}
void
os_subcommand_vfprintf(const os_subcommand_t *osc, FILE *f,
const char *fmt, va_list ap)
{
if (!osc || (osc->osc_flags & OS_SUBCOMMAND_FLAG_MAIN)) {
fprintf(f, "%s: ", getprogname());
} else {
fprintf(f, "%s::%s: ", getprogname(), osc->osc_name);
}
vcrfprintf_np(f, fmt, ap);
}