summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordana <dana@dana.is>2026-04-29 23:37:15 -0500
committerdana <dana@dana.is>2026-04-29 23:37:29 -0500
commit88d51a240019d094fcf533d54b31c4fb14e87e81 (patch)
tree4eb43d7cb3546bd299d1b3d9aefdb781b5365087
parent54381: zparseopts -M: use last as-given option name in arrays (diff)
downloadzsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar.gz
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar.bz2
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar.lz
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar.xz
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.tar.zst
zsh-88d51a240019d094fcf533d54b31c4fb14e87e81.zip
54376: zparseopts: use standard option parsing
-rw-r--r--ChangeLog4
-rw-r--r--Doc/Zsh/mod_zutil.yo23
-rw-r--r--NEWS3
-rw-r--r--README4
-rw-r--r--Src/Modules/zutil.c191
-rw-r--r--Test/V12zparseopts.ztst57
6 files changed, 126 insertions, 156 deletions
diff --git a/ChangeLog b/ChangeLog
index 41eea8dfc..883929175 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
2026-04-29 dana <dana@dana.is>
+ * 54376: Doc/Zsh/mod_zutil.yo, NEWS, README,
+ Src/Modules/zutil.c, Test/V12zparseopts.ztst: zparseopts: use
+ standard option parsing
+
* 54381: Doc/Zsh/mod_zutil.yo, README, Src/Modules/zutil.c,
Test/V12zparseopts.ztst: zparseopts -M: use last as-given
option name in arrays
diff --git a/Doc/Zsh/mod_zutil.yo b/Doc/Zsh/mod_zutil.yo
index 6e5403d70..62f87fc28 100644
--- a/Doc/Zsh/mod_zutil.yo
+++ b/Doc/Zsh/mod_zutil.yo
@@ -230,7 +230,7 @@ item(tt(zregexparse))(
This implements some internals of the tt(_regex_arguments) function.
)
findex(zparseopts)
-item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-G) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-n) var(name) ] [ tt(-v) var(argv) ] [ tt(-) ] var(spec) ...)(
+item(tt(zparseopts) [ tt(-DEFGKM) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-n) var(name) ] [ tt(-v) var(argv) ] [ tt(-) ] var(spec) ...)(
This builtin simplifies the parsing of options in positional parameters,
i.e. the set of arguments given by tt($*). Each var(spec) describes one
option and must be of the form `var(opt)[tt(=)var(array)]'. If an option
@@ -241,7 +241,7 @@ should be declared as a normal array and never as an associative array.
At least one var(spec) must be given. If a single spec is given which
is either an empty string or a single hyphen (as in
-tt(zparseopts -D -F '')), it signifies that no options are recognised.
+tt(zparseopts -DF '')), it signifies that no options are recognised.
(This is useful for future-proofing functions that might gain options
later.) Otherwise, it is an error to give any var(spec) without an
`tt(=)var(array)' unless one of the tt(-a) or tt(-A) options is used.
@@ -302,14 +302,9 @@ ambiguities may arise when at least one overlapping var(spec) takes an
argument, as in `tt(-foo: -foobar)'. In that case, the last matching
var(spec) wins. (This is not an issue with GNU-style parsing.)
-The options of tt(zparseopts) itself cannot be stacked because, for
-example, the stack `tt(-DEK)' is indistinguishable from a var(spec) for
-the GNU-style long option `tt(-)tt(-DEK)'.
-
-Nevertheless, it is strongly recommended to distinguish GNU-style long
-option var(spec)s from tt(zparseopts)'s own options by preceding them by
-a `tt(-)', `tt(--)', or short option tt(spec), as in
-tt(zparseopts -a opts - -foo). In the future this may become mandatory.
+In order to distinguish GNU-style long option var(spec)s from
+tt(zparseopts)'s own options, they em(must) be preceded by a `tt(-)',
+`tt(--)', or short option tt(spec), as in tt(zparseopts -Ga opts - -foo).
The options of tt(zparseopts) itself are:
@@ -452,4 +447,12 @@ to have the effect of
example(foo=(-a)
bar=(-a '' -b xyz))
)
+
+The following is a useful general-purpose pattern:
+
+example(local -A opts
+zparseopts -DFGA opts - x -long: || return
+(( $+opts[-x] )) && ... # handle -x
+(( $+opts[--long] )) && ... # handle --long, optarg in $opts[--long]
+)
enditem()
diff --git a/NEWS b/NEWS
index f18cb7a76..f09c11c39 100644
--- a/NEWS
+++ b/NEWS
@@ -66,6 +66,9 @@ The zsh/zutil module's zparseopts builtin learnt the following:
- a single empty option spec signifies that no options are recognised
+Also, zparseopts now uses standard argument parsing for its own options,
+enabling support for option stacking (-DFG instead of -D -F -G).
+
A new contrib function zgetopt was added. It wraps `zparseopts -G` to
provide an interface similar to util-linux's getopt(1).
diff --git a/README b/README
index 2780da583..8d686a77c 100644
--- a/README
+++ b/README
@@ -145,6 +145,10 @@ The zsh/zutil module's zparseopts builtin now always uses the last
option name as given when storing it in an array. (Previously, options
mapped with -M would sometimes be stored using the first name given.)
+Also, as a consequence of the zparseopts builtin now using standard
+argument parsing for its own options, long-option specs must be guarded
+using -- or similar.
+
Incompatibilities between 5.8.1 and 5.9
---------------------------------------
diff --git a/Src/Modules/zutil.c b/Src/Modules/zutil.c
index 72cc5ee54..7be25d306 100644
--- a/Src/Modules/zutil.c
+++ b/Src/Modules/zutil.c
@@ -1735,12 +1735,12 @@ zalloc_default_array(char ***aval, char *assoc, int keep, int num)
}
static int
-bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
+bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))
{
char *o, *p, *n, **pp, **aval, **ap, *assoc = NULL, **cp, **np;
char *paramsname = NULL, **params;
char *progname = scriptname ? scriptname : (argzero ? argzero : nam);
- int del = 0, flags = 0, extract = 0, fail = 0, gnu = 0, keep = 0;
+ int flags = 0, del, extract, fail, gnu, keep;
Zoptdesc sopts[256], d;
Zoptarr a, defarr = NULL;
Zoptval v;
@@ -1749,144 +1749,57 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
opt_arrs = NULL;
memset(sopts, 0, 256 * sizeof(Zoptdesc));
- while ((o = *args++)) {
- if (*o == '-') {
- switch (o[1]) {
- case '\0':
- o = NULL;
- break;
- case '-':
- if (o[2])
- args--;
- /* else unreachable, default parsing removes "--" */
- o = NULL;
- break;
- case 'D':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- del = 1;
- break;
- case 'E':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- extract = 1;
- break;
- case 'F':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- fail = 1;
- break;
- case 'G':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- gnu = 1;
- break;
- case 'K':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- keep = 1;
- break;
- case 'M':
- if (o[2]) {
- args--;
- o = NULL;
- break;
- }
- flags |= ZOF_MAP;
- break;
- case 'n':
- if (o[2])
- progname = o + 2;
- else if (*args)
- progname = *args++;
- else {
- zwarnnam(nam, "missing program name");
- return 1;
- }
- break;
- case 'a':
- if (defarr) {
- zwarnnam(nam, "default array given more than once");
- return 1;
- }
- if (o[2])
- n = o + 2;
- else if (*args)
- n = *args++;
- else {
- zwarnnam(nam, "missing array name");
- return 1;
- }
- defarr = (Zoptarr) zhalloc(sizeof(*defarr));
- defarr->name = n;
- defarr->num = 0;
- defarr->vals = defarr->last = NULL;
- defarr->next = NULL;
- opt_arrs = defarr;
- break;
- case 'A':
- if (assoc) {
- zwarnnam(nam, "associative array given more than once");
- return 1;
- }
- if (o[2])
- assoc = o + 2;
- else if (*args)
- assoc = *args++;
- else {
- zwarnnam(nam, "missing array name");
- return 1;
- }
- break;
- case 'v':
- if (paramsname) {
- zwarnnam(nam, "argv array given more than once");
- return 1;
- }
- if (o[2])
- paramsname = o + 2;
- else if (*args)
- paramsname = *args++;
- else {
- zwarnnam(nam, "missing array name");
- return 1;
- }
- break;
- default:
- /* Anything else is an option description */
- args--;
- o = NULL;
- break;
- }
- if (!o) {
- o = "";
- break;
- }
- } else {
- args--;
- break;
+ del = OPT_ISSET(ops, 'D');
+ extract = OPT_ISSET(ops, 'E');
+ fail = OPT_ISSET(ops, 'F');
+ gnu = OPT_ISSET(ops, 'G');
+ keep = OPT_ISSET(ops, 'K');
+ flags |= OPT_ISSET(ops, 'M') ? ZOF_MAP : 0;
+
+ if (OPT_ISSET(ops, 'a')) {
+ if (!*OPT_ARG(ops, 'a')) {
+ zwarnnam(nam, "missing array name for -a");
+ return 1;
}
+ defarr = (Zoptarr) zhalloc(sizeof(*defarr));
+ defarr->name = OPT_ARG(ops, 'a');
+ defarr->num = 0;
+ defarr->vals = defarr->last = NULL;
+ defarr->next = NULL;
+ opt_arrs = defarr;
+ }
+ if (OPT_ISSET(ops, 'A')) {
+ if (!*OPT_ARG(ops, 'A')) {
+ zwarnnam(nam, "missing array name for -A");
+ return 1;
+ }
+ assoc = OPT_ARG(ops, 'A');
+ }
+ if (OPT_ISSET(ops, 'n')) {
+ if (!*OPT_ARG(ops, 'n')) {
+ zwarnnam(nam, "missing program name for -n");
+ return 1;
+ }
+ progname = OPT_ARG(ops, 'n');
+ }
+ if (OPT_ISSET(ops, 'v')) {
+ if (!*OPT_ARG(ops, 'v')) {
+ zwarnnam(nam, "missing array name for -v");
+ return 1;
+ }
+ paramsname = OPT_ARG(ops, 'v');
+ }
+
+ params = getaparam((paramsname = paramsname ? paramsname : "argv"));
+ if (!params) {
+ zwarnnam(nam, "no such array: %s", paramsname);
+ return 1;
}
/* allow a single '' or - spec to signify no options recognised */
- if (o && *args && !args[1] && (!**args || !strcmp(*args, "-"))) {
+ if (*args && !args[1] && (!**args || !strcmp(*args, "-"))) {
args++;
- } else if (!o) {
+ } else if (!*args) {
zwarnnam(nam, "missing option descriptions");
return 1;
}
@@ -1968,11 +1881,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
return 1;
}
}
- params = getaparam((paramsname = paramsname ? paramsname : "argv"));
- if (!params) {
- zwarnnam(nam, "no such array: %s", paramsname);
- return 1;
- }
+
np = cp = pp = ((extract && del) ? arrdup(params) : params);
for (; (o = *pp); pp++) {
/* Not an option. With GNU style, this includes '-' */
@@ -2150,7 +2059,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
static struct builtin bintab[] = {
BUILTIN("zformat", 0, bin_zformat, 3, -1, 0, NULL, NULL),
- BUILTIN("zparseopts", 0, bin_zparseopts, 1, -1, 0, NULL, NULL),
+ BUILTIN("zparseopts", 0, bin_zparseopts, 0, -1, 0, "a:A:DEFGKMn:v:", NULL),
BUILTIN("zregexparse", 0, bin_zregexparse, 3, -1, 0, "c", NULL),
BUILTIN("zstyle", 0, bin_zstyle, 0, -1, 0, NULL, NULL),
};
diff --git a/Test/V12zparseopts.ztst b/Test/V12zparseopts.ztst
index 17b0a01d2..ede45eb7e 100644
--- a/Test/V12zparseopts.ztst
+++ b/Test/V12zparseopts.ztst
@@ -34,15 +34,15 @@
?(anon): bad option: -x
>-x
- () { zparseopts -F } -x
- () { zparseopts -F - } -x
() { zparseopts -F -- } -x
+ () { zparseopts -F - } -x
+ () { zparseopts -F - a } -x
() { zparseopts -F - a } -x
() { zparseopts -F a } -x
-1:zparseopts without option specs, without array (may change in future)
+1:zparseopts without option specs, without array
+?(anon):zparseopts: missing option descriptions
?(anon):zparseopts: missing option descriptions
-?(anon): bad option: -x
-?(anon): bad option: -x
+?(anon):zparseopts: no default array defined: a
?(anon):zparseopts: no default array defined: a
?(anon):zparseopts: no default array defined: a
@@ -446,3 +446,50 @@
?fn2: bad option: -x
?prog: bad option: -x
?prog: bad option: -x
+
+ () { zparseopts -DF - '' } -x
+ () {
+ local -a optv
+ zparseopts -DFa optv - a
+ typeset -p optv
+ } -a
+0:zparseopts option stacking
+?(anon): bad option: -x
+>typeset -a optv=( -a )
+
+ () {
+ local -a optv
+ zparseopts -DFa optv -x
+ }
+ () {
+ local -a optv
+ zparseopts -DFa optv - -D
+ typeset -p optv
+ } --D
+ () {
+ local -a optv
+ zparseopts -DFa optv - -DFa
+ typeset -p optv
+ } --DFa
+0:zparseopts long-option spec guarding
+?(anon):zparseopts:2: bad option: -x
+>typeset -a optv=( --D )
+>typeset -a optv=( --DFa )
+
+ () { zparseopts -a }
+ () { zparseopts -A }
+ () { zparseopts -n }
+ () { zparseopts -v }
+ () { zparseopts -a '' }
+ () { zparseopts -A '' }
+ () { zparseopts -n '' }
+ () { zparseopts -v '' }
+1:zparseopts missing/empty optargs
+?(anon):zparseopts: argument expected: -a
+?(anon):zparseopts: argument expected: -A
+?(anon):zparseopts: argument expected: -n
+?(anon):zparseopts: argument expected: -v
+?(anon):zparseopts: missing array name for -a
+?(anon):zparseopts: missing array name for -A
+?(anon):zparseopts: missing program name for -n
+?(anon):zparseopts: missing array name for -v