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
93 changes: 47 additions & 46 deletions cronic
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ cronic_help() {
exit 0
}

# Sets EXPECTED to its argument, or reports a usage error if the argument is
# not a valid exit status.
# Sets cronic_expected to its argument, or reports a usage error if the
# argument is not a valid exit status.
cronic_set_expected() {
EXPECTED=$1
cronic_expected=$1
# No leading zeros, because `[[ ... -eq ... ]]` would read the value as
# octal. At most 255, because no exit status is larger.
if ! [[ $EXPECTED =~ ^(0|[1-9][0-9]{0,2})$ ]] || [ "$EXPECTED" -gt 255 ]; then
echo "$0: --expected-status requires an integer from 0 to 255, got \"$EXPECTED\"" >&2
if ! [[ $cronic_expected =~ ^(0|[1-9][0-9]{0,2})$ ]] || [ "$cronic_expected" -gt 255 ]; then
echo "$0: --expected-status requires an integer from 0 to 255, got \"$cronic_expected\"" >&2
cronic_usage
fi
}

# The options are parsed before the temporary directory is created, so that
# `--help` and a usage error neither create nor depend on it.
EXPECTED=0
PERMITSTDERR=0
DUPLICATE_STDERR=0
cronic_expected=0
cronic_permitstderr=0
cronic_duplicate_stderr=0
while [ $# -gt 0 ]; do
case $1 in
--expected-status)
Expand All @@ -81,11 +81,11 @@ while [ $# -gt 0 ]; do
;;
--permit-stderr)
shift
PERMITSTDERR=1
cronic_permitstderr=1
;;
--duplicate-stderr)
shift
DUPLICATE_STDERR=1
cronic_duplicate_stderr=1
;;
-h | --help)
cronic_help
Expand All @@ -105,26 +105,26 @@ while [ $# -gt 0 ]; do
done
[ $# -gt 0 ] || cronic_usage

DEBUG=false
# DEBUG=true
cronic_debug=false
# cronic_debug=true

# A private directory, rather than predictable `/tmp/cronic.*.$$` names: on a
# shared machine, process ids are guessable and reused, so another user could
# pre-create those paths as symlinks and have the wrapped command's output
# written through them.
CRONIC_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/cronic.XXXXXX") || exit 1
OUT=$CRONIC_TMPDIR/out
ERR=$CRONIC_TMPDIR/err
REDUCED_ERR=$CRONIC_TMPDIR/err.reduced
TRACE=$CRONIC_TMPDIR/trace
cronic_tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/cronic.XXXXXX") || exit 1
cronic_out=$cronic_tmpdir/out
cronic_err=$cronic_tmpdir/err
cronic_reduced_err=$cronic_tmpdir/err.reduced
cronic_trace=$cronic_tmpdir/trace

# Remove the temporary files however this script terminates, including when it
# is interrupted before it reaches the end. When DEBUG is true, the files are
# retained; don't "echo" their location, because that would be appended to
# stderr.
# is interrupted before it reaches the end. When cronic_debug is true, the
# files are retained; don't "echo" their location, because that would be
# appended to stderr.
# shellcheck disable=SC2329 # the function is invoked from the trap handlers below.
cronic_cleanup() {
[ "$DEBUG" = "true" ] || rm -rf "$CRONIC_TMPDIR"
[ "$cronic_debug" = "true" ] || rm -rf "$cronic_tmpdir"
}
trap cronic_cleanup EXIT

Expand All @@ -144,70 +144,71 @@ 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 -- "$@" > "$OUT" 2> "$TRACE"
RESULT=$?
command -- "$@" > "$cronic_out" 2> "$cronic_trace"
cronic_result=$?
set -e

PATTERN="^${PS4:0:1}\\+${PS4:1}"
if grep -q "$PATTERN" "$TRACE"; then
cronic_pattern="^${PS4:0:1}\\+${PS4:1}"
if grep -q "$cronic_pattern" "$cronic_trace"; then
# `|| true` because `grep -v` exits with status 1 when it selects no lines,
# which happens whenever stderr consists only of trace lines. Without it,
# `set -e` would abort the script here, before it produces its report,
# exits with the wrapped command's status, and removes its temporary files.
grep -v "$PATTERN" "$TRACE" > "$ERR" || true
grep -v "$cronic_pattern" "$cronic_trace" > "$cronic_err" || true
else
ERR=$TRACE
cronic_err=$cronic_trace
fi

# Certain Unix programs send output to stderr even when a command is successful!
# For example, git does this, and the POSIX standard requires it for make.
# The full stderr output is shown, but file REDUCED_ERR is checked for emptiness below.
# The full stderr output is shown, but file cronic_reduced_err is checked for
# emptiness below.
# TODO: Expand this.
cat "$ERR" \
cat "$cronic_err" \
| grep -v "^Everything up-to-date$" \
| grep -v '^make\(\[[0-9]\+\]\)\?: \(Entering\|Leaving\) directory '"'"'.*'"'"'$' \
> "$REDUCED_ERR" || true
> "$cronic_reduced_err" || true

if [ $RESULT -ne "$EXPECTED" ] || { [ $PERMITSTDERR -eq 0 ] && [ -s "$REDUCED_ERR" ]; }; then
if [ $cronic_result -ne "$cronic_expected" ] || { [ $cronic_permitstderr -eq 0 ] && [ -s "$cronic_reduced_err" ]; }; then
echo "Cronic detected failure or error output for the command:"
echo "$@"
echo
if [[ $EXPECTED -eq 0 ]]; then
if [[ $RESULT -eq 0 ]]; then
echo "RESULT CODE: $RESULT (success)"
if [[ $cronic_expected -eq 0 ]]; then
if [[ $cronic_result -eq 0 ]]; then
echo "RESULT CODE: $cronic_result (success)"
else
echo "RESULT CODE: $RESULT (failure)"
echo "RESULT CODE: $cronic_result (failure)"
fi
else
echo "RESULT CODE: $RESULT (expected: $EXPECTED)"
echo "RESULT CODE: $cronic_result (expected: $cronic_expected)"
fi
echo
echo "ERROR OUTPUT:"
cat "$ERR"
cat "$cronic_err"
echo
if [ "$DEBUG" != "true" ]; then
if ! cmp --silent "$ERR" "$REDUCED_ERR"; then
if [ "$cronic_debug" != "true" ]; then
if ! cmp --silent "$cronic_err" "$cronic_reduced_err"; then
echo "REDUCED ERROR OUTPUT:"
cat "$REDUCED_ERR"
cat "$cronic_reduced_err"
echo
fi
fi
echo "STANDARD OUTPUT:"
cat "$OUT"
if [ "$TRACE" != "$ERR" ]; then
cat "$cronic_out"
if [ "$cronic_trace" != "$cronic_err" ]; then
echo
echo "TRACE-ERROR OUTPUT:"
cat "$TRACE"
cat "$cronic_trace"
fi
if [ $DUPLICATE_STDERR -eq 1 ]; then
if [ $cronic_duplicate_stderr -eq 1 ]; then
echo
echo "ERROR OUTPUT:"
cat "$ERR"
cat "$cronic_err"
fi
echo
echo "END OF CRONIC OUTPUT."
fi

# The temporary files are removed by the EXIT trap.

exit $RESULT
exit $cronic_result
37 changes: 37 additions & 0 deletions tests/cronic-test/cronic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,41 @@ for name in $function_names; do
done
PATH="$saved_path"

# 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,
# `CRONIC_OUT` and `OUT`). `TMPDIR` is omitted, because `cronic` reads it.
# The lowercase names that `cronic` assigns, such as `cronic_out`, are not
# exported: `cronic` does overwrite them, but environment variables
# conventionally have uppercase names.
variable_names=$(sed -n 's/^ *\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' "$CRONIC" | sort -u)
if [ -z "$variable_names" ]; then
echo "FAIL: found no variables in $CRONIC"
status=1
fi
exported_names=
for name in $variable_names; do
upper=$(echo "$name" | tr '[:lower:]' '[:upper:]')
for exported in "$upper" "${upper#CRONIC_}"; do
[ "$exported" = TMPDIR ] && continue
exported_names="$exported_names $exported"
done
done
cat > "$work/check-variables" << 'EOF'
#!/bin/sh
for name; do
eval "value=\${$name-}"
[ "$value" = "caller's $name" ] || exit 3
done
EOF
chmod +x "$work/check-variables"
for name in $exported_names; do
eval "$name=\"caller's \$name\""
export "${name?}"
done
# shellcheck disable=SC2086 # each name is a separate argument.
check "caller's exported variables" 0 silent "$work/check-variables" $exported_names
# shellcheck disable=SC2086 # each name is a separate argument.
unset $exported_names

exit "$status"
Loading