#!/usr/bin/perl
# vim:autoindent:tw=78:ts=4
# ------------------------------------------------------------
# $Id: sarah,v 1.24 2001/10/01 22:26:49 mattp Exp $
#
# $Revision: 1.24 $
#  Revision $Author: mattp $
#  Revision $Date: 2001/10/01 22:26:49 $
# ------------------------------------------------------------

# ------------------------------------------------------------
# Copyright 2000, Matthew Pounsett (mattp@conundrum.com)
# ------------------------------------------------------------

use File::Copy;
use Getopt::Std;
use POSIX qw( strftime );
use strict;
no strict 'refs';
use Sys::Hostname;
use Text::Abbrev;

## Defaults
##
my( $config )     = '/usr/local/etc/sarah.conf';
my( $compBin )    = '/usr/bin/gzip';
my( $compSuffix ) = 'gz';
my( $rotate_msg ) = 'logfile turned over';
my( $syslogdPID )  = '/var/run/syslog.pid';

# ------------------------------------------------------------
# Version Info
my( $VERSION ) = "0.7b";
# ------------------------------------------------------------

### ------------------------------------------ ###
### Don't play with anything below here        ###
### unless you know exactly what you're doing. ###
### ------------------------------------------ ###

my( $ME, %logfiles, $log, @fstat, %ROTATED, $pid, $signal );
my( $tmpSuffix_max_length ) = 10;
my( $hostname ) = hostname();
my( %Opt, $options );
my( $NOTROOT, $DEBUG, $VERBOSE, $SYSLOG, $QUIET );
my( $SUCCESS );
$hostname =~ s/^([^.]+)\..*/$1/;
( $ME = $0 ) =~ s/.*\/([^\/]+)/$1/;
my( $NOW ) = time();

$options = "cf:hqrsvV";
getopts( $options, \%Opt );

my( %ERROR ) = (
	"UNX_CMD" => "%s line %d: got unexpected command %s in state %d",
	"UNX_WRD" => "%s line %d: expected \'%s\' but got \'%s\' in state %d",
	"UNX_EOF" => "%s line %d: unexpected EOF (still in state %d)",
	"UNR_CMD" => "%s line %d: got unrecognized command %s in state %d",
	"UNR_STA" => "%s line %d: unrecognized state %d",
	"UNR_VAL" => "%s line %d: unregoznized value \'%s\' to command \'%s\'",
	"UNR_DVA" => "unrecognized date value %s for type %s. ignoring.",
	"INC_PAR" => "%s line %d: failed to parse included config file \'%s\'"
);


my( %MONTH ) = (
	1	=>	31,
	3	=>	31,
	4	=>	30,
	5	=>	31,
	6	=>	30,
	7	=>	31,
	8	=>	31,
	9	=>	30,
	10	=>	31,
	11	=>	30,
	12	=>	31
);

# Deal with leap-years.
#
my( $YEAR ) = strftime( "%Y", localtime( $NOW ));

if(($YEAR%4 == 0) && ($YEAR<1582 || ($YEAR%100 != 0) || ($YEAR%400 == 0))) {
	$MONTH{ 2 } = 29;
} else {
	$MONTH{ 2 } = 28;
}

my( %RANGE ) = (
	'MINUTE'	=> '0-59',
	'HOUR'		=> '0-23',
	'MONTH'		=> '1-12',
	'DOW'		=> '1-7',
	'DOM'		=> '1-' . $MONTH{ int( strftime( "%m", localtime($NOW))) }
);

my( %MULTIPLIER ) = (
	'B'	=>	1,
	'K'	=>	2**10,
	'M' =>	2**20
);

# initialize randomness.  To be used later for random filename generation.
#
srand( $NOW ^ ($$ + ($$ << 15)) );

# set default options
#
my( %options ) = (
	'SIZEMOD'	=>	'K',
	'SIZELOGIC'	=>	'AND',
	'INCDEPTH'	=>	'20'
);

sub END {
	verbose( "finished" );

	if( $SYSLOG ) {
		closelog();
	}
}

sub usage {
	print << "EOH";
usage: $ME [ -chqrsvV ] [ -f config_file ]
version: $VERSION

   -c               Check config file for errors and exit. Implies -v
   -f config_file   Read config_file for configuration instead of the default
                    /usr/local/etc/sarah.conf
   -h               Show this usage information.
   -q               Quiet mode (does not report errors)
   -r               Remove the requirement to run as root.  Will not signal
                    syslogd, or set ownership or permissions on archive
                    files.
   -s               Copy all output to syslog.
   -v               Run in verbose mode.
   -V               Run in full debug mode.

EOH
}

sub log_sort {
	my( $first, $second );
	( $first  = $a ) =~ s/.*\.(\d+)(\.$compSuffix)?$/$1/;
	( $second = $b ) =~ s/.*\.(\d+)(\.$compSuffix)?$/$1/;
	
	$first <=> $second;
}

sub reverse_sort {
	$b cmp $a;
}

sub setup_syslog {
	
	use Sys::Syslog;
	openlog( $ME, 'pid', 'daemon' );

	return( 1 );

}

sub err {
	
	my( $errKey, @errArgs ) = (@_);
	my( $errStr );

	if( $QUIET ) { return 1; }

	if( exists( $ERROR{ $errKey } )) {
		$errStr = $ERROR{ $errKey };
	} else {
		$errStr = $errKey;
	}

	printf( STDERR "%s: " . $errStr . "\n", $ME, @errArgs );

	if( $SYSLOG ) {
		syslog( 'err', $errStr, @errArgs );
	}

}

sub verbose {

	unless( $VERBOSE ) { return 1; }
	
	my( $errKey, @errArgs ) = (@_);
	my( $errStr );

	if( exists( $ERROR{ $errKey } )) {
		$errStr = $ERROR{ $errKey };
	} else {
		$errStr = $errKey;
	}

	printf( STDERR "%s: " . $errStr . "\n", $ME, @errArgs );

	if( $SYSLOG ) {
		syslog( 'debug', $errStr, @errArgs );
	}
}

sub debug {

	unless( $DEBUG ) { return 1; }
	
	my( $errKey, @errArgs ) = (@_);
	my( $errStr );

	if( exists( $ERROR{ $errKey } )) {
		$errStr = $ERROR{ $errKey };
	} else {
		$errStr = $errKey;
	}

	printf( STDERR "%s: " . $errStr . "\n", $ME, @errArgs );

	if( $SYSLOG ) {
		syslog( 'debug', $errStr, @errArgs );
	}
}


