+#! /bin/sh
+###
+### Run a program, but stash its output unless it fails
+###
+### (c) 2011 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### 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
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+
+set -e
+
+quis=${0##*/}
+usage="usage: $quis [-d DIR] [-m EMAIL] [-n NLOG] TAG COMMAND [ARGS ...]"
+ver="@VERSION@"
+version () { echo "$quis, @PACKAGE@ version $ver"; }
+
+###--------------------------------------------------------------------------
+### Parse the command line.
+
+## Initialize variables for storing command-line option values.
+logdir="@logdir@"
+maxlog=16
+unset mail
+unset owner
+unset mode
+
+## Scan the options.
+while getopts "hvd:m:n:p:u:" opt; do
+ case "$opt" in
+ h)
+ version
+ cat <<EOF
+
+$usage
+
+Run COMMAND with ARGS, logging output to DIR: if COMMAND succeeds, output
+nothing; if it fails, also write its output to stdout or mail it to EMAIL.
+
+Options:
+ -h Show this help text and exit.
+ -v Show the program's version number and exit.
+
+ -d DIR Write log files to DIR (default $logdir).
+ -m EMAIL Send email on failure to EMAIL.
+ -n MAXLOG Keep at most MAXLOG log files (default $maxlog).
+ -p MODE Set log permissions to MODE (default umask).
+ -u [OWNER][:GROUP] Set log file OWNER and GROUP (default system).
+EOF
+ exit
+ ;;
+ v)
+ version
+ exit
+ ;;
+
+ d) logdir=$OPTARG ;;
+ m) mail=$OPTARG ;;
+ n) maxlog=$OPTARG ;;
+ p) mode=$OPTARG ;;
+ u) owner=$OPTARG ;;
+ *) echo >&2 "$usage"; exit 1 ;;
+ esac
+done
+shift $(( OPTIND - 1 ))
+
+## Check the arguments.
+case $# in 0 | 1) echo >&2 "$usage"; exit 1 ;; esac
+tag=$1 cmd=$2; shift 2
+
+###--------------------------------------------------------------------------
+### Check out the environment.
+
+## Force a command to line-buffer its output. How does one do this on BSD,
+## for example?
+if stdbuf --version >/dev/null 2>&1; then
+ lbuf="stdbuf -oL --"
+else
+ lbuf=""
+fi
+
+###--------------------------------------------------------------------------
+### Set up the log file.
+
+## Find a name for the log file. In unusual circumstances, we may have
+## deleted old logs from today, so just checking for an unused sequence
+## number is insufficient. Instead, check all of the logfiles for today, and
+## use a sequence number that's larger than any of them.
+date=$(date +%Y-%m-%d) seq=1
+for i in "$logdir/$tag.$date#"*; do
+ tail=${i##*#}
+ case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
+ if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
+done
+log="$logdir/$tag.$date#$seq"
+
+## Create the file. Make sure we create it with restrictive permissions
+## and then slacken them off if necessary. This means that we don't (for
+## example) end up giving the wrong group write permission to the file for a
+## little bit.
+umask=$(umask)
+case ${mode+t} in t) ;; *) mode=$(printf %o $(( 0666 & ~umask ))) ;; esac
+umask 077; exec 3>"$log"; umask $umask
+case ${owner+t} in t) chown "$owner" "$log" ;; esac
+chmod $mode "$log"
+
+###--------------------------------------------------------------------------
+### Run the program.
+
+## Write a log header.
+cat >&3 <<EOF
+ Started $cmd at $(date +"%Y-%m-%d %H:%M:%S %z")
+ Lines beginning \`|' are stdout; lines beginning \`*' are stderr
+
+EOF
+
+## Run the program, interleaving stdout and stderr in a vaguely useful way.
+## This involves what I can only describe as a `shell game' (sorry) with file
+## descriptors.
+##
+## In the middle, we have the actual command, hacked so as to line-buffer
+## stdout (so that we can better interleave stderr). We capture its stdout
+## and stderr into pipelines, one at a time, in which we pluck out lines one
+## by one and prefix them with distinctive characters, and then write them to
+## another pipe (fd 4) which is written via cat(1) to the log file. (This is
+## not a `useless use of cat': I rely on the write atomicity guarantee of
+## pipes in order to prevent intermingling of the stdout and stderr lines --
+## of course, if they're too long to fit in the pipe buffer then we'll just
+## lose.)
+##
+## Finally, there's a problem because we only get the exit status of the last
+## stage of a pipeline, where we actually wanted the status of the first. So
+## we write that to another pipe (fd 5) and pick it out using command
+## substitution.
+rc=$(
+ { { { { set +e; $lbuf "$cmd" "$@"; echo $? >&5; } |
+ while read line; do echo "| $line"; done >&4; } 2>&1 |
+ while read line; do echo "* $line"; done >&4; } 4>&1 |
+ cat >&3; } 5>&1 </dev/null
+)
+
+## Write the log trailer.
+cat >&3 <<EOF
+
+ Ended $cmd at $(date +"%Y-%m-%d %H:%M:%S %z") with status $rc
+EOF
+exec 3>&-
+
+###--------------------------------------------------------------------------
+### Delete old log files if there are too many.
+
+## Count up the logfiles.
+nlog=0
+for i in "$logdir/$tag".*; do
+ if [ ! -f "$i" ]; then continue; fi
+ nlog=$(( nlog + 1 ))
+done
+
+## If there are too many, go through and delete some early ones.
+if [ $nlog -gt $maxlog ]; then
+ n=$(( nlog - maxlog ))
+ for i in "$logdir/$tag".*; do
+ if [ ! -f "$i" ]; then continue; fi
+ rm -f "$i"
+ n=$(( n - 1 ))
+ if [ $n -eq 0 ]; then break; fi
+ done
+fi
+
+###--------------------------------------------------------------------------
+### Do something useful with the result.
+
+case $rc,${mail+t} in
+ 0,*)
+ ## Everything worked. Leave the results in the log file in case someone
+ ## cares.
+ ;;
+ *,t)
+ ## Failed, and we have an email address. Send mail and appear to
+ ## succeed: we've done our job and reported the situation. The idea is
+ ## to prevent something else (e.g., cron) from producing another report
+ ## for the same problem, but without the useful content.
+ mail -s "$tag: $cmd failed (status = $rc)" "$mail" <"$log"
+ rc=0
+ ;;
+ *)
+ ## Failed, and no email address. Write the accumulated stuff.
+ cat "$log"
+ ;;
+esac
+
+## Exit with an appropriate status.
+exit $rc
+
+###----- That's all, folks --------------------------------------------------