chiark / gitweb /
hush: New handy program.
authorMark Wooding <mdw@distorted.org.uk>
Wed, 14 Dec 2011 01:47:10 +0000 (01:47 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 14 Dec 2011 01:54:44 +0000 (01:54 +0000)
Makefile.am
configure.ac
debian/control
debian/inst
hush.1.in [new file with mode: 0644]
hush.in [new file with mode: 0755]

index 66f70a1b5b3f86085354700c50f8fb82a0458525..64eeabfb69dad364fd546647eab3de63257ea255 100644 (file)
@@ -155,7 +155,8 @@ SUBSTITUTIONS = \
        PYTHON=$(PYTHON) \
        PERL=$(PERL) \
        TCLSH=$(TCLSH) \
-       BASH=$(BASH)
+       BASH=$(BASH) \
+       logdir=$(logdir)
 
 EXTRA_DIST             += config/confsubst
 
@@ -172,6 +173,19 @@ dist_man_MANS              += create.1
 dist_bin_SCRIPTS       += z
 dist_man_MANS          += z.1
 
+bin_SCRIPTS            += hush
+man_MANS               += hush.1
+CLEANFILES             += hush hush.1
+EXTRA_DIST             += hush.in hush.1.in
+
+hush: hush.in Makefile
+       $(SUBST) $(srcdir)/hush.in >$@.new $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
+hush.1: hush.1.in Makefile
+       $(SUBST) $(srcdir)/hush.1.in >$@.new $(SUBSTITUTIONS) && \
+               mv $@.new $@
+
 ## bash scripts.
 if HAVE_BASH
 
index 572648969ab648e9533123a3f39b6f6999169bab..b171850d8368740c789008395cf7b3b194d60e70 100644 (file)
@@ -31,6 +31,16 @@ AC_CONFIG_AUX_DIR([config])
 AM_INIT_AUTOMAKE([foreign])
 mdw_SILENT_RULES
 
+AC_ARG_WITH([logdir],
+       AS_HELP_STRING([--with-logdir=DIR],
+                      [Write log files here.]),
+       [logdir=$withval],
+       [logdir=/var/log
+        for i in /var/log /var/adm; do
+          if test -d $i; then logdir=$i; break; fi
+        done])
+AC_SUBST(logdir)
+
 AC_CANONICAL_HOST
 
 dnl--------------------------------------------------------------------------
index c5d505e9221c867d57a3ec7dedbff4389a0ce4f0..09a461ec33333d168a24fa36b5b55040f8e7c799 100644 (file)
@@ -28,7 +28,8 @@ Depends:
        inplace,
        stamp,
        space,
-       getpass
+       getpass,
+       hush
 Description: Dummy package for convenience.
 
 Package: mdwopt-perl
@@ -155,3 +156,12 @@ Architecture: any-i386 any-amd64
 Section: utils
 Description: Shows basic model information about x86 processors.
  The cpuid program is probably better for most people.
+
+Package: hush
+Architecture: all
+Section: utils
+Description: Run a command, hiding its output in a logfile unless it fails
+ The hush program is useful for running noisy programs from cron or similar,
+ where you get spammed with uninteresting success reports.  hush runs a
+ command, logging its output, but, unless the command actually fails, it
+ produces no output of its own.
index 1f01de4e916b424e3111b7b391b0f13d59e72c15..25007bfc1b31a51a7b2ace5f7315d2fac3486539 100644 (file)
@@ -50,3 +50,5 @@ z zz /usr/bin
 z.1 zz /usr/share/man/man1
 x86-model x86-model /usr/bin
 x86-model.1 x86-model /usr/share/man/man1
+hush hush /usr/bin
+hush.1 hush /usr/share/man/man1
diff --git a/hush.1.in b/hush.1.in
new file mode 100644 (file)
index 0000000..e5c2bc7
--- /dev/null
+++ b/hush.1.in
@@ -0,0 +1,169 @@
+.TH hush 1 "14 December 2011" "Edgeware tools"
+.SH NAME
+hush \- run a program, quietly unless it fails
+.SH SYNOPSIS
+.B hush
+.RB [ \-d
+.IR directory ]
+.RB [ \-m
+.IR email-address ]
+.RB [ \-n
+.IR maxlog ]
+.RB [ \-u
+.IR owner ][\fB: group ]
+.br
+       \c
+.I tag
+.I command
+.IR arguments ...
+.SH DESCRIPTION
+The
+.B hush
+program runs a command.  The command's output (i.e., what it writes to
+its standard output and standard error file descriptors) is always
+logged to a file.  If the command succeeds,
+.B hush
+itself outputs nothing; if it fails, then
+.B hush
+either writes the command's output to its own stdout, or sends it via
+email.  It is intended to be used when running noisy programs via
+.BR cron (8),
+to reduce the amount of uninteresting mail (`cronspam') produced by an
+essentially working system.
+.PP
+The following command-line options are recognized.
+.TP
+.B \-h
+Write a help message describing
+.BR hush 's
+command line options and usage to standard output, and exit.
+.TP
+.B \-v
+Write
+.BR hush 's
+version number to standard output, and exit.
+.TP
+.BI "\-d " directory
+Write log files to
+.I directory
+rather than the default, 
+.BR "@logdir@" .
+.TP
+.BI "\-m " email-address
+Rather than writing its output to stdout if the command fails, send the
+command's output to
+.IR email-address .
+and exit with status 0.  (This is perhaps a surprising choice, but it
+prevents the caller from taking additional action to report a problem
+which has already been escalated to a human.)
+.TP
+.BI "\-n " maxlog
+If necessary, delete old logfiles so that no more than
+.I maxlog
+log files are left.
+.TP
+.BI "\-p " mode
+Set the permissions on the logfile to
+.I mode ,
+a mode specification acceptable to
+.BR chmod (1), though relative permissions will be applied to mode
+.B 600
+(i.e.,
+.BR u=rw,og= ).
+.TP
+.BI "\-u \fR[" user\fR][ : group\fR]
+Set the ownership and/or group of the logfile.  If the
+.I user
+is specified, then the file's owner is set; if the
+.I group
+is specified, the file's group is set.  (Some care is taken to ensure
+that the file is never available to members of the wrong group.)
+.SS Operation
+The given
+.I command
+is executed with the
+given
+.IR arguments ,
+with stdin redirected from
+.BR /dev/null ,
+and stdout and stderr redirected to separate pipes.  If it is available,
+.BR stdbuf (1)
+is used to ensure that the
+.IR command 's
+stdout is line-buffered.
+.PP
+The
+.IR command 's
+output is collected in a log file named
+.IB logdir / tag . date # seq
+where
+.TP
+.I logdir
+is the argument of the
+.B \-d
+option, or
+.B @logdir@
+by default;
+.TP
+.I tag
+is the
+.I tag
+string given to
+.B hush
+as a command-line argument;
+.TP
+.I date
+is the current date, in ISO8601 form (in local time); and
+.TP
+.I seq
+is a sequence number, chosen to ensure that log file names are distinct
+and sort in chronological order.
+.PP
+The log file begins with a header giving the exact start time (in local
+time, with an offset from UTC) and a brief summary of the log format; it
+ends with another timestamp and the
+.IR command 's
+exit status.  In between is the command's output.  Lines written to
+stdout begin with
+.RB ` | ';
+lines to stderr begin with
+.RB ` * '.
+The two are interleaved in an attempt to help the reader identify how
+much progress the
+.I command
+had made when it encountered an error; however, because the streams are
+read asynchronously, this isn't perfect, and lines may appear earlier or
+later than they ought to.
+.PP
+If the
+.I command
+succeeds, as mentioned,
+.B hush
+exits without printing anything.  If it fails, and the
+.B \-m
+option was given, the log file is mailed to the appropriate
+.I email-address
+with a subject line
+.IP
+.IB tag :
+.I command
+.B failed (status =
+.IB rc )
+.PP
+where
+.I rc
+is the
+.IR command 's
+exit status.  If no
+.B \-m
+option was given, this log is simply written to standard output.
+.SH BUGS
+The stream interleaving isn't quite right, but it's hard to see how to
+improve it.
+.PP
+Capturing the command's output involves making a fairly large number of
+auxiliary processes and file descriptors.  This is a bit ugly.
+.SH AUTHOR
+Mark Wooding, <mdw@distorted.org.uk>
+.SH SEE ALSO
+.BR cron (8).
diff --git a/hush.in b/hush.in
new file mode 100755 (executable)
index 0000000..0ecc48e
--- /dev/null
+++ b/hush.in
@@ -0,0 +1,208 @@
+#! /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 --------------------------------------------------