diff --git a/lib/common/cmdline.c b/lib/common/cmdline.c index 9c1b810998..1ca6147583 100644 --- a/lib/common/cmdline.c +++ b/lib/common/cmdline.c @@ -1,291 +1,299 @@ /* * Copyright 2019-2021 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include static gboolean bump_verbosity(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; common_args->verbosity++; return TRUE; } pcmk__common_args_t * pcmk__new_common_args(const char *summary) { pcmk__common_args_t *args = NULL; args = calloc(1, sizeof(pcmk__common_args_t)); if (args == NULL) { crm_exit(crm_errno2exit(-ENOMEM)); } args->summary = strdup(summary); if (args->summary == NULL) { crm_exit(crm_errno2exit(-ENOMEM)); } return args; } static void free_common_args(gpointer data) { pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; free(common_args->summary); free(common_args->output_ty); free(common_args->output_dest); if (common_args->output_as_descr != NULL) { free(common_args->output_as_descr); } free(common_args); } GOptionContext * pcmk__build_arg_context(pcmk__common_args_t *common_args, const char *fmts, GOptionGroup **output_group, const char *param_string) { char *desc = crm_strdup_printf("Report bugs to %s\n", PACKAGE_BUGREPORT); GOptionContext *context; GOptionGroup *main_group; GOptionEntry main_entries[3] = { { "version", '$', 0, G_OPTION_ARG_NONE, &(common_args->version), "Display software version and exit", NULL }, { "verbose", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, bump_verbosity, "Increase debug output (may be specified multiple times)", NULL }, { NULL } }; main_group = g_option_group_new(NULL, "Application Options:", NULL, common_args, free_common_args); g_option_group_add_entries(main_group, main_entries); context = g_option_context_new(param_string); g_option_context_set_summary(context, common_args->summary); g_option_context_set_description(context, desc); g_option_context_set_main_group(context, main_group); if (fmts != NULL) { GOptionEntry output_entries[3] = { { "output-as", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_ty), NULL, "FORMAT" }, { "output-to", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_dest), "Specify file name for output (or \"-\" for stdout)", "DEST" }, { NULL } }; if (*output_group == NULL) { *output_group = g_option_group_new("output", "Output Options:", "Show output help", NULL, NULL); } common_args->output_as_descr = crm_strdup_printf("Specify output format as one of: %s", fmts); output_entries[0].description = common_args->output_as_descr; g_option_group_add_entries(*output_group, output_entries); g_option_context_add_group(context, *output_group); } free(desc); // main_group is now owned by context, we don't free it here // cppcheck-suppress memleak return context; } void pcmk__free_arg_context(GOptionContext *context) { if (context == NULL) { return; } g_option_context_free(context); } void pcmk__add_main_args(GOptionContext *context, GOptionEntry entries[]) { GOptionGroup *main_group = g_option_context_get_main_group(context); g_option_group_add_entries(main_group, entries); } void pcmk__add_arg_group(GOptionContext *context, const char *name, const char *header, const char *desc, GOptionEntry entries[]) { GOptionGroup *group = NULL; group = g_option_group_new(name, header, desc, NULL, NULL); g_option_group_add_entries(group, entries); g_option_context_add_group(context, group); // group is now owned by context, we don't free it here // cppcheck-suppress memleak } gchar ** pcmk__cmdline_preproc(char **argv, const char *special) { GPtrArray *arr = NULL; bool saw_dash_dash = false; + bool copy_option = false; if (argv == NULL) { return NULL; } if (g_get_prgname() == NULL && argv && *argv) { gchar *basename = g_path_get_basename(*argv); g_set_prgname(basename); g_free(basename); } arr = g_ptr_array_new(); for (int i = 0; argv[i] != NULL; i++) { /* If this is the first time we saw "--" in the command line, set * a flag so we know to just copy everything after it over. We also * want to copy the "--" over so whatever actually parses the command * line when we're done knows where arguments end. */ if (saw_dash_dash == false && strcmp(argv[i], "--") == 0) { saw_dash_dash = true; } if (saw_dash_dash == true) { g_ptr_array_add(arr, g_strdup(argv[i])); continue; } + if (copy_option == true) { + g_ptr_array_add(arr, g_strdup(argv[i])); + copy_option = false; + continue; + } + /* This is just a dash by itself. That could indicate stdin/stdout, or * it could be user error. Copy it over and let glib figure it out. */ if (pcmk__str_eq(argv[i], "-", pcmk__str_casei)) { g_ptr_array_add(arr, g_strdup(argv[i])); continue; } /* This is a short argument, or perhaps several. Iterate over it * and explode them out into individual arguments. */ if (g_str_has_prefix(argv[i], "-") && !g_str_has_prefix(argv[i], "--")) { /* Skip over leading dash */ char *ch = argv[i]+1; /* This looks like the start of a number, which means it is a negative * number. It's probably the argument to the preceeding option, but * we can't know that here. Copy it over and let whatever handles * arguments next figure it out. */ if (*ch != '\0' && *ch >= '1' && *ch <= '9') { bool is_numeric = true; while (*ch != '\0') { if (!isdigit(*ch)) { is_numeric = false; break; } ch++; } if (is_numeric) { g_ptr_array_add(arr, g_strdup_printf("%s", argv[i])); continue; } else { /* This argument wasn't entirely numeric. Reset ch to the * beginning so we can process it one character at a time. */ ch = argv[i]+1; } } while (*ch != '\0') { /* This is a special short argument that takes an option. getopt * allows values to be interspersed with a list of arguments, but * glib does not. Grab both the argument and its value and * separate them into a new argument. */ if (special != NULL && strchr(special, *ch) != NULL) { /* The argument does not occur at the end of this string of * arguments. Take everything through the end as its value. */ if (*(ch+1) != '\0') { g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); g_ptr_array_add(arr, g_strdup(ch+1)); break; /* The argument occurs at the end of this string. Hopefully * whatever comes next in argv is its value. It may not be, * but that is not for us to decide. */ } else { g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + copy_option = true; ch++; } /* This is a regular short argument. Just copy it over. */ } else { g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); ch++; } } /* This is a long argument, or an option, or something else. * Copy it over - everything else is copied, so this keeps it easy for * the caller to know what to do with the memory when it's done. */ } else { g_ptr_array_add(arr, g_strdup(argv[i])); } } g_ptr_array_add(arr, NULL); return (char **) g_ptr_array_free(arr, FALSE); } G_GNUC_PRINTF(3, 4) gboolean pcmk__force_args(GOptionContext *context, GError **error, const char *format, ...) { int len = 0; char *buf = NULL; gchar **extra_args = NULL; va_list ap; gboolean retval = TRUE; va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len > 0); va_end(ap); if (!g_shell_parse_argv(buf, NULL, &extra_args, error)) { g_strfreev(extra_args); free(buf); return FALSE; } retval = g_option_context_parse_strv(context, &extra_args, error); g_strfreev(extra_args); free(buf); return retval; } diff --git a/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c index 9a752efdf0..edc564008c 100644 --- a/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c +++ b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c @@ -1,124 +1,157 @@ /* * Copyright 2020-2021 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #define LISTS_EQ(a, b) { \ g_assert_cmpint(g_strv_length((gchar **) (a)), ==, g_strv_length((gchar **) (b))); \ for (int i = 0; i < g_strv_length((a)); i++) { \ g_assert_cmpstr((a)[i], ==, (b)[i]); \ } \ } static void empty_input(void) { g_assert_null(pcmk__cmdline_preproc(NULL, "")); } static void no_specials(void) { const char *argv[] = { "-a", "-b", "-c", "-d", NULL }; const gchar *expected[] = { "-a", "-b", "-c", "-d", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); LISTS_EQ(processed, expected); g_strfreev(processed); processed = pcmk__cmdline_preproc((char **) argv, ""); LISTS_EQ(processed, expected); g_strfreev(processed); } static void single_dash(void) { const char *argv[] = { "-", NULL }; const gchar *expected[] = { "-", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); LISTS_EQ(processed, expected); g_strfreev(processed); } static void double_dash(void) { const char *argv[] = { "-a", "--", "-bc", NULL }; const gchar *expected[] = { "-a", "--", "-bc", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); LISTS_EQ(processed, expected); g_strfreev(processed); } static void special_args(void) { const char *argv[] = { "-aX", "-Fval", NULL }; const gchar *expected[] = { "-a", "X", "-F", "val", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, "aF"); LISTS_EQ(processed, expected); g_strfreev(processed); } static void special_arg_at_end(void) { const char *argv[] = { "-a", NULL }; const gchar *expected[] = { "-a", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, "a"); LISTS_EQ(processed, expected); g_strfreev(processed); } static void long_arg(void) { const char *argv[] = { "--blah=foo", NULL }; const gchar *expected[] = { "--blah=foo", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); LISTS_EQ(processed, expected); g_strfreev(processed); } static void negative_score(void) { const char *argv[] = { "-v", "-1000", NULL }; const gchar *expected[] = { "-v", "-1000", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); LISTS_EQ(processed, expected); g_strfreev(processed); } static void negative_score_2(void) { const char *argv[] = { "-1i3", NULL }; const gchar *expected[] = { "-1", "-i", "-3", NULL }; gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); LISTS_EQ(processed, expected); g_strfreev(processed); } +static void +string_arg_with_dash(void) { + const char *argv[] = { "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + const gchar *expected[] = { "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_2(void) { + const char *argv[] = { "-n", "crm_mon_options", "-v", "-1i3", NULL }; + const gchar *expected[] = { "-n", "crm_mon_options", "-v", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_3(void) { + const char *argv[] = { "-abc", "-1i3", NULL }; + const gchar *expected[] = { "-a", "-b", "-c", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "c"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/common/cmdline/preproc/empty_input", empty_input); g_test_add_func("/common/cmdline/preproc/no_specials", no_specials); g_test_add_func("/common/cmdline/preproc/single_dash", single_dash); g_test_add_func("/common/cmdline/preproc/double_dash", double_dash); g_test_add_func("/common/cmdline/preproc/special_args", special_args); g_test_add_func("/common/cmdline/preproc/special_arg_at_end", special_arg_at_end); g_test_add_func("/common/cmdline/preproc/long_arg", long_arg); g_test_add_func("/common/cmdline/preproc/negative_score", negative_score); g_test_add_func("/common/cmdline/preproc/negative_score_2", negative_score_2); + g_test_add_func("/common/cmdline/preproc/string_arg_with_dash", string_arg_with_dash); + g_test_add_func("/common/cmdline/preproc/string_arg_with_dash_2", string_arg_with_dash_2); + g_test_add_func("/common/cmdline/preproc/string_arg_with_dash_3", string_arg_with_dash_3); return g_test_run(); }