Austin Group Defect Tracker

Aardvark Mark III


Viewing Issue Simple Details Jump to Notes ] Issue History ] Print ]
ID Category Severity Type Date Submitted Last Update
0000789 [1003.1(2008)/Issue 7] Shell and Utilities Comment Enhancement Request 2013-11-08 16:02 2015-05-31 18:27
Reporter weeks View Status public  
Assigned To ajosey
Priority normal Resolution Open  
Status Under Review  
Name Nathan Weeks
Organization USDA-ARS
User Reference
Section 2.9.2
Page Number 2341
Line Number 74464-74467
Interp Status ---
Final Accepted Text
Summary 0000789: Add set -o pipefail
Description The "set -o pipefail" option is a useful extension that allows the detection
of errors in pipelines where all commands are expected to have a zero exit
status, rather than just the last command; in my experience, this applies to
the majority of pipelines used in shell scripts, making the pipefail option
indispensable for writing robust shell scripts.

The pipefail option was originally implemented in ksh93 back in 1998; has
since been implemented in bash 3.0, busybox sh 1.16.0, and mksh R46; and will
be in zsh 4.0.2. It is notably absent from dash and FreeBSD/NetBSD /bin/sh.
Desired Action It may be clearer to describe the exit status of a pipeline with a table,
since the exit status of a pipeline would now depend on both the pipefail
option in addition to the ! reserved word.

In section 2.9.2, change:

    If the pipeline does not begin with the ! reserved word, the exit status shall
    be the exit status of the last command specified in the pipeline. Otherwise,
    the exit status shall be the logical NOT of the exit status of the last
    command. That is, if the last command returns zero, the exit status shall be 1;
    if the last command returns greater than zero, the exit status shall be zero.

to:

    The exit status of a pipeline shall depending on if the pipefail option is
    enabled, and if the pipeline is preceded by the ! reserved word to return
    the logical NOT of the exit status of the pipeline. The following table
    summarizes the effect of the pipefail option and the ! reserved word:

  pipefail Enabled    Preceded by !      Exit Status

    no                  no             The exit status of the last command
                                       specified in the pipeline.

    no                  yes            Zero, if the last command in
                                       the pipeline returned an exit
                                       status of 0; otherwise, 1.

    yes                 no             Zero, if all commands in the pipeline
                                       returned an exit status of 0;
                                       otherwise, the exit status of the
                                       last command specified in the
                                       pipeline that returned a non-zero
                                       exit status.

    yes                 yes            Zero, if any command in the
                                       pipeline returned a non-zero
                                       exit status; otherwise, 1.


In the description of "set -o option", add:

    pipefail
        When this option is enabled, the exit status of a pipeline shall be the
        exit status of the last command specified in the pipeline to return a
        non-zero exit status, or zero if all commands returned an exit status
        of 0.

It may be beneficial to include a note in this description referencing section
2.9.2, as the behavior of the ! reserved word also influences the exit status
of a pipeline.
Tags No tags attached.
Attached Files

- Relationships

-  Notes
(0001976)
eblake (manager)
2013-11-08 16:15

If we standardize this, then we need to also take some time to revisit what shell utilities should do when writing to a pipe when the reader closes early. Is it okay for a utility to exit with status 0 even when encountering a SIGPIPE (or EPIPE write() failure if SIGPIPE is ignored), when the utilities main goal is a filter that writes to stdout? Right now, it's perfectly okay for 'grep $pattern largefile | head -n1' to have grep exit with non-zero status because the pattern matched enough lines in largefile that it hits a write failure when head exits early; but if you support a pipefail mode, then grep exiting with non-zero status due to writing to a closed pipe would cause seemingly random behavior (a pipeline may succeed normally, but fail when the system is heavily loaded).
(0001977)
Don Cragun (manager)
2013-11-08 16:57

I merged the attached file containing the table describing the update to a pipeline's exit status into the Desired Action and removed the attached file.
(0001978)
shware_systems (reporter)
2013-11-08 21:05
edited on: 2013-11-12 16:08

Adding set -o implies set +o too... A pipe could be wrapped with set +o pipefail; pipe_and_error_handling; set -o pipefail; easy enough. An application usage blurb that this sort of case needs to be taken into account should be provided, as that is a valid looking concern, but I'm not sure any behavioral changes otherwise are needed. An alternative would be a pipeonce version where this behavior usually off but a set -o pipeonce; pipe; just apples to that pipe.

My concern with this would be: with the current way you know the exit status applies to the last process of the pipe. Just setting an option doesn't provide a way for the script to determine which process errored first so appropriate recovery can be attempted, and the error code should reflect that process. A new special variable would be needed, similar to argument count, to handle that. The proposed behavior, of last error is reported, may be a cascade of simply "Unexpected EOF" rather than "Socket connection timed out" that curl might report as first error.

(0001982)
weeks (reporter)
2013-11-12 15:42

