git: 439eb9c9771b - main - sysconf: accept a unique prefix of a target keyword

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Thu, 08 Oct 2026 16:29:12 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=439eb9c9771b8a62b5bcc8d2f7337f26c4d717e2

commit 439eb9c9771b8a62b5bcc8d2f7337f26c4d717e2
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-10-08 16:26:02 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-10-08 16:26:02 +0000

    sysconf: accept a unique prefix of a target keyword
    
    Exact match still wins.  An ambiguous prefix is rejected with the
    list of matches.  Move keyword selection and the rc pass-through
    into sysconf-targets.c.  Bump sysconf(8) to 2.1.
    
    Reviewed by:    kfv
    Differential Revision:  https://reviews.freebsd.org/D59924
---
 usr.sbin/sysconf/Makefile              |   1 +
 usr.sbin/sysconf/sysconf-targets.c     | 179 +++++++++++++++++++++++++++++++++
 usr.sbin/sysconf/sysconf.8             |  22 +++-
 usr.sbin/sysconf/sysconf.c             |  91 ++---------------
 usr.sbin/sysconf/sysconf_priv.h        |   7 +-
 usr.sbin/sysconf/tests/sysconf_test.sh |  49 +++++++++
 6 files changed, 262 insertions(+), 87 deletions(-)

diff --git a/usr.sbin/sysconf/Makefile b/usr.sbin/sysconf/Makefile
index 84a12628cdc5..2c96997ff787 100644
--- a/usr.sbin/sysconf/Makefile
+++ b/usr.sbin/sysconf/Makefile
@@ -12,6 +12,7 @@ MAN=		sysconf.8			\
 		sysconf-targets.8
 
 SRCS=		sysconf.c			\
+		sysconf-targets.c		\
 		sysconf_edit_assign.c		\
 		sysconf_edit_make.c		\
 		sysconf_edit_write.c		\
