Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
Re: [PATCH] Fix (mainly nameref) issues in builtin "unset"
- X-seq: zsh-workers 54722
- From: Philippe Altherr <philippe.altherr@xxxxxxxxx>
- To: Bart Schaefer <schaefer@xxxxxxxxxxxxxxxx>
- Cc: Zsh hackers list <zsh-workers@xxxxxxx>
- Subject: Re: [PATCH] Fix (mainly nameref) issues in builtin "unset"
- Date: Tue, 9 Jun 2026 03:03:53 +0200
- 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=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=XgFPCG2tDjUh1vT9+ngAG4WGDK8TrAcWxVsi2jrPAxw=; fh=vuDAGjptCPc/P1/HEZ3j98yPJEW1XjuEoEAmmwDS/+U=; b=ir+f2PBD506X04oPpgbmJJ15prmz8GIi1xoiO0fdsr2H4Lt6JY+wq/7FN5nkCiVjwW NFW6TbDmQfiHjStbB4kVyMhG4VOIw2tNFM4FDJX3flOcBWhdYaz5diDk9dp5NyUqAIkV mWL9Rc8S1xDIM27/yWQ7Y+1mUZOYlNDtNQJmi/QtQH8xqymTgWlKeb34cbV6utCRCHbt P9vlwtkP3VU5SQLoIm8w0zGkhS0XX//X93zKQtvCGL+kxXjoz3RvdjtjOZOyxohRwdCO QexJ0SzB/1VbEtPi5Ll89TRumnLE1rcj2HeN9pNUzrR8ejuiFBPEseTByL1/lAg6ngyE xRKQ==; darn=zsh.org
- Arc-seal: i=1; a=rsa-sha256; t=1780967046; cv=none; d=google.com; s=arc-20240605; b=LORzfnP1chPnDbKL0XnNWby+h5nUWFiyQ/i1z+3zVqH6siw9VjJ1/0smThmareYYbG b5fH5n34mkRFkaHh4E4pbFgxMXc+nLsk/GmJSMYOhKsCx8kJIHsAHiCyloNr2OIk/Qf5 chJMHxKPI2tyGIcuJ4abjw66D7+SZ5Uo0EzM1+NT9qXsj9kSPhHSLlRZAFGjgf6l7Ju1 5rWau/epjbrSoDIknpm80anWgXnNjGxEkw4R2060IrCl9RB9jYx+yXeCP8dwEpUf4Dhq IRP4Uv8KlzhzaCVFEmuCazSO1DL+l2IawHRDXo2qixAZrVeP8MeRriOSLPV7xzFD3kfy Vj3g==
- Archived-at: <https://zsh.org/workers/54722>
- In-reply-to: <CAH+w=7ZZCZPVeUP530Ku+WTRweM-jARsUg2ABguMqhc8YS1Qjw@mail.gmail.com>
- List-id: <zsh-workers.zsh.org>
- References: <CAGdYchuxyh2qztz9x4CdHZDXrLJ5d-u_p2LRgBmDrZPk-wK92Q@mail.gmail.com> <CAGdYchtvG5np68K7Yf92Avf8J5--WOW8phaWg8ZNYTuqO0mfWw@mail.gmail.com> <CAH+w=7Zxxw7Zxo2NFVLJXy485QZxZu1u+2ema1UY1SPGHaxVgA@mail.gmail.com> <CAH+w=7Yyr4TMiJY1aN3CdUxmXH5C--q+WmH_JDMkFstcQYQCHw@mail.gmail.com> <CAH+w=7ZZCZPVeUP530Ku+WTRweM-jARsUg2ABguMqhc8YS1Qjw@mail.gmail.com>
I synced a few days ago but forgot to send an updated patch. I resynced a few minutes and here is the latest version.
I see that there are already a few more of my patches that were committed. Hopefully there is no conflict with these. I will check and send a new version if needed.
Philippe
diff --git a/Src/builtin.c b/Src/builtin.c
index 96bfd64cb..0ae75e6bd 100644
--- a/Src/builtin.c
+++ b/Src/builtin.c
@@ -3841,10 +3841,9 @@ bin_unset(char *name, char **argv, Options ops, int func)
/* record pointer to next, since we may free this one */
next = (Param) pm->node.next;
if (pattry(pprog, pm->node.nam)) {
- if (!OPT_ISSET(ops,'n') &&
- (pm->node.flags & PM_NAMEREF) && pm->u.str)
- unsetparam(pm->u.str);
- else
+ if (OPT_ISSET(ops,'n') ||
+ ((pm = resolve_nameref(pm)) &&
+ !(pm->node.flags & PM_NAMEREF)))
unsetparam_pm(pm, 0, 1);
match++;
}
@@ -3937,22 +3936,11 @@ bin_unset(char *name, char **argv, Options ops, int func)
zerrnam(name, "%s: invalid element for unset", s);
returnval = 1;
}
- } else {
- if (!OPT_ISSET(ops,'n')) {
- int ref = (pm->node.flags & PM_NAMEREF);
- if (!(pm = resolve_nameref(pm)))
- continue;
- if (ref && pm->level < locallevel &&
- !(pm->node.flags & PM_READONLY)) {
- /* Just mark unset, do not remove from table */
- stdunsetfn(pm, 0);
- pm->node.flags |= PM_DECLARED;
- continue;
- }
- }
+ } else if (OPT_ISSET(ops,'n') ||
+ ((pm = resolve_nameref(pm)) &&
+ !(pm->node.flags & PM_NAMEREF)))
if (unsetparam_pm(pm, 0, 1))
returnval = 1;
- }
if (ss)
*ss = '[';
}
diff --git a/Src/params.c b/Src/params.c
index 68d4c0bc3..b95efe9a8 100644
--- a/Src/params.c
+++ b/Src/params.c
@@ -499,6 +499,16 @@ static Param argvparam;
* times in the same list. Non of that is harmful as long as only
* instances that are still references referring to the ending scope
* are updated when the scope ends.
+ *
+ * The list corresponding to the global scope never receives any of
+ * the named references described above. Instead, it's used to track
+ * global parameters that were unset via a named reference while in a
+ * scope where they were hidden by a nested parameter with the same
+ * name. In such cases, the global parameter's Param instance can't be
+ * deleted as usual. Instead, it's marked as unset and added to the
+ * global scope's list. Each time a scope ends, the list is traversed
+ * and parameters that are still unset but no longer hidden are
+ * deleted.
*/
static LinkList *scoperefs = NULL;
static int scoperefs_num = 0;
@@ -3904,6 +3914,24 @@ unsetparam_pm(Param pm, int altflag, int exp)
(pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL)
return 0;
+ /*
+ * Global variables can only be deleted if they aren't hidden by a
+ * local one with the same name.
+ */
+ if (!pm->level &&
+ pm != (Param) (paramtab == realparamtab ?
+ /* getnode2() to avoid autoloading */
+ paramtab->getnode2(paramtab, pm->node.nam) :
+ paramtab->getnode(paramtab, pm->node.nam))) {
+ LinkList refs;
+ if (!scoperefs)
+ scoperefs = zshcalloc((scoperefs_num = 8) * sizeof(refs));
+ if (!scoperefs[0])
+ scoperefs[0] = znewlinklist();
+ zpushnode(scoperefs[0], pm);
+ return 0;
+ }
+
/* remove parameter node from table */
paramtab->removenode(paramtab, pm->node.nam);
@@ -5878,6 +5906,15 @@ endparamscope(void)
setscope(pm);
}
}
+ /* Delete unset global variables that were hidden at unset time */
+ if ((refs = scoperefs ? scoperefs[0] : NULL)) {
+ scoperefs[0] = NULL;
+ for (Param pm; refs && (pm = (Param)getlinknode(refs));) {
+ if ((pm->node.flags & PM_UNSET) && !(pm->node.flags & PM_DECLARED))
+ unsetparam_pm(pm, 1, 0);
+ }
+ freelinklist(refs, NULL);
+ }
unqueue_signals();
}
diff --git a/Test/K01nameref.ztst b/Test/K01nameref.ztst
index 77a01ea48..68b36f415 100644
--- a/Test/K01nameref.ztst
+++ b/Test/K01nameref.ztst
@@ -1026,15 +1026,15 @@ F:Checking for a bug in zmodload that affects later tests
typeset -p .K01.{scalar,assoc,array,integer,double,float,readonly}
unset .K01.{scalar,assoc,array,integer,double,float}
0:unset various types via nameref, including a readonly special
->typeset -g .K01.scalar
->typeset -g -A .K01.assoc
->typeset -g -a .K01.array
->typeset -g -i .K01.integer
->typeset -g -E .K01.double
->typeset -g -F .K01.float
>typeset -g -r .K01.readonly=RO
*?*read-only variable: ARGC
*?*read-only variable: .K01.readonly
+*?*no such variable: .K01.scalar
+*?*no such variable: .K01.assoc
+*?*no such variable: .K01.array
+*?*no such variable: .K01.integer
+*?*no such variable: .K01.double
+*?*no such variable: .K01.float
unset -n ref
unset one
@@ -1946,6 +1946,220 @@ F:converting from association/array to string should work here too
># d:reference to not-yet-defined - local - ref1
>typeset -i var=42
+ test-unset() {
+ typeset var0=foo
+ typeset -n ref1=var0 ref2=ref1
+ typeset cmd=(unset $@); echo "#" $cmd; $cmd
+ typeset -p var0 ref1 ref2
+ }
+ test-unset -n ref1
+ test-unset -n ref2
+ test-unset -n -m ref1
+ test-unset -n -m ref2
+ unfunction test-unset
+0:unsetting references with -n unsets the references
+># unset -n ref1
+>typeset var0=foo
+>typeset -n ref2=ref1
+># unset -n ref2
+>typeset var0=foo
+>typeset -n ref1=var0
+># unset -n -m ref1
+>typeset var0=foo
+>typeset -n ref2=ref1
+># unset -n -m ref2
+>typeset var0=foo
+>typeset -n ref1=var0
+
+ test-unset() {
+ typeset var0=foo
+ typeset -n ref1=var0 ref2=ref1
+ typeset cmd=(unset $@); echo "#" $cmd; $cmd
+ typeset -p var0 ref1 ref2
+ }
+ test-unset ref1
+ test-unset ref2
+ test-unset -m ref1
+ test-unset -m ref2
+ unfunction test-unset
+0:unsetting references without -n unsets the referred parameters
+># unset ref1
+>typeset -n ref1=var0
+>typeset -n ref2=ref1
+># unset ref2
+>typeset -n ref1=var0
+>typeset -n ref2=ref1
+># unset -m ref1
+>typeset -n ref1=var0
+>typeset -n ref2=ref1
+># unset -m ref2
+>typeset -n ref1=var0
+>typeset -n ref2=ref1
+
+ test-unset() {
+ typeset var0=12345
+ typeset -n ref1=var0 ref2=ref1
+ typeset cmd=(unset $@); echo "#" $cmd; $cmd
+ typeset -p var0
+ }
+ test-unset ref1"[3]"
+ test-unset ref2"[3]"
+ test-unset -n ref1"[3]"
+ test-unset -n ref2"[3]"
+ unfunction test-unset
+0:unsetting subscripted references unsets the referred elements
+># unset ref1[3]
+>typeset var0=1245
+># unset ref2[3]
+>typeset var0=1245
+># unset -n ref1[3]
+>typeset var0=1245
+># unset -n ref2[3]
+>typeset var0=1245
+
+ test-unset() {
+ typeset -r var=foo
+ typeset -n ref=var
+ typeset cmd=(unset $@); echo "#" $cmd; { $cmd 2>&1 } always { TRY_BLOCK_ERROR=0 }
+ typeset -p var
+ }
+ test-unset var
+ test-unset -m var
+ test-unset ref
+ test-unset -m ref
+ test-unset var"[2]"
+ test-unset ref"[2]"
+ test-unset -n ref"[2]"
+ unfunction test-unset
+0:unsetting read-only parameter triggers an error
+># unset var
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset -m var
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset ref
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset -m ref
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset var[2]
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset ref[2]
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+># unset -n ref[2]
+>test-unset:3: read-only variable: var
+>typeset -r var=foo
+
+ test-unset() {
+ typeset -n ref1 ref2=ref1
+ typeset cmd=(unset $@); echo "#" $cmd; $cmd
+ typeset -p ref1 ref2
+ }
+ test-unset ref1
+ test-unset ref2
+ test-unset -m ref1
+ test-unset -m ref2
+ unfunction test-unset
+0:unsetting placeholder references or their referents has no effect
+># unset ref1
+>typeset -n ref1
+>typeset -n ref2=ref1
+># unset ref2
+>typeset -n ref1
+>typeset -n ref2=ref1
+># unset -m ref1
+>typeset -n ref1
+>typeset -n ref2=ref1
+># unset -m ref2
+>typeset -n ref1
+>typeset -n ref2=ref1
+
+ test-unset() {
+ typeset -n ref1=undefined ref2=ref1
+ typeset cmd=(unset $@); echo "#" $cmd; $cmd
+ typeset -p ref1 ref2
+ }
+ typeset -p undefined 2>&1
+ test-unset ref1
+ test-unset ref2
+ test-unset -m ref1
+ test-unset -m ref2
+ unfunction test-unset
+0:unsetting references to not-yet-defined variables or their referents has no effect
+>(eval):typeset:6: no such variable: undefined
+># unset ref1
+>typeset -n ref1=undefined
+>typeset -n ref2=ref1
+># unset ref2
+>typeset -n ref1=undefined
+>typeset -n ref2=ref1
+># unset -m ref1
+>typeset -n ref1=undefined
+>typeset -n ref2=ref1
+># unset -m ref2
+>typeset -n ref1=undefined
+>typeset -n ref2=ref1
+
+ test-unset() {
+ typeset -n refg1=g1 refl1=l1
+ () {
+ typeset -g g1=glb1 g2=glb2
+ typeset l1=lcl1 l2=lcl2
+ () {
+ typeset -n refg2=g2 refl2=l2
+ typeset cmd=(unset $@ refg1 refg2 refl1 refl2); echo "#" $cmd; $cmd
+ } $@
+ typeset -p g1 g2 l1 l2 2>&1
+ } $@
+ unset g1 g2
+ }
+ test-unset
+ test-unset -m
+ unfunction test-unset
+0:unsetting references referring to parameters in enclosing scopes unsets the parameters
+># unset refg1 refg2 refl1 refl2
+>(anon):typeset:7: no such variable: g1
+>(anon):typeset:7: no such variable: g2
+># unset -m refg1 refg2 refl1 refl2
+>(anon):typeset:7: no such variable: g1
+>(anon):typeset:7: no such variable: g2
+
+ test-unset() {
+ typeset -g g=glb
+ typeset l=lcl
+ typeset -n refg=g refl=l
+ () {
+ typeset g=hide-g
+ typeset l=hide-l
+ typeset cmd=(unset $@ refg refl); echo "#" $cmd; $cmd
+ echo "# inner scope"
+ typeset -p g l 2>&1
+ } $@
+ echo "# outer scope"
+ typeset -p g l 2>&1
+ unset g
+ }
+ test-unset
+ test-unset -m
+ unfunction test-unset
+0:unsetting references referring to hidden parameters unsets the hidden parameters
+># unset refg refl
+># inner scope
+>typeset g=hide-g
+>typeset l=hide-l
+># outer scope
+>test-unset:typeset:12: no such variable: g
+># unset -m refg refl
+># inner scope
+>typeset g=hide-g
+>typeset l=hide-l
+># outer scope
+>test-unset:typeset:12: no such variable: g
+
typeset -n ref1
typeset -n ref2
typeset -n ref3=ref2
Messages sorted by:
Reverse Date,
Date,
Thread,
Author