mtimeout.1: Use correct dash for number ranges.
[misc] / hush.in
CommitLineData
c818aced
MW
1#! /bin/sh
2###
3### Run a program, but stash its output unless it fails
4###
5### (c) 2011 Mark Wooding
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
10### This program is free software; you can redistribute it and/or modify
11### it under the terms of the GNU General Public License as published by
12### the Free Software Foundation; either version 2 of the License, or
13### (at your option) any later version.
14###
15### This program is distributed in the hope that it will be useful,
16### but WITHOUT ANY WARRANTY; without even the implied warranty of
17### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18### GNU General Public License for more details.
19###
20### You should have received a copy of the GNU General Public License
21### along with this program; if not, write to the Free Software
22### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
23
24set -e
25
26quis=${0##*/}
27usage="usage: $quis [-d DIR] [-m EMAIL] [-n NLOG] TAG COMMAND [ARGS ...]"
28ver="@VERSION@"
29version () { echo "$quis, @PACKAGE@ version $ver"; }
30
31###--------------------------------------------------------------------------
32### Parse the command line.
33
34## Initialize variables for storing command-line option values.
35logdir="@logdir@"
36maxlog=16
37unset mail
38unset owner
39unset mode
40
41## Scan the options.
42while getopts "hvd:m:n:p:u:" opt; do
43 case "$opt" in
44 h)
45 version
46 cat <<EOF
47
48$usage
49
50Run COMMAND with ARGS, logging output to DIR: if COMMAND succeeds, output
51nothing; if it fails, also write its output to stdout or mail it to EMAIL.
52
53Options:
54 -h Show this help text and exit.
55 -v Show the program's version number and exit.
56
57 -d DIR Write log files to DIR (default $logdir).
58 -m EMAIL Send email on failure to EMAIL.
59 -n MAXLOG Keep at most MAXLOG log files (default $maxlog).
60 -p MODE Set log permissions to MODE (default umask).
61 -u [OWNER][:GROUP] Set log file OWNER and GROUP (default system).
62EOF
63 exit
64 ;;
65 v)
66 version
67 exit
68 ;;
69
70 d) logdir=$OPTARG ;;
71 m) mail=$OPTARG ;;
72 n) maxlog=$OPTARG ;;
73 p) mode=$OPTARG ;;
74 u) owner=$OPTARG ;;
75 *) echo >&2 "$usage"; exit 1 ;;
76 esac
77done
8ac62321 78shift $(( $OPTIND - 1 ))
c818aced
MW
79
80## Check the arguments.
81case $# in 0 | 1) echo >&2 "$usage"; exit 1 ;; esac
82tag=$1 cmd=$2; shift 2
83
84###--------------------------------------------------------------------------
85### Check out the environment.
86
87## Force a command to line-buffer its output. How does one do this on BSD,
88## for example?
89if stdbuf --version >/dev/null 2>&1; then
90 lbuf="stdbuf -oL --"
91else
92 lbuf=""
93fi
94
95###--------------------------------------------------------------------------
96### Set up the log file.
97
98## Find a name for the log file. In unusual circumstances, we may have
99## deleted old logs from today, so just checking for an unused sequence
100## number is insufficient. Instead, check all of the logfiles for today, and
101## use a sequence number that's larger than any of them.
102date=$(date +%Y-%m-%d) seq=1
103for i in "$logdir/$tag.$date#"*; do
104 tail=${i##*#}
105 case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
8ac62321 106 if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( $tail + 1 )); fi
c818aced
MW
107done
108log="$logdir/$tag.$date#$seq"
109
110## Create the file. Make sure we create it with restrictive permissions
111## and then slacken them off if necessary. This means that we don't (for
112## example) end up giving the wrong group write permission to the file for a
113## little bit.
114umask=$(umask)
8ac62321 115case ${mode+t} in t) ;; *) mode=$(printf %o $(( 0666 & ~$umask ))) ;; esac
c818aced
MW
116umask 077; exec 3>"$log"; umask $umask
117case ${owner+t} in t) chown "$owner" "$log" ;; esac
118chmod $mode "$log"
119
120###--------------------------------------------------------------------------
121### Run the program.
122
123## Write a log header.
124cat >&3 <<EOF
125 Started $cmd at $(date +"%Y-%m-%d %H:%M:%S %z")
126 Lines beginning \`|' are stdout; lines beginning \`*' are stderr
127
128EOF
129
130## Run the program, interleaving stdout and stderr in a vaguely useful way.
131## This involves what I can only describe as a `shell game' (sorry) with file
132## descriptors.
133##
134## In the middle, we have the actual command, hacked so as to line-buffer
135## stdout (so that we can better interleave stderr). We capture its stdout
136## and stderr into pipelines, one at a time, in which we pluck out lines one
137## by one and prefix them with distinctive characters, and then write them to
138## another pipe (fd 4) which is written via cat(1) to the log file. (This is
139## not a `useless use of cat': I rely on the write atomicity guarantee of
140## pipes in order to prevent intermingling of the stdout and stderr lines --
141## of course, if they're too long to fit in the pipe buffer then we'll just
142## lose.)
143##
144## Finally, there's a problem because we only get the exit status of the last
145## stage of a pipeline, where we actually wanted the status of the first. So
146## we write that to another pipe (fd 5) and pick it out using command
147## substitution.
4641d6e2 148copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; }
c818aced 149rc=$(
12e09263 150 { { { { set +e; $lbuf "$cmd" "$@" 3>&- 4>&- 5>&-; echo $? >&5; } |
4641d6e2
MW
151 copy "|" >&4; } 2>&1 |
152 copy "*" >&4; } 4>&1 |
1e22187d 153 cat -u >&3; } 5>&1 </dev/null
c818aced
MW
154)
155
156## Write the log trailer.
157cat >&3 <<EOF
158
159 Ended $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") with status $rc
160EOF
161exec 3>&-
162
163###--------------------------------------------------------------------------
164### Delete old log files if there are too many.
165
f4dc42b8
MW
166## Find out the tails of the logfile names. We assume that we're responsible
167## for all of these, and therefore that they're nicely formed.
168logs="" nlog=0
c818aced
MW
169for i in "$logdir/$tag".*; do
170 if [ ! -f "$i" ]; then continue; fi
f9f9787a 171 nlog=$(( $nlog + 1 ))
f4dc42b8 172 logs="$logs ${i#$logdir/$tag.}"
c818aced
MW
173done
174
175## If there are too many, go through and delete some early ones.
176if [ $nlog -gt $maxlog ]; then
f9f9787a 177 n=$(( $nlog - $maxlog ))
f4dc42b8
MW
178 for i in $logs; do echo $i; done | sort -t# -k1,1 -k2n | while read i; do
179 rm -f "$logdir/$tag.$i"
f9f9787a 180 n=$(( $n - 1 ))
c818aced
MW
181 if [ $n -eq 0 ]; then break; fi
182 done
183fi
184
185###--------------------------------------------------------------------------
186### Do something useful with the result.
187
188case $rc,${mail+t} in
189 0,*)
190 ## Everything worked. Leave the results in the log file in case someone
191 ## cares.
192 ;;
193 *,t)
194 ## Failed, and we have an email address. Send mail and appear to
195 ## succeed: we've done our job and reported the situation. The idea is
196 ## to prevent something else (e.g., cron) from producing another report
197 ## for the same problem, but without the useful content.
198 mail -s "$tag: $cmd failed (status = $rc)" "$mail" <"$log"
199 rc=0
200 ;;
201 *)
202 ## Failed, and no email address. Write the accumulated stuff.
203 cat "$log"
204 ;;
205esac
206
207## Exit with an appropriate status.
208exit $rc
209
210###----- That's all, folks --------------------------------------------------