Re: Zsh's and ksh's bang (!) expansion flags have different meanings

Philippe Altherr <[email protected]> Tue, 21 Jul 2026 16:27:08 +0200
Newsgroups gmane.comp.shells.zsh.devel
Message-ID <CAGdYchtH3T9V+y9jguM5AZz_taXCKYhkgMCP2stbDKvLMvP75A@mail.gmail.com>
--0000000000001e5ecc06571fd23a
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

>
> >>> In ksh, the (!) flag can be used to list the keys of arrays and
> associative arrays. In that regard, it's closer to Zsh's (k) flag than
> Zsh's (!) flag


> The (k) flag was originally intended to mimic ksh's ! name prefix (due
> to ! colliding with history).  The character "k" was chosen as a
> mnemonic for "like (k)sh".


Funny, I always assumed it was (k)ey (and (v)alue). Good thing that both
ksh and key start with the same letter :-)

I suppose, rather than introduce (!), I
> could have extended (k) for the named reference behavior, to preserve
> that similarity.  Could still try that if it seems a good idea.


No, I don't think this would be wise. Zsh's (!) and ksh's ! really do two
different things.

Zsh's (!) means don't dereference the parameter and use its value as is.
For references, it implies using the referent name. For other types of
parameters it has no effect. Afaik, there is no equivalent mechanism in
ksh, which *ALWAYS* fully dereferences the parameter.

Ksh's ! means return the name of the referred variable instead of its
value, or, if there are multiple elements, print the names/keys/indexes of
the referred elements instead of their values. The latter part matches what
Ksh's (k) performs for associative tables.

Even though Zsh's (!) and ksh's ! perform two fundamentally different
things, there is one case where they produce the same result. Consider the
following definitions:

typeset -a var0=3D(abc def ghi)
typeset -n ref1=3Dvar0
typeset -n ref2=3Dref1

In Zsh, the (!) in ${(!)ref1} means don't dereference ref1 and use its
value as is, so the substitution returns the string "var0". In ksh, the !
in ${!ref1} means return the name of the variable referred by ref1, which
is the name of the variable var0, so, like in Zsh, the string "var0". This
equality of results is however limited to direct variable references with
no subscripts. In Zsh, ${(!)var0} and ${(!)ref2} respectively return "abc
def ghi" and "ref1", while in ksh ${!var0} and ${!ref2} both return "var0"
and in Zsh ${(!)ref1[1]} returns "v" while in ksh ${!ref1[1]} returns "
var0[1]".

This shows that even though Zsh's (!) and ksh's ! use the same character
flag and sometimes happen to return the same value, they really should not
be considered as being related; they perform two fundamentally different
things.

