Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions cronic
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@
# This script runs under bash 3.2, which is /bin/bash on macOS, so it uses a
# feature of a later bash only after checking the version.

# The caller's shell options that this script changes, as the letters that `$-`
# uses for them, which this script restores for the command. They include any
# that the caller passed by exporting SHELLOPTS. This script turns xtrace off,
# so that its own commands are not traced to stderr, which would make every run
# produce output; the redirection discards the trace of `set +x` itself. It
# turns allexport off, so that its own variables are not exported to the
# command; `cronic_caller_flags`, which is assigned before that, is un-exported
# explicitly.
{
cronic_caller_flags=${-//[^aeux]/}
set +ax
export -n cronic_caller_flags
} 2> /dev/null

# Exit if a command exits with a non-zero status, and treat unset variables
# as an error.
set -eu
Expand Down Expand Up @@ -163,11 +177,20 @@ fi
# The command runs in a subshell rather than in this shell, so that a builtin
# such as `exit` or `exec` cannot end this script before it reports.
# `command` because a function of this script, such as `cronic_usage`, would
# otherwise run in place of a program of the same name.
# otherwise run in place of a program of the same name. The redirections
# apply to the whole subshell, so that they also capture what it writes after
# the command finishes, such as the output of an EXIT trap that the command
# sets. The subshell restores the caller's `-a`, `-e`, `-u`, and `-x` options,
# replacing this script's, for a builtin and also for a bash script when the
# caller exported SHELLOPTS. It turns off this script's nounset (errexit is off
# already) in the same command that turns on the caller's options, so that only
# the command is traced. The `:+` avoids a bare `-`, which has a special
# meaning to `set`.
set +e
(
command -- "$@" > "$cronic_out" 2> "$cronic_trace"
)
set +u${cronic_caller_flags:+ -$cronic_caller_flags}
command -- "$@"
) > "$cronic_out" 2> "$cronic_trace"
cronic_result=$?
set -e

Expand Down
77 changes: 77 additions & 0 deletions tests/cronic-test/cronic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,83 @@ check "builtin command" 0 silent :
# A builtin that would end the shell does not prevent the report.
check "exit builtin" 3 report exit 3
check "exec builtin" 3 report exec "$work/trace-only" 3
# Output that the subshell writes after the command finishes, such as from an
# EXIT trap that the command sets, is captured rather than leaked.
check "EXIT trap writes to stderr" 0 "report:^from the trap$" \
trap 'echo "from the trap" >&2' EXIT
check "EXIT trap writes to stdout" 0 silent trap 'echo "from the trap"' EXIT
# A builtin runs with `set +u`, as it would in an ordinary shell.
# shellcheck disable=SC2016 # The expansion is for the wrapped command to do.
check "builtin with an unset variable" 0 silent \
eval ': "$cronic_test_unset_variable"'
# When the caller exports SHELLOPTS, a bash script runs with the caller's
# options rather than with `cronic`'s own `set -eu`. BASH_ENV is empty so that
# the script runs no startup file.
# Creates "$work/cronic-with-NAME", which runs `cronic` with SHELLOPTS exported,
# after `set OPTION` if OPTION is given.
make_cronic_wrapper() { # NAME [OPTION]
{
echo "#!/bin/bash"
if [ $# -gt 1 ]; then
echo "set $2"
fi
echo "export SHELLOPTS"
echo "exec \"$CRONIC\" \"\$@\""
} > "$work/cronic-with-$1"
chmod +x "$work/cronic-with-$1"
}
make_cronic_wrapper shellopts
make_cronic_wrapper nounset -u
make_cronic_wrapper errexit -e
make_cronic_wrapper allexport -a
# The wrapper's own trace goes to /dev/null, and `cronic` reads no startup
# file, so that any trace in the output comes from `cronic`.
cat > "$work/cronic-with-xtrace" << EOF
#!/bin/bash
unset BASH_ENV
exec 3>&2 2> /dev/null
set -x
export SHELLOPTS
exec "$CRONIC" "\$@" 2>&3 3>&-
EOF
chmod +x "$work/cronic-with-xtrace"
saved_cronic="$CRONIC"
CRONIC="$work/cronic-with-shellopts"
# shellcheck disable=SC2016 # The expansion is for the wrapped command to do.
check "bash script with an unset variable, SHELLOPTS exported" 0 silent \
env BASH_ENV= bash -c ': "$cronic_test_unset_variable"'
check "bash script with a failing command, SHELLOPTS exported" 0 silent \
env BASH_ENV= bash -c 'false; true'
CRONIC="$work/cronic-with-nounset"
# shellcheck disable=SC2016 # The expansion is for the wrapped command to do.
check "bash script with an unset variable, caller's nounset exported" 127 \
"report:cronic_test_unset_variable" \
env BASH_ENV= bash -c ': "$cronic_test_unset_variable"'
CRONIC="$work/cronic-with-errexit"
check "bash script with a failing command, caller's errexit exported" 1 report \
env BASH_ENV= bash -c 'false; true'
# When the caller exports allexport, `cronic` does not export its own variables
# to the command, but a variable that the command assigns is exported.
CRONIC="$work/cronic-with-allexport"
check "no cronic variables exported, caller's allexport exported" 0 silent \
sh -c '! env | grep -q "^cronic_"'
# shellcheck disable=SC2016 # The expansion is for the wrapped command to do.
check "command's variable exported, caller's allexport exported" 0 silent \
eval 'cronic_test_variable=1; sh -c "[ \"\$cronic_test_variable\" = 1 ]"'
# When the caller exports xtrace, `cronic` traces only the command, not itself.
CRONIC="$work/cronic-with-xtrace"
check "bash script, caller's xtrace exported" 0 silent \
env BASH_ENV= bash -c 'true'
check "bash script that fails, caller's xtrace exported" 1 "report:^+ false$" \
env BASH_ENV= bash -c 'false'
if grep -q cronic_ "$work/output"; then
echo "FAIL: caller's xtrace exported: the report traces cronic itself:"
cat "$work/output"
status=1
else
echo "PASS: caller's xtrace exported: the report does not trace cronic itself"
fi
CRONIC="$saved_cronic"

# The wrapped command sees the caller's exported variables unchanged. For
# each variable that `cronic` assigns, the caller exports that name in
Expand Down
Loading