Aside from using "set +o pipefail" to disable pipefail for an entire pipeline,
if it is known beforehand that a command in a pipeline may not read all the
output from the previous command, pipefail can be selectively disabled by
running the affected command(s) in a subshell that ignores SIGPIPE; e.g.:

    grep "$pattern" largefile | (trap '' PIPE; sort) | head -n 1

This allows the grep & sort exit statuses to still influence the exit status
of the entire pipeline, where the sort command may return a non-zero exit
status for reasons other than a SIGPIPE.
(0001983)
eblake (manager)
2013-11-12 15:51

(trap '' PIPE; sort) isolates you from sort dying from SIGPIPE, but does NOT isolate you from sort exiting with non-zero status due to claiming that an EPIPE failure to write() is fatal. Which brings me back to my argument: if we are going to mandate 'set -o pipefail', we also need to standardize on whether utilities must treat EPIPE as fatal (as implied by the current wording on error handling), or whether it makes sense to mandate that some utilities shall treat EPIPE as non-fatal (namely, any utility that has no side effects other than outputting to stdout, where EPIPE is a sign that the reader on the other end no longer cares about that output).
(0002067)
weeks (reporter)
2013-12-11 22:40

Eric: understood; I had completely missed that point in your first response. It may be okay for certain standard utilities to exit with status 0 upon write() failing due to EPIPE, though in that case I don't think it would be okay for such utilities to ignore SIGPIPE by default; this gives the script writer control over whether or not writing to a closed pipe constitutes an error (e.g., using the previously-mentioned workaround if a SIGPIPE is expected).

Even without such a mandate, forcing the "sort" stage in the previous example pipeline to exit with status 0, e.g.:

grep "$pattern" largefile | (sort; true) | head -n 1

is still an improvement over not having the pipefail option at all---at least "grep" can fail and cause the exit status of the pipeline to be nonzero.
(0002107)
shware_systems (reporter)
2014-01-16 23:15

Per phone call 2014-Jan-09, text from Etherpad reflecting discussion points:

Knowing a pipeline failed is not as useful without an array variable of which process(es) in the pipeline failed - both ksh and bash have pipefail, but they also have an array variable extension that gives this extra information
Proposed is an index variable that relates failure code reported to process position in pipe, absent a standardized array capability. Maybe something like 'fail() { return $1; }; fail 2 | : | fail 3' producing "2 0 3" in the new variable. Maybe named POSIX_PIPE_STATUS
Generically, value uses space separated string of the return codes for each member of the pipe in decimal notation. Not discussed, would a new pipe invocation clear prior value, or is it the scripts responsibility to clear it?
Presuming the first, a caveat that a script has to copy the value if error recovery also uses a pipe may be desirable, rather than reference it directly.

A method to check for parent relations may be: call
cat /etc/termcap | cat | ps -f

and then check for PPID in the ps output

Action: Andrew: Ask shell maintainers: Would it be appropriate to add set -o pipefail and set +o pipefail without also adding the arrays that provide the exit status of all processes in the pipeline (or some non-array equivalent; see above)?
Standardizing arrays first may be the higher priority, as arrays offer a lot more benefits than just pipeline status
(0002253)
weeks (reporter)
2014-05-28 15:47

mksh, bash, and zsh implement PIPESTATUS (pipestatus, for zsh) arrays containing the exit statuses of the previous foreground pipeline; however, busybox ash lacks arrays, and ksh does not have an equivalent array of exit statuses.

It would be expedient to first standardize common practice that's portable between the existing implementations ("set -o pipefail" only), then possibly mandate an array of exit statuses if/when arrays are standardized.
(0002685)
stephane (reporter)
2015-05-29 21:14

I find "last" in the proposed text ambiguous as there's a possible confusion between "last" meaning "rightmost" or "last to exit".

In:

set -o pipefail
(sleep 3; exit 3) | (sleep 1; exit 2) | true

$? should be 2 (the rightmost command that exited with a non-zero status), not 3 (the last command to exit with a non-zero exit status after 3 seconds).
(0002689)
stephane (reporter)
2015-05-31 18:27
edited on: 2015-05-31 19:27

IMO, changing utilities so that they return success upon SIGPIPE is out of the question. Think for instance of scripts that should be able to exit early when the command they run dies of a SIGPIPE when stdout is a broken pipe.

#! /bin/sh -
utility1 || exit
echo foo
utility3 || exit
echo bar
...


We could make the pipefail implementation ignore the status of commands that die of SIGPIPE, but then again, deciding what constitutes a SIGPIPE death is not always clear and the decision would be better left to the application.

For instance, in:

   (cmd1; exit) | script2 | cmd3

The first subshell would typically terminate normally with exit code 141 (128+SIGPIPE) if the pipe became broken. Could be the same for script2, though an exit code of 141 could also be caused by an unrelated pipe.

Also, option handling is not really compatible with the function features. Options are better avoided where possible.

For instance in:

set -o pipefail
cmd1 | cmd2 | cmd3
set +o pipefail