sub parse {

	my( $CONFIG )  = shift;
	my( $OPT_PTR ) = shift;
	my( $LOG_PTR ) = shift;
	my( $FH )      = shift;

	if( $FH =~ m/fh(\d+)/ ) {
		if( $1 >= $options{ INCDEPTH } ) {
			err( "exceeded include depth limit of %d",$options{ INCDEPTH });
			return( 0 );
		}
		$FH++
	} else {
		$FH = 'fh00';
	}

	my( $PARSE_STATE, $LINE, $WORD, $LOG );
	
	# Valid config file commands.
	#
	my( %COMMAND ) = (
		'options'			=>	100,
		'sizemod'			=>	104,
		'sizelogic'			=>	105,
		'incdepth'			=>	106,
	
		'log'				=>	200,
		'archivedir'		=>	210,
		'owner'				=>	211,
		'group'				=>	212,
		'mode'				=>	213,
		'keep'				=>	214,
		'size'				=>	215,
		'flags'				=>	216,
		'pid'				=>	217,
		'signal'			=>	218,
		'index'				=>	219,

		'date'				=>	290,
		'minute'			=>	294,
		'hour'				=>	295,
		'dom'				=>	296,
		'month'				=>	297,
		'dow'				=>	298,

		'include'			=>	300
	);

	my( %STATE ) = (
		IDLE				=>	0,
		SEMICOLON			=>	3,
	
		OPT					=>	100,
		OPT_OPEN			=>	101,
		OPT_IDLE			=>	102,
		OPT_SEMICOLON		=>	103,
		OPT_SIZEMOD			=>	104,
		OPT_SIZELOGIC		=>	105,
		OPT_INCDEPTH		=>	106,

		OPT_ARCHIVEDIR		=>	110,
		OPT_OWNER			=>	111,
		OPT_GROUP			=>	112,
		OPT_MODE			=>	113,
		OPT_KEEP			=>	114,
		OPT_SIZE			=>	115,
		OPT_FLAGS			=>	116,
		OPT_PID				=>	117,
		OPT_SIGNAL			=>	118,
		OPT_INDEX			=>	119,

		OPT_DATE			=>	120,
		OPT_DATE_OPEN		=>	121,
		OPT_DATE_IDLE		=>	122,
		OPT_DATE_SEMICOLON	=>	123,
		OPT_DATE_MINUTE		=>	124,
		OPT_DATE_HOUR		=>	125,
		OPT_DATE_DOM		=>	126,
		OPT_DATE_MONTH		=>	127,
		OPT_DATE_DOW		=>	128,
	
		LOG					=>	200,
		LOG_OPEN			=>	201,
		LOG_IDLE			=>	202,
		LOG_SEMICOLON		=>	203,
		LOG_ARCHIVEDIR		=>	210,
		LOG_OWNER			=>	211,
		LOG_GROUP			=>	212,
		LOG_MODE			=>	213,
		LOG_KEEP			=>	214,
		LOG_SIZE			=>	215,
		LOG_FLAGS			=>	216,
		LOG_PID				=>	217,
		LOG_SIGNAL			=>	218,
		LOG_INDEX			=>	219,

		LOG_DATE			=>	290,
		LOG_DATE_OPEN		=>	291,
		LOG_DATE_IDLE		=>	292,
		LOG_DATE_SEMICOLON	=>	293,
		LOG_DATE_MINUTE		=>	294,
		LOG_DATE_HOUR		=>	295,
		LOG_DATE_DOM		=>	296,
		LOG_DATE_MONTH		=>	297,
		LOG_DATE_DOW		=>	298,

		INCLUDE				=>	300,
		INCLUDE_OPEN		=>	301,
		INCLUDE_IDLE		=>	302,
		INCLUDE_SEMICOLON	=>	303
	
	);
	
	my( %ABBREV ) = ();
	abbrev( \%ABBREV, sort keys( %COMMAND ) );
	
	unless( open( $FH, $CONFIG )) {
		err( "failed to open config file %s: %s", $CONFIG, $! );
		return( 0 );
	}
	
	$PARSE_STATE = $STATE{ IDLE };

	debug( "parsing config file \'%s\'", $CONFIG );
	
	LINE:
	while( $LINE = <$FH> ) {
	
	
		WORD:
		while( $LINE =~ s/\s*(#|{|}|;|[^#{};\s]+)(\s*.*)/$2/ ) {
	
			$WORD = $1;
			
			# If this is starting a comment, skip the rest of the line.
			#
			if( $WORD =~ /#/ ) { next LINE; }

			if( $PARSE_STATE eq $STATE{ IDLE } ) {
				# We're starting clean.  Expecting a valid command, so let's
				# check first to see if this is an abbreviation.  If it is,
				# expand it to the full command word.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
	
				# Have we at least got a valid command?
				#
				unless( exists( $COMMAND{ lc( $WORD ) } ) ) {
					err( 'UNR_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
					next WORD;
				}
	
				# We've got a valid command... let's see if it's one we're
				# expecting.
				#
	
				# Are we setting options?
				#
				if( lc( $WORD ) eq 'options' ) {
					$PARSE_STATE = $STATE{ OPT };
				} 
				# Are we including another config?
				#
				elsif( lc( $WORD ) eq 'include' ) {
					$PARSE_STATE = $STATE{ INCLUDE };
				} 
				# Are we defining a new logfile?
				#
				elsif( lc( $WORD ) eq 'log' ) {
					$PARSE_STATE = $STATE{ LOG };
				}
				# Whatever command we got doesn't belong here.
				#
				else {
					err( 'UNX_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
				}
	
			} # IDLE


			elsif( $PARSE_STATE eq $STATE{ SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD,
						$PARSE_STATE );
				}
			} # SEMICOLON


			elsif( $PARSE_STATE eq $STATE{ OPT } ) {
				if( $WORD eq "{" ) {
					$PARSE_STATE = $STATE{ OPT_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., "{", $WORD, $PARSE_STATE );
				}
			} # OPT


			elsif( $PARSE_STATE eq $STATE{ OPT_IDLE } ) {
				# Try a translation in case this directive has been
				# abbreveated.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
				# Are we closing out the options section?
				#
				if( $WORD eq "}" ) {
					$PARSE_STATE = $STATE{ SEMICOLON };
				}
				# No?  Then we must be getting a useful options directive.
				#
				elsif( lc( $WORD ) eq "sizemod" ) {
					$PARSE_STATE = $STATE{ OPT_SIZEMOD };
				} # sizemod
				elsif( lc( $WORD ) eq "sizelogic" ) {
					$PARSE_STATE = $STATE{ OPT_SIZELOGIC };
				} # sizelogic
				elsif( lc( $WORD ) eq "archivedir" ) {
					$PARSE_STATE = $STATE{ OPT_ARCHIVEDIR };
				} # archivedir
				elsif( lc( $WORD ) eq "owner" ) {
					$PARSE_STATE = $STATE{ OPT_OWNER };
				} # owner
				elsif( lc( $WORD ) eq "group" ) {
					$PARSE_STATE = $STATE{ OPT_GROUP };
				} # group
				elsif( lc( $WORD ) eq "mode" ) {
					$PARSE_STATE = $STATE{ OPT_MODE };
				} # mode
				elsif( lc( $WORD ) eq "keep" ) {
					$PARSE_STATE = $STATE{ OPT_KEEP };
				} # keep
				elsif( lc( $WORD ) eq "size" ) {
					$PARSE_STATE = $STATE{ OPT_SIZE };
				} # size
				elsif( lc( $WORD ) eq "flags" ) {
					$PARSE_STATE = $STATE{ OPT_FLAGS };
				} # flags
				elsif( lc( $WORD ) eq "pid" ) {
					$PARSE_STATE = $STATE{ OPT_PID };
				} # pid
				elsif( lc( $WORD ) eq "signal" ) {
					$PARSE_STATE = $STATE{ OPT_SIGNAL };
				} # signal
				elsif( lc( $WORD ) eq "index" ) {
					$PARSE_STATE = $STATE{ OPT_INDEX };
				} # index
				elsif( lc( $WORD ) eq "date" ) {
					$PARSE_STATE = $STATE{ OPT_DATE };
				} # date
				else {
					# Have we at least got a valid command?
					#
					unless( exists( $COMMAND{ lc( $WORD ) } ) ) {
						err( 'UNR_CMD', $CONFIG, $., $WORD,
							$PARSE_STATE );
						next WORD;
					}
					err( 'UNR_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
				}
			} # OPT_IDLE


			elsif( $PARSE_STATE eq $STATE{ OPT_SIZELOGIC } ) {
				$OPT_PTR->{ SIZELOGIC } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set option %s to %s", 'SIZELOGIC', $WORD );
			} # OPT_SIZELOGIC


			elsif( $PARSE_STATE eq $STATE{ OPT_SIZEMOD } ) {
				if( exists( $MULTIPLIER{ uc($WORD) } )) {
					$OPT_PTR->{ SIZEMOD } = uc($WORD);
					debug( "set option %s to %s", 'SIZEMOD', uc($WORD) );
				} else {
					err( 'UNR_VAL',$CONFIG,$.,$WORD,'SIZEMOD' );
				}
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
			} # OPT_SIZELOGIC


			elsif( $PARSE_STATE eq $STATE{ OPT_ARCHIVEDIR } ) {
				$OPT_PTR->{ ARCHIVEDIR } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'ARCHIVEDIR', $WORD );
			} # OPT_ARCHIVEDIR


			elsif( $PARSE_STATE eq $STATE{ OPT_OWNER } ) {
				$OPT_PTR->{ OWNER } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'OWNER', $WORD );
			} # OPT_OWNER


			elsif( $PARSE_STATE eq $STATE{ OPT_GROUP } ) {
				$OPT_PTR->{ GROUP } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'GROUP', $WORD );
			} # OPT_GROUP


			elsif( $PARSE_STATE eq $STATE{ OPT_MODE } ) {
				$OPT_PTR->{ MODE } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'MODE', $WORD );
			} # OPT_MODE


			elsif( $PARSE_STATE eq $STATE{ OPT_KEEP } ) {
				$OPT_PTR->{ KEEP } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'KEEP', $WORD );
			} # OPT_KEEP


			elsif( $PARSE_STATE eq $STATE{ OPT_SIZE } ) {
				$OPT_PTR->{ SIZE } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'SIZE', $WORD );
			} # OPT_SIZE


			elsif( $PARSE_STATE eq $STATE{ OPT_FLAGS } ) {
				$OPT_PTR->{ FLAGS } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'FLAGS', $WORD );
			} # OPT_FLAGS


			elsif( $PARSE_STATE eq $STATE{ OPT_PID } ) {
				$OPT_PTR->{ PID } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'PID', $WORD );
			} # OPT_PID


			elsif( $PARSE_STATE eq $STATE{ OPT_SIGNAL } ) {
				$OPT_PTR->{ SIGNAL } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'SIGNAL', $WORD );
			} # OPT_SIGNAL


			elsif( $PARSE_STATE eq $STATE{ OPT_INDEX } ) {
				$OPT_PTR->{ INDEX } = $WORD;
				$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				debug( "set default option %s to %s", 'INDEX', $WORD );
			} # OPT_INDEX


			elsif( $PARSE_STATE eq $STATE{ OPT_DATE } ) {
				if( $WORD eq "{" ) {
					# Commented this out... has the effect that as soon as
					# even an empty DATE {} structure appears in the config
					# file, the date hash is created for the current logfile.
					# This ends up causing the date/time to be taken into
					# account, even if no date/time directives are defined.
					# This is a bit redundant, since if no date/time
					# directives are defined they will match all dates/times
					# anyway.  It's better to leave the hash to be defined
					# when one of the directives are first defined.
					#
					# $LOG_PTR->{ $LOG }{ DATE } = { };
					$PARSE_STATE = $STATE{ OPT_DATE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., "{", $WORD, $PARSE_STATE );
				}
			} # OPT_DATE


			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_IDLE } ) {
				# Try a translation in case this directive has been
				# abbreveated.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
				# Are we closing out the date section?
				#
				if( $WORD eq "}" ) {
					$PARSE_STATE = $STATE{ OPT_SEMICOLON };
				}
				# No?  Then we must be getting a useful log directive.
				#
				elsif( lc( $WORD ) eq "minute" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_MINUTE };
				} # minute
				elsif( lc( $WORD ) eq "hour" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_HOUR };
				} # hour
				elsif( lc( $WORD ) eq "dom" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_DOM };
				} # dom
				elsif( lc( $WORD ) eq "month" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_MONTH };
				} # month
				elsif( lc( $WORD ) eq "dow" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_DOW };
				} # dow
				else {
					# Have we at least got a valid command?
					#
					unless( exists( $COMMAND{ lc( $WORD ) } ) ) {
						err( 'UNR_CMD', $CONFIG, $., $WORD, $PARSE_STATE );
						next WORD;
					}
					err( 'UNR_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
				}
			} # OPT_DATE_IDLE
			

			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_MINUTE } ) {
				$OPT_PTR->{ DATE }{ MINUTE } = $WORD;
				$PARSE_STATE = $STATE{ OPT_DATE_SEMICOLON };
				debug( "set default option %s to %s", 'MINUTE', $WORD );
			} # OPT_DATE_MINUTE
			

			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_HOUR } ) {
				$OPT_PTR->{ DATE }{ HOUR } = $WORD;
				$PARSE_STATE = $STATE{ OPT_DATE_SEMICOLON };
				debug( "set default option %s to %s", 'HOUR', $WORD );
			} # OPT_DATE_HOUR
			

			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_DOM } ) {
				$OPT_PTR->{ DATE }{ DOM } = $WORD;
				$PARSE_STATE = $STATE{ OPT_DATE_SEMICOLON };
				debug( "set default option %s to %s", 'DOM', $WORD );
			} # OPT_DATE_DOM
			

			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_MONTH } ) {
				$OPT_PTR->{ DATE }{ MONTH } = $WORD;
				$PARSE_STATE = $STATE{ OPT_DATE_SEMICOLON };
				debug( "set default option %s to %s", 'MONTH', $WORD );
			} # OPT_DATE_MONTH
			

			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_DOW } ) {
				$OPT_PTR->{ DATE }{ DOW } = $WORD;
				$PARSE_STATE = $STATE{ OPT_DATE_SEMICOLON };
				debug( "set default option %s to %s", 'DOW', $WORD );
			} # OPT_DATE_DOW


			elsif( $PARSE_STATE eq $STATE{ OPT_DATE_SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ OPT_DATE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD, $PARSE_STATE );
				}
			} # OPT_DATE_SEMICOLON


			elsif( $PARSE_STATE eq $STATE{ OPT_SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ OPT_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD,
						$PARSE_STATE );
				}
			} # OPT_SEMICOLON


			elsif( $PARSE_STATE eq $STATE{ INCLUDE } ) {
				if( $WORD eq "{" ) {
					$PARSE_STATE = $STATE{ INCLUDE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., "{", $WORD, $PARSE_STATE );
				}
			} # INCLUDE


			elsif( $PARSE_STATE eq $STATE{ INCLUDE_IDLE } ) {
				# Try a translation in case this directive has been
				# abbreveated.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
				# Are we closing out the include section?
				#
				if( $WORD eq "}" ) {
					$PARSE_STATE = $STATE{ SEMICOLON };
				} else {
					# No?  Then we must be getting a file name.
					# $WORD will be the full path to the file, we hope.
					#
					debug( "including config file \'%s\'", $WORD );

					unless( parse( $WORD, $OPT_PTR, $LOG_PTR, $FH )) {
						err( 'INC_PAR', $CONFIG, $., $WORD );
					}
					$PARSE_STATE = $STATE{ INCLUDE_SEMICOLON };
				}

			} # INCLUDE_IDLE

			elsif( $PARSE_STATE eq $STATE{ INCLUDE_SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ INCLUDE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD,
						$PARSE_STATE );
				}
			} # INCLUDE_SEMICOLON



			elsif( $PARSE_STATE eq $STATE{ LOG } ) {
				$LOG = $WORD;
				$PARSE_STATE = $STATE{ LOG_OPEN };
				debug( "configuring logfile %s", $WORD );
				if( exists( $OPT_PTR->{ ARCHIVEDIR } )) {
					$LOG_PTR->{ $LOG }{ ARCHIVEDIR } = $OPT_PTR->{ ARCHIVEDIR };
				} # archivedir
				if( exists( $OPT_PTR->{ OWNER } )) {
					$LOG_PTR->{ $LOG }{ OWNER } = $OPT_PTR->{ OWNER };
				} # owner
				if( exists( $OPT_PTR->{ GROUP } )) {
					$LOG_PTR->{ $LOG }{ GROUP } = $OPT_PTR->{ GROUP };
				} # group
				if( exists( $OPT_PTR->{ MODE } )) {
					$LOG_PTR->{ $LOG }{ MODE } = $OPT_PTR->{ MODE };
				} # mode
				if( exists( $OPT_PTR->{ KEEP } )) {
					$LOG_PTR->{ $LOG }{ KEEP } = $OPT_PTR->{ KEEP };
				} # keep
				if( exists( $OPT_PTR->{ SIZE } )) {
					$LOG_PTR->{ $LOG }{ SIZE } = $OPT_PTR->{ SIZE };
				} # size
				if( exists( $OPT_PTR->{ FLAGS } )) {
					$LOG_PTR->{ $LOG }{ FLAGS } = $OPT_PTR->{ FLAGS };
				} # flags
				if( exists( $OPT_PTR->{ PID } )) {
					$LOG_PTR->{ $LOG }{ PID } = $OPT_PTR->{ PID };
				} # pid
				if( exists( $OPT_PTR->{ SIGNAL } )) {
					$LOG_PTR->{ $LOG }{ SIGNAL } = $OPT_PTR->{ SIGNAL };
				} # signal
				if( exists( $OPT_PTR->{ INDEX } )) {
					$LOG_PTR->{ $LOG }{ INDEX } = $OPT_PTR->{ INDEX };
				} # index
				if( exists( $OPT_PTR->{ DATE } )) {
					if( exists( $OPT_PTR->{ DATE }{ MINUTE } )) {
						$LOG_PTR->{ $LOG }{ DATE }{ MINUTE } = 
							$OPT_PTR->{ DATE }{ MINUTE };
					} # minute
					if( exists( $OPT_PTR->{ DATE }{ HOUR } )) {
						$LOG_PTR->{ $LOG }{ DATE }{ HOUR } = 
							$OPT_PTR->{ DATE }{ HOUR };
					} # hour
					if( exists( $OPT_PTR->{ DATE }{ DOM } )) {
						$LOG_PTR->{ $LOG }{ DATE }{ DOM } = 
							$OPT_PTR->{ DATE }{ DOM };
					} # dom
					if( exists( $OPT_PTR->{ DATE }{ MONTH } )) {
						$LOG_PTR->{ $LOG }{ DATE }{ MONTH } = 
							$OPT_PTR->{ DATE }{ MONTH };
					} # month
					if( exists( $OPT_PTR->{ DATE }{ DOW } )) {
						$LOG_PTR->{ $LOG }{ DATE }{ DOW } = 
							$OPT_PTR->{ DATE }{ DOW };
					} # dow
				} # date
			} # LOG


			elsif( $PARSE_STATE eq $STATE{ LOG_OPEN } ) {
				if( $WORD eq "{" ) {
					$PARSE_STATE = $STATE{ LOG_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., "{", $WORD,
						$PARSE_STATE );
				}
			} # LOG_OPEN


			elsif( $PARSE_STATE eq $STATE{ LOG_IDLE } ) {
				# Try a translation in case this directive has been
				# abbreveated.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
				# Are we closing out the log section?
				#
				if( $WORD eq "}" ) {
					$PARSE_STATE = $STATE{ SEMICOLON };
				}
				# No?  Then we must be getting a useful log directive.
				#
				elsif( lc( $WORD ) eq "archivedir" ) {
					$PARSE_STATE = $STATE{ LOG_ARCHIVEDIR };
				} # archivedir
				elsif( lc( $WORD ) eq "owner" ) {
					$PARSE_STATE = $STATE{ LOG_OWNER };
				} # owner
				elsif( lc( $WORD ) eq "group" ) {
					$PARSE_STATE = $STATE{ LOG_GROUP };
				} # group
				elsif( lc( $WORD ) eq "mode" ) {
					$PARSE_STATE = $STATE{ LOG_MODE };
				} # mode
				elsif( lc( $WORD ) eq "keep" ) {
					$PARSE_STATE = $STATE{ LOG_KEEP };
				} # keep
				elsif( lc( $WORD ) eq "size" ) {
					$PARSE_STATE = $STATE{ LOG_SIZE };
				} # size
				elsif( lc( $WORD ) eq "date" ) {
					$PARSE_STATE = $STATE{ LOG_DATE };
				} # date
				elsif( lc( $WORD ) eq "flags" ) {
					$PARSE_STATE = $STATE{ LOG_FLAGS };
					$LOG_PTR->{ $LOG }{ FLAGS } = '';
				} # flags
				elsif( lc( $WORD ) eq "pid" ) {
					$PARSE_STATE = $STATE{ LOG_PID };
				} # pid
				elsif( lc( $WORD ) eq "signal" ) {
					$PARSE_STATE = $STATE{ LOG_SIGNAL };
				} # signal
				elsif( lc( $WORD ) eq "index" ) {
					$PARSE_STATE = $STATE{ LOG_INDEX };
				} # index
				else {
					# Have we at least got a valid command?
					#
					unless( exists( $COMMAND{ lc( $WORD ) } ) ) {
						err( 'UNR_CMD', $CONFIG, $., $WORD,
							$PARSE_STATE );
						next WORD;
					}
					err( 'UNR_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
				}
			} # LOG_IDLE


			elsif( $PARSE_STATE eq $STATE{ LOG_ARCHIVEDIR } ) {
				$LOG_PTR->{ $LOG }{ ARCHIVEDIR } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'ARCHIVEDIR', $WORD );
			} # LOG_ARCHIVEDIR


			elsif( $PARSE_STATE eq $STATE{ LOG_OWNER } ) {
				$LOG_PTR->{ $LOG }{ OWNER } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'OWNER', $WORD );
			} # LOG_OWNER
			

			elsif( $PARSE_STATE eq $STATE{ LOG_GROUP } ) {
				$LOG_PTR->{ $LOG }{ GROUP } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'GROUP', $WORD );
			} # LOG_GROUP


			elsif( $PARSE_STATE eq $STATE{ LOG_MODE } ) {
				$LOG_PTR->{ $LOG }{ MODE } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'MODE', $WORD );
			} # LOG_MODE


			elsif( $PARSE_STATE eq $STATE{ LOG_KEEP } ) {
				$LOG_PTR->{ $LOG }{ KEEP } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'KEEP', $WORD );
			} # LOG_KEEP


			elsif( $PARSE_STATE eq $STATE{ LOG_SIZE } ) {
				$LOG_PTR->{ $LOG }{ SIZE } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'SIZE', $WORD );
			} # LOG_SIZE


			elsif( $PARSE_STATE eq $STATE{ LOG_DATE } ) {
				if( $WORD eq "{" ) {
					# Commented this out... has the effect that as soon as
					# even an empty DATE {} structure appears in the config
					# file, the date hash is created for the current logfile.
					# This ends up causing the date/time to be taken into
					# account, even if no date/time directives are defined.
					# This is a bit redundant, since if no date/time
					# directives are defined they will match all dates/times
					# anyway.  It's better to leave the hash to be defined
					# when one of the directives are first defined.
					#
					# $LOG_PTR->{ $LOG }{ DATE } = { };
					$PARSE_STATE = $STATE{ LOG_DATE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., "{", $WORD, $PARSE_STATE );
				}
			} # LOG_DATE


			elsif( $PARSE_STATE eq $STATE{ LOG_FLAGS } ) {
				# Are we finished setting flags?
				#
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ LOG_IDLE };
				} else {
					# Nope?  We must be adding a new flag.
					#
					$LOG_PTR->{ $LOG }{ FLAGS } .= $WORD;
					debug( "log %s: set flag %s", $LOG, $WORD );
				}
			} # LOG_FLAGS


			elsif( $PARSE_STATE eq $STATE{ LOG_PID } ) {
				$LOG_PTR->{ $LOG }{ PID } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'PID', $WORD );
			} # LOG_PID


			elsif( $PARSE_STATE eq $STATE{ LOG_SIGNAL } ) {
				$LOG_PTR->{ $LOG }{ SIGNAL } = $WORD;
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'SIGNAL', $WORD );
			} # LOG_SIGNAL


			elsif( $PARSE_STATE eq $STATE{ LOG_INDEX } ) {
				if ( lc( $WORD ) =~ m/^(date|ordinal)$/ ) {
					$LOG_PTR->{ $LOG }{ INDEX } = $WORD;
					debug( "log %s: set %s to %s", $LOG, 'INDEX', $WORD );
				} else {
					err( "UNR_VAL", $CONFIG, $., $WORD, 'INDEX' );
				}
				$PARSE_STATE = $STATE{ LOG_SEMICOLON };
			} # LOG_INDEX


			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_IDLE } ) {
				# Try a translation in case this directive has been
				# abbreveated.
				#
				if( exists( $ABBREV{ lc( $WORD ) } ) ) {
					$WORD = $ABBREV{ lc( $WORD ) };
				}
				# Are we closing out the date section?
				#
				if( $WORD eq "}" ) {
					$PARSE_STATE = $STATE{ LOG_SEMICOLON };
				}
				# No?  Then we must be getting a useful log directive.
				#
				elsif( lc( $WORD ) eq "minute" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_MINUTE };
				} # minute
				elsif( lc( $WORD ) eq "hour" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_HOUR };
				} # hour
				elsif( lc( $WORD ) eq "dom" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_DOM };
				} # dom
				elsif( lc( $WORD ) eq "month" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_MONTH };
				} # month
				elsif( lc( $WORD ) eq "dow" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_DOW };
				} # dow
				else {
					# Have we at least got a valid command?
					#
					unless( exists( $COMMAND{ lc( $WORD ) } ) ) {
						err( 'UNR_CMD', $CONFIG, $., $WORD, $PARSE_STATE );
						next WORD;
					}
					err( 'UNR_CMD',$CONFIG,$.,$WORD,$PARSE_STATE );
				}
			} # LOG_DATE_IDLE
			

			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_MINUTE } ) {
				$LOG_PTR->{ $LOG }{ DATE }{ MINUTE } = $WORD;
				$PARSE_STATE = $STATE{ LOG_DATE_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'MINUTE', $WORD );
			} # LOG_DATE_MINUTE
			

			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_HOUR } ) {
				$LOG_PTR->{ $LOG }{ DATE }{ HOUR } = $WORD;
				$PARSE_STATE = $STATE{ LOG_DATE_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'HOUR', $WORD );
			} # LOG_DATE_HOUR
			

			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_DOM } ) {
				$LOG_PTR->{ $LOG }{ DATE }{ DOM } = $WORD;
				$PARSE_STATE = $STATE{ LOG_DATE_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'DOM', $WORD );
			} # LOG_DATE_DOM
			

			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_MONTH } ) {
				$LOG_PTR->{ $LOG }{ DATE }{ MONTH } = $WORD;
				$PARSE_STATE = $STATE{ LOG_DATE_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'MONTH', $WORD );
			} # LOG_DATE_MONTH
			

			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_DOW } ) {
				$LOG_PTR->{ $LOG }{ DATE }{ DOW } = $WORD;
				$PARSE_STATE = $STATE{ LOG_DATE_SEMICOLON };
				debug( "log %s: set %s to %s", $LOG, 'DOW', $WORD );
			} # LOG_DATE_DOW


			elsif( $PARSE_STATE eq $STATE{ LOG_DATE_SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ LOG_DATE_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD, $PARSE_STATE );
				}
			} # LOG_DATE_SEMICOLON


			elsif( $PARSE_STATE eq $STATE{ LOG_SEMICOLON } ) {
				if( $WORD eq ";" ) {
					$PARSE_STATE = $STATE{ LOG_IDLE };
				} else {
					err( 'UNX_WRD', $CONFIG, $., ";", $WORD, $PARSE_STATE );
				}
			} # LOG_SEMICOLON


			else {

				err( 'UNR_STA', $CONFIG, $., $PARSE_STATE );
			}
		
		} # WORD

	} # LINE


	unless( $PARSE_STATE eq $STATE{ IDLE } ) {

		err( 'UNX_EOF', $CONFIG, $., $PARSE_STATE );
	}

	close( "$FH" );
	debug( "finished parsing config file \'$CONFIG\'" );

	return( 1 );
} # sub parse


