Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
[PATCH] zformat: replace -qn by spec quoting indicator syntax
- X-seq: zsh-workers 54767
- From: dana <dana@xxxxxxx>
- To: zsh-workers@xxxxxxx
- Subject: [PATCH] zformat: replace -qn by spec quoting indicator syntax
- Date: Sun, 14 Jun 2026 03:59:54 +0000
- Archived-at: <https://zsh.org/workers/54767>
- Feedback-id: i9be146f9:Fastmail
- List-id: <zsh-workers.zsh.org>
i'd been thinking more about how to solve the %-quoting problems in
completion (w/54573) and i decided that the -qn functionality i added to
zformat (w/54602) is not generic enough -- there's no value of n that
_description could use that works for all helpers. we could make it so
that you could pass the n down somehow, but here's a different approach
which incorporates an idea from mikael again
instead of index-based quoting with -qn, allow each spec to explicitly
indicate its quoting behaviour with the syntax %d: (quoted) and %%d:
(unquoted). this is similar to how you can toggle the effect of certain
options in parameter expansion (e.g. ^ and ^^). so:
# without -qQ: don't quote by default
% zformat -F REPLY '%a, %b, %c, %d, %%e' a:%aaa% %b:%bbb% %%c:%ccc%
% echo $REPLY
%aaa%, %%bbb%%, %ccc%, %d, %e
# with -Q: quote specs by default, don't leave quotes in format string
% zformat -QF REPLY '%a, %b, %c, %d, %%e' a:%aaa% %b:%bbb% %%c:%ccc%
% echo $REPLY
%%aaa%%, %%bbb%%, %ccc%, %d, %e
# with -q: quote specs by default, leave quotes in format string
% zformat -qF REPLY '%a, %b, %c, %d, %%e' a:%aaa% %b:%bbb% %%c:%ccc%
% echo $REPLY
%%aaa%%, %%bbb%%, %ccc%, %d, %%e
another patch will follow to make use of this in completion
dana
diff --git a/Doc/Zsh/mod_zutil.yo b/Doc/Zsh/mod_zutil.yo
index 50d4576af..11fe8a073 100644
--- a/Doc/Zsh/mod_zutil.yo
+++ b/Doc/Zsh/mod_zutil.yo
@@ -158,8 +158,8 @@ var(pattern) matches at least one of the strings in the value.
enditem()
)
findex(zformat)
-xitem(tt(zformat -f) [ tt(-qQ) [ var(n) ] ] var(param) var(format) var(spec) ...)
-xitem(tt(zformat -F) [ tt(-qQ) [ var(n) ] ] var(param) var(format) var(spec) ...)
+xitem(tt(zformat -f) [ tt(-qQ) ] ] var(param) var(format) var(spec) ...)
+xitem(tt(zformat -F) [ tt(-qQ) ] ] var(param) var(format) var(spec) ...)
item(tt(zformat -a) var(array) var(sep) var(spec) ...)(
This builtin provides different forms of formatting.
@@ -180,6 +180,11 @@ can be achieved by giving a negative minimum field width. If a maximum
field width is specified, the var(string) will be truncated after that
many characters.
+The var(char) in each var(spec) may optionally be preceded by an
+escaping indicator, either `tt(%)' to explicitly enable the
+var(spec)-escaping behaviour of tt(-q) described below, or `tt(%%)' to
+explicitly disable it. The default behaviour is to em(not) escape specs.
+
Any `tt(%)' sequence in tt(format) that does not match a given var(spec)
(or one of the special sequences described below) is output as-is. If
desired, these sequences may be processed by a second round of
@@ -222,8 +227,8 @@ number the condition is true when the width is em(greater than) that
number, and with a negative number the condition is true when the width
is em(less than or equal to) the absolute value of that number.
-With tt(-q), `tt(%)' characters in the var(spec)s are escaped as they
-are inserted into the formatted string, and pre-escaped `tt(%)' and
+With tt(-q), `tt(%)' characters in the var(spec)s are escaped by default
+as they are inserted into the formatted string, and pre-escaped `tt(%)' and
`tt(RPAR())` characters in the format string are left as they are. For
example:
@@ -235,14 +240,6 @@ the tt(%B)...tt(%b) sequences could be used with prompt expansion to
produce bold text. One notable use case is formatting a description to
be passed to tt(compadd -x) in a completion function.
-tt(-q) may be followed by an optional integer argument var(n) to escape
-only the first var(n) var(spec)s. For example,
-
-example(zformat -Fq1 REPLY '%D %d' d:%foo% D:%bar%)
-
-outputs `tt(%bar% %%foo%%)' to tt(REPLY). This is the only case where
-the order that var(spec)s are given in is significant.
-
Since the output with tt(-q) is expected to be subject to further
processing, width specifiers don't count the extra escape characters,
ensuring that the widths are correct em(after) that processing.
@@ -252,6 +249,13 @@ against the em(pre-escaped) spec lengths.
tt(-Q) is like tt(-q) except that it interprets pre-escaped `tt(%)' and
`tt(RPAR())` characters in the format string as normal.
+The optional escaping indicator can be used to exempt individual
+var(spec)s from escaping as described above. For example:
+
+example(zformat -qF REPLY '%d %D' d:%foo% %%D:%bar%)
+
+outputs `tt(%%foo%% %bar%)' to tt(REPLY).
+
The form using the tt(-a) option can be used for aligning
strings. Here, the var(spec)s are of the form
`var(left)tt(:)var(right)' where `var(left)' and `var(right)' are
diff --git a/Src/Modules/zutil.c b/Src/Modules/zutil.c
index add055460..0b412c50a 100644
--- a/Src/Modules/zutil.c
+++ b/Src/Modules/zutil.c
@@ -984,21 +984,12 @@ static int
bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))
{
unsigned char qopt = OPT_ISSET(ops, 'q') ? 'q' : OPT_ISSET(ops, 'Q') ? 'Q' : 0;
- int presence = 0, quote = INT_MAX;
+ int presence = 0;
if (OPT_ISSET(ops, 'q') && OPT_ISSET(ops, 'Q')) {
zwarnnam(nam, "only one of -qQ allowed");
return 1;
}
- // the error here is more meaningful than the following ones with e.g. -q1F
- if (OPT_HASARG(ops, qopt)) {
- char *qptr;
- quote = (int) zstrtol(OPT_ARG(ops, qopt), &qptr, 10);
- if (quote < 0 || *qptr) {
- zwarnnam(nam, "bad argument to -%c: %s", qopt, OPT_ARG(ops, qopt));
- return 1;
- }
- }
if (OPT_ISSET(ops, 'a') + OPT_ISSET(ops, 'f') + OPT_ISSET(ops, 'F') < 1) {
zwarnnam(nam, "one of -afF expected");
return 1;
@@ -1007,8 +998,8 @@ bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))
zwarnnam(nam, "only one of -afF allowed");
return 1;
}
- if (OPT_ISSET(ops, 'a') && OPT_ISSET(ops, 'q')) {
- zwarnnam(nam, "-q not allowed with -a");
+ if (OPT_ISSET(ops, 'a') && qopt) {
+ zwarnnam(nam, "-qQ not allowed with -a");
return 1;
}
@@ -1018,26 +1009,37 @@ bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))
/* fall-through */
case 'f':
{
- char **ap, *specs[256] = {0}, *out;
- int i, olen, oused = 0;
+ char **ap, *specs[256] = {0}, *out, *arg;
+ int quote, olen, oused = 0;
int qspecs[256] = {0};
/* Parse the specs in argv. */
- for (i = 1, ap = args + 2; *ap; i++, ap++) {
- if (!ap[0][0] || ap[0][0] == '-' || ap[0][0] == '.' ||
- ap[0][0] == '%' || ap[0][0] == ')' ||
- idigit(ap[0][0]) || ap[0][1] != ':') {
+ for (ap = args + 2; (arg = *ap); ap++) {
+ // quote by default (spec like d:...) with -qQ
+ quote = !!qopt;
+
+ // spec like %%d:... -- explicitly disable quoting
+ if (arg[0] == '%' && arg[1] == '%') {
+ quote = 0;
+ arg += 2;
+ // spec like %d:... -- explicitly enable quoting
+ } else if (arg[0] == '%') {
+ quote = 1;
+ arg += 1;
+ }
+
+ if (!arg[0] || arg[0] == '-' || arg[0] == '.' ||
+ arg[0] == '%' || arg[0] == ')' ||
+ idigit(arg[0]) || arg[1] != ':') {
zwarnnam(nam, "invalid spec: %s", *ap);
return 1;
}
- // need to quote specs here because zformat_substring() won't
- // know the order
- if (qopt && quote >= i) {
+ if (quote) {
int len = 0, pct = 0;
- char *aptr, *sptr, *spec = *ap + 2;
+ char *aptr, *sptr, *spec = arg + 2;
- for (aptr = *ap + 2; *aptr; aptr++, len++) {
+ for (aptr = arg + 2; *aptr; aptr++, len++) {
if (*aptr == '%') {
len++, pct++;
}
@@ -1046,7 +1048,7 @@ bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))
if (pct) {
spec = (char *) zhalloc(len + 1);
sptr = spec;
- for (aptr = *ap + 2; *aptr; aptr++) {
+ for (aptr = arg + 2; *aptr; aptr++) {
*sptr++ = *aptr;
if (*aptr == '%') {
*sptr++ = *aptr;
@@ -1055,10 +1057,10 @@ bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))
*sptr = '\0';
}
- specs[(unsigned char) ap[0][0]] = spec;
- qspecs[(unsigned char) ap[0][0]] = pct;
+ specs[(unsigned char) *arg] = spec;
+ qspecs[(unsigned char) *arg] = pct;
} else {
- specs[(unsigned char) ap[0][0]] = ap[0] + 2;
+ specs[(unsigned char) *arg] = arg + 2;
}
}
@@ -2141,7 +2143,7 @@ bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))
}
static struct builtin bintab[] = {
- BUILTIN("zformat", 0, bin_zformat, 2, -1, 0, "afFq:%Q:%", NULL),
+ BUILTIN("zformat", 0, bin_zformat, 2, -1, 0, "afFqQ", 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/V13zformat.ztst b/Test/V13zformat.ztst
index 449b96338..63654bb27 100644
--- a/Test/V13zformat.ztst
+++ b/Test/V13zformat.ztst
@@ -275,35 +275,32 @@
zformat -qa reply .
zformat -aq reply .
-1:-a + -q not allowed
-?(eval):zformat:1: -q not allowed with -a
-?(eval):zformat:2: -q not allowed with -a
+ zformat -aQ reply .
+1:-a + -qQ not allowed
+?(eval):zformat:1: -qQ not allowed with -a
+?(eval):zformat:2: -qQ not allowed with -a
+?(eval):zformat:3: -qQ not allowed with -a
zformat -Fq REPLY F && print -r - $REPLY
zformat -qF REPLY F && print -r - $REPLY
zformat -FqF REPLY F && print -r - $REPLY
- zformat -q1 -F REPLY F && print -r - $REPLY
- zformat -q1F REPLY F && print -r - $REPLY
- zformat -q-1 -F REPLY F && print -r - $REPLY
-1:optional argument to -q
+ zformat -qqF REPLY F && print -r - $REPLY
+ zformat -qQF REPLY F && print -r - $REPLY
+1:more than one of -qQ not allowed
>F
>F
>F
>F
-?(eval):zformat:5: bad argument to -q: 1F
-?(eval):zformat:6: bad option: --
+?(eval):zformat:5: only one of -qQ allowed
-# the spec order in the format string differs from the order in the arguments
-# here to make sure we're testing -qn's effects on the latter
- for 1 in '' 0 1 2 3; do
- zformat -Fq$1 REPLY '%%x %) %. %X %D %d' d:%foo% D:%bar% && print -r - $REPLY
+ for 1 in -F -FQ -Fq; do
+ zformat $1 REPLY '%a, %b, %c, %d, %%e' a:%aaa% %b:%bbb% %%c:%ccc%
+ print -r - $REPLY
done
-0:-q with and without optarg
->%%x %) %. %X %%bar%% %%foo%%
->%%x %) %. %X %bar% %foo%
->%%x %) %. %X %bar% %%foo%%
->%%x %) %. %X %%bar%% %%foo%%
->%%x %) %. %X %%bar%% %%foo%%
+0:spec quoting indicators, -Q vs -q
+>%aaa%, %%bbb%%, %ccc%, %d, %e
+>%%aaa%%, %%bbb%%, %ccc%, %d, %e
+>%%aaa%%, %%bbb%%, %ccc%, %d, %%e
zformat -Fq REPLY '%(x.%%/%d.%%/%D)' x:1 d:%foo% D:%bar% && print -r - $REPLY
zformat -Fq REPLY '%(X.%%/%d.%%/%D)' x:1 d:%foo% D:%bar% && print -r - $REPLY
@@ -340,11 +337,3 @@
0:-q: ternary width test ignores extra %s
>t f f
>t t f
-
- zformat -FQ REPLY '%%x %) %. %X %D %d' d:%foo% D:%bar% && print -r - $REPLY
- zformat -FQ0 REPLY '%%x %) %. %X %D %d' d:%foo% D:%bar% && print -r - $REPLY
- zformat -FQ1 REPLY '%%x %) %. %X %D %d' d:%foo% D:%bar% && print -r - $REPLY
-0:-Q, -Q0, -Q1
->%x ) %. %X %%bar%% %%foo%%
->%x ) %. %X %bar% %foo%
->%x ) %. %X %bar% %%foo%%
Messages sorted by:
Reverse Date,
Date,
Thread,
Author