chiark / gitweb /
mtimeout.c: Fix (impossible) `printf' format-string bug.
[misc] / hush.in
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
24 set -e
25
26 quis=${0##*/}
27 usage="usage: $quis [-d DIR] [-m EMAIL] [-n NLOG] TAG COMMAND [ARGS ...]"
28 ver="@VERSION@"
29 version () { echo "$quis, @PACKAGE@ version $ver"; }
30
31 ###--------------------------------------------------------------------------
32 ### Parse the command line.
33
34 ## Initialize variables for storing command-line option values.
35 logdir="@logdir@"
36 maxlog=16
37 unset mail
38 unset owner
39 unset mode
40
41 ## Scan the options.
42 while getopts "hvd:m:n:p:u:" opt; do
43   case "$opt" in
44     h)
45       version
46       cat <<EOF
47
48 $usage
49
50 Run COMMAND with ARGS, logging output to DIR: if COMMAND succeeds, output
51 nothing; if it fails, also write its output to stdout or mail it to EMAIL.
52
53 Options:
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).
62 EOF
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
77 done
78 shift $(( OPTIND - 1 ))
79
80 ## Check the arguments.
81 case $# in 0 | 1) echo >&2 "$usage"; exit 1 ;; esac
82 tag=$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?
89 if stdbuf --version >/dev/null 2>&1; then
90   lbuf="stdbuf -oL --"
91 else
92   lbuf=""
93 fi
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.
102 date=$(date +%Y-%m-%d) seq=1
103 for i in "$logdir/$tag.$date#"*; do
104   tail=${i##*#}
105   case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
106   if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
107 done
108 log="$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.
114 umask=$(umask)
115 case ${mode+t} in t) ;; *) mode=$(printf %o $(( 0666 & ~umask ))) ;; esac
116 umask 077; exec 3>"$log"; umask $umask
117 case ${owner+t} in t) chown "$owner" "$log" ;; esac
118 chmod $mode "$log"
119
120 ###--------------------------------------------------------------------------
121 ### Run the program.
122
123 ## Write a log header.
124 cat >&3 <<EOF
125         Started $cmd at $(date +"%Y-%m-%d %H:%M:%S %z")
126         Lines beginning \`|' are stdout; lines beginning \`*' are stderr
127
128 EOF
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.
148 copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; }
149 rc=$(
150   { { { { set +e; $lbuf "$cmd" "$@" 3>&- 4>&- 5>&-; echo $? >&5; } |
151         copy "|" >&4; } 2>&1 |
152       copy "*" >&4; } 4>&1 |
153     cat -u >&3; } 5>&1 </dev/null
154 )
155
156 ## Write the log trailer.
157 cat >&3 <<EOF
158
159         Ended $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") with status $rc
160 EOF
161 exec 3>&-
162
163 ###--------------------------------------------------------------------------
164 ### Delete old log files if there are too many.
165
166 ## Count up the logfiles.
167 nlog=0
168 for i in "$logdir/$tag".*; do
169   if [ ! -f "$i" ]; then continue; fi
170   nlog=$(( nlog + 1 ))
171 done
172
173 ## If there are too many, go through and delete some early ones.
174 if [ $nlog -gt $maxlog ]; then
175   n=$(( nlog - maxlog ))
176   for i in "$logdir/$tag".*; do
177     if [ ! -f "$i" ]; then continue; fi
178     rm -f "$i"
179     n=$(( n - 1 ))
180     if [ $n -eq 0 ]; then break; fi
181   done
182 fi
183
184 ###--------------------------------------------------------------------------
185 ### Do something useful with the result.
186
187 case $rc,${mail+t} in
188   0,*)
189     ## Everything worked.  Leave the results in the log file in case someone
190     ## cares.
191     ;;
192   *,t)
193     ## Failed, and we have an email address.  Send mail and appear to
194     ## succeed: we've done our job and reported the situation.  The idea is
195     ## to prevent something else (e.g., cron) from producing another report
196     ## for the same problem, but without the useful content.
197     mail -s "$tag: $cmd failed (status = $rc)" "$mail" <"$log"
198     rc=0
199     ;;
200   *)
201     ## Failed, and no email address.  Write the accumulated stuff.
202     cat "$log"
203     ;;
204 esac
205
206 ## Exit with an appropriate status.
207 exit $rc
208
209 ###----- That's all, folks --------------------------------------------------