Add iterator-style API
authorSimon Marchi <simon.marchi@efficios.com>
Sat, 10 Apr 2021 13:51:38 +0000 (09:51 -0400)
committerPhilippe Proulx <eeppeliteloop@gmail.com>
Mon, 31 May 2021 20:13:52 +0000 (16:13 -0400)
The current API of argpar does not give the user control on when to stop
parsing the arguments.  It either stops at the end of the argument list
or at the first unknown option, if using `fail_on_unknown_opt`.

To make it more convenient to implement "multi-level" command line
interfaces, add an iterator-style API where the user repeatedly calls a
function to get the next item.

By "multi-level" CLI, I mean something like git, where the top-level
command and a subcommand have their own set of options.

    $ top-level --hello there sub-command --hello

For most cases, we could probably work something out by using the
`fail_on_unknown_opt` option combined with the
`argpar_item_non_opt::orig_index` field.  Even if the parsing of the
top-level command goes too far, we can resume the parsing at the index
of the sub-command + 1.  But the example above shows a corner case where
top-level's `--hello` option takes an argument but sub-command's
`--hello` doesn't.  I don't think we could handle this today.

In any case, I think that it's simpler in the end to give a bit more
control to the user and let her get items one by one.  I think it
results in code that is easier to follow.

Using the proposed API, the user starts by creating an argpar_iter
object using argpar_iter_create.  It then calls argpar_iter_parse_next
as much as needed to get one argpar_item at a time.  The returned
argpar_items must be freed with argpar_item_destroy.  In the end, the
argpar_iter must be freed with argpar_iter_destroy.

Testing-wise, I adapted the existing tests to be executed with both
APIs, so the coverage should be pretty good.

Change-Id: I82685672977fbfa96f96bcc678a3e05c6adab76c
Signed-off-by: Simon Marchi <simon.marchi@efficios.com>
argpar/argpar.c
argpar/argpar.h
tests/test_argpar.c

index f5275aa755649cde191e5e71807e990e2f85a3cd..9e3ab7625c6f5d6f51e05d98b7d872ddcbea9da8 100644 (file)
@@ -1,7 +1,8 @@
 /*
  * SPDX-License-Identifier: MIT
  *
- * Copyright 2019 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2019-2021 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2020-2021 Simon Marchi <simon.marchi@efficios.com>
  */
 
 #include <assert.h>
 # define ARGPAR_PRINTF_FORMAT printf
 #endif
 
+/*
+ * An argpar iterator.
+ *
+ * Such a structure contains the state of an iterator between
+ * calls to argpar_iter_parse_next().
+ */
+struct argpar_iter {
+       /*
+        * Data provided by the user to argpar_iter_create(); immutable
+        * afterwards.
+        */
+       unsigned int argc;
+       const char * const *argv;
+       const struct argpar_opt_descr *descrs;
+
+       /*
+        * Index of the argument to process in the next
+        * argpar_iter_parse_next() call.
+        */
+       unsigned int i;
+
+       /* Counter of non-option arguments */
+       int non_opt_index;
+
+       /*
+        * Current character of the current short option group: if it's
+        * not `NULL`, the parser is in within a short option group,
+        * therefore it must resume there in the next
+        * argpar_iter_parse_next() call.
+        */
+       const char *short_opt_ch;
+};
+
 static __attribute__((format(ARGPAR_PRINTF_FORMAT, 1, 0)))
 char *argpar_vasprintf(const char *fmt, va_list args)
 {
@@ -104,20 +138,21 @@ end:
        return success;
 }
 
