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/%d@%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} (www.uxora.com) 0.0.4
#-    author          Michel VONGVILAY
#-    copyright       Copyright (c) http://www.uxora.com
#-    license         GNU General Public License
#-    script_id       12345
#-
#================================================================
#  HISTORY
#     2015/03/01 : mvongvilay : Script creation
#     2015/04/01 : mvongvilay : Add long options and improvements
# 
#================================================================
#  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: "; head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#+" | sed -e "s/^#+[ ]*//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
usagefull() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#[%+-]" | sed -e "s/^#[%+-]//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
scriptinfo() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#-" | sed -e "s/^#-//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=$(head -200 ${0} |grep -n "^# END_OF_HEADER" | 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}.$(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/%d@%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}.tmp.rc";
fileLock="${SCRIPT_DIR_TEMP}/${SCRIPT_NAME%.*}.${SCRIPT_ID}.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}.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/%d@%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 $*"
#sleep 5
  #== 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
<<--OUTPUT--
 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/%d@%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

 IMPLEMENTATION
    version         template.sh (www.uxora.com) 0.0.4
    author          Michel VONGVILAY
    copyright       Copyright (c) http://www.uxora.com
    license         GNU General Public License
    script_id       12345
--OUTPUT--

# Display version info
$ ./template.sh -v
<<--OUTPUT--
 IMPLEMENTATION
    version         template.sh (www.uxora.com) 0.0.4
    author          Michel VONGVILAY
    copyright       Copyright (c) http://www.uxora.com
    license         GNU General Public License
    script_id       12345
--OUTPUT--

 

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"
<<--OUTPUT--
[I] template.sh start at 15/04/09 16H50 with process id 30570 by uxora@localhost.localdomain:/home/uxora (NOLOG)
[C] ls -lrt temp*.sh
[O]      1	-rwxr-x---. 1 uxora uxora 7848 Apr  9 16:50 template.sh
[I] template.sh finished at 16H50 (Time=0s, Error=0, Warning=0, RC=0).
--OUTPUT--

# Execute with log file in different folder and prepend timestamp to log
$ ./template.sh -to /tmp/DEFAULT "temp*.sh"
<<--OUTPUT--
15/04/09@17:01:01[I] template.sh start at 15/04/09 17H01 with process id 30799 by uxora@localhost.localdomain:/home/uxora (LOG=/tmp/template.1504091701.30799.24503.log)
15/04/09@17:01:01[C] ls -lrt temp*.sh
15/04/09@17:01:01[O]      1	-rwxr-x---. 1 uxora uxora 7848 Apr  9 16:50 template.sh
15/04/09@17:01:01[I] template.sh finished at 17H01 (Time=0s, Error=0, Warning=0, RC=0).
--OUTPUT--

# Check generated logfile
$ cat /tmp/template.1504091701.30799.24503.log
<<--OUTPUT--
15/04/09@17:01:01[I] template.sh start at 15/04/09 17H01 with process id 30799 by uxora@localhost.localdomain:/home/uxora (LOG=/tmp/template.1504091701.30799.24503.log)
15/04/09@17:01:01[C] ls -lrt temp*.sh
15/04/09@17:01:01[O]      1	-rwxr-x---. 1 uxora uxora 7848 Apr  9 16:50 template.sh
15/04/09@17:01:01[I] template.sh finished at 17H01 (Time=0s, Error=0, Warning=0, RC=0).
--OUTPUT--

 

Now let's see some error management:

# Execute an error in ls command
$ ./template.sh -o DEFAULT "temp*.sh" file1
<<--OUTPUT--
[I] template.sh start at 15/04/09 16H55 with process id 30640 by uxora@localhost.localdomain:/home/uxora (LOG=/home/uxora/template.1504091655.30640.23959.log)
[C] ls -lrt temp*.sh file1
[O]      1	ls: cannot access file1: No such file or directory
[O]      2	-rwxr-x---. 1 uxora uxora 7848 Apr  9 16:50 template.sh
[E] ERROR: Command failed: ls -lrt temp*.sh file1 (RC=2)
[E] template.sh finished at 16H55 (Time=0s, Error=1, Warning=0, RC=2).
--OUTPUT--

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

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

# Testing the lock file
$ ./template.sh 2>&1 1>/dev/null & sleep 1 && ./template.sh
<<--OUTPUT--
[1] 17454
[E] ERROR: template.sh: /tmp/template.12345.lock: lock file detected
--OUTPUT--

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   

Atishtum Rah
# Problem with lock file test solved.Atishtum Rah 2015-04-28 22:42
Removing the comment on line 264 (see below) gave the second invocation of the script enough time to test if a lock file existed.

You need to add a note about this.

262. #== start your program here ==#
263. exec_cmd "ls -lrt $*"
264. #sleep 5
265. #== end your program here ==#
Reply | Reply with quote | Quote
UxOra DBA
# RE: Problem with lock file test solved.UxOra DBA 2015-05-14 17:14
Sorry for the late reply ... but yes you are right.
You need to uncomment line 264 in order to get the first program still running while executing the second one.
Reply | Reply with quote | Quote
Atishtum Rah
# Lock file not working.Atishtum Rah 2015-04-26 16:24
KSH Version AJM 93u+ 2012-08-01 running on Linux Mint 17.1 (Cinnamon)

The command './template.sh 2>&1 1>/dev/null & sleep 1 && ./template.sh' gives the following display:

[1] 5714
[1]+ Done ./template.sh 2>&1 > /dev/null
template.sh: start 15/04/26@17:16:19 with process id 5751 by atishtum@phoenix:/home/atishtum/Korn (NOLOG)
[C] ls -lrt
[O] 1 total 772
[O] 2 -rw-r--r-- 1 atishtum atishtum 430 Jul 12 2000 ftptest
.
. #lines removed
.
[O] 25 -rwxr-xr-x 1 atishtum atishtum 9113 Apr 26 17:15 template.sh
template.sh finished at 17H16 (Time=0.052s, Error=0, Warning=0, RC=0).

Instead of:

[1] 5714
|[E] ERROR: template.sh: /tmp/template.12345.lock: lock file detected
Reply | Reply with quote | Quote
UxOra DBA
# Script updatedUxOra DBA 2015-04-09 15: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
UxOra DBA
# uploaded fileUxOra DBA 2015-03-31 15:37
uploaded template.zip
Reply | Reply with quote | Quote
Matthias
# Template LicenseMatthias 2015-11-04 09:14
Great template.

In the template you mention the GPL license, so this means the template is under GPL license? If not it would be useful not to mention this license in the template as it prevents the usage of the template for any script which will not be GPL :)
Reply | Reply with quote | Quote
UxOra DBA
# Template LicenseUxOra DBA 2015-11-04 12:33
Thanks, yes under GPL license.

As far as I understand GPL license,
it can be modified and used with anything (GPL or not) as long as it is for personal use, or internal use only in your company, with no obligation to make your source code available.
But if it is used in distributed application, then the whole application needs to be release under GPL with source available.

This license seems to be the right sharing spirit to me.
Reply | Reply with quote | Quote