Source code in SkeletonDaemon.sh

To see how this is used return to this tutorial index.

#!/bin/ksh -u
# This is a skeleton for a daemon shell script with short & long argument processing.
# The point about a daemon is that it is not run at a terminal but either started
# at system boot or perhaps from cron.

# It is similar to Skeleton.sh but has some extra standard bits to perform logging
# to a file and option to mail people of success/failure.
# Note that if this is being run from cron that the output will be mailed to the user,
# so there is some sense in generating messages to stderr.

# This probably contains features that you might not want or need, just delete those bits.
# Festures:
# * Logging to a log file or stderr. Verbose & quiet options
# * Send email to named people, perhaps only on error
# * Test mode/switch
# * Site and then user specific startup - set defaults
# * Lock to prevent multiple instances running at once

# Look for 'blah' - you will need to change things there.

# This also works with modern versions of bash

# Copyright (c) Alain D D Williams <addw@phcomp.co.uk> April 2012-2020
# It is expected that you will modify this skeleton for your own use.
# This code is released as free software under the BSD 3 clause license:
#   https://directory.fsf.org/wiki/License:BSD-3-Clause
# SCCS: @(#)SkeletonDaemon.sh	1.6 10/26/20 13:44:53

PROGNAME=${0##*/}

# Log & Mail the error message and abort the script.
# Quiet mode is switched off, ie something always said to stderr.
function Die {
	Error=y
	quiet=0
	Log "$@"
	MailThem "$@"
	exit 2
}

# Generate a note in verbose mode
# Always log to the log file
# If the first arg is a digit, only log to stderr if $verbose is at least that number.
function Note {
	typeset -i level=1
	[[ $# -gt 1 && $1 == [0-9] ]] && level=$1 && shift
	echo "$( date '+%Y''%m%d:%H''%M' ): $@" >> $LogFile
	(( verbose < level )) && return
	echo "$*" >&2
}

# Log to file. Also log to the user (stderr) unless quiet.
# If option '-b' put a blank line out first.
function Log {
	if [[ $# -ge 1 && $1 = -b ]]
	then	echo "" >> $LogFile
		(( quiet == 1 )) || echo "" >&2
		shift
	fi

	echo "$*" >> $LogFile

	(( quiet == 1 )) && return

	echo "$PROGNAME: $*" >&2
}

# Put the contents of a file to the log file.
# Send it to the user unless quiet.
# Args: introduction_string filename end_comment_string
# First output the introduction_string unless it is zero length (emtpy)
# Then the file
# Then the end_comment_string if it is present and not empty
function LogFile {
	[[ -n $1 ]] && echo "$1" >> $LogFile
	cat $2 >> $LogFile
	[[ $# -ge 3 && -n $3 ]] && echo "$3" >> $LogFile

	[[ $quiet = 1 ]] && return

	[[ -n $1 ]] && echo "$1"
	cat $2
	[[ $# -ge 3 && -n $3 ]] && echo "$3"
}

# Log to user & file, to user (stderr) only in verb mode.
# If option '-b' put a blank line out first:
function VerbLog {
	if [[ $# -ge 1 && $1 = -b ]]
	then	echo "" >> $LogFile
		(( verbose == 0 )) || echo "" >&2
		shift
	fi

	echo "$( date '+%Y''%m%d:%H''%M' ): $@" >> $LogFile
	(( verbose == 0 )) && return
	echo "$@" >&2
}

# Mail the arguments to those who we have been asked to email - if any
# Also show disk usage - if mounted
function MailThem {
	[[ -z $MailTo ]] && return

	[[ $Error = n ]] && Worked=Success || Worked=Failure
	( echo "$*"
	  echo ""
	  # Maybe some more information here
	) | mail -s "$PROGNAME: blah, blah: $Worked" $MailTo
}

# Generate a usage message and exit. If there is an argument print it and exit code is 2
function Usage {
	(( $# > 0 )) && echo "$PROGNAME: $*" >&2 && MailThem "$*"

	sed -e 's/^^//' <<-!
	Description ... blah, blah
	Multiple instances running in parallel is prevented by use of: $LockFile
	Usage: $PROGNAME [-opts] [files]
	-h --help
	^	Help message
	-L file	Log file - default $LogFile
	-M addr --mail-to=addr
	^	Mail a success/fail message to addr, space separated list. May be given more than once.
	--mail-only-on-error
	^	Send mail only if/when there is an error. The --mail-to option must be given.
	--na=arg
	^	A long option that needs an argument
	^	Can use space instead of '=': --na arg
	--oa[=arg]
	^	A long option that has an optional argument
	^	Here the '=' cannot be replaced by a space
	-q --quiet
	^	Don't complain about non errors
	-t --test
	^	Use test account and files, blah, blah
	-v	Verbose - say a bit more than usual about what is happening.
	^	Give this more than once for increased verbosity.
	- --	Denotes the end of the options.  Arguments after this will be handled as file names
	^	even if they start with a '-'.
	^	This may either be a '-' or '--'.
	More words, blah, blah
	Version: 1.6 10/26/20
	!
	exit $#
}


# **** Start ****
# Default values
Error=n			# Set to 'y' it something goes wrong
verbose=0		# May be multi level
quiet=0
testing=0
MailTo=			# Who to email results to
MailOnlyOnError=n
LogFile=/var/log/${PROGNAME%.sh}
LockFile=/var/run/${PROGNAME%.sh}.pid

LogFileTest=/tmp/${PROGNAME%.sh}.log
LockFileTest=/tmp/${PROGNAME%.sh}.pid

LockTimeout=$(( 19 * 60 ))	# Assume a program stuck if still around for this long

# Look for a site or user specific configuration file(s) for this application.
# This should be a script that might (typically) set configuration values above.
# Use:
# * /etc/default/program
# * $home/.program - in the home directory of the effective user (not the value in $HOME)
for confroot in /etc/default/ ~$( id --user --name )/.
do	[[ -f $confroot${PROGNAME%.sh} ]] && . $confroot${PROGNAME%.sh}
done

# Long Options, value: 0 - no argument, 1 - required argument, 2 - optional argument
typeset -A LongOpts=([help]=0 [quiet]=0 [mail-only-on-error]=0 [mail-to]=1  [na]=1 [oa]=2 [test]=0)
ShortOpts=:hL:M:qtv

# Parse options, recognise --options
while	[[ $# -ge $OPTIND ]] && eval opt=\${$OPTIND} || break
	[[ $opt == - ]] && shift && break
	if [[ $opt == --?* ]]
	then	opt=${opt#--}
		shift

		# Argument to option ?
		[[ $opt == *=* ]] && OPTARG=${opt#*=} && opt=${opt%=$OPTARG} && hasArg=1 || typeset OPTARG= hasArg=0

		# Check if known option and if it has an argument if it must:
		if [[ -z ${LongOpts[$opt]:-} ]]
		then	OPTARG=$opt && opt='?'
		else	if [[ $hasArg = 0 && ${LongOpts[$opt]} = 1 ]]
			then	# Required argument. Can grab next arg ?
				if (( $# >= $OPTIND ))
				then	OPTARG=$1   && shift
				else	OPTARG=$opt && opt=:
				fi
			fi
			[[ $hasArg = 1 && ${LongOpts[$opt]} = 0 ]] && OPTARG=$opt && opt=::
		fi
		true # for the while

	else	getopts $ShortOpts opt
	fi
do	case "$opt" in
	h|help)	Usage ;;
	q|quiet)quiet=1 ;;
	L)	LogFile="$OPTARG" ;;
	M|mail-to)
		MailTo="$MailTo $OPTARG" ;;
	mail-only-on-error)
		MailOnlyOnError=y ;;
	na)	echo "Option --na has the argument that it needs: '$OPTARG'" ;;
	oa)	if [[ $hasArg = 1 ]]
		then	echo "Option --oa has optional argument: '$OPTARG'"
		else	echo "Option --oa does not have an optional argument"
		fi ;;
	v)	(( verbose++ )) ;;
	t|test)	testing=1 ;;
	::)	Usage "Unexpected argument to option '$OPTARG'" ;;
	:)	Usage "Missing argument to option '$OPTARG'" ;;
	\?)	Usage "Unknown option '$OPTARG'" ;;
	*)	Usage "Internal program error, unrecognised argument '$opt'" ;;
	esac
done
shift $((OPTIND - 1))

# Different parameters when testing.
# Do this early since it will affect where errors are written to.
if (( testing ))
then	LockFile=$LockFileTest
	LogFile=$LogFileTest
	# .....
fi

# Consistency checking:
[[ $MailOnlyOnError = y && -z $MailTo ]] &&
	Die "Option --mail-only-on-error given without the --mail-to option"

# Ensure that there is a writable log file:
[[ -f $LogFile ]] || touch $LogFile || Die "Can't write to logfile: $LogFile"

# Stop 2 instances running at once.
# Put this program's PID into the lockfile
lockfile -r 0 -l $LockTimeout $LockFile 2>/dev/null || Die "Cannot acquire lock on $LockFile, owned by PID $(<$LockFile)"
chmod u+w $LockFile
echo $$ > $LockFile
trap "rm -f $LockFile" EXIT

# Easy to find line in the log file marking the start of a new run:
Log -b "**************** Starting at $( date ). Running as: $( id ). Pid: $$"

Log "verbose=$verbose quiet=$quiet testing=$testing"

# Actually do the work that this is supposed to do:
for a
do	echo "arg='$a'"
done

# Perhaps send email unconditionally, or just on error:
if [[ $MailOnlyOnError = n || $Error = y ]]
then	VerbLog "Sending email to: $MailTo"
	MailThem "Report at end of job"
fi

# Show that this ended and when:
Log "**** Ending $( date ) ****"

# end

Return to this tutorial index.