I guess this would conflict with nameref-to-assoc, which currently does
>> what I would expect. (Unless I'm missing something).
>> % typeset -n n=3Da
>> % echo ${n}
>> 1 2
>> % echo ${(k)n}
>> a b
>
> % typeset -A a=3D( a 1 b 2 )
>
>
> Indeed. For reference, ksh;
>
> $ typeset -A foo=3D([a]=3D1 [b]=3D2)
> $ typeset -n bar=3Dfoo
> $ echo ${!bar}
> foo
> $ echo "${!bar[@]}"
> a b
>
> Given Zsh's default unsubscripted-array expansion behavior, it seems like
> it would be hard to maintain the distinction between "what does this
> nameref point to" and "what are that thing's keys" if those semantics wer=
e
> combined in a single flag.
>

Fully agree.

With the introduction of named references, Zsh's emulation of ksh's ! has
been changed to map to Zsh's (!k)
<https://github.com/zsh-users/zsh/blob/3c0372aef88a6b7cc86f0902b1ba3f14a751=
18c9/Src/subst.c#L2121>.
I guess that was done for the case of direct variable references where
Zsh's (!) happens to perform the same as ksh's ! but that combination
breaks the example above and makes it difficult to explains what Zsh's
emulation of ksh's ! performs.

% typeset -A foo=3D([a]=3D1 [b]=3D2)
% typeset -n bar=3Dfoo
% emulate ksh
% echo "${!bar}"
foo
% echo "${!bar[@]}"'
foo

I think that this should be reverted such that Zsh's emulation of ksh's !
maps to Zsh's (k) as it used to do. It's true that the first result will
then no longer match ksh's but the second will. I think it's a better
trade-off because it doesn't try to do two different things at the same
time and thus is easier to explain and understand.

I will prepare a patch=E2=80=A6

Philippe

--0000000000001e5ecc06571fd23a
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div dir=3D"ltr"><blockquote class=3D"gmail_quote" style=
=3D"margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding=
-left:1ex"><span class=3D"gmail-im">&gt;&gt;&gt; In ksh, the (!) flag can b=
e used to list the keys of arrays and associative arrays. In that regard, i=
t&#39;s closer to Zsh&#39;s (k) flag than Zsh&#39;s (!) flag</span></blockq=
uote><blockquote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0.8ex;bo=
rder-left:1px solid rgb(204,204,204);padding-left:1ex"><span class=3D"gmail=
-im"><br></span>The (k) flag was originally intended to mimic ksh&#39;s ! n=
ame prefix (due<br>to ! colliding with history).=C2=A0 The character &quot;=
k&quot; was chosen as a<br>mnemonic for &quot;like (k)sh&quot;.</blockquote=
><div><br></div><div>Funny, I always assumed it was (k)ey (and (v)alue). Go=
od thing that both ksh and key start with the same letter :-)</div><div><br=
></div><div><blockquote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0=
.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">I suppose, ra=
ther than introduce (!), I<br>could have extended (k) for the named referen=
ce behavior, to preserve<br>that similarity.=C2=A0 Could still try that if =
it seems a good idea.</blockquote><div><br></div><div>No, I don&#39;t think=
 this would be wise. Zsh&#39;s (!) and ksh&#39;s=C2=A0! really do two diffe=
rent things.</div><div><br></div><div>Zsh&#39;s (!) means don&#39;t derefer=
ence the parameter and use its value as is. For references, it implies usin=
g the referent name. For other types of parameters it has no effect. Afaik,=
 there is no equivalent mechanism in ksh, which <b>ALWAYS</b> fully derefer=
ences the parameter.</div><div><br></div><div>Ksh&#39;s ! means return the =
name of the referred variable instead of its value, or, if there are multip=
le elements, print the names/keys/indexes of the referred elements instead =
of their values. The latter part matches what Ksh&#39;s=C2=A0(k) performs f=
or associative tables.</div><div><br></div><div>Even though Zsh&#39;s (!) a=
nd ksh&#39;s=C2=A0! perform two fundamentally different things, there is on=
e case where they produce the same result. Consider the following definitio=
ns:</div><div><br></div><div><font face=3D"monospace">typeset -a var0=3D(ab=
c def ghi)</font></div><div><font face=3D"monospace">typeset -n ref1=3Dvar0=
</font></div><div><div><font face=3D"monospace">typeset -n ref2=3Dref1</fon=
t></div><div><br></div></div><div><font face=3D"arial, sans-serif">In Zsh, =
the (!) in ${(!)ref1} means don&#39;t dereference=C2=A0ref1 and use its val=
ue as is, so the substitution returns the string &quot;var0&quot;. In ksh, =
the ! in ${!ref1} means return the name of the variable referred by ref1, w=
hich is the name of the variable var0, so, like in Zsh, the string &quot;va=
r0&quot;. This equality of results is however limited to direct variable re=
ferences with no subscripts. In Zsh, ${(!)var0} and ${(!)ref2} respectively=
 return &quot;abc def ghi&quot; and &quot;ref1&quot;, while in ksh=C2=A0${!=
var0} and ${!ref2} both return &quot;var0&quot; and in Zsh=C2=A0<span style=
=3D"background-color:transparent">${(!)ref1[1]} returns &quot;v&quot; while=
 in ksh=C2=A0</span><span style=3D"background-color:transparent">${!ref1[1]=
} returns &quot;</span><span style=3D"font-variant-ligatures:no-common-liga=
tures;color:rgb(0,0,0);background-color:transparent">var0[1]&quot;.</span><=
/font></div>





<div><font face=3D"arial, sans-serif"><br></font></div><div><font face=3D"a=
rial, sans-serif">This shows that even though=C2=A0</font><span style=3D"ba=
ckground-color:transparent">Zsh&#39;s (!) and ksh&#39;s=C2=A0! use the same=
 character flag and sometimes happen to return the same value, they really =
