Hi hackers,
While reworking the pg_waldump TAP test [1] I ran "pg_waldump --stats
--limit 5" and it failed on Windows CI with 'could not locate WAL file
"5"'. For a long option with optional_argument and no "=", the port
getopt_long() increments optind in the missing-argument branch and
again at the end of the long option path, so the next option is
skipped and its value becomes a positional argument. The same branch
returns BADARG when optstring starts with a colon although nothing is
missing.
glibc handles both correctly, so this only happens in CI on Windows, for
--stats in pg_waldump and --install-missing in pg_amcheck when another
option follows. psql's --help is also optional_argument but exits
before anything after it matters.
0001 gives optional_argument its own branch that sets optarg to NULL
and falls through to the common optind increment. The remaining else
is then only the missing required argument case.
0002 adds src/test/modules/test_getopt_long, since nothing in
check-world exercises this file on Linux or macOS. A small harness
takes an optstring, a long option list and the arguments, and prints
one line per getopt_long() return plus the leftover non-options.
src/port/getopt_long.c is compiled into it directly so it is tested
everywhere. A second copy linked without it runs the same cases
against the system getopt_long().
The TAP test is a text table of named cases, each with its arguments
and expected output:
== long optional, separate not consumed
args: --gamma foo
--gamma
-- foo
== short missing arg, silent
optstring: :ab:
args: -b
:
--
It covers short options attached, separate and bundled, missing and
unknown options with and without the leading colon, long required
arguments in each form, long optional arguments first, middle and
last, the flag pointer form, "--", a lone "-" and non-option
reordering. Port and system output match on every case on Linux and
macOS. Only the error message wording differs and the test accepts
either.
0003 fixes a second problem the tests turned up. The port moves each
non-option to the end of argv as soon as it sees it, so by the time a
following option looks for its argument, a non-option that came before
it on the command line is already sitting after it and gets taken:
"pg_amcheck mydb --jobs" parses as --jobs=mydb rather than reporting
the missing argument. The system getopt_long() reports the missing
argument. The fix bounds the argument search by the start of the
moved block. 0004 adds the test cases for it, kept separate in case
0003 is backpatched without the test module.
This behavior comes from the non-option reordering added in 411b720343.
CC'ing Nathan in case I'm missing an intended aspect of that
implementation.
Without 0001 and 0003 the port binary fails 16 cases, the 12 optional
argument ones that 0001 fixes and the four from 0004 that 0003 fixes.
The system binary passes throughout on Linux. With both patches, all
244 checks pass.
Both fixes seem worth backpatching. The test module itself probably
doesn't need to be.
AI review also flagged a couple of possible portability issues with
the system getopt_long() comparison on BSD, particularly around
differences in error messages. I don't have a BSD system to test on,
so will see what the buildfarm says.
[1]
https://www.postgresql.org/message-id/CAH7T-araSEdsNpxiKaMOW8kr_ZHhekCLHx5yzm2ksq4FdgULbA%40mail.gmail.com
Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
From daaa4883cc523984e567cfb5323bc6e4f032f401 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Sat, 29 Aug 2026 00:34:18 +0000
Subject: [PATCH 1/4] Fix optional argument handling in port getopt_long()
A long option with an optional argument and no "=" advanced optind
twice, skipping the next argument, and returned BADARG when optstring
starts with a colon even though nothing is missing.
---
src/port/getopt_long.c | 20 ++++++--------------
1 file changed, 6 insertions(+), 14 deletions(-)
diff --git a/src/port/getopt_long.c b/src/port/getopt_long.c
index 2e869fed58b..7489a0206ee 100644
--- a/src/port/getopt_long.c
+++ b/src/port/getopt_long.c
@@ -137,31 +137,23 @@ retry:
{
if (place[namelen] == '=')
optarg = place + namelen + 1;
- else if (optind < argc - 1 &&
- has_arg == required_argument)
+ else if (has_arg == optional_argument)
+ optarg = NULL;
+ else if (optind < argc - 1)
{
optind++;
optarg = argv[optind];
}
else
{
+ /* required argument missing */
optind++;
- if (optstring[0] == ':')
- {
- place = EMSG;
- return BADARG;
- }
-
- if (opterr && has_arg == required_argument)
+ if (opterr && optstring[0] != ':')
fprintf(stderr,
"%s: option requires an argument -- %s\n",
argv[0], place);
-
place = EMSG;
-
- if (has_arg == required_argument)
- return BADCH;
- optarg = NULL;
+ return optstring[0] == ':' ? BADARG : BADCH;
}
}
else
--
2.17.1
From 7525ace3a35ddf08e3dd021004adec1dea77ef67 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Sat, 29 Aug 2026 13:55:44 +0000
Subject: [PATCH 4/4] Test getopt_long() with a non-option before a missing
argument
---
.../test_getopt_long/t/001_getopt_long.pl | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/test/modules/test_getopt_long/t/001_getopt_long.pl b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
index 85c49d7d678..b407967a148 100644
--- a/src/test/modules/test_getopt_long/t/001_getopt_long.pl
+++ b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
@@ -75,6 +75,18 @@ args: -ab
--
stderr: requires an argument -- '?b'?
+== short missing arg, non-option before
+args: foo -b
+?
+-- foo
+stderr: requires an argument -- '?b'?
+
+== short missing arg, lone dash before
+args: - -b
+?
+-- -
+stderr: requires an argument -- '?b'?
+
== short unknown
args: -x
?
@@ -184,6 +196,18 @@ args: -a --beta
--
stderr: requires an argument -- beta|[`']--beta' requires an argument
+== long required, missing, non-option before
+args: foo --beta
+?
+-- foo
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
+== long required, missing, lone dash before
+args: - --beta
+?
+-- -
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
== long optional, alone
args: --gamma
--gamma
--
2.17.1
From acb641037186b254567d2b352340772f455d2c17 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Sat, 29 Aug 2026 13:34:07 +0000
Subject: [PATCH 2/4] Add test_getopt_long module
TAP tests for the port getopt_long(). The harness compiles
src/port/getopt_long.c in directly since libpgport uses the system
implementation on most platforms. A second copy linked without it runs
the same cases against the system getopt_long() for comparison.
---
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_getopt_long/Makefile | 39 ++
src/test/modules/test_getopt_long/README | 18 +
src/test/modules/test_getopt_long/meson.build | 48 +++
.../test_getopt_long/t/001_getopt_long.pl | 372 ++++++++++++++++++
.../test_getopt_long/test_getopt_long.c | 149 +++++++
7 files changed, 628 insertions(+)
create mode 100644 src/test/modules/test_getopt_long/Makefile
create mode 100644 src/test/modules/test_getopt_long/README
create mode 100644 src/test/modules/test_getopt_long/meson.build
create mode 100644 src/test/modules/test_getopt_long/t/001_getopt_long.pl
create mode 100644 src/test/modules/test_getopt_long/test_getopt_long.c
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index bb88b3058ed..c85e8aff866 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -31,6 +31,7 @@ SUBDIRS = \
test_dsm_registry \
test_escape \
test_extensions \
+ test_getopt_long \
test_ginpostinglist \
test_int128 \
test_integerset \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index ce09e00531d..3af2b1473e2 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -32,6 +32,7 @@ subdir('test_dsa')
subdir('test_dsm_registry')
subdir('test_escape')
subdir('test_extensions')
+subdir('test_getopt_long')
subdir('test_ginpostinglist')
subdir('test_int128')
subdir('test_integerset')
diff --git a/src/test/modules/test_getopt_long/Makefile b/src/test/modules/test_getopt_long/Makefile
new file mode 100644
index 00000000000..eb53b2c2231
--- /dev/null
+++ b/src/test/modules/test_getopt_long/Makefile
@@ -0,0 +1,39 @@
+# src/test/modules/test_getopt_long/Makefile
+
+PGFILEDESC = "standalone getopt_long tester"
+PGAPPICON = win32
+
+TAP_TESTS = 1
+
+OBJS = test_getopt_long.o $(WIN32RES)
+
+# getopt_long.o is not in OBJS, or --with-llvm would look for a
+# getopt_long.c here to build getopt_long.bc from.
+EXTRA_CLEAN = test_getopt_long$(X) test_getopt_long_system$(X) getopt_long.o
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_getopt_long
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+all: test_getopt_long$(X) test_getopt_long_system$(X)
+
+%.o: $(top_srcdir)/$(subdir)/%.c
+
+# The port getopt_long() is compiled in directly rather than taken from
+# libpgport, which uses the system implementation where one exists.
+getopt_long.o: $(top_srcdir)/src/port/getopt_long.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+test_getopt_long$(X): test_getopt_long.o getopt_long.o $(WIN32RES)
+ $(CC) $(CFLAGS) $^ $(PG_LIBS_INTERNAL) $(LDFLAGS) $(LDFLAGS_EX) $(PG_LIBS) $(LIBS) -o $@
+
+# linked against the system getopt_long(), where there is one
+test_getopt_long_system$(X): test_getopt_long.o $(WIN32RES)
+ $(CC) $(CFLAGS) $^ $(PG_LIBS_INTERNAL) $(LDFLAGS) $(LDFLAGS_EX) $(PG_LIBS) $(LIBS) -o $@
diff --git a/src/test/modules/test_getopt_long/README b/src/test/modules/test_getopt_long/README
new file mode 100644
index 00000000000..f1fffcdfc90
--- /dev/null
+++ b/src/test/modules/test_getopt_long/README
@@ -0,0 +1,18 @@
+Module `test_getopt_long`
+=========================
+
+This module tests the getopt_long() implementation in src/port/getopt_long.c.
+Most platforms provide their own getopt_long() and libpgport does not include
+the port version there, so the test program compiles the port source in
+directly and thus exercises it everywhere. `test_getopt_long_system` is the
+same program without it, so the tests also run against the system
+getopt_long() and check the two agree.
+
+`test_getopt_long OPTSTRING LONGOPTS [ARG ...]` parses ARGs with the given
+short option string and long option list and prints one line per
+getopt_long() return, followed by a line listing the remaining non-option
+arguments. See the header comment in test_getopt_long.c for the format. The
+TAP test compares that output against the expected sequence for a range of
+argument layouts: attached and separate values, optional arguments in first,
+middle and last position, missing arguments, "--", a bare "-", and
+non-option reordering.
diff --git a/src/test/modules/test_getopt_long/meson.build b/src/test/modules/test_getopt_long/meson.build
new file mode 100644
index 00000000000..aa2e533b119
--- /dev/null
+++ b/src/test/modules/test_getopt_long/meson.build
@@ -0,0 +1,48 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# The port getopt_long() is compiled in directly rather than taken from
+# libpgport, which uses the system implementation where one exists.
+test_getopt_long_sources = files(
+ 'test_getopt_long.c',
+ '../../../port/getopt_long.c',
+)
+
+if host_system == 'windows'
+ test_getopt_long_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'test_getopt_long',
+ '--FILEDESC', 'standalone getopt_long tester',
+ ])
+endif
+
+test_getopt_long = executable('test_getopt_long',
+ test_getopt_long_sources,
+ dependencies: [frontend_code],
+ kwargs: default_bin_args + {
+ 'install': false,
+ },
+)
+
+# Same program linked against the system getopt_long(), where there is one,
+# to check the port version behaves the same.
+test_getopt_long_system = executable('test_getopt_long_system',
+ files('test_getopt_long.c'),
+ dependencies: [frontend_code],
+ kwargs: default_bin_args + {
+ 'install': false,
+ },
+)
+
+tests += {
+ 'name': 'test_getopt_long',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'tests': [
+ 't/001_getopt_long.pl',
+ ],
+ 'deps': [
+ test_getopt_long,
+ test_getopt_long_system,
+ ],
+ },
+}
diff --git a/src/test/modules/test_getopt_long/t/001_getopt_long.pl b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
new file mode 100644
index 00000000000..85c49d7d678
--- /dev/null
+++ b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
@@ -0,0 +1,372 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test the port implementation of getopt_long().
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $longopts = 'alpha,beta:,gamma::,*verbose,alphabet';
+
+# Each case is a paragraph: a "== title" line, optional "optstring:" and
+# "args:" lines (default "ab:" and none), then the expected output lines.
+# An optional final "stderr: REGEX" line gives the expected error message;
+# without it stderr must be empty. The system getopt_long() words messages
+# differently, so the regexes accept both forms.
+my $cases = <<'EOF';
+== no arguments
+--
+
+== only non-options
+args: x y
+-- x y
+
+== short no arg
+args: -a
+-a
+--
+
+== short attached arg
+args: -bfoo
+-b=foo
+--
+
+== short separate arg
+args: -b foo
+-b=foo
+--
+
+== short bundled
+args: -ab foo
+-a
+-b=foo
+--
+
+== short bundled, attached arg
+args: -abfoo
+-a
+-b=foo
+--
+
+== short arg looks like option
+args: -b -a
+-b=-a
+--
+
+== short missing arg
+args: -b
+?
+--
+stderr: requires an argument -- '?b'?
+
+== short missing arg, silent
+optstring: :ab:
+args: -b
+:
+--
+
+== short missing arg at end of bundle
+args: -ab
+-a
+?
+--
+stderr: requires an argument -- '?b'?
+
+== short unknown
+args: -x
+?
+--
+stderr: (illegal|invalid) option -- '?x'?
+
+== short unknown, silent
+optstring: :ab:
+args: -x
+?
+--
+
+== short unknown in bundle
+args: -axa foo
+-a
+?
+-a
+-- foo
+stderr: (illegal|invalid) option -- '?x'?
+
+== long no arg
+args: --alpha
+--alpha
+--
+
+== long no arg then non-option
+args: --alpha foo
+--alpha
+-- foo
+
+== long no arg, longer name also defined
+args: --alphabet
+--alphabet
+--
+
+== long unknown
+args: --nope
+?
+--
+stderr: illegal option -- nope|unrecognized option [`']--nope'
+
+== long unknown, silent
+optstring: :ab:
+args: --nope
+?
+--
+
+== long unknown then option
+args: --nope -a
+?
+-a
+--
+stderr: illegal option -- nope|unrecognized option [`']--nope'
+
+== long required, equals
+args: --beta=foo
+--beta=foo
+--
+
+== long required, separate
+args: --beta foo
+--beta=foo
+--
+
+== long required, empty
+args: --beta=
+--beta=
+--
+
+== long required, value contains equals
+args: --beta=a=b
+--beta=a=b
+--
+
+== long required, double dash as value
+args: --beta --
+--beta=--
+--
+
+== long required, separate then more
+args: --beta foo -a bar
+--beta=foo
+-a
+-- bar
+
+== long required, value looks like option
+args: --beta --alpha
+--beta=--alpha
+--
+
+== long required, missing
+args: --beta
+?
+--
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
+== long required, missing, silent
+optstring: :ab:
+args: --beta
+:
+--
+
+== long required, missing after others
+args: -a --beta
+-a
+?
+--
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
+== long optional, alone
+args: --gamma
+--gamma
+--
+
+== long optional, alone, silent
+optstring: :ab:
+args: --gamma
+--gamma
+--
+
+== long optional, equals
+args: --gamma=foo
+--gamma=foo
+--
+
+== long optional, empty
+args: --gamma=
+--gamma=
+--
+
+== long optional, separate not consumed
+args: --gamma foo
+--gamma
+-- foo
+
+== long optional, separate not consumed, silent
+optstring: :ab:
+args: --gamma foo
+--gamma
+-- foo
+
+== long optional, first
+args: --gamma -a foo
+--gamma
+-a
+-- foo
+
+== long optional, first with equals
+args: --gamma=x -a foo
+--gamma=x
+-a
+-- foo
+
+== long optional, middle
+args: -a --gamma -b foo
+-a
+--gamma
+-b=foo
+--
+
+== long optional, middle before non-option
+args: -a --gamma foo -b bar
+-a
+--gamma
+-b=bar
+-- foo
+
+== long optional between non-options
+args: foo --gamma bar -a
+--gamma
+-a
+-- foo bar
+
+== long optional, last
+args: -a -b foo --gamma
+-a
+-b=foo
+--gamma
+--
+
+== long optional, last, silent
+optstring: :ab:
+args: -a --gamma
+-a
+--gamma
+--
+
+== long optional followed by long
+args: --gamma --alpha
+--gamma
+--alpha
+--
+
+== long optional followed by long required
+args: --gamma --beta foo
+--gamma
+--beta=foo
+--
+
+== long optional followed by lone dash
+args: --gamma -
+--gamma
+-- -
+
+== long optional repeated
+args: --gamma --gamma=1 --gamma
+--gamma
+--gamma=1
+--gamma
+--
+
+== long optional then double dash
+args: --gamma -- foo
+--gamma
+-- foo
+
+== long flag
+args: --verbose
+flag:--verbose
+--
+
+== long flag among others
+args: -a --verbose --gamma
+-a
+flag:--verbose
+--gamma
+--
+
+== double dash ends options
+args: -a -- -b foo
+-a
+-- -b foo
+
+== double dash first
+args: -- -a
+-- -a
+
+== double dash last
+args: -a --
+-a
+--
+
+== double dash repeated
+args: -a -- -- foo
+-a
+-- -- foo
+
+== non-options reordered to end
+args: foo -a bar -b baz qux
+-a
+-b=baz
+-- foo bar qux
+
+== lone dash is a non-option
+args: -a - -b x
+-a
+-b=x
+-- -
+
+== non-options then double dash
+args: foo -a -- -b bar
+-a
+-- foo -b bar
+EOF
+
+# test_getopt_long_system uses the system getopt_long() where there is one;
+# its outputs must match, only the error message wording differs.
+foreach my $exe ('test_getopt_long', 'test_getopt_long_system')
+{
+ foreach my $case (split /\n\n/, $cases)
+ {
+ my @lines = split /\n/, $case;
+ my ($title) = shift(@lines) =~ /^== (.*)/;
+ my $optstring = 'ab:';
+ $optstring = $1 if $lines[0] =~ /^optstring: (.*)/ and shift @lines;
+ my @args;
+ @args = split ' ', $1 if $lines[0] =~ /^args: (.*)/ and shift @lines;
+ my $stderr_re;
+ $stderr_re = $1 if $lines[-1] =~ /^stderr: (.*)/ and pop @lines;
+
+ my ($stdout, $stderr) =
+ run_command([ $exe, $optstring, $longopts, @args ]);
+ is($stdout, join("\n", @lines), "$exe $title: output");
+ if (defined $stderr_re)
+ {
+ like($stderr, qr/$stderr_re/, "$exe $title: stderr");
+ }
+ else
+ {
+ is($stderr, '', "$exe $title: no stderr");
+ }
+ }
+}
+
+done_testing();
diff --git a/src/test/modules/test_getopt_long/test_getopt_long.c b/src/test/modules/test_getopt_long/test_getopt_long.c
new file mode 100644
index 00000000000..9027e55eb52
--- /dev/null
+++ b/src/test/modules/test_getopt_long/test_getopt_long.c
@@ -0,0 +1,149 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_getopt_long.c
+ * Test program for the src/port implementation of getopt_long()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_getopt_long/test_getopt_long.c
+ *
+ * Usage: test_getopt_long OPTSTRING LONGOPTS [ARG ...]
+ *
+ * LONGOPTS is a comma separated list of long option names, each optionally
+ * followed by ":" (required argument) or "::" (optional argument), and
+ * optionally prefixed with "*" to make getopt_long() set a flag variable
+ * instead of returning a value. The remaining ARGs are parsed with
+ * getopt_long() and each return is printed on its own line:
+ *
+ * -x short option x
+ * -x=VALUE short option x with argument VALUE
+ * --name long option name
+ * --name=VALUE long option name with argument VALUE
+ * flag:--name long option name, delivered via its flag pointer
+ * ? BADCH (unknown option or missing argument)
+ * : BADARG (missing argument, optstring starts with ':')
+ *
+ * After getopt_long() returns -1, a final line "--" lists the remaining
+ * (non-option) arguments, space separated.
+ *
+ * src/port/getopt_long.c is compiled into this program directly so the
+ * port implementation is tested even on platforms where libpgport would
+ * normally use the system's getopt_long(). test_getopt_long_system is the
+ * same program linked without it, for comparison against the system one.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "getopt_long.h"
+
+#define MAX_LONGOPTS 32
+
+/* long options without a flag return this plus their index */
+#define LONGOPT_BASE 1000
+
+static struct option longopts[MAX_LONGOPTS + 1];
+static char *longnames[MAX_LONGOPTS];
+static int flagval;
+
+static void
+parse_longopts(char *spec)
+{
+ int n = 0;
+ char *tok;
+
+ for (tok = strtok(spec, ","); tok != NULL; tok = strtok(NULL, ","))
+ {
+ struct option *opt = &longopts[n];
+ char *colon;
+
+ if (n >= MAX_LONGOPTS)
+ {
+ fprintf(stderr, "too many long options\n");
+ exit(1);
+ }
+
+ if (tok[0] == '*')
+ {
+ tok++;
+ opt->flag = &flagval;
+ opt->val = n + 1;
+ }
+ else
+ {
+ opt->flag = NULL;
+ opt->val = LONGOPT_BASE + n;
+ }
+
+ colon = strchr(tok, ':');
+ if (colon == NULL)
+ opt->has_arg = no_argument;
+ else if (colon[1] == ':')
+ opt->has_arg = optional_argument;
+ else
+ opt->has_arg = required_argument;
+ if (colon != NULL)
+ *colon = '\0';
+
+ longnames[n] = tok;
+ opt->name = tok;
+ n++;
+ }
+
+ longopts[n].name = NULL;
+}
+
+int
+main(int argc, char **argv)
+{
+ const char *optstring;
+ char **args;
+ int nargs;
+ int c;
+
+ if (argc < 3)
+ {
+ fprintf(stderr, "Usage: %s OPTSTRING LONGOPTS [ARG ...]\n", argv[0]);
+ exit(1);
+ }
+
+ optstring = argv[1];
+ parse_longopts(argv[2]);
+
+ /* build the argv that getopt_long() will see, and may reorder */
+ nargs = argc - 2;
+ args = palloc((nargs + 1) * sizeof(char *));
+ args[0] = argv[0];
+ for (int i = 1; i < nargs; i++)
+ args[i] = argv[i + 2];
+ args[nargs] = NULL;
+
+ while ((c = getopt_long(nargs, args, optstring, longopts, NULL)) != -1)
+ {
+ if (c == 0)
+ {
+ printf("flag:--%s\n", longnames[flagval - 1]);
+ continue;
+ }
+
+ if (c >= LONGOPT_BASE)
+ printf("--%s", longnames[c - LONGOPT_BASE]);
+ else if (c == '?' || c == ':')
+ printf("%c", c);
+ else
+ printf("-%c", c);
+
+ if (optarg != NULL)
+ printf("=%s", optarg);
+ printf("\n");
+ }
+
+ printf("--");
+ for (int i = optind; i < nargs; i++)
+ printf(" %s", args[i]);
+ printf("\n");
+
+ return 0;
+}
--
2.17.1
From e6be21d4b1070e94dcab0d92a590186676823b71 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Sat, 29 Aug 2026 13:55:44 +0000
Subject: [PATCH 3/4] Do not take a moved non-option as an option argument in
port getopt_long()
Non-options are moved to the end of argv as they are found, so one
that preceded an option was already behind it when the option looked
for its argument. "foo -b" parsed as -b with argument foo instead of
reporting the missing argument.
---
src/port/getopt_long.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/port/getopt_long.c b/src/port/getopt_long.c
index 7489a0206ee..269762280d8 100644
--- a/src/port/getopt_long.c
+++ b/src/port/getopt_long.c
@@ -65,6 +65,7 @@ getopt_long(int argc, char *const argv[],
const char *oli; /* option letter list index */
static int nonopt_start = -1;
static bool force_nonopt = false;
+ int argend;
if (!*place)
{ /* update scanning pointer */
@@ -135,11 +136,18 @@ retry:
if (has_arg != no_argument)
{
+ /*
+ * Non-options already moved to the back of argv
+ * preceded this option, so they cannot be its
+ * argument.
+ */
+ argend = (nonopt_start == -1) ? argc : nonopt_start;
+
if (place[namelen] == '=')
optarg = place + namelen + 1;
else if (has_arg == optional_argument)
optarg = NULL;
- else if (optind < argc - 1)
+ else if (optind < argend - 1)
{
optind++;
optarg = argv[optind];
@@ -213,9 +221,11 @@ retry:
}
else
{ /* need an argument */
+ argend = (nonopt_start == -1) ? argc : nonopt_start;
+
if (*place) /* no white space */
optarg = place;
- else if (argc <= ++optind)
+ else if (argend <= ++optind)
{ /* no arg */
place = EMSG;
if (*optstring == ':')
--
2.17.1