Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
PATCH 1/6: handle multibyte arguments in error messages
- X-seq: zsh-workers 54710
- From: Mikael Magnusson <mikachu@xxxxxxxxx>
- To: zsh-workers@xxxxxxx
- Subject: PATCH 1/6: handle multibyte arguments in error messages
- Date: Mon, 8 Jun 2026 22:45:01 +0200
- Archived-at: <https://zsh.org/workers/54710>
- List-id: <zsh-workers.zsh.org>
A lot of places check option strings char by char, then send that char
to zwarn/zerr, instead of reading out the full multibyte character.
Doing so is quite easy with unmeta_one in most cases, so just go ahead
and do that. The print functions in fact expect a wchar_t for %c anyway.
In some cases, the logic changes slightly because the pointer ends up at the next
element instead of the last byte of the current element which felt a little too
gross.
Also renames "unknown option" to "bad option" because that's what every
other place calls it (also one place was missing the colon, added that).
To reproduce the math errors, since it's slightly less obvious (you need
a multibyte character that is also an invalid parameter name, otherwise
it actually works fine)
% zsh -c '(( ¯ ))'
zsh:1: bad math expression: illegal character: ^
% zsh -c 'zmodload zsh/mathfunc; (( sin(1¯) ))'
zsh:1: bad math expression: illegal character: ^
% zsh -c 'の=5; echo $(( の ))' # unchanged because it is working
5
---
I also added some tests because B12 was looking very empty, not sure if
this is the correct way to handle the intersection of possibly not
compiled module with possibly not available UTF-8 locale, but X02 and
some others do a similar thing with ZSH_TEST_LANG, though in those cases
that's handled specially by comptest, not the test itself. If this seems like the
right way to do it, I can throw in some tests for the other stuff fixed here too.
Or maybe just add a test for the generic paths in D07multibyte?
Src/Builtins/rlimits.c | 25 +++++++++++++------------
Src/Builtins/sched.c | 7 ++++---
Src/Modules/stat.c | 3 ++-
Src/Modules/system.c | 3 ++-
Src/Modules/zftp.c | 5 ++++-
Src/Zle/complete.c | 13 ++++++++-----
Src/Zle/zle_thingy.c | 2 +-
Src/builtin.c | 33 +++++++++++++++++----------------
Src/exec.c | 3 ++-
Src/glob.c | 3 ++-
Src/math.c | 12 ++++++++----
Src/options.c | 28 +++++++++++++++-------------
Test/B12limit.ztst | 25 +++++++++++++++++++++++++
13 files changed, 103 insertions(+), 59 deletions(-)
diff --git a/Src/Builtins/rlimits.c b/Src/Builtins/rlimits.c
index e76c2118db..6f5d04dfb4 100644
--- a/Src/Builtins/rlimits.c
+++ b/Src/Builtins/rlimits.c
@@ -236,7 +236,7 @@ free_resinfo(void)
/**/
static int
-find_resource(char c)
+find_resource(convchar_t c)
{
int i;
for (i=0; i<RLIM_NLIMITS; ++i) {
@@ -738,13 +738,14 @@ bin_ulimit(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
return 1;
}
res = -1;
- if (options && *options == '-') {
+ if (options && *options++ == '-') {
argv++;
- while (*++options) {
- if(*options == Meta)
- *++options ^= 32;
+ while (*options) {
+ int sz;
+ convchar_t opt = unmeta_one(options, &sz);
+ options += sz;
res = -1;
- switch (*options) {
+ switch (opt) {
case 'H':
hard = 1;
continue;
@@ -752,8 +753,8 @@ bin_ulimit(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
soft = 1;
continue;
case 'N':
- if (options[1]) {
- number = options + 1;
+ if (*options) {
+ number = options;
} else if (*argv) {
number = *argv++;
} else {
@@ -768,7 +769,7 @@ bin_ulimit(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
/*
* fake it so it looks like we just finished an option...
*/
- while (options[1])
+ while (*options)
options++;
break;
case 'a':
@@ -781,15 +782,15 @@ bin_ulimit(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
nres = RLIM_NLIMITS;
continue;
default:
- res = find_resource(*options);
+ res = find_resource(opt);
if (res < 0) {
/* unrecognised limit */
- zwarnnam(name, "bad option: -%c", *options);
+ zwarnnam(name, "bad option: -%c", opt);
return 1;
}
break;
}
- if (options[1]) {
+ if (*options) {
resmask |= 1 << res;
nres++;
}
diff --git a/Src/Builtins/sched.c b/Src/Builtins/sched.c
index 835e72cb76..49c66d0407 100644
--- a/Src/Builtins/sched.c
+++ b/Src/Builtins/sched.c
@@ -194,9 +194,10 @@ bin_sched(char *nam, char **argv, UNUSED(Options ops), UNUSED(int func))
} else if (!strcmp(arg, "o")) {
flags |= SCHEDFLAG_TRASH_ZLE;
} else {
- if (*arg)
- zwarnnam(nam, "bad option: -%c", *arg);
- else
+ if (*arg) {
+ convchar_t warg = unmeta_one(arg, NULL);
+ zwarnnam(nam, "bad option: -%c", warg);
+ } else
zwarnnam(nam, "option expected");
return 1;
}
diff --git a/Src/Modules/stat.c b/Src/Modules/stat.c
index f7ec06e9e7..cb216f33af 100644
--- a/Src/Modules/stat.c
+++ b/Src/Modules/stat.c
@@ -450,7 +450,8 @@ bin_stat(char *name, char **args, Options ops, UNUSED(int func))
ops->ind['s'] = 1;
break;
} else {
- zwarnnam(name, "bad option: -%c", *arg);
+ convchar_t warg = unmeta_one(arg, NULL);
+ zwarnnam(name, "bad option: -%c", warg);
return 1;
}
}
diff --git a/Src/Modules/system.c b/Src/Modules/system.c
index 3c0b8421ab..4d2f8c42a3 100644
--- a/Src/Modules/system.c
+++ b/Src/Modules/system.c
@@ -653,7 +653,8 @@ bin_zsystem_flock(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
break;
default:
- zwarnnam(nam, "flock: unknown option: %c", *optptr);
+ convchar_t opt = unmeta_one(optptr, NULL);
+ zwarnnam(nam, "flock: bad option: %c", opt);
return 1;
}
optptr++;
diff --git a/Src/Modules/zftp.c b/Src/Modules/zftp.c
index 29170ad113..6babdf4296 100644
--- a/Src/Modules/zftp.c
+++ b/Src/Modules/zftp.c
@@ -3093,7 +3093,10 @@ bin_zftp(char *name, char **args, UNUSED(Options ops), UNUSED(int func))
break;
default:
- zwarnnam(name, "preference %c not recognized", *ptr);
+ int sz;
+ convchar_t p = unmeta_one(ptr, &sz);
+ ptr += sz - 1;
+ zwarnnam(name, "preference %c not recognized", p);
break;
}
}
diff --git a/Src/Zle/complete.c b/Src/Zle/complete.c
index 4bc048776a..b8e203b80f 100644
--- a/Src/Zle/complete.c
+++ b/Src/Zle/complete.c
@@ -269,9 +269,10 @@ parse_cmatcher(char *name, char *s)
case 'M': fl = CMF_LINE; break;
case 'x': break;
default:
- if (name)
- zwarnnam(name, "unknown match specification character `%c'",
- *s);
+ if (name) {
+ convchar_t c = unmeta_one(s, NULL);
+ zwarnnam(name, "unknown match specification character `%c'", c);
+ }
return pcm_err;
}
if (s[1] != ':') {
@@ -796,7 +797,8 @@ bin_compadd(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
argv++;
goto ca_args;
default:
- zwarnnam(name, "bad option: -%c", *p);
+ convchar_t c = unmeta_one(p, NULL);
+ zwarnnam(name, "bad option: -%c", c);
zsfree(mstr);
zfree(dat.dpar, dparsize);
return 1;
@@ -1163,7 +1165,8 @@ bin_compset(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))
case 'S': test = CVT_SUFPAT; break;
case 'q': return set_comp_sep();
default:
- zwarnnam(name, "bad option -%c", argv[0][1]);
+ convchar_t c = unmeta_one(&argv[0][1], NULL);
+ zwarnnam(name, "bad option: -%c", c);
return 1;
}
if (argv[0][2]) {
diff --git a/Src/Zle/zle_thingy.c b/Src/Zle/zle_thingy.c
index 94e67f8384..f6c521d11b 100644
--- a/Src/Zle/zle_thingy.c
+++ b/Src/Zle/zle_thingy.c
@@ -780,7 +780,7 @@ bin_zle_call(char *name, char **args, UNUSED(Options ops), UNUSED(char func))
setbindk = 1;
break;
default:
- zwarnnam(name, "unknown option: %s", *args);
+ zwarnnam(name, "bad option: %s", *args);
goto cleanup_zle_call;
}
}
diff --git a/Src/builtin.c b/Src/builtin.c
index 96bfd64cb5..f61a41af33 100644
--- a/Src/builtin.c
+++ b/Src/builtin.c
@@ -387,9 +387,8 @@ execbuiltin(LinkList args, LinkList assigns, Builtin bn)
/* The above loop may have exited on an invalid option. (We *
* assume that any option requiring metafication is invalid.) */
if (*arg) {
- if(*arg == Meta)
- *++arg ^= 32;
- zwarnnam(name, "bad option: %c%c", "+-"[sense], *arg);
+ convchar_t warg = unmeta_one(arg, NULL);
+ zwarnnam(name, "bad option: %c%c", "+-"[sense], warg);
return 1;
}
arg = *++argv;
@@ -619,18 +618,20 @@ bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
hadplus |= !action;
if(!args[0][1])
*args = "--";
- while (*++*args) {
- if(**args == Meta)
- *++*args ^= 32;
- if(**args != '-' || action)
+ ++*args;
+ while (**args) {
+ int sz;
+ convchar_t arg = unmeta_one(*args, &sz);
+ *args += sz;
+ if(arg != '-' || action)
hadopt = 1;
/* The pseudo-option `--' signifies the end of options. */
- if (**args == '-') {
+ if (arg == '-') {
hadend = 1;
args++;
goto doneoptions;
- } else if (**args == 'o') {
- if (!*++*args)
+ } else if (arg == 'o') {
+ if (!**args)
args++;
if (!*args) {
printoptionstates(hadplus);
@@ -642,8 +643,8 @@ bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
else if(dosetopt(optno, action, 0, opts))
zerrnam(nam, "can't change option: %s", *args);
break;
- } else if(**args == 'A') {
- if(!*++*args)
+ } else if(arg == 'A') {
+ if(!**args)
args++;
array = action ? 1 : -1;
arrayname = *args;
@@ -655,13 +656,13 @@ bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
goto doneoptions;
}
break;
- } else if (**args == 's')
+ } else if (arg == 's')
sort = action ? 1 : -1;
else {
- if (!(optno = optlookupc(**args)))
- zerrnam(nam, "bad option: -%c", **args);
+ if (!(optno = optlookupc(arg)))
+ zerrnam(nam, "bad option: -%c", arg);
else if(dosetopt(optno, action, 0, opts))
- zerrnam(nam, "can't change option: -%c", **args);
+ zerrnam(nam, "can't change option: -%c", arg);
}
}
args++;
diff --git a/Src/exec.c b/Src/exec.c
index c1393a3843..567c71a75a 100644
--- a/Src/exec.c
+++ b/Src/exec.c
@@ -3298,7 +3298,8 @@ execcmd_exec(Estate state, Execcmd_params eparams,
cflags |= BINF_DASH;
break;
default:
- zerr("unknown exec flag -%c", *cmdopt);
+ convchar_t opt = unmeta_one(cmdopt, NULL);
+ zerr("unknown exec flag -%c", opt);
lastval = 1;
errflag |= ERRFLAG_ERROR;
if (forked)
diff --git a/Src/glob.c b/Src/glob.c
index 5c6288060a..946a50c06f 100644
--- a/Src/glob.c
+++ b/Src/glob.c
@@ -1756,7 +1756,8 @@ zglob(LinkList list, LinkNode np, int nountok)
}
default:
untokenize(--s);
- zerr("unknown file attribute: %c", *s);
+ convchar_t attr = unmeta_one(s, NULL);
+ zerr("unknown file attribute: %c", attr);
restore_globstate(saved);
return;
}
diff --git a/Src/math.c b/Src/math.c
index ff0165a877..fab26116f7 100644
--- a/Src/math.c
+++ b/Src/math.c
@@ -1100,8 +1100,10 @@ callmathfunc(char *o)
break;
}
}
- if (*a && !errflag)
- zerr("bad math expression: illegal character: %c", *a);
+ if (*a && !errflag) {
+ convchar_t wa = unmeta_one(a, NULL);
+ zerr("bad math expression: illegal character: %c", wa);
+ }
if (!errflag) {
if (argc >= f->minargs && (f->maxargs < 0 ||
argc <= f->maxargs)) {
@@ -1497,8 +1499,10 @@ matheval(char *s)
x = mathevall(s, MPREC_TOP, &junk);
zsh_eval_context_pop();
mtok = xmtok;
- if (*junk)
- zerr("bad math expression: illegal character: %c", *junk);
+ if (*junk) {
+ convchar_t j = unmeta_one(junk, NULL);
+ zerr("bad math expression: illegal character: %c", j);
+ }
return x;
}
diff --git a/Src/options.c b/Src/options.c
index 8127ccdcaa..2cf1f48721 100644
--- a/Src/options.c
+++ b/Src/options.c
@@ -591,24 +591,26 @@ bin_setopt(char *nam, char **args, UNUSED(Options ops), int isun)
/* loop through command line options (begins with "-" or "+") */
while (*args && (**args == '-' || **args == '+')) {
action = (**args == '-') ^ isun;
- if(!args[0][1])
+ if (!args[0][1])
*args = "--";
- while (*++*args) {
- if(**args == Meta)
- *++*args ^= 32;
+ ++*args;
+ while (**args) {
+ int sz;
+ convchar_t arg = unmeta_one(*args, &sz);
+ *args += sz;
/* The pseudo-option `--' signifies the end of options. */
- if (**args == '-') {
+ if (arg == '-') {
args++;
goto doneoptions;
- } else if (**args == 'o') {
- if (!*++*args)
+ } else if (arg == 'o') {
+ if (!**args)
args++;
if (!*args) {
zwarnnam(nam, "string expected after -o");
inittyptab();
return 1;
}
- if(!(optno = optlookup(*args))) {
+ if (!(optno = optlookup(*args))) {
zwarnnam(nam, "no such option: %s", *args);
retval |= 1;
} else if (dosetopt(optno, action, 0, opts)) {
@@ -616,14 +618,14 @@ bin_setopt(char *nam, char **args, UNUSED(Options ops), int isun)
retval |= 1;
}
break;
- } else if(**args == 'm') {
+ } else if (arg == 'm') {
match = 1;
} else {
- if (!(optno = optlookupc(**args))) {
- zwarnnam(nam, "bad option: -%c", **args);
+ if (!(optno = optlookupc(arg))) {
+ zwarnnam(nam, "bad option: -%c", arg);
retval |= 1;
} else if (dosetopt(optno, action, 0, opts)) {
- zwarnnam(nam, "can't change option: -%c", **args);
+ zwarnnam(nam, "can't change option: -%c", arg);
retval |= 1;
}
}
@@ -718,7 +720,7 @@ optlookup(char const *name)
/**/
int
-optlookupc(char c)
+optlookupc(convchar_t c)
{
if(c < FIRST_OPT || c > LAST_OPT)
return 0;
diff --git a/Test/B12limit.ztst b/Test/B12limit.ztst
index 9dce598246..bc73d660a9 100644
--- a/Test/B12limit.ztst
+++ b/Test/B12limit.ztst
@@ -1,6 +1,7 @@
%prep
+ ZSH_TEST_LANG=$(ZTST_find_UTF8)
if ! zmodload zsh/rlimits 2>/dev/null
then
ZTST_unimplemented="the zsh/rlimits module was disabled by configure (see config.modules)"
@@ -26,3 +27,27 @@ F:report this to zsh-workers mailing list.
}
0:check if limit option letters are unique
+ ulimit -z
+1:bad option letter
+?(eval):ulimit:1: bad option: -z
+
+ ulimit -N
+1:missing number after -N
+?(eval):ulimit:1: number required after -N
+
+ ulimit -N abc
+1:invalid number after -N
+?(eval):ulimit:1: invalid number: abc
+
+ ulimit -at
+1:resource option combined with -a
+?(eval):ulimit:1: no limits allowed with -a
+
+ if [[ -z $ZSH_TEST_LANG ]]; then
+ ZTST_skip='no UTF-8 locale'
+ else
+ LANG=$ZSH_TEST_LANG
+ ulimit -の
+ fi
+1:bad multibyte option letter
+?(eval):ulimit:5: bad option: -の
--
2.38.1
Messages sorted by:
Reverse Date,
Date,
Thread,
Author