-static
-void destroy_item(struct argpar_item * const item)
+ARGPAR_HIDDEN
+void argpar_item_destroy(const struct argpar_item * const item)
 {
        if (!item) {
                goto end;
        }
 
        if (item->type == ARGPAR_ITEM_TYPE_OPT) {
-               struct argpar_item_opt * const opt_item = (void *) item;
+               struct argpar_item_opt * const opt_item =
+                       (struct argpar_item_opt *) item;
 
                free((void *) opt_item->arg);
        }
 
-       free(item);
+       free((void *) item);
 
 end:
        return;
@@ -163,7 +198,7 @@ void destroy_item_array(struct argpar_item_array * const array)
                unsigned int i;
 
                for (i = 0; i < array->n_items; i++) {
-                       destroy_item(array->items[i]);
+                       argpar_item_destroy(array->items[i]);
                }
 
                free(array->items);
@@ -224,7 +259,7 @@ struct argpar_item_opt *create_opt_item(
        goto end;
 
 error:
-       destroy_item(&opt_item->base);
+       argpar_item_destroy(&opt_item->base);
        opt_item = NULL;
 
 end:
@@ -285,72 +320,74 @@ static
 enum parse_orig_arg_opt_ret parse_short_opts(const char * const short_opts,
                const char * const next_orig_arg,
                const struct argpar_opt_descr * const descrs,
-               struct argpar_parse_ret * const parse_ret,
-               bool * const used_next_orig_arg)
+               struct argpar_iter * const iter,
+               char ** const error, struct argpar_item ** const item)
 {
        enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK;
-       const char *short_opt_ch = short_opts;
+       bool used_next_orig_arg = false;
+       const char *opt_arg = NULL;
+       const struct argpar_opt_descr *descr;
+       struct argpar_item_opt *opt_item;
 
        if (strlen(short_opts) == 0) {
-               argpar_string_append_printf(&parse_ret->error, "Invalid argument");
+               argpar_string_append_printf(error, "Invalid argument");
                goto error;
        }
 
-       while (*short_opt_ch) {
-               const char *opt_arg = NULL;
-               const struct argpar_opt_descr *descr;
-               struct argpar_item_opt *opt_item;
-
-               /* Find corresponding option descriptor */
-               descr = find_descr(descrs, *short_opt_ch, NULL);
-               if (!descr) {
-                       ret = PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT;
-                       argpar_string_append_printf(&parse_ret->error,
-                               "Unknown option `-%c`", *short_opt_ch);
-                       goto error;
-               }
+       if (!iter->short_opt_ch) {
+               iter->short_opt_ch = short_opts;
+       }
 
-               if (descr->with_arg) {
-                       if (short_opt_ch[1]) {
-                               /* `-oarg` form */
-                               opt_arg = &short_opt_ch[1];
-                       } else {
-                               /* `-o arg` form */
-                               opt_arg = next_orig_arg;
-                               *used_next_orig_arg = true;
-                       }
+       /* Find corresponding option descriptor */
+       descr = find_descr(descrs, *iter->short_opt_ch, NULL);
+       if (!descr) {
+               ret = PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT;
+               argpar_string_append_printf(error,
+                       "Unknown option `-%c`", *iter->short_opt_ch);
+               goto error;
+       }
 
-                       /*
-                        * We accept `-o ''` (empty option's argument),
-                        * but not `-o` alone if an option's argument is
-                        * expected.
-                        */
-                       if (!opt_arg || (short_opt_ch[1] && strlen(opt_arg) == 0)) {
-                               argpar_string_append_printf(&parse_ret->error,
-                                       "Missing required argument for option `-%c`",
-                                       *short_opt_ch);
-                               *used_next_orig_arg = false;
-                               goto error;
-                       }
+       if (descr->with_arg) {
+               if (iter->short_opt_ch[1]) {
+                       /* `-oarg` form */
+                       opt_arg = &iter->short_opt_ch[1];
+               } else {
+                       /* `-o arg` form */
+                       opt_arg = next_orig_arg;
+                       used_next_orig_arg = true;
                }
 
-               /* Create and append option argument */
-               opt_item = create_opt_item(descr, opt_arg);
-               if (!opt_item) {
+               /*
+                * We accept `-o ''` (empty option argument), but not
+                * `-o` alone if an option argument is expected.
+                */
+               if (!opt_arg || (iter->short_opt_ch[1] && strlen(opt_arg) == 0)) {
+                       argpar_string_append_printf(error,
+                               "Missing required argument for option `-%c`",
+                               *iter->short_opt_ch);
+                       used_next_orig_arg = false;
                        goto error;
                }
+       }
 
-               if (!push_item(parse_ret->items, &opt_item->base)) {
-                       goto error;
-               }
+       /* Create and append option argument */
+       opt_item = create_opt_item(descr, opt_arg);
+       if (!opt_item) {
+               goto error;
+       }
 
-               if (descr->with_arg) {
-                       /* Option has an argument: no more options */
-                       break;
-               }
+       *item = &opt_item->base;
+       iter->short_opt_ch++;
 
-               /* Go to next short option */
-               short_opt_ch++;
+       if (descr->with_arg || !*iter->short_opt_ch) {
+               /* Option has an argument: no more options */
+               iter->short_opt_ch = NULL;
+
+               if (used_next_orig_arg) {
+                       iter->i += 2;
+               } else {
+                       iter->i++;
+               }
        }
 
        goto end;
@@ -368,13 +405,14 @@ static
 enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
                const char * const next_orig_arg,
                const struct argpar_opt_descr * const descrs,
-               struct argpar_parse_ret * const parse_ret,
-               bool * const used_next_orig_arg)
+               struct argpar_iter * const iter,
+               char ** const error, struct argpar_item ** const item)
 {
        const size_t max_len = 127;
        enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK;
        const struct argpar_opt_descr *descr;
        struct argpar_item_opt *opt_item;
+       bool used_next_orig_arg = false;
 
        /* Option's argument, if any */
        const char *opt_arg = NULL;
@@ -389,8 +427,7 @@ enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
        const char *long_opt_name = long_opt_arg;
 
        if (strlen(long_opt_arg) == 0) {
-               argpar_string_append_printf(&parse_ret->error,
-                       "Invalid argument");
+               argpar_string_append_printf(error, "Invalid argument");
                goto error;
        }
 
@@ -401,7 +438,7 @@ enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
 
                /* Isolate the option name */
                if (long_opt_name_size > max_len) {
-                       argpar_string_append_printf(&parse_ret->error,
+                       argpar_string_append_printf(error,
                                "Invalid argument `--%s`", long_opt_arg);
                        goto error;
                }
@@ -414,7 +451,7 @@ enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
        /* Find corresponding option descriptor */
        descr = find_descr(descrs, '\0', long_opt_name);
        if (!descr) {
-               argpar_string_append_printf(&parse_ret->error,
+               argpar_string_append_printf(error,
                        "Unknown option `--%s`", long_opt_name);
                ret = PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT;
                goto error;
@@ -428,23 +465,22 @@ enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
                } else {
                        /* `--long-opt arg` style */
                        if (!next_orig_arg) {
-                               argpar_string_append_printf(&parse_ret->error,
+                               argpar_string_append_printf(error,
                                        "Missing required argument for option `--%s`",
                                        long_opt_name);
                                goto error;
                        }
 
                        opt_arg = next_orig_arg;
-                       *used_next_orig_arg = true;
+                       used_next_orig_arg = true;
                }
        } else if (eq_pos) {
                /*
                 * Unexpected `--opt=arg` style for a long option which
                 * doesn't accept an argument.
                 */
-               argpar_string_append_printf(&parse_ret->error,
-                       "Unexpected argument for option `--%s`",
-                       long_opt_name);
+               argpar_string_append_printf(error,
+                       "Unexpected argument for option `--%s`", long_opt_name);
                goto error;
        }
 
@@ -454,10 +490,13 @@ enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg,
                goto error;
        }
 
-       if (!push_item(parse_ret->items, &opt_item->base)) {
-               goto error;
+       if (used_next_orig_arg) {
+               iter->i += 2;
+       } else {
+               iter->i++;
        }
 
+       *item = &opt_item->base;
        goto end;
 
 error:
@@ -473,8 +512,9 @@ static
 enum parse_orig_arg_opt_ret parse_orig_arg_opt(const char * const orig_arg,
                const char * const next_orig_arg,
                const struct argpar_opt_descr * const descrs,
-               struct argpar_parse_ret * const parse_ret,
-               bool * const used_next_orig_arg)
+               struct argpar_iter * const iter,
+               char ** const error,
+               struct argpar_item ** const item)
 {
        enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK;
 
@@ -483,13 +523,11 @@ enum parse_orig_arg_opt_ret parse_orig_arg_opt(const char * const orig_arg,
        if (orig_arg[1] == '-') {
                /* Long option */
                ret = parse_long_opt(&orig_arg[2],
-                       next_orig_arg, descrs, parse_ret,
-                       used_next_orig_arg);
+                       next_orig_arg, descrs, iter, error, item);
        } else {
                /* Short option */
                ret = parse_short_opts(&orig_arg[1],
-                       next_orig_arg, descrs, parse_ret,
-                       used_next_orig_arg);
+                       next_orig_arg, descrs, iter, error, item);
        }
 
        return ret;
@@ -521,93 +559,170 @@ end:
 }
 
 ARGPAR_HIDDEN
-struct argpar_parse_ret argpar_parse(unsigned int argc,
-               const char * const *argv,
-               const struct argpar_opt_descr * const descrs,
-               bool fail_on_unknown_opt)
+struct argpar_iter *argpar_iter_create(const unsigned int argc,
+               const char * const * const argv,
+               const struct argpar_opt_descr * const descrs)
 {
-       struct argpar_parse_ret parse_ret = { 0 };
-       unsigned int i;
-       unsigned int non_opt_index = 0;
+       struct argpar_iter * const iter = argpar_zalloc(struct argpar_iter);
 
-       parse_ret.items = new_item_array();
-       if (!parse_ret.items) {
-               goto error;
+       if (!iter) {
+               goto end;
        }
 
-       for (i = 0; i < argc; i++) {
-               enum parse_orig_arg_opt_ret parse_orig_arg_opt_ret;
-               bool used_next_orig_arg = false;
-               const char * const orig_arg = argv[i];
-               const char * const next_orig_arg =
-                       i < argc - 1 ? argv[i + 1] : NULL;
+       iter->argc = argc;
+       iter->argv = argv;
+       iter->descrs = descrs;
 
-               if (orig_arg[0] != '-') {
-                       /* Non-option argument */
-                       struct argpar_item_non_opt *non_opt_item =
-                               create_non_opt_item(orig_arg, i, non_opt_index);
+end:
+       return iter;
+}
 
-                       if (!non_opt_item) {
-                               goto error;
-                       }
+ARGPAR_HIDDEN
+void argpar_iter_destroy(struct argpar_iter * const iter)
+{
+       free(iter);
+}
 
-                       non_opt_index++;
+ARGPAR_HIDDEN
+enum argpar_iter_parse_next_status argpar_iter_parse_next(
+               struct argpar_iter * const iter,
+               const struct argpar_item ** const item,
+               char ** const error)
+{
+       enum argpar_iter_parse_next_status status;
+       enum parse_orig_arg_opt_ret parse_orig_arg_opt_ret;
+       const char *orig_arg;
+       const char *next_orig_arg;
 
-                       if (!push_item(parse_ret.items, &non_opt_item->base)) {
-                               goto error;
-                       }
+       ARGPAR_ASSERT(iter->i <= iter->argc);
+       *error = NULL;
+
+       if (iter->i == iter->argc) {
+               status = ARGPAR_ITER_PARSE_NEXT_STATUS_END;
+               goto end;
+       }
 
-                       continue;
+       orig_arg = iter->argv[iter->i];
+       next_orig_arg =
+               iter->i < (iter->argc - 1) ? iter->argv[iter->i + 1] : NULL;
+
+       if (orig_arg[0] != '-') {
+               /* Non-option argument */
+               struct argpar_item_non_opt * const non_opt_item =
+                       create_non_opt_item(orig_arg, iter->i,
+                               iter->non_opt_index);
+
+               if (!non_opt_item) {
+                       status = ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR;
+                       goto end;
                }
 
-               /* Option argument */
-               parse_orig_arg_opt_ret = parse_orig_arg_opt(orig_arg,
-                       next_orig_arg, descrs, &parse_ret, &used_next_orig_arg);
-               switch (parse_orig_arg_opt_ret) {
-               case PARSE_ORIG_ARG_OPT_RET_OK:
-                       break;
-               case PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT:
-                       ARGPAR_ASSERT(!used_next_orig_arg);
+               iter->non_opt_index++;
+               iter->i++;
+               *item = &non_opt_item->base;
+               status = ARGPAR_ITER_PARSE_NEXT_STATUS_OK;
+               goto end;
+       }
 
+       /* Option argument */
+       parse_orig_arg_opt_ret = parse_orig_arg_opt(orig_arg,
+               next_orig_arg, iter->descrs, iter, error,
+               (struct argpar_item **) item);
+       switch (parse_orig_arg_opt_ret) {
+       case PARSE_ORIG_ARG_OPT_RET_OK:
+               status = ARGPAR_ITER_PARSE_NEXT_STATUS_OK;
+               break;
+       case PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT:
+               prepend_while_parsing_arg_to_error(error, iter->i, orig_arg);
+               status = ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT;
+               break;
+       case PARSE_ORIG_ARG_OPT_RET_ERROR:
+               prepend_while_parsing_arg_to_error(error, iter->i, orig_arg);
+               status = ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR;
+               break;
+       default:
+               abort();
+       }
+
+end:
+       return status;
+}
+
+ARGPAR_HIDDEN
+unsigned int argpar_iter_get_ingested_orig_args(
+               const struct argpar_iter * const iter)
+{
+       return iter->i;
+}
+
+ARGPAR_HIDDEN
+struct argpar_parse_ret argpar_parse(const unsigned int argc,
+               const char * const * const argv,
+               const struct argpar_opt_descr * const descrs,
+               const bool fail_on_unknown_opt)
+{
+       struct argpar_parse_ret parse_ret = { 0 };
+       const struct argpar_item *item = NULL;
+       struct argpar_iter *iter = NULL;
+
+       parse_ret.items = new_item_array();
+       if (!parse_ret.items) {
+               parse_ret.error = strdup("Failed to create items array.");
+               ARGPAR_ASSERT(parse_ret.error);
+               goto error;
+       }
+
+       iter = argpar_iter_create(argc, argv, descrs);
+       if (!iter) {
+               parse_ret.error = strdup("Failed to create argpar iter.");
+               ARGPAR_ASSERT(parse_ret.error);
+               goto error;
+       }
+
+       while (true) {
+               enum argpar_iter_parse_next_status status;
+
+               status = argpar_iter_parse_next(iter, &item, &parse_ret.error);
+               if (status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR) {
+                       goto error;
+               } else if (status == ARGPAR_ITER_PARSE_NEXT_STATUS_END) {
+                       break;
+               } else if (status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT) {
                        if (fail_on_unknown_opt) {
-                               prepend_while_parsing_arg_to_error(
-                                       &parse_ret.error, i, orig_arg);
+                               parse_ret.ingested_orig_args =
+                                       argpar_iter_get_ingested_orig_args(iter);
                                goto error;
                        }
 
-                       /*
-                        * The current original argument is not
-                        * considered ingested because it triggered an
-                        * unknown option.
-                        */
-                       parse_ret.ingested_orig_args = i;
                        free(parse_ret.error);
                        parse_ret.error = NULL;
-                       goto end;
-               case PARSE_ORIG_ARG_OPT_RET_ERROR:
-                       prepend_while_parsing_arg_to_error(
-                               &parse_ret.error, i, orig_arg);
-                       goto error;
-               default:
-                       abort();
+                       break;
                }
 
-               if (used_next_orig_arg) {
-                       i++;
+               ARGPAR_ASSERT(status == ARGPAR_ITER_PARSE_NEXT_STATUS_OK);
+
+               if (!push_item(parse_ret.items, (void *) item)) {
+                       goto error;
                }
+
+               item = NULL;
        }
 
-       parse_ret.ingested_orig_args = argc;
-       free(parse_ret.error);
-       parse_ret.error = NULL;
+       ARGPAR_ASSERT(!parse_ret.error);
+       parse_ret.ingested_orig_args =
+               argpar_iter_get_ingested_orig_args(iter);
        goto end;
 
 error:
-       /* That's how we indicate that an error occured */
+       ARGPAR_ASSERT(parse_ret.error);
+
+       /* That's how we indicate that an error occurred */
        destroy_item_array(parse_ret.items);
        parse_ret.items = NULL;
 
 end:
+       argpar_iter_destroy(iter);
+       argpar_item_destroy(item);
        return parse_ret;
 }
 
index 00334cd6fb9d6f0c14a7fc08f5a21d114e778e65..45dd84ed9d493d0bfb5208bfada2a7ed25750938 100644 (file)
@@ -1,7 +1,8 @@
 /*
  * SPDX-License-Identifier: MIT
  *
- * Copyright 2019 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2019-2021 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2020-2021 Simon Marchi <simon.marchi@efficios.com>
  */
 
 #ifndef BABELTRACE_ARGPAR_H
 
 #include <stdbool.h>
 
+/*
+ * argpar is a library which provides facilities for command-line
+ * argument parsing.
+ *
+ * Two APIs are available:
+ *
+ * Iterator API:
+ *     Create a parsing iterator with argpar_iter_create(), then
+ *     repeatedly call argpar_iter_parse_next() to access the parsing
+ *     results, until one of:
+ *
+ *     * There are no more arguments.
+ *
+ *     * The argument parser encounters an error (for example, an
+ *       unknown option).
+ *
+ *     * You need to stop.
+ *
+ *     This API provides more parsing control than the next one.
+ *
+ * Single call API:
+ *     Call argpar_parse(), which parses the arguments until one of:
+ *
+ *     * There are no more arguments.
+ *
+ *     * It encounters an argument parsing error.
+ *
+ *     argpar_parse() returns a single array of parsing results.
+ *
+ * Both methods parse the arguments `argv` of which the count is `argc`
+ * using the sentinel-terminated (use `ARGPAR_OPT_DESCR_SENTINEL`)
+ * option descriptor array `descrs`.
+ *
+ * argpar considers ALL the elements of `argv`, including the first one,
+ * so that you would typically pass `argc - 1` and `&argv[1]` from what
+ * main() receives.
+ *
+ * The argpar parsers support:
+ *
+ * * Short options without an argument, possibly tied together:
+ *
+ *       -f -auf -n
+ *
+ * * Short options with argument:
+ *
+ *       -b 45 -f/mein/file -xyzhello
+ *
+ * * Long options without an argument:
+ *
+ *       --five-guys --burger-king --pizza-hut --subway
+ *
+ * * Long options with arguments:
+ *
+ *       --security enable --time=18.56
+ *
+ * * Non-option arguments (anything else).
+ *
+ * The argpar parsers don't accept `-` or `--` as arguments. The latter
+ * means "end of options" for many command-line tools, but this library
+ * is all about keeping the order of the arguments, so it doesn't mean
+ * much to put them at the end. This has the side effect that a
+ * non-option argument cannot have the form of an option, for example if
+ * you need to pass the exact relative path `--component`. In that case,
+ * you would need to pass `./--component`. There's no generic way to
+ * escape `-` as of this version.
+ *
+ * Both argpar_iter_create() and argpar_parse() accept duplicate options
+ * (they produce one item for each instance).
+ *
+ * A returned parsing item has the type `struct argpar_item *`. Each
+ * item is to be casted to the appropriate type
+ * (`struct argpar_item_opt *` or `struct argpar_item_non_opt *`)
+ * depending on its `type` member.
+ *
+ * Both argpar_iter_create() and argpar_parse() produce the items in
+ * the same order that the arguments were parsed, including non-option
+ * arguments. This means, for example, that for:
+ *
+ *     --hello --count=23 /path/to/file -ab --type file magie
+ *
+ * The produced items are, in this order:
+ *
+ * 1. Option item (`--hello`).
+ * 2. Option item (`--count` with argument `23`).
+ * 3. Non-option item (`/path/to/file`).
+ * 4. Option item (`-a`).
+ * 5. Option item (`-b`).
+ * 6. Option item (`--type` with argument `file`).
+ * 7. Non-option item (`magie`).
+ */
+
 /* Sentinel for an option descriptor array */
 #define ARGPAR_OPT_DESCR_SENTINEL      { -1, '\0', NULL, false }
 
 /*
- * ARGPAR_HIDDEN: if argpar is used in some shared library, we don't want them
- * to be exported by that library, so mark them as "hidden".
+ * ARGPAR_HIDDEN: if argpar is used in some shared library, we don't
+ * want them to be exported by that library, so mark them as "hidden".
  *
- * On Windows, symbols are local unless explicitly exported,
- * see https://gcc.gnu.org/wiki/Visibility
+ * On Windows, symbols are local unless explicitly exported; see
+ * <https://gcc.gnu.org/wiki/Visibility>.
  */
 #if defined(_WIN32) || defined(__CYGWIN__)
 #define ARGPAR_HIDDEN
 #define ARGPAR_HIDDEN __attribute__((visibility("hidden")))
 #endif
 
+/* Forward-declaration for the opaque type */
+struct argpar_iter;
+
 /* Option descriptor */
 struct argpar_opt_descr {
        /* Numeric ID for this option */
@@ -95,7 +190,13 @@ struct argpar_item_array {
 
 /* What is returned by argpar_parse() */
 struct argpar_parse_ret {
-       /* Array of `struct argpar_item *`, or `NULL` on error */
+       /*
+        * Array of `struct argpar_item *`, or `NULL` on error.
+        *
+        * Do NOT destroy those items manually with
+        * argpar_iter_destroy(): call argpar_parse_ret_fini() to
+        * finalize the whole structure.
+        */
        struct argpar_item_array *items;
 
        /* Error string, or `NULL` if none */
@@ -106,63 +207,17 @@ struct argpar_parse_ret {
 };
 
 /*
- * Parses the arguments `argv` of which the count is `argc` using the
- * sentinel-terminated (use `ARGPAR_OPT_DESCR_SENTINEL`) option
- * descriptor array `descrs`.
- *
- * This function considers ALL the elements of `argv`, including the
- * first one, so that you would typically pass `argc - 1` and
- * `&argv[1]` from what main() receives.
+ * Parses arguments in `argv` until the end is reached or an error is
+ * encountered.
  *
- * This argument parser supports:
- *
- * * Short options without an argument, possibly tied together:
- *
- *       -f -auf -n
- *
- * * Short options with argument:
- *
- *       -b 45 -f/mein/file -xyzhello
- *
- * * Long options without an argument:
- *
- *       --five-guys --burger-king --pizza-hut --subway
- *
- * * Long options with arguments:
- *
- *       --security enable --time=18.56
- *
- * * Non-option arguments (anything else).
- *
- * This function does not accept `-` or `--` as arguments. The latter
- * means "end of options" for many command-line tools, but this function
- * is all about keeping the order of the arguments, so it does not mean
- * much to put them at the end. This has the side effect that a
- * non-option argument cannot have the form of an option, for example if
- * you need to pass the exact relative path `--component`. In that case,
- * you would need to pass `./--component`. There's no generic way to
- * escape `-` for the moment.
- *
- * This function accepts duplicate options (the resulting array of items
- * contains one entry for each instance).
- *
- * On success, this function returns an array of items
- * (`struct argpar_item *`). Each item is to be casted to the
- * appropriate type (`struct argpar_item_opt *` or
- * `struct argpar_item_non_opt *`) depending on its type.
- *
- * The returned array contains the items in the same order that the
- * arguments were parsed, including non-option arguments. This means,
- * for example, that for
- *
- *     --hello --meow=23 /path/to/file -b
- *
- * the function returns an array of four items: two options, one
- * non-option, and one option.
+ * On success, this function returns an array of items (field `items` of
+ * `struct argpar_parse_ret`).
  *
  * In the returned structure, `ingested_orig_args` is the number of
  * ingested arguments within `argv` to produce the resulting array of
- * items. If `fail_on_unknown_opt` is true, then on success
+ * items.
+ *
+ * If `fail_on_unknown_opt` is true, then on success
  * `ingested_orig_args` is equal to `argc`. Otherwise,
  * `ingested_orig_args` contains the number of original arguments until
  * an unknown _option_ occurs. For example, with
@@ -193,11 +248,10 @@ struct argpar_parse_ret {
  * for example contains two ingested original arguments, but four
  * resulting items.
  *
- * On failure, the returned structure's `items` member is `NULL`, and
- * the `error` string member contains details about the error.
+ * On failure, the `items` member of the returned structure is `NULL`,
+ * and the `error` string member contains details about the error.
  *
- * You can finalize the returned structure with
- * argpar_parse_ret_fini().
+ * Finalize the returned structure with argpar_parse_ret_fini().
  */
 ARGPAR_HIDDEN
 struct argpar_parse_ret argpar_parse(unsigned int argc,
@@ -206,12 +260,90 @@ struct argpar_parse_ret argpar_parse(unsigned int argc,
                bool fail_on_unknown_opt);
 
 /*
- * Finalizes what is returned by argpar_parse().
+ * Finalizes what argpar_parse() returns.
  *
- * It is safe to call argpar_parse() multiple times with the same
- * structure.
+ * You may call argpar_parse() multiple times with the same structure.
  */
 ARGPAR_HIDDEN
 void argpar_parse_ret_fini(struct argpar_parse_ret *ret);
 
+/*
+ * Creates an argument parsing iterator.
+ *
+ * This function initializes the returned structure, but doesn't
+ * actually start parsing the arguments.
+ *
+ * `*argv` and `*descrs` must NOT change for the lifetime of the
+ * returned iterator (until you call argpar_iter_destroy()).
+ *
+ * Call argpar_iter_parse_next() with the returned iterator to obtain
+ * the next parsing result (item).
+ */
+ARGPAR_HIDDEN
+struct argpar_iter *argpar_iter_create(unsigned int argc,
+               const char * const *argv,
+               const struct argpar_opt_descr *descrs);
+
+/*
+ * Destroys `iter`, as returned by argpar_iter_create().
+ */
+ARGPAR_HIDDEN
+void argpar_iter_destroy(struct argpar_iter *iter);
+
+/*
+ * Return type of argpar_iter_parse_next().
+ */
+enum argpar_iter_parse_next_status {
+       ARGPAR_ITER_PARSE_NEXT_STATUS_OK,
+       ARGPAR_ITER_PARSE_NEXT_STATUS_END,
+       ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT,
+       ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR,
+};
+
+/*
+ * Parses and returns the next item from `iter`.
+ *
+ * On success, this function sets `*item` to an item which describes the
+ * next option or non-option argument and returns
+ * `ARGPAR_ITER_PARSE_NEXT_STATUS_OK`. Destroy `*item` with
+ * argpar_item_destroy().
+ *
+ * If there are no more items to return, this function returns
+ * `ARGPAR_ITER_PARSE_NEXT_STATUS_END`.
+ *
+ * On failure (status codes
+ * `ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT` and
+ * `ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR`), this function sets `*error`
+ * to a descriptive error string. Free `*error` with free().
+ *
+ * Create an argument parsing iterator with argpar_iter_create().
+ */
+enum argpar_iter_parse_next_status argpar_iter_parse_next(
+               struct argpar_iter *iter, const struct argpar_item **item,
+               char **error);
+
+/*
+ * Returns the number of ingested elements from `argv`, as passed to
+ * argpar_iter_create() to create `*iter`, that were required to produce
+ * the previously returned items.
+ */
+ARGPAR_HIDDEN
+unsigned int argpar_iter_get_ingested_orig_args(const struct argpar_iter *iter);
+
+/*
+ * Destroys `item`, as created by argpar_iter_parse_next().
+ */
+ARGPAR_HIDDEN
+void argpar_item_destroy(const struct argpar_item *item);
+
+/*
+ * Destroys `_item` (`const struct argpar_item *`) and sets it to
+ * `NULL`.
+ */
+#define ARGPAR_ITEM_DESTROY_AND_RESET(_item)   \
+       {                                       \
+               argpar_item_destroy(_item);     \
+               _item = NULL;                   \
+       }
+
 #endif /* BABELTRACE_ARGPAR_H */
index f4944eac1e37d1a836e80966ae4118d1e166c45e..2a24567f30f1f7c89132de2dd9f958ce6a6a044c 100644 (file)
@@ -1,5 +1,6 @@
 /*
- * Copyright (c) 2019 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2019-2021 Philippe Proulx <pproulx@efficios.com>
+ * Copyright (c) 2020-2021 Simon Marchi <simon.marchi@efficios.com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include "argpar/argpar.h"
 
 /*
- * Tests that the command line `cmdline`, with non-quoted
- * space-delimited arguments, once parsed given the option descriptors
- * `descrs` and the option `fail_on_unknown_opt`, succeeds and gives the
- * expected command line `expected_cmd_line` and number of ingested
- * original arguments `expected_ingested_orig_args`.
- *
- * The resulting command-line is built from the resulting arguments,
- * space-delimiting each argument, preferring the `--long-opt=arg` style
- * over the `-s arg` style, and using the `arg<A,B>` form for non-option
- * arguments where `A` is the original argument index and `B` is the
- * non-option argument index.
+ * Append `item` to `res_str` to incrementally build an expected command line string.
  */
 static
-void test_succeed(const char *cmdline,
+void append_to_res_str(GString *res_str, const struct argpar_item *item)
+{
+       if (res_str->len > 0) {
+               g_string_append_c(res_str, ' ');
+       }
+
+       switch (item->type) {
+       case ARGPAR_ITEM_TYPE_OPT:
+       {
+               const struct argpar_item_opt *item_opt =
+                       (const void *) item;
+
+               if (item_opt->descr->long_name) {
+                       g_string_append_printf(res_str, "--%s",
+                               item_opt->descr->long_name);
+
+                       if (item_opt->arg) {
+                               g_string_append_printf(res_str, "=%s",
+                                       item_opt->arg);
+                       }
+               } else if (item_opt->descr->short_name) {
+                       g_string_append_printf(res_str, "-%c",
+                               item_opt->descr->short_name);
+
+                       if (item_opt->arg) {
+                               g_string_append_printf(res_str, " %s",
+                                       item_opt->arg);
+                       }
+               }
+
+               break;
+       }
+       case ARGPAR_ITEM_TYPE_NON_OPT:
+       {
+               const struct argpar_item_non_opt *item_non_opt =
+                       (const void *) item;
+
+               g_string_append_printf(res_str, "%s<%u,%u>",
+                       item_non_opt->arg, item_non_opt->orig_index,
+                       item_non_opt->non_opt_index);
+               break;
+       }
+       default:
+               abort();
+       }
+}
+
+/* Test using the all-at-once argpar_parse function. */
+
+static
+void test_succeed_argpar_parse(const char *cmdline,
                const char *expected_cmd_line,
                const struct argpar_opt_descr *descrs,
                unsigned int expected_ingested_orig_args)
@@ -70,69 +111,121 @@ void test_succeed(const char *cmdline,
        }
 
        for (i = 0; i < parse_ret.items->n_items; i++) {
-               const struct argpar_item *arg = parse_ret.items->items[i];
-
-               switch (arg->type) {
-               case ARGPAR_ITEM_TYPE_OPT:
-               {
-                       const struct argpar_item_opt *arg_opt =
-                               (const void *) arg;
-
-                       if (arg_opt->descr->long_name) {
-                               g_string_append_printf(res_str, "--%s",
-                                       arg_opt->descr->long_name);
-
-                               if (arg_opt->arg) {
-                                       g_string_append_printf(res_str, "=%s",
-                                               arg_opt->arg);
-                               }
-
-                               g_string_append_c(res_str, ' ');
-                       } else if (arg_opt->descr->short_name) {
-                               g_string_append_printf(res_str, "-%c",
-                                       arg_opt->descr->short_name);
-
-                               if (arg_opt->arg) {
-                                       g_string_append_printf(res_str, " %s",
-                                               arg_opt->arg);
-                               }
-
-                               g_string_append_c(res_str, ' ');
-                       }
+               const struct argpar_item *item = parse_ret.items->items[i];
 
-                       break;
+               append_to_res_str(res_str, item);
+       }
+
+       ok(strcmp(expected_cmd_line, res_str->str) == 0,
+               "argpar_parse() returns the expected parsed arguments "
+               "for command line `%s`", cmdline);
+       if (strcmp(expected_cmd_line, res_str->str) != 0) {
+               diag("Expected: `%s`", expected_cmd_line);
+               diag("Got:      `%s`", res_str->str);
+       }
+
+end:
+       argpar_parse_ret_fini(&parse_ret);
+       g_string_free(res_str, TRUE);
+       g_strfreev(argv);
+}
+
+/* Test using the iterator API. */
+
+static
+void test_succeed_argpar_iter(const char *cmdline,
+               const char *expected_cmd_line,
+               const struct argpar_opt_descr *descrs,
+               unsigned int expected_ingested_orig_args)
+{
+       struct argpar_iter *iter = NULL;
+       const struct argpar_item *item = NULL;
+       char *error = NULL;
+       GString *res_str = g_string_new(NULL);
+       gchar **argv = g_strsplit(cmdline, " ", 0);
+       unsigned int i, actual_ingested_orig_args;
+
+       assert(argv);
+       assert(res_str);
+
+       iter = argpar_iter_create(g_strv_length(argv),
+               (const char * const *) argv, descrs);
+       assert(iter);
+
+       for (i = 0; ; i++) {
+               enum argpar_iter_parse_next_status status;
+
+               ARGPAR_ITEM_DESTROY_AND_RESET(item);
+               status = argpar_iter_parse_next(iter, &item, &error);
+
+               ok(status == ARGPAR_ITER_PARSE_NEXT_STATUS_OK ||
+                       status == ARGPAR_ITER_PARSE_NEXT_STATUS_END ||
+                       status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT,
+                       "argpar_iter_parse_next() for command line `%s` call #%u: status", cmdline, i);
+
+               if (status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT) {
+                       ok(error, "argpar_iter_parse_next() for command line `%s` call #%u: error returned", cmdline, i);
+               } else {
+                       ok(!error, "argpar_iter_parse_next() for command line `%s` call #%u: no error returned", cmdline, i);
                }
-               case ARGPAR_ITEM_TYPE_NON_OPT:
-               {
-                       const struct argpar_item_non_opt *arg_non_opt =
-                               (const void *) arg;
-
-                       g_string_append_printf(res_str, "%s<%u,%u> ",
-                               arg_non_opt->arg, arg_non_opt->orig_index,
-                               arg_non_opt->non_opt_index);
+
+               if (status == ARGPAR_ITER_PARSE_NEXT_STATUS_END ||
+                               status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT) {
+                       ok(!item, "argpar_iter_parse_next() for command line `%s` call #%u: no item returned", cmdline, i);
                        break;
                }
-               default:
-                       abort();
-               }
+
+               append_to_res_str(res_str, item);
        }
 
-       if (res_str->len > 0) {
-               g_string_truncate(res_str, res_str->len - 1);
+       actual_ingested_orig_args = argpar_iter_get_ingested_orig_args(iter);
+       ok(actual_ingested_orig_args == expected_ingested_orig_args,
+               "argpar_iter_get_ingested_orig_args() returns the correct number of ingested "
+               "original arguments for command line `%s`", cmdline);
+       if (actual_ingested_orig_args != expected_ingested_orig_args) {
+               diag("Expected: %u    Got: %u", expected_ingested_orig_args,
+                       actual_ingested_orig_args);
        }
 
        ok(strcmp(expected_cmd_line, res_str->str) == 0,
-               "argpar_parse() returns the expected parsed arguments "
+               "argpar_iter_parse_next() returns the expected parsed arguments "
                "for command line `%s`", cmdline);
        if (strcmp(expected_cmd_line, res_str->str) != 0) {
                diag("Expected: `%s`", expected_cmd_line);
                diag("Got:      `%s`", res_str->str);
        }
 
-end:
-       argpar_parse_ret_fini(&parse_ret);
+       argpar_item_destroy(item);
+       argpar_iter_destroy(iter);
        g_string_free(res_str, TRUE);
        g_strfreev(argv);
+       free(error);
+}
+
+/*
+ * Tests that the command line `cmdline`, with non-quoted
+ * space-delimited arguments, once parsed given the option descriptors
+ * `descrs`, succeeds and gives the expected command line `expected_cmd_line`
+ * and number of ingested original arguments `expected_ingested_orig_args`.
+ *
+ * The resulting command-line is built from the resulting arguments,
+ * space-delimiting each argument, preferring the `--long-opt=arg` style
+ * over the `-s arg` style, and using the `arg<A,B>` form for non-option
+ * arguments where `A` is the original argument index and `B` is the
+ * non-option argument index.
+ *
+ * Test with both the parse-all-at-once and iterator-style APIs.
+ */
+static
+void test_succeed(const char *cmdline,
+               const char *expected_cmd_line,
+               const struct argpar_opt_descr *descrs,
+               unsigned int expected_ingested_orig_args)
+{
+       test_succeed_argpar_parse(cmdline, expected_cmd_line, descrs,
+               expected_ingested_orig_args);
+       test_succeed_argpar_iter(cmdline, expected_cmd_line, descrs,
+               expected_ingested_orig_args);
 }
 
 static
@@ -493,13 +586,10 @@ void succeed_tests(void)
        }
 }
 
-/*
- * Tests that the command line `cmdline`, with non-quoted
- * space-delimited arguments, once parsed given the option descriptors
- * `descrs`, fails and gives the expected error `expected_error`.
- */
+/* Test using the all-at-once argpar_parse function. */
+
 static
-void test_fail(const char *cmdline, const char *expected_error,
+void test_fail_argpar_parse(const char *cmdline, const char *expected_error,
                const struct argpar_opt_descr *descrs)
 {
        struct argpar_parse_ret parse_ret;
@@ -530,6 +620,73 @@ end:
        g_strfreev(argv);
 }
 
+/* Test using the iterator API. */
+
+static
+void test_fail_argpar_iter(const char *cmdline, const char *expected_error,
+               const struct argpar_opt_descr *descrs)
+{
+       struct argpar_iter *iter = NULL;
+       const struct argpar_item *item = NULL;
+       gchar **argv = g_strsplit(cmdline, " ", 0);
+       unsigned int i;
+       char *error = NULL;
+
+       iter = argpar_iter_create(g_strv_length(argv),
+               (const char * const *) argv, descrs);
+       assert(iter);
+
+       for (i = 0; ; i++) {
+               enum argpar_iter_parse_next_status status;
+
+               ARGPAR_ITEM_DESTROY_AND_RESET(item);
+               status = argpar_iter_parse_next(iter, &item, &error);
+
+               ok(status == ARGPAR_ITER_PARSE_NEXT_STATUS_OK ||
+                       status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR ||
+                       status == ARGPAR_ITER_PARSE_NEXT_STATUS_ERROR_UNKNOWN_OPT,
+                       "argpar_iter_parse_next() for command line `%s` call #%u: status", cmdline, i);
+
+               if (status != ARGPAR_ITER_PARSE_NEXT_STATUS_OK) {
+                       ok(!item, "argpar_iter_parse_next() for command line `%s` call #%u: no item returned", cmdline, i);
+                       ok(error, "argpar_iter_parse_next() for command line `%s` call #%u: error returned", cmdline, i);
+                       break;
+               }
+
+               ok(item, "argpar_iter_parse_next() for command line `%s` call #%u: item returned", cmdline, i);
+               ok(!error, "argpar_iter_parse_next() for command line `%s` call #%u: no error returned", cmdline, i);
+       }
+
+       ok(strcmp(expected_error, error) == 0,
+               "argpar_iter_parse_next() writes the expected error string "
+               "for command line `%s`", cmdline);
+       if (strcmp(expected_error, error) != 0) {
+               diag("Expected: `%s`", expected_error);
+               diag("Got:      `%s`", error);
+       }
+
+       argpar_item_destroy(item);
+       argpar_iter_destroy(iter);
+       free(error);
+       g_strfreev(argv);
+}
+
+/*
+ * Tests that the command line `cmdline`, with non-quoted
+ * space-delimited arguments, once parsed given the option descriptors
+ * `descrs`, fails and gives the expected error `expected_error`.
+ *
+ * Test with both the parse-all-at-once and iterator-style APIs.
+ */
+
+static
+void test_fail(const char *cmdline, const char *expected_error,
+               const struct argpar_opt_descr *descrs)
+{
+       test_fail_argpar_parse(cmdline, expected_error, descrs);
+       test_fail_argpar_iter(cmdline, expected_error, descrs);
+}
+
 static
 void fail_tests(void)
 {
@@ -645,7 +802,7 @@ void fail_tests(void)
 
 int main(void)
 {
-       plan_tests(132);
+       plan_tests(419);
        succeed_tests();
        fail_tests();
        return exit_status();
This page took 0.060402 seconds and 4 git commands to generate.