Differences between revisions 13 and 15 (spanning 2 versions)
Revision 13 as of 2016-04-25 14:49:27
Size: 713
Editor: OrvilleMcq
Comment:
Revision 15 as of 2016-09-16 11:43:34
Size: 4300
Editor: ip-83-134-84-140
Comment: explain why set -e ignores process substitution
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
Melvin Hartung is what my wife enjoys to call me although it is not the title on my birth certification. Tennessee is her birth place. I work as a inventory control and order filler but I've always wanted my own business. The favorite pastime for him and his children is to fence and he's been doing it for quite a whilst. Her spouse and her preserve a web site. You may want to check it out: http://www.netgestalter.de/blog/blogging/wieder-zurueck-und-das-ende-der-blogparade.html<br><br><<BR>>
<<BR>>
 http://i1038.photobucket.com/albums/a462/Harukomel/HTML/4.jpg Visit my blog post ... [[http://www.netgestalter.de/blog/blogging/wieder-zurueck-und-das-ende-der-blogparade.html|social media consulting]]<br>
<<Anchor(faq105)>>
== Why doesn't set -e (or set -o errexit, or trap ERR) do what I expected? ==
`set -e` was an attempt to add "automatic error detection" to the shell. Its goal was to cause the shell to abort any time an error occurred, so you don't have to put `|| exit 1` after each important command.

That goal is non-trivial, because many commands intentionally return non-zero. For example,

{{{
  if [ -d /foo ]; then ...; else ...; fi
}}}

Clearly we don't want to abort when the conditional, `[ -d /foo ]`, returns non-zero (because the directory does not exist) -- our script wants to handle that in the `else` part. So the implementors decided to make a bunch of special rules, like "commands that are part of an `if` test are immune", or "commands in a pipeline, other than the last one, are immune".

