7

I have a following script sandbox.sh,

#!/bin/bash
set -eu -o pipefail -E

function func1() {
  echo "FUNC1"
  exit 1
}

function func2() {
  local ret
  ret=$(func1)
  echo $ret
  echo "(func2)This line shouldn't be reached:'${?}'" >&2
}

var=$(func1) # The Line
echo "main:This line shouldn't be reached:'${var}':'${?}'" >&2

(GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu))

This stops executing expectedly,

$ bash -eu sandbox.sh 
$ 

However, if I modify "The Line" to var=$(func2) to call func1 through func2, it will give me following output

$ bash sandbox.sh 
(func2)This line shouldn't be reached:'0'
main:This line shouldn't be reached:'FUNC1':'0'
$ 

To me, it seems command substitution behaves differently when it is placed inside a function, but I don't see why bash is designed so. Also it is a quite possible situation where a function's output is used by another and such a difference is confusing.

NOTE: If I rewrite func2 like following,

function func2() {
  func1
}

The script stops at The Line. However, programmers quite often want to manipulate output from func1, I believe.

Hiroshi_U
  • 167
  • 1
  • 7
  • 5
    This has nothing to do with functions, but with subshells (ie with your $(...) command substitutions). set -e / -o errexit is not inherited in subshells. Simpler example: bash -c 'set -e; echo $(false; echo survived)'. If you had used var=$(set -e; func2) in "The Line", the "shouldn't be reached" lines wouldn't have been reached. –  Sep 14 '19 at 02:09
  • 1
    Another option is to add shopt -s inherit_errexit at the beginning of your script (also enabled in posix mode). Notice that set -e / errexit is independent of the ERR trap and is not affected by set -E / errtrace –  Sep 14 '19 at 02:20
  • @mosvy, Thanks, set -e made my script work as intended. But it also made puzzled me a bit more. Why don't I need to add set -e to the line that performs command substitution in func2? – Hiroshi_U Sep 14 '19 at 02:21
  • because func1 calls exit 1, causing the subshell to exit with a non-zero status, which will also be the exit status of ret=$(func1). –  Sep 14 '19 at 02:25
  • Ah! and func2 that contains ret=$(func1) is executed in a shell whose -e is set at The Line. – Hiroshi_U Sep 14 '19 at 02:29
  • I think shopt -s inherit_errexit should be considered sort of "best practice". Because otherwise we cannot reuse functions reliably. To my memory, I have never seen web sites that introduces the technique as a popular good practice. Is there any reason for that? Or any drawbacks? – Hiroshi_U Sep 14 '19 at 02:32
  • imho, set -e is too tricky to be used reliably -- the basic idea behind it is right, but the implementation is lousy (no, I haven't checked the correctness of those tables! ;-)). –  Sep 14 '19 at 02:36
  • 1
    @mosvy, note that inherit_errexit is on when bash is running as sh. I wouldn't say the idea is right, I'm not sure it could ever be implemented with a clear and consistent API. To me, it's better avoided, reserved for the most basic of scripts (i.e. with the real-life definition of script, when you just put in a file a plain sequence of commands run one after the other). – Stéphane Chazelas Sep 14 '19 at 06:12
  • @Kusalananda the answers to the purported dupe in no way address set -e not being inherited in subshells in bash, inherit_errexit being a workaround for it, it being the default in posix mode, or the pitfalls of set -e in general. –  Sep 14 '19 at 13:10
  • 1
    @mosvy You mentioned in a flagging message that I put this question on hold in order to reduce its visibility. This is not so. I put it on hold because I honestly thought it was a duplicate. I'm reopening it now because I believe that you may have a good answer for this question. – Kusalananda Sep 14 '19 at 23:14

1 Answers1

6

This is all perfectly understandable if we step through slowly. Some more logging is required, so run bash with the -x parameter, which will echo commands just before bash executes them, prefixed by + .

First run

$ bash -x sandbox.sh; echo $?
+ set -eu -o pipefail -E
++ func1
++ echo FUNC1
++ exit 1
+ var=FUNC1
1
  • -e says this shell will exit immediately a command returns non-zero. Crucially though, you run func1 in a subshell (using $( )). The trace above shows this fact by using two +s as the prefix (++ ).
  • The subshell spits out FUNC1 on stdout, and then exits with return code 1.
    • Note: -e is off inside this subshell. The reason the subshell quit was due to the exit command, not -e. You can't really tell this due to the way func1 is written.
  • Back in the first shell, we assign FUNC1 to the variable var. However, the exit code of this assignment command is the exit code of the last command substitution. Bash sees this failure (i.e., non-zero exit code), and quits.

To quote the manual's SIMPLE COMMAND EXPANSION section:

If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed.

Second run

Exactly the same explanation as the first run. We note again that the -e is not in effect inside the subshell. This time however, there is a material difference — we get a clearer view of what is happening.

  • The exit code of func2 is the exit code of its last command
  • That echo always succeeds.
  • func2 always succeeds
  • The assignment always succeeds.

-e has no effect.

shopt -s inherit_errexit ?

This will turn on -e in subshells. It is however a difficult bedfellow. It does not guarantee we assert when a command fails.

Consider this:

set -e
shopt -s inherit_errexit

f() { echo a; (exit 22); echo b; }

echo "f says [$(f)] $?" echo byee

This time the command substitution is part of an echo, rather than an assignment, and we get

+ set -e
+ shopt -s inherit_errexit
++ f
++ echo a
++ exit 22
+ echo 'f says [a] 22'
f says [a] 22
+ echo byee
byee
  • The subshell sees a command that fails with exit code 22. Since -e is in effect, the shell exits with code 22 (echo b does not execute).
  • Back in the first shell, echo gets a as the output of f, and 22 as the exit code of the subshell
  • Thing is, unlike an assignment, the exit code of the echo is zero.

Version

$ bash --version
GNU bash, version 5.0.17(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.

bobbogo
  • 194