From fac21c8788e860accfc1f3e2b519d355283b1fb5 Mon Sep 17 00:00:00 2001 From: Philippe Proulx Date: Sat, 6 Jul 2019 23:15:58 -0400 Subject: [PATCH] Add internal command-line argument parser API This patch adds an internal command-line argument parser API. The exact API is well documented in `src/argpar/argpar.h`. The features that I'm looking for are: * C API. * Support for ordered arguments in the results, even between options and non-option arguments (sometimes called positional arguments). * Support for GNU-style arguments, including "glued" short options and long options with `=` to set the option's argument. * Portable. * Compatible license. * No global variables (we're trying to avoid that programming style throughout the project). * Easy to use. None of the popular libraries I looked at, including popt of course, met all those specifications. The goal of this is: 1. To simplify the parsing of general options (before the command name) in bt_config_cli_args_create(). It is currently hard-coded. This is not possible with popt because it would fail with an unknown option, and you would not know the position of the command name's argument within the array. This is possible with g_option_context_parse(). 2. To make it possible, in the `convert` command, to assign parameters and other position-specific options to a non-option argument, for example: babeltrace2 /path/to/trace --params=some=param \ mein-other-trace --name=travel This is not possible with popt as it collects all the non-option arguments as an array of leftover arguments. This is also not possible with g_option_context_parse() for the same reasons as with popt. getopt_long() could satisfy both 1. and 2., but it's somewhat a pain to use and to maintain the available options, as you need to specify the long options in a table and the equivalent short options as a single string with a special encoding (the `convert` command's option string would be particularily ugly). Also: getopt_long() plays a lot with global variables; it's not thread-safe: the parser's state is global. Also: the upstream getopt_long()'s (glibc) license is LGPL, which is more restrictive than our MIT license, so I think we want to avoid that. I believe having our own (tested) CLI argument parser is beneficial, especially in the long term: we can drop a direct dependency (and popt is getting old), not introduce a new one, it's about 450 lines of documented C code, and if we ever need something very specific in the argument parsing strategy in the future, we can add it directly. As a reference, FFmpeg, a project which has complex argument parsing, similar to Babeltrace's `convert` and `run` commands, has its own argument parser (see parse_options() in [1]). Only the CLI will use the bt_argpar_*() API, but to make the unit tests simpler, I put it in its own convenience library. [1]: https://github.com/FFmpeg/FFmpeg/blob/b7b6ddd59693008c35b3247496ecc946331d0856/fftools/cmdutils.c Signed-off-by: Philippe Proulx Change-Id: Iff4fc305b9e9171c694e1e79428bd3838ddd989d Reviewed-on: https://review.lttng.org/c/babeltrace/+/1646 CI-Build: Simon Marchi Tested-by: jenkins --- .gitignore | 1 + configure.ac | 2 + src/Makefile.am | 1 + src/argpar/Makefile.am | 3 + src/argpar/argpar.c | 463 +++++++++++++++++++++++++++ src/argpar/argpar.h | 212 ++++++++++++ tests/Makefile.am | 7 +- tests/argpar/Makefile.am | 9 + tests/argpar/test_argpar.c | 641 +++++++++++++++++++++++++++++++++++++ 9 files changed, 1338 insertions(+), 1 deletion(-) create mode 100644 src/argpar/Makefile.am create mode 100644 src/argpar/argpar.c create mode 100644 src/argpar/argpar.h create mode 100644 tests/argpar/Makefile.am create mode 100644 tests/argpar/test_argpar.c diff --git a/.gitignore b/.gitignore index 12826fe2..2141ebfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /tests/bitfield/test_bitfield +/tests/argpar/test_argpar /tests/ctf-writer/ctf_writer /tests/lib/plugin /tests/lib/test_bt_uuid diff --git a/configure.ac b/configure.ac index 8b6960b2..cf89a9d5 100644 --- a/configure.ac +++ b/configure.ac @@ -693,6 +693,7 @@ AC_CONFIG_FILES([ doc/man/Makefile include/Makefile Makefile + src/argpar/Makefile src/babeltrace2-ctf-writer.pc src/babeltrace2.pc src/bindings/Makefile @@ -738,6 +739,7 @@ AC_CONFIG_FILES([ src/python-plugin-provider/Makefile tests/bitfield/Makefile tests/ctf-writer/Makefile + tests/argpar/Makefile tests/lib/Makefile tests/lib/test-plugin-plugins/Makefile tests/Makefile diff --git a/src/Makefile.am b/src/Makefile.am index 90619a2f..b08827fe 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,6 +1,7 @@ SUBDIRS = \ common \ py-common \ + argpar \ ctfser \ fd-cache \ compat \ diff --git a/src/argpar/Makefile.am b/src/argpar/Makefile.am new file mode 100644 index 00000000..63e97769 --- /dev/null +++ b/src/argpar/Makefile.am @@ -0,0 +1,3 @@ +noinst_LTLIBRARIES = libbabeltrace2-argpar.la + +libbabeltrace2_argpar_la_SOURCES = argpar.c argpar.h diff --git a/src/argpar/argpar.c b/src/argpar/argpar.c new file mode 100644 index 00000000..fe78a008 --- /dev/null +++ b/src/argpar/argpar.c @@ -0,0 +1,463 @@ +/* + * Copyright 2019 Philippe Proulx + * + * 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. + */ + +#include +#include +#include + +#include "common/assert.h" + +#include "argpar.h" + +static +void destroy_item(struct bt_argpar_item * const item) +{ + if (!item) { + goto end; + } + + if (item->type == BT_ARGPAR_ITEM_TYPE_OPT) { + struct bt_argpar_item_opt * const opt_item = (void *) item; + + g_free((void *) opt_item->arg); + } + + g_free(item); + +end: + return; +} + +static +struct bt_argpar_item_opt *create_opt_item( + const struct bt_argpar_opt_descr * const descr, + const char * const arg) +{ + struct bt_argpar_item_opt *opt_item = + g_new0(struct bt_argpar_item_opt, 1); + + if (!opt_item) { + goto end; + } + + opt_item->base.type = BT_ARGPAR_ITEM_TYPE_OPT; + opt_item->descr = descr; + + if (arg) { + opt_item->arg = g_strdup(arg); + if (!opt_item) { + goto error; + } + } + + goto end; + +error: + destroy_item(&opt_item->base); + opt_item = NULL; + +end: + return opt_item; +} + +static +struct bt_argpar_item_non_opt *create_non_opt_item(const char * const arg, + const unsigned int orig_index, + const unsigned int non_opt_index) +{ + struct bt_argpar_item_non_opt * const non_opt_item = + g_new0(struct bt_argpar_item_non_opt, 1); + + if (!non_opt_item) { + goto end; + } + + non_opt_item->base.type = BT_ARGPAR_ITEM_TYPE_NON_OPT; + non_opt_item->arg = arg; + non_opt_item->orig_index = orig_index; + non_opt_item->non_opt_index = non_opt_index; + +end: + return non_opt_item; +} + +static +const struct bt_argpar_opt_descr *find_descr( + const struct bt_argpar_opt_descr * const descrs, + const char short_name, const char * const long_name) +{ + const struct bt_argpar_opt_descr *descr; + + for (descr = descrs; descr->short_name || descr->long_name; descr++) { + if (short_name && descr->short_name && + short_name == descr->short_name) { + goto end; + } + + if (long_name && descr->long_name && + strcmp(long_name, descr->long_name) == 0) { + goto end; + } + } + +end: + return !descr->short_name && !descr->long_name ? NULL : descr; +} + +enum parse_orig_arg_opt_ret { + PARSE_ORIG_ARG_OPT_RET_OK, + PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT = -2, + PARSE_ORIG_ARG_OPT_RET_ERROR = -1, +}; + +static +enum parse_orig_arg_opt_ret parse_short_opts(const char * const short_opts, + const char * const next_orig_arg, + const struct bt_argpar_opt_descr * const descrs, + struct bt_argpar_parse_ret * const parse_ret, + bool * const used_next_orig_arg) +{ + enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK; + const char *short_opt_ch = short_opts; + + if (strlen(short_opts) == 0) { + g_string_append(parse_ret->error, "Invalid argument"); + goto error; + } + + while (*short_opt_ch) { + const char *opt_arg = NULL; + const struct bt_argpar_opt_descr *descr; + struct bt_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; + g_string_append_printf(parse_ret->error, + "Unknown option `-%c`", *short_opt_ch); + goto error; + } + + 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; + } + + /* + * 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)) { + g_string_append_printf(parse_ret->error, + "Missing required argument for option `-%c`", + *short_opt_ch); + *used_next_orig_arg = false; + goto error; + } + } + + /* Create and append option argument */ + opt_item = create_opt_item(descr, opt_arg); + if (!opt_item) { + goto error; + } + + g_ptr_array_add(parse_ret->items, opt_item); + + if (descr->with_arg) { + /* Option has an argument: no more options */ + break; + } + + /* Go to next short option */ + short_opt_ch++; + } + + goto end; + +error: + if (ret == PARSE_ORIG_ARG_OPT_RET_OK) { + ret = PARSE_ORIG_ARG_OPT_RET_ERROR; + } + +end: + return ret; +} + +static +enum parse_orig_arg_opt_ret parse_long_opt(const char * const long_opt_arg, + const char * const next_orig_arg, + const struct bt_argpar_opt_descr * const descrs, + struct bt_argpar_parse_ret * const parse_ret, + bool * const used_next_orig_arg) +{ + const size_t max_len = 127; + enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK; + const struct bt_argpar_opt_descr *descr; + struct bt_argpar_item_opt *opt_item; + + /* Option's argument, if any */ + const char *opt_arg = NULL; + + /* Position of first `=`, if any */ + const char *eq_pos; + + /* Buffer holding option name when `long_opt_arg` contains `=` */ + char buf[max_len + 1]; + + /* Option name */ + const char *long_opt_name = long_opt_arg; + + if (strlen(long_opt_arg) == 0) { + g_string_append(parse_ret->error, "Invalid argument"); + goto error; + } + + /* Find the first `=` in original argument */ + eq_pos = strchr(long_opt_arg, '='); + if (eq_pos) { + const size_t long_opt_name_size = eq_pos - long_opt_arg; + + /* Isolate the option name */ + if (long_opt_name_size > max_len) { + g_string_append_printf(parse_ret->error, + "Invalid argument `--%s`", long_opt_arg); + goto error; + } + + memcpy(buf, long_opt_arg, long_opt_name_size); + buf[long_opt_name_size] = '\0'; + long_opt_name = buf; + } + + /* Find corresponding option descriptor */ + descr = find_descr(descrs, '\0', long_opt_name); + if (!descr) { + g_string_append_printf(parse_ret->error, + "Unknown option `--%s`", long_opt_name); + ret = PARSE_ORIG_ARG_OPT_RET_ERROR_UNKNOWN_OPT; + goto error; + } + + /* Find option's argument if any */ + if (descr->with_arg) { + if (eq_pos) { + /* `--long-opt=arg` style */ + opt_arg = eq_pos + 1; + } else { + /* `--long-opt arg` style */ + if (!next_orig_arg) { + g_string_append_printf(parse_ret->error, + "Missing required argument for option `--%s`", + long_opt_name); + goto error; + } + + 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) { + goto error; + } + + g_ptr_array_add(parse_ret->items, opt_item); + goto end; + +error: + if (ret == PARSE_ORIG_ARG_OPT_RET_OK) { + ret = PARSE_ORIG_ARG_OPT_RET_ERROR; + } + +end: + return ret; +} + +static +enum parse_orig_arg_opt_ret parse_orig_arg_opt(const char * const orig_arg, + const char * const next_orig_arg, + const struct bt_argpar_opt_descr * const descrs, + struct bt_argpar_parse_ret * const parse_ret, + bool * const used_next_orig_arg) +{ + enum parse_orig_arg_opt_ret ret = PARSE_ORIG_ARG_OPT_RET_OK; + + BT_ASSERT(orig_arg[0] == '-'); + + if (orig_arg[1] == '-') { + /* Long option */ + ret = parse_long_opt(&orig_arg[2], + next_orig_arg, descrs, parse_ret, + used_next_orig_arg); + } else { + /* Short option */ + ret = parse_short_opts(&orig_arg[1], + next_orig_arg, descrs, parse_ret, + used_next_orig_arg); + } + + return ret; +} + +static +void prepend_while_parsing_arg_to_error(GString * const error, + const unsigned int i, const char * const arg) +{ + /* 🙁 There's no g_string_prepend_printf()! */ + GString * const tmp_str = g_string_new(NULL); + + BT_ASSERT(error); + BT_ASSERT(arg); + + if (!tmp_str) { + goto end; + } + + g_string_append_printf(tmp_str, "While parsing argument #%u (`%s`): %s", + i + 1, arg, error->str); + g_string_assign(error, tmp_str->str); + g_string_free(tmp_str, TRUE); + +end: + return; +} + +BT_HIDDEN +struct bt_argpar_parse_ret bt_argpar_parse(unsigned int argc, + const char * const *argv, + const struct bt_argpar_opt_descr * const descrs, + bool fail_on_unknown_opt) +{ + struct bt_argpar_parse_ret parse_ret = { 0 }; + unsigned int i; + unsigned int non_opt_index = 0; + + parse_ret.error = g_string_new(NULL); + if (!parse_ret.error) { + goto error; + } + + parse_ret.items = g_ptr_array_new_with_free_func( + (GDestroyNotify) destroy_item); + if (!parse_ret.items) { + goto error; + } + + 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; + + if (orig_arg[0] != '-') { + /* Non-option argument */ + struct bt_argpar_item_non_opt *non_opt_item = + create_non_opt_item(orig_arg, i, non_opt_index); + + if (!non_opt_item) { + goto error; + } + + non_opt_index++; + g_ptr_array_add(parse_ret.items, non_opt_item); + continue; + } + + /* 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: + BT_ASSERT(!used_next_orig_arg); + + if (fail_on_unknown_opt) { + prepend_while_parsing_arg_to_error( + parse_ret.error, i, orig_arg); + goto error; + } + + /* + * The current original argument is not + * considered ingested because it triggered an + * unknown option. + */ + parse_ret.ingested_orig_args = i; + g_string_free(parse_ret.error, TRUE); + 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(); + } + + if (used_next_orig_arg) { + i++; + } + } + + parse_ret.ingested_orig_args = argc; + g_string_free(parse_ret.error, TRUE); + parse_ret.error = NULL; + goto end; + +error: + if (parse_ret.items) { + /* That's how we indicate that an error occured */ + g_ptr_array_free(parse_ret.items, TRUE); + parse_ret.items = NULL; + } + +end: + return parse_ret; +} + +BT_HIDDEN +void bt_argpar_parse_ret_fini(struct bt_argpar_parse_ret *ret) +{ + BT_ASSERT(ret); + + if (ret->items) { + g_ptr_array_free(ret->items, TRUE); + ret->items = NULL; + } + + if (ret->error) { + g_string_free(ret->error, TRUE); + ret->error = NULL; + } +} diff --git a/src/argpar/argpar.h b/src/argpar/argpar.h new file mode 100644 index 00000000..85a663dc --- /dev/null +++ b/src/argpar/argpar.h @@ -0,0 +1,212 @@ +#ifndef BABELTRACE_ARGPAR_H +#define BABELTRACE_ARGPAR_H + +/* + * Copyright 2019 Philippe Proulx + * + * 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. + */ + +#include +#include + +#include "common/macros.h" + +/* Sentinel for an option descriptor array */ +#define BT_ARGPAR_OPT_DESCR_SENTINEL { -1, '\0', NULL, false } + +/* Option descriptor */ +struct bt_argpar_opt_descr { + /* Numeric ID for this option */ + const int id; + + /* Short option character, or `\0` */ + const char short_name; + + /* Long option name (without `--`), or `NULL` */ + const char * const long_name; + + /* True if this option has an argument */ + const bool with_arg; +}; + +/* Item type */ +enum bt_argpar_item_type { + /* Option */ + BT_ARGPAR_ITEM_TYPE_OPT, + + /* Non-option */ + BT_ARGPAR_ITEM_TYPE_NON_OPT, +}; + +/* Base item */ +struct bt_argpar_item { + enum bt_argpar_item_type type; +}; + +/* Option item */ +struct bt_argpar_item_opt { + struct bt_argpar_item base; + + /* Corresponding descriptor */ + const struct bt_argpar_opt_descr *descr; + + /* Argument, or `NULL` if none */ + const char *arg; +}; + +/* Non-option item */ +struct bt_argpar_item_non_opt { + struct bt_argpar_item base; + + /* + * Complete argument, pointing to one of the entries of the + * original arguments (`argv`). + */ + const char *arg; + + /* Index of this argument amongst all original arguments (`argv`) */ + unsigned int orig_index; + + /* Index of this argument amongst other non-option arguments */ + unsigned int non_opt_index; +}; + +/* What is returned by bt_argpar_parse() */ +struct bt_argpar_parse_ret { + /* Array of `struct bt_argpar_item *`, or `NULL` on error */ + GPtrArray *items; + + /* Error string, or `NULL` if none */ + GString *error; + + /* Number of original arguments (`argv`) ingested */ + unsigned int ingested_orig_args; +}; + +/* + * Parses the arguments `argv` of which the count is `argc` using the + * sentinel-terminated (use `BT_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. + * + * 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 bt_argpar_item *`). Each item is to be casted to the + * appropriate type (`struct bt_argpar_item_opt *` or + * `struct bt_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. + * + * 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 + * `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 + * + * --great --white contact nuance --shark nuclear + * + * if `--shark` is not described within `descrs` and + * `fail_on_unknown_opt` is false, then `ingested_orig_args` is 4 (two + * options, two non-options), whereas `argc` is 6. + * + * This makes it possible to know where a command name is, for example. + * With those arguments: + * + * --verbose --stuff=23 do-something --specific-opt -f -b + * + * and the descriptors for `--verbose` and `--stuff` only, the function + * returns the `--verbose` and `--stuff` option items, the + * `do-something` non-option item, and that three original arguments + * were ingested. This means you can start the next argument parsing + * stage, with option descriptors depending on the command name, at + * `&argv[3]`. + * + * Note that `ingested_orig_args` is not always equal to the number of + * returned items, as + * + * --hello -fdw + * + * 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. + * + * You can finalize the returned structure with + * bt_argpar_parse_ret_fini(). + */ +BT_HIDDEN +struct bt_argpar_parse_ret bt_argpar_parse(unsigned int argc, + const char * const *argv, + const struct bt_argpar_opt_descr *descrs, + bool fail_on_unknown_opt); + +/* + * Finalizes what is returned by bt_argpar_parse(). + * + * It is safe to call bt_argpar_parse() multiple times with the same + * structure. + */ +BT_HIDDEN +void bt_argpar_parse_ret_fini(struct bt_argpar_parse_ret *ret); + +#endif /* BABELTRACE_ARGPAR_H */ diff --git a/tests/Makefile.am b/tests/Makefile.am index 1780f486..845b9c44 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = utils lib bitfield ctf-writer plugins +SUBDIRS = utils lib bitfield ctf-writer plugins argpar # Directories added to EXTRA_DIST will be recursively copied to the distribution. EXTRA_DIST = $(srcdir)/data \ @@ -49,6 +49,9 @@ if ENABLE_PYTHON_BINDINGS TESTS_BINDINGS += bindings/python/bt2/test_python_bt2 endif +TESTS_ARGPAR = \ + argpar/test_argpar + TESTS_CLI = \ cli/test_trace_read \ cli/test_packet_seq_num \ @@ -125,6 +128,7 @@ LOG_DRIVER = env AM_TAP_AWK='$(AWK)' \ $(SHELL) $(srcdir)/utils/tap-driver.sh TESTS_NO_BITFIELD = \ + $(TESTS_ARGPAR) \ $(TESTS_BINDINGS) \ $(TESTS_CLI) \ $(TESTS_CTF_WRITER) \ @@ -139,6 +143,7 @@ check-$(1): $(MAKE) $(AM_MAKEFLAGS) TESTS="$2" check endef +$(eval $(call check_target,argpar,$(TESTS_ARGPAR))) $(eval $(call check_target,bindings,$(TESTS_BINDINGS))) $(eval $(call check_target,bitfield,$(TESTS_BITFIELD))) $(eval $(call check_target,cli,$(TESTS_CLI))) diff --git a/tests/argpar/Makefile.am b/tests/argpar/Makefile.am new file mode 100644 index 00000000..2b635216 --- /dev/null +++ b/tests/argpar/Makefile.am @@ -0,0 +1,9 @@ +AM_CPPFLAGS += -I$(top_srcdir)/tests/utils + +noinst_PROGRAMS = test_argpar +test_argpar_SOURCES = test_argpar.c +test_argpar_LDADD = \ + $(top_builddir)/tests/utils/tap/libtap.la \ + $(top_builddir)/src/common/libbabeltrace2-common.la \ + $(top_builddir)/src/logging/libbabeltrace2-logging.la \ + $(top_builddir)/src/argpar/libbabeltrace2-argpar.la diff --git a/tests/argpar/test_argpar.c b/tests/argpar/test_argpar.c new file mode 100644 index 00000000..fa915d87 --- /dev/null +++ b/tests/argpar/test_argpar.c @@ -0,0 +1,641 @@ +/* + * Copyright (c) 2019 Philippe Proulx + * + * 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 + * the Free Software Foundation; under version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include "tap/tap.h" +#include "common/assert.h" + +#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` form for non-option + * arguments where `A` is the original argument index and `B` is the + * non-option argument index. + */ +static +void test_succeed(const char *cmdline, + const char *expected_cmd_line, + const struct bt_argpar_opt_descr *descrs, + unsigned int expected_ingested_orig_args) +{ + struct bt_argpar_parse_ret parse_ret; + GString *res_str = g_string_new(NULL); + gchar **argv = g_strsplit(cmdline, " ", 0); + unsigned int i; + + BT_ASSERT(argv); + BT_ASSERT(res_str); + parse_ret = bt_argpar_parse(g_strv_length(argv), + (const char * const *) argv, descrs, false); + ok(parse_ret.items, + "bt_argpar_parse() succeeds for command line `%s`", cmdline); + ok(!parse_ret.error, + "bt_argpar_parse() does not write an error for command line `%s`", cmdline); + ok(parse_ret.ingested_orig_args == expected_ingested_orig_args, + "bt_argpar_parse() returns the correct number of ingested " + "original arguments for command line `%s`", cmdline); + if (parse_ret.ingested_orig_args != expected_ingested_orig_args) { + diag("Expected: %u Got: %u", expected_ingested_orig_args, + parse_ret.ingested_orig_args); + } + + if (!parse_ret.items) { + fail("bt_argpar_parse() returns the expected parsed arguments " + "for command line `%s`", cmdline); + goto end; + } + + for (i = 0; i < parse_ret.items->len; i++) { + const struct bt_argpar_item *arg = parse_ret.items->pdata[i]; + + switch (arg->type) { + case BT_ARGPAR_ITEM_TYPE_OPT: + { + const struct bt_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, ' '); + } + + break; + } + case BT_ARGPAR_ITEM_TYPE_NON_OPT: + { + const struct bt_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); + break; + } + default: + abort(); + } + } + + if (res_str->len > 0) { + g_string_truncate(res_str, res_str->len - 1); + } + + ok(strcmp(expected_cmd_line, res_str->str) == 0, + "bt_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: + bt_argpar_parse_ret_fini(&parse_ret); + g_string_free(res_str, TRUE); + g_strfreev(argv); +} + +static +void succeed_tests(void) +{ + /* No arguments */ + { + const struct bt_argpar_opt_descr descrs[] = { + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "", + "", + descrs, 0); + } + + /* Single long option */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "salut", false }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--salut", + "--salut", + descrs, 1); + } + + /* Single short option */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'f', NULL, false }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-f", + "-f", + descrs, 1); + } + + /* Short and long option (aliases) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'f', "flaw", false }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-f --flaw", + "--flaw --flaw", + descrs, 2); + } + + /* Long option with argument (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "tooth", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--tooth 67", + "--tooth=67", + descrs, 2); + } + + /* Long option with argument (equal form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "polish", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--polish=brick", + "--polish=brick", + descrs, 1); + } + + /* Short option with argument (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'c', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-c chilly", + "-c chilly", + descrs, 2); + } + + /* Short option with argument (glued form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'c', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-cchilly", + "-c chilly", + descrs, 1); + } + + /* Short and long option (aliases) with argument (all forms) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', "dry", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--dry=rate -dthing --dry street --dry=shape", + "--dry=rate --dry=thing --dry=street --dry=shape", + descrs, 5); + } + + /* Many short options, last one with argument (glued form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', NULL, false }, + { 0, 'e', NULL, false }, + { 0, 'f', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-defmeow", + "-d -e -f meow", + descrs, 1); + } + + /* Many options */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', NULL, false }, + { 0, 'e', "east", true }, + { 0, '\0', "mind", false }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-d --mind -destart --mind --east cough -d --east=itch", + "-d --mind -d --east=start --mind --east=cough -d --east=itch", + descrs, 8); + } + + /* Single non-option argument */ + { + const struct bt_argpar_opt_descr descrs[] = { + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "kilojoule", + "kilojoule<0,0>", + descrs, 1); + } + + /* Two non-option arguments */ + { + const struct bt_argpar_opt_descr descrs[] = { + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "kilojoule mitaine", + "kilojoule<0,0> mitaine<1,1>", + descrs, 2); + } + + /* Single non-option argument mixed with options */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', NULL, false }, + { 0, '\0', "squeeze", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-d sprout yes --squeeze little bag -d", + "-d sprout<1,0> yes<2,1> --squeeze=little bag<5,2> -d", + descrs, 7); + } + + /* Unknown short option (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-d salut -e -d meow", + "-d salut", + descrs, 2); + } + + /* Unknown short option (glued form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'd', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-dsalut -e -d meow", + "-d salut", + descrs, 1); + } + + /* Unknown long option (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "sink", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--sink party --food --sink impulse", + "--sink=party", + descrs, 2); + } + + /* Unknown long option (equal form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "sink", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--sink=party --food --sink=impulse", + "--sink=party", + descrs, 1); + } + + /* Unknown option before non-option argument */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "thumb", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--thumb=party --food bateau --thumb waves", + "--thumb=party", + descrs, 1); + } + + /* Unknown option after non-option argument */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "thumb", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--thumb=party wound --food --thumb waves", + "--thumb=party wound<1,0>", + descrs, 2); + } + + /* Valid `---opt` */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "-fuel", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "---fuel=three", + "---fuel=three", + descrs, 1); + } + + /* Long option containing `=` in argument (equal form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "zebra", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--zebra=three=yes", + "--zebra=three=yes", + descrs, 1); + } + + /* Short option's argument starting with `-` (glued form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'z', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-z-will", + "-z -will", + descrs, 1); + } + + /* Short option's argument starting with `-` (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'z', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-z -will", + "-z -will", + descrs, 2); + } + + /* Long option's argument starting with `-` (space form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "janine", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--janine -sutto", + "--janine=-sutto", + descrs, 2); + } + + /* Long option's argument starting with `-` (equal form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "janine", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "--janine=-sutto", + "--janine=-sutto", + descrs, 1); + } + + /* Long option's empty argument (equal form) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'f', NULL, false }, + { 0, '\0', "yeah", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_succeed( + "-f --yeah= -f", + "-f --yeah= -f", + descrs, 3); + } +} + +/* + * 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`. + */ +static +void test_fail(const char *cmdline, const char *expected_error, + const struct bt_argpar_opt_descr *descrs) +{ + struct bt_argpar_parse_ret parse_ret; + gchar **argv = g_strsplit(cmdline, " ", 0); + + parse_ret = bt_argpar_parse(g_strv_length(argv), + (const char * const *) argv, descrs, true); + ok(!parse_ret.items, + "bt_argpar_parse() fails for command line `%s`", cmdline); + ok(parse_ret.error, + "bt_argpar_parse() writes an error string for command line `%s`", + cmdline); + if (parse_ret.items) { + fail("bt_argpar_parse() writes the expected error string"); + goto end; + } + + ok(strcmp(expected_error, parse_ret.error->str) == 0, + "bt_argpar_parse() writes the expected error string " + "for command line `%s`", cmdline); + if (strcmp(expected_error, parse_ret.error->str) != 0) { + diag("Expected: `%s`", expected_error); + diag("Got: `%s`", parse_ret.error->str); + } + +end: + bt_argpar_parse_ret_fini(&parse_ret); + g_strfreev(argv); +} + +static +void fail_tests(void) +{ + /* Unknown long option */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "thumb", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "--thumb=party --meow", + "While parsing argument #2 (`--meow`): Unknown option `--meow`", + descrs); + } + + /* Unknown short option */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "thumb", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "--thumb=party -x", + "While parsing argument #2 (`-x`): Unknown option `-x`", + descrs); + } + + /* Missing long option argument */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, '\0', "thumb", true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "--thumb", + "While parsing argument #1 (`--thumb`): Missing required argument for option `--thumb`", + descrs); + } + + /* Missing short option argument */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'k', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "-k", + "While parsing argument #1 (`-k`): Missing required argument for option `-k`", + descrs); + } + + /* Missing short option argument (multiple glued) */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'a', NULL, false }, + { 0, 'b', NULL, false }, + { 0, 'c', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "-abc", + "While parsing argument #1 (`-abc`): Missing required argument for option `-c`", + descrs); + } + + /* Invalid `-` */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'a', NULL, false }, + { 0, 'b', NULL, false }, + { 0, 'c', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "-ab - -c", + "While parsing argument #2 (`-`): Invalid argument", + descrs); + } + + /* Invalid `--` */ + { + const struct bt_argpar_opt_descr descrs[] = { + { 0, 'a', NULL, false }, + { 0, 'b', NULL, false }, + { 0, 'c', NULL, true }, + BT_ARGPAR_OPT_DESCR_SENTINEL + }; + + test_fail( + "-ab -- -c", + "While parsing argument #2 (`--`): Invalid argument", + descrs); + } +} + +int main(void) +{ + plan_tests(129); + succeed_tests(); + fail_tests(); + return exit_status(); +} -- 2.34.1