/* opt.c - command-line option parsing * **************************************************************** * Copyright (C) 1998, 2000 Thomas Lord * * See the file "COPYING" for further information about * the copyright and warranty status of this work. */ #include "hackerlab/os/alloca.h" #include "hackerlab/os/stddef.h" #include "hackerlab/os/limits.h" #include "hackerlab/bugs/panic.h" #include "hackerlab/char/char-class.h" #include "hackerlab/char/str.h" #include "hackerlab/mem/mem.h" #include "hackerlab/vu/safe.h" #include "hackerlab/cmd/opt.h" /************************************************************************ *(h0 "Parsing Command-line Options" * :include ("hackerlab/cmd/opt.h")) * * The functions and macros in this chapter provide a convenient and * standard way to parse command line arguments and provide on-line * help for command-line invocation. An example at the end of the * chapter illustrates their use. * * It is important to understand that these functions are designed * to support *consistent* command line interfaces, rather than * *arbitrary* command line interfaces. Consequently, these functions * aren't especially useful for implementing some of the standard Posix * command line syntaxes. * */ /*(menu) */ /*(include-documentation "opt.h") */ /************************************************************************ *(h1 "Parsing Options") * */ /*(c opt_low) * int opt_low (t_uchar * opt_string, * t_uchar * opt_arg, * struct opt_desc ** desc, * struct opt_desc * opts, * int * argcp, * char ** argv, * t_uchar * opt_string_space); * * Return the next command line option and modify argc/argv to * remove that option. * * `opts' should point to an array of `struct opt_desc' whose final * element contains a value less than 0 in the field `opt_value'. * * Ordinarily `opt' returns the `opt_value' field from the element of * `opts' that matches the next argument. If the field `requires_arg' * is not 0, then the option argument is also returned. The option * and its argument are removed from argc/argv. `opt_string' returns * a string naming the option ("-x", "--long-opt" or * "--long-opt=value"). The string returned in `opt_string' may be * overwritten by later calls to `opt'. `opt_arg' returns * the argument to the option, if the argument requires an argument * and one was provided. A string returned in `opt_arg' points to * a string or substring from `argv'. `desc' returns the option * description for the option returned. * * Any of `opt_string', `opt_arg', and `desc' may be 0. * * `opt_string_space' should point to an array of three characters. * If the next option is a short option, the value stored in `*opt_string' * will point to `opt_string_space'. `opt_string_space' may be 0 * if `opt_string' is 0. * * If the option requires an argument but none is provided, `*opt_arg' * is set to 0, and the `opt_value' field of the option is returned; * argc/argv is not modified. To see that an option with required * argument has been correctly provided, it is necessary to check both * the return value of `opt', and the value of `*opt_arg'. This * behavior is to facilitate providing a specific error message to the * user -- to quote POSIX: "option arguments may not be optional". * * If the next argument is not an ordinary option, a value less than 0 * is returned. These special values are given symbolic names in * `opt.h' (`enum opt_return_values'): * * opt_dash -- the next argument is simply "-". * opt_double_dash -- the next argument is simply "--". * * In either case, the argument ("-" or "--") is removed from * argc/argv. * * opt_unknown -- the next argument does not match * any known option. * * opt_bogus_argument -- a recognized long option was * provided with an option argument, * but the option does not accept * an argument. * * opt_none -- the next argument does not begin * with "-". * * In any of those cases, argc/argv is not modified. */ int opt_low (t_uchar ** opt_string, t_uchar ** opt_arg, struct opt_desc ** desc, struct opt_desc * opts, int * argcp, char ** argv, t_uchar * opt_string_space) { int argc; int choice; t_uchar ** argvx; argc = *argcp; argvx = (t_uchar **)argv; if (opt_string) *opt_string = 0; if (opt_arg) *opt_arg = 0; if (desc) *desc = 0; if (argc == 0) return opt_none; if (!argvx[1]) return opt_none; if (argvx[1][0] != '-') return opt_none; if (argvx[1][1] != '-') { /* short option */ if (opt_string) { opt_string_space[0] = '-'; opt_string_space[1] = argvx[1][1]; opt_string_space[2] = 0; *opt_string = opt_string_space; } if (!argvx[1][1]) { opt_shift (argcp, argv); return opt_dash; } for (choice = 0; opts[choice].opt_value >= 0; ++choice) { if (opts[choice].char_name) { if (opts[choice].char_name[0] == argvx[1][1]) { if (desc) *desc = &opts[choice]; if (!opts[choice].requires_arg) { /* character option, no option argument. */ opt_shift_char_option (argcp, argv); return opts[choice].opt_value; } if (argvx[1][2]) { /* character option, with argument, "-xargument". */ if (opt_arg) *opt_arg = argvx[1] + 2; opt_shift (argcp, argv); return opts[choice].opt_value; } if (argc > 2) { /* character option, with argument, "-x argument". */ if (opt_arg) *opt_arg = argvx[2]; opt_shift (argcp, argv); /* shift away "-x" */ opt_shift (argcp, argv); /* shift away "argument" */ return opts[choice].opt_value; } { /* character option, missing argument. */ if (opt_arg) *opt_arg = 0; return opts[choice].opt_value; } } } } /* unrecognized character option */ return opt_unknown; } else { /* long option */ t_uchar * opt_name; t_uchar * opt_end; int opt_length; t_uchar * eq_arg; if (!str_cmp (argvx[1], "--")) { if (opt_string) *opt_string = "--"; opt_shift (argcp, argv); return opt_double_dash; } if (opt_string) *opt_string = argvx[1]; opt_name = argvx[1] + 2; eq_arg = str_chr_index (opt_name, '='); if (eq_arg) opt_end = eq_arg; else opt_end = opt_name + str_length (opt_name); opt_length = opt_end - opt_name; for (choice = 0; opts[choice].opt_value >= 0; ++choice) { t_uchar * spec_end; int spec_length; if (!opts[choice].long_name) continue; spec_end = str_chr_index (opts[choice].long_name, '='); if (!spec_end) spec_end = str_chr_index (opts[choice].long_name, ' '); if (spec_end) spec_length = spec_end - opts[choice].long_name; else spec_length = str_length (opts[choice].long_name); if ( (opt_length <= spec_length) && !str_cmp_n (opt_name, opt_length, opts[choice].long_name, spec_length) && ( (opt_length == spec_length) || char_is_space(opts[choice].long_name[opt_length]) || ('=' == opts[choice].long_name[opt_length]))) { if (desc) *desc = &opts[choice]; if (!opts[choice].requires_arg) { /* long option, no argument expected */ if (eq_arg) { /* command line contained an argument */ return opt_bogus_argument; } opt_shift (argcp, argv); return opts[choice].opt_value; } /* long option, argument expected */ if (eq_arg) { /* --foo=bar */ if (opt_arg) *opt_arg = eq_arg + 1; opt_shift (argcp, argv); return opts[choice].opt_value; } if (argc > 2) { /* --foo bar */ if (opt_arg) *opt_arg = argvx[2]; opt_shift (argcp, argv); opt_shift (argcp, argv); return opts[choice].opt_value; } { /* --foo, missing argument. */ if (opt_arg) *opt_arg = 0; return opts[choice].opt_value; } } } /* unrecognized long option */ return opt_unknown; } } int opt (alloc_limits limits, struct opt_parsed ** parsed_p, struct opt_desc * opts, int * argcp, char ** argv) { t_uchar * opt_string; t_uchar * arg_string; t_uchar arg_string_space[MB_LEN_MAX * 3]; struct opt_desc * desc; int o; struct opt_parsed * parsed; o = opt_low (&opt_string, &arg_string, &desc, opts, argcp, argv, arg_string_space); parsed = (struct opt_parsed *)lim_malloc_contiguous (limits, sizeof (struct opt_parsed), offsetof (struct opt_parsed, opt_string), str_length (opt_string) + 1, offsetof (struct opt_parsed, arg_string), str_length (arg_string) + 1, LIM_END); if (!parsed) return opt_allocation_failure; parsed->opt_value = o; if (!opt_string) parsed->opt_string = 0; else str_cpy (parsed->opt_string, opt_string); if (!arg_string) parsed->arg_string = 0; else str_cpy (parsed->arg_string, arg_string); parsed->desc = desc; if (*parsed_p) lim_free (limits, (void *)*parsed_p); *parsed_p = parsed; return o; } /*(c opt_standard) * int opt_standard (alloc_limits limits, * struct opt_parsed ** parsed_p, * struct opt_desc * opts, * int * argcp, * char * argvp[], * t_uchar * program_name, * t_uchar * usage, * t_uchar * version_string, * t_uchar * long_help, * int help_option, * int long_help_option, * int version_option); * * This function calls `opt', but handles some commonly * implemented options internally, in a standard way. * * The parameters `limits', `parsed_p', `opts', `argcp', * and `argvp' are as to `opt_low'. * * The parameters `program_name', `usage' and `version_string' are * used in error and informational messages printed by this function. * * The parameters `help_option', `long_help_option' and `version_option' * are the `name' value for some standard options, which are * usually given the command line names: * * -h --help help_option * -H long_help_option * -V --version version_option * * If this function detects the `help_option', it prints a * multi-line message summarizing the available options * to standard output and exits the process with a 0 status. * * If this function detects the `long_help_option', it prints a * multi-line message summarizing the available options * to standard output, adds text from `long_help' and exits the * process with a 0 status. The first line of `long_help' should * summarize the command (conventionally uncapitalized and without * punctuation). The rest of `long_help' should expand upon the * explanation. Long help messages are formatted as: * * first line of long_help * usage: program_name .... * option list * * rest of long_help * * If this function detects the `version_option', it prints a * single-line message containing `version_string' * to standard output and exits the process with a 0 status. * * If this function detects an unrecognized option, a missing option * argument, or an argument provided for an option that doesn't accept * arguments, it prints a usage message to standard error and exits * the process with a non-0 status. */ int opt_standard (alloc_limits limits, struct opt_parsed ** parsed_p, struct opt_desc * opts, int * argc, char * argv[], t_uchar * program_name, t_uchar * usage, t_uchar * version_string, t_uchar * long_help, int help_option, int long_help_option, int version_option) { int o; o = opt (limits, parsed_p, opts, argc, argv); if (o == opt_none) return o; if ((*parsed_p)->desc && (*parsed_p)->desc->requires_arg && !(*parsed_p)->arg_string) { safe_printfmt (2, "missing argument for `%s'\n", (*parsed_p)->opt_string); opt_usage (2, argv[0], program_name, usage, 1); panic_exit (); } if (o == help_option) { if (long_help) { t_uchar * nl; nl = str_chr_index (long_help, '\n'); if (nl) { safe_printfmt (1, "%.*s\n", (int)(nl - long_help), long_help); } } opt_usage (1, argv[0], program_name, usage, 0); safe_printfmt (1, "\n"); opt_help (1, opts); safe_printfmt (1, "\n"); exit (0); } if (o == long_help_option) { t_uchar * nl; nl = str_chr_index (long_help, '\n'); if (nl) safe_printfmt (1, "%.*s\n", (int)(nl - long_help), long_help); opt_usage (1, argv[0], program_name, usage, 0); safe_printfmt (1, "\n"); opt_help (1, opts); safe_printfmt (1, "\n"); if (nl) safe_printfmt (1, "%s", nl + 1); else safe_printfmt (1, "%s", long_help); safe_printfmt (1, "\n"); exit (0); } if (o == version_option) { safe_printfmt (1, "%s %s\n", program_name, version_string); exit (0); } if (o == opt_allocation_failure) { panic ("allocation failure while parsing arguments"); } switch (o) { case opt_dash: return o; case opt_double_dash: return o; case opt_unknown: safe_printfmt (2, "unhandled option `%s'\n", argv[1]); opt_usage (2, argv[0], program_name, usage, 1); panic_exit (); case opt_bogus_argument: safe_printfmt (2, "option `%s' does not accept an argument\n", argv[1]); opt_usage (2, argv[0], program_name, usage, 1); panic_exit (); default: if (o < 0) { safe_printfmt (2, "illegal option code %d for `%s'\n", o, argv[1]); panic ("internal error processing options"); } return o; } } /************************************************************************ *(h1 "Printing Help Messages") * */ /*(c opt_usage) * void opt_usage (int fd, * char * argv0, * t_uchar * program_name, * t_uchar * usage, * int suggest_help); * * Print a usage message on `fd' using the format: * * "usage: %s %s\n", program_name, usage * * If `suggest_help' is not 0, also print: * * "try %s --help\n", argv0 * * By convention, `argv0' should be the first command line string * passed to the program (the name by which the program was invoked). * * `program_name' should be the default name for the program (not * necessarily the name by which it was invoked). */ void opt_usage (int fd, char * argv0, t_uchar * program_name, t_uchar * usage, int suggest_help) { safe_printfmt (fd, "usage: %s %s\n", program_name, usage); if (suggest_help) safe_printfmt (fd, "try %s --help\n", argv0); } /*(c opt_help) * void opt_help (int fd, struct opt_desc * opts); * * Print a help message based on `opts'. * * A long option name in `opts' can have the form "--foo=BAR". * The text "=BAR" is included in the help message. */ void opt_help (int fd, struct opt_desc * opts) { int x; int max_char_opt_len; int max_long_opt_len; int max_desc_len; int line_count; int desc_column; t_uchar * buf; t_uchar * occ_buf; t_uchar * occ_this; t_uchar * occ_next; max_long_opt_len = 0; max_char_opt_len = 0; max_desc_len = 0; line_count = 0; for (x = 0; opts[x].opt_value >= 0; ++x) { int q; if (opts[x].char_name) { q = 1 + str_length (opts[x].char_name); if (q > max_char_opt_len) max_char_opt_len = q; } if (opts[x].long_name) { q = 2 + str_length (opts[x].long_name); if (q > max_long_opt_len) max_long_opt_len = q; } if (opts[x].desc) { occ_buf = occ_this = occ_next = str_save(0, opts[x].desc); while ((occ_this = str_separate(&occ_next, "\n")) != NULL) { q = str_length (occ_this); if (q > max_desc_len) max_desc_len = q; line_count++; } lim_free(0, occ_buf); } } desc_column = 2 + max_char_opt_len + 2 + max_long_opt_len + 2; buf = (t_uchar *)alloca ((desc_column + max_desc_len) * line_count + 1); for (x = 0; opts[x].opt_value >= 0; ++x) { int column; int amt; buf[0] = 0; str_cat (buf, " "); if (opts[x].char_name) { str_cat (buf, "-"); str_cat (buf, opts[x].char_name); if (opts[x].long_name) str_cat (buf, ", "); } column = str_length (buf); if (opts[x].long_name) { str_cat (buf, "--"); str_cat (buf, opts[x].long_name); column = str_length (buf); } while (column < desc_column) buf[column++] = ' '; buf[column] = 0; if (opts[x].desc) { occ_buf = occ_this = occ_next = str_save(0, opts[x].desc); while ((occ_this = str_separate(&occ_next, "\n")) != NULL) { str_cat (buf, occ_this); if (occ_next != NULL) { size_t l; str_cat (buf, "\n"); l = str_length (buf); mem_set (buf + l, ' ', desc_column); buf[l + desc_column] = 0; } } lim_free(0, occ_buf); } str_cat (buf, "\n"); amt = str_length (buf); safe_write_retry (fd, buf, amt); } } /************************************************************************ *(h1 "Shifting Arguments") * */ /*(c opt_shift_char_option) * void opt_shift_char_option (int * argcp, char ** argv); * * Remove a single character argument from argc/argv. For example, * * Before: * * *argcp = 3 * argv = {"prog", "-abc", "-def", 0} * * After: * * *argcp = 3 * argv = {"prog", "-bc", "-def", 0} * * Before: * * *argcp = 3 * argv = {"prog", "-a", "-def", 0} * * After: * * *argcp = 2 * argv = {"prog", "-def", 0} * */ void opt_shift_char_option (int * argcp, char ** argv) { t_uchar ** argvx; argvx = (t_uchar **)argv; if (!argvx[1][2]) opt_shift (argcp, argv); else mem_move (argvx[1] + 1, argvx[1] + 2, 1 + str_length (argvx[1] + 2)); } /*(c opt_shift) * void opt_shift (int * argcp, char ** argv); * * Remove a single argument from argc/argv. For example, * * Before: * * *argcp = 3 * argv = {"prog", "--abc", "-def", 0} * * After: * * *argcp = 2 * argv = {"prog", "-def", 0} * */ void opt_shift (int * argcp, char ** argv) { int argc; int x; t_uchar ** argvx; argc = *argcp; argvx = (t_uchar **)argv; --argc; for (x = 1; x < argc; ++x) argvx[x] = argvx[x + 1]; argvx[x] = 0; *argcp = argc; } #if 0 /************************************************************************ *(h1 "An Example of Option Parsing ") * * The following program illustrates option parsing with `opt'. * These examples illustrates its usage: * * % test --version * test 1.0.0 * * % test --help * usage: ./,test [options] [input-file] * * -h, --help Display a help message and exit. * -V, --version Display a release identifier string * and exit. * -o file, --output-file=file Write output to FILE. * * % ./,test -o one --output-file two -othree --output-file=four * output file argument: `one' * output file argument: `two' * output file argument: `three' * output file argument: `four' * * % ./,test -xyzzy * unhandled option `-xyzzy' * usage: ./,test [options] [input-file] * try ./,test --help * * * Here's the code: * insert*/ #include "hackerlab/cmd/main.h" static t_uchar * program_name = "test"; static t_uchar * usage = "[options]"; static t_uchar * version_string = "1.0"; #define OPTS(OP, OP2) \ OP (opt_help_msg, "h", "help", 0, \ "Display a help message and exit.") \ OP (opt_version, "V", "version", 0, \ "Display a release identifier string") \ OP2 (opt_version, 0, 0, 0, "and exit.") \ OP (opt_output_file, "o file", "output-file=file", 1, \ "Write output to FILE.") enum options { OPTS (OPT_ENUM, OPT_IGN) }; struct opt_desc opts[] = { OPTS (OPT_DESC, OPT_DESC) {-1, 0, 0, 0, 0} }; int main (int argc, char * argv[]) { int errn; int o; struct opt_parsed * option; option = 0; while (1) { o = opt_standard (lim_use_must_malloc, &option, opts, &argc, argv, program_name, usage, version_string, opt_help_msg, opt_version); if (o == opt_none) break; switch (o) { default: safe_printfmt (2, "unhandled option `%s'\n", option->opt_string); panic ("internal error parsing arguments"); usage_error: opt_usage (2, argv[0], program_name, usage, 1); panic_exit (); bogus_arg: safe_printfmt (2, "ill-formed argument for `%s' (`%s')\n", option->opt_string, option->arg_string); goto usage_error; case opt_output_file: if (!opt_arg) { printfmt (&errn, 2, "missing argument for `%s'\n", opt_string); goto usage_error; } printfmt (&errn, 2, "output file argument: `%s'\n", opt_arg); break; } } return 0; } /*end-insert */ #endif