bash
shouldn't print the job status when non-interactive.
If that's indeed for an interactive bash
, you can do:
{ pid=$(sleep 20 >&3 3>&- & echo "$!"); } 3>&1
We want sleep
's stdout to go to where it was before, not the pipe that feeds the $pid
variable. So we save the outer stdout in the file descriptor 3 (3>&1
) and restore it for sleep
inside the command substitution. So pid=$(...)
returns as soon as echo
terminates because there's nothing left with an open file descriptor to the pipe that feeds $pid
.
However note that because it's started from a subshell (here in a command substitution), that sleep
will not run in a separate process group. So it's not the same as running sleep 20 &
with regards to I/O to the terminal for instance.
Maybe better would be to use a shell that supports spawning disowned background jobs like zsh
where you can do:
sleep 20 &! pid=$!
With bash
, you can approximate it with:
{ sleep 20 2>&3 3>&- & } 3>&2 2> /dev/null; pid=$!; disown "$pid"
bash
outputs the [1] 21578
to stderr. So again, we save stderr before redirecting to /dev/null, and restore it for the sleep
command. That way, the [1] 21578
goes to /dev/null
but sleep
's stderr goes as usual.
If you're going to redirect everything to /dev/null anyway, you can simply do:
{ apt-get update & } > /dev/null 2>&1; pid=$!; disown "$pid"
To redirect only stdout:
{ apt-get-update 2>&3 3>&- & } 3>&2 > /dev/null 2>&1; pid=$!; disown "$pid"
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
runs a shell which runs
sleep
. The pid given by$!
is the shell’s.You don’t need the substitution here,
would give you
sleep
’s pid.I imagine
sleep
in your example replaces something else, but in any case a background substitution is probably unnecessary. Using only a subshell,will typically give you
sleep
’s pid, because many shells replace themselves with the child command when they run the last command in a subshell, which effectively reuses the subshell’s pid forsleep
; butwill give you the shell’s pid, not
sleep
’s.