User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

It's sometimes painful to create a shell script from scratch ...

So here is a script template that can help to start any good shell script.

This template contains header which is use for usage info as well, useful predefined functions and predefined variables, and manage short and long options.

(template.sh zip file is attached in this article)

The script in this article has been tested ...
... on Oracle Linux 7
... with #!/bin/ksh and #!/bin/bash shebang

Script source code

Template.sh is a template shell script which is also available for downloading at the end of this article.

#!/bin/ksh
#================================================================
# HEADER
#================================================================
#% SYNOPSIS
#+    ${SCRIPT_NAME} [-hv] [-o[file]] args ...
#%
#% DESCRIPTION
#%    This is a script template
#%    to start any good shell script.
#%
#% OPTIONS
#%    -o [file], --output=[file]    Set log file (default=/dev/null)
#%                                  use DEFAULT keyword to autoname file
#%                                  The default value is /dev/null.
#%    -t, --timelog                 Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S")
#%    -x, --ignorelock              Ignore if lock file exists
#%    -h, --help                    Print this help
#%    -v, --version                 Print script information
#%
#% EXAMPLES
#%    ${SCRIPT_NAME} -o DEFAULT arg1 arg2
#%
#================================================================
#- IMPLEMENTATION
#-    version         ${SCRIPT_NAME} 0.0.5
#-    author          Michel VONGVILAY (https://www.uxora.com)
#-    license         CC-BY-SA Creative Commons License
#-    script_id       0
#-
#================================================================
#  HISTORY
#     2015/03/01 : mvongvilay : Script creation
#     2015/04/01 : mvongvilay : Add long options and improvements
#     2017/09/01 : mvongvilay : Licence change and minor code change
# 
#================================================================
#  DEBUG OPTION
#    set -n  # Uncomment to check your syntax, without execution.
#    set -x  # Uncomment to debug this shell script
#
#================================================================
# END_OF_HEADER
#================================================================
trap 'error "${SCRIPT_NAME}: FATAL ERROR at $(date "+%HH%M") (${SECONDS}s): Interrupt signal intercepted! Exiting now..."
	2>&1 | tee -a ${fileLog:-/dev/null} >&2 ;
	exit 99;' INT QUIT TERM
trap 'cleanup' EXIT

#============================
#  FUNCTIONS
#============================
  #== exec_cmd function ==#
exec_cmd() {
	{
		${*}
		rc_save
	} 2>&1 | fecho CAT "${*}"
	rc_restore
	rc_assert "Command failed: ${*}"
	return $rc ;
}

  #== fecho function ==#
