blob: 800b1680ff90bb5af214e680a6ff549dab6cf575 [file] [log] [blame]
/* libnih
*
* option.c - command-line argument and option parsing
*
* Copyright © 2009 Scott James Remnant <scott@netsplit.com>.
* Copyright © 2009 Canonical Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/string.h>
#include <nih/main.h>
#include <nih/option.h>
#include <nih/logging.h>
/**
* NihOptionCtx:
* @parent: parent object for returned array,
* @argc: number of arguments,
* @argv: arguments array,
* @options: list of options,
* @nargs: number of non-option arguments,
* @args: non-option arguments to return,
* @arg: current argument index,
* @nonopt: index of first non-option argument,
* @optend: index of end of options.
*
* This structure is passed between the various parsing functions to avoid
* having to pass a dozen different variables around and to keep us
* re-entrant.
**/
typedef struct nih_option_ctx {
const void *parent;
int argc;
char **argv;
NihOption *options;
size_t nargs;
char **args;
int arg;
int nonopt;
int optend;
} NihOptionCtx;
/* Prototypes for static functions */
static NihOption * nih_option_get_short (NihOptionCtx *ctx, int option);
static int nih_option_short (NihOptionCtx *ctx);
static NihOption * nih_option_get_long (NihOptionCtx *ctx,
const char *option, size_t len);
static int nih_option_long (NihOptionCtx *ctx);
static int nih_option_handle (NihOptionCtx *ctx, NihOption *opt);
static int nih_option_handle_arg (NihOptionCtx *ctx, NihOption *opt,
const char *arg);
static const char *nih_option_next_nonopt (NihOptionCtx *ctx);
static void nih_option_help (NihOption *options);
static void nih_option_group_help (NihOptionGroup *group,
NihOption *options,
NihOptionGroup **groups);
/**
* default_options:
*
* These default options are appended to those defined by the user
* so they can be overriden.
**/
static const NihOption default_options[] = {
{ 'q', "quiet",
N_("reduce output to errors only"),
NULL, NULL, NULL, nih_option_quiet },
{ 'v', "verbose",
N_("increase output to include informational messages"),
NULL, NULL, NULL, nih_option_verbose },
{ 0, "debug", NULL, NULL, NULL, NULL, nih_option_debug },
{ 0, "help",
N_("display this help and exit"),
NULL, NULL, NULL, NULL },
{ 0, "version",
N_("output version information and exit"),
NULL, NULL, NULL, NULL },
NIH_OPTION_LAST
};
/**
* usage_stem:
*
* This string is prepended to the program's usage line if given.
**/
static const char *usage_stem = NULL;
/**
* usage_string:
*
* This string is appended to the program's usage line if given.
**/
static const char *usage_string = NULL;
/**
* synopsis_string:
*
* This string is output after the program's usage string if given.
**/
static const char *synopsis_string = NULL;
/**
* help_string:
*
* This string is output after the options if given.
**/
static const char *help_string = NULL;
/**
* footer_string:
*
* This string is output after the options and help if given.
**/
static const char *footer_string = NULL;
/**
* nih_option_parser:
* @parent: parent object for returned array,
* @argc: number of arguments,
* @argv: command-line arguments,
* @options: options to look for,
* @break_nonopt: stop processing options at first non-option argument.
*
* Parses the command-line arguments given in @argv looking for options
* described in @options, or those built-in. Options are handled according
* to common UNIX semantics so that short options may be grouped together
* and arguments need not immediately follow the option that requires it.
*
* Remaining non-option arguments are placed into an array for processing
* by the caller. If @break_nonopt is FALSE then the first non-option
* argument concludes option processing and all subsequent options are
* considered to be ordinary arguments; this is most useful when the
* first argument should be a command.
*
* Both the array itself, and the array items are allocated with nih_alloc();
* the items are children of the array, so it is only necessary to call
* nih_free() on the array.
*
* If @parent is not NULL, it should be a pointer to another object which
* will be used as a parent for the returned array. When all parents
* of the returned array are freed, the returned array will also be
* freed.
*
* Errors are handled by printing a message to standard error.
*
* Returns: non-option arguments array or NULL on error.
**/
char **
nih_option_parser (const void *parent,
int argc,
char *argv[],
NihOption *options,
int break_nonopt)
{
NihOptionCtx ctx;
nih_assert (argc > 0);
nih_assert (argv != NULL);
nih_assert (options != NULL);
ctx.parent = parent;
ctx.argc = argc;
ctx.argv = argv;
ctx.options = nih_option_join (NULL, options, default_options);
ctx.nargs = 0;
ctx.args = NIH_MUST (nih_str_array_new (parent));
ctx.nonopt = 0;
ctx.optend = 0;
/* Iterate the arguments looking for options */
for (ctx.arg = 1; ctx.arg < argc; ctx.arg++) {
char *arg;
arg = ctx.argv[ctx.arg];
if ((arg[0] != '-') || (arg[1] == '\0')
|| (ctx.optend && ctx.arg > ctx.optend)) {
/* Not an option */
if (ctx.arg > ctx.nonopt) {
NIH_MUST (nih_str_array_add
(&ctx.args, parent, &ctx.nargs,
ctx.argv[ctx.arg]));
if (break_nonopt)
ctx.optend = ctx.arg;
}
} else if (arg[1] != '-') {
/* Short option */
if (nih_option_short (&ctx) < 0)
goto error;
} else if (arg[2] != '\0') {
/* Long option */
if (nih_option_long (&ctx) < 0)
goto error;
} else {
/* End of options */
ctx.optend = ctx.arg;
}
}
nih_free (ctx.options);
return ctx.args;
error:
nih_free (ctx.options);
nih_free (ctx.args);
return NULL;
}
/**
* nih_option_get_short:
* @ctx: parsing context,
* @option: option to find.
*
* Find the option structure with the given short @option. If an option
* exists with the short option '-' this is used instead if no specific
* option is found.
*
* Returns; pointer to option, or NULL if not found.
**/
static NihOption *
nih_option_get_short (NihOptionCtx *ctx,
int option)
{
NihOption *opt, *catch = NULL;
for (opt = ctx->options; (opt->option || opt->long_option); opt++) {
if (opt->option == '-')
catch = opt;
if (opt->option == option)
return opt;
}
return catch;
}
/**
* nih_option_short:
* @ctx: parsing context.
*
* Process the current argument as a list of short options, handling
* each one individually.
*
* If the first option in the list expects a value, then the rest of
* the argument is taken to be the option argument rather than further
* options.
*
* Returns: zero on success, negative value on invalid option.
**/
static int
nih_option_short (NihOptionCtx *ctx)
{
char *ptr;
nih_assert (ctx != NULL);
nih_assert (program_name != NULL);
for (ptr = ctx->argv[ctx->arg] + 1; *ptr != '\0'; ptr++) {
NihOption *opt;
opt = nih_option_get_short (ctx, *ptr);
if (! opt) {
fprintf (stderr, _("%s: invalid option: -%c\n"),
program_name, *ptr);
nih_main_suggest_help ();
return -1;
}
/* If the option takes an argument, this is the first
* option in the list and there are further characters;
* treat the rest as the argument
*/
if (opt->arg_name && (ptr[-1] == '-') && (ptr[1] != '\0')) {
if (nih_option_handle_arg (ctx, opt, ptr + 1) < 0)
return -1;
break;
}
/* Otherwise it's an ordinary option */
if (nih_option_handle (ctx, opt) < 0)
return -1;
}
return 0;
}
/**
* nih_option_get_long:
* @ctx: parsing context,
* @option: option to find,
* @len: length of option.
*
* Find the option structure with the given long @option, of which only
* the first @len characters will be read. If an option named "--" exists
* then it is used if no other option could be found.
*
* Returns; pointer to option, or NULL if not found.
**/
static NihOption *
nih_option_get_long (NihOptionCtx *ctx,
const char *option,
size_t len)
{
NihOption *opt, *catch = NULL;
for (opt = ctx->options; (opt->option || opt->long_option); opt++) {
if (! opt->long_option)
continue;
if (! strcmp (opt->long_option, "--"))
catch = opt;
if (strlen (opt->long_option) > len)
continue;
if (! strncmp (option, opt->long_option, len))
return opt;
}
return catch;
}
/**
* nih_option_long:
* @ctx: parsing context.
*
* Process the current argument as a long option to be handled.
*
* If the option expects a value then it may be separated from the option
* name by an '=' sign.
*
* Returns: zero on success, negative value on invalid option.
**/
static int
nih_option_long (NihOptionCtx *ctx)
{
NihOption *opt;
char *arg, *ptr;
size_t len;
nih_assert (ctx != NULL);
nih_assert (program_name != NULL);
/* Check for an equals sign that separates the option name from
* an argument.
*/
arg = ctx->argv[ctx->arg] + 2;
ptr = strchr (arg, '=');
len = (ptr ? (size_t)(ptr - arg) : strlen (arg));
/* Find the option */
opt = nih_option_get_long (ctx, arg, len);
if (! opt) {
fprintf (stderr, _("%s: invalid option: --%s\n"),
program_name, arg);
nih_main_suggest_help ();
return -1;
}
/* Handle the case where there's an argument; either we need
* to process it, or error
*/
if (ptr != NULL) {
if (opt->arg_name) {
if (nih_option_handle_arg (ctx, opt, ptr + 1) < 0)
return -1;
return 0;
} else {
fprintf (stderr, _("%s: unexpected argument: --%s\n"),
program_name, arg);
nih_main_suggest_help ();
return -1;
}
}
/* Otherwise it's an ordinary option */
if (nih_option_handle (ctx, opt) < 0)
return -1;
return 0;
}
/**
* nih_option_handle:
* @ctx: parsing context,
* @opt: option to handle.
*
* Handle an option which either does not take an argument, or should
* take the next non-option argument from the command-line. For options
* with arguments, this calls nih_option_handle_arg(); for those without,
* this calls the setter function or treats the value member as a pointer
* to an int to store TRUE in.
*
* Returns: zero on success, negative value on invalid option.
**/
static int
nih_option_handle (NihOptionCtx *ctx,
NihOption *opt)
{
nih_assert (ctx != NULL);
nih_assert (opt != NULL);
/* Handle the special cased options first */
if (opt->long_option && (! strcmp (opt->long_option, "help"))) {
/* --help */
nih_option_help (ctx->options);
nih_free (ctx->options);
nih_free (ctx->args);
exit (0);
} else if (opt->long_option
&& (! strcmp (opt->long_option, "version"))) {
/* --version */
nih_main_version ();
nih_free (ctx->options);
nih_free (ctx->args);
exit (0);
}
if (opt->arg_name) {
const char *arg;
arg = nih_option_next_nonopt (ctx);
if (! arg) {
fprintf (stderr, _("%s: missing argument: %s\n"),
program_name, ctx->argv[ctx->arg]);
nih_main_suggest_help ();
return -1;
}
return nih_option_handle_arg (ctx, opt, arg);
} else if (opt->setter) {
return opt->setter (opt, NULL);
} else if (opt->value) {
int *value = (int *)opt->value;
*value = TRUE;
}
return 0;
}
/**
* nih_option_handle_arg:
* @ctx: parsing context,
* @opt: option to handle,
* @arg: argument.
*
* Handle an option which has the argument specified, either calling
* the setter function or treating the value member (if present) as the
* address of a char * to store the duplicated argument value in.
*
* Returns: zero on success, negative value on invalid option.
**/
static int
nih_option_handle_arg (NihOptionCtx *ctx,
NihOption *opt,
const char *arg)
{
nih_assert (ctx != NULL);
nih_assert (opt != NULL);
nih_assert (opt->arg_name != NULL);
nih_assert (arg != NULL);
if (opt->setter) {
return opt->setter (opt, arg);
} else if (opt->value) {
char **value = (char **)opt->value;
if (*value)
nih_free (*value);
*value = NIH_MUST (nih_strdup (ctx->parent, arg));
}
return 0;
}
/**
* nih_option_next_nonopt:
* @ctx: parsing context.
*
* Iterates the command-line arguments looking for the next argument
* that is not an option. Updates the nonopt member of @ctx to point
* at the option used.
*
* Returns: next non-option argument or NULL if none remain.
**/
static const char *
nih_option_next_nonopt (NihOptionCtx *ctx)
{
nih_assert (ctx != NULL);
if (ctx->nonopt < ctx->arg)
ctx->nonopt = ctx->arg;
while (++ctx->nonopt < ctx->argc) {
char *arg;
arg = ctx->argv[ctx->nonopt];
if ((arg[0] != '-')
|| (ctx->optend && ctx->nonopt > ctx->optend)) {
return arg;
} else if ((arg[1] == '-') && (arg[2] == '\0')) {
/* End of options */
ctx->optend = ctx->nonopt;
}
}
return NULL;
}
/**
* nih_option_join:
* @parent: parent object for new array,
* @a: first option array,
* @b: second option array.
*
* Joins the two option arrays together to produce a combined array containing
* the options from @a followed by the options from @b.
*
* The new list is allocated with nih_alloc(), but the members are just
* copied in from @a and @b including any pointers therein. Freeing the
* new array with nih_free() is entirely safe.
*
* If @parent is not NULL, it should be a pointer to another object which
* will be used as a parent for the returned array. When all parents
* of the returned array are freed, the returned array will also be
* freed.
*
* Returns: combined option array.
**/
NihOption *
nih_option_join (const void *parent,
const NihOption *a,
const NihOption *b)
{
const NihOption *opt;
NihOption *opts;
size_t alen = 0, blen = 0;
nih_assert (a != NULL);
nih_assert (b != NULL);
/* Count options in first list */
for (opt = a; (opt->option || opt->long_option); opt++)
alen++;
/* Count options in second list */
for (opt = b; (opt->option || opt->long_option); opt++)
blen++;
/* Allocate combined list */
opts = NIH_MUST (nih_alloc (parent,
sizeof (NihOption) * (alen + blen + 1)));
/* Copy options, making sure to copy the last option from b */
memcpy (opts, a, sizeof (NihOption) * alen);
memcpy (opts + alen, b, sizeof (NihOption) * (blen + 1));
return opts;
}
/**
* nih_option_count:
* @option: NihOption invoked,
* @arg: argument to parse.
*
* This option setter may be used to create arguments that count the number
* of times they are placed on the command line.
*
* The value member of @option must be a pointer to an integer variable,
* the arg_name member must be NULL.
*
* Returns: always returns zero.
**/
int
nih_option_count (NihOption *option,
const char *arg)
{
int *value;
nih_assert (option != NULL);
nih_assert (option->value != NULL);
nih_assert (arg == NULL);
value = (int *)option->value;
(*value)++;
return 0;
}
/**
* nih_option_int:
* @option: NihOption invoked,
* @arg: argument to parse.
*
* This option setter may be used to parse an integer from the command line
* and store it in the value member of @option, which must be a pointer to
* an integer variable.
*
* The arg_name member of @option must not be NULL.
*
* Returns: zero on success, non-zero on error.
**/
int
nih_option_int (NihOption *option,
const char *arg)
{
char *endptr;
int *value;
nih_assert (option != NULL);
nih_assert (option->value != NULL);
nih_assert (arg != NULL);
value = (int *)option->value;
*value = strtol (arg, &endptr, 10);
if (*endptr) {
fprintf (stderr, _("%s: illegal argument: %s\n"),
program_name, arg);
nih_main_suggest_help ();
return -1;
}
return 0;
}
/**
* nih_option_quiet:
* @option: NihOption invoked,
* @arg: argument to parse.
*
* This option setter is used by the built-in -q/--quiet option to set the
* default logging level to ERROR.
*
* Returns: always returns zero.
**/
int
nih_option_quiet (NihOption *option,
const char *arg)
{
nih_assert (option != NULL);
nih_assert (arg == NULL);
nih_log_set_priority (NIH_LOG_ERROR);
return 0;
}
/**
* nih_option_verbose:
* @option: NihOption invoked,
* @arg: argument to parse.
*
* This option setter is used by the built-in -v/--verbose option to set the
* default logging level to INFO.
*
* Returns: always returns zero.
**/
int
nih_option_verbose (NihOption *option,
const char *arg)
{
nih_assert (option != NULL);
nih_assert (arg == NULL);
nih_log_set_priority (NIH_LOG_INFO);
return 0;
}
/**
* nih_option_debug:
* @option: NihOption invoked,
* @arg: argument to parse.
*
* This option setter is used by the built-in --debug option to set the
* default logging level to DEBUG.
*
* Returns: always returns zero.
**/
int
nih_option_debug (NihOption *option,
const char *arg)
{
nih_assert (option != NULL);
nih_assert (arg == NULL);
nih_log_set_priority (NIH_LOG_DEBUG);
return 0;
}
/**
* nih_option_set_usage_stem:
* @usage: usage stem.
*
* Set the usage stem prepended to the program usage line in the help output,
* this should be a static translated string.
*
* The string should not be terminated with a newline.
**/
void
nih_option_set_usage_stem (const char *usage)
{
usage_stem = usage;
}
/**
* nih_option_set_usage:
* @usage: usage string.
*
* Set the usage string appended to the program usage line in the help output,
* this should be a static translated string.
*
* The string should not be terminated with a newline.
**/
void
nih_option_set_usage (const char *usage)
{
usage_string = usage;
}
/**
* nih_option_set_synopsis:
* @synopsis: synopsis string.
*
* Set the synopsis string, shown after the program usage line in the help
* output. This should be a static translated string. It will be
* automatically wrapped to the screen width.
*
* The string should not be terminated with a newline.
**/
void
nih_option_set_synopsis (const char *synopsis)
{
synopsis_string = synopsis;
}
/**
* nih_option_set_help:
* @help: help string.
*
* Set the help string, this is displayed after the options in the help
* output. This should be a static translated string. It will be
* automatically wrapped to the screen width.
*
* The string should not be terminated with a newline.
**/
void
nih_option_set_help (const char *help)
{
help_string = help;
}
/**
* nih_option_set_footer:
* @footer: footer string.
*
* Set the footer string, this is displayed after the options and help
* text in the output. This should be a static translated string.
*
* The string should not be terminated with a newline.
**/
void
nih_option_set_footer (const char *footer)
{
footer_string = footer;
}
/**
* nih_option_help:
* @options: program options list.
*
* Output a description of the program's options to standard output
* grouped by the group member of the option.
**/
static void
nih_option_help (NihOption *options)
{
NihOption *opt;
nih_local NihOptionGroup **groups = NULL;
size_t group, ngroups;
int other = FALSE;
nih_assert (program_name != NULL);
/* Count the number of option groups */
ngroups = 0;
for (opt = options; (opt->option || opt->long_option); opt++) {
if (! opt->group) {
other = TRUE;
continue;
}
for (group = 0; group < ngroups; group++) {
if (groups[group] == opt->group)
break;
}
if (group < ngroups)
continue;
groups = NIH_MUST (nih_realloc (groups, NULL,
(sizeof (NihOptionGroup *)
* (ngroups + 1))));
groups[ngroups++] = opt->group;
}
printf ("%s: %s", _("Usage"), program_name);
if (usage_stem) {
printf (" %s", usage_stem);
} else {
printf (" %s", _("[OPTION]..."));
}
if (usage_string)
printf (" %s", usage_string);
printf ("\n");
/* Wrap the synopsis to the screen width */
if (synopsis_string) {
nih_local char *str;
str = NIH_MUST (nih_str_screen_wrap (NULL, synopsis_string,
0, 0));
printf ("%s\n", str);
}
printf ("\n");
/* Iterate the option groups we found in order, and display
* only their options
*/
for (group = 0; group < ngroups; group++)
nih_option_group_help (groups[group], options, groups);
/* Display the other group */
if (other)
nih_option_group_help (NULL, options, groups);
/* Wrap the help to the screen width */
if (help_string) {
nih_local char *str;
str = NIH_MUST (nih_str_screen_wrap (NULL, help_string, 0, 0));
printf ("%s\n", str);
if (package_bugreport || footer_string)
printf ("\n");
}
/* Append the footer */
if (footer_string) {
printf ("%s\n", footer_string);
if (package_bugreport)
printf ("\n");
}
/* Append the bug report address */
if (package_bugreport) {
if (strchr (package_bugreport, '@')) {
printf (_("Report bugs to <%s>\n"), package_bugreport);
} else {
printf (_("Report bugs at <%s>\n"), package_bugreport);
}
}
}
/**
* nih_option_group_help:
* @group: group to display,
* @options: program options list,
* @groups: all groups.
*
* Output a list of the program's options in the given option group to
* standard output.
**/
static void
nih_option_group_help (NihOptionGroup *group,
NihOption *options,
NihOptionGroup **groups)
{
NihOption *opt;
size_t width;
nih_assert (options != NULL);
if (group) {
printf (_("%s options:\n"), _(group->title));
} else if (groups) {
printf (_("Other options:\n"));
} else {
printf (_("Options:\n"));
}
width = nih_max (nih_str_screen_width (), 50U) - 31;
for (opt = options; (opt->option || opt->long_option); opt++) {
nih_local char *str = NULL;
char *ptr;
size_t len = 0;
if (opt->group != group)
continue;
if (! opt->help)
continue;
/* Indent by two spaces */
printf (" ");
len += 2;
/* Display the short option */
if (opt->option) {
printf ("-%c", opt->option);
len += 2;
/* Seperate short and long option, or
* give the argument name
*/
if (opt->long_option) {
printf (", ");
len += 2;
} else if (opt->arg_name) {
printf (" %s", opt->arg_name);
len += strlen (opt->arg_name) + 1;
}
} else {
/* Make all long options the same indent
* whether or not there's a short one
*/
printf (" ");
len += 4;
}
/* Display the long option */
if (opt->long_option) {
printf ("--%s", opt->long_option);
len += strlen (opt->long_option) + 2;
/* With the argument name */
if (opt->arg_name) {
printf ("=%s", opt->arg_name);
len += strlen (opt->arg_name) + 1;
}
}
/* Format the help string to fit in the latter
* half of the screen
*/
str = NIH_MUST (nih_str_wrap (NULL, opt->help, width, 0, 2));
/* Write the description to the screen */
ptr = str;
while (ptr && *ptr) {
size_t linelen;
/* Not enough room on this line */
if (len > 28) {
printf ("\n");
len = 0;
}
/* Pad line up to the right column */
while (len < 30) {
printf (" ");
len++;
}
/* Output the line up until the next line */
linelen = strcspn (ptr, "\n");
printf ("%.*s\n", (int)linelen, ptr);
len = 0;
/* Skip to the next line */
ptr += linelen;
if (*ptr == '\n')
ptr++;
}
}
printf ("\n");
}