Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0347a30
Run the command with the caller's shell options, in a subshell
mernst Oct 10, 2026
4882e52
Do not let an exported xtrace or verbose make cronic write to stderr
mernst Oct 10, 2026
b170cb7
Simplify: inline a test command, drop a redundant comment
mernst Oct 10, 2026
3b11fa1
Merge cronic-caller-options; never trace cronic itself; simplify
mernst Oct 10, 2026
d488e05
Keep the redirections out of the subshell's traces; also save and res…
mernst Oct 10, 2026
76ff4f6
Merge remote-tracking branch 'upstream/master' into cronic-caller-opt…
mernst Oct 10, 2026
ad31cec
Explain why the subshell is not an operand of ||, and the limitation …
mernst Oct 10, 2026
f9bc9c2
Restore all of the caller's set -o options; document limitations; fix…
mernst Oct 11, 2026
8b65a0c
Merge branch 'cronic-caller-options' into cronic-no-self-trace
mernst Oct 11, 2026
52191f5
Merge remote-tracking branch 'upstream/master' into cronic-no-self-trace
mernst Oct 11, 2026
20282ca
cronic: do not trace cronic's own `command -- ...` line when the call…
mernst Oct 11, 2026
bd37b2c
Suppress a shellcheck false positive
mernst Oct 11, 2026
41e6f95
cronic: fix the trace suppression under bash 3.2, and avoid an extra …
mernst Oct 11, 2026
004c6ee
cronic: turn the caller's verbose off for cronic and restore it for t…
mernst Oct 11, 2026
1a9ef91
Merge branch 'cronic-verbose-test-wrappers' into cronic-no-command-trace
mernst Oct 11, 2026
8a59d8f
cronic: put the copyright header near the top, and clarify the verbos…
mernst Oct 11, 2026
00f2c49
cronic: shorten the comment about the option-saving block
mernst Oct 11, 2026
b67130f
Address review findings: portable shebangs, keep caller's options on,…
mernst Oct 11, 2026
e5d33a5
Merge branch 'cronic-verbose-test-wrappers' of github.com:mernst/plum…
mernst Oct 11, 2026
dd7cedf
cronic: restore only the options that cronic changes, using $- letters
mernst Oct 11, 2026
9e13f70
cronic: do not handle verbose; document that the caller should not ex…
mernst Oct 11, 2026
1867f64
cronic-test: remove SHELLOPTS with `env -u` rather than the bashism `…
mernst Oct 11, 2026
60f4d81
Merge remote-tracking branch 'origin/cronic-no-self-trace' into croni…
mernst Oct 11, 2026
ccfbe14
Address review findings: document an inherited BASH_XTRACEFD, quote t…
mernst Oct 11, 2026
febd38e
Address review findings: keep bash warnings out of the expected trace…
mernst Oct 11, 2026
cf493e9
Simplify: keep the option-saving code with its comment, shorten comme…
mernst Oct 11, 2026
16946b4
Merge remote-tracking branch 'origin/cronic-no-self-trace' into croni…
mernst Oct 11, 2026
6774063
Merge branch 'master' of https://github.com/plume-lib/plume-scripts i…
mernst Oct 11, 2026
8cce3eb
Separate paragraphs in long comments
mernst Oct 11, 2026
1074846
Remove the trace of the `command` line from the trace file, rather th…
mernst Oct 11, 2026
da43e5c
Remove tests made redundant by the EXIT-trap trace test
mernst Oct 11, 2026
1cb7f3b
Usage info
mernst Oct 11, 2026
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
36 changes: 30 additions & 6 deletions cronic
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@
# execution traces from the error output, so jobs can be run with execution
# tracing to aid forensic debugging. Apart from its options, Cronic simply
# executes its arguments.
#
# Extended by Michael Ernst to add the --expected-status, --permit-stderr, and
# --duplicate-stderr options, which may be given in any order.
# The options and the treatment of execution traces are documented in function
# `cronic_help`; run `cronic --help`.
#
# For usage information, run `cronic --help`.
#
# Limitations, which `cronic --help` documents: every run produces output if
# the caller exports the verbose option or runs `bash -v cronic`, or if the file
# named by BASH_ENV writes to stderr.

# This script runs under the first `bash` in PATH, which on macOS may be
# /bin/bash, bash 3.2. So it uses a feature of a later bash, such as
# BASH_XTRACEFD (bash 4.1), only after checking the version; see the
# BASH_VERSINFO check below.

# Limitations, which `cronic --help` documents: every run produces output if
# the caller exports the verbose option or runs `bash -v cronic`, or if the file
# named by BASH_ENV writes to stderr.

# Save the caller's settings of the options that this script changes, as the
# letters that `$-` uses for them, so that the command runs with them. The
# command inherits every other option, including every `shopt` option,
# unchanged. If this script ever changes another option, add its letter here.
#
# Then turn off xtrace, so that this script does not trace itself to stderr,
# which would make every run produce output, and allexport, so that this script
# does not export its variables to the command. `cronic_caller_flags`, which is
Expand Down Expand Up @@ -76,6 +79,8 @@ cronic_help() {
echo "stderr, from which lines that start with PS4 are removed. That removal"
echo "is skipped if PS4 contains \"\$\", a backquote, a backslash, or a newline,"
echo "because such a PS4 can expand differently on each line."
echo "If the caller's xtrace is on, cronic's own trace of running COMMAND is"
echo "removed from the TRACE OUTPUT; that works only if PS4 contains no newline."
echo "Do not run \"bash -v cronic\", nor export the verbose option (\"set -v\") via"
echo "SHELLOPTS: bash would echo the lines of this script (and, if exported, of a"
echo "bash COMMAND) to stderr, so every run would produce output."
Expand Down Expand Up @@ -192,20 +197,25 @@ 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.
#
# The subshell restores the caller's options that this script changed.
# Otherwise, if the caller exported SHELLOPTS, a bash script that reads an unset
# variable (as `/usr/bin/texi2dvi` does when /bin/sh is bash) would fail. Of
# those options, only nounset is on here, so a single `set` command turns it off
# and turns on the caller's, so that the restoring is not traced. The `:+`
# avoids a bare `-`, which has a special meaning to `set`.
#
# `command` because a function of this script, such as `cronic_usage`, would
# 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 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`,
Expand All @@ -221,6 +231,18 @@ set -e
# Unsetting BASH_XTRACEFD also closes its file descriptor.
unset BASH_XTRACEFD