fecho() {
	myType=${1} ; shift ;
	[[ ${SCRIPT_TIMELOG_FLAG:-0} -ne 0 ]] && printf "$( date ${SCRIPT_TIMELOG_FORMAT} )"
	printf "[${myType%[A-Z][A-Z]}] ${*}\n"
	if [[ "${myType}" = CAT ]]; then
		if [[ ${SCRIPT_TIMELOG_FLAG:-0} -eq 0 ]]; then
			cat -un - | awk '$0="[O] "$0; fflush();' ;
		elif [[ "${GNU_AWK_FLAG}" ]]; then # fast - compatible linux
			cat -un - | awk -v tformat=${SCRIPT_TIMELOG_FORMAT#+} '$0=strftime(tformat)"[O] "$0; fflush();' ; 
		else # average speed and more resource intensive- compatible unix/linux
			cat -un - | while read LINE; do \
				[[ ${OLDSECONDS:=$(( ${SECONDS}-1 ))} -lt ${SECONDS} ]] && OLDSECONDS=$(( ${SECONDS}+1 )) \
				&& TSTAMP="$( date ${SCRIPT_TIMELOG_FORMAT} )"; printf "${TSTAMP}[O] ${LINE}\n"; \
			done 
		fi
	fi
}

  #== custom function ==#
check_cre_file() {
	myFile=${1}
	[[ "${myFile}" = "/dev/null" ]] && return 0
	[[ -e ${myFile} ]] && error "${SCRIPT_NAME}: ${myFile}: File already exists" && return 1
	touch ${myFile} 2>&1 1>/dev/null
	[[ $? -ne 0 ]] && error "${SCRIPT_NAME}: ${myFile}: Cannot create file" && return 2
	rm -f ${myFile} 2>&1 1>/dev/null
	[[ $? -ne 0 ]] && error "${SCRIPT_NAME}: ${myFile}: Cannot delete file" && return 3
	return 0
}

#============================
#  ALIAS AND FUNCTIONS
#============================

  #== error management function ==#
info() { fecho INF "${*}"; }
warning() { fecho WRN "WARNING: ${*}" 1>&2; countWrn=$(( ${countWrn} + 1 )); }
error() { fecho ERR "ERROR: ${*}" 1>&2; countErr=$(( ${countErr} + 1 )); }
cleanup() { [[ -e "${fileRC}" ]] && rm ${fileRC}; [[ -e "${fileLock}" ]] && [[ "$( head -1 ${fileLock} )" = "${EXEC_ID}" ]] && rm ${fileLock}; }
scriptfinish() { [[ $rc -eq 0 ]] && endType="INF" || endType="ERR";
	fecho ${endType} "${SCRIPT_NAME} finished at $(date "+%HH%M") (Time=${SECONDS}s, Error=${countErr}, Warning=${countWrn}, RC=$rc).";
	exit $rc ; }

  #== usage function ==#
usage() { printf "Usage: "; scriptinfo usg ; }
usagefull() { scriptinfo ful ; }
scriptinfo() { headFilter="^#-"
	[[ "$1" = "usg" ]] && headFilter="^#+"
	[[ "$1" = "ful" ]] && headFilter="^#[%+]"
	[[ "$1" = "ver" ]] && headFilter="^#-"
	head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "${headFilter}" | sed -e "s/${headFilter}//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g"; }

  #== inter program return code function ==#
rc_save() { rc=$? && echo $rc > ${fileRC} ; }
rc_restore() { [[ -r "${fileRC}" ]] && rc=$(cat ${fileRC}) ; }
rc_assert() { [[ $rc -ne 0 ]] && error "${*} (RC=$rc)"; }

#============================
#  FILES AND VARIABLES
#============================

  #== general variables ==#
SCRIPT_HEADSIZE=$(grep -sn "^# END_OF_HEADER" ${0} | head -1 | cut -f1 -d:)
SCRIPT_ID="$(scriptinfo | grep script_id | tr -s ' ' | cut -d' ' -f3)"
SCRIPT_NAME="$(basename ${0})" # scriptname without path
SCRIPT_UNIQ="${SCRIPT_NAME%.*}.${SCRIPT_ID}.${HOSTNAME%%.*}"
SCRIPT_UNIQ_DATED="${SCRIPT_UNIQ}.$(date "+%y%m%d%H%M%S").${$}"
SCRIPT_DIR="$( cd $(dirname "$0") && pwd )" # script directory
SCRIPT_DIR_TEMP="/tmp" # Make sure temporary folder is RW

SCRIPT_TIMELOG_FLAG=0
SCRIPT_TIMELOG_FORMAT="+%y/%m/%[email protected]%H:%M:%S"

HOSTNAME="$(hostname)"
FULL_COMMAND="${0} $*"
EXEC_DATE=$(date "+%y%m%d%H%M%S")
EXEC_ID=${$}
GNU_AWK_FLAG="$(awk --version 2>/dev/null | head -1 | grep GNU)"

fileRC="${SCRIPT_DIR_TEMP}/${SCRIPT_UNIQ_DATED}.tmp.rc";
fileLock="${SCRIPT_DIR_TEMP}/${SCRIPT_UNIQ}.lock"
fileLog="/dev/null"
rc=0;

countErr=0;
countWrn=0;

  #== option variables ==#
flagOptErr=0
flagOptLog=0
flagOptTimeLog=0
flagOptIgnoreLock=0

#============================
#  PARSE OPTIONS WITH GETOPTS
#============================

  #== set short options ==#
SCRIPT_OPTS=':o:txhv-:'

  #== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
	[timelog]=t
	[ignorelock]=x
	[output]=o
	[help]=h
	[man]=h
)

  #== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
	#== translate long options to short ==#
	if [[ "x$OPTION" == "x-" ]]; then
		LONG_OPTION=$OPTARG
		LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
		LONG_OPTIND=-1
		[[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
		[[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
		OPTION=${ARRAY_OPTS[$LONG_OPTION]}
		[[ "x$OPTION" = "x" ]] &&  OPTION="?" OPTARG="-$LONG_OPTION"
		
		if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
			if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then 
				OPTION=":" OPTARG="-$LONG_OPTION"
			else
				OPTARG="$LONG_OPTARG";
				if [[ $LONG_OPTIND -ne -1 ]]; then
					[[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
					shift $OPTIND
					OPTIND=1
				fi
			fi
		fi
	fi

	#== options follow by another option instead of argument ==#
	if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then 
		OPTARG="$OPTION" OPTION=":"
	fi

	#== manage options ==#
	case "$OPTION" in
		o ) fileLog="${OPTARG}"
			[[ "${OPTARG}" = *"DEFAULT" ]] && fileLog="$( echo ${OPTARG} | sed -e "s/DEFAULT/${SCRIPT_UNIQ_DATED}.log/g" )"
			flagOptLog=1
		;;
		
		t ) flagOptTimeLog=1
			SCRIPT_TIMELOG_FLAG=1
		;;
		
		x ) flagOptIgnoreLock=1
		;;
		
		h ) usagefull
			exit 0
		;;
		
		v ) scriptinfo
			exit 0
		;;
		
		: ) error "${SCRIPT_NAME}: -$OPTARG: option requires an argument"
			flagOptErr=1
		;;
		
		? ) error "${SCRIPT_NAME}: -$OPTARG: unknown option"
			flagOptErr=1
		;;
	esac
