#!/usr/bin/env bash
#
# mail-wrapper -- Report results of a command by email
-# Copyright (c)2017 Alexander Barton (alex@barton.de)
+# Copyright (c)2017,2018,2023 Alexander Barton (alex@barton.de)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
[ -r "$ax_common" ] && . "$ax_common"
done
if [ -z "$ax_common_sourced" ]; then
- echo "Error ($(basename "$0")): \"ax-common.sh\" not found, aborting!"
+ echo "Error ($NAME): \"ax-common.sh\" not found, aborting!" >&2
echo "Please install 'ax-unix', \"Alex' UNIX Tools & Scripts\", and try again."
exit 99
fi
echo " $NAME {parameters} [<command> [<arg> [<…>]]]"
echo
echo " -C Use the \"C\" locale, no localized (error) messages."
+ echo " --dontfail|-n Don't return the error code of the command called."
echo " --errors|-e Generate email on errors only."
echo " --from|-f Email address of the sender of the email."
+ echo " --stderr-is-warning|-W Exit code indicates error; stderr is only warning."
echo " --subject|-s <subject> Subject for the email."
+ echo " --time|-t Include timing information."
echo " --to|-t <address> Email address to send the email to."
echo
- echo "When no <command> is given, $NAME reads from standard input."
+ echo "When no <command> is given, $NAME reads from standard input. The default for"
+ echo "<from> and <to> for the current user is \"$to\"."
echo
} >&2
exit "${1:-0}"
usage 2
}
+clean_up() {
+ if [[ -z "$proc_fd_works" ]]; then
+ ax_debug "Cleaning temporary files ..."
+ [[ -n "$buffer_file" ]] && rm -f "$buffer_file"
+ [[ -n "$error_file" ]] && rm -f "$error_file"
+ fi
+}
+
+# Convert UNIX time stamp to date text.
+time_t_to_date() {
+ t="$1"
+ shift
+ if ! date -d @"$t" "$@" 2>/dev/null; then
+ # "date -d @<time_t>" (GNU variant) failed!
+ date -r "$t" "$@"
+ fi
+}
+
+time_t_to_duration() {
+ s="$1"
+ if [[ "$s" -ge 60 ]]; then
+ m=$((s / 60))
+ s=$((s % 60))
+ if [[ "$m" -ge 60 ]]; then
+ h=$((m / 60))
+ m=$((m % 60))
+ if [[ "$h" -ge 24 ]]; then
+ d=$((h / 24))
+ h=$((h % 24))
+ echo "${d}d${2}${h}h${2}${m}m${2}${s}s"
+ else
+ echo "${h}h${2}${m}m${2}${s}s"
+ fi
+ else
+ echo "${m}m${2}${s}s"
+ fi
+ else
+ echo "${s}s"
+ fi
+}
+
+case "$(uname)" in
+ "Darwin")
+ unset proc_fd_works
+ ;;
+ *)
+ proc_fd_works=1
+esac
+
# Initialize internal state.
-unset is_error
+buffer_file=""
+error_file=""
+error_level=0
host=$(hostname -f 2>/dev/null || hostname)
+trap clean_up EXIT
+
# Some defaults (can be adjusted by command line parameters).
unset do_errors_only
+unset dont_fail
+unset stderr_is_warning
unset subject
+unset time
from="${LOGNAME:-root} <${LOGNAME:-root}@$host>"
to="$from"
"--debug"|"-D")
export DEBUG=1
;;
+ "--dontfail"|"-n")
+ dont_fail=1
+ ;;
"--errors"|"-e")
do_errors_only=1
;;
[[ $# -gt 0 ]] || syntax_error
subject="$1"
;;
+ "--stderr-is-warning"|"-W")
+ stderr_is_warning=1
+ ;;
+ "--time"|"-t")
+ time=1
+ ;;
+ "--suppress-empty")
+ # Ignore this switch for compatibility with an other
+ # "mail-wrapper" script. This is the default anyway!
+ ;;
"--to"|"-t")
shift
[[ $# -gt 0 ]] || syntax_error
done
# Initialize the "buffer file" on file handle #3. This file will store all
-# output, stdout and stderr combined. The file is immediately unliked so that
+# output, stdout and stderr combined. The file is immediately unlinked so that
# we can't leak stale files. Afterwards this script accesses the "buffer file"
# by its file descriptor only.
buffer_file=$(mktemp) \
ax_debug "buffer_file=\"$buffer_file\""
exec 3>"$buffer_file" \
|| ax_abort -l "Failed to redirect FD #3 to buffer file!"
-rm "$buffer_file" \
- || ax_error -l "Failed to delete buffer file: \"$buffer_file\"!"
-buffer_file="/dev/fd/3"
+if [[ -n "$proc_fd_works" ]]; then
+ rm "$buffer_file" \
+ || ax_error -l "Failed to delete buffer file: \"$buffer_file\"!"
+ buffer_file="/dev/fd/3"
+fi
if [[ $# -gt 0 ]]; then
# Execute command and save output in buffer file.
ax_debug "error_file=\"$error_file\""
exec 4>"$error_file" \
|| ax_abort -l "Failed to redirect FD #4 to error file!"
- rm "$error_file" \
- || ax_error -l "Failed to delete error buffer file: \"$error_file\"!"
- error_file="/dev/fd/4"
+ if [[ -n "$proc_fd_works" ]]; then
+ rm "$error_file" \
+ || ax_error -l "Failed to delete error buffer file: \"$error_file\"!"
+ error_file="/dev/fd/4"
+ fi
job=$(basename "$1")
ax_debug "Running command \"$*\" ..."
+ start_t=$EPOCHSECONDS
exit_code=$(
"$@" 2>&1 1>&3 | tee "$error_file" >&3
echo "${PIPESTATUS[0]}"
)
+ end_t=$EPOCHSECONDS
else
# Read from stdin and save it to the buffer file.
error_file="/dev/null"
job="Job"
ax_debug "Reading from stdin ..."
+ start_t=$EPOCHSECONDS
while read -r line; do
echo "$line" >&3 \
|| ax_abort -l "Failed to write to buffer file!"
done
+ end_t=$EPOCHSECONDS
exit_code=0
fi
ax_debug "exit_code=$exit_code"
+declare -i count_all count_err
count_all=$(wc -l <"$buffer_file" || ax_abort -l "Failed to count buffer file!")
count_err=$(wc -l <"$error_file" || ax_abort -l "Failed to count error file!")
# Error or no error -- that's the question! An error is assumed when either the
# exit code of the command was non-zero or there was output to stderr.
-[[ $count_err -gt 0 || $exit_code -ne 0 ]] && is_error=1
+# But when stderr_is_warning is set, messages on stderr result on a warning only!
+if [[ $exit_code -ne 0 ]]; then
+ error_level=2
+elif [[ $count_err -gt 0 ]]; then
+ [[ -n $stderr_is_warning ]] && error_level=1 || error_level=2
+else
+ error_level=0
+fi
# Construct email subject ...
[[ -z "$subject" ]] && subject="$host: $job report"
-[[ -n "$is_error" ]] && subject="$subject - ERROR!" || subject="$subject - success"
+if [[ "$error_level" -eq 0 ]]; then
+ subject="$subject - success"
+elif [[ "$error_level" -eq 1 ]]; then
+ subject="$subject - WARNING!"
+else
+ subject="$subject - ERROR!"
+fi
ax_debug "from=\"$from\""
ax_debug "to=\"$to\""
ax_debug "count_all=$count_all"
ax_debug "count_err=$count_err"
-ax_debug "is_error=$is_error"
+ax_debug "error_level=$error_level"
# No errors detected (exit code & stderr), and email should be sent on errors
# only: so exit early!
-[[ -z "$is_error" && -n "$do_errors_only" ]] && exit $exit_code
+[[ "$error_level" -lt 2 && -n "$do_errors_only" ]] && exit $exit_code
# No error detected and no output at all: skip email, exit early:
-[[ -z "$is_error" && $count_all -eq 0 ]] && exit $exit_code
+[[ "$error_level" -eq 0 && $count_all -eq 0 ]] && exit $exit_code
# Build the report mail.
# Make sure to ignore all mail(1) configuration files, system wide /etc/mailrc
# (by using the "-n" option) as well as ~/.mailrc (by setting the MAILRC
-# environment varialbe).
+# environment variable).
export MAILRC=/dev/null
(
echo "$job report:"
echo " - Host: $host"
echo " - User: $(id -un)"
echo " - Exit code: $exit_code"
+ [[ -n "$time" ]] && printf " - Duration: %s\n" "$(time_t_to_duration $((end_t - start_t)) ' ')"
echo
if [[ $# -gt 0 ]]; then
# A command name is known (not stdin), show it!
echo "$@"
echo
fi
+ [[ -n "$time" ]] && printf "%s - %s:\n\n" "$(time_t_to_date "$start_t")" "$(time_t_to_date "$end_t")"
if [[ $count_err -gt 0 ]]; then
# Prefix mail with all error messages.
echo "Error summary:"
) | mail -n -a "From: $from" -s "$subject" "$to" \
|| ax_abort -l "Failed to send email to \"$to\"!"
-exit $exit_code
+[[ -n "$dont_fail" ]] && exit 0 || exit $exit_code