# With the caller's xtrace on, the first line of the trace file is the trace of
# the `command -- ...` line above, which is this script's, not the command's,
# so it is removed. (The `set` command before it is not traced.) For a builtin
# COMMAND, such as `exit`, that line is the builtin's own trace. If PS4
# contains a newline, then that trace spans several lines, of which only the
# first is removed. A bash before 4.1 writes the trace to stderr, and that line
# is not removed from it.
if [ -n "${cronic_xtrace_fd-}" ] && [[ $cronic_caller_flags == *x* ]]; then
sed 1d "$cronic_xtrace" > "$cronic_xtrace.reduced"
cronic_xtrace=$cronic_xtrace.reduced
fi

# End stderr with a newline, if it is not empty. `grep` ends its output with a
# newline, so without this, the filtered stderr would differ from the
# unfiltered stderr even when `grep` removed no line, and the report would
Expand Down Expand Up @@ -282,11 +304,13 @@ fi
# For example, git does this, and the POSIX standard requires it for make.
# The full stderr output is shown, but file cronic_reduced_err is checked for
# emptiness below.
#
# GNU make names itself by the name it was invoked as, without the directory,
# such as `make`, `gmake`, `gnumake` (on macOS), or `mingw32-make`. It quotes
# the directory as 'dir' from version 4.0, and as `dir' before then; macOS's
# /usr/bin/make is GNU make 3.81.
# TODO: Expand this.
#
# `|| [ $? -eq 1 ]` because `grep -v` exits with status 1 when it selects no
# lines. If `grep` fails (status 2), the unreduced stderr is used.
grep -a -E -v \
Expand Down
38 changes: 22 additions & 16 deletions tests/cronic-test/cronic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# printed, and the exit status would be grep's 1 rather than the wrapped
# command's. (The temporary files would still be removed, by `cronic`'s EXIT
# trap, so the left-behind-files check in `check` does not detect that bug.)
#
# The checks that catch that bug are the "trace-only stderr" checks that use
# `stderr-lines` with a literal PS4, and the "nested trace in stderr" check,
# whose bash script writes its trace lines to stderr whatever the bash version.
Expand Down Expand Up @@ -139,6 +140,7 @@ chmod +x "$work/stderr-lines"
# not error output, whatever PS4 is. An earlier bash, such as macOS's
# /bin/bash, writes them to stderr, where `cronic` cannot recognize them when
# PS4 contains an expansion or an escape, so it reports them as error output.
#
# This tests for the features themselves, rather than for the bash version
# that `cronic` tests for, so that it detects a wrong version test in `cronic`.
# `cronic` and the bash scripts above all run under the first `bash` in PATH,
Expand Down Expand Up @@ -450,20 +452,12 @@ check "exported SHELLOPTS with allexport, command's variable exported" 0 silent
# produces no output at all: `cronic` does not trace itself.
check "exported SHELLOPTS with xtrace, trace-only stderr" 0 silent \
"$default_shellopts:xtrace" "$work/trace-only" 0
# With xtrace exported, the report's trace section is the trace of running the
# command, followed by the command's own trace, with nothing that `cronic` did
# to set up the command's options. Bash writes the trace via BASH_XTRACEFD, so
# it appears under "TRACE OUTPUT".
# Bash quotes a word in a trace line if it contains a space or a shell
# metacharacter, so let bash write the expected trace of the path, which is
# under the caller's TMPDIR. The trace goes to a separate file descriptor,
# and stderr is discarded, so that an unrelated warning from bash, such as for
# an uninstalled locale, is not part of the expected trace.
# shellcheck disable=SC2016 # The expansion is for bash to do.
traced_command=$(bash -c 'BASH_XTRACEFD=3; set -x; : -- "$1" 0' sh "$work/trace-and-stderr" 3>&1 2> /dev/null | sed 's/^+ : /+ command /')
# With xtrace exported, the report's trace section is the command's own trace,
# with nothing that `cronic` did to set up the command's options or to run the
# command, such as its `command -- ...` line. Bash writes the trace via
# BASH_XTRACEFD, so it appears under "TRACE OUTPUT".
cat > "$work/trace-section.goal" << EOF
TRACE OUTPUT:
$traced_command
+ set -x
+ echo 'a real error'
+ exit 0
Expand All @@ -472,6 +466,19 @@ EOF
check_trace_section "exported SHELLOPTS with xtrace, trace output" \
"$work/trace-section.goal" \
"$CRONIC" "$default_shellopts:xtrace" "$work/trace-and-stderr" 0
# With xtrace exported, the trace lines of a builtin are nested 1 deeper than
# the builtin, whose own trace is the `command -- ...` line, which is not
# reported. The trace of an EXIT trap that the builtin sets is reported, even
# if the builtin returns normally rather than calling `exit`.
cat > "$work/exit-trap-trace-section.goal" << 'EOF'
TRACE OUTPUT:
++ trap 'echo trapped >&2' EXIT
++ echo trapped

EOF
check_trace_section "exported SHELLOPTS with xtrace, EXIT trap of a builtin" \
"$work/exit-trap-trace-section.goal" \
"$CRONIC" "$default_shellopts:xtrace" eval "trap 'echo trapped >&2' EXIT"

CRONIC="$real_cronic"

Expand Down Expand Up @@ -512,12 +519,11 @@ check "caller's exported variables" 0 silent "$work/check-variables" $exported_n
# shellcheck disable=SC2086 # each name is a separate argument.
unset $exported_names

# `bash -x cronic` does not trace `cronic` itself, but does trace running the
# command. The command does not inherit xtrace, because SHELLOPTS is not
# exported, so its own `set -x` is not traced.
# `bash -x cronic` traces neither `cronic` itself nor its `command -- ...` line.
# The command does not inherit xtrace, because SHELLOPTS is not exported, so
# its own `set -x` is not traced.
cat > "$work/bash-x-trace-section.goal" << EOF
TRACE OUTPUT:
$traced_command
+ echo 'a real error'
+ exit 0

Expand Down
Loading