1
0
Fork 0
mirror of git://git.code.sf.net/p/cdesktopenv/code synced 2025-02-13 11:42:21 +00:00

Fix conditional expansions ${array[i]=value}, ${array[i]?error}

$ unset foo
    $ echo ${foo[42]=bar}
    (empty line)

Instead of the empty line, 'bar' was expected. As foo[42] was
unset, the conditional assignment should have worked.

    $ unset foo
    $ : ${foo[42]?error: unset}
    (no output)

The expansion should have thrown an error with the given message.

This bug was introduced in ksh 93t 2008-10-01. Thanks to @JohnoKing
for finding the breaking change.

Analysis: The problem was experimenally determined to be in in the
following lines of nv_putsub(). If the array member is unset (i.e.
null), the value is set to the empty string instead:

src/cmd/ksh93/sh/array.c
1250: 		else
1251:			ap->val[size].cp = Empty;

It makes some sense: if there is a value (even an empty one), the
variable is set and these expansions should behave accordingly.
Sure enough, deleting these lines fixes the bug, but at the expense
of introducing a lot of other array-related regressions. So we need
a way to special-case the affected expansions.

Where to do this? If we replace line 1251 with an abort(3) call, we
get this stack trace:

0   libsystem_kernel.dylib    __pthread_kill + 10
1   libsystem_pthread.dylib   pthread_kill + 284
2   libsystem_c.dylib         abort + 127
3   ksh                       nv_putsub + 1411 (array.c:1255)
4   ksh                       nv_endsubscript + 940 (array.c:1547)
5   ksh                       nv_create + 4732 (name.c:1066)
6   ksh                       nv_open + 1951 (name.c:1425)
7   ksh                       varsub + 4934 (macro.c:1322)
[rest omitted]

The special-casing needs to be done on line 1250 of array.c, but
flagged in varsub() which processes these expansions. So, varsub()
calls nv_open() calls nv_create() calls nv_endsubscript() calls
nv_putsub(). That's a fairly deep call stack, so passing an extra
flag argument does not seem doable. I did try an approach using a
couple of new bit flags passed via these functions' flags and mode
parameters, but the way this code base uses bit flags is so
intricate, it does not seem to be possible to add or change
anything without unwanted side effects in all sorts of places.

So the only fix I can think of adds yet another global flag
variable for a very special case. It's ugly, but it works.
An elegant fix would probably involve a fairly comprehensive
redesign, which is simply not going to happen.

src/cmd/ksh93/include/shell.h:
- Add global sh.cond_expan flag.

src/cmd/ksh93/sh/array.c: nv_putsub():
- Do not set value to empty string if sh.cond_expan is set.

src/cmd/ksh93/sh/macro.c: varsub():
- Set sh.cond_expan flag while calling nv_open() for one of the
  affected expansions.
- Minor refactoring for legibility and to make the fix fit better.
- SSOT: Instead of repeating string "REPLY", use the node's nvname.
- Do not pointlessly add an extra 0 byte when saving id for error
  message; sfstruse() already adds this.

Thanks to @oguz-ismail for the bug report.

Resolves: https://github.com/ksh93/ksh/issues/383
This commit is contained in:
Martijn Dekker 2022-02-05 22:52:47 +00:00
parent 493a31053e
commit 65aff0befb
5 changed files with 37 additions and 11 deletions

4
NEWS
View file

@ -5,6 +5,10 @@ Any uppercase BUG_* names are modernish shell bug IDs.
2022-02-05: 2022-02-05:
- Fixed: for indexed arrays, given an unset array member a[i] with i > 0,
${a[i]=value} failed to assign the value and ${v[i]?error} failed to throw
an error.
- Fixed: the -a/--allexport shell option incorrectly exported some variables - Fixed: the -a/--allexport shell option incorrectly exported some variables
with names containing a dot to the environment. Note that the '-x' typeset with names containing a dot to the environment. Note that the '-x' typeset
attribute may still be set for them, but this will now not have any attribute may still be set for them, but this will now not have any

View file