done
shift $((${OPTIND} - 1)) ## shift options

#============================
#  MAIN SCRIPT
#============================

[ $flagOptErr -eq 1 ] && usage 1>&2 && exit 1 ## print usage if option error and exit

  #== Check/Set arguments ==#
[[ $# -gt 2 ]] && error "${SCRIPT_NAME}: Too many arguments" && usage 1>&2 && exit 2

  #== Check files ==#
check_cre_file ${fileRC}  || exit 3
check_cre_file ${fileLog} || exit 3
if [[ ${flagOptIgnoreLock} -eq 0 ]]; then
	[[ -e ${fileLock} ]] && error "${SCRIPT_NAME}: ${fileLock}: lock file detected" && exit 4
	check_cre_file ${fileLock} || exit 4
fi

  #== Create files ==#
[[ "${fileLog}" != "/dev/null" ]] && touch ${fileLog} && fileLog="$( cd $(dirname "${fileLog}") && pwd )"/"$(basename ${fileLog})"
[[ ! -e ${fileLock} ]] && echo "${EXEC_ID}" > ${fileLock}

  #== Main part ==#
  #===============#
{ trap 'kill -TERM ${$}; exit 99;' TERM

info "${SCRIPT_NAME}: start $(date "+%y/%m/%[email protected]%H:%M:%S") with process id ${EXEC_ID} by ${USER}@${HOSTNAME}:${PWD}"\
	$( [[ ${flagOptLog} -eq 1 ]] && echo " (LOG: ${fileLog})" || echo " (NOLOG)" );

  #== start your program here ==#
exec_cmd "ls -lrt $*"
info "Sleeping for 2 seconds ..." && sleep 2
  #== end   your program here ==#

scriptfinish ; } 2>&1 | tee ${fileLog}

  #== End ==#
  #=========#
rc_restore
exit $rc

Check how this script template works

Display help with the followins options -h --help --man
and display script information only with -v --version

# Display help
$ ./template.sh --help
     SYNOPSIS
        template.sh [-hv] [-o[file]] args ...

     DESCRIPTION
        This is a script template
        to start any good shell script.

     OPTIONS
        -o [file], --output=[file]    Set log file (default=/dev/null)
                                      use DEFAULT keyword to autoname file
                                      The default value is /dev/null.
        -t, --timelog                 Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S")
        -x, --ignorelock              Ignore if lock file exists
        -h, --help                    Print this help
        -v, --version                 Print script information

     EXAMPLES
        template.sh -o DEFAULT arg1 arg2


# Display version info
$ ./template.sh -v
     IMPLEMENTATION
        version         template.sh 0.0.5
        author          Michel VONGVILAY (https://www.uxora.com)
        license         CC-BY-SA Creative Commons License
        script_id       0

Use -o or --output options to specify a output log file, otherwise no log file is created.
Log file should contains exactly what you see in terminal output.

# Execute with no log
$ ./template.sh "temp*.sh"
    [I] template.sh: start 17/09/[email protected]:52:23 with process id 3311 by [email protected]:/mnt/nfs/uxora_share (NOLOG)
    [C] ls -lrt temp*.sh
    [O]      1      -rwxrwxrwx 1 1026 users 8929 Sep 14 14:37 template.sh
    [I] Sleeping for 2 seconds ...
    [I] template.sh finished at 14H52 (Time=2.040s, Error=0, Warning=0, RC=0).


# Execute with log file in different folder and prepend timestamp to log
$ ./template.sh -to /tmp/DEFAULT "temp*.sh"
    17/09/[email protected]:52:55[I] template.sh: start 17/09/[email protected]:52:55 with process id 3492 by [email protected]:/mnt/nfs/uxora_share (LOG: /tmp/template.0.oralab01.170914145255.3492.log)
    17/09/[email protected]:52:55[C] ls -lrt temp*.sh
    17/09/[email protected]:52:55[O]      1     -rwxrwxrwx 1 1026 users 8929 Sep 14 14:37 template.sh
    17/09/[email protected]:52:55[I] Sleeping for 2 seconds ...
    17/09/[email protected]:52:57[I] template.sh finished at 14H52 (Time=2.056s, Error=0, Warning=0, RC=0).


# Check generated logfile
$ cat /tmp/template.0.oralab01.170914145255.3492.log
    17/09/[email protected]:52:55[I] template.sh: start 17/09/[email protected]:52:55 with process id 3492 by [email protected]:/mnt/nfs/uxora_share (LOG: /tmp/template.0.oralab01.170914145255.3492.log)
    17/09/[email protected]:52:55[C] ls -lrt temp*.sh
    17/09/[email protected]:52:55[O]      1     -rwxrwxrwx 1 1026 users 8929 Sep 14 14:37 template.sh
    17/09/[email protected]:52:55[I] Sleeping for 2 seconds ...
    17/09/[email protected]:52:57[I] template.sh finished at 14H52 (Time=2.056s, Error=0, Warning=0, RC=0).

Now let's see some error management:

# Execute an error in ls command
$ ./template.sh -o DEFAULT "temp*.sh" file1
    [I] template.sh: start 17/09/[email protected]:55:34 with process id 4321 by [email protected]:/mnt/nfs/uxora_share (LOG: /mnt/nfs/uxora_share/template.0.oralab01.170914145534.4321.log)
    [C] ls -lrt temp*.sh file1
    [O]      1      ls: cannot access file1: No such file or directory
    [O]      2      -rwxrwxrwx 1 1026 users 8929 Sep 14 14:37 template.sh
    [E] ERROR: Command failed: ls -lrt temp*.sh file1 (RC=2)
    [I] Sleeping for 2 seconds ...
    [E] template.sh finished at 14H55 (Time=2.070s, Error=1, Warning=0, RC=2).


# Execute with more than two args
$ ./template.sh -o /tmp/test.log "temp*.sh" file2 file3
    [E] ERROR: template.sh: Too many arguments
    Usage:     template.sh [-hv] [-o[file]] args ...


# Execute with invalid option
$ ./template.sh --invalidopt
    [E] ERROR: template.sh: --invalidopt: unknown option
    Usage:     template.sh [-hv] [-o[file]] args ...


# Testing the lock file
$ ./template.sh 2>&1 1>/dev/null & sleep 1 && ./template.sh
    [1] 5585
    [E] ERROR: template.sh: /tmp/template.0.oralab01.lock: lock file detected

# Interrupting with Ctrl+C signal
$ ./template.sh "temp*.sh"
    [I] template.sh: start 17/09/[email protected]:01:45 with process id 6567 by [email protected]:/mnt/nfs/uxora_share (NOLOG)
    [C] ls -lrt temp*.sh
    [O]      1      -rwxrwxrwx 1 1026 users 8929 Sep 14 14:37 template.sh
    [I] Sleeping for 2 seconds ...
    ^C[E] ERROR: template.sh: FATAL ERROR at 15H01 (0.536s): Interrupt signal intercepted! Exiting now...

In the output, you can see notice each line starts with a kind of tag :

  • [I] for Information
  • [C] for executed Command
  • [O] for Output of the executed command
  • [E] for Error
  • [W] for Warning

This script should catch the following signal:

  • INT signal, which is usually sent by CTL+C
  • TERM signal, which can be sent with kill -TERM -<script PID>

 

Please leave comments and suggestions,
Michel.

Attachments:
Download this file (template.zip)template.zip3 kB

Enjoyed this article? Please like it or share it.

Add comment

Please connect with one of social login below (or fill up name and email)

     


Security code
Refresh

Comments   

UxOra DBA
# Script update 2UxOra DBA 2017-09-15 01:48
Script template has been updated:
  • Change licence to CC-BY-SA Creative Commons License
  • Minor code changes

template.zip has been updated as well.
Reply | Reply with quote | Quote
UxOra DBA
# Script updatedUxOra DBA 2015-04-09 17:24
Script template has been updated and improved:
  • to be able to parse short and long options
  • to be compatible with ksh and bash
  • add lock file management
  • improve trap and his propagation
  • by adding an option to prepend timestamp to log output
  • to be able use DEFAULT keyword with different path

template.zip has been updated as well.
Reply | Reply with quote | Quote