#!/bin/bash
set -u
#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# this package.
#------------------------------------------------------------------------------
#==============================================================================
# Declarations and Environment
declare -i ErrorCount=0
declare -i WarningCount=0
declare -i Debug=0
declare -i NoOp=0
declare -i SilentMode=0
declare ThisScript=${0##*/}
declare ThisUser=
declare Version=1.0.0
declare CmdLine="$0 $*"
declare LogsDir="${LOGS:-${HOME:-/tmp}}"
declare Log=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare RequiredUtils="awk date grep id ls tail wc"
#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
function dbg () { [[ "$Debug" -eq 0 ]] || msg "DEBUG: $*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function warnmsg () { msg "\\nWarning: ${1:-Unknown Warning}\\n"; WarningCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }
#------------------------------------------------------------------------------
# Function: terminate
function terminate
{
# Disable signal trapping.
trap - EXIT SIGINT SIGTERM
dbg "$ThisScript: EXITCODE: $ErrorCount"
# Stop logging.
[[ "$Log" == off ]] || msg "\\nLog is: $Log\\n${H1}"
# With the trap removed, exit.
exit "$ErrorCount"
}
#------------------------------------------------------------------------------
# Function: usage (documentation)
#
# Input:
# $1 - style, '-h' (for short usage synopsis) or '-man' (for full man-page
# documentation). Default is -h.
#
# $2 - error message (optional). Specify this if usage() is called due to
# usage error, in which case the given message displayed first, followed by
# the standard usage message (short or long depending on $1). If displaying an
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure the supplied usage error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
declare style=${1:--h}
declare errorMessage=${2:-}
[[ -n "$errorMessage" ]] && \
errmsg "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
msg "USAGE for $ThisScript v$Version:
$ThisScript EDITME <required> literal [-optional] [-L <log>] [-si] [-n] [-d|-D]
or
$ThisScript [-h|-man]
"
if [[ $style == -man ]]; then
echo -e "
DESCRIPTION: EDITME
OPTIONS:
-L <log>
Specify the path to a log file, or the special value 'off' to disable
logging. By default, all output (stdout and stderr) goes to
EDITME_DEFAULT_LOG
$LogsDir/
NOTE: This script is self-logging. That is, all standard and error
output displayed is also captured in the log file. Use of 'tee' and
redirection with '>' and '2>&1' are unnecessary.
-si Operate silently. All output (stdout and stderr) is redirected to the log
only; no output appears on the terminal. This cannot be used with
'-L off'.
EDITME: This is useful when running from cron, as it prevents automatic
email from being sent by cron directly, as it does when a script called
from cron generates any output. This script is then responsible for
email handling, if any is to be done.
-n No-Op. Prints commands instead of running them.
DEBUGGING OPTIONS:
-d Set debugging verbosity.
-D Set extreme debugging verbosity using bash 'set -x' mode. Implies '-d'.
HELP OPTIONS:
-h Display short command line usage synopsis.
-man Display this documentation manual page.
EXAMPLES:
SEE ALSO:
"
fi
exit 1
}
#==============================================================================
# Command Line Processing
declare -i shiftArgs=0
set +u
while [[ $# -gt 0 ]]; do
case $1 in
(-h) usage -h;;
(-man) usage -man;;
(-si) SilentMode=1;;
(-n) NoOp=1;;
(-L) Log="$2"; shiftArgs=1;;
(-d) Debug=1;; # Debug mode.
(-D) Debug=1; set -x;; # Use bash 'set -x' extreme debugging mode.
(*) usage -h "Unknown arg: $1)";;
esac
# Shift (modify $#) the appropriate number of times.
shift; while [[ $shiftArgs -gt 0 ]]; do
[[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
shiftArgs=$shiftArgs-1
shift
done
done
set -u
#==============================================================================
# Command Line Verification
[[ "$SilentMode" -eq 1 && "$Log" == off ]] && \
usage -h "Cannot use '-si' with '-L off'."
[[ -n "$Log" ]] || \
Log="${LogsDir}/${ThisScript%.sh}.log"
#==============================================================================
# Main Program
for u in $RequiredUtils; do
[[ -n "$(command -v "$u")" ]] || errmsg "Missing required utility: $u"
done
[[ "$ErrorCount" -eq 0 ]] || bail "Aborted early due to missing required utilitiies."
trap terminate EXIT SIGINT SIGTERM
if [[ "$Log" != off ]]; then
# EDITME: Use this block if the default Log name is consistent each time
# this script is called. This rotates the old log with the same name out to
# a timestamp name. If possible, the 'ls' is used to determine the timestamp
# of the old log, using options to 'ls' that should work across a wide range
# of Linux and Mac versions. If a usable timestamp from the old log file
# cannot be determiend, use the current time of rotation, else use the word
# 'old'.
if [[ -e "$Log" ]]; then
# shellcheck disable=SC2012
OldLogTimestamp="$(ls -l --time-style +'%Y%m%d-%H%M%S' "$Log" 2>/dev/null | awk '{print $6}')"
# shellcheck disable=SC2012
[[ -n "$OldLogTimestamp" ]] ||\
OldLogTimestamp="$(ls -l -D '%Y%m%d-%H%M%S' "$Log" 2>/dev/null | awk '{print $6}')"
[[ -n "$OldLogTimestamp" ]] ||\
OldLogTimestamp="$(date +'%Y%m%d-%H%M%S' "$Log" 2>/dev/null)"
[[ -n "$OldLogTimestamp" ]] ||\
OldLogTimestamp="old"
OldLog="${LogsDir}/${ThisScript%.sh}.${OldLogTimestamp].log"
mv -f "$Log" "$OldLog" || bail "Could not do: mv -f \"$Log\" \"$OldLog\""
[[ "$SilentMode" -eq 0 ]] || dbg "Rotated $Log to $OldLog"
fi
touch "$Log" || bail "Couldn't touch log file [$Log]."
# Redirect stdout and stderr to a log file.
if [[ "$SilentMode" -eq 0 ]]; then
exec > >(tee "$Log")
exec 2>&1
else
exec >"$Log"
exec 2>&1
fi
msg "${H1}\\nLog is: $Log\\n"
fi
ThisUser=$(id -n -u)
msg "Started $ThisScript v$Version as $ThisUser@${HOSTNAME%%.*} at $(date).\\nCommand Line was: $CmdLine"
[[ "$NoOp" -eq 1 ]] && msg "NO_OP: Operating in DRY RUN mode."
if [[ $ErrorCount -eq 0 && $WarningCount -eq 0 ]]; then
msg "${H2}\\nAll processing completed successfully.\\n"
elif [[ $ErrorCount -eq 0 ]]; then
msg "${H2}\\nProcessing completed, but with $WarningCount warning. Review the output above carefully.\\n"
else
msg "${H2}\\nProcessing completed, but with $ErrorCount errors and $WarningCount warnings. Scan above output carefully.\\n"
fi
# Display runtime.
msg "That took $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\\n"
# See the terminate() function, where this script actually exits.
exit "$ErrorCount"