#! /bin/sh
# rotate-log --- rotate accounting logs, optionally compressing old logs

# Copyright (C) 1992, 1995, 1996, 1998 Noah S. Friedman

# Author: Noah Friedman <friedman@prep.ai.mit.edu>
# 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
