Discussion:
[Help-bash] When pipes fail (and when not)
Paul-Jürgen Wagner
2018-11-05 16:33:23 UTC
Permalink
Dear Bashers,

I have the following script:

for i in {0..9}; do echo $i; sleep 1; done | tee foo | dd bs=1 count=10

When I execute it on a Linux box (bash version 4.2.45(2)), the whole
pipe writes

0
1
2
3
4

to foo, prints the same on the terminal, and exits as expected. Same
behaviour occurs on Cygwin (bash 4.4.12(3)). But when I run this in a
GNURoot on an android tablet (bash 4.3.30(1)), I get

0
1
2
3
4
5
6
7
8
9

in file foo and

0
1
2
3
4
tee: standard output: Broken pipe
tee: write error

on the terminal. Apparently the broken pipe does not cause the whole
pipe to quit. Could anyone explain why? My script relies on the last
command being able to terminate the whole pipe. Is there a way to
ensure this behaviour on GNURoot (android)?

Kind regards,

Paul

P.S.: I noticed that with GNURoot on android, process substitution does
not work, probably because /dev can not be written? Is that related to
the problem above?
Bob Proulx
2018-11-25 01:12:45 UTC
Permalink
Post by Paul-Jürgen Wagner
for i in {0..9}; do echo $i; sleep 1; done | tee foo | dd bs=1 count=10
Looks good. The sleep pushes the race condition such that dd almost
always exits first (on a heavily load system this might not be true as
it is not guaranteed but is heavily biased that way). Not all
operating systems behave that way. IIRC the old MS-DOS implemented
pipes through temporary files and all of the first would run to
conclusion before the second and then the third in the pipeline. It
wasn't a multitasking system. Just as an example.

This means that after dd exits upon the next write that tee makes it
will receive SIGPIPE and exit with a non-zero exit code and
WIFSIGNALED(wstatus) (see man 2 wait) will indicate it exited due to a
signal.

Since tee has exited then upon the next write the echo will exit.
Since echo is a shell builtin the for shell loop will exit. If that
were /bin/echo or other non-builtin then that external would upon the
next write receive SIGPIPE and exit due to the signal and also
WIFSIGNALED(wstatus) will indicate this.

In general in an I/O filter pipeline all of the processes to the right
of the first command to exit will read an EOF from their input and
therefore exit normally. All of the processes to the left will
receive a SIGPIPE upon their next write and exit WIFSIGNALED.

A | B | C | D | E

If C exits then D and E receive EOF when they next read from their
inputs. Upon their next writes B and then A will receive SIGPIPE and
exit.

Because I/O filter programs read and write continuously this is a good
design paradigm for them. They will run and exit together as a set.
However if a process, say A in the above, is a long running cpu
intensive program that only writes output infrequently then it may
continue running and using resources for an indefinite time determined
by what the process is doing before it writes output and receives
SIGPIPE and exits.

If A is doing something for two hours before writing its next output
then it will still continue running for the next two hours even though
its output reading process has already exited. Let me emphasize that
it is not until the *next* write that I/O will trigger the SIGPIPE to
be delivered to terminate the writing process.

A somewhat involved example to show this:

for i in {0..9}; do { /bin/echo $i || { echo "echo failed: rc=$?"; break; } 1>&2 ;}; sleep 2; done | dd bs=1 count=10 status=none
0
1
2
3
4
echo failed: rc=141

And remember the documentation for the shell is:

The return value of a simple command is its exit status, or 128+n if
the command is terminated by signal n.

Therefore since 141 is > 128 we know the /bin/echo was terminated by a
signal and 141-128=13 therefore it was terminated with signal 13
SIGPIPE. We catch that in the || { ... ;} error handling portion,
print the exit code here and break out of the loop. If we didn't
break out of it then the loop would continue for all ten loops and
print out the failure and rc code another four times for five times
total.

for i in {0..9}; do { /bin/echo $i || { echo "echo failed: rc=$?"; } 1>&2 ;}; sleep 2; done | dd bs=1 count=10 status=none
0
1
2
3
4
echo failed: rc=141
echo failed: rc=141
echo failed: rc=141
echo failed: rc=141
echo failed: rc=141

Interestingly if we use the bash builtin echo then no diagnostic is
printed. The shell knows it has caught a signal however and breaks
out of the loop itself. The lack of a diagnostic is arguably a bug.
However dash behaves exactly the same way. Therefore I would not be
in a hurry to change anything as scripts almost certainly depend upon
the current quiet behavior.

