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:
...
- Performs the various shell expansions....
- Performs any necessary redirections and removes the redirection operators and their operands from the argument list.
- 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])