Skip to content
Closed
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
45 changes: 41 additions & 4 deletions cronic
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,27 @@
# 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.

# Exit if a command exits with a non-zero status, and treat unset variables
# as an error.
set -eu
# Save the caller's shell options, as the letters that `$-` uses for them,
# before this script changes them for itself. This script changes only
# allexport, errexit, nounset, verbose, and xtrace, so the command inherits the
# caller's other `set -o` options, and its `shopt` options, without any
# restoring; an option that this script changes must be added to the pattern.
# `$(set +o)` would save every `set -o` option, but bash turns off verbose in a
# command substitution, so `set +o` there would report verbose as off.
#
# Then, exit if a command exits with a non-zero status, and treat unset
# variables as an error. Do not export this script's variables to the command,
# even if the caller's exported SHELLOPTS contains allexport. Do not trace this
# script's own commands, even if the caller's exported SHELLOPTS contains
# verbose or xtrace, so that the report's "TRACE OUTPUT" is the command's.
# The traces of these two commands are discarded, so that this script produces
# no output when the command succeeds. (The lines that verbose prints before
# this point cannot be discarded.)
{
cronic_caller_flags=${-//[^aeuvx]/}
set -eu +a +vx
} 2> /dev/null
export -n cronic_caller_flags

# Prints the usage line.
cronic_usage_line() {
Expand Down Expand Up @@ -160,12 +178,31 @@ if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1)));
export BASH_XTRACEFD=$cronic_xtrace_fd
fi

# Run the command with the caller's `set -o` shell options rather than this
# script's. If the caller exported SHELLOPTS, then the command would otherwise
# inherit this script's options, and a bash script that reads an unset variable
# (as `/usr/bin/texi2dvi` does when /bin/sh is bash) would fail.
# 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 subshell 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` line is traced. The `:+`
# avoids a bare `-`, which has a special meaning to `set`. The redirections are
# on the command rather than on the subshell, so that the trace of the `command`
# line does not go to the command's standard error; under bash 4.1 or later,
# it goes to the trace file, and under an older bash, to this script's
# standard error.
# The subshell is not the operand of `||` or the like, because then bash would
# ignore `set -e` in it and would remove "errexit" from the SHELLOPTS that the
# command inherits.
# Limitation: if the command is a builtin that runs code in the subshell, such
# as `source` or `eval`, that code 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.
set +e
(
set +u${cronic_caller_flags:+ -$cronic_caller_flags}
command -- "$@" > "$cronic_out" 2> "$cronic_trace"
)
cronic_result=$?
Expand Down
63 changes: 63 additions & 0 deletions tests/cronic-test/cronic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@

set -eu

# Do not let the user's environment file run in `cronic` or in the bash commands
# below. It could write to stderr, especially when an exported SHELLOPTS turns
# on `-u` for it.
unset BASH_ENV
# Do not pass this script's own options, such as `-e` and `-u`, to `cronic` and
# to the commands below. If SHELLOPTS is exported, then run this script again
# without it, because bash makes SHELLOPTS read-only, so it cannot be unset.
# `printenv` matches the name exactly, unlike a search of the output of `env`,
# which can match a line within the value of another variable.
if printenv SHELLOPTS > /dev/null; then
exec env -u SHELLOPTS sh "$0" "$@"
fi

SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
CRONIC="$(CDPATH='' cd -- "${SCRIPT_DIR}/../.." && pwd -P)/cronic"

Expand Down Expand Up @@ -88,6 +101,24 @@ printf '%s\n' "$@" 1>&2
EOF
chmod +x "$work/stderr-lines"

# A bash command that reads an unset variable.
cat > "$work/read-unset" << 'EOF'
#!/bin/bash
unset CRONIC_TEST_UNSET_VARIABLE
echo "value: ${CRONIC_TEST_UNSET_VARIABLE}" > /dev/null
exit "$1"
EOF
chmod +x "$work/read-unset"

# Runs `cronic` with SHELLOPTS set to its first argument and exported.
cat > "$work/cronic-with-shellopts" << 'EOF2'
#!/bin/bash
shellopts="$1"
shift
exec env SHELLOPTS="$shellopts" "$REAL_CRONIC" "$@"
EOF2
chmod +x "$work/cronic-with-shellopts"

# temp_files: prints `cronic`'s temporary files, in a canonical order.
temp_files() {
find "$TMPDIR" -mindepth 1 -maxdepth 1 2> /dev/null | sort
Expand Down Expand Up @@ -269,6 +300,38 @@ check "builtin command" 0 silent :
check "exit builtin" 3 report exit 3
check "exec builtin" 3 report exec "$work/trace-only" 3

# When SHELLOPTS is exported, `cronic`'s own `-e` and `-u` options do not reach
# the wrapped command, but the caller's options do.
REAL_CRONIC="$CRONIC"
export REAL_CRONIC
CRONIC="$work/cronic-with-shellopts"
default_shellopts=braceexpand:hashall:interactive-comments
check "exported SHELLOPTS, command reads an unset variable" 0 silent \
"$default_shellopts" "$work/read-unset" 0
check "exported SHELLOPTS, command ignores a failure" 0 silent \
"$default_shellopts" bash -c 'false; :'
check "exported SHELLOPTS with nounset, command reads an unset variable" 1 \
"report:unbound variable" \
"$default_shellopts:nounset" "$work/read-unset" 0
check "exported SHELLOPTS with errexit, command ignores a failure" 1 report \
"$default_shellopts:errexit" bash -c 'false; :'
# shellcheck disable=SC2016 # The expansion is for the command to do.
check "exported SHELLOPTS with allexport, command's environment" 0 silent \
"$default_shellopts:allexport" bash -c \
'case $SHELLOPTS in *allexport*) ;; *) exit 2 ;; esac
! printenv cronic_caller_flags && ! printenv cronic_out'
check "exported SHELLOPTS with xtrace, trace-only stderr" 0 silent \
"$default_shellopts:xtrace" "$work/read-unset" 0
check "exported SHELLOPTS with xtrace, trace in the report" 3 \
"report:^+ exit 3$" "$default_shellopts:xtrace" bash -c 'exit 3'
# The trace in the report is the command's, not `cronic`'s own.
if grep -q -e '^+ set ' -e '^+ cronic_' "$work/output"; then
echo "FAIL: exported SHELLOPTS with xtrace, cronic's own commands are traced:"
cat "$work/output"
status=1
fi
CRONIC="$REAL_CRONIC"

# The wrapped command sees the caller's exported variables unchanged. For
# each variable that `cronic` assigns, the caller exports that name in
# uppercase, both with and without any "cronic_" prefix (for example,
Expand Down
Loading