diff --git a/usr.sbin/sysconf/sysconf-targets.c b/usr.sbin/sysconf/sysconf-targets.c
new file mode 100644
index 000000000000..940502d4ad02
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-targets.c
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Target keyword selection: unique-prefix match, and the rc pass-through.
+ */
+
+#include "sysconf_priv.h"
+
+/*
+ * Unified target keywords: libbsdconf formats plus pass-throughs. Exact
+ * match wins; otherwise a unique prefix is accepted. Keep alphabetical so
+ * ambiguous-match lists read naturally (e.g. `s' -> src, sysctl).
+ */
+static const char *const target_keywords[] = {
+	"generic",
+	"loader",
+	"make",
+	"rc",
+	"src",
+	"sysctl",
+	NULL
+};
+
+/*
+ * Map `token' to a canonical keyword from `keywords'. Exact match wins;
+ * otherwise a unique prefix is accepted. Ambiguous prefixes print the
+ * matches and call usage(). Unknown tokens return NULL.
+ */
+const char *
+resolve_keyword(const char *token, const char *const *keywords,
+    const char *what)
+{
+	char matches[128];
+	const char *found;
+	size_t off;
+	size_t tlen;
+	unsigned int n;
+	unsigned int nmatch;
+
+	if (token == NULL || *token == '\0')
+		return (NULL);
+	tlen = strlen(token);
+	for (n = 0; keywords[n] != NULL; n++)
+		if (strcmp(keywords[n], token) == 0)
+			return (keywords[n]);
+	found = NULL;
+	nmatch = 0;
+	off = 0;
+	matches[0] = '\0';
+	for (n = 0; keywords[n] != NULL; n++) {
+		if (strncmp(keywords[n], token, tlen) != 0)
+			continue;
+		nmatch++;
+		found = keywords[n];
+		if (off > 0 && off < sizeof(matches))
+			off += (size_t)snprintf(matches + off,
+			    sizeof(matches) - off, ", ");
+		if (off < sizeof(matches))
+			off += (size_t)snprintf(matches + off,
+			    sizeof(matches) - off, "%s",
+			    keywords[n]);
+	}
+	if (nmatch == 1)
+		return (found);
+	if (nmatch > 1) {
+		warnx("ambiguous %s `%s'; matches %s", what, token, matches);
+		usage();
+	}
+	return (NULL);
+}
+
+static const char *
+resolve_target_keyword(const char *token)
+{
+
+	return (resolve_keyword(token, target_keywords, "target"));
+}
+
+/*
+ * Locate the target keyword in `argv' without consuming or validating
+ * anything: the first argument that is neither an option cluster nor the
+ * argument of one (per OPTSTRING; an unknown option letter is assumed to
+ * take no argument). A `--' ends option scanning as usual. Returns the
+ * argv index of the target, or -1 when there is none. Used before any
+ * getopt(3) processing so that the `rc' pass-through target (see
+ * exec_sysrc() below) can be detected while the command line is still
+ * pristine.
+ *
+ * Attached arguments (`-fPATH', `-R/altroot') are self-contained: the rest
+ * of the cluster is the option's argument, so the following argv element is
+ * not consumed. Only a trailing option letter that takes an argument and
+ * has nothing after it in the cluster (`-f PATH') skips the next element.
+ */
+static int
+find_target(int argc, char *argv[])
+{
+	int n;
+	const char *o;
+	const char *p;
+
+	for (n = 1; n < argc; n++) {
+		if (strcmp(argv[n], "--") == 0)
+			return (n + 1 < argc ? n + 1 : -1);
+		if (argv[n][0] == '-' && argv[n][1] != '\0') {
+			for (p = argv[n] + 1; *p != '\0'; p++) {
+				o = strchr(OPTSTRING, *p);
+				if (o != NULL && o[1] == ':') {
+					/* Takes an argument */
+					if (p[1] != '\0') {
+						;
+						/* attached: rest of argv[n] */
+					} else if (n + 1 < argc) {
+						n++;
+						/* separate argv */
+					}
+					break;
+				}
+			}
+			continue;
+		}
+		return (n);
+	}
+
+	return (-1);
+}
+
+/*
+ * The `rc' pass-through: rc.conf(5) is sysrc(8)'s domain, and its feature
+ * set is a superset of ours, so no option or argument is validated,
+ * interpreted, or reordered here. Every argument except the target
+ * keyword itself (at argv index `skip') is handed to sysrc(8) verbatim.
+ * Never returns.
+ */
+static void
+exec_sysrc(int argc, char *argv[], int skip)
+{
+	int n;
+	int nargc = 0;
+	char **nargv;
+
+	if ((nargv = calloc((size_t)argc + 1, sizeof(*nargv))) == NULL)
+		err(EXIT_FAILURE, NULL);
+	nargv[nargc++] = (char *)(uintptr_t)"sysrc";
+	for (n = 1; n < argc; n++) {
+		if (n == skip)
+			continue;
+		nargv[nargc++] = argv[n];
+	}
+	nargv[nargc] = NULL;
+
+	execvp("sysrc", nargv);
+	err(EXIT_FAILURE, "sysrc");
+}
+
+/*
+ * Resolve the target keyword. The `rc' target execs sysrc(8) and does
+ * not return, so `--help' and `--version' are not intercepted on that
+ * path. Unique-prefix shorthands are expanded (`r', `m'). Returns the
+ * canonical keyword, or NULL when the token is unknown or absent.
+ */
+const char *
+target_resolve(int argc, char *argv[])
+{
+	const char *resolved;
+	int n;
+
+	n = find_target(argc, argv);
+	if (n < 0)
+		return (NULL);
+	resolved = resolve_target_keyword(argv[n]);
+	if (resolved != NULL && strcmp(resolved, "rc") == 0)
+		exec_sysrc(argc, argv, n); /* never returns */
+	return (resolved);
+}
diff --git a/usr.sbin/sysconf/sysconf.8 b/usr.sbin/sysconf/sysconf.8
index c026d40f68f9..b6147bd39088 100644
--- a/usr.sbin/sysconf/sysconf.8
+++ b/usr.sbin/sysconf/sysconf.8
@@ -3,7 +3,7 @@
 .\"
 .\" SPDX-License-Identifier: BSD-2-Clause
 .\"
-.Dd September 16, 2026
+.Dd September 22, 2026
 .Dt SYSCONF 8
 .Os
 .Sh NAME
@@ -51,7 +51,8 @@ and
 .Pq Xr rc.conf 5 .
 The first positional argument selects a
 .Ar target
-keyword;
+keyword
+.Pq or a unique prefix of one; see Sx TARGETS ;
 options are equally accepted before it.
 Each target is documented on its own page
 .Pq see Sx TARGETS ;
@@ -266,6 +267,23 @@ See
 .Xr sysconf-generic 8 .
 .El
 .Pp
+A unique prefix of a target keyword is accepted:
+.Ql sy
+selects
+.Cm sysctl
+and
+.Ql sr
+selects
+.Cm src .
+.Ql s
+matches both and is rejected with a list of matches.
+An exact keyword always wins.
+Prefix matching is case-sensitive and applies to the start of the keyword
+only, so
+.Ql ctl
+does not select
+.Cm sysctl .
+.Pp
 Cross-cutting rules for multi-file sourcing,
 defaults,
 and where writes versus removals land are in
