Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
[PATCH] Fix assigning/typesetting of unset references (and related bugs)
- X-seq: zsh-workers 54236
- From: Philippe Altherr <philippe.altherr@xxxxxxxxx>
- To: Zsh hackers list <zsh-workers@xxxxxxx>
- Subject: [PATCH] Fix assigning/typesetting of unset references (and related bugs)
- Date: Sun, 22 Mar 2026 23:40:59 +0000
- Arc-authentication-results: i=1; mx.google.com; arc=none
- Arc-message-signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=to:subject:message-id:date:from:mime-version:dkim-signature; bh=cGdFT0Dox9VWQRM+9TWstIfmKjN9jXgrfnaZbH62qeY=; fh=BgAYDYpL6Ne/A5nWEMVJiHiBtrz8Imz3uf26RDwgQX4=; b=FP2GK2AZYSC2QndXi/lEVuG1I8k8WQ9wp0HRSFla3r6XIs2FPZJpT67F+VZ0M0ynKD yW4G50Ya8ezZSVra3il5WYgEaVFyEuX1BmEhLZ1Jdd3Jh1Nl/UmYehgKG8jPO5vuiW2w E2Y5bfIN4cijKTHMcN0VQCuYdKDDSj9HZckhaPk7EoN+GLSjBxq+myVTzc58KpNrWA7W QFzXen1Eozco2YauuLmdRM+WIrKPXDouvxiVE6RWOlyqXSf5TAU7IdLiP2/H4D9azwT1 JUDAhlfkPC75lVMmiN4jKbhUQAlhOtHPrzTIRpsOPNCyaOPwUdxUKjG1hwq7WG3eiAku 5DTQ==; darn=zsh.org
- Arc-seal: i=1; a=rsa-sha256; t=1774222871; cv=none; d=google.com; s=arc-20240605; b=ZAiS+YwOIb9UOem8s70QLjQhm9C8XX3fBV+N2hS7rK25xdNV2/iHv8fn9nXacGLXco 5Sqq2lYMvlHbTcpsIiMI66HqeIuLSgdot/kulrPYRZzZYkdmfpnavweP6mAj5hus+At6 /hDmG6MHB1D1QE7JGELgCqyE+fM4jram0v2NcbitK/vhwOvLtPCaMgAzTDM6MhB7IVPh Ou3LmYs0U0ITBzxhoR/RE4mMEQ/9zioeUlYqbszH5tTKUeeyTn+ZbSvYKh9Lk80ltv48 N3IMc3pQyZZVfJ+fl43VKIUFoezhB+yqk+REG3EzTiW78jhK5aMRZFYs+LSW3X0/io/h jogg==
- Archived-at: <https://zsh.org/workers/54236>
- List-id: <zsh-workers.zsh.org>
Assigning an unset local reference R, wrongly resurrects the parameter R as a new reference instead of resurrecting it as a new string parameter R:
$ () { typeset -g -n gref=foo; unset -n gref; gref=bar; typeset -p gref }
typeset -g gref=bar
$ () { typeset -n lref=foo; unset -n lref; lref=bar; typeset -p lref }
typeset -n lref=bar
The same bug occurs if one typesets R instead of assigning it:
() { typeset -g -n gref=foo; unset -n gref; typeset -g gref=bar; typeset -p gref }
typeset -g gref=bar
() { typeset -n lref=foo; unset -n lref; typeset lref=bar; typeset -p lref }
typeset -n lref=bar
The following patch fixes these bugs as well as a number of related bugs:
For more details about the related bugs have a look on GitHub at the tests added in the
first commit of the patch and look for all the "F:BUG" comments in the expected output of the tests.
Philippe
diff --git a/Src/builtin.c b/Src/builtin.c
index c269dc1c6..7c095149d 100644
--- a/Src/builtin.c
+++ b/Src/builtin.c
@@ -2038,6 +2038,7 @@ typeset_single(char *cname, char *pname, Param pm, int func,
if ((pm = resolve_nameref(pm)))
pname = pm->node.nam;
if (pm && (pm->node.flags & PM_NAMEREF) &&
+ (!(pm->node.flags & PM_UNSET) || (pm->node.flags & PM_DECLARED)) &&
(on & ~(PM_NAMEREF|PM_LOCAL|PM_READONLY))) {
/* Changing type of PM_SPECIAL|PM_AUTOLOAD is a fatal error. *
* Should this be a fatal error as well, rather than warning? */
diff --git a/Src/params.c b/Src/params.c
index 3199fd17b..afc67eb14 100644
--- a/Src/params.c
+++ b/Src/params.c
@@ -1044,7 +1044,9 @@ createparam(char *name, int flags)
if (oldpm && !(flags & PM_NAMEREF) &&
(oldpm->level == locallevel ?
!(oldpm->node.flags & PM_RO_BY_DESIGN) : !(flags & PM_LOCAL)) &&
- (oldpm->node.flags & PM_NAMEREF)) {
+ (oldpm->node.flags & PM_NAMEREF) &&
+ (!(oldpm->node.flags & PM_UNSET) ||
+ (oldpm->node.flags & PM_DECLARED))) {
/**
* Here we only have to deal with namerefs that refer to
* not-yet-defined or unset variable. All other namerefs
@@ -1054,14 +1056,18 @@ createparam(char *name, int flags)
**/
Param lastpm = resolve_nameref_rec(oldpm, NULL, 1);
if (lastpm) {
- if (lastpm->node.flags & PM_NAMEREF) {
+ if (lastpm->node.flags & PM_NAMEREF &&
+ (!(lastpm->node.flags & PM_UNSET) ||
+ (lastpm->node.flags & PM_DECLARED))) {
char *refname = GETREFNAME(lastpm);
if (refname && *refname) {
+ /* nameref pointing to a not-yet-defined variable */
name = refname;
oldpm = NULL;
} else {
+ /* nameref pointing to an uninitialized nameref */
if (!(lastpm->node.flags & PM_READONLY)) {
- if (flags) {
+ if (flags & ~PM_LOCAL) {
/* Only plain scalar assignment allowed */
zerr("%s: can't change type of named reference",
name); /* Differs from ksh93u+ */
@@ -3386,6 +3392,10 @@ assignaparam(char *s, char **val, int flags)
if (!(v = fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING))) {
createparam(t, PM_ARRAY);
created = 1;
+ } else if (v->pm->node.flags & PM_NAMEREF) {
+ zwarn("%s: can't change type of a named reference", t);
+ unqueue_signals();
+ return NULL;
} else if (!(PM_TYPE(v->pm->node.flags) & (PM_ARRAY|PM_HASHED)) &&
!(v->valflags & VALFLAG_REFSLICE) &&
!(v->pm->node.flags & (PM_SPECIAL|PM_TIED))) {
@@ -6321,35 +6331,30 @@ resolve_nameref(Param pm)
static Param
resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)
{
- Param hn = pm;
- if (pm && (pm->node.flags & PM_NAMEREF)) {
- char *refname = GETREFNAME(pm);
- if (pm->node.flags & PM_TAGGED) {
- zerr("%s: invalid self reference", pm->node.nam);
- return NULL;
- } else if (pm->node.flags & PM_UNSET) {
- /* Semaphore with createparam() */
- pm->node.flags &= ~PM_UNSET;
- return pm;
- }
+ Param ref = pm;
+ char *refname;
+ if (!pm || !(pm->node.flags & PM_NAMEREF) || (pm->node.flags & PM_UNSET)
/* pm->width is the offset of any subscript */
/* If present, it has to be the end of any chain, see fetchvalue() */
- if (refname && *refname && !pm->width) {
- queue_signals();
- if ((hn = (Param)gethashnode2(realparamtab, refname))) {
- if ((hn = loadparamnode(paramtab, upscope(hn, pm), refname)) &&
- hn != stop && !(hn->node.flags & PM_UNSET)) {
- /* user can't tag a nameref, safe for loop detection */
- pm->node.flags |= PM_TAGGED;
- hn = resolve_nameref_rec(hn, stop, keep_lastref);
- pm->node.flags &= ~PM_TAGGED;
- }
- } else if (keep_lastref)
- hn = pm;
- unqueue_signals();
- }
+ || pm->width || !(refname = GETREFNAME(pm)) || !*refname)
+ return pm;
+ if (pm->node.flags & PM_TAGGED) {
+ zerr("%s: invalid self reference", pm->node.nam);
+ return NULL;
}
- return hn;
+ queue_signals();
+ if ((pm = (Param)gethashnode2(realparamtab, refname))) {
+ if ((pm = loadparamnode(paramtab, upscope(pm, ref), refname)) &&
+ pm != stop && !(pm->node.flags & PM_UNSET)) {
+ /* user can't tag a nameref, safe for loop detection */
+ ref->node.flags |= PM_TAGGED;
+ pm = resolve_nameref_rec(pm, stop, keep_lastref);
+ ref->node.flags &= ~PM_TAGGED;
+ }
+ } else if (keep_lastref)
+ pm = ref;
+ unqueue_signals();
+ return pm;
}
/**/
diff --git a/Src/zsh.h b/Src/zsh.h
index 0dc83ded4..dd58c0816 100644
--- a/Src/zsh.h
+++ b/Src/zsh.h
@@ -1927,7 +1927,10 @@ struct tieddata {
#define PM_DONTIMPORT (1<<22) /* do not import this variable */
#define PM_DECLARED (1<<22) /* explicitly named with typeset */
#define PM_RESTRICTED (1<<23) /* cannot be changed in restricted mode */
-#define PM_UNSET (1<<24) /* has null value */
+#define PM_UNSET (1<<24) /* If PM_DECLARED is also present, parameter
+ * has null value. Otherwise, parameter was
+ * unset.
+ */
#define PM_DEFAULTED (PM_DECLARED|PM_UNSET)
#define PM_REMOVABLE (1<<25) /* special can be removed from paramtab */
#define PM_AUTOLOAD (1<<26) /* autoloaded from module */
diff --git a/Test/K01nameref.ztst b/Test/K01nameref.ztst
index e0f7b1879..8885fcd2d 100644
--- a/Test/K01nameref.ztst
+++ b/Test/K01nameref.ztst
@@ -187,11 +187,13 @@
>typeset -n ptr=gvar
typeset -n ptr
+ typeset -p ptr
typeset -t ptr
typeset -p ptr
0:change type of a placeholder
F:Other type changes are fatal errors, should this also be?
->typeset -n ptr=''
+>typeset -n ptr
+>typeset -n ptr
*?*ptr: can't change type of a named reference
typeset -n ptr=var
@@ -1659,4 +1661,231 @@ F:converting from association/array to string should work here too
1:self reference via upper reference
?(anon):3: ref1: invalid self reference
+ # The only purpose of this test is to define the helper functions
+ # needed by the tests below about assigning and typesetting named
+ # references in different contexts.
+ #
+ # Runs the code passed in "$1" for all combinations of the following
+ # variables:
+ #
+ # - K: Kind of target reference. The assigned/typset reference is one
+ # of the following kind:
+ # - "i": unInitialized reference:
+ # e.g., "ref0" in "typeset -n ref0"
+ # - "s": unSet reference:
+ # e.g., "ref0" in "typeset -n ref0; unset ref0"
+ # - "d": reference to not-yet-Defined variable:
+ # e.g., "ref0" in "typeset -n ref0=var" where "var" isn't defined
+ #
+ # - G: Global vs local references
+ # - "-g": All test variables/references are global ones.
+ # - "" : All test variables/references are local ones.
+ #
+ # - N: direct vs indirect references
+ # - "0": The target reference "ref0" is directly assigned/typeset.
+ # - "1": The target reference "ref1" is assigned/typeset via a
+ # named reference "ref1" that refers to "ref0".
+ test-all() {
+ local K G N;
+ for K in i s d; do
+ for G in -g ""; do
+ for N in 0 1; do
+ test-one $1
+ done
+ done
+ done
+ unset -n ref0 ref1 var
+ }
+ # Runs the code passed in "$1" for the context specified by the
+ # variables "K", "G", and "N".
+ test-one() {
+ unset -n ref0 ref1 var
+ # In several contexts different code paths are involved depending
+ # on whether the target reference "ref0" is assigned/typeset
+ # directly or via a named reference that referes to it. However, it
+ # doesn't seem to make a difference whether the chain of references
+ # is short or long. Therefore only chains of one reference are
+ # tested. One can manually double check that longer chains work the
+ # same by replacing the following line by one of the commented one
+ # just below and checking that all tests still pass.
+ ((N < 1)) || typeset $G -n ref1=ref0
+ # ((N < 1)) || typeset $G -n refX=ref0 ref1=refX
+ # ((N < 1)) || typeset $G -n refX=ref0 refY=refX ref1=refY
+ local kind print
+ case $K in
+ i) kind="uninitialized reference" ; check=ref0; typeset $G -n ref0;;
+ s) kind="unset reference" ; check=ref0; typeset $G -n ref0; unset -n ref0;;
+ d) kind="reference to not-yet-defined"; check=var ; typeset $G -n ref0=var;;
+ *) echo "Unrecognized case: $case"; return;;
+ esac
+ echo "# $K:$kind - ${${G:-local}/-g/global} - ref$N"
+ { eval ${(e)1}; typeset -p $check } always { TRY_BLOCK_ERROR=0 } 2>&1
+ }
+0:define the helper functions needed by the tests below
+
+ # The call to "test-all 'ref$N=str'" expands to the equivalent of the
+ # following:
+ #
+ # # Test uninitialized references
+ # typeset -g -n ref0 ; ref0=str; typeset -p ref0; unset -n ref0
+ # typeset -g -n ref0 ref1=ref0; ref1=str; typeset -p ref0; unset -n ref0 ref1
+ # typeset -n ref0 ; ref0=str; typeset -p ref0; unset -n ref0
+ # typeset -n ref0 ref1=ref0; ref1=str; typeset -p ref0; unset -n ref0 ref1
+ # # Test unset references
+ # typeset -g -n ref0 ; unset -n ref0; ref0=str; typeset -p ref0; unset -n ref0
+ # ...
+ # # Test references to not-yet-defined variables
+ # typeset -g -n ref0=var ; ref0=str; typeset -p var ; unset -n ref0 var
+ # ...
+ test-all 'ref$N=str'
+0:assign with string
+># i:uninitialized reference - global - ref0
+>typeset -g -n ref0=str
+># i:uninitialized reference - global - ref1
+>typeset -g -n ref0=str
+># i:uninitialized reference - local - ref0
+>typeset -n ref0=str
+># i:uninitialized reference - local - ref1
+>typeset -n ref0=str
+># s:unset reference - global - ref0
+>typeset -g ref0=str
+># s:unset reference - global - ref1
+>typeset -g ref0=str
+># s:unset reference - local - ref0
+>typeset ref0=str
+># s:unset reference - local - ref1
+>typeset ref0=str
+># d:reference to not-yet-defined - global - ref0
+>typeset -g var=str
+># d:reference to not-yet-defined - global - ref1
+>typeset -g var=str
+># d:reference to not-yet-defined - local - ref0
+>typeset -g var=str
+># d:reference to not-yet-defined - local - ref1
+>typeset -g var=str
+
+ test-all 'ref$N=( foo bar )'
+0:assign with array
+># i:uninitialized reference - global - ref0
+>(eval):1: ref0: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - global - ref1
+>(eval):1: ref1: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - local - ref0
+>(eval):1: ref0: can't change type of a named reference
+>typeset -n ref0
+># i:uninitialized reference - local - ref1
+>(eval):1: ref1: can't change type of a named reference
+>typeset -n ref0
+># s:unset reference - global - ref0
+>typeset -g -a ref0=( foo bar )
+># s:unset reference - global - ref1
+>typeset -g -a ref0=( foo bar )
+># s:unset reference - local - ref0
+>typeset -a ref0=( foo bar )
+># s:unset reference - local - ref1
+>typeset -a ref0=( foo bar )
+># d:reference to not-yet-defined - global - ref0
+>typeset -g -a var=( foo bar )
+># d:reference to not-yet-defined - global - ref1
+>typeset -g -a var=( foo bar )
+># d:reference to not-yet-defined - local - ref0
+>typeset -g -a var=( foo bar )
+># d:reference to not-yet-defined - local - ref1
+>typeset -g -a var=( foo bar )
+
+ test-all 'typeset $G ref$N=str'
+0:typeset with string
+># i:uninitialized reference - global - ref0
+>typeset -g -n ref0=str
+># i:uninitialized reference - global - ref1
+>typeset -g -n ref0=str
+># i:uninitialized reference - local - ref0
+>typeset -n ref0=str
+># i:uninitialized reference - local - ref1
+>typeset -n ref0=str
+># s:unset reference - global - ref0
+>typeset -g ref0=str
+># s:unset reference - global - ref1
+>typeset -g ref0=str
+># s:unset reference - local - ref0
+>typeset ref0=str
+># s:unset reference - local - ref1
+>typeset ref0=str
+># d:reference to not-yet-defined - global - ref0
+>typeset -g var=str
+># d:reference to not-yet-defined - global - ref1
+>typeset -g var=str
+># d:reference to not-yet-defined - local - ref0
+>typeset var=str
+># d:reference to not-yet-defined - local - ref1
+>typeset var=str
+
+ test-all 'typeset $G -a ref$N=( foo bar )'
+0:typeset with array
+># i:uninitialized reference - global - ref0
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - global - ref1
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - local - ref0
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -n ref0
+># i:uninitialized reference - local - ref1
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -n ref0
+># s:unset reference - global - ref0
+>typeset -g -a ref0=( foo bar )
+># s:unset reference - global - ref1
+>typeset -g -a ref0=( foo bar )
+># s:unset reference - local - ref0
+>typeset -a ref0=( foo bar )
+># s:unset reference - local - ref1
+>typeset -a ref0=( foo bar )
+># d:reference to not-yet-defined - global - ref0
+>typeset -g -a var=( foo bar )
+># d:reference to not-yet-defined - global - ref1
+>typeset -g -a var=( foo bar )
+># d:reference to not-yet-defined - local - ref0
+>typeset -a var=( foo bar )
+># d:reference to not-yet-defined - local - ref1
+>typeset -a var=( foo bar )
+
+ typeset i42=42
+ test-all 'typeset $G -i ref$N=i42'
+0:typeset with integer
+># i:uninitialized reference - global - ref0
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - global - ref1
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -g -n ref0
+># i:uninitialized reference - local - ref0
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -n ref0
+># i:uninitialized reference - local - ref1
+>(eval):typeset:1: ref0: can't change type of a named reference
+>typeset -n ref0
+># s:unset reference - global - ref0
+>typeset -g -i ref0=42
+># s:unset reference - global - ref1
+>typeset -g -i ref0=42
+># s:unset reference - local - ref0
+>typeset -i ref0=42
+># s:unset reference - local - ref1
+>typeset -i ref0=42
+># d:reference to not-yet-defined - global - ref0
+>typeset -g -i var=42
+># d:reference to not-yet-defined - global - ref1
+>typeset -g -i var=42
+># d:reference to not-yet-defined - local - ref0
+>typeset -i var=42
+># d:reference to not-yet-defined - local - ref1
+>typeset -i var=42
+
+ unfunction test-one test-all
+0:delete the helper functions used in the tests above
+
%clean
diff --git a/Test/V10private.ztst b/Test/V10private.ztst
index 4abbbf5c9..256844be1 100644
--- a/Test/V10private.ztst
+++ b/Test/V10private.ztst
@@ -370,7 +370,7 @@ F:See K01nameref.ztst up-reference part 5
F:Here ptr1 finds private ptr2 by scope mismatch
>typeset -n ptr1=ptr2
>typeset -hn ptr2
-*?*read-only variable: ptr2
+?(anon):1: read-only variable: ptr2
() {
typeset -n ptr1=ptr2
@@ -378,7 +378,7 @@ F:Here ptr1 finds private ptr2 by scope mismatch
typeset -p ptr1 ptr2
typeset val=LOCAL
() {
- ptr1=val || print -u2 ptr1: assignment failed
+ ptr1=val # Test dies here as ptr2 is private and uninitialized
typeset -n
printf "%s=%s\n" ptr1 "$ptr1" ptr2 "$ptr2"
}
@@ -390,13 +390,7 @@ F:See K01nameref.ztst up-reference part 5
F:Here ptr1 finds private ptr2 by scope mismatch
>typeset -n ptr1=ptr2
>typeset -hn ptr2=''
->ptr1=ptr2
->ptr1=
->ptr2=
->typeset -n ptr1=ptr2
->typeset -hn ptr2=''
-*?*ptr1: assignment failed
-*?*no such variable: ptr2
+?(anon):1: ptr1: can't modify read-only parameter
typeset ptr2
() {
Messages sorted by:
Reverse Date,
Date,
Thread,
Author