Arithmetic expression in redirection

I don't have a concrete citation for why this behavior exists, but going off the notes in SC2257* there are some interesting points to note in the manual.

When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment
§3.7.3 Command Execution Environment

This reflects what SC2257 notes, though it's unclear about which environment the redirection's value is evaluated in. However §3.1.1 Shell Operation seems to say that redirection happens before this execution (sub)environment is invoked:

Basically, the shell does the following:
...

  1. Performs the various shell expansions....
  2. Performs any necessary redirections and removes the redirection operators and their operands from the argument list.
  3. Executes the command.

We can see that this isn't limited to arithmetic expansions but also other state-changing expansions like :=:

$ bash -c 'date >"${word:=wow}.txt"; echo "word=${word}"'
word=

$ bash -c 'echo >"${word:=wow}.txt"; echo "word=${word}"'
word=wow

Interestingly, this does not appear to be a (well-defined) subshell environment, because BASH_SUBSHELL remains set to 0:

$ date >"${word:=$BASH_SUBSHELL}.txt"; ls
0.txt

We can also check some other shells, and see that zsh has the same behavior, though dash does not:

$ zsh -c 'date >"${word:=wow}.txt"; echo "word=${word}"'
word=

$ zsh -c 'echo >"${word:=wow}.txt"; echo "word=${word}"'
word=wow

$ dash -c 'date >"${word:=wow}.txt"; echo "word=${word}"'
word=wow

$ dash -c 'echo >"${word:=wow}.txt"; echo "word=${word}"'
word=wow

I skimmed the zsh guide but didn't find an exact mention of this behavior there either.

Needless to say, this does not appear to be well-documented behavior, so it's fortunate that ShellCheck can help catch it. It does however appear to be long-standing behavior, it's reproducible in Bash 3, 4, and 5.

* Unfortunately the commit that added SC2257 doesn't link to an Issue or any other further context.


Shellcheck's advice is sound; sometimes redirections are performed in subshells. However, the crux of this behavior is when expansions occur:

bind_int_variable          variables.c:3410    cnt = 2, late binding
expr_bind_variable         expr.c:336          
exp0                       expr.c:1040         
exp1                       expr.c:1007         
exppower                   expr.c:962          
expmuldiv                  expr.c:887          
exp3                       expr.c:861          
expshift                   expr.c:837          
exp4                       expr.c:807          
exp5                       expr.c:785          
expband                    expr.c:767          
expbxor                    expr.c:748          
expbor                     expr.c:729          
expland                    expr.c:702          
explor                     expr.c:674          
expcond                    expr.c:627          
expassign                  expr.c:512          
expcomma                   expr.c:492          
subexpr                    expr.c:474          
evalexp                    expr.c:439          
param_expand               subst.c:9498        parameter expansion, including arith subst
expand_word_internal       subst.c:9990        
shell_expand_word_list     subst.c:11335       
expand_word_list_internal  subst.c:11459       
expand_words_no_vars       subst.c:10988       
redirection_expand         redir.c:287         expansions post-fork()
do_redirection_internal    redir.c:844         
do_redirections            redir.c:230         redirections are done in child process
execute_disk_command       execute_cmd.c:5418  fork to run date(1)
execute_simple_command     execute_cmd.c:4547  
execute_command_internal   execute_cmd.c:842   
execute_command            execute_cmd.c:394   
reader_loop                eval.c:175          
main                       shell.c:805         

When execute_disk_command() is called, it forks and then executes date(1). After the fork() and before the execve(), redirections and additional expansions are done (via do_redirections()). Variables expanded and bound post-fork will not reflect in the parent shell.

From BASH's perspective, however, this is just a simple command rather than a subshell command. This is an implicit subshell.


See execute_disk_command() in execute_cmd.c

Execute a simple command that is hopefully defined in a disk file
somewhere.
1) fork ()
2) connect pipes
3) look up the command
4) do redirections
5) execve ()
6) If the execve failed, see if the file has executable mode set.
If so, and it isn't a directory, then execute its contents as
a shell script.

(references taken from commit 9e49d343e3cd7e20dad1b86ebfb764e8027596a7 [browse tree])

Tags:

Linux

Bash