[[ksh behaves worse leaving the for loop running in the background
after having logged the termination of the command line and printing
the prompt! ksh behaves strangely in this area and feels quite
buggy.]]
Post by Paul-Jürgen Wagner
But when I run this in a GNURoot on an android tablet (bash
4.3.30(1)), I get
0
1
2
3
4
5
6
7
8
9
in file foo and
By this I presume that 'tee' did not get the SIGPIPE and did not exit.
Again using the somewhat complex test example to ignore SIGPIPE.

man bash

trap [-lp] [[arg] sigspec ...]

If arg is the null string the signal specified by each sigspec
is ignored by the shell and by the commands it invokes.

Therefore 'trap "" PIPE' ignores the signal. Running this in a bash
-c subprocess avoids mungling the currently running command line
shell's trap state. Otherwise I would need to reset the handler or
exit that shell to get back to a sane state.

bash -c 'trap "" PIPE; for i in {0..9}; do { /bin/echo $i || { echo "echo failed: rc=$?"; break; } 1>&2 ;}; sleep 2; done | dd bs=1 count=10 status=none'
0
1
2
3
4
/bin/echo: write error: Broken pipe
echo failed: rc=1
Post by Paul-Jürgen Wagner
0
1
2
3
4
tee: standard output: Broken pipe
tee: write error
This confirms that tee did not get a SIGPIPE as instead after the
point where it normally would have it called write on the pipe and
the write on a closed pipe returned EPIPE.

man 2 write

EPIPE fd is connected to a pipe or socket whose reading end is closed.
When this happens the writing process will also receive a
SIGPIPE signal. (Thus, the write return value is seen only if
the program catches, blocks or ignores this signal.)

That additional parenthetical is a new addition in my memory and I
think a very good one. Because one would only ever see EPIPE if the
environment is trapping and ignoring SIGPIPE which is something that
they should never do. (Never say never. There is undoubtedly special
cases where ignoring SIGPIPE is useful or required. But as a general
statement one should never do it. Ignoring SIGPIPE causes people to
see "Broken pipe" errors.)

Therefore I conclude that your Android environment is mistakenly
installing a SIG_IGN handler for SIGPIPE in the environment.

man 7 signal

A child created via fork(2) inherits a copy of its parent's signal
dispositions. During an execve(2), the dispositions of handled signals
are reset to the default; the dispositions of ignored signals are left
unchanged.
Post by Paul-Jürgen Wagner
on the terminal. Apparently the broken pipe does not cause the whole pipe
to quit. Could anyone explain why? My script relies on the last command
being able to terminate the whole pipe. Is there a way to ensure this
behaviour on GNURoot (android)?
Not having debugged this completely to root cause I can't say for
certain but I feel confident that the root cause of this problem is
something in the parent environment is ignoring SIGPIPE. This is
being inherited by all of the later children processes. That's bad.
Find that and fix it and it should all work normally.

For what it is worth I tried this on Android using Termux and
received the same result. I feel confident that the parent
environment ignoring SIGPIPE is the problem.
Post by Paul-Jürgen Wagner
P.S.: I noticed that with GNURoot on android, process substitution does not
work, probably because /dev can not be written? Is that related to the
problem above?
No. I don't think so. That would be a different problem. And
strictly speaking that isn't a portable operation anyway.

Again a very complicated topic. I hope I didn't screw up something in
my analysis of it. If I did however someone please call it out and
improve upon the answering of it.

Bob

P.S. Using {0..9} as in "for i in {0..9}" is really not a good way to
do this. It expands to be the full set.

for i in 0 1 2 3 4 5 6 7 8 9

For 10 items this is okay. But people get used to doing that and then
do it for 10 zillion items. That eats up a *LOT* of memory.

for i in {0..99999999}

In bash it is better to simply use a real for loop.

for ((i=1; i<=10000000; i++)); do

And next I am of the opinion that counting numbers should be whole
counting numbers starting at 1 not 0 and proceeding on from there.
But that is a different topic.
Paul Wagner
2018-11-27 13:32:48 UTC
Permalink
Dear Bob,

a really really huge Thank You for the effort you put into this
explanation!
Post by Bob Proulx
Therefore I conclude that your Android environment is mistakenly
installing a SIG_IGN handler for SIGPIPE in the environment.
With that as a working hypothesis, could I install my own SIGPIPE
handler then and kill the subshell tee runs in? Something like

for i in {0..9}; do echo $i; sleep 1; done | { trap 'kill $$' pipe; tee
foo; } | dd bs=1 count=10

