Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5c9a5ea
Do not pass `cronic`'s `-u` option to the wrapped command
mernst Oct 8, 2026
8cc2a78
Run the command under `cronic` with the caller's shell options; repor…
mernst Oct 8, 2026
f9bd4a1
Restore only the caller's `-e` and `-u` around the wrapped command
mernst Oct 8, 2026
ec112d5
Address code review: validate --expected-status range, accept options…
mernst Oct 8, 2026
9bbaff5
Address code review: honor the caller's `-e` in shell functions, do n…
mernst Oct 8, 2026
a9235f1
Parse `cronic`'s options in any order, and report usage errors
mernst Oct 8, 2026
a630dec
Move option parsing to its own pull request; restore all of the calle…
mernst Oct 8, 2026
97d5209
Address code review: do not hide a command named `cleanup`, and resto…
mernst Oct 8, 2026
df5755e
Document `cronic`'s options only in its help message
mernst Oct 8, 2026
e28d20e
Address code review: hide cronic's function, fix option handling, PS4…
mernst Oct 8, 2026
14f3102
Address code review: match all of PS4, validate --expected-status, ch…
mernst Oct 8, 2026
0e697d9
Merge master's option parsing into cronic-nounset
mernst Oct 8, 2026
2986987
Run `cronic`'s command with `command`, and explain a missing option v…
mernst Oct 8, 2026
1eaba4c
Merge branch 'cronic-options' into cronic-nounset
mernst Oct 9, 2026
1ddbeec
Address code review: run under bash 3.2, simplify PS4 matching, remov…
mernst Oct 9, 2026
632fc66
Merge branch 'master' into cronic-nounset
mernst Oct 9, 2026
7c34a33
Filter trace lines only when PS4 is literal, so that no real error is…
mernst Oct 9, 2026
ea5b9e9
Address code review: keep the caller's cronic_ functions, simplify op…
mernst Oct 9, 2026
676facf
Describe what an unguarded grep would do, not what it did
mernst Oct 9, 2026
5e73d51
Check for predictable temporary file names while `cronic` runs the co…
mernst Oct 9, 2026
42adb1b
Prefix `cronic`'s variables with `CRONIC_`, so that it does not chang…
mernst Oct 9, 2026
dfeec8b
Filter trace lines only when PS4 is literal, and escape its metachara…
mernst Oct 9, 2026
8e9b449
Remove the PS4 and temporary-file-test changes, which are in #133 and…
mernst Oct 9, 2026
73b1f46
Locate cronic's temporary files by their contents, not by the process id
mernst Oct 9, 2026
931c4bf
Use lowercase names for `cronic`'s variables, and test every variable…
mernst Oct 9, 2026
7b79de0
Merge remote-tracking branch 'origin/cronic-temp-file-test' into cron…
mernst Oct 9, 2026
7028f1e
Merge remote-tracking branch 'origin/cronic-prefix' into cronic-nounset
mernst Oct 9, 2026
0943578
Merge remote-tracking branch 'origin/cronic-ps4' into cronic-nounset
mernst Oct 9, 2026
391b400
Brevity
mernst Oct 9, 2026
34c5e92
Merge remote-tracking branch 'upstream-master' into cronic-nounset
mernst Oct 9, 2026
e06d20c
Merge remote-tracking branch 'origin/cronic-prefix' into cronic-nounset
mernst Oct 9, 2026
0c3b540
Clarify what the exported-variables test of `cronic` checks
mernst Oct 9, 2026
82fe537
Merge remote-tracking branch 'origin/cronic-prefix' into cronic-nounset
mernst Oct 9, 2026
0f1fd83
Merge remote-tracking branch 'upstream-master' into cronic-nounset
mernst Oct 9, 2026
f62af46
Merge remote-tracking branch 'upstream/master' into cronic-ps4
mernst Oct 9, 2026
63cea8c
Separate bash execution traces from error output via BASH_XTRACEFD
mernst Oct 9, 2026
52ff12b
Merge remote-tracking branch 'origin/cronic-ps4' into cronic-nounset
mernst Oct 9, 2026
eeecb45
Merge remote-tracking branch 'upstream-master' into cronic-nounset
mernst Oct 9, 2026
7476e3b
Restore only the caller's options; reserve cronic_ names; call builti…
mernst Oct 9, 2026
d5e1141
Unset the caller's cronic_ functions up front; read names whole; drop…
mernst Oct 9, 2026
23c71c5
Simplify option restoring; turn off verbose first; avoid process subs…
mernst Oct 9, 2026
a14ba5a
Merge branch 'master' into cronic-nounset
mernst Oct 10, 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
111 changes: 103 additions & 8 deletions cronic
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/bin/bash
{
cronic_caller_flags=${-//[^euvx]/}
if compgen -e -X '!SHELLOPTS' SHELLOPTS > /dev/null; then set +vx; fi
} 2> /dev/null

# Cronic v2 - cron job report wrapper
# Copyright 2007 Chuck Houpt
Expand All @@ -14,6 +18,56 @@
# --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`.
# COMMAND runs in a subshell, with the caller's shell options rather than this
# script's. Names that start with `cronic_` are reserved for this script:
# COMMAND does not see a variable or function with such a name, even one that
# the caller exported.
# This script assumes that the caller has not exported a function with the name
# of a builtin or program that this script uses, such as `set` or `cat`; such a
# function would run in place of the builtin or program.
# This script assumes that bash has the `compgen` builtin, which is absent only
# if bash was built with `--disable-progcomp`. Without it, an exported
# SHELLOPTS could make this script trace itself to stderr, and the command could
# see a `cronic_` function that the caller exported.
# 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 first command sets cronic_caller_flags to the caller's shell options, as
# the letters that `$-` uses for them, before this script changes them for
# itself. This script changes only 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. `$-` reports the options without the cost of a command
# substitution, such as `$(set +o)`, in which bash would also report errexit as
# off.
# If the caller exported SHELLOPTS, then the first command also stops this
# script from tracing itself or echoing its input: that output would go to
# this script's stderr, and cron would send email for every run. An exported
# `verbose` nonetheless echoes every line that bash reads before it reaches the
# `set +vx`, which is why that command comes first, before any comment. Bash
# cannot tell whether xtrace came from SHELLOPTS or from the command line, so
# `bash -x cronic` traces this script only if SHELLOPTS is not exported. The
# filter makes `compgen` match SHELLOPTS exactly, not every exported variable
# whose name starts with SHELLOPTS. The redirection discards the trace of the
# first command.

# Unset every function whose name starts with `cronic_`, all of which the caller
# exported, because this script has not yet defined any. Then the only such
# functions are this script's, which the subshell that runs the command unsets
# by name. Testing the status of `compgen` first avoids the cost of a command
# substitution when there is no such function. Each name is read whole, rather
# than split from an unquoted expansion, so that a name that contains a glob
# character is not expanded to file names. The names are read from a
# here-string rather than from a process substitution, which bash before 5.1
# rejects in posix mode, as an exported SHELLOPTS can set. The redirection
# discards the trace of these commands.
{
if compgen -A function cronic_ > /dev/null; then
while read -r cronic_name; do
unset -f "$cronic_name"
done <<< "$(compgen -A function cronic_)"
fi
} 2> /dev/null

# Exit if a command exits with a non-zero status, and treat unset variables
# as an error.
Expand Down Expand Up @@ -44,6 +98,8 @@ cronic_help() {
echo " is less likely to be overlooked"
echo " -- end the options, so that COMMAND may start with \"-\""
echo " -h, --help print this message"
echo "Names that start with \"cronic_\" are reserved: COMMAND does not see a"
echo "variable or function with such a name, even one that the caller exported."
echo "Execution traces (from \"set -x\") are not treated as error output."
echo "Bash 4.1 or later writes them to a separate file, via BASH_XTRACEFD; they"
echo "appear in the report under \"TRACE OUTPUT\". Other shells write them to"
Expand Down Expand Up @@ -152,22 +208,61 @@ trap 'cronic_cleanup; trap - TERM EXIT; kill -s TERM $$' TERM
# error output exactly, whatever PS4 is. The descriptor is allocated by bash
# (as 10 or higher), so that it does not collide with a descriptor, such as 3,
# that the wrapped command uses for its own purposes. Bash before 4.1 supports
# neither BASH_XTRACEFD nor descriptor allocation.
# neither BASH_XTRACEFD nor descriptor allocation. Only the subshell that runs
# the command sets BASH_XTRACEFD, so that this script's own trace, under
# `bash -x cronic`, goes to this script's stderr rather than into the report.
cronic_xtrace_fd=
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1))); then
exec {cronic_xtrace_fd}> "$cronic_xtrace"
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 via `exec`, so that it can be a
# shell builtin or an exported shell function; and 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 is not the left operand of `||`, because
# bash would then ignore `set -e` throughout the subshell, including in a
# shell function that it runs. The redirections are on the subshell, and the
# subshell sets BASH_XTRACEFD before it restores the caller's xtrace, so that
# when `-x` is set, the trace of running the command goes into the command's
# trace output rather than to this script's stderr.
# The subshell unsets every variable whose name starts with `cronic_` and every
# function that this script defined, so that a builtin or shell function that
# is the command does not see this script's variables and functions, and so
# that a function does not take precedence over a program of the same name.
# This script's functions are listed, rather than found by
# `compgen -A function cronic_`, to avoid the cost of a command substitution.
# The list must name every function that this script defines before this
# point; the test suite fails if the command can see one that it omits.
# Then the subshell restores the caller's options. The subshell first turns
# off this script's xtrace and verbose, so that the restoring is not traced or
# echoed; any error in the restoring goes to the command's error output. The
# subshell then needs to turn off only this script's nounset (errexit is off
# already), in the same command that turns on the caller's options, so that
# restoring verbose and xtrace neither echoes nor traces the restoring of the
# others. The `:+` avoids a bare `set -`, which would turn off verbose and
# xtrace.
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 -- "$@" > "$cronic_out" 2> "$cronic_trace"
(
{ set +vx; } 2> /dev/null
if [ -n "$cronic_xtrace_fd" ]; then
export BASH_XTRACEFD=$cronic_xtrace_fd
fi
unset -f cronic_usage_line cronic_usage cronic_help \
cronic_set_expected cronic_cleanup
eval "unset -v \"\${!cronic_@}\"
set +u${cronic_caller_flags:+ -$cronic_caller_flags}"
"$@"
) > "$cronic_out" 2> "$cronic_trace"
cronic_result=$?
set -e

# Unsetting BASH_XTRACEFD also closes its file descriptor.
unset BASH_XTRACEFD
if [ -n "$cronic_xtrace_fd" ]; then
exec {cronic_xtrace_fd}>&-
fi

# Prints a basic regular expression that matches its argument literally.
# The argument must not contain a backslash.
Expand Down
Loading
Loading