diff --git a/usr.sbin/sysconf/sysconf.c b/usr.sbin/sysconf/sysconf.c
index 10b7ed302f0f..8e23dc7a8278 100644
--- a/usr.sbin/sysconf/sysconf.c
+++ b/usr.sbin/sysconf/sysconf.c
@@ -57,82 +57,6 @@ const char	*jailname = NULL;	/* `-j jail' */
 const char	*module = NULL;		/* `-k module' */
 const char	*rootdir = "";		/* `-R dir' */
 
-/*
- * Locate the target keyword in `argv' without consuming or validating
- * anything: the first argument that is neither an option cluster nor the
- * argument of one (per OPTSTRING; an unknown option letter is assumed to
- * take no argument). A `--' ends option scanning as usual. Returns the
- * argv index of the target, or -1 when there is none. Used before any
- * getopt(3) processing so that the `rc' pass-through target (see
- * exec_sysrc() below) can be detected while the command line is still
- * pristine.
- *
- * Attached arguments (`-fPATH', `-R/altroot') are self-contained: the rest
- * of the cluster is the option's argument, so the following argv element is
- * not consumed. Only a trailing option letter that takes an argument and
- * has nothing after it in the cluster (`-f PATH') skips the next element.
- */
-static int
-find_target(int argc, char *argv[])
-{
-	int n;
-	const char *o;
-	const char *p;
-
-	for (n = 1; n < argc; n++) {
-		if (strcmp(argv[n], "--") == 0)
-			return (n + 1 < argc ? n + 1 : -1);
-		if (argv[n][0] == '-' && argv[n][1] != '\0') {
-			for (p = argv[n] + 1; *p != '\0'; p++) {
-				o = strchr(OPTSTRING, *p);
-				if (o != NULL && o[1] == ':') {
-					/* Takes an argument */
-					if (p[1] != '\0') {
-						;
-						/* attached: rest of argv[n] */
-					} else if (n + 1 < argc) {
-						n++;
-						/* separate argv */
-					}
-					break;
-				}
-			}
-			continue;
-		}
-		return (n);
-	}
-
-	return (-1);
-}
-
-/*
- * The `rc' pass-through: rc.conf(5) is sysrc(8)'s domain, and its feature
- * set is a superset of ours, so no option or argument is validated,
- * interpreted, or reordered here -- every argument except the target
- * keyword itself (at argv index `skip') is handed to sysrc(8) verbatim.
- * Never returns.
- */
-static void
-exec_sysrc(int argc, char *argv[], int skip)
-{
-	int n;
-	int nargc = 0;
-	char **nargv;
-
-	if ((nargv = calloc((size_t)argc + 1, sizeof(*nargv))) == NULL)
-		err(EXIT_FAILURE, NULL);
-	nargv[nargc++] = (char *)(uintptr_t)"sysrc";
-	for (n = 1; n < argc; n++) {
-		if (n == skip)
-			continue;
-		nargv[nargc++] = argv[n];
-	}
-	nargv[nargc] = NULL;
-
-	execvp("sysrc", nargv);
-	err(EXIT_FAILURE, "sysrc");
-}
-
 /*
  * Run the getopt(3) loop over `argv', setting the option globals. Called
  * once for the options preceding the target keyword and once for those
@@ -241,19 +165,16 @@ main(int argc, char *argv[])
 	int have_writes = 0;
 	int n;
 	int rv = EXIT_SUCCESS;
+	const char *resolved = NULL;
 	const char *target = NULL;
 
 	pgm = getprogname();
 
 	/*
-	 * The `rc' target is a pure pass-through to sysrc(8), detected
-	 * while the command line is still pristine so that nothing --
-	 * `--help' and `--version' included -- is intercepted on its way
-	 * there (see exec_sysrc()).
+	 * Target selection, including the rc pass-through, runs while
+	 * argv is still pristine (see target_resolve()).
 	 */
-	n = find_target(argc, argv);
-	if (n > 0 && strcmp(argv[n], "rc") == 0)
-		exec_sysrc(argc, argv, n); /* never returns */
+	resolved = target_resolve(argc, argv);
 
 	/*
 	 * Honor `--help' and `--version' wherever they appear (getopt(3)
@@ -287,7 +208,7 @@ main(int argc, char *argv[])
 	 * program name.
 	 */
 	if (argc > 0) {
-		target = argv[0];
+		target = resolved != NULL ? resolved : argv[0];
 		argc--;
 		argv++;
 #ifdef __FreeBSD__
@@ -679,6 +600,8 @@ help(void)
 	    "pass-through: all other arguments go to sysrc(8) verbatim");
 	fprintf(stderr, TGTFMT, "generic",
 	    "no default files; requires -f file");