To experiment with that, I tried (under Cygwin, where the SIGPIPE is
working fine)

trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

which terminates after '4' but never writes 'foo', making me wonder if
the trap handler ever got called. When I try

trap '' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

as you did in your expose, I get the expected behaviour of

0
1
2
3
4
10+0 records in
10+0 records out
10 bytes copied, 4.10439 s, 0.0 kB/s
-bash: echo: write error: Broken pipe
-bash: echo: write error: Broken pipe
-bash: echo: write error: Broken pipe
-bash: echo: write error: Broken pipe
-bash: echo: write error: Broken pipe

so setting the trap handler actually works in this case, but why not in
the former one? Or did I miss something else?
Post by Bob Proulx
For what it is worth I tried this on Android using Termux and
received the same result. I feel confident that the parent
environment ignoring SIGPIPE is the problem.
Interesting is that inside Termux the behaviour is the same. I am not
familiar with Termux, but GNUroot uses a proot-environment, and from
what I read in the FAQ Termux might operate the same way. I will
probably have a look at the proot sources, might find a clue there.
Post by Bob Proulx
Post by Paul-Jürgen Wagner
P.S.: I noticed that with GNURoot on android, process substitution does not
work, probably because /dev can not be written? Is that related to the
problem above?
No. I don't think so. That would be a different problem. And
strictly speaking that isn't a portable operation anyway.
Regarding process substitution not working, in the meantime I found out
that /dev is just lacking the /dev/fd-directory, but /proc has it, so on
a rooted Android device a simple 'ln -s /proc/self/fd /dev/fd' before
starting the proot-environment does the trick.

Again, a big Thank You!

Paul

P.S.
Post by Bob Proulx
P.S. Using {0..9} as in "for i in {0..9}" is really not a good way to
do this. It expands to be the full set.
for i in 0 1 2 3 4 5 6 7 8 9
For 10 items this is okay. But people get used to doing that and then
do it for 10 zillion items. That eats up a *LOT* of memory.
I get your point. I just wanted to make the example *really* short, and
I never intended more than 10 iterations.
Post by Bob Proulx
And next I am of the opinion that counting numbers should be whole
counting numbers starting at 1 not 0 and proceeding on from there.
Totally agree. I'm just *very* lazy, and typing 0..9 saved me one
keystroke compared to 1..10 ;-)
Chet Ramey
2018-11-27 18:03:57 UTC
Permalink
Post by Paul Wagner
Dear Bob,
a really really huge Thank You for the effort you put into this explanation!
Post by Bob Proulx
Therefore I conclude that your Android environment is mistakenly
installing a SIG_IGN handler for SIGPIPE in the environment.
With that as a working hypothesis, could I install my own SIGPIPE handler
then and kill the subshell tee runs in? 
If bash starts up with a signal ignored, you can't install a trap for it or
reset its disposition to SIG_DFL. You have to fix the problem at its root.
--
``The lyf so short, the craft so long to lerne.'' - Chaucer
``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU ***@case.edu http://tiswww.cwru.edu/~chet/
Paul Wagner
2018-11-28 12:15:22 UTC
Permalink
Post by Chet Ramey
If bash starts up with a signal ignored, you can't install a trap for it or
reset its disposition to SIG_DFL. You have to fix the problem at its root.
Thanks for pointing that out.

I still don't understand why setting the trap on a system that has
SIGPIPE vital doesn't work, so I'd be very greatful for any hints.

Regards,

Paul
Eric Blake
2018-11-28 12:54:07 UTC
Permalink
Post by Paul Wagner
Post by Chet Ramey
If bash starts up with a signal ignored, you can't install a trap for it or
reset its disposition to SIG_DFL. You have to fix the problem at its root.
Thanks for pointing that out.
I still don't understand why setting the trap on a system that has
SIGPIPE vital doesn't work, so I'd be very greatful for any hints.
It's a lame restriction from POSIX based on historical practice (that I
_really_ wish we weren't stuck with):
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap

