-6

summary: If in bash I attempt to assign the output of a missing function to a previously-declared (i.e., not {constant, read-only}) variable, I can detect the failure with "normal" tests. But if I attempt to assign the output of a missing function to a variable while declareing it (e.g., to make the var {constant, read-only}), not only does the assignment not fail with "normal" tests, but I cannot force failure with "normal" builtins. How can I make the latter case fail?

details:

I recently ran into a problem in a much larger bash script, which I've attempted to distill into the following 2 scripts. Basically, I'm sorta-kinda doing TDD with bash ( snark > /dev/null ), so among other things

  • I want missing commands/functions to fail fast
  • I want to prevent rewriting constants

However, bash seems to be allowing me to assign the output of a missing function to a variable while declareing the var. E.g., the following script (saved as /path/to/assign_at_declare.sh ) ...

#!/usr/bin/env bash

function foo() {
    return 0
}

# function bar() {}               # does not exist!

declare ret_val=''
declare -r MESSAGE_PREFIX="$(basename "${BASH_SOURCE}"):"
declare -r ERROR_PREFIX="${MESSAGE_PREFIX} ERROR:"

echo -e "\n${MESSAGE_PREFIX} a naïve 1st attempt:\n"

declare -ir FOO1_VAL="$(foo)"   # this should succeed, and does
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned '${ret_val}', exiting ..."
    exit 3
elif [[ -z "${FOO1_VAL}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned null, exiting ..."
    exit 4
else
    echo "${MESSAGE_PREFIX} FOO1_VAL='${FOO1_VAL}'"
fi

declare -ir BAR1_VAL="$(bar)"   # this should fail ... but doesn't
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned '${ret_val}', exiting ..."
    exit 5
elif [[ -z "${BAR1_VAL}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned null, exiting ..."
    exit 6
else
    echo "${MESSAGE_PREFIX} BAR1_VAL='${BAR1_VAL}'"
fi

echo -e "\n${MESSAGE_PREFIX} get tough using \`set\` builtins:\n"
# see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
set -o errexit
set -o pipefail

declare -ir FOO2_VAL="$(foo)"   # this should succeed, and does
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned '${ret_val}', exiting ..."
    exit 3
elif [[ -z "${FOO2_VAL}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned null, exiting ..."
    exit 4
else
    echo "${MESSAGE_PREFIX} FOO2_VAL='${FOO2_VAL}'"
fi

declare -ir BAR2_VAL="$(bar)"   # this should fail ... but doesn't
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned '${ret_val}', exiting ..."
    exit 5
elif [[ -z "${BAR2_VAL}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned null, exiting ..."
    exit 6
else
    echo "${MESSAGE_PREFIX} BAR2_VAL='${BAR2_VAL}'"
fi

exit 0

... produces the following output:

assign_at_declare.sh: a naïve 1st attempt:

assign_at_declare.sh: FOO1_VAL='0'
/path/to/assign_at_declare.sh: line 27: bar: command not found
assign_at_declare.sh: BAR1_VAL='0'

assign_at_declare.sh: get tough using `set` builtins:

assign_at_declare.sh: FOO2_VAL='0'
/path/to/assign_at_declare.sh: line 56: bar: command not found
assign_at_declare.sh: BAR2_VAL='0'

This seems odd, since I do not observe this behavior if I attempt to assign the output from the missing function to a variable after declareing the var (i.e., if the var is not {constant, read-only}) as in the following script (saved as /path/to/assign_after_declare.sh ) ...

#!/usr/bin/env bash

function foo() {
    return 0
}

# function bar() {}           # does not exist!

declare ret_val=''
declare -i foo_val=0
declare -i bar_val=0
declare -r MESSAGE_PREFIX="$(basename "${BASH_SOURCE}"):"
declare -r ERROR_PREFIX="${MESSAGE_PREFIX} ERROR:"

echo -e "\n${MESSAGE_PREFIX} following works as expected\n"

foo_val="$(foo)"           # this should succeed, and does with/out `declare`
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned '${ret_val}', exiting ..."
    exit 3
elif [[ -z "${foo_val}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} foo returned null, exiting ..."
    exit 4
else
    echo "${MESSAGE_PREFIX} foo_val='${foo_val}'"
fi

bar_val="$(bar)"           # this succeeds with `declare`, fails without
ret_val="${?}"
if   [[ "${ret_val}" -ne 0  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned '${ret_val}', exiting ..."
    exit 5
elif [[ -z "${bar_val}"  ]] ; then
    >&2 echo "${ERROR_PREFIX} bar returned null, exiting ..."
    exit 6
else
    echo "${MESSAGE_PREFIX} bar_val='${bar_val}'"
fi

exit 0

... which produces the following output:

assign_after_declare.sh: following works as expected

assign_after_declare.sh: foo_val='0'
/path/to/assign_after_declare.sh: line 29: bar: command not found
assign_after_declare.sh: ERROR: bar returned '127', exiting ...

Is there a way to force bash to fail-fast when assigning during a declare? If so, I await your answer.

Alternatively, is bash Working As Designed? If so, please link to a reference; I tried searching this question, but my selectors were either incorrect or returned so many unrelated responses as to be useless.

  • What is 'un-causing problems'? It sounds nasty. – datUser Feb 8 '18 at 21:27
1

declare's return status is:

zero unless an invalid option is encountered, an attempt is made to define a function using -f foo=bar, an attempt is made to assign a value to a readonly variable, an attempt is made to assign a value to an array variable without using the compound assignment syntax, one of the names is not a valid shell variable name, an attempt is made to turn off readonly status for a readonly variable, an attempt is made to turn off array status for an array variable, or an attempt is made to display a non-existent function with -f.

As none of these situations apply, Bash is Working As Designed, as you put it.

$(bar) executes bar in a subshell, which exits with an error and no standard output. The substitution results in an empty string, which is interpreted as zero for an integer variable. declare then returns 0, as documented.


You could detect a failure by, for example:

declare -ir x="$(bar || echo failed running bar >&3)"

with fd 3 set up appropriately in advance. Such setup is left as an exercise to the reader.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.