#!/bin/bash
#< Log rotation script, able to rotate and compress logs - run from cron

BASENAME="/usr/bin/basename"
AWK="/usr/bin/awk"
BZIP="/usr/bin/bzip2"
CP="/bin/cp"
CUT="/usr/bin/cut"
ECHO="/bin/echo"
EGREP="/usr/bin/egrep"
GREP="/usr/bin/grep"
GZIP="/usr/bin/gzip"
SED="/usr/bin/sed"

#############################################################################
# Config file should contain fields of the following format:
#
# logfile_to_rotate:num_rotations:action
#
# where action is one of
# g	GZIP after rotation
# b	BZIP after rotation
# n	do nothing after rotation
#
# Whole line comments, starting with '#', are permitted
#
# No white space should exist in the file
#
CONFIG_FILE="/Users/kevin/home_projects/logrevolver/0.1/etc/logrevolver.conf"
#############################################################################

THIS_PROG=$( ${BASENAME} $0 )
VERBOSE=0

function print_usage {
   {
      ${ECHO} "Usage: ${THIS_PROG} [-h] [-c <config_file>]"
      ${ECHO} "   -c <config_file>   Specify path to configuration file"
      ${ECHO} "   -h                 Display this help message"
      ${ECHO} "   -v                 Verbose mode"
   } >&2
}

function print_error {
   ${ECHO} "Error: $@" >&2
}

function check_file_exists {
   if [ -e "$1" ]; then
      return 0
   else
      return 1
   fi
}

function check_config_file_syntax {
   # Here, we'll go over each line in the config file and ensure
   # that target logs exist, number of rotations is sane, and archival
   # options are correct. We end up parsing the config file twice (once here,
   # and once in the process_logs function. Oh well.
   LINE_COUNT=0
   ERROR_COUNT=0
   while read LINE; do
      (( LINE_COUNT = LINE_COUNT + 1 ))
      ${ECHO} "${LINE}" | ${GREP} '^#' >/dev/null 2>&1
      if [ "$?" -eq "0" ]; then
         continue
      fi
      NUM_FIELDS=$( ${ECHO} "${LINE}" | ${AWK} -v FS=":" '{print NF}' ) 
      if [ "${NUM_FIELDS}" -ne "3" ]; then
         print_error "Line ${LINE_COUNT} does not have three fields"
         (( ERROR_COUNT = ERROR_COUNT + 1 ))
      else
         LOGFILE=$( ${ECHO} "${LINE}" | ${CUT} -d: -f1 )
         ROTATIONS=$( ${ECHO} "${LINE}" | ${CUT} -d: -f2 )
         METHOD=$( ${ECHO} "${LINE}" | ${CUT} -d: -f3 )
         if [ ! -e "${LOGFILE}" ]; then
            print_error "Line ${LINE_COUNT} logfile does not exist"
            (( ERROR_COUNT = ERROR_COUNT + 1 ))
         fi
         ${ECHO} "${ROTATIONS}" | ${EGREP} "^[0-9]+$" >/dev/null 2>&1
         if [ "$?" -ne "0" ]; then
            print_error "Line ${LINE_COUNT} rotation count should be an integer"
            (( ERROR_COUNT = ERROR_COUNT + 1 ))
         else
            if [ "${ROTATIONS}" -eq "0" ]; then
               print_error "Line ${LINE_COUNT} rotation count should be greater than zero"
               (( ERROR_COUNT = ERROR_COUNT + 1 ))
            fi
         fi
         ${ECHO} "${METHOD}" | ${GREP} "^[bgn]$" >/dev/null 2>&1
         if [ "$?" -ne "0" ]; then
           print_error "Line ${LINE_COUNT} invalid archival method specified"
           (( ERROR_COUNT = ERROR_COUNT + 1 ))
         fi
      fi   
   done < ${CONFIG_FILE}
   if [ "${ERROR_COUNT}" -gt "0" ]; then
      print_error "${ERROR_COUNT} errors found in configuration file. Exiting."
      exit 1
   fi
}

function check_config_file {
   check_file_exists "${CONFIG_FILE}"
   if [ "$?" -ne "0" ]; then
      print_error "Config file ${CONFIG_FILE} does not exist"
      exit 1
   fi
   check_config_file_syntax 
}

function process_logs {
   ${GREP} '^[^#]' ${CONFIG_FILE} | while read LINE; do
      ROTATIONS=$( ${ECHO} "${LINE}" | ${CUT} -d: -f2 )
      METHOD=$( ${ECHO} "${LINE}" | ${CUT} -d: -f3 )
      for (( i = $(( ROTATIONS - 1 )); i >= 0; i-- )); do
         LOGFILE=$( ${ECHO} "${LINE}" | ${CUT} -d: -f1 )
         if [ "${i}" -gt "0" ]; then
            case ${METHOD} in
               "b") LOGFILE="${LOGFILE}.${i}.bz2" ;;
               "g") LOGFILE="${LOGFILE}.${i}.gz"  ;;
               "n") LOGFILE="${LOGFILE}.${i}"     ;;
            esac
         fi
         if [ -e "${LOGFILE}" ]; then
            (( NEXTi = i + 1 ))
            if [ "${i}" -eq "0" ]; then
               TARGET="${LOGFILE}.${NEXTi}"
               ${CP} -p ${LOGFILE} ${TARGET}
               > ${LOGFILE}
               case ${METHOD} in
                  "b")  (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}.bz2"
                        }
                        ${BZIP} -9 -f ${TARGET}
                        ;;
                  "g")  
                        (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}.gz"
                        }
                        ${GZIP} -9 -f ${TARGET}
                        ;;
                  "n")  (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}"
                        }
                        ;;
               esac
            else
               case ${METHOD} in
                  "b")  TARGET=$( ${ECHO} "${LOGFILE}" | ${SED} 's/\.bz2$//' )
                        TARGET="${TARGET%.*}.${NEXTi}"
                        (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}.bz2"
                        }
                        ${CP} -p ${LOGFILE} ${TARGET}.bz2
                        ;;
                  "g")  TARGET=$( ${ECHO} "${LOGFILE}" | ${SED} 's/\.gz$//' )
                        TARGET="${TARGET%.*}.${NEXTi}"
                        (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}.gz"
                        }
                        ${CP} -p ${LOGFILE} ${TARGET}.gz
                        ;;
                  "n")  TARGET="${LOGFILE%.*}.${NEXTi}"
                        (( VERBOSE )) && {
                           ${ECHO} "--> Rotating ${LOGFILE} to ${TARGET}"
                        }
                        ${CP} -p ${LOGFILE} ${TARGET}
                        ;;
               esac
            fi
         fi
      done
   done
}

while getopts ":hc:v" OPTION; do
  case ${OPTION} in
     "h")  print_usage && exit 0   ;;
     "c")  CONFIG_FILE="${OPTARG}" ;;
     "v")  VERBOSE=1               ;;
     *  )  print_usage && exit 1   ;;
  esac
done

check_config_file
process_logs