| // Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "argparse.h" | |
| #include <assert.h> | |
| #include <errno.h> | |
| #include <limits.h> | |
| #include <stdarg.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| // Convenience typedefs for the implementation. | |
| typedef enum argparse_option_type opt_type_t; | |
| typedef struct argparse_option_layout opt_layout_t; | |
| typedef enum argparse_status status_t; | |
| // Return true iff a given option |type| requires a parameter. | |
| // Update this function when argparse_option_type changes! | |
| static bool | |
| opt_type_requires_parameter(opt_type_t const type) | |
| { | |
| return (type == ARGPARSE_OPTION_TYPE_STRING) || (type == ARGPARSE_OPTION_TYPE_INT) || | |
| (type == ARGPARSE_OPTION_TYPE_DOUBLE); | |
| } | |
| #ifndef NDEBUG | |
| static bool | |
| opt_layout_is_help_option(const opt_layout_t * const layout) | |
| { | |
| return layout->opt_type == ARGPARSE_OPTION_TYPE_HELP; | |
| } | |
| #endif | |
| // Print parsing error to stderr then return ARGPARSE_STATUS_ERROR | |
| static bool | |
| argparse_error(const char * fmt, ...) | |
| { | |
| va_list args; | |
| fprintf(stderr, "ERROR: "); | |
| va_start(args, fmt); | |
| vfprintf(stderr, fmt, args); | |
| va_end(args); | |
| fprintf(stderr, "\n"); | |
| return false; | |
| } | |
| // Returns true if |arg| matches |layout| exactly (e.g. "--foo" or "-f"). | |
| static bool | |
| opt_layout_matches(const opt_layout_t * layout, const char * arg) | |
| { | |
| assert(arg[0] == '-'); | |
| return (arg[1] == '-' && !strcmp(layout->opt_long, arg + 2)) || | |
| (arg[1] == layout->opt_char && arg[2] == 0); | |
| } | |
| // Returns true if --help or -? appears on the command line, and is not | |
| // a parameter of a previous option. | |
| static bool | |
| argparse_has_help_argument(int argc, | |
| const char * const * argv, | |
| const opt_layout_t * layouts, | |
| const opt_layout_t * layouts_limit, | |
| void * const * const val_ptrs) | |
| { | |
| // Uses the fact that the help option is the sentinel at the end of the list. | |
| assert(opt_layout_is_help_option(&layouts_limit[-1])); | |
| const opt_layout_t * const help = layouts_limit - 1; | |
| for (int pos = 1; pos < argc; ++pos) | |
| { | |
| const char * arg = argv[pos]; | |
| if (arg[0] != '-') | |
| continue; | |
| if (arg[1] == '-' && arg[2] == '\0') // Treat -- as a parser stop. | |
| return false; | |
| if (!opt_layout_matches(help, arg)) | |
| continue; | |
| // Found it, check previous argument now. | |
| bool is_help = true; | |
| if (pos > 1) | |
| { | |
| arg = argv[pos - 1]; | |
| if (arg[0] == '-') | |
| { | |
| const opt_layout_t * layout; | |
| for (layout = layouts; layout < layouts_limit; ++layout) | |
| { | |
| if (opt_layout_matches(layout, arg)) | |
| { | |
| is_help = false; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (is_help) | |
| { | |
| *(bool *)val_ptrs[help - layouts] = true; // set help_needed flag. | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| static bool | |
| opt_layout_apply(const opt_layout_t * layout, void * const ptr, const char * parameter) | |
| { | |
| switch (layout->opt_type) | |
| { | |
| case ARGPARSE_OPTION_TYPE_FLAG: | |
| *((bool *)ptr) = true; | |
| break; | |
| case ARGPARSE_OPTION_TYPE_STRING: | |
| *((const char **)ptr) = parameter; | |
| break; | |
| case ARGPARSE_OPTION_TYPE_COUNTER: | |
| *((int *)ptr) += 1; | |
| break; | |
| case ARGPARSE_OPTION_TYPE_INT: { | |
| assert(parameter); | |
| char * end = NULL; | |
| long val_long = strtol(parameter, &end, 0); | |
| if (val_long < INT_MIN || val_long > INT_MAX) | |
| return argparse_error("Integer value out of range: %s", parameter); | |
| if (!*parameter || *end != '\0') | |
| return argparse_error("Integer expected: %s", parameter); | |
| *((struct argparse_int *)ptr) = | |
| (struct argparse_int){ .used = 1, .value = (int)val_long }; | |
| break; | |
| } | |
| case ARGPARSE_OPTION_TYPE_DOUBLE: { | |
| assert(parameter); | |
| char * end = NULL; | |
| errno = 0; | |
| double val_double = strtod(parameter, &end); | |
| if (errno == ERANGE) | |
| return argparse_error("Double value out of range: %s", parameter); | |
| if (!*parameter || *end != '\0') | |
| return argparse_error("Double expected: %s", parameter); | |
| *((struct argparse_double *)ptr) = | |
| (struct argparse_double){ .used = 1, .value = val_double }; | |
| break; | |
| } | |
| case ARGPARSE_OPTION_TYPE_HELP: | |
| // This corresponds to -? and --help. Return an error, the client | |
| // should check options.help_needed after that, | |
| assert(opt_layout_is_help_option(layout)); | |
| *((bool *)ptr) = true; | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Find the pointer past the last item in a |layouts| array. | |
| static const opt_layout_t * | |
| opt_layouts_get_limit(const opt_layout_t * layouts) | |
| { | |
| while (layouts->opt_type != ARGPARSE_OPTION_TYPE_HELP) | |
| layouts++; | |
| assert(opt_layout_is_help_option(layouts)); | |
| return layouts + 1; | |
| } | |
| void | |
| argparse_print_help_internal(const char * program_name, | |
| const char * program_description, | |
| const opt_layout_t * const layouts, | |
| argparse_print_func print, | |
| void * out) | |
| { | |
| print(out, "Usage: %s [options] ...\n\n", program_name ? program_name : "<program>"); | |
| if (program_description) | |
| print(out, "%s\n\n", program_description); | |
| const opt_layout_t * layouts_limit = opt_layouts_get_limit(layouts); | |
| const opt_layout_t * layout; | |
| // Liberally chosen esthetical constants. | |
| const size_t margin = 2; | |
| const size_t max_column1_width = 16; | |
| const size_t max_line_width = 64; | |
| // TECHNICAL NOTE: This uses the fact that printf("%*s", count, "") will | |
| // always output |count| spaces to the output. Used for margins and padding. | |
| // First pass is used to measure text widths, second pass is used to print. | |
| size_t column1_width = 0; | |
| size_t column2_pos = 0; | |
| size_t column2_width = 0; | |
| for (int pass = 0; pass < 2; ++pass) | |
| { | |
| for (layout = layouts; layout < layouts_limit; ++layout) | |
| { | |
| // Print/measure the first column. | |
| bool do_print = (pass == 1); | |
| bool add_newline = false; // To add a newline after 'long' text. | |
| if (do_print) | |
| print(out, "%*s", margin, ""); | |
| size_t pos = margin; | |
| if (layout->opt_char) | |
| { | |
| if (do_print) | |
| print(out, "-%c", layout->opt_char); | |
| pos += 2; | |
| } | |
| if (layout->opt_long) | |
| { | |
| if (layout->opt_char) | |
| { | |
| if (do_print) | |
| print(out, ", "); | |
| pos += 2; | |
| } | |
| const char * option = layout->opt_long; | |
| size_t option_len = strlen(layout->opt_long); | |
| if (do_print) | |
| { | |
| print(out, "--%s", option); | |
| } | |
| pos += 2 + option_len; | |
| if (opt_type_requires_parameter(layout->opt_type)) | |
| { | |
| if (do_print) | |
| { | |
| print(out, "="); | |
| // print uppercase version of option | |
| for (size_t n = 0; n < option_len; n++) | |
| { | |
| int ch = option[n]; | |
| if (ch >= 'a' && ch < 'z') // avoid locale-dependent toupper(). | |
| ch ^= ('a' ^ 'A'); | |
| else if (ch == '-') | |
| ch = '_'; | |
| print(out, "%c", ch); | |
| } | |
| } | |
| pos += 1 + option_len; | |
| } | |
| } | |
| // Compute the width of the first column here. | |
| size_t width = pos - margin; | |
| if (width > max_column1_width) | |
| { | |
| width = max_column1_width; | |
| add_newline = true; | |
| } | |
| if (width > column1_width) | |
| column1_width = width; | |
| // Print second column text (no measurement on pass 0). | |
| if (pass == 1) | |
| { | |
| if (!layout->opt_description) | |
| { | |
| print(out, "\n"); // No description at all?? | |
| } | |
| else | |
| { | |
| if (pos > column2_pos) | |
| { | |
| print(out, "\n"); | |
| pos = 0; | |
| } | |
| if (pos < column2_pos) | |
| { | |
| print(out, "%*s", column2_pos - pos, ""); | |
| pos = column2_pos; | |
| } | |
| // Handle multi-line text now. | |
| const char * text = layout->opt_description; | |
| const char * text_limit = text + strlen(text); | |
| size_t line_margin = 0; | |
| while (text < text_limit) | |
| { | |
| const char * line = text; | |
| size_t line_len = (text_limit - text); | |
| text = text_limit; | |
| // Split lines longer than |column2_width| here. | |
| if (line_len > column2_width) | |
| { | |
| const char * line_end = line + column2_width; | |
| while (line_end > line && line_end[-1] != ' ') | |
| line_end--; | |
| if (line_end != line) | |
| { | |
| text = line_end; | |
| while (line_end > line && line_end[-1] == ' ') | |
| line_end--; | |
| line_len = (line_end - line); | |
| } | |
| add_newline = true; | |
| } | |
| print(out, "%*s%.*s\n", line_margin, "", line_len, line); | |
| line_margin = column2_pos; | |
| } | |
| } | |
| if (add_newline) | |
| print(out, "\n"); | |
| } | |
| } | |
| column2_pos = margin + column1_width + 2; | |
| column2_width = max_line_width - column2_pos; | |
| } | |
| } | |
| static void | |
| argparse_fprintf(void * opaque, const char * fmt, ...) | |
| { | |
| va_list args; | |
| va_start(args, fmt); | |
| vfprintf((FILE *)opaque, fmt, args); | |
| va_end(args); | |
| } | |
| void | |
| argparse_print_help(const char * program_name, | |
| const char * program_description, | |
| const opt_layout_t * const layouts) | |
| { | |
| argparse_print_help_internal(program_name, | |
| program_description, | |
| layouts, | |
| argparse_fprintf, | |
| stdout); | |
| } | |
| bool | |
| argparse_parse_args(int * const p_argc, | |
| const char ** const argv, | |
| const opt_layout_t * const layouts, | |
| void * const * const val_ptrs) | |
| { | |
| int argc = *p_argc; | |
| const char ** argv_write = argv + 1; | |
| const opt_layout_t * layouts_limit = opt_layouts_get_limit(layouts); | |
| int arg_pos = 1; // Ignore first item, i.e program name. | |
| // A first pass to detect whether --help or -? appears on the command | |
| // line, and not a parameter of a previous option. In this case, return | |
| // immediately, without trying to parse the other options, which could | |
| // be totally random. | |
| if (argparse_has_help_argument(argc, argv, layouts, layouts_limit, val_ptrs)) | |
| return false; | |
| for (arg_pos = 1; arg_pos < argc; arg_pos++) | |
| { | |
| const char * arg = argv[arg_pos]; | |
| if (arg[0] != '-') | |
| { // Not an option. | |
| *argv_write++ = arg; | |
| continue; | |
| } | |
| const opt_layout_t * layout; | |
| const char * parameter = NULL; | |
| if (arg[1] != '-') | |
| { // Short option | |
| arg += 1; | |
| do | |
| { // Process all characters until one of them requires a parameter | |
| int ch = *arg++; | |
| // Find option in list | |
| for (layout = layouts; layout < layouts_limit; ++layout) | |
| { | |
| if (layout->opt_char && layout->opt_char == ch) | |
| break; | |
| } | |
| if (layout == layouts_limit) | |
| return argparse_error("Unknown option -%c, please see --help", ch); | |
| // Handle optional parameter | |
| if (opt_type_requires_parameter(layout->opt_type)) | |
| { | |
| if (arg[0]) | |
| { | |
| // Parameter follows option char, e.g. -Iinclude | |
| parameter = arg; | |
| arg = ""; // Stop iteration after option processing. | |
| } | |
| else | |
| { | |
| // Parameter must appear as the next command-line argument. | |
| if (arg_pos + 1 >= argc) | |
| { | |
| return argparse_error("Missing parameter after -%c option!", ch); | |
| } | |
| arg_pos += 1; | |
| parameter = argv[arg_pos]; | |
| } | |
| } | |
| if (!opt_layout_apply(layout, val_ptrs[layout - layouts], parameter)) | |
| return false; | |
| } | |
| while (*arg); | |
| continue; | |
| } | |
| // Process long option | |
| arg += 2; | |
| if (*arg == '\0') | |
| { // Treat '--' as a special case to stop processing. | |
| arg_pos += 1; | |
| break; | |
| } | |
| // Extract potential parameter and option name length | |
| const char * arg_end = strchr(arg, '='); | |
| size_t arg_len; | |
| if (arg_end) | |
| { // For --include=<dir> | |
| parameter = arg_end + 1; | |
| arg_len = arg_end - arg; | |
| } | |
| else | |
| { | |
| arg_len = strlen(arg); | |
| } | |
| // Find in options list | |
| for (layout = layouts; layout < layouts_limit; layout++) | |
| { | |
| if (layout->opt_long) | |
| { | |
| size_t opt_len = strlen(layout->opt_long); | |
| if (opt_len == arg_len && !memcmp(layout->opt_long, arg, arg_len)) | |
| break; | |
| } | |
| } | |
| if (layout == layouts_limit) | |
| return argparse_error("Unknown option --%s, please see --help", arg); | |
| // Extract parameter if needed and ensure there is no extra one! | |
| if (opt_type_requires_parameter(layout->opt_type)) | |
| { | |
| if (!parameter) | |
| { | |
| if (arg_pos + 1 >= argc) | |
| return argparse_error("Missing parameter after --%s option!", arg); | |
| arg_pos += 1; | |
| parameter = argv[arg_pos]; | |
| } | |
| } | |
| else if (parameter) | |
| { | |
| return argparse_error("Option --%s does not take a parameter!", arg); | |
| } | |
| if (!opt_layout_apply(layout, val_ptrs[layout - layouts], parameter)) | |
| return false; | |
| } | |
| // Copy the rest of the arguments and adjust argc. | |
| while (arg_pos < argc) | |
| { | |
| *argv_write++ = argv[arg_pos++]; | |
| } | |
| *p_argc = (argv_write - argv); | |
| // Done | |
| return true; | |
| } |