Bash – $-expansions not performed as expected when following a redirection

assignmentbashio-redirectionshellzsh

It seems that bash and zsh will perform the variable and arithmetic expansions in a child process when

a) they're following a redirection operator like <, >, >> or <<<.

b) the command they're part of is not a built-in or function.

bash -c 'i=0; /bin/echo > $((i=7)).txt; echo $i'
0
zsh -c 'i=0; /bin/echo > $((i=7)).txt; echo $i'
0

ksh -c 'i=0; /bin/echo > $((i=7)).txt; echo $i'
7

ksh above is like any other shell except bash or zsh.

This has nothing to do with those being arithmetic expansions: analogously, the same thing happens with

unset i; /bin/echo >${i:=7}.txt; echo $i

will only print 7 in shells other than bash or zsh.

However, as if this weren't bad enough, the behavior is not consistent in any fathomable way between bash and zsh:

bash -c 'i=0; command echo > $((i++)).txt; echo $i'
1
zsh -c 'i=0; command echo > $((i++)).txt; echo $i'
0

bash -c 'i=0; i=$i /usr/bin/printenv i > $((++i)).bash; echo $i; cat *.bash'
0
0
zsh -c 'i=0; i=$i /usr/bin/printenv i > $((++i)).zsh; echo $i; cat *.zsh'
0
1

So, my question is: What does the standard say? Is this acceptable?

I was able to find a lot about variable assignments as in KEY=val cmd and when they may or may not "affect the current execution environment", but nothing about the interaction between redirections, $-expansions and external commands.

And it could NOT be that it also applies to the variable assignments done as part of $-expansions, because ls $((i=2+3)) results in i being set to 5 in all the shells no matter if ls is an external command or a built-in.

Best Answer

This is left unspecified, so each shell is allowed to do what it wants, and doesn't have to document the details. (From a historical perspective, it was unspecified because different shells did different things.) Technically a shell would be allowed to flip a coin. In practice, the details of what runs in a separate environment and what doesn't can depend on optimizations made in certain cases, e.g. depending on whether a command is a built-in, depending on whether a redirection is to/from /dev/null, depending on whether traps are active, depending on whether set -e is in effect, based on whether the command is the last one in a list, etc.

From SUSv4 (POSIX.1-2008) “Shell and Utilities” — §2.9.1 ”Simple Commands“:

If the command name is not a special built-in utility or function, the variable assignments shall be exported for the execution environment of the command and shall not affect the current execution environment except as a side-effect of the expansions performed in step 4. In this case it is unspecified:

  • Whether or not the assignments are visible for subsequent expansions in step 4

  • Whether variable assignments made as side-effects of these expansions are visible for subsequent expansions in step 4, or in the current shell execution environment, or both

To clarify: “step 4” includes parameter expansion (e.g. ${i:=7}) and arithmetic expansion (e.g. $((i=7))). The variable expansions that ”shall not affect the current execution environment“ are the ones at the front of the command, e.g. i=7 ls. So this paragraph says, among other things, that if a parameter expansion or a variable expansion modifies the value of a variable, it's unspecified whether this has an effect after the command returns.

In practice, shells usually apply a redirection to an external command by forking first and doing the redirection in the subshell. But they differ in whether they determine the target of the redirection before or after creating the subshell.

Related Question