git: 439eb9c9771b - main - sysconf: accept a unique prefix of a target keyword
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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
}