Bash program not executed if redirection would fail
It's not really a question of ordering checks, simply the order in which the shell sets things up. Redirections are set up before the command is run; so in your example, the shell tries to open ~root/log
for appending before trying to do anything involving ./write_file.py
. Since the log file can't be opened, the redirection fails and the shell stops processing the command line at that point.
One way to demonstrate this is to take a non-executable file and attempt to run it:
$ touch demo
$ ./demo
zsh: permission denied: ./demo
$ ./demo > ~root/log
zsh: permission denied: /root/log
This shows that the shell doesn't even look at ./demo
when the redirection can't be set up.
From the bash man page, section REDIRECTION (emphasis by me):
Before a command is executed, its input and output may be redirected using a special notation interpreted by the shell.
...
A failure to open or create a file causes the redirection to fail.
So the shell tries to open the target file for stdout
, which fails, and the command isn't executed at all.
It's worth observing that the shell must establish redirections before starting the program.
Consider your example:
./write_file.py >> ~root/log
What happens in the shell is:
- We (the shell)
fork()
; the child process inherits the open file descriptors from its parent (the shell). - In the child process, we
fopen()
(the expansion of) "~root/log", anddup2()
it to fd 1 (andclose()
the temporary fd). If thefopen()
fails, callexit()
to report the error to the parent. - Still in the child, we
exec()
"./write_file.py". This process is now no longer running any of our code (unless we failed to execute, in which case weexit()
to report the error to the parent). - The parent will
wait()
for the child to terminate, and handle its exit code (by copying it into$?
, at least).
So the redirection has to happen in the child between fork()
and exec()
: it can't happen before fork()
because it must not change the shell's stdout, and it can't happen after exec()
because the filename and the shell's executable code have now been replaced by the Python program. The parent has no access to the file descriptors of the child (and even if it did, it couldn't guarantee to redirect between exec()
and the first write to stdout).