$(<servers.txt)
is a special shortcut in bash for $(cat <servers.txt)
, as documented under “Command substitution”. It is an exception to the regular syntax where the redirection would be applied to an empty command and thus do nothing except signal an error if the file doesn't exist. Bash doesn't extend this exception to here documents or here strings: you need to include cat
explicitly.
This feature was added in bash 2.02 (it's mentioned in the changelog). In the source code, it's implemented by the logic around the call to cat_file
in parse_and_execute
in builtins/evalstring.c
.
You know, of course, that $(…)
causes the command(s) within the parentheses
to run in a subshell. And you know, of course, that jobs
is a shell builtin.
Well, it looks like jobs
clears a job from the shell’s memory
once its death has been reported.
But, when you run $(jobs)
, the jobs
command runs in a subshell,
so it doesn’t get a chance to tell the parent shell
(the one that’s running the script) that the death of the background job
(ping
, in your example) has been reported.
So, each time the shell spawns a subshell to run the $(jobs)
thingie,
that subshell still has a complete list of jobs
(i.e., the ping
job is there, even though it’s dead after the 5th iteration),
and so jobs
still (again) believes that
it needs to report on the status of the ping
job
(even if it’s been dead for the past four seconds).
This explains why running an unadulterated jobs
command
within the loop causes it to exit as expected:
once you run jobs
in the parent shell, the parent shell knows that
the job’s termination has been reported to the user.
Why is it different in the interactive shell?
Because, whenever a foreground child of an interactive shell terminates,
the shell reports on any background jobs that have terminated1
while the foreground process was running.
So, the ping
terminates while the sleep 1
is running,
and when the sleep
terminates,
the shell reports on the background job’s death.
Et voilà.
1 It might be more accurate to say “any background jobs
that have changed state while the foreground process was running.”
I believe that it might also report on jobs that have been suspended
(kill -SUSP
, the programmatic equivalent to Ctrl+Z)
or become unsuspended (kill -CONT
, which is what the fg
command does).
Best Answer
Bash will run the last or only command of a subshell with
exec
if it figures it can do so safely, as an optimisation. You can validate this clearly withpstree
:That is why your
( sleep 5 )
shows up as just thesleep
command, and no intermediate shell. In the last example above, theecho
forces the shell to do something afterpstree
completes, so there's a real shell around to use; in the middle case, the spawned shell just immediatelyexec
spstree
, so it looks the same as the first case (which is standard fork-exec). Many shells do this.On the other hand, anything run in the background requires spawning a new process: fundamentally, that's what "background" is. Commands in braces do ordinarily run in the current shell, but that can't happen if they're to run in the background. So that the parent shell can keep doing whatever it's doing while the background commands run, a new shell has to be created to run the whole
{ ... }
in. ThusIn this case, there's no difference whether there's another trailing command or not. Bash documents this behaviour for
&
:You can also see this happen by backgrounding a built-in command like
read
:My shell now has two children: a
bash
runningread
, and thepstree
command printing that output.It's true that the shell could make a further optimisation and apply it to backgrounded
{ ... }
as it does to parenthesised subshells, but it doesn't. It's possible some other shells do, but I haven't found one in quick testing. It's a pretty rare case and formally different, and there are some odd corner cases.