should not be considered as being related; they perform two fundamentally d=
ifferent things.</span></div><div><br></div></div></div><div class=3D"gmail=
_quote gmail_quote_container"><blockquote class=3D"gmail_quote" style=3D"ma=
rgin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:=
1ex"><div dir=3D"ltr"><blockquote class=3D"gmail_quote" style=3D"margin:0px=
 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">I g=
uess this would conflict with nameref-to-assoc, which currently does what I=
 would expect. (Unless I&#39;m missing something).<br><font face=3D"monospa=
ce">% typeset -n n=3Da</font><br><font face=3D"monospace">% echo ${n}</font=
><br><font face=3D"monospace">1 2</font><br><font face=3D"monospace">% echo=
 ${(k)n}</font><br><font face=3D"monospace">a b</font></blockquote><div dir=
=3D"ltr"><blockquote style=3D"margin:0px 0px 0px 40px;border-width:medium;b=
order-style:none;border-color:currentcolor;padding:0px"><font face=3D"monos=
pace">% typeset -A a=3D( a 1 b 2 )<br></font></blockquote></div><div><br></=
div>Indeed. For reference, ksh;<blockquote style=3D"margin:0px 0px 0px 40px=
;border-width:medium;border-style:none;border-color:currentcolor;padding:0p=
x"><div><font face=3D"monospace">$ typeset -A foo=3D([a]=3D1 [b]=3D2)</font=
></div><div><font face=3D"monospace">$ typeset -n bar=3Dfoo</font></div><di=
v><font face=3D"monospace">$ echo ${!bar}</font></div><div><font face=3D"mo=
nospace">foo</font></div><div><font face=3D"monospace">$ echo &quot;${!bar[=
@]}&quot;</font></div><div><font face=3D"monospace">a b</font></div><div><f=
ont face=3D"monospace"><br></font></div></blockquote><div><font face=3D"ari=
al, sans-serif">Given Zsh&#39;s default unsubscripted-array expansion behav=
ior, it seems like it would be hard to maintain the distinction between &qu=
ot;what does this nameref point to&quot; and &quot;what are that thing&#39;=
s keys&quot; if those semantics were combined in a single flag.</font></div=
></div></blockquote><div><br></div><div>Fully agree.</div><div><br></div><d=
iv>With the introduction of named references, Zsh&#39;s emulation of ksh&#3=
9;s=C2=A0! has been changed to <a href=3D"https://github.com/zsh-users/zsh/=
blob/3c0372aef88a6b7cc86f0902b1ba3f14a75118c9/Src/subst.c#L2121">map to Zsh=
&#39;s (!k)</a>. I guess that was done for the case of direct variable refe=
rences where Zsh&#39;s (!) happens to perform the same as ksh&#39;s ! but t=
hat combination breaks the example above and makes it difficult to explains=
 what Zsh&#39;s emulation of ksh&#39;s ! performs.</div><div><br></div><div=
><font face=3D"monospace">% typeset -A foo=3D([a]=3D1 [b]=3D2)</font></div>=
<div><font face=3D"monospace">% typeset -n bar=3Dfoo</font></div><div><font=
 face=3D"monospace">% emulate ksh</font></div><div><font face=3D"monospace"=
>% echo &quot;${!bar}&quot;</font></div><div><font face=3D"monospace">foo</=
font></div><div><font face=3D"monospace">%=C2=A0</font><span style=3D"font-=
family:monospace;background-color:transparent">echo &quot;${!bar[@]}&quot;&=
#39;</span></div><div><span style=3D"font-family:monospace">foo</span><span=
 style=3D"font-family:monospace;background-color:transparent"></span></div>=
<div><br></div><div>I think that this should be reverted such that=C2=A0Zsh=
&#39;s emulation of ksh&#39;s ! maps to Zsh&#39;s=C2=A0(k) as it used to do=
. It&#39;s true that the first result will then no longer match ksh&#39;s b=
ut the second will. I think it&#39;s a better trade-off because it doesn&#3=
9;t try to do two different things at the same time and thus is easier to e=
xplain and understand.</div><div><br></div><div>I will prepare a patch=E2=
=80=A6</div><div><br></div><div>Philippe</div><div><br></div></div></div>

--0000000000001e5ecc06571fd23a--