From: Yu Watanabe Date: Sun, 30 Nov 2025 02:10:02 +0000 (+0900) Subject: musl: introduce wrappers for getopt() and getopt_long() X-Git-Tag: v259-rc3~43 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=53f5aa3fd2423b9dac91049a5d4e8df5666cf4b3;p=thirdparty%2Fsystemd.git musl: introduce wrappers for getopt() and getopt_long() musl's getopt_long() behaves something different in handling optional arguments: ``` $ journalctl _PID=1 _COMM=systemd --since 19:19:01 -n all --follow Failed to add match 'all': Invalid argument ``` This introduces getopt_long_fix() that reorders the passed arguments to make getopt_long() provided by musl works as what we expect. Also, musl's getopt() always behaves POSIXLY_CORRECT mode, and stops parsing arguments when a non-option string found. Let's always use getopt_long(). --- diff --git a/src/include/musl/getopt.h b/src/include/musl/getopt.h new file mode 100644 index 00000000000..6aed1dfd262 --- /dev/null +++ b/src/include/musl/getopt.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/* getopt() is provided both in getopt.h and unistd.h. Hence, we need to tentatively undefine it. */ +#undef getopt + +#include_next + +/* musl's getopt() always behaves POSIXLY_CORRECT mode, and stops parsing arguments when a non-option string + * found. Let's always use getopt_long(). */ +int getopt_fix(int argc, char * const *argv, const char *optstring); +#define getopt(argc, argv, optstring) getopt_fix(argc, argv, optstring) + +/* musl's getopt_long() behaves something different in handling optional arguments. + * ======== + * $ journalctl _PID=1 _COMM=systemd --since 19:19:01 -n all --follow + * Failed to add match 'all': Invalid argument + * ======== + * Here, we introduce getopt_long_fix() that reorders the passed arguments to make getopt_long() provided by + * musl works as what we expect. */ +int getopt_long_fix( + int argc, + char * const *argv, + const char *optstring, + const struct option *longopts, + int *longindex); + +#define getopt_long(argc, argv, optstring, longopts, longindex) \ + getopt_long_fix(argc, argv, optstring, longopts, longindex) diff --git a/src/include/musl/unistd.h b/src/include/musl/unistd.h new file mode 100644 index 00000000000..8fbaaf63e37 --- /dev/null +++ b/src/include/musl/unistd.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/* getopt() is provided both in getopt.h and unistd.h. Hence, we need to tentatively undefine it. */ +#undef getopt + +#include_next + +/* musl's getopt() always behaves POSIXLY_CORRECT mode, and stops parsing arguments when a non-option string + * found. Let's always use getopt_long(). */ +int getopt_fix(int argc, char * const *argv, const char *optstring); +#define getopt(argc, argv, optstring) getopt_fix(argc, argv, optstring) diff --git a/src/libc/musl/getopt.c b/src/libc/musl/getopt.c new file mode 100644 index 00000000000..15c3afa49bd --- /dev/null +++ b/src/libc/musl/getopt.c @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +static int first_non_opt = 0, last_non_opt = 0; +static bool non_opt_found = false, dash_dash = false; + +static void shift(char * const *argv, int start, int end) { + char **av = (char**) argv; + char *saved = av[end]; + + for (int i = end; i > start; i--) + av[i] = av[i - 1]; + + av[start] = saved; +} + +static void exchange(int argc, char * const *argv) { + /* input: + * + * first_non_opt last_non_opt optind + * | | | + * v v v + * aaaaa bbbbb ccccc --prev-opt prev-opt-arg ddddd --next-opt + * + * output: + * first_non_opt last_non_opt optind + * | | | + * v v v + * --prev-opt prev-opt-arg aaaaa bbbbb ccccc ddddd --next-opt + */ + + /* First, move previous arguments. */ + int c = optind - 1 - last_non_opt; + if (c > 0) { + for (int i = 0; i < c; i++) + shift(argv, first_non_opt, optind - 1); + first_non_opt += c; + last_non_opt += c; + } + + /* Then, skip entries that do not start with '-'. */ + while (optind < argc && (argv[optind][0] != '-' || argv[optind][1] == '\0')) { + if (!non_opt_found) { + first_non_opt = optind; + non_opt_found = true; + } + last_non_opt = optind; + optind++; + } +} + +int getopt_long_fix( + int argc, + char * const *argv, + const char *optstring, + const struct option *longopts, + int *longindex) { + + int r; + + if (optind == 0 || first_non_opt == 0 || last_non_opt == 0) { + /* initialize musl's internal variables. */ + (void) (getopt_long)(/* argc= */ -1, /* argv= */ NULL, /* optstring= */ NULL, /* longopts= */ NULL, /* longindex= */ NULL); + first_non_opt = last_non_opt = 1; + non_opt_found = dash_dash = false; + } + + if (first_non_opt >= argc || last_non_opt >= argc || optind > argc || dash_dash) + return -1; + + /* Do not shuffle arguments when optstring starts with '+' or '-'. */ + if (!optstring || optstring[0] == '+' || optstring[0] == '-') + return (getopt_long)(argc, argv, optstring, longopts, longindex); + + exchange(argc, argv); + + if (optind < argc && strcmp(argv[optind], "--") == 0) { + if (first_non_opt < optind) + shift(argv, first_non_opt, optind); + first_non_opt++; + optind++; + dash_dash = true; + if (non_opt_found) + optind = first_non_opt; + return -1; + } + + r = (getopt_long)(argc, argv, optstring, longopts, longindex); + if (r < 0 && non_opt_found) + optind = first_non_opt; + + return r; +} + +int getopt_fix(int argc, char * const *argv, const char *optstring) { + return getopt_long_fix(argc, argv, optstring, /* longopts= */ NULL, /* longindex= */ NULL); +} diff --git a/src/libc/musl/meson.build b/src/libc/musl/meson.build index 387f04dac41..d40a2f19115 100644 --- a/src/libc/musl/meson.build +++ b/src/libc/musl/meson.build @@ -5,6 +5,7 @@ if get_option('libc') != 'musl' endif libc_wrapper_sources += files( + 'getopt.c', 'printf.c', 'stdio.c', 'stdlib.c', diff --git a/src/test/meson.build b/src/test/meson.build index 29747009548..2366dbaf1c2 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -111,6 +111,7 @@ simple_tests += files( 'test-format-util.c', 'test-fs-util.c', 'test-fstab-util.c', + 'test-getopt.c', 'test-glob-util.c', 'test-gpt.c', 'test-gunicode.c', diff --git a/src/test/test-getopt.c b/src/test/test-getopt.c new file mode 100644 index 00000000000..e17621af6a9 --- /dev/null +++ b/src/test/test-getopt.c @@ -0,0 +1,516 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "strv.h" +#include "tests.h" + +typedef struct Entry { + int opt; + const char *argument; + const char *nextarg; +} Entry; + +static void test_getopt_long_one( + char **argv, + const char *optstring, + const struct option *longopts, + const Entry *entries, + char **remaining) { + + _cleanup_free_ char *joined = strv_join(argv, ", "); + log_debug("/* %s(%s) */", __func__, joined); + + _cleanup_free_ char *saved_argv0 = NULL; + ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0])); + + int c, argc = strv_length(argv); + size_t i = 0, n_entries = 0; + + for (const Entry *e = entries; e && e->opt != 0; e++) + n_entries++; + + optind = 0; + while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) >= 0) { + if (c < 0x100) + log_debug("%c: %s", c, strna(optarg)); + else + log_debug("0x%x: %s", (unsigned) c, strna(optarg)); + + ASSERT_LT(i, n_entries); + ASSERT_EQ(c, entries[i].opt); + ASSERT_STREQ(optarg, entries[i].argument); + if (entries[i].nextarg) + ASSERT_STREQ(argv[optind], entries[i].nextarg); + i++; + } + + ASSERT_EQ(i, n_entries); + ASSERT_LE(optind, argc); + ASSERT_EQ(argc - optind, (int) strv_length(remaining)); + for (int j = optind; j < argc; j++) + ASSERT_STREQ(argv[j], remaining[j - optind]); + ASSERT_STREQ(argv[0], saved_argv0); +} + +TEST(getopt_long) { + enum { + ARG_VERSION = 0x100, + ARG_REQUIRED, + ARG_OPTIONAL, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version" , no_argument, NULL, ARG_VERSION }, + { "required1", required_argument, NULL, 'r' }, + { "required2", required_argument, NULL, ARG_REQUIRED }, + { "optional1", optional_argument, NULL, 'o' }, + { "optional2", optional_argument, NULL, ARG_OPTIONAL }, + {}, + }; + + test_getopt_long_one(STRV_MAKE("arg0"), + "hr:o::", options, + NULL, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "string3", + "string4"), + "hr:o::", options, + NULL, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "--", + "string1", + "string2", + "string3", + "string4"), + "hr:o::", options, + NULL, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "--", + "string3", + "string4"), + "hr:o::", options, + NULL, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "string3", + "string4", + "--"), + "hr:o::", options, + NULL, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "--help"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "-h"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "--help", + "string1", + "string2", + "string3", + "string4"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "-h", + "string1", + "string2", + "string3", + "string4"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "--help", + "string3", + "string4"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "-h", + "string3", + "string4"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "string3", + "string4", + "--help"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "string3", + "string4", + "-h"), + "hr:o::", options, + (Entry[]) { + { 'h', NULL }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4")); + + test_getopt_long_one(STRV_MAKE("arg0", + "--required1", "reqarg1"), + "hr:o::", options, + (Entry[]) { + { 'r', "reqarg1" }, + {} + }, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "-r", "reqarg1"), + "hr:o::", options, + (Entry[]) { + { 'r', "reqarg1" }, + {} + }, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "string2", + "-r", "reqarg1"), + "hr:o::", options, + (Entry[]) { + { 'r', "reqarg1" }, + {} + }, + STRV_MAKE("string1", + "string2")); + + test_getopt_long_one(STRV_MAKE("arg0", + "--optional1=optarg1"), + "hr:o::", options, + (Entry[]) { + { 'o', "optarg1" }, + {} + }, + NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "--optional1", "string1"), + "hr:o::", options, + (Entry[]) { + { 'o', NULL, "string1" }, + {} + }, + STRV_MAKE("string1")); + + test_getopt_long_one(STRV_MAKE("arg0", + "-ooptarg1"), + "hr:o::", options, + (Entry[]) { + { 'o', "optarg1" }, + {} + }, NULL); + + test_getopt_long_one(STRV_MAKE("arg0", + "-o", "string1"), + "hr:o::", options, + (Entry[]) { + { 'o', NULL, "string1" }, + {} + }, + STRV_MAKE("string1")); + + test_getopt_long_one(STRV_MAKE("arg0", + "string1", + "--help", + "--version", + "string2", + "--required1", "reqarg1", + "--required2", "reqarg2", + "--required1=reqarg3", + "--required2=reqarg4", + "string3", + "--optional1", "string4", + "--optional2", "string5", + "--optional1=optarg1", + "--optional2=optarg2", + "-h", + "-r", "reqarg5", + "-rreqarg6", + "-ooptarg3", + "-o", + "string6", + "-o", + "-h", + "-o", + "--help", + "string7", + "-hooptarg4", + "-hrreqarg6", + "--", + "--help", + "--required1", + "--optional1"), + "hr:o::", options, + (Entry[]) { + { 'h' }, + { ARG_VERSION }, + { 'r', "reqarg1" }, + { ARG_REQUIRED, "reqarg2" }, + { 'r', "reqarg3" }, + { ARG_REQUIRED, "reqarg4" }, + { 'o', NULL, "string4" }, + { ARG_OPTIONAL, NULL, "string5" }, + { 'o', "optarg1" }, + { ARG_OPTIONAL, "optarg2" }, + { 'h' }, + { 'r', "reqarg5" }, + { 'r', "reqarg6" }, + { 'o', "optarg3" }, + { 'o', NULL, "string6" }, + { 'o', NULL, "-h" }, + { 'h' }, + { 'o', NULL, "--help" }, + { 'h' }, + { 'h' }, + { 'o', "optarg4" }, + { 'h' }, + { 'r', "reqarg6" }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string4", + "string5", + "string6", + "string7", + "--help", + "--required1", + "--optional1")); +} +static void test_getopt_one( + char **argv, + const char *optstring, + const Entry *entries, + char **remaining) { + + _cleanup_free_ char *joined = strv_join(argv, ", "); + log_debug("/* %s(%s) */", __func__, joined); + + _cleanup_free_ char *saved_argv0 = NULL; + ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0])); + + int c, argc = strv_length(argv); + size_t i = 0, n_entries = 0; + + for (const Entry *e = entries; e && e->opt != 0; e++) + n_entries++; + + optind = 0; + while ((c = getopt(argc, argv, optstring)) >= 0) { + log_debug("%c: %s", c, strna(optarg)); + + ASSERT_LT(i, n_entries); + ASSERT_EQ(c, entries[i].opt); + ASSERT_STREQ(optarg, entries[i].argument); + if (entries[i].nextarg) + ASSERT_STREQ(argv[optind], entries[i].nextarg); + i++; + } + + ASSERT_EQ(i, n_entries); + ASSERT_LE(optind, argc); + ASSERT_EQ(argc - optind, (int) strv_length(remaining)); + for (int j = optind; j < argc; j++) + ASSERT_STREQ(argv[j], remaining[j - optind]); + ASSERT_STREQ(argv[0], saved_argv0); +} + +TEST(getopt) { + test_getopt_one(STRV_MAKE("arg0"), + "hr:o::", + NULL, + NULL); + + test_getopt_one(STRV_MAKE("arg0", + "string1", + "string2"), + "hr:o::", + NULL, + STRV_MAKE("string1", + "string2")); + + test_getopt_one(STRV_MAKE("arg0", + "-h"), + "hr:o::", + (Entry[]) { + { 'h', NULL }, + {} + }, + NULL); + + test_getopt_one(STRV_MAKE("arg0", + "-r", "reqarg1"), + "hr:o::", + (Entry[]) { + { 'r', "reqarg1" }, + {} + }, + NULL); + + test_getopt_one(STRV_MAKE("arg0", + "string1", + "string2", + "-r", "reqarg1"), + "hr:o::", + (Entry[]) { + { 'r', "reqarg1" }, + {} + }, + STRV_MAKE("string1", + "string2")); + + test_getopt_one(STRV_MAKE("arg0", + "-ooptarg1"), + "hr:o::", + (Entry[]) { + { 'o', "optarg1" }, + {} + }, + NULL); + + test_getopt_one(STRV_MAKE("arg0", + "-o", "string1"), + "hr:o::", + (Entry[]) { + { 'o', NULL, "string1" }, + {} + }, + STRV_MAKE("string1")); + + test_getopt_one(STRV_MAKE("arg0", + "string1", + "string2", + "string3", + "-h", + "-r", "reqarg5", + "-rreqarg6", + "-ooptarg3", + "-o", + "string6", + "-o", + "-h", + "-o", + "string7", + "-hooptarg4", + "-hrreqarg6"), + "hr:o::", + (Entry[]) { + { 'h' }, + { 'r', "reqarg5" }, + { 'r', "reqarg6" }, + { 'o', "optarg3" }, + { 'o', NULL, "string6" }, + { 'o', NULL, "-h" }, + { 'h' }, + { 'o', NULL, "string7" }, + { 'h' }, + { 'o', "optarg4" }, + { 'h' }, + { 'r', "reqarg6" }, + {} + }, + STRV_MAKE("string1", + "string2", + "string3", + "string6", + "string7")); +} + +DEFINE_TEST_MAIN(LOG_DEBUG);