#! /bin/sh # rotate-log --- rotate accounting logs, optionally compressing old logs # Copyright (C) 1992, 1995, 1996, 1998 Noah S. Friedman # Author: Noah Friedman # Created: 1992-02-10 # $Id: rotate-log,v 1.14 1998/01/04 22:16:49 friedman Exp $ # 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, 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, you can either send email to this # program's maintainer or write to: The Free Software Foundation, # Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA. # Commentary: # Code: # Name by which this script was invoked. progname=`echo "$0" | sed -e 's/[^\/]*\///g'` # To prevent hairy quoting and escaping later. bq='`' eq="'" dq='"' known_compress_methods='compact compress gzip bzip bzip2' usage="Usage: $progname {options} [logfile-name ...] Options are: -c, --compress Compress old logs while rotating. -C, --compress-active Compress the active log (which is first rotated to log.0). Normally this isn't done because it may cause a race condition if the file is still open, e.g. for process accounting, syslogs, etc. Exercise caution using this option. This option implies --compress. -D, --debug Turn on shell debugging ($bq${bq}set -x$eq$eq). -d, --default-directory DIR Default directory for log files listed without pathname (i.e. no "/" chars in name) -g, --group, --chgrp GROUP Change group ownership to GROUP on rotated logs. -h, --help You're looking at it. -M, --compress-method METHOD Method to use for compressing, one of: $known_compress_methods. Default is ${bq}gzip$eq. -m, --mode, --chmod MODE Change permissions to MODE on rotated logs. -N, --no-new-log After rotating log->log.0, do not recreate log. -n, --number-of-logs N Keep up to N versions of log total (default 7). Rotated logs are numbered zero-origin. The ${dq}active$dq log is included in the count, so the highest kept log number would be 6 by default. -o, --owner, --chown OWNER Change owner of logs to OWNER (you should use OWNER.GROUP and avoid using --group if the ${bq}chown${eq} command supports this syntax). -s, --size-threshold SIZE Do not rotate unless working log file is SIZE units or larger. Units of kilobytes (${bq}k$eq) or megabytes (${bq}m$eq) may be specified, e.g. $bq-s 200k$eq. If no unit is specified, the size is interpreted as bytes. By default no minimum size is required for rotation. -v, --verbose Report renaming of files as they occur. -V, --verbose-if-not-rotated If a size threshold is specified with $bq--size-threshold$eq and the primary log file doesn't meet that threshold, print a message saying so. $bq--verbose$eq alone doesn't report this condition since it is one of inactivity, rather than activity. " # Usage: eval "$getopt"; value=$optarg # or optarg_optional=t; eval "$getopt"; value=$optarg # # This function automatically shifts the positional args as appropriate. # The argument to an option is optional if the variable `optarg_optional' # is non-empty. Otherwise, the argument is required and getopt will cause # the program to exit on an error. optarg_optional is reset to be empty # after every call to getopt. The argument (if any) is stored in the # variable `optarg'. # # Long option syntax is `--foo=bar' or `--foo bar'. 2nd argument # won't get used if first long option syntax was used. # # Note: because of broken bourne shells, using --foo=bar syntax can # actually screw the quoting of args that end with trailing newlines. # Specifically, most shells strip trailing newlines from substituted # output, regardless of quoting. getopt=' { optarg= case "$1" in --*=* ) optarg=`echo "$1" | sed -e "1s/^[^=]*=//"` shift ;; * ) case ${2+set} in set ) optarg="$2" shift shift ;; * ) case "$optarg_optional" in "" ) case "$1" in --*=* ) option=`echo "$1" | sed -e "1s/=.*//;q"` ;; * ) option="$1" ;; esac exec 1>&2 echo "$progname: option $bq$option$eq requires argument." echo "$progname: use $bq--help$eq to list option syntax." exit 1 ;; esac shift ;; esac ;; esac optarg_optional= }' # Initialize variables. # Don't use `unset' since old bourne shells don't have this command. # Instead, assign them an empty value. chgrp=: chmod=: chown=: compressp= compress_activep= compress_method=gzip create_new_log_p=t debug= default_directory=. number_of_logs=7 size_threshold= verbose= verbose_not_rotated= # Parse command line arguments. # Make sure that all wildcarded options are long enough to be unambiguous. # It's a good idea to document the full long option name in each case. # Long options which take arguments will need a `*' appended to the # canonical name to match the value appended after the `=' character. while : ; do case $# in 0) break ;; esac case "$1" in -C | --compress-active | --compress-a* ) compressp=t compress_activep=t shift ;; -c | --compress ) compressp=t shift ;; -D | --debug | --deb* ) debug=t shift ;; -d | --default-directory* | --def* ) eval "$getopt" default_directory="$optarg" ;; -g | --group | --g* | --chgrp* | --chg* ) eval "$getopt" chgrp="${CHGRP-chgrp} $optarg" ;; -M | --compress-method* | --compress-m* ) eval "$getopt" compress_method="$optarg" ;; -m | --mode | --m* | --chmod* | --chm* ) eval "$getopt" chmod="${CHMOD-chmod} $optarg" ;; -N | --no-new-log | --no* ) create_new_log_p= shift ;; -n | --number-of-logs* | --nu* ) eval "$getopt" number_of_logs="$optarg" ;; -o | --owner | --o* | --chown* | --cho* ) eval "$getopt" chown="${CHOWN-chown} $optarg" ;; -s | --size-threshold* | --s* ) eval "$getopt" size_threshold= size_unit= eval `echo "$optarg" \ | sed -ne '/^[0-9]*[0-9km]$/!{ s/.*/size_unit=invalid/ p q } /[km]$/!{s/$/1/;} s/^/size_threshold=/ s/\(.\)$/ size_unit=\1/ p'` case "$size_unit" in invalid ) echo "$progname: $optarg: Invalid size specifier." 1>&2 exit 1 ;; 1 ) : ;; [Kk] ) size_threshold=`expr $size_threshold '*' 1024` ;; [Mm] ) size_threshold=`expr $size_threshold '*' 1024 '*' 1024` ;; esac ;; -V | --verbose-if-not-rotated | --verbose-* ) verbose_not_rotated=t shift ;; -v | --verbose | --v* ) verbose=t shift ;; -h | --help | --h* ) echo "$usage" 1>&2 exit 1 ;; -- ) # Stop option processing shift break ;; -? | --* ) case "$1" in --*=* ) arg=`echo "$1" | sed -e 's/=.*//'` ;; * ) arg="$1" ;; esac exec 1>&2 echo "$progname: unknown or ambiguous option $bq$arg$eq" echo "$progname: Use $bq--help$eq for a list of options." exit 1 ;; -??* ) # Split grouped single options into separate args and try again optarg="$1" shift set fnord `echo "x$optarg" | sed -e 's/^x-//;s/\(.\)/-\1 /g'` ${1+"$@"} shift ;; * ) break ;; esac done case "$debug" in t ) set -x ;; esac case $# in 0 ) echo "$progname: at least one log name is required as an argument." 1>&2 echo "$usage" 1>&2 ;; esac case "$compressp:$compress_method" in # re_suffix is regexp-quoted t:compact ) compress_cmd='compact -f' suffix='.C' re_suffix='\.C' ;; t:compress ) compress_cmd='compress -f' suffix='.Z' re_suffix='\.Z' ;; t:gzip ) compress_cmd='gzip'; suffix='.gz' re_suffix='\.gz' ;; t:bzip ) compress_cmd='bzip'; suffix='.bz' re_suffix='\.bz' ;; t:bzip2 ) compress_cmd='bzip2'; suffix='.bz2' re_suffix='\.bz2' ;; t:* ) echo "$progname: unknown compress-method $bq$compress_method$eq" 1>&2 echo "$progname: Known compression methods: $known_compress_methods" 1>&2 exit 1 ;; esac orig_cwd=${PWD-`pwd`} max_log_number=`expr $number_of_logs - 1` # make it zero-origin # Used to quote strings so that sed will match exactly that string # (i.e. quote special characters so that they lose their specialness). sed_regexp_quote='s/\([][*.\\\/?|^$]\)/\\\1/g' re_end='$' # to avoid quoting nightmares in command substitutions for log in ${1+"$@"} ; do cd "$orig_cwd" dirname=`echo "$log" \ | sed -e 's/\/*$// s/\/[^\/]*$//'` basename=`echo "$log" \ | sed -e 's/\/*$// s/.*\///'` case "$dirname" in "$basename" ) dirname=$default_directory ;; esac if cd "$dirname" ; then : else echo "$progname: skipping $bq$log$eq logs." 1>&2 continue fi case "$compressp" in t ) num=$max_log_number while : ; do case $num in -1 ) break ;; esac file="$basename.$num" test -f "$file" \ && $compress_cmd "$file" \ && test ".$verbose" = '.t' \ && echo "$file -> $file$suffix" num=`expr $num - 1` done ;; esac num= case "$size_threshold" in '' ) : ;; * ) if test -f "$basename"; then # Using wc should be relatively inexpensive since most (if not all) # implementations simply stat the file and use st_size to obtain the # number of characters. # This is also more reliable than using `ls' and parsing out irrelevant # fields since not all versions of `ls' print the same number of # fields given identical options, and choosing fields based on the # presence of numerals only is risky since a file might have a uid # or gid with no corresponding name. logsize=`wc -c $basename | sed -e 's/^ *//;s/ .*//'` if test "$logsize" -lt "$size_threshold"; then case "$verbose_not_rotated" in t) echo "$progname: $log: size=$logsize less than threshold=$size_threshold; not rotating." ;; esac continue fi else # If log doesn't exist, we'll skip any rotation. case "$verbose_not_rotated" in t) echo "$progname: $log: does not exist; not rotating." ;; esac num=-2 fi ;; esac # Check to see if there are any gaps in the existing logfiles and, if so, # fill them before rotating the higher-numbered logfiles. case "$num" in -2 ) : ;; * ) re_basename=`echo "$basename" | sed -e "$sed_regexp_quote"` numbers=`ls -1 $basename.*$suffix 2> /dev/null \ | sed -ne "/^$re_basename[.][0-9][0-9]*$re_suffix$re_end/!d s/^$re_basename[.]// s/$re_suffix$re_end// p" \ | sort -n` num=-1 nextnum=0 for n in $numbers; do if test $n -eq $number_of_logs || test $n -gt $nextnum ; then break fi num=$n nextnum=`expr $n + 1` done ;; esac while : ; do case $num in -2 ) break ;; -1 ) oldfile="$basename" newfile="$basename.0" ;; * ) oldfile="$basename.$num$suffix" newfile="$basename.$nextnum$suffix" ;; esac test -f "$oldfile" \ && test $nextnum -lt $max_log_number \ && mv "$oldfile" "$newfile" \ && { case "$verbose" in t ) echo "$oldfile -> $newfile" ;; esac $chmod "$newfile" $chgrp "$newfile" $chown "$newfile" } nextnum=$num num=`expr $num - 1` done # Create new logfile (or rather, append to it just in case some other # random process has already created it and started writing into it) case "$create_new_log_p" in t) >> "$basename" $chmod "$basename" $chgrp "$basename" $chown "$basename" ;; esac case "$compress_activep" in t) test -f "$basename.0" \ && $compress_cmd "$basename.0" \ && test ".$verbose" = '.t' \ && echo "$basename.0 -> $basename.0$suffix" esac case "$create_new_log_p:$verbose" in t:t ) echo "Created $log" ;; esac done # rotate-log ends here