"Signals that were ignored on entry to a non-interactive shell cannot be
trapped or reset, although no error need be reported when attempting to
do so. An interactive shell may reset or catch signals ignored on entry.
Traps shall remain in place for a given shell until explicitly changed
with another trap command."
--
Eric Blake, Principal Software Engineer
Red Hat, Inc. +1-919-301-3266
Virtualization: qemu.org | libvirt.org
Paul Wagner
2018-11-28 15:02:11 UTC
Permalink
Post by Eric Blake
Post by Paul Wagner
I still don't understand why setting the trap on a system that has
SIGPIPE vital doesn't work, so I'd be very greatful for any hints.
It's a lame restriction from POSIX based on historical practice (that
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap
"Signals that were ignored on entry to a non-interactive shell cannot
be trapped or reset, although no error need be reported when
attempting to do so. An interactive shell may reset or catch signals
ignored on entry. Traps shall remain in place for a given shell until
explicitly changed with another trap command."
Maybe I don't get your point, but I think there might be a
missunderstanding: In my previous post I was referring to my example on
Linux, where SIGPIPE is not ignored by default:

for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

ends as expected after 5 iterations, so SIGPIPE is not ignored in the
current shell;

trap '' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

reports a write error five times after the fifth iteration, as expected;
but installing another trap like

trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

ends after 5 iterations (so SIGPIPE is not trapped, but handled again),
but does not write 'foo' to stderr (the terminal), and I don't
understand why.

Thanks for your patience,

Paul
Eric Blake
2018-11-28 15:45:56 UTC
Permalink
Post by Paul Wagner
Post by Eric Blake
Post by Paul Wagner
I still don't understand why setting the trap on a system that has
SIGPIPE vital doesn't work, so I'd be very greatful for any hints.
It's a lame restriction from POSIX based on historical practice (that
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap
"Signals that were ignored on entry to a non-interactive shell cannot
be trapped or reset, although no error need be reported when
attempting to do so. An interactive shell may reset or catch signals
ignored on entry. Traps shall remain in place for a given shell until
explicitly changed with another trap command."
Maybe I don't get your point, but I think there might be a
missunderstanding:  In my previous post I was referring to my example on
Okay, there was indeed some misunderstanding. My quote was about the
situation where a bash script starts with sigpipe inherited as ignored -
there is NOTHING the script can do to undo that, short of re-execing via
an intermediary non-shell process that can lift the sigpipe ignored
inheritance before going back into shell. (And that's the situation
that most frequently plagues CI test setups, if they leak an ignored
sigpipe into child processes under test). The fact that POSIX forbids
the shell from undoing an inherited ignored sigpipe is rather awkward,
but we are stuck with it.

Now, on to your complaint - when does the trap handler for sigpipe
actually fire. Well, that depends on whether bash started with sigpipe
ignored - because if it did, you cannot undo that effect short of
re-execing.
Post by Paul Wagner
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
ends as expected after 5 iterations, so SIGPIPE is not ignored in the
current shell;
I'm running tests with an interactive environment where I _know_ that
sigpipe is not ignored from the outside. Note, however, that the
subsidiary bash takes a full 10 seconds to complete, regardless of
whether sigpipe is inherited ignored or default:

