diff --git a/cronic b/cronic index b26d786..3c92872 100755 --- a/cronic +++ b/cronic @@ -15,6 +15,21 @@ # The options and the treatment of execution traces are documented in function # `cronic_help`; run `cronic --help`. +# The caller's options, as commands that restore them. This script restores +# the caller's options while it runs the wrapped command. Otherwise, any bash +# script that the wrapped command runs, such as a `#!/bin/bash` script, would +# get this script's `set -eu` rather than the caller's options and, for example, +# fail on its first unset variable. +# Limitation: if the wrapped command is a builtin that runs code in this +# script's shell, such as `source` or `eval`, that code also runs with the +# caller's options, even if the caller does not export SHELLOPTS. So if the +# caller does not use `set -u`, that code silently expands an unset variable +# to the empty string. +# Not `$(set +o)`, because a command substitution, unlike a process +# substitution, does not inherit `set -e`. `read` exits with status 1 at the +# end of its input. +read -r -d '' cronic_caller_opts < <(set +o) || true + # Exit if a command exits with a non-zero status, and treat unset variables # as an error. set -eu @@ -158,11 +173,17 @@ if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1))); export BASH_XTRACEFD=$cronic_xtrace_fd fi +# The wrapped command runs in a subshell with the caller's options. It is not +# the operand of `||` or the like, because then bash would ignore `set -e` and +# would remove "errexit" from the SHELLOPTS that the wrapped command inherits. set +e -# `command` because the wrapped command runs in this shell, where a function of -# this script, such as `cronic_usage`, would otherwise run in place of a -# program of the same name. -command -- "$@" > "$cronic_out" 2> "$cronic_trace" +( + eval "$cronic_caller_opts" + # `command` because the wrapped command runs in a subshell of this shell, + # where a function of this script, such as `cronic_usage`, would otherwise + # run in place of a program of the same name. + command -- "$@" +) > "$cronic_out" 2> "$cronic_trace" cronic_result=$? set -e diff --git a/tests/cronic-test/cronic-test.sh b/tests/cronic-test/cronic-test.sh index 7b62509..dcd1a9b 100755 --- a/tests/cronic-test/cronic-test.sh +++ b/tests/cronic-test/cronic-test.sh @@ -374,4 +374,30 @@ check "PS4 with a newline, error line that contains its second line" 0 \ "report:^error: bad b input$" "$work/stderr-lines" "error: bad b input" unset PS4 +# A command that expands an unset variable. +cat > "$work/unset-variable" << 'EOF' +#!/bin/bash +echo "${cronic_test_unset_variable}" +EOF +chmod +x "$work/unset-variable" + +# A caller that sets the option given as its first argument, such as `-u` or +# `+u`, and exports SHELLOPTS. When the caller exports SHELLOPTS, the wrapped +# command gets the caller's `set -u` setting, not `cronic`'s. +cat > "$work/caller" << EOF +#!/bin/bash +set "\$1" +shift +export SHELLOPTS +exec "$CRONIC" "\$@" +EOF +chmod +x "$work/caller" +saved_cronic="$CRONIC" +CRONIC="$work/caller" +check "exported SHELLOPTS without nounset, unset variable" 0 silent \ + +u "$work/unset-variable" +check "exported SHELLOPTS with nounset, unset variable" 1 \ + "report:unbound variable" -u "$work/unset-variable" +CRONIC="$saved_cronic" + exit "$status"