]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
musl: introduce wrappers for getopt() and getopt_long()
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sun, 30 Nov 2025 02:10:02 +0000 (11:10 +0900)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Fri, 5 Dec 2025 10:01:09 +0000 (11:01 +0100)
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().

src/include/musl/getopt.h [new file with mode: 0644]
src/include/musl/unistd.h [new file with mode: 0644]
src/libc/musl/getopt.c [new file with mode: 0644]
src/libc/musl/meson.build
src/test/meson.build
src/test/test-getopt.c [new file with mode: 0644]

diff --git a/src/include/musl/getopt.h b/src/include/musl/getopt.h
new file mode 100644 (file)
index 0000000..6aed1df
--- /dev/null
@@ -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 <getopt.h>
+
+/* 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 (file)
index 0000000..8fbaaf6
--- /dev/null
@@ -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 <unistd.h>
+
+/* 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 (file)
index 0000000..15c3afa
--- /dev/null
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdbool.h>
+#include <string.h>
+
+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);
+}
index 387f04dac410457699c4df6816f52e4f56df9db9..d40a2f19115efd7da727bdceb689b490e273aeff 100644 (file)
@@ -5,6 +5,7 @@ if get_option('libc') != 'musl'
 endif
 
 libc_wrapper_sources += files(
+        'getopt.c',
         'printf.c',
         'stdio.c',
         'stdlib.c',
index 2974700954876e4e3301c51cb81f9689b24cd4c6..2366dbaf1c2d00f2b91149417ba0e75bc9b6015e 100644 (file)
@@ -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 (file)
index 0000000..e17621a
--- /dev/null
@@ -0,0 +1,516 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#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);