$ time (trap '' pipe; bash -c 'for i in {0..9}; do /bin/echo $i; sleep
1; done | dd bs=1 count=10')
0
1
2
3
4
10+0 records in
10+0 records out
10 bytes copied, 4.00699 s, 0.0 kB/s
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe

real 0m10.033s
user 0m0.026s
sys 0m0.007s

$ time (trap - pipe; bash -c 'for i in {0..9}; do /bin/echo $i; sleep 1;
done | dd bs=1 count=10')
0
1
2
3
4
10+0 records in
10+0 records out
10 bytes copied, 4.0058 s, 0.0 kB/s
real 0m10.027s
user 0m0.016s
sys 0m0.012s

So, even when sigpipe is not ignored, and even though the output stops
after 5 seconds, the subshell takes the full ten seconds to complete
execution because I have not actually stopped the loop, but merely
changed what happens with the /bin/echo processes outputting to a pipe
that no longer has a reading process.

But, when bash starts with sigpipe ignored, I cannot undo that from
within bash:

$ time (trap '' pipe; bash -c 'trap - pipe; for i in {0..9}; do
/bin/echo $i; sleep 1; done | dd bs=1 count=10')
0
1
2
3
4
10+0 records in
10+0 records out
10 bytes copied, 4.00624 s, 0.0 kB/s
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe
/bin/echo: write error: Broken pipe

real 0m10.029s
user 0m0.016s
sys 0m0.014s


So, the fact that you were able to install a trap handler that made a
difference shows that you are, indeed, in an environment where you did
not inherit an ignored sigpipe.
Post by Paul Wagner
trap '' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
reports a write error five times after the fifth iteration, as expected;
but installing another trap like
trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
ends after 5 iterations (so SIGPIPE is not trapped, but handled again),
but does not write 'foo' to stderr (the terminal), and I don't
understand why.
The other consideration is to figure out WHEN the trap handler fires.
It does NOT fire every time that /bin/echo dies due to SIGPIPE, but ONLY
if the entire pipeline dies due to SIGPIPE. But 'dd' did not die due to
SIGPIPE, therefore the trap handler never fires.

The fact that you have installed a trap handler at all determines
whether bash lets /bin/echo inherit sigpipe ignored or defaulted, and
therefore determines whether /bin/echo outputs an error message; but
since the trap handler is NOT firing (because the pipeline itself is not
dying due to SIGPIPE, which would only happen if 'dd' exited that way),
the loop is executing all ten /bin/echo processes, whether or not the dd
process is still around to consume them, and without your handler ever
firing.
--
Eric Blake, Principal Software Engineer
Red Hat, Inc. +1-919-301-3266
Virtualization: qemu.org | libvirt.org
Paul Wagner
2018-11-28 16:49:41 UTC
Permalink
On 28.11.2018 16:45, Eric Blake wrote:

Dear Eric,

thanks for your thorough investigation. I think I'm starting to
understand what is going on ... and that I don't have any options with
my current implementation.
Post by Eric Blake
Now, on to your complaint - when does the trap handler for sigpipe
Sorry if it came across as a complaint. I intended it as an innocent
question.

Kindest regards,

Paul
Chet Ramey
2018-11-28 18:14:47 UTC
Permalink
Post by Paul Wagner
but installing another trap like
trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
ends after 5 iterations (so SIGPIPE is not trapped, but handled again), but
does not write 'foo' to stderr (the terminal), and I don't understand why.
Each element of a pipeline is executed in a subshell environment, described
in

http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_12

"A subshell environment shall be created as a duplicate of the shell
environment, except that signal traps that are not being ignored shall be
set to the default action. [...] Additionally, each command of a multi-
command pipeline is in a subshell environment; as an extension, however,
any or all commands in a pipeline may be executed in the current
environment."

Subshell environments don't inherit traps from the parent shell, unless
the trap has specified that the signal is to be ignored. Bash always
executes the first element of a pipeline in a subshell environment.

If you define the trap in the same subshell environment as the for loop,
you'll get the output you want.

Chet
--
``The lyf so short, the craft so long to lerne.'' - Chaucer
``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU ***@case.edu http://tiswww.cwru.edu/~chet/
Bob Proulx
2018-11-28 21:16:02 UTC
Permalink
Post by Chet Ramey
Post by Paul Wagner
but installing another trap like
trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
ends after 5 iterations (so SIGPIPE is not trapped, but handled again), but
does not write 'foo' to stderr (the terminal), and I don't understand why.
Using echo as in the above is the bash builtin echo which doesn't
print diagnostics. You would need to use an external standalone
command such as /bin/echo or one you write yourself that prints
diagnostic messages.
Post by Chet Ramey
Each element of a pipeline is executed in a subshell environment, described
in
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_12
"A subshell environment shall be created as a duplicate of the shell
environment, except that signal traps that are not being ignored shall be
set to the default action. [...] Additionally, each command of a multi-
command pipeline is in a subshell environment; as an extension, however,
any or all commands in a pipeline may be executed in the current
environment."
As I read that it appears to give a solution. It says that if a
signal is not being ignored then it will be set back to the default.
Therefore if we set a SIGPIPE handler to anything trivial but not
ignore and not default then that would make it not be ignored and
therefore should be set back to the default SIG_DFL. But that does
not seem to be true.
Post by Chet Ramey
Subshell environments don't inherit traps from the parent shell, unless
the trap has specified that the signal is to be ignored. Bash always
executes the first element of a pipeline in a subshell environment.
If you define the trap in the same subshell environment as the for loop,
you'll get the output you want.
trap 'echo foo 1>&2' PIPE # set a PIPE handler so it is not ignored

But I can't create any example which provides for resetting SIGPIPE
handling back to the original SIG_DFL in a child of a process where
the parent had previously ignored it. I recall this always having
been the case forever for shells.

Perhaps in the above the "traps that are not being ignored shall be
set to the default action" really means set back to the previous
action? Where the previous action was that it was ignored?

Bob
Chet Ramey
2018-11-28 22:10:57 UTC
Permalink
Post by Bob Proulx
Post by Chet Ramey
Post by Paul Wagner
but installing another trap like
trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10
ends after 5 iterations (so SIGPIPE is not trapped, but handled again), but
does not write 'foo' to stderr (the terminal), and I don't understand why.
Using echo as in the above is the bash builtin echo which doesn't
print diagnostics.
Clearly it's not true that the builtin echo doesn't print diagnostics. If
the SIGPIPE doesn't cause the shell to exit (it's a fatal error) the
builtin echo will print a write error message.
Post by Bob Proulx
Post by Chet Ramey
Each element of a pipeline is executed in a subshell environment, described
in
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_12
"A subshell environment shall be created as a duplicate of the shell
environment, except that signal traps that are not being ignored shall be
set to the default action. [...] Additionally, each command of a multi-
command pipeline is in a subshell environment; as an extension, however,
any or all commands in a pipeline may be executed in the current
environment."
As I read that it appears to give a solution. It says that if a
signal is not being ignored then it will be set back to the default.
Therefore if we set a SIGPIPE handler to anything trivial but not
ignore and not default then that would make it not be ignored and
therefore should be set back to the default SIG_DFL. But that does
not seem to be true.
Yes, it's exactly what happens. If you catch SIGPIPE with a trap in the
parent shell, the SIGPIPE disposition is set to SIG_DFL in the subshell
begun to execute the first pipeline element. If you don't trap it, that
subshell exits when it gets the SIGPIPE thrown by the write on a broken
pipe. That's why this script doesn't print any "foo"s and exits right
after dd prints its messages.

trap 'echo foo >&2' pipe
for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10


The only instance in which this will not happen is when SIGPIPE is set
to SIG_IGN before the parent shell executes. A subshell doesn't count as
"when the shell starts;" POSIX determined that via an interpretation.
Bash used to consider the subshell a "shell starting," but changed to
match the interpretation a while back (I didn't check exactly when, but
I use bash-4.4 for all my examples).
Post by Bob Proulx
Post by Chet Ramey
Subshell environments don't inherit traps from the parent shell, unless
the trap has specified that the signal is to be ignored. Bash always
executes the first element of a pipeline in a subshell environment.
If you define the trap in the same subshell environment as the for loop,
you'll get the output you want.
trap 'echo foo 1>&2' PIPE # set a PIPE handler so it is not ignored
But I can't create any example which provides for resetting SIGPIPE
handling back to the original SIG_DFL in a child of a process where
the parent had previously ignored it. I recall this always having
been the case forever for shells.
Yes, exactly. If the shell starts up with a signal set to SIG_IGN, you
can't do anything with it. The shell doesn't have to tell you you can't,
either. If the shell itself sets the signal to be ignored via trap, you
can use another trap to change the disposition.

You can show this by running the following:

trap '' SIGPIPE
{ trap 'echo foo >&2' pipe ; for i in {0..9}; do echo $i; sleep 1; done; }
| dd bs=1 count=10

If you run that script, you'll get five "foo"s and five write error
messages.

If you run this script:

trap '' SIGPIPE
{ for i in {0..9}; do echo $i; sleep 1; done; } | dd bs=1 count=10

you'll get five error messages because the subshell inherited the
SIG_IGN.
Post by Bob Proulx
Perhaps in the above the "traps that are not being ignored shall be
set to the default action" really means set back to the previous
action? Where the previous action was that it was ignored?
What? A signal can have three possible dispositions: SIG_DFL, SIG_IGN,
and an application signal handler. Signals that are ignored when the
shell starts are unmodifiable. Signals that are set to SIG_IGN by the
shell via a trap stay ignored in the subshell, but the subshell can
modify them, as shown above. Signals that are not ignored are either
SIG_DFL or trapped. In that case, the subshell sets them to SIG_DFL when
it is forked, but can set traps to modify their disposition.

Chet
--
``The lyf so short, the craft so long to lerne.'' - Chaucer
``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU ***@case.edu http://tiswww.cwru.edu/~chet/
Paul Wagner
2018-12-06 08:22:59 UTC
Permalink
Dear Bashers,

after leaving me with sleepless nights, yesterday the following solution
to the problem that if SIGPIPE is caught in the enclosing environment,

for i in {0..9}; do echo $i; sleep 1; done | dd bs=1 count=10

does not terminate after '5', came to me:

{ echo $BASHPID; for i in {0..9}; do echo $i; sleep 1; done; } | { read
pid; dd bs=1 count=10; kill -s term $pid; }

Being aware that this might be a really ugly solution, I'd appreciate
any comments or improvements on it.

Regards,

Paul

Loading...