sub dump_config {
	my( $OPT_PTR, $LOG_PTR ) = ( @_ );
	my( $LOG, $MAJOR, $MINOR );
	
	if( keys( %$OPT_PTR )) {
		print "OPTIONS {\n";
		foreach $MAJOR ( keys %$OPT_PTR ) {
			if( ref( $OPT_PTR->{ $MAJOR } )) {
				print "\t$MAJOR {\n";
				foreach $MINOR ( keys( %{ $OPT_PTR->{ $MAJOR }})) {
					print "\t\t$MINOR $OPT_PTR->{ $MAJOR }{ $MINOR };\n";
				} # foreach $MINOR
				print "\t};\n";
			} else { # if ref $MAJOR
				print "\t$MAJOR $OPT_PTR->{ $MAJOR };\n";
			} # if ref $MAJOR
		} # foreach $MAJOR
		print "};\n\n";
	} # if keys %$OPT_PTR

	foreach $LOG ( keys( %$LOG_PTR )) {
		
		print "LOG $LOG {\n";

		foreach $MAJOR ( keys( %{ $LOG_PTR->{ $LOG }} )) {

			if( ref( $LOG_PTR->{ $LOG }{ $MAJOR } )) {
				print "\t$MAJOR {\n";
				foreach $MINOR ( keys %{ $LOG_PTR->{ $LOG }{ $MAJOR }} ) {
					print "\t\t$MINOR $LOG_PTR->{$LOG}{$MAJOR}{$MINOR};\n";
				} # foreach $MINOR
				print "\t};\n";
			} else { # if ref $MAJOR
				print "\t$MAJOR $LOG_PTR->{ $LOG }{ $MAJOR };\n";
			} # if ref $MAJOR
		} # foreach $MAJOR
		print "};\n\n";
	} # keys %$LOG_PTR
} # sub dump_config


