diff --git a/cronic b/cronic index 70f35d7..7b224bc 100755 --- a/cronic +++ b/cronic @@ -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) @@ -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 @@ -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 @@ -144,65 +144,66 @@ 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." @@ -210,4 +211,4 @@ fi # The temporary files are removed by the EXIT trap. -exit $RESULT +exit $cronic_result diff --git a/tests/cronic-test/cronic-test.sh b/tests/cronic-test/cronic-test.sh index 35cc622..42048f3 100755 --- a/tests/cronic-test/cronic-test.sh +++ b/tests/cronic-test/cronic-test.sh @@ -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"