diff --git a/NEWS b/NEWS index 404633eeb72b..228cf8af9e14 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/src/cmd/ksh93/include/version.h b/src/cmd/ksh93/include/version.h index 4dce00ef7306..23ff3e0734bc 100644 --- a/src/cmd/ksh93/include/version.h +++ b/src/cmd/ksh93/include/version.h @@ -18,7 +18,7 @@ #include #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. diff --git a/src/cmd/ksh93/sh.1 b/src/cmd/ksh93/sh.1 index cb345341eb46..107c3705547b 100644 --- a/src/cmd/ksh93/sh.1 +++ b/src/cmd/ksh93/sh.1 @@ -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 . diff --git a/src/cmd/ksh93/sh/lex.c b/src/cmd/ksh93/sh/lex.c index 1160c57f3bf2..b01fee53fe27 100644 --- a/src/cmd/ksh93/sh/lex.c +++ b/src/cmd/ksh93/sh/lex.c @@ -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 '#': diff --git a/src/cmd/ksh93/sh/macro.c b/src/cmd/ksh93/sh/macro.c index a90fcc54811c..d4dca5277ca5 100644 --- a/src/cmd/ksh93/sh/macro.c +++ b/src/cmd/ksh93/sh/macro.c @@ -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); @@ -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); @@ -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,'.')) @@ -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); @@ -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) { @@ -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} */ @@ -1655,6 +1705,7 @@ static int varsub(Mac_t *mp) } c = RBRACE; } +skip: nulflg = 0; if(type && c==':') { diff --git a/src/cmd/ksh93/tests/variables.sh b/src/cmd/ksh93/tests/variables.sh index 8976cfaad04c..8757594fd2b0 100755 --- a/src/cmd/ksh93/tests/variables.sh +++ b/src/cmd/ksh93/tests/variables.sh @@ -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 @@ -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))