sub match_date {

	my( $dateRef ) = ( @_ );

	my( $type, $value, @NewList, $iter );

	# Cycle through each segment of the date hash.
	#
	foreach $type ( 'MINUTE', 'HOUR', 'DOM', 'MONTH', 'DOW' ) {

		@NewList = ();

		# If this particular type isn't defined, that's as good as setting it
		# to a *, so do that.
		#
		unless( exists( $dateRef->{ $type } ) ) {
			$dateRef->{ $type } = $RANGE{ $type };
		}

		foreach $value ( split /,/, $dateRef->{ $type } ) {

			# Replace any *'s with the full range for the date type.
			#
			$value =~ s/\*/$RANGE{ $type }/;

			# Convert any ranges with step values first.
			#
			if( $value =~ m/^(\d+)-(\d+)\/(\d+)$/ ) {
				for( $iter = $1; $iter <= $2; $iter += $3 ) {
					push( @NewList, $iter );
				}
			}
			# Now take care of any remaining ranges.
			#
			elsif( $value =~ m/^(\d+)-(\d+)$/ ) {
				push( @NewList, ($1..$2) );
			}
			# And finally single values.
			#
			elsif( $value =~ m/^(\d+)$/ ) {
				push( @NewList, $1 );
			}
			# Whatever else is left is an invalid type.
			#
			else {
				err( 'UNR_DVA', $value, $type );
			}
		}

		# Pack it all back together into a comma separated list.
		#
		$dateRef->{ $type } = join( ",", @NewList );
	}

	# Next, take care of special values.  
	#
	# A 0 for Day Of the Week needs to be converted to a 7 (both are Sunday).
	#
	$dateRef->{ DOW } =~ s/0+(,|$)/7$1/g;
	# A 0 in the Day Of the Month field needs to be replaced with the last day
	# of this month.  We have to run this twice because this regex has
	# problems with sequential zeros.
	#
	$dateRef->{ DOM } =~
		s/(^|,)0+(,|$)/$1$MONTH{strftime("%m",localtime($NOW))}$2/g;
	$dateRef->{ DOM } =~
		s/(^|,)0+(,|$)/$1$MONTH{strftime("%m",localtime($NOW))}$2/g;

	# Now that we've expanded all the ranges and step values, we can see if
	# the current time matches what we've got.
	#
	if(
		( grep { strftime( "%M", localtime($NOW) ) == int( $_ ) }
			split( ",", $dateRef->{ MINUTE } ) )
		&&
		( grep { strftime( "%H", localtime($NOW) ) == int( $_ ) }
			split( ",", $dateRef->{ HOUR } ) )
		&&
		( grep { strftime( "%u", localtime($NOW) ) == int( $_ ) }
			split( ",", $dateRef->{ DOW } ) )
		&&
		( grep { strftime( "%d", localtime($NOW) ) == int( $_ ) }
			split( ",", $dateRef->{ DOM } ) )
		&&
		( grep { strftime( "%m", localtime($NOW) ) == int( $_ ) }
			split( ",", $dateRef->{ MONTH } ) )
	) {
		return( 1 );
	}

	return( 0 );

}

