#!/bin/bash
# Author: Ben Wong <ben@wongs.net>
# Created on: <2001-08-08 11:56:03 hackerb9>
# Time-stamp: <2002-11-20 14:13:11 bbb>
# onlogdo: a hack to handle a common problem: when a certain message
# shows up in the log file, an action should be performed. E.g.: when
# a PC-card network device is inserted, it should be ifconfig'd or
# perhaps dhclient should be run. This program can do that in a fairly
# reasonable way.
### FIRST, THE HELPER FUNCTIONS ###
function showmanpage () {
cat <<'EOF'
NAME
onlogdo - monitor a log file and execute commands based on patterns
SYNOPSIS
onlogdo <logfile> <pattern> <command> [ <pattern> <command> ... ]
logfile: a file that gets appended to (e.g., /var/log/messages),
pattern: a string (can use bash's extended pathname globbing syntax),
command: the command to run when the previous pattern is seen.
DESCRIPTION
Onlogdo is a hack to handle a common problem: when a certain
message shows up in a log file, an action should be performed.
E.g.: when a PC-card network device is inserted, it should be
ifconfig'd or perhaps dhclient should be run. This program can do
that in a fairly reasonable way.
Onlogdo continuously reads lines as they are appended to a log
file. When a line matches a given pattern, the command associated
with that pattern is run in the background. The patterns
recognized are standard pathname globbing (*, ?, []) plus bash's
extended pattern matching (extglob) syntax (see bash(1)).
The log file is reopened properly, if the log file being read is
rotated. Also, since it blocks when waiting for new lines, onlogdo
takes up almost no CPU time.
PATTERN MATCHING SYNTAX
Don't worry about learning the pattern matching the first time you
read this manual; you'll rarely need it, unless you're anal about
being as succinct as possible. (Like the author).
The following is an excerpt from bash's man page; see bash(1) for
full details. In the following description, a pattern-list is a
list of one or more patterns separated by a |. Composite patterns
may be formed using one or more of the following sub-patterns:
* Matches any string, including the null string.
? Matches any single character.
[...] Matches any one of the enclosed characters. A pair of
characters separated by a hyphen denotes a range
expression. If the first character following the [ is a
! or a ^ then any character not enclosed is matched.
?(pattern-list)
Matches zero or one occurrence of the given patterns
*(pattern-list)
Matches zero or more occurrences of the given patterns
+(pattern-list)
Matches one or more occurrences of the given patterns
@(pattern-list)
Matches exactly one of the given patterns
!(pattern-list)
Matches anything except one of the given patterns
EXAMPLES
Pipelines are okay:
onlogdo /var/log/messages '*' 'sleep 5; echo olleH | rev'
Wildcards match filenames as usual in a command:
onlogdo /var/log/messages 'Segfault' 'rm /*.core'
Multiple pattern and command pairs are allowed:
onlogdo /var/log/messages \
'ep0 at pcmcia' '/etc/rc.d/dhclient start' \
'ep0 detached' '/etc/rc.d/dhclient stop' \
'wi0 at pcmcia' '/etc/rc.d/dhclient start' \
'wi0 detached' '/etc/rc.d/dhclient stop'
Bash's extended pathname globbing to do the same as above:
onlogdo /var/log/messages \
'@(ep|wi)[0-9] at pcmcia' '/etc/rc.d/dhclient start' \
'@(ep|wi)[0-9] detached' '/etc/rc.d/dhclient stop'
A useful example for NetBSD/hpcmips:
onlogdo /var/log/messages 'hpcapm: resume' 'sleep 1; xrefresh'
Under NetBSD/hpcmips-1.5.1, the X server screen is overwritten by the
console on an APM resume. I put the last "onlogdo" example in my
system xinitrc, so xrefresh will be run automatically. (The sleep
is there to wait for the console to finish junking up the screen).
SEE ALSO
tail(1), bash(1)
BUGS
If this had been written in Perl it would have used "normal"
regexp syntax instead of pathname expansion. Bash's extensions
make the patterns as powerful as regexps, but more recondite.
There is no (documented) way for a command to refer to specifics
in the line that matched. E.g., if the pattern was "@(ep|wi)0",
the command wouldn't know if the line contained ep0 or wi0.
The author has spent way too much time perfecting a kludge. No
matter what you're using onlogdo for, there's probably a more
"correct" way to do it. On the other hand, onlogdo is widely
applicable and easy to use, so at least you won't be wasting much
time while doing it the "wrong" way.
There should be a real man page.
AUTHOR
Ben Wong <Benjamin.Wong@cc.gatech.edu>
HISTORY
Onlogdo started life as a one line kludge to work around a bug in
the interaction between APM and the X server in NetBSD-1.5/hpcmips
in September of 2001.
EOF
}
### USAGE AND ARGUMENT SANITY CHECKING ###
if [[ $# -lt 3 ]]; then
showmanpage
exit 1
fi
# Emacs's syntax highlighting doesn't handle single ticks (') properly.
# Check if -v (verbose) flag was given.
if [[ "$1" == "-v" ]]; then
ifverbose=echo # Echo commands verbosely, if -v.
shift
else
ifverbose=: # Usually just run a no-op.
fi
# Make sure the log file is readable.
if [[ ! -r "$1" ]]; then
echo "onlogdo: \"$1\" is not readable" >&2
exit 1
fi
if [[ -d "$1" ]]; then
echo "onlogdo: \"$1\" is a directory" >&2
exit 1
fi
### SIGNAL HANDLING AND EXIT CLEANUP ###
# Run cleanup function when shell exits.
trap cleanup EXIT
cleanup () {
echo "onlogdo: cleaning up..." >&2
if [[ "$TAILPID" ]]; then
kill $TAILPID # Kill the bg tail process.
fi
rm -f "$PIPE" # Delete the named pipe.
rmdir "$PIPEDIR"
return 0
}
### MAIN ROUTINE STARTS HERE ###
# Don't expand wildcards in pattern arguments as filenames.
set -o noglob
# Use bash's extended pattern matching operators
shopt -s extglob
# Create a pipe to read the log file a line at a time;
# Use mktemp for security.
PIPEDIR=$(mktemp -d /tmp/onlogdo.XXXXXX)
PIPE="$PIPEDIR/$(echo $1 | sed 's#^/##; s#/#.#g')" # var.log.messages
rm -f $PIPE
mknod $PIPE p
tail -Fn0 $1 >$PIPE & # FSF tail: "tail --retry -fn0"
TAILPID=$!
exec 5< $PIPE
# Copy the positional paramaters in to a variable that can be indexed.
params=("$0" "$@")
# Repeatedly read from the pipe, process each line.
while :; do
while read REPLY <&5; do
$ifverbose -e "\nline: \"$REPLY\""
for ((i=2; i<$#; i+=2)); do
pattern=${params[$i]}
command=${params[$((i+1))]}
$ifverbose "pattern: \"$pattern\""
if [[ -z "${REPLY##*$pattern*}" ]]; then
$ifverbose "*MATCHED*"
$ifverbose "command: \"$command\""
set +o noglob # Allow pathname matching in commands
eval $command &
set -o noglob # No pathname expansion for patterns
fi
done
done
# We get here whenever the pipe first blocks (returns EOF)
sleep 1 # Don't loop too quickly
done
### MAIN ROUTINE ENDS HERE ###
# Note: if we ever get here, the cleanup() function will be called
# automatically since we're trapping on signal 0.