4

Suppose you have a following script sandbox.sh.

(This looks similar to Command substitution inside a function does not stop the script on a failure even if -e is set, but I believe a bit different one)

At "The Line", I am trying to pass an output from func2 to func1 as a positional parameter.

#!/bin/bash
set -eu -o pipefail -E
shopt -s inherit_errexit

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

function func2() {
  echo "value from func2"
  exit 1
}

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

This is a possible situation, where a programmer desires to construct a data by reusing existing functions in a concise manner, I believe.

However, this results in following output.

$ bash sandbox.sh 
(func1)This line shouldn't be reached:arg='value from func2': '0'
main:This line shouldn't be reached:var='':'0'
$ 

I interpreted this behavior that the sub-shell that invokes func2 is not inheriting errexit behaviour from the sub-shell for func1. Thus, func1 takes unset value as its argument.

I tried to make it work intendedly by modifying "The Line", like

# A.
var="$(set -e; func1 "$(set -e; func2)")" # The Line

Or

# B.
var="$(func1 "$(func2 || exit 1)" || exit 1)" # The Line

But no luck yet. Actually, I am puzzled at neither A. nor B. not fixing the behavior. What is a best practice for this? Or shouldn't I attempt to nest command substitutions from the first place?

Hiroshi_U
  • 167
  • 1
  • 7

1 Answers1

0

Based on an answer so far (, which seems to be deleted) and experiments I made, I came to think that this (nested command substitution) should only be considered a 'bad practice' or a at best 'very cumbersome'.

The best I could do is

#!/bin/bash
set -eu -o pipefail -E
shopt -s inherit_errexit

function func1() {
  [[ $? == 0 ]]; return 1 # The boilerplate line
  local arg="${1}"
  echo "(func1)This line shouldn't be reached:arg='${arg}': '${?}'" >&2
}

function func2() {
  echo "value from func2"
  return 1
}

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

This results in an expected output (empty) and non-zero (1) exit code. The trick is to check if the $? is 0 at the first line in a definition of function. And this (The boilerplate line) needs to be inserted for every function definition that is reused in 'nested command substitution'. If you want to use 'the nested command substitution' syntax without becoming anxious, you need to familiarize yourself with inserting the boilerplate every time you create a new function.

And is 'The boilerplate line' really harmless? Apparently, yes, I believe. Since if you set -eu, an immediate abortion is expected whenever a failure is detected, either way. But I'm not good at bash programming and maybe missing some potential pitfall. I really would like to know more experienced people's insights. Thanks in advance.

Hiroshi_U
  • 167
  • 1
  • 7
  • Consider echo "$(false)"; functions are irrelevant to the general issue. This example also shows that nested command-replacement isn't necessary to experience the issue. – bukzor Feb 11 '21 at 23:40
  • Nested command substitution is not a bad practice. It's just that all this set -euo pipefail; shopt -s inherit_errexit is a questionable thing. No good solution. Although some recommend it. No generally accepted opinion as well. As for the boilerplate code, that sounds like a bad practice. – x-yuri Oct 20 '21 at 14:29