Shell detection of empty subshell

SC1143 suggests that commented parts of a wrapped shell command to be wrapped in a subshell.

Is the Posix shell "smart enough" to not launch a subshell when it sees that it does nothing? What about Bash and Zsh?

Asked By: AvidSeeker

||

As ilkkachu writes, POSIX itself specifies that

The shell shall expand the command substitution by executing command in a subshell environment (see Shell Execution Environment) and replacing the command substitution (the text of command plus the enclosing "$()" or backquotes) with the standard output of the command, removing sequences of one or more <newline> characters at the end of the substitution.

However looking at shells’ actual behaviour reveals some surprises (well, one). I’m using a script containing only

echo 
Before 
`# commented` 
After

Bash and Zsh fork a subshell to run the comment-only command substitution:

$ strace -f -e process bash bttest
execve("/bin/bash", ["bash", "bttest"], 0x7ffe31522a10 /* 67 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 3134851 attached
, child_tidptr=0x7fe3ef0c6a10) = 3134851
[pid 3134851] exit_group(0)             = ?
[pid 3134851] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3134851, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = 3134851
wait4(-1, 0x7ffecdd13810, WNOHANG, NULL) = -1 ECHILD (No child processes)
Before After
exit_group(0)                           = ?
+++ exited with 0 +++
$ strace -f -e process zsh bttest
execve("/usr/bin/zsh", ["zsh", "bttest"], 0x7fffa2f78140 /* 67 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 3134903 attached
, child_tidptr=0x7f236ce63750) = 3134903
[pid 3134903] exit_group(0)             = ?
[pid 3134902] kill(3134903, 0)          = 0
[pid 3134903] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3134903, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED|WCONTINUED, {ru_utime={tv_sec=0, tv_usec=593}, ru_stime={tv_sec=0, tv_usec=0}, ...}) = 3134903
wait4(-1, 0x7ffcabc4a7d4, WNOHANG|WSTOPPED|WCONTINUED, 0x7ffcabc4a7f0) = -1 ECHILD (No child processes)
kill(3134903, 0)                        = -1 ESRCH (No such process)
Before After
exit_group(0)                           = ?
+++ exited with 0 +++

DASH on the other hand parses the contents inside the backticks to determine whether they represent a builtin command or an external command, and if that parsing results in an empty “node” (an empty command), skips it entirely:

$ strace -f -e process dash bttest
execve("/bin/dash", ["dash", "bttest"], 0x7ffe61eee5c0 /* 67 vars */) = 0
Before After
exit_group(0)                           = ?
+++ exited with 0 +++

If the backticks include a command, even a builtin, DASH does fork, same as Bash and Zsh; with

echo 
Before 
`: # commented` 
After

we get

$ strace -f -e process dash bttest
execve("/bin/dash", ["dash", "bttest"], 0x7fff762e6260 /* 41 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7effab920850) = 3905359
strace: Process 3905359 attached
[pid 3905359] exit_group(0)             = ?
[pid 3905359] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3905359, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 3905359
wait4(-1, 0x7ffdb1e8683c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Before After
exit_group(0)                           = ?
+++ exited with 0 +++

So there is at least one shell “smart” enough to not launch a subshell in this scenario.

Some shells, such as ksh93, don’t fork at all for subshells not involving external commands; even with the : variant of the test, strace shows

$ strace -f -e process ksh93 bttest
execve("/bin/ksh93", ["ksh93", "bttest"], 0x7ffce3c73510 /* 41 vars */) = 0
Before After
exit_group(0)                           = ?
+++ exited with 0 +++

(There is no “POSIX shell” — POSIX is a specification, with no reference implementations. No shell that I’m aware of strictly implements POSIX and only POSIX.)

Answered By: Stephen Kitt
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.