]> arthur.barton.de Git - ax-unix.git/blobdiff - mail/wrapper/mail-wrapper
mail-wrapper: Implement new --time [-t] option to show the duration
[ax-unix.git] / mail / wrapper / mail-wrapper
index 7c5a19b98c2b5a358e1c817763f5a0402ad6f4ac..fcda76e2ffa89a37664984e4deda79e7e5994fe8 100755 (executable)
@@ -1,7 +1,7 @@
 #!/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
@@ -20,7 +20,7 @@ for dir in "$HOME/lib" "$HOME/.ax" /usr/local /opt/ax /usr; do
        [ -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
@@ -34,12 +34,16 @@ usage() {
                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}"
@@ -50,13 +54,69 @@ syntax_error() {
        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"
 
@@ -70,6 +130,9 @@ while [[ $# -gt 0 ]]; do
                "--debug"|"-D")
                        export DEBUG=1
                        ;;
+               "--dontfail"|"-n")
+                       dont_fail=1
+                       ;;
                "--errors"|"-e")
                        do_errors_only=1
                        ;;
@@ -86,6 +149,16 @@ while [[ $# -gt 0 ]]; do
                        [[ $# -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
@@ -103,7 +176,7 @@ while [[ $# -gt 0 ]]; do
 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) \
@@ -111,9 +184,11 @@ 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.
@@ -123,42 +198,62 @@ if [[ $# -gt 0 ]]; then
        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\""
@@ -174,19 +269,19 @@ fi
 
 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:"
@@ -194,6 +289,7 @@ export MAILRC=/dev/null
        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!
@@ -201,6 +297,7 @@ export MAILRC=/dev/null
                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:"
@@ -218,4 +315,4 @@ export MAILRC=/dev/null
 ) | 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