zsh word splitting behavior with variable and subshell expansion
TL;DR: With zsh’s default settings:
- Variable expansion
${name}
- NEVER undergoes word splitting, and even without quoting they’ll be passed with all spaces, newlines etc. intact.
- Subshell output
$(cat something)
- DOES undergo word splitting,
unless wrapped in quotes like
"$(cat something)"
(except when assigning the result to a variable).
- DOES undergo word splitting,
unless wrapped in quotes like
#!/usr/bin/env zsh
# -*- coding: utf-8 -*-
# The only differences I observed in the behavior of variable
# expansion were around how newlines and spaces are treated ("word
# splitting"). When word splitting is performed, newlines are turned
# into spaces, and consecutive spaces are collapsed into a single one.
#
# I never observed any issue with unicode characters, or special
# characters that are significant to the shell in regular syntax -
# these always passed through unmodified and uninterpreted.
# For debugging: Shows commands being invoked, with actual quoted
# arguments.
set -x
# Output some test data.
cmd() {
cat <<"EOF"
OneWord
Two Words
Many Spaces
Leading spaces!
An 'apostrophe', "quotes", $ sign, (parens), ? mark, \ backslash, \
'An '\''apostrophe'\'', "quotes", $ sign, (parens), ? mark, \ backslash, now escaped for a shell \'
c'est français
🤣
EOF
}
# NO SHELL BEHAVIOR: Store the output as a single scalar string.
# Spaces, newlines, special characters are intact. Either works.
content="$(cmd)"
content=$(cmd)
# NO SHELL BEHAVIOR: Echo the content exactly as it was stored, with
# spaces, newlines, special characters intact. Unlike bash, zsh
# doesn't perform word splitting on expanded variables by default, but
# you can change that with setopt SH_WORD_SPLIT. Either works.
echo "$content"
echo $content
# NO SHELL BEHAVIOR: Echo the content exactly, without storing it in
# a variable.
echo "$(cmd)"
# CAUSES SHELL BEHAVIOR: Perform word splitting: Newlines are treated
# as spaces, consecutive spaces are dropped,
echo $(cmd)
# CAUSES SHELL BEHAVIOR: ${=name} performs word splitting on $name
# even if it's quoted.
echo "${=content}"
# Any text surrounding ${=name} will get "glued" to the first & last
# words in $name after word splitting is performed, even if there are
# spaces in the surrounding text, but otherwise each word in $name
# gets passed as a separate argument.
#
# This split happens even if it looks like we're passing a single
# argument:
food='barbecue basil spam eggs'
print -rl -- "We have ${=food} and nothing else"
# Output:
# We have barbecue
# basil
# spam
# eggs and nothing else