These rules are extremely convoluted, and they still fail to catch even some remarkably simple cases. Even worse, the rules ''change'' from one Bash version to another, as Bash attempts to track the extremely slippery POSIX definition of this "feature". When a SubShell is involved, it gets worse still -- the behavior changes depending on whether Bash is invoked in POSIX mode. [[http://fvue.nl/wiki/Bash:_Error_handling|Another wiki]] has a page that covers this in more detail. Be sure to check the caveats.

''Exercise for the reader: why doesn't this example print anything?''

{{{#!highlight bash
#!/usr/bin/env bash
set -e
i=0
let i++
echo "i is $i"
}}}

''Exercise 2: why does '''this''' one sometimes appear to work? In which versions of bash does it work, and in which versions does it fail?''

{{{#!highlight bash
#!/usr/bin/env bash
set -e
i=0
((i++))
echo "i is $i"
}}}

''Exercise 3: why aren't these two scripts identical?''
{{{#!highlight bash
#!/usr/bin/env bash
set -e
test -d nosuchdir && echo no dir
echo survived
}}}

{{{#!highlight bash
#!/usr/bin/env bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
}}}

''Exercise 4: why aren't '''these''' two scripts identical?''
{{{#!highlight bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
}}}

{{{#!highlight bash
set -e
f() { if test -d nosuchdir; then echo no dir; fi; }
f
echo survived
}}}

''Exercise 5: under what conditions will this fail?''
{{{#!highlight bash
set -e
read -r foo < configfile
}}}

([[/Answers|Answers]])

Even if you use `expr(1)` (which we ''do not'' recommend -- use [[ArithmeticExpression|arithmetic expressions]] instead), you still run into the same problem:

{{{#!highlight bash
set -e
foo=$(expr 1 - 1)
# The following command will not be executed:
echo survived
}}}

Another pitfall associated with `set -e` occurs when you use commands that ''look like'' assignments but aren't, such as `export`, `declare`, `typeset` or `local`.

{{{#!highlight bash
set -e
f() { local var=$(somecommand that fails); }
f # will not exit

g() { local var; var=$(somecommand that fails); }
g # will exit
}}}

In function `f`, the exit status of `somecommand` is discarded. It won't trigger the `set -e` because the exit status of `local` masks it (the assignment to the variable succeeds, so `local` returns status 0). In function `g`, the `set -e` is triggered because it uses a ''real'' assignment which returns the exit status of `somecommand`.

Using [[ProcessSubstitution|Process substitution]], the exit code is also discarded as it is not visible from the main script:

{{{#!highlight bash
set -e
cat <(somecommand that fails)
echo survived
}}}

Using a pipe makes no difference, as only the ''rightmost'' process is considered:

{{{#!highlight bash
set -e
somecommand that fails | cat -
echo survived
}}}

`set -o pipefail` is a workaround by returning the exit code of the ''first'' failed process:

{{{#!highlight bash
set -e -o pipefail
failcmd1 | failcmd2 | cat -
# The following command will not be executed:
echo survived
}}}

GreyCat's personal recommendation is simple: don't use `set -e`. Add your own error checking instead.

rking's personal recommendation is to go ahead and use `set -e`, but beware of possible gotchas. It has useful semantics, so to exclude it from the toolbox is to give into FUD.

Why doesn't set -e (or set -o errexit, or trap ERR) do what I expected?

set -e was an attempt to add "automatic error detection" to the shell. Its goal was to cause the shell to abort any time an error occurred, so you don't have to put || exit 1 after each important command.

That goal is non-trivial, because many commands intentionally return non-zero. For example,

  if [ -d /foo ]; then ...; else ...; fi

Clearly we don't want to abort when the conditional, [ -d /foo ], returns non-zero (because the directory does not exist) -- our script wants to handle that in the else part. So the implementors decided to make a bunch of special rules, like "commands that are part of an if test are immune", or "commands in a pipeline, other than the last one, are immune".

These rules are extremely convoluted, and they still fail to catch even some remarkably simple cases. Even worse, the rules change from one Bash version to another, as Bash attempts to track the extremely slippery POSIX definition of this "feature". When a SubShell is involved, it gets worse still -- the behavior changes depending on whether Bash is invoked in POSIX mode. Another wiki has a page that covers this in more detail. Be sure to check the caveats.

Exercise for the reader: why doesn't this example print anything?

   1 #!/usr/bin/env bash
   2 set -e
   3 i=0
   4 let i++
   5 echo "i is $i"

Exercise 2: why does this one sometimes appear to work? In which versions of bash does it work, and in which versions does it fail?

   1 #!/usr/bin/env bash
   2 set -e
   3 i=0
   4 ((i++))
   5 echo "i is $i"

Exercise 3: why aren't these two scripts identical?

   1 #!/usr/bin/env bash
   2 set -e
   3 test -d nosuchdir && echo no dir
   4 echo survived

   1 #!/usr/bin/env bash
   2 set -e
   3 f() { test -d nosuchdir && echo no dir; }
   4 f
   5 echo survived

Exercise 4: why aren't these two scripts identical?

   1 set -e
   2 f() { test -d nosuchdir && echo no dir; }
   3 f
   4 echo survived

   1 set -e
   2 f() { if test -d nosuchdir; then echo no dir; fi; }
   3 f
   4 echo survived

Exercise 5: under what conditions will this fail?

   1 set -e
   2 read -r foo < configfile

(Answers)

Even if you use expr(1) (which we do not recommend -- use arithmetic expressions instead), you still run into the same problem:

   1 set -e
   2 foo=$(expr 1 - 1)
   3 # The following command will not be executed:
   4 echo survived

Another pitfall associated with set -e occurs when you use commands that look like assignments but aren't, such as export, declare, typeset or local.

   1 set -e
   2 f() { local var=$(somecommand that fails); }
   3 f    # will not exit
   4 
   5 g() { local var; var=$(somecommand that fails); }
   6 g    # will exit

In function f, the exit status of somecommand is discarded. It won't trigger the set -e because the exit status of local masks it (the assignment to the variable succeeds, so local returns status 0). In function g, the set -e is triggered because it uses a real assignment which returns the exit status of somecommand.

Using Process substitution, the exit code is also discarded as it is not visible from the main script:

   1 set -e
   2 cat <(somecommand that fails)
   3 echo survived

Using a pipe makes no difference, as only the rightmost process is considered:

   1 set -e
   2 somecommand that fails | cat -
   3 echo survived

set -o pipefail is a workaround by returning the exit code of the first failed process:

   1 set -e -o pipefail
   2 failcmd1 | failcmd2 | cat -
   3 # The following command will not be executed:
   4 echo survived

GreyCat's personal recommendation is simple: don't use set -e. Add your own error checking instead.

rking's personal recommendation is to go ahead and use set -e, but beware of possible gotchas. It has useful semantics, so to exclude it from the toolbox is to give into FUD.

BashFAQ/105 (last edited 2021-03-11 06:07:25 by dsl-66-36-156-249)