Skip to content

Instantly share code, notes, and snippets.

@ormaaj
Last active April 27, 2024 22:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ormaaj/4942297 to your computer and use it in GitHub Desktop.
Save ormaaj/4942297 to your computer and use it in GitHub Desktop.
Array evaluation order tests
#!/usr/bin/env ksh
# Testcase runs in ksh93t or greater. Tests should run in any shell provided
# you can supply all the necessary workarounds, and they correctly interpret
# ksh93 printf %q output (requires $'...'). At least one level of recursive
# arithmetic variable evaluation must also be supported.
# Dan Douglas <ormaaj@gmail.com>
namespace main {
# e.g. add "set -x" to hacks
typeset -A shells=(
[mksh]=(
typeset -a hacks=('unset -v _')
vvar='print -r -- "mksh: $KSH_VERSION"'
)
[ksh]=(
typeset -a hacks=(
'typeset -a kshKludge alpha=({a..z})'
'function .sh.math.kshKludge x { printf "%s " ${alpha[x]}; }'
'function kshKludge.get { .sh.value=${.sh.name}\(${.sh.subscript}\); }'
)
vvar='print -r -- "ksh93: ${.sh.version}"'
)
[zsh]=(
typeset -a hacks=('emulate ksh')
vvar='print -r -- "zsh: $ZSH_VERSION"'
)
[bash]=(
typeset -a hacks=('shopt -s extglob lastpipe')
vvar='printf %s\\n "bash: $BASH_VERSION"'
)
)
typeset -a tests=(
# Simple assignment:
(
docstring='a[ $1 a ]=${b[ $2 b ]:=${c[ $3 c ]}}'
setup=
testcase='a[$(printf "1 " >&3)x[0]]=${b[$(printf "2 " >&3)${kshKludge[1]:-x[1]},0]:=${c[$(printf "3 " >&3)x[2]]}}'
)
# Integer array simple assignment:
(
docstring='a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]}'
setup='typeset -ia a'
testcase='a[$(printf "1 " >&3)x[0]]=${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=c[$(printf "3 " >&3)x[2]]}'
)
# simple assignment w/ integer PE assignment:
(
docstring='a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]}'
setup='typeset -ia b'
testcase='a[$(printf "1 " >&3)x[0]]=${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=c[$(printf "3 " >&3)x[2]]}'
)
# Both Integer array simple assignment + integer PE assignment:
(
docstring='a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]}'
setup='typeset -ia a b'
testcase='a[$(printf "1 " >&3)x[0]]=${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=c[$(printf "3 " >&3)x[2]]}'
)
# Array assignment within arithmetic context:
(
docstring='(( a[ $1 a ] = b[ $2 b ] ${c[ $3 c ]} ))'
setup=
testcase='((a[$(printf "1 " >&3)x[0]]=b[$(printf "2 " >&3)x[1],1]${c[${kshKludge[2]:-x[2]}$(printf "3 " >&3),1]}))'
)
# Array assignment within arithmetic context + PE assignment:
(
docstring='(( a[ $1 a ] = ${b[ $2 b ]:=c[ $3 c ]} ))'
setup='typeset -ia b'
testcase='((a[$(printf "1 " >&3)x[0]]=${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=c[$(printf "3 " >&3)x[2],1]}))'
)
# Compound assignment
(
docstring='a+=( [ $1 a ]=${b[ $2 b ]:=${c[ $3 c ]}} [ $4 d ]=$(( $5 e )) )'
setup='typeset -a a'
testcase='a+=([x[0]$(printf "1 " >&3),0]="${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=${c[$(printf "3 " >&3)x[2],1]}}" [x[3]$(printf "4 " >&3)]="$((x[4]$(printf "5 " >&3)))")'
)
# Compound integer array assignment
(
docstring='a+=( [ $1 a ]=${b[ $2 b ]:=c[ $3 c ]} [ $4 d ]=${5}e )'
setup='typeset -ia a'
testcase='a+=([x[0]$(printf "1 " >&3),0]="${b[${kshKludge[1]:-x[1]}$(printf "2 " >&3),0]:=c[$(printf "3 " >&3)x[2],1]}" [x[3]$(printf "4 " >&3)]="x[4]$(printf "5 " >&3)")'
)
)
# Allow sort-of Bash-like -v logic with printf. Nice ksh command
# substitution performance at the cost of stripping trailing newlines
function printfv {
if [[ $1 == -v && ${2:+_} ]]; then
nameref x=$2
shift 2
x=$(command printf "$@")
else
command printf "$@"
fi
}
# Combine commands to eval sequentially. Builds a "qball".
# ''eval $qball'' unravels it.
# Strings of nested escaping grow faster as nesting gets deeper!
# qball [ -v var ] var1 var2 var3 ...
function qball {
if [[ $1 == -v && ${2:+_} ]]; then
nameref ret=$2
typeset assign=ret
shift 2
else
typeset assign
fi
typeset x ref=x
while ! ${1:+false}; do
nameref var=$1
${2:+:} typeset ref=$assign
${var:+printfv ${ref:+-v "$ref"} 'eval %q' "${x:+${x};}${var}"}
shift
done
}
# Build scripts from the given template vars and run the testcases.
# Normally I dislike "shell templates". This seems an acceptable usage.
function runTests {
nameref t=$1 s=$2
typeset ts sh
# Bundle setup + testcase pairs into an eval-able tuple. This
# guarantees sequential commands are safely concatenated into one
# variable that may be evaluated within the same subshell.
# Print docstrings at the same time.
typeset -a allTests
for ts in "${!t[@]}"; do
allTests+=("$(qball ${t[ts].setup:+"t[$ts].setup"} "t[${ts}].testcase")")
printf '%-65s %s\n' "${t[ts].docstring}" "${t[ts].setup:-No attributes}"
done
echo
# This isn't as confusing as it looks. The outer heredoc is the
# testcase part, which only needs to expand once. The inner heredoc is
# shell-specific code expanded for each iteration.
for sh in "${!s[@]}"; do
{
printf '%s\n' "${ { _=$("$sh" -s </dev/fd/0 2>&1 >&3); } 3>&1;}" ${_:+"${sh} errors:" "$_"}
echo
} <<-EOF
set -f
# Setup shell-specific hacks
typeset z
for z in $(printf '%q ' "${s[$sh].hacks[@]}"); do
eval "\$z"
done
# Print the shell version string
${s[$sh].vvar}
# Inject the testcase code (probably faster than sourcing.)
$(</dev/fd/4)
EOF
done 3<&0 <<-EOF 4<&0 <&3-
function main {
# A variable to access through arithmetic recursion. Sadly, not all
# shells support nice sequence expansions.
typeset -a $(print -rn -- {a..z})
x+=($(printf '%q ' 'c[$(printf "%s " '{a..z}' >&3)1]'))
# Run the actual tests.
typeset testCode
for testCode; do
(eval "\$testCode")
echo
done 3>&1
}
# Ensure single-letter globals are free
unset -v $(print -rn -- {a..z})
main $(printf '%q ' "${allTests[@]}")
EOF
}
cat <<-"EOF"
Each testcase prints evaluation order for indexed array assignment
contexts. Each context is tested for expansions (represented by digits) and
arithmetic (letters), ordered from left to right within the expression. The
output corresponds to the way evaluation is re-ordered for each shell:
EOF
echo
set -f # optional
# typeset -ft runTests
runTests tests shells
}
# vim: set fenc=utf-8 ff=unix ft=sh :
Each testcase prints evaluation order for indexed array assignment
contexts. Each context is tested for expansions (represented by digits) and
arithmetic (letters), ordered from left to right within the expression. The
output corresponds to the way evaluation is re-ordered for each shell:
a[ $1 a ]=${b[ $2 b ]:=${c[ $3 c ]}} No attributes
a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]} typeset -ia a
a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]} typeset -ia b
a[ $1 a ]=${b[ $2 b ]:=c[ $3 c ]} typeset -ia a b
(( a[ $1 a ] = b[ $2 b ] ${c[ $3 c ]} )) No attributes
(( a[ $1 a ] = ${b[ $2 b ]:=c[ $3 c ]} )) typeset -ia b
a+=( [ $1 a ]=${b[ $2 b ]:=${c[ $3 c ]}} [ $4 d ]=$(( $5 e )) ) typeset -a a
a+=( [ $1 a ]=${b[ $2 b ]:=c[ $3 c ]} [ $4 d ]=${5}e ) typeset -ia a
bash: 4.2.42(1)-release
2 b 3 c 2 b 1 a
2 b 3 2 b 1 a c
2 b 3 2 b c 1 a
2 b 3 2 b c 1 a c
1 2 3 c b a
1 2 b 3 2 b c c a
1 2 b 3 c 2 b 4 5 e a d
1 2 b 3 2 b 4 5 a c d e
ksh93: Version AJM 93v- 2013-02-22
1 2 b b a
1 2 b b a
1 2 b b a
1 2 b b a
1 2 3 c b a
1 2 b b a
1 2 b b a 4 5 e d
1 2 b b a 4 5 d e
mksh: @(#)MIRBSD KSH R44 2013/02/24
2 b 3 c 1 a
2 b 3 1 a c
2 b 3 c 1 a
2 b 3 c 1 a
1 2 3 c a b
1 2 b 3 c a
1 2 b 3 c 4 5 e a d
1 2 b 3 4 5 a c d e
zsh: 5.0.2
2 b 3 c 2 b 1 a
2 b 3 2 b 1 a c
2 b 1 a
2 b 1 a
1 2 3 c b a
1 2 b a
1 2 b 3 c 2 b 4 5 e
1 2 b 3 2 b 4 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment