$ perl -E 'say "y" while 1' | head -1
y
$ $ perl -E 'say "y" while 1' | head -1
y
$ # cat.py
import sys
from signal import signal, SIGPIPE, SIG_DFL
signal(SIGPIPE,SIG_DFL)
for line in sys.stdin:
print(line)[0] https://docs.python.org/3/library/signal.html#note-on-sigpip...
Should be 141 instead of 1. Convention is that when a program dies because of a signal, the exit code should be 128 + the signal number, in this case 13. So, 128 + 13 = 141.
Here are some examples:
$ ( yes; >&2 echo $? ) | head -0
141
$ ( ping localhost; >&2 echo $? ) | head -0
141
$ ( perl -E 'say "y" while 1'; >&2 echo $? ) | head -0
141
In python, when using signal(SIGPIPE, SIG_DFL), you get the correct behaviour (at least regarding the exit status): $ ( python -c '
import sys
from signal import signal, SIGPIPE, SIG_DFL
signal(SIGPIPE,SIG_DFL)
while True: print("y");
' ; >&2 echo $? ) | head -0
141
> Do not set SIGPIPE’s disposition to SIG_DFL in order to avoid BrokenPipeError. Doing that would cause your program to exit unexpectedly also whenever any socket connection is interrupted while your program is still writing to it.Regarding that socket behavior, if that's the standard in other programming environments, why is it an issue from python's perspective? Doesn't seem worth eschewing standard unix conventions. I mean, it says "unexpectedly", but isn't it actually the expected behavior? Unexpected is for python to say that what's default (SIG_DFL) is unexpected.
Note that this is incorrect. This is a common misconception about POSIX/UNIX. Not every process exits with an exit code; that's right, a process can exit without an integer exit code! A process exits with either a) an exit code, _OR_ b) the signal which terminated the program (as a separate status in the underlying struct, rather than as an exit code).
Many shells–including bash–provide an abstraction that translates process exits caused by signals into 128+signo, but this is only to fake an exit code for processes as seen when executed within the shell; this is a nonstandard and shell-specific abstraction.
Look at the man page for waitpid() for details. There are macros to test for the difference between an exit status with a code vs. an exit due to a signal; eg. WIFEXITED() and WIFSIGNALED(). WEXITSTATUS() will return the exit status... if one exists because WIFEXITED() tests truthful. Hint: if you're used to seeing your shell report 128+signo, that's because your particular shell happens to check waitpid() for WIFSIGNALED() and sets $? to 128+signo for you… as an abstracted convenience.
tldr; An exit code, vs. an exit caused by a signal, are mutually exclusive statuses. Processes terminated directly by a signal do not have an integer exit code; your shell may provide a fabricated exit code to make things appear simpler, but that is shell-specific and NOT a standard convention provided by POSIX or UNIX.
I was also surprised to find that, at least on my linux system, the status integer returned by wait encodes the signal number in the least significant bits and the exit status in the most significant bits. So, when a process dies from SIGPIPE, the status (as set by wait(&status)) is 13; when it does exit(1), it's 256; when it does exit(2), it's 512; when it does exit(3), it's 768, etc. Maybe that encoding was done to avoid misuse from people seeing their exit codes in the raw status returned by wait, and thinking they didn't need to use the macros.
In any case, this does mean that, indeed, while the shell does display the same status for both `yes | head -0` and `(exit 141)`, they are in fact different.
TIL
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
os.kill(os.getpid(), signal.SIGPIPE)
(Then there's no need to fiddle with stderr.) rm -vfr folder/ | head
involves SIGPIPE causing problems because it may or may not kill rm before it finishes deleting the directory, based on its internal output buffer size. rm -vrf directory/ | (head; cat >/dev/null)In any case, I think that in the vast majority of pipelines, the default SIGPIPE behavior is not what is desired.
More real-life example fitting your model would be "start this long-running process and show me first 10 lines of output, just to make sure it didn't bail right away", but then, a more common pattern would be to keep a copy of the whole log too:
myfoo >/var/log/foo & tail -f /var/log/foo | head
Counter-example would be "yes", but also unordered sampling. Let's say I want to know if there is file with "foo" in its name somewhere in a very large directory, just a yes/no. Continuing directory traversal after finding one is a waste of time. SIGPIPE is desirable here as well: find directory/ -name '*foo*' | head -1The the Unix philosophy is that users are smart and should be obeyed, not that programs should do what they think should have asked for instead. Forcing the user to negotiate with the program like a TARDIS is madness
How is this better? At least with the exception you can cleanly terminate your program if you need to.
Naturally, in the rare case you want to do some clean-up you can:
$ perl -E '$SIG{PIPE} = sub { die "cleanup" }; say "y" while 1' | head -1
y
cleanup at -e line 1.Python provides mechanisms to handle signals. The point of a signal is to indicate something outside of your process has happened and you may want to respond to it. It's up to the program to respond to relevant signals and Python in no way stops a developer from doing that.
I don't follow your analogy. The default signal disposition for SIGPIPE is to silently terminate the process. Normally processes compose nicely on the command-line and this requires no extra work from the programmer. By disregarding this convention, most Python programs pollute your terminal when used in pipelines, which is why I claim that in this case Python is a bad unix citizen.
Python in no way stops you from a having a typical response to SIGPIPE, it's as simple as handling the exception and doing a sys.exit(), but it doesn't "just happen". This makes the typical path a little more contrived, but I don't think having explicit handling makes it a bad citizen.
To torture my analogy, in my mind a "bad citizen" of the highway may be a car incapable of doing the minimum speed limit, whereas Python just has a manual transmission vs Perl's automatic (in this case).
I'm splitting hairs though, your point is fair. I just think your objection is a little strong for what comes down to convenience.
Defaults matter. It's like the difference between opt-in and opt-out for organ donation, or the difference between encouraging and requiring cars to have airbags.