Well, it gets more complicated if you bring job control to the
equation.

In:

{ cmd1; cmd2 | cmd3; }

Most shells (zsh being an exception) start a job for cmd1 and
one for cmd2|cmd3, IOW for each pipeline (but in effect, only
the pipeline of the $$, not of subshells).

zsh tries to address it by forking upon ^Z (and then you can
resume the compound command in background or in foreground).

That means that if you do:

{ sleep 3; a=1; echo "$a"; }

Suspend with ^Z upon sleep and then resume with fg, that works
as expected but $a is not updated in the main shell as the a=1
assignment was done in a subshell.

ksh93 seems to be doing something similar but only when the
suspended command is builtin (like sleep).

bash and a few other shells implement the "wait and cooperative
exit" behaviour as described at
https://www.cons.org/cracauer/sigint.html for SIGINT/SIGQUIT
(in interactive shells and also in scripts).

That's why if you press ^C in:

{ sleep 3; echo x; }

You don't see the x. That's not because the compound command is
treated as a job but because the shell exits (returns to the
prompt for an interactive shell) when it sees sleep was killed
by a SIGINT. (note that you don't see the y in [{ sleep 3; echo
x; }; echo y] either.

That's also why you're able to interrupt a:

while true; do sleep 1; done

But you will see the x in

{ ping 127.1; echo x; }

or you won't be able to interrupt:

while true; do ping 127.1; done

Because ping intercepts SIGINT.

For loops, upon ^Z, bash suspends the current pipeline and exits
the loop.

$ for i in 3 4; do sleep "$i"; echo "$i"; done; echo "done: $i"
^Z
[1]+  Stopped                 sleep "$i"
done: 3

dash/ksh suspend the current pipeline but carries on until the
end, and the suspended jobs are only reported at the end.

Where it gets ugly with shells like bash is when you suspend a
function execution.

In:

foo() { sleep 3; echo "$?"; }
foo

If you press ^Z in bash, you see 148. and you've got a suspended
sleep 3 job.

And of course

foo() { ping localhost; echo x; }
foo

You do see the x after ^C (but that's the same for a "ping
localhost; echo x" script).

ksh93 seems to work like zsh here (treats the function
invocation as a job and forks upon suspend).

Other complications include:

cmd | { cmd3; cmd4; }

In shells where the compound command is not run in a subshell,
and also the command substitutions:

cmd1 $(cmd2) $(cmd3) | cmd4 $((a = $(cmd5)))

Are all cmdx part of the same job? In what order are they run,
by which shell? What happens if you ^Z while cmd2 is running?

There was a related discussion on the zsh mailing list some time
ago, I think the behaviour changed then as a result  but is
still not perfect.
https://www.zsh.org/mla/workers/2018/msg00318.html

builtins do complicate things significantly as well. Not to
mention the effects of set -m in subshells or non-interactive
shells and traps.

I don't think any shell has a perfect solution in all cases, but
in any case, their behaviour all differ from one another.

How the shell creates and manages processes and handles signals
is mostly unspecified in POSIX. I thought it was on purpose to
allow the sort of optimisations  done by ksh93 (where subshells
are not implemented by forks for instance), or builtins.

It seems to me it will have to stay that way as I can't see how
we can reconciliate all behaviours. There is definitely scope
for improvement though, if only to make it more explicit what is
not specified.

-- 
Stephane

Reply via email to