diff --git a/include/crm/common/cmdline_internal.h b/include/crm/common/cmdline_internal.h index 069bbb45da..d60ba7fca1 100644 --- a/include/crm/common/cmdline_internal.h +++ b/include/crm/common/cmdline_internal.h @@ -1,190 +1,168 @@ /* - * Copyright 2019-2024 the Pacemaker project contributors + * Copyright 2019-2025 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. */ #ifndef PCMK__CRM_COMMON_CMDLINE_INTERNAL__H #define PCMK__CRM_COMMON_CMDLINE_INTERNAL__H #include #ifdef __cplusplus extern "C" { #endif typedef struct { char *summary; char *output_as_descr; gboolean version; gboolean quiet; unsigned int verbosity; char *output_ty; char *output_dest; } pcmk__common_args_t; /*! * \internal * \brief Allocate a new common args object * * \param[in] summary Summary description of tool for man page * * \return Newly allocated common args object * \note This function will immediately exit the program if memory allocation * fails, since the intent is to call it at the very beginning of a * program, before logging has been set up. */ pcmk__common_args_t * pcmk__new_common_args(const char *summary); /*! * \internal * \brief Create and return a GOptionContext containing the command line options * supported by all tools. * * \note Formatted output options will be added unless fmts is NULL. This allows * for using this function in tools that have not yet been converted to * formatted output. It should not be NULL in any tool that calls * pcmk__register_formats() as that function adds its own command line * options. * * \param[in,out] common_args A ::pcmk__common_args_t structure where the * results of handling command options will be written. * \param[in] fmts The help string for which formats are supported. * \param[in,out] output_group A ::GOptionGroup that formatted output related * command line arguments should be added to. * \param[in] param_string A string describing any remaining command line * arguments. */ GOptionContext * pcmk__build_arg_context(pcmk__common_args_t *common_args, const char *fmts, GOptionGroup **output_group, const char *param_string); /*! * \internal * \brief Clean up after pcmk__build_arg_context(). This should be called * instead of ::g_option_context_free at program termination. * * \param[in,out] context Argument context to free */ void pcmk__free_arg_context(GOptionContext *context); /*! * \internal * \brief Add options to the main application options * * \param[in,out] context Argument context to add options to * \param[in] entries Option entries to add * * \note This is simply a convenience wrapper to reduce duplication */ void pcmk__add_main_args(GOptionContext *context, const GOptionEntry entries[]); /*! * \internal * \brief Add an option group to an argument context * * \param[in,out] context Argument context to add group to * \param[in] name Option group name (to be used in --help-NAME) * \param[in] header Header for --help-NAME output * \param[in] desc Short description for --help-NAME option * \param[in] entries Array of options in group * * \note This is simply a convenience wrapper to reduce duplication */ void pcmk__add_arg_group(GOptionContext *context, const char *name, const char *header, const char *desc, const GOptionEntry entries[]); /*! * \internal * \brief Prepare the command line for being added to a pcmk__output_t as the * request * * This performs various transformations on the command line arguments, such * as surrounding arguments containing spaces with quotes and escaping any * single quotes in the string. * * \param[in,out] argv Command line (typically from pcmk__cmdline_preproc()) */ gchar *pcmk__quote_cmdline(gchar **argv); /*! * \internal * \brief Pre-process command line arguments to preserve compatibility with * getopt behavior. * * getopt and glib have slightly different behavior when it comes to processing * single command line arguments. getopt allows this: -x, while glib will * try to handle like it is additional single letter arguments. glib * prefers -x instead. * * This function scans argv, looking for any single letter command line options * (indicated by the 'special' parameter). When one is found, everything after * that argument to the next whitespace is converted into its own value. Single * letter command line options can come in a group after a single dash, but * this function will expand each group into many arguments. * * Long options and anything after "--" is preserved. The result of this function * can then be passed to ::g_option_context_parse_strv for actual processing. * * In pseudocode, this: * * pcmk__cmdline_preproc(4, ["-XbA", "--blah=foo", "-aF", "-Fval", "--", "--extra", "-args"], "aF") * * Would be turned into this: * * ["-X", "-b", "-A", "--blah=foo", "-a", "F", "-F", "val", "--", "--extra", "-args"] * * This function does not modify argv, and the return value is built of copies * of all the command line arguments. It is up to the caller to free this memory * after use. * * \note This function calls g_set_prgname assuming it wasn't previously set and * assuming argv is not NULL. It is not safe to call g_set_prgname more * than once so clients should not do so after calling this function. * * \param[in] argv The command line arguments. * \param[in] special Single-letter command line arguments that take a value. * These letters will all have pre-processing applied. * * \note @COMPAT We should drop this at a new major or minor release series * after we no longer support building with Booth <1.2, which invokes * Pacemaker CLI tools using the getopt syntax. */ gchar ** pcmk__cmdline_preproc(char *const *argv, const char *special); -/*! - * \internal - * \brief Process extra arguments as if they were provided by the user on the - * command line. - * - * \param[in,out] context The command line option processing context. - * \param[out] error A place for errors to be collected. - * \param[in] format The command line to be processed, potentially with - * format specifiers. - * \param[in] ... Arguments to be formatted. - * - * \note The first item in the list of arguments must be the name of the - * program, exactly as if the format string were coming from the - * command line. Otherwise, the first argument will be ignored. - * - * \return TRUE if processing succeeded, or FALSE otherwise. If FALSE, error - * should be checked and displayed to the user. - */ -G_GNUC_PRINTF(3, 4) -gboolean -pcmk__force_args(GOptionContext *context, GError **error, const char *format, ...); - #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_CMDLINE_INTERNAL__H diff --git a/lib/common/cmdline.c b/lib/common/cmdline.c index 5b6b1654ed..6ecc9cb1e4 100644 --- a/lib/common/cmdline.c +++ b/lib/common/cmdline.c @@ -1,344 +1,317 @@ /* * Copyright 2019-2025 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_EX_OSERR); } // cppcheck-suppress nullPointerOutOfMemory args->summary = strdup(summary); // cppcheck-suppress nullPointerOutOfMemory if (args->summary == NULL) { free(args); args = NULL; crm_exit(CRM_EX_OSERR); } 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) { GOptionContext *context; GOptionGroup *main_group; GOptionEntry main_entries[3] = { { "version", '$', 0, G_OPTION_ARG_NONE, &(common_args->version), N_("Display software version and exit"), NULL }, { "verbose", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, bump_verbosity, N_("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, "Report bugs to " PCMK__BUG_URL "\n"); 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, N_("FORMAT") }, { "output-to", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_dest), N_( "Specify file name for output (or \"-\" for stdout)"), N_("DEST") }, { NULL } }; if (*output_group == NULL) { *output_group = g_option_group_new("output", N_("Output Options:"), N_("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); } // main_group is now owned by context, we don't free it here 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, const 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, const 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 } gchar * pcmk__quote_cmdline(gchar **argv) { GString *cmdline = NULL; if (argv == NULL) { return NULL; } for (int i = 0; argv[i] != NULL; i++) { gint argc = 0; /* Quote the argument if it's unparsable as-is (empty, all whitespace, * or having mismatched quotes), or if it contains more than one token */ if (!g_shell_parse_argv(argv[i], &argc, NULL, NULL) || (argc > 1)) { gchar *quoted = g_shell_quote(argv[i]); pcmk__add_word(&cmdline, 128, quoted); g_free(quoted); } else { pcmk__add_word(&cmdline, 128, argv[i]); } } if (cmdline == NULL) { return NULL; } return g_string_free(cmdline, FALSE); } gchar ** pcmk__cmdline_preproc(char *const *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; } /* "-INFINITY" is almost certainly meant as a string, not as an option * list */ if (strcmp(argv[i], "-INFINITY") == 0) { 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 */ const 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') { fprintf(stderr, "Deprecated argument format '-%c%s' used.\n", *ch, ch+1); fprintf(stderr, "Please use '-%c %s' instead. " "Support will be removed in a future release.\n", *ch, ch+1); 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); - pcmk__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; -}