]> arthur.barton.de Git - ax-unix.git/blob - mail/wrapper/mail-wrapper
mail-wrapper: Implement new --time [-t] option to show the duration
[ax-unix.git] / mail / wrapper / mail-wrapper
1 #!/usr/bin/env bash
2 #
3 # mail-wrapper -- Report results of a command by email
4 # Copyright (c)2017,2018,2023 Alexander Barton (alex@barton.de)
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11
12 NAME=$(basename "$0")
13
14 # Include "ax-common.sh":
15 ax_common_sourced=
16 for dir in "$HOME/lib" "$HOME/.ax" /usr/local /opt/ax /usr; do
17         [ -z "$ax_common_sourced" ] || break
18         ax_common="${dir}/lib/ax/ax-common.sh"
19         # shellcheck source=/usr/local/lib/ax/ax-common.sh
20         [ -r "$ax_common" ] && . "$ax_common"
21 done
22 if [ -z "$ax_common_sourced" ]; then
23         echo "Error ($NAME): \"ax-common.sh\" not found, aborting!" >&2
24         echo "Please install 'ax-unix', \"Alex' UNIX Tools & Scripts\", and try again."
25         exit 99
26 fi
27 unset dir ax_common ax_common_sourced
28
29 usage() {
30         {
31                 echo
32                 echo "Usage:"
33                 echo "  $NAME [--help|--usage]"
34                 echo "  $NAME {parameters} [<command> [<arg> [<…>]]]"
35                 echo
36                 echo "  -C                      Use the \"C\" locale, no localized (error) messages."
37                 echo "  --dontfail|-n           Don't return the error code of the command called."
38                 echo "  --errors|-e             Generate email on errors only."
39                 echo "  --from|-f               Email address of the sender of the email."
40                 echo "  --stderr-is-warning|-W  Exit code indicates error; stderr is only warning."
41                 echo "  --subject|-s <subject>  Subject for the email."
42                 echo "  --time|-t               Include timing information."
43                 echo "  --to|-t <address>       Email address to send the email to."
44                 echo
45                 echo "When no <command> is given, $NAME reads from standard input. The default for"
46                 echo "<from> and <to> for the current user is \"$to\"."
47                 echo
48         } >&2
49         exit "${1:-0}"
50 }
51
52 syntax_error() {
53         ax_error -l "Syntax error!"
54         usage 2
55 }
56
57 clean_up() {
58         if [[ -z "$proc_fd_works" ]]; then
59                 ax_debug "Cleaning temporary files ..."
60                 [[ -n "$buffer_file" ]] && rm -f "$buffer_file"
61                 [[ -n "$error_file" ]] && rm -f "$error_file"
62         fi
63 }
64
65 # Convert UNIX time stamp to date text.
66 time_t_to_date() {
67         t="$1"
68         shift
69         if ! date -d @"$t" "$@" 2>/dev/null; then
70                 # "date -d @<time_t>" (GNU variant) failed!
71                 date -r "$t" "$@"
72         fi
73 }
74
75 time_t_to_duration() {
76         s="$1"
77         if [[ "$s" -ge 60 ]]; then
78                 m=$((s / 60))
79                 s=$((s % 60))
80                 if [[ "$m" -ge 60 ]]; then
81                         h=$((m / 60))
82                         m=$((m % 60))
83                         if [[ "$h" -ge 24 ]]; then
84                                 d=$((h / 24))
85                                 h=$((h % 24))
86                                 echo "${d}d${2}${h}h${2}${m}m${2}${s}s"
87                         else
88                                 echo "${h}h${2}${m}m${2}${s}s"
89                         fi
90                 else
91                         echo "${m}m${2}${s}s"
92                 fi
93         else
94                 echo "${s}s"
95         fi
96 }
97
98 case "$(uname)" in
99         "Darwin")
100                 unset proc_fd_works
101                 ;;
102         *)
103                 proc_fd_works=1
104 esac
105
106 # Initialize internal state.
107 buffer_file=""
108 error_file=""
109 error_level=0
110 host=$(hostname -f 2>/dev/null || hostname)
111
112 trap clean_up EXIT
113
114 # Some defaults (can be adjusted by command line parameters).
115 unset do_errors_only
116 unset dont_fail
117 unset stderr_is_warning
118 unset subject
119 unset time
120 from="${LOGNAME:-root} <${LOGNAME:-root}@$host>"
121 to="$from"
122
123 # Parse the command line ...
124 while [[ $# -gt 0 ]]; do
125         case "$1" in
126                 "-C")
127                         unset LANG
128                         export LC_ALL="C"
129                         ;;
130                 "--debug"|"-D")
131                         export DEBUG=1
132                         ;;
133                 "--dontfail"|"-n")
134                         dont_fail=1
135                         ;;
136                 "--errors"|"-e")
137                         do_errors_only=1
138                         ;;
139                 "--from"|"-f")
140                         shift
141                         [[ $# -gt 0 ]] || syntax_error
142                         from="$1"
143                         ;;
144                 "--help"|"--usage")
145                         usage
146                         ;;
147                 "--subject"|"-s")
148                         shift
149                         [[ $# -gt 0 ]] || syntax_error
150                         subject="$1"
151                         ;;
152                 "--stderr-is-warning"|"-W")
153                         stderr_is_warning=1
154                         ;;
155                 "--time"|"-t")
156                         time=1
157                         ;;
158                 "--suppress-empty")
159                         # Ignore this switch for compatibility with an other
160                         # "mail-wrapper" script. This is the default anyway!
161                         ;;
162                 "--to"|"-t")
163                         shift
164                         [[ $# -gt 0 ]] || syntax_error
165                         to="$1"
166                         ;;
167                 "-"*)
168                         syntax_error
169                         ;;
170                 *)
171                         # Command to execute follows in command line.
172                         break
173                         ;;
174         esac
175         shift
176 done
177
178 # Initialize the "buffer file" on file handle #3. This file will store all
179 # output, stdout and stderr combined. The file is immediately unlinked so that
180 # we can't leak stale files. Afterwards this script accesses the "buffer file"
181 # by its file descriptor only.
182 buffer_file=$(mktemp) \
183         || ax_abort -l "Failed to create buffer file: \"$buffer_file\"!"
184 ax_debug "buffer_file=\"$buffer_file\""
185 exec 3>"$buffer_file" \
186         || ax_abort -l "Failed to redirect FD #3 to buffer file!"
187 if [[ -n "$proc_fd_works" ]]; then
188         rm "$buffer_file" \
189                 || ax_error -l "Failed to delete buffer file: \"$buffer_file\"!"
190         buffer_file="/dev/fd/3"
191 fi
192
193 if [[ $# -gt 0 ]]; then
194         # Execute command and save output in buffer file.
195         # Use a sub-shell to not pollute our name space!
196         error_file=$(mktemp) \
197                 || ax_abort -l "Failed to create error buffer file: \"$error_file\"!"
198         ax_debug "error_file=\"$error_file\""
199         exec 4>"$error_file" \
200                 || ax_abort -l "Failed to redirect FD #4 to error file!"
201         if [[ -n "$proc_fd_works" ]]; then
202                 rm "$error_file" \
203                         || ax_error -l "Failed to delete error buffer file: \"$error_file\"!"
204                 error_file="/dev/fd/4"
205         fi
206
207         job=$(basename "$1")
208
209         ax_debug "Running command \"$*\" ..."
210         start_t=$EPOCHSECONDS
211         exit_code=$(
212                 "$@" 2>&1 1>&3 | tee "$error_file" >&3
213                 echo "${PIPESTATUS[0]}"
214         )
215         end_t=$EPOCHSECONDS
216 else
217         # Read from stdin and save it to the buffer file.
218         error_file="/dev/null"
219         job="Job"
220
221         ax_debug "Reading from stdin ..."
222         start_t=$EPOCHSECONDS
223         while read -r line; do
224                 echo "$line" >&3 \
225                         || ax_abort -l "Failed to write to buffer file!"
226         done
227         end_t=$EPOCHSECONDS
228         exit_code=0
229 fi
230
231 ax_debug "exit_code=$exit_code"
232
233 declare -i count_all count_err
234 count_all=$(wc -l <"$buffer_file" || ax_abort -l "Failed to count buffer file!")
235 count_err=$(wc -l <"$error_file" || ax_abort -l "Failed to count error file!")
236
237 # Error or no error -- that's the question! An error is assumed when either the
238 # exit code of the command was non-zero or there was output to stderr.
239 # But when stderr_is_warning is set, messages on stderr result on a warning only!
240 if [[ $exit_code -ne 0 ]]; then
241         error_level=2
242 elif [[ $count_err -gt 0 ]]; then
243         [[ -n $stderr_is_warning ]] && error_level=1 || error_level=2
244 else
245         error_level=0
246 fi
247
248 # Construct email subject ...
249 [[ -z "$subject" ]] && subject="$host: $job report"
250 if [[ "$error_level" -eq 0 ]]; then
251         subject="$subject - success"
252 elif [[ "$error_level" -eq 1 ]]; then
253         subject="$subject - WARNING!"
254 else
255         subject="$subject - ERROR!"
256 fi
257
258 ax_debug "from=\"$from\""
259 ax_debug "to=\"$to\""
260 ax_debug "subject=$subject"
261
262 if [[ -n "$DEBUG" ]]; then
263         echo "--- stdout+stderr ---"
264         cat "$buffer_file"
265         echo "--- stderr ---"
266         cat "$error_file"
267         echo "---"
268 fi
269
270 ax_debug "count_all=$count_all"
271 ax_debug "count_err=$count_err"
272 ax_debug "error_level=$error_level"
273
274 # No errors detected (exit code & stderr), and email should be sent on errors
275 # only: so exit early!
276 [[ "$error_level" -lt 2 && -n "$do_errors_only" ]] && exit $exit_code
277
278 # No error detected and no output at all: skip email, exit early:
279 [[ "$error_level" -eq 0 && $count_all -eq 0 ]] && exit $exit_code
280
281 # Build the report mail.
282 # Make sure to ignore all mail(1) configuration files, system wide /etc/mailrc
283 # (by using the "-n" option) as well as ~/.mailrc (by setting the MAILRC
284 # environment variable).
285 export MAILRC=/dev/null
286 (
287         echo "$job report:"
288         echo
289         echo " - Host: $host"
290         echo " - User: $(id -un)"
291         echo " - Exit code: $exit_code"
292         [[ -n "$time" ]] && printf " - Duration: %s\n" "$(time_t_to_duration $((end_t - start_t)) ' ')"
293         echo
294         if [[ $# -gt 0 ]]; then
295                 # A command name is known (not stdin), show it!
296                 echo "Command:"
297                 echo "$@"
298                 echo
299         fi
300         [[ -n "$time" ]] && printf "%s - %s:\n\n" "$(time_t_to_date "$start_t")" "$(time_t_to_date "$end_t")"
301         if [[ $count_err -gt 0 ]]; then
302                 # Prefix mail with all error messages.
303                 echo "Error summary:"
304                 echo "-----------------------------------------------------------------------------"
305                 cat "$error_file" \
306                         || ax_abort -l "Failed to dump error file!"
307                 echo "-----------------------------------------------------------------------------"
308                 echo
309         fi
310         if [[ $count_all -ne $count_err ]]; then
311                 # Show full output when different to "error output" only.
312                 cat "$buffer_file" \
313                         || ax_abort -l "Failed to dump buffer file!"
314         fi
315 ) | mail -n -a "From: $from" -s "$subject" "$to" \
316         || ax_abort -l "Failed to send email to \"$to\"!"
317
318 [[ -n "$dont_fail" ]] && exit 0 || exit $exit_code