@ -354,6 +354,7 @@ struct Shell_s
void *defpathlist; void *defpathlist;
void *cdpathlist; void *cdpathlist;
char **argaddr; char **argaddr;
char cond_expan; /* set while processing ${var=val}, ${var:=val}, ${var?err}, ${var:?err} */
void *optlist; void *optlist;
struct sh_scoped global; struct sh_scoped global;
struct checkpt checkbase; struct checkpt checkbase;

View file

@ -1247,7 +1247,7 @@ Namval_t *nv_putsub(Namval_t *np,register char *sp,register long mode)
nv_arraychild(np,mp,0); nv_arraychild(np,mp,0);
nv_setvtree(mp); nv_setvtree(mp);
} }
else else if(!sh.cond_expan)
ap->val[size].cp = Empty; ap->val[size].cp = Empty;
if(!sp && !array_covered(np,ap)) if(!sp && !array_covered(np,ap))
ap->header.nelem++; ap->header.nelem++;

View file

@ -1304,27 +1304,28 @@ retry1:
if(c=='=' || (c==':' && d=='=')) if(c=='=' || (c==':' && d=='='))
flag |= NV_ASSIGN; flag |= NV_ASSIGN;
flag &= ~NV_NOADD; flag &= ~NV_NOADD;
sh.cond_expan = 1; /* tell nv_putsub() not to change value from null to empty */
np = nv_open(id,sh.var_tree,flag|NV_NOFAIL);
sh.cond_expan = 0;
} }
#if SHOPT_FILESCAN #if SHOPT_FILESCAN
if(sh.cur_line && *id=='R' && strcmp(id,"REPLY")==0) else if(sh.cur_line && strcmp(id,REPLYNOD->nvname)==0)
{ {
sh.argaddr=0; sh.argaddr=0;
np = REPLYNOD; np = REPLYNOD;
} }
else
#endif /* SHOPT_FILESCAN */ #endif /* SHOPT_FILESCAN */
else
{ {
if(sh.argaddr) if(sh.argaddr)
flag &= ~NV_NOADD; flag &= ~NV_NOADD;
/*
* Get a node pointer (np) to the parameter, if any.
*/
np = nv_open(id,sh.var_tree,flag|NV_NOFAIL); np = nv_open(id,sh.var_tree,flag|NV_NOFAIL);
if(!np) }
{ if(!np)
sfprintf(sh.strbuf,"%s%c",id,0); {
id = sfstruse(sh.strbuf); /* id points to stack, which will be overwritten; save it for error message */
} sfputr(sh.strbuf,id,-1);
id = sfstruse(sh.strbuf);
} }
if(isastchar(mode)) if(isastchar(mode))
var = 0; var = 0;

View file

@ -783,5 +783,25 @@ got=$("$SHELL" -c '
[[ $exp == $got ]] || err_exit 'Unsetting all elements of an array adds a spurious [0] element' \ [[ $exp == $got ]] || err_exit 'Unsetting all elements of an array adds a spurious [0] element' \
"(expected $(printf %q "$exp"), got $(printf %q "$got"))" "(expected $(printf %q "$exp"), got $(printf %q "$got"))"
# ======
# https://github.com/ksh93/ksh/issues/383
unset foo
exp=bar
got=$(echo "${foo[42]=bar}")
[[ $got == "$exp" ]] || err_exit '${array[index]=value} does not assign value' \
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
exp=baz
got=$(echo "${foo[42]:=baz}")
[[ $got == "$exp" ]] || err_exit '${array[index]:=value} does not assign value' \
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
exp=': parameter not set'
got=$(set +x; redirect 2>&1; : ${foo[42]?})
[[ $got == *"$exp" ]] || err_exit '${array[index]?error} does not throw error' \
"(expected match of *$(printf %q "$exp"), got $(printf %q "$got"))"
exp=': parameter null'
got=$(set +x; redirect 2>&1; foo[42]=''; : ${foo[42]:?})
[[ $got == *"$exp" ]] || err_exit '${array[index]:?error} does not throw error' \
"(expected match of *$(printf %q "$exp"), got $(printf %q "$got"))"
# ====== # ======
exit $((Errors<125?Errors:125)) exit $((Errors<125?Errors:125))