If any of those cmd1/2/3 is a function, setting that option could unexpectedly change the behaviour of that function. This kind of issue crops up all the time for other options. For instance, the bash completion system (based on functions) fails when you change some of the default options. Some shells like zsh can alleviate the problem by resetting the options to their default in a local scope, but even then that's cumbersome and not ideal (and there's nothing like that in POSIX sh yet).

Instead I'm suggesting introducing a new builtin to check the pipeline failure which could also address some of the other deficiencies with termination status query in shells. Something like:

  status [-acsfl] [-i <signame>[+]] [-I <signame>[+]]...
  status -p [-acs]

Without argument, status returns true if the previous command succeeds and the same exit status otherwise (mainly a no-op unless ! was used which is ignored).

With:
 - -f -l (implies -a (for all/any)): returns true unless any of the commands in the pipeline did not return true. In which case the status of the first (leftmost) for -f or last (rightmost) for -l is returned instead (if -a is given without -l/-f, -l is implied).
 - -c: returns true if any (with -a) or the last command (without -a) in the last pipeline exited with a coredump.
 - -s: returns true unless any (with -a) or the last command (without -a) in the last pipeline exited with a signal. Otherwise returns the $? corresponding to the signal of that command (the first with -f, the last with -l)
 - -i: when reporting the status, ignore commands that die of a signame signal. If expressed as signame+, also ignore commands that returned with a status corresponding to that signame (-i pipe ignores commands that died of a SIGKILL, -i pipe+ also ignores commands that died of exit(141) (on systems where shells use that code to report a death by SIGPIPE).
 - -I: same as above but only ignores the signal in all but the last pipeline element.

With -p, print status information instead. With -p alone, equivalent to echo "$?" unless ! was used for the last pipeline.

With:
 -a: print the status of all elements of the pipeline, one per line (from the leftmost pipeline element to the rightmost)
 -s: for those commands that died of a signal, print the signal name (pipe/int/kill...) instead of number computed like $?
 -c: for those processes that generated a core, append +core.

Examples:

system("cmd1 | cmd2; status -a") would also check the exit status of cmd1. Similar to:

system("set -o pipefail; cmd1 | cmd2") without side-effects if cmd1 or cmd2 is a function.

system("cmd1 | cmd2; status -aI pipe") same but ignores SIGPIPE death of cmd1

system("cmd1 | cmd2; status -aI pipe+") same but also ignores a *reported* SIGPIPE death.

system("cmd1 | cmd2; status -ai xcpu+ -i alrm+ -I pipe+") ignore xcpu/alrm deaths only report real errors.


cmd1 | cmd2 | cmd3
status -pasc

would print:

pipe
segv+core
0

if cmd2 died of a SIGSEGV and dumped a core and as a result cmd1 died of a SIGPIPE. While cmd3 did an exit(0).

A note about "!":

set -o pipefail
! cmd1 | cmd2

would be written:

cmd1 | cmd2; ! status -a

! cmd2 | cmd2; ! status -a
has the same effect, except in case of "set -e" and cmd2 fails.

What do you think?


- Issue History
Date Modified Username Field Change
2013-11-08 16:02 weeks New Issue
2013-11-08 16:02 weeks Status New => Under Review
2013-11-08 16:02 weeks Assigned To => ajosey
2013-11-08 16:02 weeks Name => Nathan Weeks
2013-11-08 16:02 weeks Organization => USDA-ARS
2013-11-08 16:02 weeks Section => 2.9.2
2013-11-08 16:02 weeks Page Number => 0
2013-11-08 16:02 weeks Line Number => 0
2013-11-08 16:06 weeks File Added: pipeline_exit_status.txt
2013-11-08 16:07 weeks Desired Action Updated
2013-11-08 16:15 eblake Note Added: 0001976
2013-11-08 16:50 Don Cragun Interp Status => ---
2013-11-08 16:50 Don Cragun Desired Action Updated
2013-11-08 16:57 Don Cragun Page Number 0 => 2341
2013-11-08 16:57 Don Cragun Line Number 0 => 74464-74467
2013-11-08 16:57 Don Cragun Note Added: 0001977
2013-11-08 16:58 Don Cragun File Deleted: pipeline_exit_status.txt
2013-11-08 21:05 shware_systems Note Added: 0001978
2013-11-12 15:42 weeks Note Added: 0001982
2013-11-12 15:51 eblake Note Added: 0001983
2013-11-12 16:08 shware_systems Note Edited: 0001978
2013-12-11 22:40 weeks Note Added: 0002067
2014-01-16 23:15 shware_systems Note Added: 0002107
2014-05-28 15:47 weeks Note Added: 0002253
2015-05-29 21:14 stephane Note Added: 0002685
2015-05-31 18:27 stephane Note Added: 0002689
2015-05-31 19:27 stephane Note Edited: 0002689


Mantis 1.1.6[^]
Copyright © 2000 - 2008 Mantis Group
Powered by Mantis Bugtracker