# Iteration 12: Command Substitution
# === Basic $(...) form ===
=== simple command substitution
echo $(pwd)
---
(command (word "echo") (word "$(pwd)"))
---
=== command substitution with args
echo $(ls -la)
---
(command (word "echo") (word "$(ls -la)"))
---
=== command substitution in middle of word
echo prefix$(pwd)suffix
---
(command (word "echo") (word "prefix$(pwd)suffix"))
---
=== command substitution at start
echo $(echo hi)there
---
(command (word "echo") (word "$(echo hi)there"))
---
=== command substitution at end
echo hello$(echo world)
---
(command (word "echo") (word "hello$(echo world)"))
---
=== multiple command substitutions
echo $(echo a)$(echo b)
---
(command (word "echo") (word "$(echo a)$(echo b)"))
---
=== command substitution with pipeline
echo $(cat file | grep pattern)
---
(command (word "echo") (word "$(cat file | grep pattern)"))
---
=== command substitution with list
echo $(echo a; echo b)
---
(command (word "echo") (word "$(echo a; echo b)"))
---
=== nested command substitution
echo $(echo $(pwd))
---
(command (word "echo") (word "$(echo $(pwd))"))
---
=== deeply nested substitution
echo $(echo $(echo $(pwd)))
---
(command (word "echo") (word "$(echo $(echo $(pwd)))"))
---
# === In double quotes ===
=== command substitution in double quotes
echo "$(pwd)"
---
(command (word "echo") (word "\"$(pwd)\""))
---
=== command substitution with text in quotes
echo "Current dir: $(pwd)"
---
(command (word "echo") (word "\"Current dir: $(pwd)\""))
---
=== command substitution and variable in quotes
echo "User $(whoami) at $PWD"
---
(command (word "echo") (word "\"User $(whoami) at $PWD\""))
---
# === Backtick form ===
=== backtick simple
echo `pwd`
---
(command (word "echo") (word "`pwd`"))
---
=== backtick with args
echo `ls -la`
---
(command (word "echo") (word "`ls -la`"))
---
=== backtick in word
echo prefix`pwd`suffix
---
(command (word "echo") (word "prefix`pwd`suffix"))
---
=== backtick in double quotes
echo "`pwd`"
---
(command (word "echo") (word "\"`pwd`\""))
---
=== backtick with pipeline
echo `cat file | head -1`
---
(command (word "echo") (word "`cat file | head -1`"))
---
# Regression: the backtick body is parsed by the real parser (fork-
# and-merge), so constructs that need grammar-aware parsing (case
# patterns, control flow, word splitting) work structurally rather
# than via a char-level mini-scanner.
=== case statement inside backtick
x=`case y in (a) echo hi;; esac`
---
(command (word "x=`case y in (a) echo hi;; esac`"))
---
=== if statement inside backtick
x=`if true; then echo hi; fi`
---
(command (word "x=`if true; then echo hi; fi`"))
---
=== for loop inside backtick
x=`for i in a b; do echo $i; done`
---
(command (word "x=`for i in a b; do echo $i; done`"))
---
# Regression: the inner fork consumes backslash-escape pairs (\\, \`,
# \$) via Lexer::peek_backtick_escape so the forked parser sees the
# escaped char rather than the raw backslash. Raw text is still copied
# into wb.value verbatim so the S-expression output round-trips.
=== escaped dollar inside backtick
echo `echo \$HOME`
---
(command (word "echo") (word "`echo \\$HOME`"))
---
=== backtick with control flow inside double quotes
echo "`if true; then echo a; fi`"
---
(command (word "echo") (word "\"`if true; then echo a; fi`\""))
---
=== double backslash inside backtick
echo `printf %s \\`
---
(command (word "echo") (word "`printf %s \\\\`"))
---
=== nested cmdsub with case inside backtick
x=`echo $(case y in (a) z;; esac)`
---
(command (word "x=`echo $(case y in (a) z;; esac)`"))
---
=== nested backticks via backslash escape
x=`echo \`date\``
---
(command (word "x=`echo \\`date\\``"))
---
# === Mixed forms ===
=== dollar and backtick
echo $(pwd) `date`
---
(command (word "echo") (word "$(pwd)") (word "`date`"))
---
=== dollar inside backtick
echo `echo $(pwd)`
---
(command (word "echo") (word "`echo $(pwd)`"))
---
# === With redirections inside ===
=== command substitution with redirect
echo $(cat < file)
---
(command (word "echo") (word "$(cat < file)"))
---
=== command substitution with output redirect
x=$(cmd > /dev/null)
---
(command (word "x=$(cmd > /dev/null)"))
---
# === With control structures inside ===
=== command substitution with if
echo $(if true; then echo yes; fi)
---
(command (word "echo") (word "$(if true; then\n echo yes;\nfi)"))
---
=== command substitution with for
echo $(for i in a b; do echo $i; done)
---
(command (word "echo") (word "$(for i in a b;\ndo\n echo $i;\ndone)"))
---
=== command substitution with case
echo $(case $x in a) echo a;; esac)
---
(command (word "echo") (word "$(case $x in a)\n echo a\n ;;\nesac)"))
---
=== case with redirect inside cmdsub
x="$(case y in a) z ;; esac 2>/dev/null)"
---
(command (word "x=\"$(case y in a)\n z\n ;;\nesac 2> /dev/null)\""))
---
=== case with leading paren in cmdsub
x="$(case y in (a) z;; esac)"
---
(command (word "x=\"$(case y in a)\n z\n ;;\nesac)\""))
---
# === In assignments ===
=== simple assignment with cmdsub
foo=$(pwd)
---
(command (word "foo=$(pwd)"))
---
=== export with cmdsub
export PATH=$(pwd):$PATH
---
(command (word "export") (word "PATH=$(pwd):$PATH"))
---
# === Empty and edge cases ===
=== empty command substitution
echo $()
---
(command (word "echo") (word "$()"))
---
=== command substitution whitespace only
echo $( )
---
(command (word "echo") (word "$()"))
---
=== command substitution with newline
echo $(
pwd
)
---
(command (word "echo") (word "$(pwd)"))
---
# === Brutal edge cases ===
# Subshell inside command substitution - note space after $(
=== subshell inside cmdsub
echo $( (echo in subshell) )
---
(command (word "echo") (word "$( ( echo in subshell ))"))
---
=== brace group inside cmdsub
echo $( { echo brace; } )
---
(command (word "echo") (word "$({ echo brace; })"))
---
=== while inside cmdsub
echo $(while false; do echo x; done)
---
(command (word "echo") (word "$(while false; do\n echo x;\ndone)"))
---
=== until inside cmdsub
echo $(until true; do echo x; done)
---
(command (word "echo") (word "$(until true; do\n echo x;\ndone)"))
---
=== function definition inside cmdsub
echo $(f() { echo hi; }; f)
---
(command (word "echo") (word "$(function f () \n{ \n echo hi\n}; f)"))
---
=== background job in cmdsub
echo $(echo bg &)
---
(command (word "echo") (word "$(echo bg &)"))
---
=== backgrounded heredoc in cmdsub
echo $(cat <<EOF &
body
EOF
)
---
(command (word "echo") (word "$(cat <<EOF &\nbody\nEOF\n)"))
---
=== multiple statements in cmdsub
echo $(echo a; echo b; echo c)
---
(command (word "echo") (word "$(echo a; echo b; echo c)"))
---
=== and list inside cmdsub
echo $(true && echo yes)
---
(command (word "echo") (word "$(true && echo yes)"))
---
=== or list inside cmdsub
echo $(false || echo fallback)
---
(command (word "echo") (word "$(false || echo fallback)"))
---
# Complex case statements inside command substitution
=== case with multiple patterns in cmdsub
echo $(case $x in a) echo a;; b) echo b;; *) echo default;; esac)
---
(command (word "echo") (word "$(case $x in a)\n echo a\n ;;\n b)\n echo b\n ;;\n *)\n echo default\n ;;\nesac)"))
---
=== case with or pattern in cmdsub
echo $(case $x in a|b|c) echo match;; esac)
---
(command (word "echo") (word "$(case $x in a | b | c)\n echo match\n ;;\nesac)"))
---
=== nested case in cmdsub
echo $(case $a in x) case $b in y) echo xy;; esac;; esac)
---
(command (word "echo") (word "$(case $a in x)\n case $b in y)\n echo xy\n ;;\n esac\n ;;\nesac)"))
---
# Nested if/elif/else inside cmdsub
=== if else inside cmdsub
echo $(if true; then echo yes; else echo no; fi)
---
(command (word "echo") (word "$(if true; then\n echo yes;\nelse\n echo no;\nfi)"))
---
=== elif chain inside cmdsub
echo $(if false; then echo a; elif true; then echo b; else echo c; fi)
---
(command (word "echo") (word "$(if false; then\n echo a;\nelse\n if true; then\n echo b;\n else\n echo c;\n fi;\nfi)"))
---
# Quotes inside command substitution
=== double quotes inside cmdsub
echo $(echo "hello world")
---
(command (word "echo") (word "$(echo \"hello world\")"))
---
=== single quotes inside cmdsub
echo $(echo 'hello world')
---
(command (word "echo") (word "$(echo 'hello world')"))
---
=== mixed quotes inside cmdsub
echo $(echo "it's" 'a "test"')
---
(command (word "echo") (word "$(echo \"it's\" 'a \"test\"')"))
---
=== escaped quotes in cmdsub
echo $(echo \"quoted\")
---
(command (word "echo") (word "$(echo \\\"quoted\\\")"))
---
# Nested command substitution variations
=== triple nested cmdsub
echo $(echo $(echo $(echo deep)))
---
(command (word "echo") (word "$(echo $(echo $(echo deep)))"))
---
=== cmdsub in middle of other cmdsub
echo $(echo start $(pwd) end)
---
(command (word "echo") (word "$(echo start $(pwd) end)"))
---
=== multiple cmdsubs in one inner command
echo $(echo $(whoami) at $(pwd))
---
(command (word "echo") (word "$(echo $(whoami) at $(pwd))"))
---
# Backtick edge cases
=== empty backtick
echo ``
---
(command (word "echo") (word "``"))
---
=== backtick with variable
echo `echo $HOME`
---
(command (word "echo") (word "`echo $HOME`"))
---
=== backtick with quotes
echo `echo "hello"`
---
(command (word "echo") (word "`echo \"hello\"`"))
---
# File reading shortcut (bash extension)
=== file reading shortcut
echo $(<file.txt)
---
(command (word "echo") (word "$(< file.txt)"))
---
=== file reading with path
echo $(</etc/passwd)
---
(command (word "echo") (word "$(< /etc/passwd)"))
---
# Command substitution as command itself
=== cmdsub as command
$(echo echo) hello
---
(command (word "$(echo echo)") (word "hello"))
---
=== cmdsub produces pipeline
$(echo "cat | head") file
---
(command (word "$(echo \"cat | head\")") (word "file"))
---
# Parameter expansion inside command substitution
=== param expansion inside cmdsub
echo $(echo ${foo:-default})
---
(command (word "echo") (word "$(echo ${foo:-default})"))
---
=== complex param in cmdsub
echo $(echo ${x##*/})
---
(command (word "echo") (word "$(echo ${x##*/})"))
---
# Special characters
=== cmdsub with semicolons
echo $(echo a; echo b; echo c;)
---
(command (word "echo") (word "$(echo a; echo b; echo c)"))
---
=== cmdsub with newlines and semicolons
echo $(
echo a
echo b
)
---
(command (word "echo") (word "$(echo a\necho b)"))
---
# NOTE: $((expr)) is now properly parsed as arithmetic expansion (see 13_arithmetic.tests)
# Command substitution with special positional params
=== cmdsub with positional param
echo $(echo $1)
---
(command (word "echo") (word "$(echo $1)"))
---
=== cmdsub with all args
echo $(echo $@)
---
(command (word "echo") (word "$(echo $@)"))
---
=== cmdsub with exit status
echo $(echo $?)
---
(command (word "echo") (word "$(echo $?)"))
---
# Real-world patterns
=== dirname pattern
dir=$(cd "$(dirname "$0")" && pwd)
---
(command (word "dir=$(cd \"$(dirname \"$0\")\" && pwd)"))
---
=== basename pattern
name=$(basename "$file" .txt)
---
(command (word "name=$(basename \"$file\" .txt)"))
---
=== find exec pattern
files=$(find . -name "*.txt")
---
(command (word "files=$(find . -name \"*.txt\")"))
---
=== command -v check
if cmd=$(command -v git); then echo found; fi
---
(if (command (word "cmd=$(command -v git)")) (command (word "echo") (word "found")))
---
=== redirect before command in cmdsub
echo $(<file cmd)
---
(command (word "echo") (word "$(cmd < file)"))
---
=== cmdsub with redirect in param expansion default
ver=${GOVERSION:-$(<${file} jq -r .GoVersion)}
---
(command (word "ver=${GOVERSION:-$(jq -r .GoVersion < ${file})}"))
---
=== cmdsub with here-string in double quotes
echo "[$(cmd <<< a)]"
---
(command (word "echo") (word "\"[$(cmd <<< a)]\""))
---
=== cmdsub inside single quotes within double quotes
echo "a='$( cmd )'"
---
(command (word "echo") (word "\"a='$(cmd)'\""))
---
=== cmdsub with pipe-both operator
x="$(cmd |& cat)"
---
(command (word "x=\"$(cmd 2>&1 | cat)\""))
---
=== cmdsub normalizes 1> to >
x=$(cmd 1>/dev/null)
---
(command (word "x=$(cmd > /dev/null)"))
---
=== heredoc delimiter with closing paren on same line
x=$(cat <<E
hello
E)
---
(command (word "x=$(cat <<E\nhello\nE\n)"))
---
=== while loop with redirect in cmdsub
x=$(while true; do echo x;done < /f)
---
(command (word "x=$(while true; do\n echo x;\ndone < /f)"))
---
=== C-style for loop in cmdsub
x=$(for ((i=1; i<=n; i++)); do echo $i; done)
---
(command (word "x=$(for ((i=1; i<=n; i++))\ndo\n echo $i;\ndone)"))
---
=== escaped dollar brace is not brace cmdsub
x="\${ ${VAR}"
---
(command (word "x=\"\\${ ${VAR}\""))
---
=== conditional expression in loop inside cmdsub
x=$(while [[ $a ]]; do echo x; done)
---
(command (word "x=$(while [[ -n $a ]]; do\n echo x;\ndone)"))
---
=== negated command in cmdsub
x=$(! cmd arg)
---
(command (word "x=$(! cmd arg)"))
---
=== time command in cmdsub
x=$(time cmd arg)
---
(command (word "x=$(time cmd arg)"))
---
=== cmdsub with comment in array assignment
informergen_external_apis=(
$(
cd ${KUBE_ROOT}/staging/src
# because client-gen doesn't do policy/v1alpha1, we have to skip it too
find k8s.io/api -name types.go | xargs -n1 dirname | sort | grep -v pkg.apis.policy.v1alpha1
)
)
---
(command (word "informergen_external_apis=($(cd ${KUBE_ROOT}/staging/src\nfind k8s.io/api -name types.go | xargs -n1 dirname | sort | grep -v pkg.apis.policy.v1alpha1))"))
---
=== brace group with redirect in cmdsub pipeline
x=$({ echo a; } 2>/dev/null | cat)
---
(command (word "x=$({ echo a; } 2> /dev/null | cat)"))
---
=== cond-not inside cmdsub
x=$(if [[ ! ${y} ]]; then echo z; fi)
---
(command (word "x=$(if [[ ! -n ${y} ]]; then\n echo z;\nfi)"))
---
=== nested for loop do indentation in cmdsub
x=$(for a in b; do for c in d; do echo e; done; done)
---
(command (word "x=$(for a in b;\ndo\n for c in d;\n do\n echo e;\n done;\ndone)"))
---
=== subshell inside command substitution
x=$(((echo hi)))
---
(command (word "x=$(((echo hi)))"))
---
=== heredoc with pipe in cmdsub
x=$(cat <<EOF |
hello
EOF
cmd)
---
(command (word "x=$(cat <<EOF |\nhello\nEOF\n cmd)"))
---
=== heredoc with pipe in cmdsub
x=$(cat <<END | wc
hello
END
)
---
(command (word "x=$(cat <<END |\nhello\nEND\n wc)"))
---
=== normalize 1>& to >& with variable target in cmdsub
x=$(echo 1>&$fd)
---
(command (word "x=$(echo >&$fd)"))
---
=== escaped dollar followed by cmdsub in double quotes
echo "\$(A) $(B)"
---
(command (word "echo") (word "\"\\$(A) $(B)\""))
---
# MRE: Whitespace after heredoc delimiter in command substitution
# Parable preserves leading spaces, bash normalizes to newline
=== heredoc followed by indented command in cmdsub
amu=$({ cat <<EOF
line1
EOF
cat file; })
---
(command (word "amu=$({ cat <<EOF\nline1\nEOF\n\ncat file; })"))
---
# Parameter length ${#var} inside command substitution
# The # after { must not be treated as a comment
=== param length inside cmdsub
echo $(echo ${#var})
---
(command (word "echo") (word "$(echo ${#var})"))
---
=== param length at start of cmdsub
echo $(${#x})
---
(command (word "echo") (word "$(${#x})"))
---
=== array length inside cmdsub
echo $(echo ${#arr[@]})
---
(command (word "echo") (word "$(echo ${#arr[@]})"))
---
=== param length in assignment inside cmdsub
x=$(y=${#z}; echo $y)
---
(command (word "x=$(y=${#z}; echo $y)"))
---
=== multiple param lengths inside cmdsub
echo $(echo ${#a} ${#b})
---
(command (word "echo") (word "$(echo ${#a} ${#b})"))
---
=== param length nested in inner cmdsub
echo $(echo $(echo ${#var}))
---
(command (word "echo") (word "$(echo $(echo ${#var}))"))
---
=== heredoc in cmdsub with unmatched open paren in body
x=$(cat <<'EOF'
foo
(bar
EOF
)
---
(command (word "x=$(cat <<'EOF'\nfoo\n(bar\nEOF\n)"))
---
=== heredoc in cmdsub with unmatched close paren in body
x=$(cat <<EOF
foo
bar)
EOF
)
---
(command (word "x=$(cat <<EOF\nfoo\nbar)\nEOF\n)"))
---
=== heredoc in cmdsub with tab stripping and unmatched paren
x=$(cat <<-EOF
(indented unclosed
EOF
)
---
(command (word "x=$(cat <<-EOF\n(indented unclosed\nEOF\n)"))
---
=== herestring in cmdsub is not heredoc
x=$(cat <<<"(oops")
---
(command (word "x=$(cat <<< \"(oops\")"))
---
=== arithmetic left shift in cmdsub is not heredoc
x=$(( 1 << 4 ))
---
(command (word "x=$(( 1 << 4 ))"))
---
=== two heredocs in cmdsub drain in order
x=$(cat <<A <<B
foo
A
bar
B
)
---
(command (word "x=$(cat <<A <<B\nfoo\nA\nbar\nB\n)"))
---
=== heredoc in cmdsub with double-quoted delimiter
x=$(cat <<"EOF"
(body
EOF
)
---
(command (word "x=$(cat <<'EOF'\n(body\nEOF\n)"))
---
=== heredoc in cmdsub with backslash-escaped delimiter
x=$(cat <<\EOF
(unclosed
EOF
)
---
(command (word "x=$(cat <<'EOF'\n(unclosed\nEOF\n)"))
---
=== nested cmdsub with inner heredoc and unmatched paren
x=$(echo $(cat <<E
(body
E
))
---
(command (word "x=$(echo $(cat <<E\n(body\nE\n))"))
---
=== empty heredoc body in cmdsub with unmatched outer paren
x=$(cat <<EOF
EOF
)
---
(command (word "x=$(cat <<EOF\nEOF\n)"))
---
# Regression: the inner fork's heredoc state must not leak into the outer
# parser's pending-heredoc queue. The outer `<<EOF` is queued AFTER the
# cmdsub is fully consumed, and its body must be read from the lines that
# follow the outer command line.
=== outer heredoc follows cmdsub argument
cat $(echo foo) <<EOF
body
EOF
---
(command (word "cat") (word "$(echo foo)") (redirect "<<" "body
"))
---
# --- #40 follow-ups: edge paths exercised by the canonical reformatter ---
=== tab-stripped quoted heredoc in cmdsub
x=$(cat <<-'EOF'
body
EOF
)
---
(command (word "x=$(cat <<-'EOF'\nbody\nEOF\n)"))
---
=== function body with && separator in cmdsub
echo $(f() { hi && echo hi; }; f)
---
(command (word "echo") (word "$(function f () \n{ \n hi && echo hi\n}; f)"))
---
=== varfd close-fd in cmdsub
echo $(exec {fd}>&-)
---
(command (word "echo") (word "$(exec {fd}>&-)"))
---
# --- #39 follow-ups: sloppy heredoc terminator edge cases ---
=== tab-stripped heredoc with trailing parens in cmdsub
$( ( cat <<-E
body
E ) )
---
(command (word "$( ( cat <<-E\nbody\nE\n ))"))
---
=== heredoc regular heredoc interleaved in cmdsub
$(cat <<A >>o <<B
one
A
two
B
)
---
(command (word "$(cat <<A >> o <<B\none\nA\ntwo\nB\n)"))
---
# Negative: trailing whitespace on delimiter line must not trigger the
# sloppy-match path — it's the existing `trim_end() == delimiter`
# path that accepts this, not #39's paren-suffix logic.
=== heredoc trailing whitespace in cmdsub
$(cat <<EOF
body
EOF
)
---
(command (word "$(cat <<EOF\nbody\nEOF\n)"))
---