Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ This documents significant changes in the dev branch of ksh 93u+m.
For full details, see the git log at: https://github.com/ksh93/ksh
Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library.

2026-02-10:

- Support for ${$var} indirect expansion has been backported from ksh93v-.
If the variable var expands to a variable name, ${$var} expands to the
value of that variable. Otherwise, it expands to an empty string.

2026-02-06:

- Fixed a file descriptor leak introduced in 93u+ 2012-07-27 that occurred
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/ksh93/include/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#include <ast_release.h>
#include "git.h"

#define SH_RELEASE_DATE "2026-02-09" /* must be in this format for $((.sh.version)) */
#define SH_RELEASE_DATE "2026-02-10" /* must be in this format for $((.sh.version)) */
/*
* This comment keeps SH_RELEASE_DATE a few lines away from SH_RELEASE_SVER to avoid
* merge conflicts when cherry-picking dev branch commits onto a release branch.
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/ksh93/sh.1
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,11 @@ Expands to the type name (see
below) or attributes of the variable referred to by
.IR vname .
.TP
\f3${$\fP\f2parameter\^\fP\f3}\fP
If \f3$\fP\f2parameter\fP expands to the name of a variable, this expands
to the value of that variable. Otherwise, it expands to an empty string.
It is undefined for special parameters.
.TP
\f3${!\fP\f2vname\^\fP\f3}\fP
Expands to the name of the variable referred to by
.IR vname .
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/ksh93/sh/lex.c
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ int sh_lex(Lex_t* lp)
break;
case '@':
case '!':
if(n!=S_ALP)
if(n!=S_ALP && n!=S_DIG)
goto dolerr;
/* FALLTHROUGH */
case '#':
Expand Down
55 changes: 53 additions & 2 deletions src/cmd/ksh93/sh/macro.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ typedef struct _mac_
#define M_NAMESCAN 6 /* ${!var*} */
#define M_NAMECOUNT 7 /* ${#var*} */
#define M_TYPE 8 /* ${@var} */
#define M_EVAL 9 /* ${$var} */

static noreturn void mac_error(void);
static int substring(const char*, size_t, const char*, int[], int);
Expand Down Expand Up @@ -1206,6 +1207,12 @@ static int varsub(Mac_t *mp)
}
/* FALLTHROUGH */
case S_SPC2:
if(type==M_BRACE && c=='$' && isalnum(mode=fcpeek(0)))
{
type = M_EVAL;
mode = c;
goto retry1;
}
var = 0;
*id = c;
v = special(c);
Expand Down Expand Up @@ -1402,6 +1409,39 @@ static int varsub(Mac_t *mp)
sfputr(sh.strbuf,id,-1);
id = sfstruse(sh.strbuf);
}
if(type==M_EVAL && np && (v=nv_getval(np)))
{
/* ${$var} indirect expansion */
char *last;
long x;
errno = 0;
x = strtol(v,&last,10);
type = M_BRACE;
if(*last==0)
{
int n = (int)x;
if(errno==ERANGE || x!=n || x<0)
{
errormsg(SH_DICT,ERROR_system(1),e_number,v);
UNREACHABLE();
}
np = NULL;
v = NULL;
idnum = 0;
if(n==0)
v = special(n);
else if(n<=sh.st.dolc)
{
sh.used_pos = 1;
v = sh.st.dolv[n];
idnum = n;
}
fcseek(-LEN);
stkseek(stkp, offset);
break;
} else
np = nv_open(v,sh.var_tree,flag|NV_NOFAIL);
}
if(isastchar(mode))
var = 0;
if((!np || nv_isnull(np)) && type==M_BRACE && c==RBRACE && !(flag&NV_ARRAY) && strchr(id,'.'))
Expand Down Expand Up @@ -1560,7 +1600,7 @@ static int varsub(Mac_t *mp)
v = id;
type = M_BRACE;
}
else if(type==M_TYPE)
else if(type==M_TYPE || type==M_EVAL)
type = M_BRACE;
}
stkseek(stkp,offset);
Expand Down Expand Up @@ -1590,7 +1630,7 @@ static int varsub(Mac_t *mp)
c = fcget();
if(type>M_TREE)
{
if(c!=RBRACE)
if(c!=RBRACE && type!=M_EVAL)
mac_error();
if(type==M_NAMESCAN || type==M_NAMECOUNT)
{
Expand Down Expand Up @@ -1626,6 +1666,16 @@ static int varsub(Mac_t *mp)
v = nv_getsub(np);
}
}
else if(type==M_EVAL)
{
/* ${$var} indirect expansion */
np = v ? nv_open(v,sh.var_tree,NV_NOREF|NV_NOADD|NV_VARNAME|NV_NOFAIL) : NULL;
if(np)
{
v = nv_getval(np);
goto skip;
}
}
else
{
/* type==M_SIZE: ${#var} */
Expand Down Expand Up @@ -1655,6 +1705,7 @@ static int varsub(Mac_t *mp)
}
c = RBRACE;
}
skip:
nulflg = 0;
if(type && c==':')
{
Expand Down
65 changes: 64 additions & 1 deletion src/cmd/ksh93/tests/variables.sh
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ set -- "${@-}"
if (( $# !=1 ))
then err_exit '"${@-}" not expanding to null string'
fi
for i in : % + / 3b '**' '***' '@@' '{' '[' '}' !! '*a' '$foo'
for i in : % + / 3b '**' '***' '@@' '{' '[' '}' !! '*a' '$$'
do (eval : \${"$i"} 2> /dev/null) && err_exit "\${$i} not a syntax error"
done

Expand Down Expand Up @@ -1811,5 +1811,68 @@ do got=$(eval "
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
done

# ======
# Tests for ${$var} indirection
unset foo bar
bar=correct
foo=bar
[[ ${$foo} == correct ]] || err_exit "\${\$foo} doesn't point to \$bar" \
"(expected '$bar', got $(printf %q "${$foo}"))"

set abc def
abc=foo
def=bar
[[ ${$2:1:1} == a ]] || err_exit '${$2:1:1} not correct with $2=def and def=bar'
OPTIND=2
[[ ${$OPTIND:1:1} == e ]] || err_exit '${$OPTIND:1:1} not correct with OPTIND=2 and $2=def'

# If bar is set and expands to a variable that isn't set, we should get an empty string
got=$("$SHELL" -c '
function foo {
print ${$bar}
print ${$bar:1:1}
}
bar=set_paramater_is_not_a_var
foo
exit 0
')
[[ -z "$got" ]] || err_exit "\${\$bar} produces unexpected results when \$bar expands to the name of an unset variable" \
"(got $(printf %q "$got"))"

# If bar is not set and the syntax is valid, we should get an empty string
got=$("$SHELL" -c '
unset bar
echo ${$bar}
echo ${$bar:0:1}
')
[[ -z $got ]] || err_exit "\${\$bar} should produce an empty string when the variable bar is not set" \
"(got $(printf %q "$got"))"

# If bar is not set and the syntax is invalid, we should get a syntax error
got=$(set +x; "$SHELL" -c '
unset bar
echo ${$bar:^&^&garbage*^&^}
' 2>&1)
(($? == 1)) || err_exit "\${\$bar:^&^&garbage*^&^} should produce a syntax error" \
"(got $(printf %q "$got"))"

# Integer overflow shouldn't cause ksh to crash or print unexpected results
got=$(set +x; "$SHELL" -c '
foo=88888888888888899999999
echo ${$foo}
' 2>&1)
ret=$?
((ret == 1)) || err_exit "ksh cannot cope with integer overflow when using variable indirection" \
"(got status $ret, output: $(printf %q "$got"))"

# Negative numbers ought to also produce an error, not crash
got=$(set +x; "$SHELL" -c '
foo=-3
echo ${$foo}
' 2>&1)
ret=$?
((ret == 1)) || err_exit "ksh cannot cope with negative numbers when using variable indirection" \
"(got status $ret, output: $(printf %q "$got"))"

# ======
exit $((Errors<125?Errors:125))