sub rotate {

	my( $logfile, $logConfRef ) = ( @_ );

	my( $alph ) = join( "", ('0'..'9','a'..'z','A'..'Z' ));
	
	my( $rnd, $tmpSuffix, @fstat, @pwnam, $pid, $signal, $archiveDir );
	my( $baseLogName, $mode, $lastLog, $suffix, $lastLogSuffix );
	my( $suffixLength, $dateSuffix, $file, @archives, $newArchive );
	my( $newIndex );

	# Generate five random characters for a filename suffix.  We'll use this
	# as a place to move the logfile to, before moving it to .0 later.
	#
	foreach( '1'..'5' ) {
		$rnd   = int(rand ( length( $alph ) - 1 ) );
		$tmpSuffix .= substr( $alph, ++$rnd, 1 );
	}

	unless( -f $logfile ) {
		# The logfile isn't there for us to rotate it.
		#
		err( "tried to rotate %s but the file was unexpectedly missing.",
			$logfile );
		return( 0 );
	}

	# Find out what the base logfile name is. We need this later.
	#
	( $baseLogName = $logfile ) =~ s/^.*\/([^\/]+)/$1/;

	while( -f "$logfile.$tmpSuffix" ) {
		# The file.suffix already exists, so we'll keep tagging on to the
		# suffix until we have a unique filename.
		#
		$rnd   = int(rand ( length( $alph ) - 1 ) );
		$tmpSuffix .= substr( $alph, ++$rnd, 1 );

		if( length( $tmpSuffix ) > $tmpSuffix_max_length ) {

			# We can't get a tempfile for this logfile.  It's highly likely
			# someone's being naughty.  Return and error.
			#
			err( "cant create tempfile for %s", $logfile );
			return( 0 );
		}
	}

	# Grab a stat of the logfile -- we'll need the mode, owner and group
	# later.
	#
	@fstat = stat( $logfile );

	# Link the logfile to the tmpfile, then remove the original link.  This
	# accomplishes a move
	#
	unless( link( $logfile, "$logfile.$tmpSuffix" ) ) {
		err( "couldnt link %s to %s: %s", $logfile, "$logfile.$tmpSuffix", $! );
		return( 0 );
	}
	unless( unlink( $logfile ) ) {
		err( "couldnt remove link %s: %s", $logfile, $! );
		return( 0 );
	}
	
	# If this is not flagged as a binary file, then we'll drop in a log entry
	# noting that we've rotated the file.
	#
	unless( $logConfRef->{ FLAGS } =~ m/B/ ) {
		chmod( 0600, "$logfile.$tmpSuffix" );
		unless( open( FH, ">>$logfile.$tmpSuffix" ) ) {
			err( "couldnt open file %s for writing: %s",
				"$logfile.$tmpSuffix", $! );
			err( "couldnt log rotation for %s", "$logfile.$tmpSuffix" );
		} else {
	
			printf( FH "%s %s $ME\[%d\]: $rotate_msg\n", 
				strftime( "%b %d %H:%M:%S", localtime($NOW)),
				$hostname, $$ );
			close( FH );
		}
		unless( open( FH, ">>$logfile" ) ) {
			err( "couldnt open file %s for writing: %s", $logfile, $! );
			err( "couldnt log rotation for %s", $logfile );
		} else {
	
			printf( FH "%s %s $ME\[%d\]: $rotate_msg\n", 
				strftime( "%b %d %H:%M:%S", localtime($NOW)),
				$hostname, $$ );
			close( FH );
		}
	}

	# If the logfile doesn't exist now, create it.  (It might already exist
	# because we put a note in it.)
	#
	unless( -f $logfile ) {

		unless( open( FH, ">>$logfile" ) ) {
			err( "couldnt create file %s: %s", $logfile, $! );
		} else {
			close( FH );
		}
	}

	# Set the owner and permissiong back to the way they used to be.
	#
	if( ! chown( $fstat[4], $fstat[5], $logfile ) ) {
	
		err( "couldnt set ownership of %s to %s.%s: %s",
			$logfile, $fstat[4], $fstat[5], $!);
	} elsif( ! chmod( $fstat[2], $logfile ) ) {

		err( "couldnt set mode of %s to %o: %s",
			$logfile, $fstat[2], $!);

	}

	# If a pid file is set for this logfile, and the pid file exists, signal
	# that PID now.
	#
	SIGNAL:
	{
	if( exists( $logConfRef->{ PID } )) {

		unless( open( FH, $logConfRef->{ PID } )) {
			err( "couldnt open PID file %s for reading: %s",
				$logConfRef->{ PID }, $! );
			last SIGNAL;
		}
		$pid = <FH>;
		close( FH );

		$pid =~ s/^\s*(\d+)\s*/$1/;
		$signal = $logConfRef->{ SIGNAL } ? $logConfRef->{ SIGNAL } : 'HUP';

		if( kill( $signal, int( $pid ))) {
			$logConfRef->{ NOTIFIED } = 1;
		} else {
			err( "cant notify daemon, pid %d: %s", int( $pid ), $!);
		}

	} # IF EXISTS
	} # SIGNAL

	
	# To do the rotation of the archive files, we need to figure out where
	# archives are supposed to be.
	# Our default is to use the same directory as the active logfile.
	#
	( $archiveDir = $logfile ) =~ s/^(.*\/)[^\/]+/$1/;
	if( exists( $logConfRef->{ ARCHIVEDIR } )) {
		if( -d $logConfRef->{ ARCHIVEDIR } ) {
			$archiveDir = $logConfRef->{ ARCHIVEDIR };
		} else {
			err( "archivedir %s doesnt exist, using default archivedir",
				$logConfRef->{ ARCHIVEDIR } );
		}
	}

	# We have to do some different preliminary work here, depending on whether
	# this archive gets a date-based or ordinal index.
	#
	if( $logConfRef->{ INDEX } eq 'date' ) {
		
		# Build a date-string to use on today's archive name.
		#
		$dateSuffix = strftime( '%Y%m%d', localtime($NOW) );

		# Look to see what the last archive is from today (if there are any).
		# This will determine our index number on the archive.
		#
		unless( opendir( DH, $archiveDir )) {
			err( "failed to open archivedir %s: %s", $archiveDir, $! );
			err( "rotation of %s failed", $logfile );
			return( 0 );
		}
		while( $file = readdir( DH )) {
			if($file =~ m/^$baseLogName\.$dateSuffix-(\d+)(\.$compSuffix)?$/) {
				verbose("found log archive %s to rotate", $file);
				push( @archives, $file );
			}
		}
		closedir( DH );
		@archives = sort @archives;

		# What is our new index number for today?
		#
		if(scalar( @archives )) {
			$lastLog = $archives[ $#archives ];
			($lastLogSuffix=$lastLog) =~ s/^.*\.\d{8}-(\d+)(\.$compSuffix)?/$1/;
			$lastLogSuffix = int( $lastLogSuffix);
			$logConfRef->{ INDEX_LENGTH } = length( int($lastLogSuffix) + 1 );

			$suffix = $lastLogSuffix + 1;
		} else {
			$suffix = 0;
		}

		# How many digits is the new index?  Does it have more digits than the
		# last index?  If so, we have to rename all our previous archived logs
		# from today.
		#
		$logConfRef->{ INDEX_LENGTH } = length( int($suffix));
		if( $suffix > 0 &&
			$logConfRef->{ INDEX_LENGTH } > length(int($suffix)-1)) {
			
			verbose( "index crossed new power of ten; renaming todays archives" );
			while( $file = pop @archives ) {

				# Get a new index with enough digits.
				#
				$file =~ m/^($baseLogName\.$dateSuffix-)(\d+)(\.$compSuffix)?$/;
				$newIndex = $2;
				while( length( $newIndex ) < $logConfRef->{ INDEX_LENGTH } ) {
					$newIndex = '0' . $newIndex;
				}

				# Move to the new filename
				#
				unless( link( "$archiveDir/$file",
					"$archiveDir/$1$newIndex$3")) {

					err( "couldnt link \'%s\' to \'%s\'",
						"$archiveDir/$file",
						"$archiveDir/$1$newIndex$3" );
				}
				unlink( "$archiveDir/$file" );

			}
		}
		while( length($suffix)< $logConfRef->{ INDEX_LENGTH }) {
			$suffix = '0' . $suffix;
		}

		# Define the name of the archive file we're creating.
		#
		$newArchive = "$baseLogName.$dateSuffix-$suffix";

	} else {
			
		# Get a list of the archives we've got to rotate.
		#
		unless( opendir( DH, $archiveDir )) {
			err( "failed to open archivedir %s: %s", $archiveDir, $! );
			err( "rotation of %s failed", $logfile );
			return( 0 );
		}
		while( $file = readdir(DH) ) {
			if( $file =~ m/^$baseLogName\.\d+(\.$compSuffix)?$/ ) {
				verbose("found log archive %s to rotate", $file);
				push( @archives, $file );
			}
		}
		closedir( DH );
		@archives = sort log_sort @archives;

		# Find out how many digits should be in the new logfile suffix.
		#
		$lastLog = $archives[ $#archives ];
		($lastLogSuffix = $lastLog) =~ s/^.*\.(\d+)(\.$compSuffix)?/$1/;
		$lastLogSuffix = int( $lastLogSuffix);
		$logConfRef->{ INDEX_LENGTH } = length( int($lastLogSuffix) + 1 );

		# loop through each of the old archives, renaming them to the
		# next-oldest file name.
		#
		while( $file = pop @archives) {

			# Get the suffix from this file.
			#
			( $suffix = $file ) =~ s/^.*\.(\d+)(\.$compSuffix)?/$1/;
			$suffix = int( ++$suffix );

			# Make sure the new suffix has enough digits.
			#
			while( length( $suffix ) < $logConfRef->{ INDEX_LENGTH } ) {
				$suffix = '0' . $suffix;
			}
			# Move to the new filename
			#
			unless( link( "$archiveDir/$file",
				"$archiveDir/$baseLogName.$suffix" .
				(( $file =~ m/\.$compSuffix$/ ) ? ".$compSuffix" : "" ))) {
				
				err( "couldnt link $archiveDir/$file to " .
				"$archiveDir/$baseLogName.$suffix" .
				(( $file =~ m/\.$compSuffix$/ ) ? ".$compSuffix" : "" )
					. ": $!");

				return( 0 );
			}
			unlink( "$archiveDir/$file" );
		}
		
		# Define the suffix for the 'current' logfile.
		#
		$suffix = 0;

		# Make sure the new suffix has enough digits.
		#
		while( length( $suffix ) < $logConfRef->{ INDEX_LENGTH } ) {
			$suffix = '0' . $suffix;
		}

		# Define the name of the archive file we're creating.
		#
		$newArchive = "$baseLogName.$suffix";

	} #endif $logConfRef->{ INDEX } eq 'date'

	# We have to find out if the tmpfile and the archive directory are on the
	# same filesystem.  This tells us whether to do a move (link/unlink) or
	# copy of the tmpfile.
	#
	if( (stat( "$logfile.$tmpSuffix" ))[0] == (stat($archiveDir))[0] ) {
		# Same filesystem.. do a move
		#
		unless( link( "$logfile.$tmpSuffix",
			"$archiveDir/$newArchive" )) {

			err( "couldnt link %s to %s: %s",
				"$logfile.$tmpSuffix", "$archiveDir/$newArchive", $! );
			return( 0 );
		}
	} else {
		# different filesystems.  Do a copy.
		#
		unless( copy( "$logfile.$tmpSuffix",
			"$archiveDir/$newArchive" )) {

			err( "couldnt copy %s to %s: %s",
				"$logfile.$tmpSuffix", "$archiveDir/$newArchive", $! );
			return( 0 );
		}
	}
	unlink( "$logfile.$tmpSuffix" ) or
		err( "couldnt unlink %s: %s", "$logfile.$tmpSuffix", $! );
	
	# Set the owner and permissions of the new archive file
	#
	if( exists( $logConfRef->{ OWNER } )) {
		unless( $logConfRef->{ OWNER } =~ m/^\d+$/ ) {
			
			@pwnam = getpwnam( $logConfRef->{ OWNER });

			if( @pwnam ) {
				$logConfRef->{ OWNER } = $pwnam[2];
			} else {
				err( "owner %s is invalid, using current settings", 
					$logConfRef->{ OWNER } );
				$logConfRef->{ OWNER } = $fstat[4];
			}
		}
		$fstat[4] = $logConfRef->{ OWNER };
		debug( "selecting configured owner \'$fstat[4]\' for last log " .
			"\'$newArchive\'" );
	}
	if( exists( $logConfRef->{ GROUP } )) {
		unless( $logConfRef->{ GROUP } =~ m/^\d+$/ ) {
			
			@pwnam = getgrnam( $logConfRef->{ GROUP });

			if( @pwnam ) {
				$logConfRef->{ GROUP } = $pwnam[2];
			} else {
				err( "group %s is invalid, using current settings", 
					$logConfRef->{ GROUP } );
				$logConfRef->{ GROUP } = $fstat[5];
			}
		}
		$fstat[5] = $logConfRef->{ GROUP };
		debug( "selecting configured group \'$fstat[5]\' for last log " .
			"\'$newArchive\'" );
	}
	if( exists( $logConfRef->{ MODE } )) {
		debug( "selecting configured mode \'$logConfRef->{ MODE }\' for " .
			"last log \'$newArchive\'" );
		$fstat[2] = oct($logConfRef->{ MODE });
	}

	verbose( "setting owner \'$fstat[4]:$fstat[5]\' for last log " .
		"\'$newArchive\'" );
	verbose( "setting mode \'%o\' for last log \'$newArchive\'", $fstat[2] );
	if( ! chown( $fstat[4], $fstat[5], "$archiveDir/$newArchive" ) ) {
		err( "couldnt set ownership of %s to %s.%s: %s",
			"$archiveDir/$newArchive", $fstat[4], $fstat[5], $!);
	}
	if( ! chmod( $fstat[2], "$archiveDir/$newArchive" ) ) {
		err( "couldnt set mode of %s to %o: %s",
			"$archiveDir/$newArchive", $fstat[2], $!);
	}

	# Finally, we get rid of unwanted archives from the archive dir.
	#
	if( exists( $logConfRef->{ KEEP } ) &&
		$logConfRef->{ KEEP } > 0 ) {
			
		unless( opendir( DH, $archiveDir )) {
			err( "failed to open archivedir %s: %s", $archiveDir, $! );
			err( "rotation of %s failed", $logfile );
			return( 0 );
		}

		@archives = ();
		# Short logic split here for date and ordinal based indexes
		#
		if( $logConfRef->{ INDEX } eq 'date' ) {
			while( $file = readdir(DH) ) {
				if( $file =~ m/^$baseLogName\.\d{8}-\d+(\.$compSuffix)?$/ ) {
					push( @archives, $file );
				}
			}
			closedir( DH );
			@archives = sort reverse_sort @archives;
		} else {
			while( $file = readdir(DH) ) {
				if( $file =~ m/^$baseLogName\.\d+(\.$compSuffix)?$/ ) {
					push( @archives, $file );
				}
			}
			closedir( DH );
			@archives = sort log_sort @archives;
		}
	
		while( scalar( @archives ) > $logConfRef->{ KEEP } ) {
	
			$file = pop( @archives );
			verbose( "unlinking old archive \'%s\'", "$archiveDir/$file" );
			unless( unlink( "$archiveDir/$file" )) {
				
				err( "couldnt remove old archive file %s: %s",
					"$archiveDir/$file", $! );
			}
		}
	}



	return( 1 );
}


sub compress {

	my( $logfile, $configRef ) = ( @_ );
	my( $archiveDir, $archiveFile, $output, $index, $dateSuffix, $file );
	my( $exit, $signal, $core );

	( $archiveDir = $logfile ) =~ s/^(.*\/)[^\/]+/$1/;
	if( exists( $configRef->{ ARCHIVEDIR } )) {
		if( -d $configRef->{ ARCHIVEDIR } ) {
			$archiveDir = $configRef->{ ARCHIVEDIR };
		} else {
			err( "archivedir %s doesnt exist, using default archivedir",
				$configRef->{ ARCHIVEDIR } );
		}
	}
	$logfile =~ s/^.*\/([^\/]+)/$1/;
		
	$index = 0;

	if( $configRef->{ INDEX } eq 'date' ) {
		# Find out how many previous archives there are today, so we know
		# which one to compress
		#
		$dateSuffix = strftime( '%Y%m%d', localtime($NOW) );
		
		unless(opendir( DH, $archiveDir )) {
			err( "failed to open archivedir %s: %s", $archiveDir, $! );
			err( "rotation of %s failed", $logfile );
			return( 0 );
		}
		while( $file = readdir(DH) ) {
			if( $file =~ m/$logfile\.$dateSuffix-(\d+)/ ) {
				if( $1 > $index ) { $index = $1; }
			}
		}

		while( length( $index ) < $configRef->{ INDEX_LENGTH } ) {
			$index = '0' . $index;
		}

		$archiveFile = $archiveDir . "/" . $logfile . "." . $dateSuffix .
			"-" . $index;
	} else {
		while( length( $index ) < $configRef->{ INDEX_LENGTH } ) {
			$index = '0' . $index;
		}

		$archiveFile = $archiveDir . "/" . $logfile . "." . $index;
	} #if $configRef->{ INDEX } eq  'date'

	unless( -f $archiveFile ) {
		err( "archive %s doesnt exist, skipping compression", $archiveFile );
		return( 0 );
	}

	verbose( "compressing \'%s\'", $archiveFile );
	unless( system( $compBin, "-f", $archiveFile ) == 0) {
		
		$exit   = $? >> 8;
		$signal = $? & 127;
		$core   = $? & 128;

		if( $signal ) {
			if( $core ) {
				err( "%s exited on signal %s compressing %s: dumped core",
					$compBin, $signal, $archiveFile);
			} else {
				err( "%s exited on singal %s compressing %s",
					$compBin, $signal, $archiveFile );
			}
		}
		elsif( $exit ) {
			err( "%s returned exit status %s compressing %s: failed",
				$compBin, $exit, $archiveFile );
		}
		else {
			err( "%s exited with unknown error compressing %s",
				$compBin, $archiveFile );
		}

		return( 0 );
	}

	return( 1 );
}

sub expand { 

	my( $hashref, $level ) = ( @_ );
	my( $item );

	foreach $item ( sort keys %$hashref ) {

		print "   "x($level+1);
		print "$item => ";

		if( ref( $hashref->{ $item } ) ) {
			print "{\n";
			expand( $hashref->{ $item }, $level + 1 );

			print "   "x($level+1);
			print "}\n";
		} else {
			print "$hashref->{ $item }\n";
		}
	}
}


### ------------------ ###
### BEGIN MAIN ROUTINE ###
### ------------------ ###


# Are we being asked for help?
#
if( $Opt{ h } ) {
	usage;
	exit( 0 );
}

# Will we be validating the config file?  If we are, we also want to run in
# verbose mode.
#
if( $Opt{ c } ) {
	$Opt{ v } = 1;
}

# Verbosity and syslogging options.
#
if( $Opt{ q } ) {
	$QUIET = 1;
}
if( $Opt{ v } ) {
	$VERBOSE = 1;
	$QUIET = 0;
}
if( $Opt{ V } ) {
	$DEBUG = 1;
	$VERBOSE = 1;
	$QUIET = 0;
}

verbose( "starting sarah $VERSION" );

# Override the default config file?
#
if( $Opt{ f } ) {
	unless( -r $Opt{ f } ) {
		err( "specified config file %s is not readable: using default config",
			$Opt{ f } );
		debug( "config file override failed: using %s", $config );
	} else {
		$config = $Opt{ f };
		debug( "overriding default config file: using %s", $config );
	}
}

# Are we just validating the config file?
#
if( $Opt{ c } ) {
	$SUCCESS = parse( $config, \%options, \%logfiles );
	if( $Opt{ V } ) {
		# If we're validating the config file, AND we're in debug mode,
		# then we want to dump the contents of the config file before
		# exiting.
		dump_config( \%options, \%logfiles );
	}
	unless( $SUCCESS ) {
		err( "errors were detected parsing the config file \'%s\'",
			$config );
		exit( 1 );
	} else {
		verbose( "no errors detected parsing the config file \'%s\'",
			$config );
		exit( 0 );
	}
}


# Are we non-root?
#
# Normally this check would run earlier, but we don't want to insist on
# running as root if we're just validating the config file, and we can't
# validate the config file until other options have been parsed, such as the
# -f switch.
#
if( $Opt{ r } ) {
	$NOTROOT = 1;
}
else {
	unless( ($< == 0) && ($> == 0) ) {
		err( "must have root privs" );
		exit( 77 );
	}
}

# Syslogging options.
#
# This used to be mixed in with the verbosity stuff above, but we had to move
# the root restrictions.
#
if( $Opt{ s } || $VERBOSE ) {
	unless( $NOTROOT ) {
		$SYSLOG = 1;
		setup_syslog;
	}
}

# Done checking options.  On with the show!
#
unless( parse( $config, \%options, \%logfiles )) {
	err( "parsing of config file failed" );
	exit( 1 );
}


LOG:
foreach $log ( sort keys( %logfiles )) {

	my( $size_f, $date_f ) = (0,0);

	verbose( "examining %s", $log );

	# Do we even have a file to rotate?
	#
	unless( -f $log ) {
		err( "couldnt examine logfile %s: no such file", $log );
		next LOG;
	}

	# Check size
	#
	if( exists( $logfiles{ $log }{ SIZE } )) {

		debug( "testing size for %s", $log );

		if( -s $log >= (
			$MULTIPLIER{ $options{ SIZEMOD }} * $logfiles{ $log }{ SIZE })) {

			verbose( "file size %d >= %d: set size flag",
				-s $log,
				$MULTIPLIER{ $options{ SIZEMOD }} * $logfiles{ $log }{ SIZE });

			$size_f = 1;
		} else {
			verbose( "file size %d < %d: not setting size flag",
				-s $log,
				$MULTIPLIER{ $options{ SIZEMOD }} * $logfiles{ $log }{ SIZE });
		}
	} else {
		verbose( "SIZE directive for log %s not defined", $log );
	}


	if( exists( $logfiles{ $log }{ DATE } )) {

		debug( "testing date for %s", $log );
		$date_f = match_date( $logfiles{ $log }{ DATE } );

		if( $date_f ) {
			verbose("date spec matches current date: setting date flag");
		} else {
			verbose("date spec doesnt match current date: not setting date flag");
		}
	} else {
		verbose( "DATE directive for log %s not defined", $log );
	}


	# If neither directive is set, skip the file.
	#
	unless( exists( $logfiles{ $log }{ SIZE } ) ||
			exists( $logfiles{ $log }{ DATE } ) ) { next LOG; }


	if( ! exists( $logfiles{ $log }{ SIZE }) || $size_f ) {
		$size_f = 1;
	} else {
		$size_f = 0;
	}


	if( ! exists( $logfiles{ $log }{ DATE }) || $date_f ) {
		if( $size_f ) {
			verbose( "DATE and SIZE flags set: rotating logfile" );
			$ROTATED{ $log } = 1;
		}
		elsif( $options{ SIZELOGIC } eq "OR" ) {
			verbose( "DATE or SIZE flags set: rotating logfile" );
			$ROTATED{ $log } = 1;
		}
		else {
			verbose( "DATE and SIZE flags not set: not rotating logfile" );
		}
	} else {
		if( $options{ SIZELOGIC } eq "AND" ) {
			verbose( "DATE and SIZE flags not set: not rotating logfile" );
		}
		elsif( $size_f ) {
			verbose( "DATE or SIZE flags set: rotating logfile" );
			$ROTATED{ $log } = 1;
		}
		else {
			verbose( "no flags set: not rotating logfile" );
		}
	}

	if( $ROTATED{ $log } ) {
		unless( rotate( $log, $logfiles{ $log } )) {
			err( "rotation of log \'%s\' failed", $log );
			$ROTATED{ $log } = 0;
		}
		debug( "rotation of log \'%s\' completed", $log );
	}

	# On with the next logfile.
}

# Rotation is done .. that's the quick part.  Now we go on with compression
# which may take us into the next period.
#
COMP:
foreach $log (sort keys( %ROTATED )) {
	if( $logfiles{ $log }{ FLAGS } =~ m/Z/ ) {
		if( $ROTATED{ $log } ) {
			verbose( "compressing old \'%s\'", $log );
			unless( compress( $log, $logfiles{ $log } )) {
				err( "compression of old \'%s\' failed", $log );
			}
		} else {
			verbose( "compression flag set for log \'%s\', " .
				"but rotation failed: not doing compression", $log );
		}
	} else {
		verbose( "compression flags not set for rotated log \'%s\'", $log );
	}
}

# Finally, signal syslogd.
#
SYSLOG: {
unless( $NOTROOT ) {
	
	unless( open( FH, $syslogdPID )) {
		err( "couldnt open PID file %s for reading: %s",
			$syslogdPID, $! );
		err("syslogd couldnt be notified because a PID file couldnt be found");
		last SYSLOG;
	}

	$pid = <FH>;
	close FH;

	$pid =~ s/^\s*(\d+)\s*/$1/;
	$signal = 'HUP';

	if( kill( $signal, int( $pid ))) {
		debug( "syslogd[%d] notified with signal %s", $pid, $signal);
	} else {
		err( "cant notify syslogd, pid %d: %s", int( $pid ), $!);
	}

	
} }

exit( 0 );



__END__


=head1 NAME

B<sarah> - Syslog Automated Rotation and Archive Handler

=head1 SYNOPSIS

B<sarah> [ B<-chqrsvV> ] [ B<-f> F<config_file> ]

=head1 DESCRIPTION

B<sarah> is a program that should be scheduled to run periodically by cron(8).
B<sarah> is intended to be a replacement for newsyslog(8).  When it is
executed it archives log files if necessary.  As with newsyslog(8), if a log
file is determined to require archiving, B<sarah> rearranges the files so that
"logfile" is empty, "logfile.0" has the last period's logs in it, "logfile.1"
has the next to last periods logs in it, and so on, up to a user-specified
number of archived logs.  Optionally, the archived logs can be compressed to
save space.
 
A log may be archived for one or both of two reasons: 

=over 4

=item 1.

It is larger than the configured size

=item 2.

This is the specific configured date/time for rotation of the log.

=back



The granularity of B<sarah> is dependent on how often it is scheduled to run
by cron(8).  Since the program is quite fast, it may be scheduled to run every
minute on most machines without any ill effects; rotation by date/time assumes
this is the case.

When starting up, B<sarah> reads in a configuration file to determine which
logs may potentially be archived.  By default, this configuration file is
F</usr/local/etc/sarah.conf>.  B<sarah> is intended to be much more
configurable than other log rotators; new features, plus the consideration
that future feature requests would need to be accommodated led to the use of a
completely different configuration file format from standard log rotators,
such as newsyslog(8).

On successful termination, B<sarah> will exit with status 0.  If some failure
occurs, B<sarah> will exit with a non-zero status.  On FreeBSD systems, see
sysexits(3).

=head1 OPTIONS

The following options may be used with B<sarah>:

=over 4

=item B<-c>

Check the config file for errors and exit.  If any errors are detected,
B<sarah> will indicate what they are.  Otherwise it will print a status line
indicating all is well.  When combined with the B<-V> option (debug mode), an
interpreted version of the config file will be printed to STDOUT before
exiting.

=item B<-h>

Print out a help summary and version information then exit.

=item B<-f> I<config_file>

Instruct B<sarah> to use I<config_file> instead of the default
F</usr/local/etc/sarah.conf> for its configuration file.

=item B<-q>

Run in quiet mode.  B<sarah> will not produce any output; not even errors.  If
B<-v> or B<-V> are specified in conjunction with B<-q>, this option will be
ignored.

=item B<-r>

Remove the restriction that B<sarah> must be run as root.  In this mode
B<sarah> will not be able to send the HUP signal to syslogd(8) so the default
signaling action to take on the completion of the rotation of any logfile
will become no action.  Also, if this option is specified the B<-s> switch will be ignored.

=item B<-s>

Copy all output (including debugging and verbose mode output) to syslogd(8)
with the DAEMON facility.  Ignored if B<-r> is specified.  The B<-s> switch
can be specified in conjunction with B<-v> or B<-V> to copy debugging and
verbose output to syslog.

=item B<-v>

Place B<sarah> into verbose mode.  In this mode, it will print out each log
and its reasons for either trimming the log or skipping it.

=item B<-V>

Place B<sarah> into full debug mode.  If both B<-v> and B<-V> are specified,
B<-V> takes precedence.

=back


=head1 CONFIGURATION FILE

The B<sarah> configuration file consists of two general statements: directives
and comments.  All comments begin with a # character, either at the beginning
of a line or at the end of a directive. Any text after a # character on a line
is treated as a comment.

There are two formats for directives in the B<sarah> configuration file.  The
first applies to directives that have optional sub-directives.  This type has
the general format of the directive, possibly followed by an argument, then
any sub-directives enclosed in balanced braces ({}) and terminated by a
semicolon (;).

The second type of directive does not have any sub-directives (in fact, this
type is usually a sub-directive itself).  This type consists of the directive
followed by some number of arguments which are terminated by a semicolon (;).

Unless otherwise noted, directives may appear more than once within either the
configuration file, or within their respective configuration block.  If such
directives appear more than once, the value set by the last appearance of the
directive will be used.  All directives, except where otherwise noted, may
appear within an B<options> block to set defaults.

The following directives are supported:

=over 4

=item B<options> { };

This directive has no options.  It contains directives that set global
configuration options for the behavior of B<sarah>.  The B<options> directive
may not appear as a sub-directive of itself or any other directive.

=item B<include> { };

The B<include> directive allows multiple configuration files to be read from
within the main configuration file.  The B<include> directive can contain any
number of path names to other config files that should be read.  Each path
name should be terminated by a semi-colon.  The contents of the included
config files will be read and interpreted as if they had been inserted in the
main config file at the point of the B<include> directive.  Any number of
B<include> directives may appear in a config file, but none may appear as a
sub-directive of any other directive.  A nesting limit of 20 includes deep is 
imposed to prevent recursive includes.

=item B<log> I<path> { };

This directive opens a new block defining a new logfile.  The required I<path>
argument is the path to the logfile to be rotated.  The B<path> directive may
not appear as a sub-directive of itself or any other directive.

=item B<sizelogic> AND|OR;

This directive sets the logic used in determining whether to rotate a logfile
when both a date and size limit have been set for the logfile.  If
B<sizelogic> is set to AND, then the logfile is only rotated if the size
limit of the file is exceeded and the current date/time match the date/time
specification for the logfile.  If B<sizelogic> is set to OR, the logfile
will be rotated if either (or both) requirements are met.  The default is AND.
The B<sizelogic> directive may only appear within an B<options> block.

=item B<sizemod> B|K|M;

The B<sizemod> directive sets the modifier used with values in B<size>
directives (below).  B sets size modifier to bytes, K to kilobytes, and
M to megabytes.  The default is B, or bytes.  The B<sizemod> directive may
only appear within an B<options> block.

=item B<archivedir> I<path>;

By default, logfiles are rotated and archived in the same directory where they
originated.  Using the B<archivedir> directive, a new directory, specified by
the required I<path> argument, can be specified where archives of the logfile
will be stored.  This may be useful in situations such as keeping a central
repository of old logs, possibly on an NFS mounted filesystem.

=item B<owner> I<user>;

=item B<group> I<group>;

When logfiles are rotated, the archives are given the same owner and group as
the original logfile.  Using the B<owner> and B<group> directives, you can
specify a new owner and/or group for the logfile archives.  The required
I<user> and I<group> arguments may be either numeric, or a name which is
present in F</etc/passwd> or F</etc/group>.  The B<owner> and B<group>
directives do not affect the ownership of the logfile itself, only its
archives.

=item B<mode> I<mode>;

As with ownership, log archives are given the same mode, or permissions, as
the original logfile by default.  The B<mode> directive may be used to give
the archives different permissions.  The required I<mode> argument must be an
absolute (numeric) mode as described by chmod(1).  As with B<owner> and
B<group>, the B<mode> directive does not affect the permissions of the logfile
itself, only its archives.

=item B<index> ordinal|date;

The B<index> directive tells B<sarah> how to name its archive files.  If the
B<ordinal> argument is specified (the default) B<sarah> produces numbered
archives where the index begins at 0 (zero) with the most recent archive.  If
the B<date> argument is specified, B<sarah> produces dated archive files in
the format <log>.yyyymmdd-xx where xx is an index number beginning at 0 (zero)
with the first archive rotated on that date.  This allows for multiple
rotations of any given file in one day.

Sarah will always pad index numbers in such a way that all archives have the
same number of digits in their index, but the minimum possible number of
digits is used.  For example, if there are 10 maillog archives (maillog.0
through maillog.9) then sarah will use single digits; when the 11th archive is
created, sarah will change all the indexes to double digits (maillog.00
through maillog.10); when the 101st archive is created, three digits; and so
forth.

=item B<keep> I<integer>;

This configuration directive sets the number of archives to keep for each
logfile.  The number does not include the logfile itself.  If set to 0 (zero)
or left undefined, B<sarah> will keep an infinite number of logfiles (until
you run out of disk space).

=item B<size> I<log_size>;

The B<size> directive sets the size limit for a logfile, beyond which the
logfile will be rotated.  The required I<log_size> argument is a number in
bytes by default, but this may be changed using the B<sizemod> directive in an
B<options> section of the configuration file.  Each B<log> block requires at
least one B<size> or B<date> directive (below).  If neither directive is
defined in a B<log> block, the logfile will never be rotated.  The behavior of
B<sarah> when both B<size> and B<date> directives are defined is controlled by
the B<sizelogic> directive.

=item B<date> { };

This directive opens a new block for defining a date/time specification for
when the current logfile should be rotated.  The B<date> directive has no
parameters, but does have several sub-directives.  Each B<log> block requires
at least one B<date> directive, or one B<size> directive (above).  If neither
directive is defined in a B<log> block, the logfile will never be rotated.
The behavior of B<sarah> when both B<size> and B<date> directives are defined
is controlled by the B<sizelogic> directive.

=item B<minute> I<time_spec>;

=item B<hour> I<time_spec>;

=item B<dow> I<time_spec>;

=item B<month> I<time_spec>;

=item B<dom> I<time_spec>;

These five directives -- B<minute>, B<hour>, B<dow> (day of week), B<month>,
and B<dom> (day of month) define the date/time specification for the rotation
of a logfile.  The required I<time_spec> arguments for each directive define
the various times/dates where a logfile may be rotated.

Permitted values for the directives are as follows:

     field    allowed values
     -----    --------------
     minute   0-59
     hour     0-23
     dom      0-31 (0 means "last day of the month")
     month    1-12
     dow      0-7  (0 and 7 are Sunday)

An argument may be an asterisk (*) which stands for "first-last".

Ranges are allowed.  Ranges are two numbers separated by a hyphen.  The
specified range is inclusive.  For example, 8-11 for the B<hours> directive
specifies rotation at 8, 9, 10, and 11.

Lists of numbers are also allowed.  A list is a set of numbers (or ranges)
separated by commas.  Examples: "1,2,5,9", "0-4,8-12"

Step values can be used in conjunction with ranges.  Following a range with
"/<number>" specifies skips of the number's value within the range.  For
examples, "0-23/2" can be used in the B<hours> directive to specify rotation
every other hour.  Expanded to a list, this would be
"0,2,4,6,8,10,12,14,16,18,20,22".  Step values can also be used in conjunction
with an asterisk.  This B<hours> example could be specified "*/2".

As soon as one directive in a B<date> block is defined, the date/time will be
taken into account in the rotation of a logfile.  Leaving any directives
within a B<date> block undefined equates to entering a * for those directives.

These five directives may only appear within a B<date> block.

=item B<flags> [ Z ] [ B ];

The B<flags> directive specifies whether any special processing should be done
to the archived logfile.  The I<Z> flag will make B<sarah> compress the
archive files using gzip(1) to save space.  The I<B> flag means that the file
is binary, and so the ASCII message which B<sarah> inserts into a rotated
logfile to indicate the file has been rotated should not be included.
Arguments to the B<flags> directive may be separated by whitespace but
whitespace is not required.

=item B<pid> I<path_to_pid_file>;

This directive specifies the file to read to find the PID of the daemon
process that writes to the current logfile.  If this is set, than the signal
set by the B<signal> directive is sent to the PID read from this file upon
completion of the rotation of the current logfile.  If the B<pid> directive is
not set, the PID is read from F</var/run/syslog.pid>.

=item B<signal> I<signal_number>;

This directive sets the signal to be sent to the logging daemon upon
completion of the rotation of the current logfile.  The required
I<signal_number> argument may be either a numeric signal number or its alpha
equivalent.  By default, a SIGHUP will be sent.

=back

=head1 CONFIGURATION EXAMPLES

The cron(8) logfile is rotated once a day at midnight and compressed.  A
week's worth of archives are kept.

   log /var/cron/log {
      keep 7;
      date {
         minute 0;
         hour 0;
      }
      flags Z;
   }

The mail log is rotated at least every Monday at midnight, but also whenever
it exceeds 1 megabyte in size.  The archives are stored in a separate
location.

   options {
      sizelogic OR;
      sizemod K;
   };
   log /var/log/maillog {
      archivedir /depot/logs;
      keep 7;
      size 1024;
      date {
         minute 0;
         hour 0;
         dow 1;
      };
   };

A separate config file is loaded and interpreted before the log block for the
web server logs is interpreted.  The web server access logs are rotated every
hour onto a large filesystem (our hypothetical web server is B<very> busy).
The archive files are dated, old archives are not deleted, and ownership is
changed to allow other staff members to read them.  The web server daemon is
sent a SIGUSR1 signal.

   include {
      /usr/local/etc/cust-logs.conf;
   };
   log /usr/local/www/log/access_log {
      archivedir /depot/logs;
      date {
         minute 0;
      };
      index date;
      owner daemon;
      group staff;
      mode 0644;
      pid /usr/local/www/log/httpd.pid;
      signal USR1;
   };

The formatting of the configuration file used here is arbitrary.  As long as
whitespace is maintained around words, any formatting can be used.  The
previous example could easily appear like this, if one was so inclined:

   include{/usr/local/etc/cust-rotation.conf;};log
   /usr/local/www/log/access_log{archivedir /depot/logs;
   date{minute 0;};index date;
   owner daemon;group staff;mode 0644;
   pid /usr/local/www/log/httpd.pid; signal USR1;};

Sub-directives of the B<log> directive may also be specified within an
B<options> block to set defaults.  In the following example, both log files
may be rotated nightly at midnight, but the access_log file will only be
rotated if it is also larger than 100 kilobytes.  Both log files are indexed
by date, with 100 archive files kept.

   options {
      sizelogic AND;
      sizemod K;
      date { minute 0; hour 0; };
      index date;
      keep 100;
   };
   log /usr/local/www/log/access_log {
      size 100;
   };
   log /usr/local/www/log/error_log {
   };

=head1 FILES

F</usr/local/etc/sarah.conf>        B<sarah> configuration file

=head1 BUGS

=over 4

=item *

A mode to force B<sarah> to trim all logs is missing.

=item *

A testing mode is required where no log rotation is done, but verbose output
indicates whether logfiles would have been rotated.

=back

=head1 AUTHORS

Copyright 2000, 2001 Matthew Pounsett (mattp@conundrum.com)

Available at ftp://ftp.conundrum.com/pub/sarah/

=head1 SEE ALSO

gzip(1), syslog(3), syslogd(8), chown(8), newsyslog(8)

=cut
