#!/bin/bash # Author: Ben Wong # 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: 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 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.