+	fprintf(stderr,
+	    "A unique prefix of a target keyword is accepted.\n");
 	fprintf(stderr, "OPTIONS:\n");
 #define OPTFMT "\t%-9s %s\n"
 	fprintf(stderr, OPTFMT, "-A",
diff --git a/usr.sbin/sysconf/sysconf_priv.h b/usr.sbin/sysconf/sysconf_priv.h
index c72dd8f95c36..8add1fb3956e 100644
--- a/usr.sbin/sysconf/sysconf_priv.h
+++ b/usr.sbin/sysconf/sysconf_priv.h
@@ -37,7 +37,7 @@
 #define __unused	/* not all compilers support attributes */
 #endif
 
-#define SYSCONF_VERSION	"2.0 2026-09-16"
+#define SYSCONF_VERSION	"2.1 2026-09-22"
 
 /* getopt(3) optstring; shared by parse_options() and find_target() */
 #define OPTSTRING	"AacdDEeFf:hij:k:lLnNqR:svVx"
@@ -196,6 +196,11 @@ int		 sysctl_writable(const char *_name, const char *_value,
 		    int _quiet_unknown);
 #endif
 
+/* sysconf-targets.c */
+const char	*resolve_keyword(const char *_token,
+		    const char *const *_keywords, const char *_what);
+const char	*target_resolve(int _argc, char **_argv);
+
 /* sysconf.c */
 void		 usage(void);
 void		 help(void);
diff --git a/usr.sbin/sysconf/tests/sysconf_test.sh b/usr.sbin/sysconf/tests/sysconf_test.sh
index b81379474cf7..0914e8727d2f 100644
--- a/usr.sbin/sysconf/tests/sysconf_test.sh
+++ b/usr.sbin/sysconf/tests/sysconf_test.sh
@@ -438,6 +438,54 @@ max_bytes_cap_cleanup()
 	cleanup_root
 }
 
+atf_test_case target_prefix cleanup
+target_prefix_head()
+{
+	atf_set "descr" \
+		"Unique target prefixes resolve; ambiguous and unknown fail"
+}
+target_prefix_body()
+{
+	find_sysconf
+	setup_root
+	:> "$ROOT/etc/make.conf"
+	:> "$ROOT/etc/src-env.conf"
+	:> "$ROOT/etc/src.conf"
+	printf 'foo=1\n' > "$ROOT/etc/make.conf"
+
+	# Unique prefixes
+	atf_check -o inline:'foo: 1\n' "$SYSCONF" m -R "$ROOT" foo
+	atf_check -o inline:'foo: 1\n' "$SYSCONF" ma -R "$ROOT" foo
+	atf_check -o inline:'foo: 1\n' "$SYSCONF" make -R "$ROOT" foo
+	atf_check -o inline:'foo: 1\n' "$SYSCONF" sr -R "$ROOT" foo
+	printf 'kern.foo=1\n' > "$ROOT/etc/sysctl.conf"
+	atf_check -o inline:'kern.foo: 1\n' "$SYSCONF" sy -R "$ROOT" kern.foo
+	# generic requires -f
+	atf_check -s not-exit:0 -e match:'no default files' \
+	    "$SYSCONF" g -R "$ROOT" foo
+	atf_check -o inline:'foo: 1\n' \
+	    "$SYSCONF" g -R "$ROOT" -f /etc/make.conf foo
+
+	# Ambiguous: src and sysctl
+	atf_check -s not-exit:0 \
+	    -e match:'ambiguous target' \
+	    -e match:'src' \
+	    -e match:'sysctl' \
+	    "$SYSCONF" s foo
+	# Exact still wins
+	atf_check -o inline:'foo: 1\n' "$SYSCONF" src -R "$ROOT" foo
+	# Infix is not a prefix
+	atf_check -s not-exit:0 -e match:'unknown target' \
+	    "$SYSCONF" ctl foo
+	# src-env is not a prefix of src
+	atf_check -s not-exit:0 -e match:'src-env' -e match:'src' \
+	    "$SYSCONF" src-env foo
+}
+target_prefix_cleanup()
+{
+	cleanup_root
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case make_src_knobs
@@ -451,4 +499,5 @@ atf_init_test_cases()
 	atf_add_test_case equal_value_nocheck_mtime
 	atf_add_test_case sysctl_oid_range
 	atf_add_test_case max_bytes_cap
+	atf_add_test_case target_prefix
 }