Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
[PATCH] compvalues: support leading '!' in spec, handle escaping better
- X-seq: zsh-workers 54451
- From: dana <dana@xxxxxxx>
- To: zsh-workers@xxxxxxx
- Subject: [PATCH] compvalues: support leading '!' in spec, handle escaping better
- Date: Sat, 02 May 2026 15:30:38 -0500
- Archived-at: <https://zsh.org/workers/54451>
- Feedback-id: i9be146f9:Fastmail
- List-id: <zsh-workers.zsh.org>
the docs imply that a leading '!' in a _values spec should work the same
way it does in an _arguments spec. but it does not. it would be nice if
it did, because _values stops completing a sequence as soon as it sees a
value it doesn't recognise
in adding that i realised that specs with escaped value names only
partially work -- they can't be matched correctly because we compare the
escaped name from the spec to the un-escaped word from the command line
so this patch adds the '!' syntax, fixes the escaping inconsistencies,
and adds tests for _values
i don't think the escaping change is an issue for existing functions, it
should only fix the matching
the '!' syntax is a 'breaking' change, but i'm not worried about it
because (a) the current behaviour contradicts the documentation, (b)
it's unusual to have a leading '!' in a value name, and (c) the only
negative consequence is that the value will be hidden. i will fix any
functions in the distribution that are impacted (only one afaict)
dana
diff --git a/README b/README
index 8d686a77c..b6408d882 100644
--- a/README
+++ b/README
@@ -149,6 +149,11 @@ 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.
+The _values completion helper now understands the leading '!' spec form
+supported by _arguments, bringing it in line with the documentation. As
+a consequence, a leading '!' in a value name must now be escaped if it
+should be taken literally, as in: _values desc '!hidden' '\!literal'
+
Incompatibilities between 5.8.1 and 5.9
---------------------------------------
diff --git a/Src/Zle/computil.c b/Src/Zle/computil.c
index 55b0a9b9f..74370d0fc 100644
--- a/Src/Zle/computil.c
+++ b/Src/Zle/computil.c
@@ -1079,6 +1079,28 @@ bslashcolon(char *s)
return r;
}
+/* add backslashes before colons *and* other backslashes. this is suitable for
+ * re-escaping something that was un-escaped with rembslash(). basically it
+ * ensures that _describe handles a literal '\' correctly */
+
+static char *
+bslashcolon2(char *s)
+{
+ char *p, *r;
+
+ r = p = zhalloc((2 * strlen(s)) + 1);
+
+ while (*s) {
+ if (*s == ':' || *s == '\\')
+ *p++ = '\\';
+ *p++ = *s++;
+ }
+ *p = '\0';
+
+ return r;
+}
+
+
/* Get an index into the single array used in struct cadef
* opt is the option letter and pre is either - or +
* we only keep an array for the 94 ASCII characters from ! to ~ so
@@ -2944,6 +2966,7 @@ struct cvval {
int type; /* CVV_* below */
Caarg arg; /* argument definition */
int active; /* still allowed */
+ int not; /* don't complete this value (`!...') */
};
#define CVV_NOARG 0
@@ -2989,7 +3012,7 @@ parse_cvdef(char *nam, char **args)
Cvval val, *valp;
Caarg arg;
char **oargs = args, sep = '\0', asep = '=', *name, *descr, *p, *q, **xor, c;
- int xnum, multi, vtype, hassep = 0, words = 0;
+ int xnum, multi, vtype, hassep = 0, words = 0, not = 0;
while (args && args[0] && args[1] &&
args[0][0] == '-' &&
@@ -3031,6 +3054,8 @@ parse_cvdef(char *nam, char **args)
p = dupstring(*args);
xnum = 0;
+ if ((not = (*p == '!')))
+ p++;
/* xor list? */
if (*p == '(') {
LinkList list = newlinklist();
@@ -3049,7 +3074,7 @@ parse_cvdef(char *nam, char **args)
sav = *p;
*p = '\0';
- addlinknode(list, dupstring(q));
+ addlinknode(list, rembslash(q));
xnum++;
*p = sav;
}
@@ -3133,17 +3158,18 @@ parse_cvdef(char *nam, char **args)
xor = (char **) zalloc(2 * sizeof(char *));
xor[1] = NULL;
}
- xor[xnum] = ztrdup(name);
+ xor[xnum] = ztrdup(rembslash(name));
}
*valp = val = (Cvval) zalloc(sizeof(*val));
valp = &((*valp)->next);
val->next = NULL;
- val->name = ztrdup(name);
+ val->name = ztrdup(rembslash(name));
val->descr = ztrdup(descr);
val->xor = xor;
val->type = vtype;
val->arg = arg;
+ val->not = not;
}
return ret;
}
@@ -3571,21 +3597,23 @@ bin_compvalues(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
char *str;
for (p = cv_laststate.d->vals; p; p = p->next) {
- if (p->active) {
+ if (p->active && !p->not) {
switch (p->type) {
case CVV_NOARG: l = noarg; break;
case CVV_ARG: l = arg; break;
default: l = opt; break;
}
if (p->descr) {
- int len = strlen(p->name) + strlen(p->descr) + 2;
+ // see note on bslashcolon2()
+ char *n = bslashcolon2(p->name);
+ size_t len = strlen(n) + strlen(p->descr) + 2;
str = (char *) zhalloc(len);
- strcpy(str, p->name);
+ strcpy(str, n);
strcat(str, ":");
strcat(str, p->descr);
} else
- str = p->name;
+ str = bslashcolon2(p->name);
addlinknode(l, str);
}
}
diff --git a/Test/Y06values.ztst b/Test/Y06values.ztst
new file mode 100644
index 000000000..b80cc2927
--- /dev/null
+++ b/Test/Y06values.ztst
@@ -0,0 +1,238 @@
+# tests for _values
+
+%prep
+
+ if ( zmodload -s zsh/zpty ); then
+ source $ZTST_srcdir/comptest
+ mkdir comp.tmp
+ cd comp.tmp
+ comptestinit -z $ZTST_testdir/../Src/zsh && {
+ comptesteval 'compdef _tst tst'
+ tst_values() { comptesteval "_tst() { _values ${${(@q+)@}} }" }
+ }
+ else
+ ZTST_unimplemented='the zsh/zpty module is not available'
+ fi
+
+%test
+
+ tst_values desc a b
+ comptest $'tst \t'
+0:basic value
+>line: {tst }{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{b}
+
+ tst_values desc a b
+ comptest $'tst a \t'
+0:basic value, next word
+>line: {tst a }{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{b}
+
+ tst_values desc 'a[adesc]' 'b[bdesc]'
+ comptest $'tst \t'
+0:basic value with description
+>line: {tst }{}
+>DESCRIPTION:{desc}
+>NO:{a -- adesc}
+>NO:{b -- bdesc}
+
+ tst_values desc 'a\[b' '\!x' '\*y' '\(z' '\\\.\:\]\&'
+ comptest $'tst \t'
+0:display of escaped char in value name
+>line: {tst }{}
+>DESCRIPTION:{desc}
+>NO:{!x}
+>NO:{(z}
+>NO:{*y}
+>NO:{\.:]&}
+>NO:{a[b}
+
+ tst_values desc '!a' b c
+ comptest $'tst \t'
+0:display of hidden value
+>line: {tst }{}
+>DESCRIPTION:{desc}
+>NO:{b}
+>NO:{c}
+
+ tst_values -s, desc a b c
+ comptest $'tst b,\t'
+0:basic sequence
+>line: {tst b,}{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{c}
+
+ tst_values -s, desc a b c d e
+ comptest $'tst \'a,b,c,\t'
+ comptest $'tst "a,b,c,\t'
+0:sequence in quotes
+>line: {tst 'a,b,c,}{}
+>DESCRIPTION:{desc}
+>NO:{d}
+>NO:{e}
+>line: {tst "a,b,c,}{}
+>DESCRIPTION:{desc}
+>NO:{d}
+>NO:{e}
+
+ tst_values -s, desc '!a' b c
+ comptest $'tst a,\t'
+0:sequence continues after hidden value
+>line: {tst a,}{}
+>DESCRIPTION:{desc}
+>NO:{b}
+>NO:{c}
+
+ tst_values -s, desc '\*a' b c
+ comptest $'tst \\*a,\t'
+0:sequence continues after value with escaped meta char
+>line: {tst \*a,}{}
+>DESCRIPTION:{desc}
+>NO:{b}
+>NO:{c}
+
+ tst_values -s, desc '\:a' b c
+ comptest $'tst :a,\t'
+0:sequence continues after value with escaped non-meta char
+>line: {tst :a,}{}
+>DESCRIPTION:{desc}
+>NO:{b}
+>NO:{c}
+
+ tst_values -s, desc '*a' b c
+ comptest $'tst a,a,\t'
+0:multi value offered again
+>line: {tst a,a,}{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{b}
+>NO:{c}
+
+ tst_values -s, desc '(b c)a' b c d e
+ comptest $'tst a,\t'
+0:xor value not offered
+>line: {tst a,}{}
+>DESCRIPTION:{desc}
+>NO:{d}
+>NO:{e}
+
+ tst_values -s, desc '(d\:e)a' b c 'd\:e'
+ comptest $'tst a,\t'
+0:xor value with escaped char not offered
+>line: {tst a,}{}
+>DESCRIPTION:{desc}
+>NO:{b}
+>NO:{c}
+
+ tst_values desc 'a:val'
+ comptest $'tst a=\t'
+ tst_values desc 'a:val:'
+ comptest $'tst a=\t'
+0:value with argument not enumerated
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+
+ tst_values desc 'a:val:(x y z)'
+ comptest $'tst a=\t'
+0:value with argument enumerated
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x}
+>NO:{y}
+>NO:{z}
+
+# _arguments requires the ':'s to be escaped but i guess it's unnecessary here
+ tst_values desc 'a:val:((x:descx y:descy z:descz))'
+ comptest $'tst a=\t'
+0:value with argument enumerated with description
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x -- descx}
+>NO:{y -- descy}
+>NO:{z -- descz}
+
+# this is the syntax required by _arguments (see above)
+ tst_values desc 'a:val:((x\:descx y\:descy z\:descz))'
+ comptest $'tst a=\t'
+0:value with argument enumerated with description, escaped colon
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x -- descx}
+>NO:{y -- descy}
+>NO:{z -- descz}
+
+# should be the same as above
+ tst_values desc 'a:val:((x\\:descx y\\:descy z\\:descz))'
+ comptest $'tst a=\t'
+0:value with argument enumerated with description, escaped colon with \\
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x -- descx}
+>NO:{y -- descy}
+>NO:{z -- descz}
+
+ tst_values desc 'a:val:(("x\\:x":descx y\:descy z\:descz))'
+ comptest $'tst a=\t'
+0:value with argument enumerated with description, escaped colon in arg name
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x:x -- descx}
+>NO:{y -- descy}
+>NO:{z -- descz}
+
+ tst_values desc 'a:val:((x:x:descx y\:descy z\:descz))'
+ comptest $'tst a=\t'
+0:value with argument enumerated with description, un-escaped colon in arg desc
+>line: {tst a=}{}
+>DESCRIPTION:{val}
+>NO:{x -- x:descx}
+>NO:{y -- descy}
+>NO:{z -- descz}
+
+ tst_values desc 'a: :{ _message msg }'
+ comptest $'tst a=\t'
+0:value with argument eval string, braces
+>line: {tst a=}{}
+>MESSAGE:{msg}
+
+ tst_values desc 'a: : _message msg'
+ comptest $'tst a=\t'
+0:value with argument eval string, leading space
+>line: {tst a=}{}
+>MESSAGE:{msg}
+
+ tst_values -s: -S. desc 'a:val:(x y z)' b c
+ comptest $'tst b:a.\t'
+0:-s and -S
+>line: {tst b:a.}{}
+>DESCRIPTION:{val}
+>NO:{x}
+>NO:{y}
+>NO:{z}
+
+ tst_values -w desc a b c
+ comptest $'tst b \t'
+0:-w without -s
+>line: {tst b }{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{c}
+
+ tst_values -ws, desc a b c d
+ comptest $'tst b,c \t'
+0:-w with -s
+>line: {tst b,c }{}
+>DESCRIPTION:{desc}
+>NO:{a}
+>NO:{d}
+
+%clean
+
+ zmodload -ui zsh/zpty
Messages sorted by:
Reverse Date,
Date,
Thread,
Author