#! /bin/bash

# (best viewed with tabs every 4 characters)

#   Copyright (C) 2009 Charles
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, US

# Filename: Install
prg_ver='0.3'

# Purpose: 
#	Installs the MyBackup.sh package.  Quick and dirty prior to a .deb being justified.

# Usage: 
#	See usage() function below or run with -h option
#	Expects to find contents of unpacked MyBackup.sh.0.3.tar.gz in current directory:
#		bash_completion.d.file
#		BoM
#		BoM.checksum
#		Install
#		MyBackup.sh
#		MyBackup.sh 0.3 User Guide.pdf
#		bash_completion.d.file
#		sample.cfg
	
# Environment:
#	Developed and tested on ubuntu 8.04.2 desktop 32-bit with
#	* bash 3.2-0ubuntu18

# Support: please contact Charles via 
# https://lists.sourceforge.net/lists/options/dar-support.

# History:
#	7jun9	Charles
#			* Begun, for MyBackup.sh version 0.3

# Wishlist (in approx descending order of importance/triviality):
#   * Add "NewName" optional field in BoM

# Programmers' notes: error and trap handling: 
#   * All errors are fatal and finalise() is called.
#   * At any time, a trapped event may transfer control to finalise().
#   * To allow finalise() to tidy up before exiting, changes that need to be undone are
#     noted with global variables named <change name>_flag and the data required
#     to undo those changes is kept in global variables.
#	* finalise() uses the same functions to undo the changes as are used when they are
#	  undone routinely.
#	* $finalising_flag is used to prevent recursive calls when errors are encountered while finalise() is running,

# Programmers' notes: variable names and values
#	* Directory name variables are called *_dir and have values ending in /
#	* File name variables are called *_afn and have values beginning with /
#	* Logic flag variables are called *_flag and have values YES or NO
#	* $buf is a localised scratch buffer.
#	* $lf is a line feed.
#	* $squote is a single quote.

# Programmers' notes: maximum line length ruler
# -------+---------+---------+---------+---------+---------+---------+---------+
#        10        20        30        40        50        60        70        80

# Programmers' notes: function call tree
#	+
#	|
#	+-- initialise
#	|   |
#	|   +-- ck_BoM
#	|
#	+-- finalise
#
# Utility functions called from various places:
# 	call_msg ck_file msg 

# Function definitions in alphabetical order.  Execution begins after the last function definition.

#--------------------------
# Name: call_msg
# Purpose: calls msg() and, for error messges, if finalise() has not been called, calls it
# $1 - message class
# $2 - message
#--------------------------
function call_msg {

   	msg -l $log_afn "$@"

	if [[ $1 = 'E' ]]; then
		press_enter_to_continue 				# Ensure user has chance to see error message
		if [[ ${finalising_flag:-} != 'YES' ]]; then
			finalise 1
		fi
	fi

	return 0

}  # end of function call_msg

#--------------------------
# Name: ck_BoM
# Purpose: checks bill of materials (BoM) file
# Usage: no arguments or options, expects to find file BoM in current directory.
# Returns: always 0.  On error, does not return.
#--------------------------
function ck_BoM {

	fct "${FUNCNAME[ 0 ]}" 'started'

	local buf
	
	# Ensure readable BoM and BoM checksum files
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	BoM_afn="$PWD/BoM"
	buf="$(ck_file "$BoM_afn":fr 2>&1)"
	[[ "$buf" != '' ]] && call_msg 'E' "Bill of materials: $buf"
	BoM_checksum_afn="$PWD/BoM.checksum"
	buf="$(ck_file "$BoM_checksum_afn":fr 2>&1)"
	[[ "$buf" != '' ]] && call_msg 'E' "BoM checksum: $buf"
	
	# Check checksum
	# ~~~~~~~~~~~~~~
	#call_msg 'I' "DEVELOPMENT: skipping BoM checksum test"
	#fct "${FUNCNAME[ 0 ]}" 'returning'
	#return 0
	cksum_out="$($cksum "$BoM_afn")"
	[[ $? -ne 0 ]] && call_msg 'E' "Problem checksumming '$BoM_afn': $cksum_out"
	checksum_from_cksum="${cksum_out%% *}"
	checksum_from_checksum_file="$( $cat "$BoM_checksum_afn" 2>&1 )"
	if [[ "$checksum_from_cksum" = "$checksum_from_checksum_file" ]]; then
		call_msg 'I' "BoM checksum OK"
	else
		call_msg 'E' "BoM checksum problem: calculated checksum ($checksum_from_cksum) does not match checksum from '$BoM_checksum_afn' ($checksum_from_checksum_file)"
	fi

	fct "${FUNCNAME[ 0 ]}" 'returning'
	return 0

}  # end of function ck_BoM

#--------------------------
# Name: ck_file
# Purpose: for each file listed in the argument list: checks that it is 
#   of the specified type, is reachable, exists and that the user has the 
#   listed permissions.
# Usage: ck_file [ file_name:<filetype><permission>... ]
#	where 
#		file_name is a file name
#		type is b (block special file), f (file) or d (directoy)
#		permissions are one or more of r, w and x.  For example rx
# Returns: a bit screwy; tries to be clever but design intention unclear. Non-zero whne check fails
#--------------------------
function ck_file {

    local buf perm perms retval filetype

    # For each file ...
    # ~~~~~~~~~~~~~~~~~
    retval=0
    while [[ ${#} -gt 0 ]]
    do
	    file=${1%:*}
	    buf=${1#*:}
		filetype="${buf:0:1}"
	    perms="${buf:1:1}"
	    shift

	    if [[ $file = $perms ]]; then
		    echo "$prgnam: ck_file: no permisssions requested for file '$file'" >&2 
		    return 1
	    fi

	    # Is the file reachable and does it exist?
	    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		case $filetype in
			b )
	    		if [[ ! -b $file ]]; then
					echo "file '$file' is unreachable, does not exist or is not a block special file" >&2
					: $(( retval=retval+1 ))
					continue
	   		 	fi
				;;
			f )
	    		if [[ ! -f $file ]]; then
					echo "file '$file' is unreachable, does not exist or is not an ordinary file" >&2
					: $(( retval=retval+1 ))
					continue
	   		 	fi
				;;
			d )
	    		if [[ ! -d $file ]]; then
					echo "directory '$file' is unreachable, does not exist or is not a directory" >&2
					: $(( retval=retval+1 ))
					continue
	   		 	fi
				;;
			* )
		    	echo "$prgnam: ck_file: invalid filetype '$filetype' specified for file '$file'" >&2 
		    	return 1
		esac

	    # Does the file have the requested permissions?
	    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	    buf="$perms"
	    while [[ $buf != '' ]]
	    do
		    perm="${buf:0:1}"
		    buf="${buf:1}"
            case $perm in
	    		r )	
	    		    if [[ ! -r $file ]]; then
	    		        echo "file '$file' does not have requested read permission" >&2
	    		        let retval=retval+1
	    		        continue
	    		    fi
                    ;;
		    	w )
		    		if [[ ! -w $file ]]; then
		    			echo "file '$file' does not have requested write permission" >&2
		    			let retval=retval+1
		    			continue
		    		fi
		    		;;
		    	x )
		    		if [[ ! -x $file ]]; then
		    			echo "file '$file' does not have requested execute permission" >&2
		    			let retval=retval+1
		    			continue
		    		fi
		    		;;
		    	* )
		    		echo "$prgnam: ck_file: unexpected permisssion '$perm' requested for file '$file'" >&2
		    		return 1
		    esac
	    done

    done

return $retval

}  #  end of function ck_file

#--------------------------
# Name: fct
# Purpose: function call trace (for debugging)
# $1 - name of calling function 
# $2 - message.  If it starts with "started" or "returning" then the output is prettily indented
#--------------------------
function fct {

	if [[ $debug = 'NO' ]]; then
		return 0
	fi

	fct_ident="${fct_indent:=}"

	case $2 in
		'started'* )
			fct_indent="$fct_indent  "
			call_msg 'I' "DEBUG: $fct_indent$1: $2"
			;;
		'returning'* )
			call_msg 'I' "DEBUG: $fct_indent$1: $2"
			fct_indent="${fct_indent#  }"
			;;
		* )
			call_msg 'I' "DEBUG: $fct_indent$1: $2"
	esac

}  # end of function fct

#--------------------------
# Name: finalise
# Purpose: cleans up and gets out of here
#--------------------------
function finalise {
	fct "${FUNCNAME[ 0 ]}" 'started'

	local buf msg msgs my_retval retval

	finalising_flag='YES'
	
	# Set return value
	# ~~~~~~~~~~~~~~~~
	# Choose the highest and give message if finalising on a trapped signal
	my_retval="${prgnam_retval:-0}" 
	if [[ $1 -gt $my_retval ]]; then
		my_retval=$1
	fi
	case $my_retval in 
		129 | 130 | 131 | 143 )
			case $my_retval in
				129 )
					buf='SIGHUP'
					;;
				130 )
					buf='SIGINT'
					;;
				131 )
					buf='SIGQUIT'
					;;
				143 )
					buf='SIGTERM'
					;;
			esac
			call_msg 'I' "finalising on $buf"
			;;
	esac

	# Final log messages
	# ~~~~~~~~~~~~~~~~~~
	if [[ $global_warning_flag = 'YES' ]]; then
		msgs="$msgs${lf}There were WARNINGs. Please search log for ' W ' (without the quotes) to ensure they are not significant for your installation"
		if [[ $my_retval -eq 0 ]]; then
			my_retval=1
		fi
	fi
	if [[ "$msgs" != '' ]]; then
		msgs="${msgs#$lf}"		# strip leading linefeed
		call_msg 'E' "$msgs"
	fi
	call_msg 'I' "Exiting with return value $my_retval"
	call_msg -n l 'I' "Log file: $log_afn"

	# Exit, ensuring exit is used, not any alias
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	fct "${FUNCNAME[ 0 ]}" 'exiting'
	\exit $my_retval

}  # end of function finalise

#--------------------------
# Name: initialise
# Purpose: sets up environment and parses command line
#--------------------------
function initialise {

	fct "${FUNCNAME[ 0 ]}" 'started'

	local args buf emsg

	# Utility variables
	# ~~~~~~~~~~~~~~~~~
	lf=$'\n'							# ASCII linefeed, a.k.a newline
	squote="'"

	# Set defaults that may be overriden by command line parsing
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	cfg_dir="/etc/opt/$pkgnam/"
	doc_dir="/usr/share/doc/$pkgnam/"
	emsg=''
	log_dir="/var/opt/$pkgnam/log/"
	prg_dir="/opt/$pkgnam/"
	var_dir="/var/opt/$pkgnam/"

	# Parse command line
	# ~~~~~~~~~~~~~~~~~~
	args="${*:-}"
	while getopts c:D:dhl:p:Vv: opt 2>/dev/null
	do
		case $opt in
			c)
				if [[ ${OPTARG:0:1} != '/' ]]; then
					emsg="${lf}Option -c: invalid value '$OPTARG' Must begin with /"
				fi
				buf="${OPTARG%%*(/)}/"             # silently ensure a single trailing slash
				[[ "$buf" != "$cfg_dir" ]] && cfg_dir_changed_flag='YES'
				cfg_dir="$buf"
				;;
			D)
				if [[ ${OPTARG:0:1} != '/' ]]; then
					emsg="${lf}Option -D: invalid value '$OPTARG' Must begin with /"
				fi
				buf="${OPTARG%%*(/)}/"             # silently ensure a single trailing slash
				[[ "$buf" != "$doc_dir" ]] && doc_dir_changed_flag='YES'
				doc_dir="$buf"
				;;
			d )
				debug='YES'
				;;
			h )
				usage -v
				\exit 0
				;;
			l)
				if [[ ${OPTARG:0:1} != '/' ]]; then
					emsg="${lf}Option -l: invalid value '$OPTARG' Must begin with /"
				fi
				buf="${OPTARG%%*(/)}/"             # silently ensure a single trailing slash
				[[ "$buf" != "$log_dir" ]] && log_dir_changed_flag='YES'
				log_dir="$buf"
				;;
			p)
				if [[ ${OPTARG:0:1} != '/' ]]; then
					emsg="${lf}Option -p: invalid value '$OPTARG' Must begin with /"
				fi
				buf="${OPTARG%%*(/)}/"             # silently ensure a single trailing slash
				[[ "$buf" != "$prg_dir" ]] && prg_dir_changed_flag='YES'
				prg_dir="$buf"
				;;
			V )
				echo "$prgnam version $prg_ver"
				\exit 0
				;;
			v)
				if [[ ${OPTARG:0:1} != '/' ]]; then
					emsg="${lf}Option -v: invalid value '$OPTARG' Must begin with /"
				fi
				buf="${OPTARG%%*(/)}/"             # silently ensure a single trailing slash
				[[ "$buf" != "$var_dir" ]] && var_dir_changed_flag='YES'
				var_dir="$buf"
				;;
			* )
				emsg="${lf}Invalid option '$opt'"
		esac
	done

	# Test for extra arguments
	# ~~~~~~~~~~~~~~~~~~~~~~~~
	shift $(( $OPTIND-1 ))
	if [[ $* != '' ]]; then
        emsg="${lf}Invalid extra argument(s) '$*'"
	fi

	# Report any errors
	# ~~~~~~~~~~~~~~~~~
	if [[ $emsg != '' ]]; then
		echo "$emsg" >&2
        usage
        \exit 1
	fi
	
	# Linux executables
	# ~~~~~~~~~~~~~~~~~
	cat='/bin/cat'
	chmod='/bin/chmod'
	chown='/bin/chown'
	cksum='/usr/bin/cksum'
	cp='/bin/cp'
	date='/bin/date'
	ls='/bin/ls'
	mkdir='/bin/mkdir'
	ps='/bin/ps'
	sed='/bin/sed'
	stty='/bin/stty'

	# Ensure they are all available
	buf="$( ck_file $cat:fx $chmod:fx $cksum:fx $cp:fx $date:fx $ls:fx $mkdir:fx $ps:fx $sed:fx $stty:fx 2>&1 )"
	if [[ $? -ne 0 ]] ; then
		echo "$prgnam: terminating on Linux executable problem(s):$lf$buf" >&2
		\exit 1
	fi

	# Note whether being run from a terminal
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# The "standard" test is to check $PS1 but this test is more reliable
	buf="$( $ps -p $$ -o tty 2>&1 )"
	case $buf in
		*TT*-* )
			interactive_flag='NO'
			;;
		*TT* )
			interactive_flag='YES'
			;;
		* )
			echo "$prgnam: Unable to determine if being run interactively.  ps output was: $buf" >&2
			\exit 1
	esac

	# Ensure current directory is writeable
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	buf="$( ck_file $PWD:dwx 2>&1 )"
	if [[ $? -ne 0 ]] ; then
		echo "$prgnam: Terminating on working directory problem:$lf$buf" >&2
		\exit 1
	fi

	# Set default file and directory creation permissions
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	umask 0022

	# Initialise log file
	# ~~~~~~~~~~~~~~~~~~~
	log_afn="$PWD/Install.log.$( $date +%y-%m-%d@%H:%M:%S)"

	# Up to this point any messages have been given using echo followed by \exit 1.  Now 
	# the essentials for call_msg() and finalise() have been established, all future messages 
	# will be sent using call_msg() and error mesages will then call finalise().

	call_msg 'I' "$prgnam version $prg_ver started with command line '$args'.  Logging to '$log_afn'"

	fct "${FUNCNAME[ 0 ]}" 'started (this message delayed until logging initialised)'

	# Exit if not running interactively
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	if [[ $interactive_flag != 'YES' ]]; then
		call_msg 'E' 'Not running interactively'
	fi

	# Check bill of materials (BoM) file
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	ck_BoM

	fct "${FUNCNAME[ 0 ]}" 'returning'

}  # end of function initialise

#--------------------------
# Name: msg
# Purpose: generalised messaging interface
# Usage: msg [ -l logfile ] [ -n l|s|t ] class msg_text
#    -l logs any error messages to logfile unless -n l overrides
#    -n suppress:
#		l writing to log
#		s writing to stdout or stderr
#		t writing timestamp and I|W|E to log
#    class must be one of I, W or E indicating Information, Warning or Error
#    msg_text is the text of the message
# Return code:  always zero (exits on error)
#--------------------------
function msg {

    local args buf indent line logfile message_text no_log_flag no_screen_flag no_timestamp_flag preamble

    # Parse any options
    # ~~~~~~~~~~~~~~~~~
	args="${@:-}"
	OPTIND=0								# Don't inherit a value
	no_log_flag='NO'
	no_screen_flag='NO'
	no_timestamp_flag='NO'
	while getopts l:n: opt 2>/dev/null
	do
		case $opt in
			l)
				logfile="$OPTARG"
				;;
			n)
				case ${OPTARG:=} in
					'l' )
						no_log_flag='YES'
						;;
					's' )
						no_screen_flag='YES'
						;;
					't' )
						no_timestamp_flag='YES'
						;;
					* )
			    		echo "$prgnam: msg: invalid -n option argument, '$OPTARG'" >&2
			    		\exit 1
				esac
				;;
			* )
			    echo "$prgnam: msg: invalid option, '$opt'" >&2
			    \exit 1
			    ;;
		esac
	done
	shift $(( $OPTIND-1 ))

    # Parse the mandatory positional parameters (class and message)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    class="${1:-}"
    case "$class" in 
    	I | 'E' ) 
	    	;;
    	'W' ) 
			global_warning_flag='YES'
	    	;;
	    * )
	    	echo "$prgnam: msg: invalid arguments: '$args'" >&2
	    	\exit 1
    esac

    message_text="$2"

    # Write to log if required
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    if [[ $logfile != ''  && $no_log_flag == 'NO' ]]
    then
		if [[ $no_timestamp_flag == 'NO' ]]; then
			buf="$( $date +%H:%M:%S ) $class "
		else
			buf=''
		fi
		echo "$buf$message_text" >> $logfile
		if [[ $? -ne 0 ]]
		then
			echo "$prgnam: msg: unable to write to log file '$logfile'" >&2
			\exit 1
		fi
	fi
	
	# Write to stdout or stderr if not suppressed
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	if [[ $no_screen_flag = 'NO' ]]; then
	    case "$class" in 
	    	I  ) 
				echo "$message_text" >&1
		    	;;
		    W  ) 
				echo "WARNING: $message_text" >&1
		    	;;
		    E )
				echo "ERROR: $message_text" >&2
		    	;;
	    esac
	fi

    return 0

}  #  end of function msg

#--------------------------
# Name: press_enter_to_continue
# Purpose: prints "Press enter to continue..." message and waits for user to do so
#--------------------------
function press_enter_to_continue {

	echo "Press enter to continue..."
	read

}  # end of function press_enter_to_continue

#--------------------------
# Name: tty_msg
# Purpose: puts message on /dev/tty and reads user's response.  If empty then continues.  If q or Q then quits
# Usage: tty_msg <message> <quit message>
# <quit message> is optional
# Return value:  always 0
#--------------------------
function tty_msg {

	fct "${FUNCNAME[ 0 ]}" 'started'

	local reply

	while true
	do
		echo "$1  (Press Enter to continue or Q to quit)" > /dev/tty
		read reply
		case "$reply" in
			'q' | 'Q' )
				call_msg 'I' "${2:-User chose to quit when asked '$1'}"
				finalise 0
				;;
			'' )
				break
		esac
	done	

	echo 'Continuing ...' > /dev/tty

	fct "${FUNCNAME[ 0 ]}" 'returning'
	return 0

}  # end of tty_msg

#--------------------------
# Name: usage
# Purpose: prints usage message
#--------------------------
function usage {
	fct "${FUNCNAME[ 0 ]}" 'started'

	echo "usage: $prgnam [-c <cfg dir>} [-d] [-h] [-l <log dir>] [-p <prg dir>] [-V] [-v <var dir>]" >&2
	if [[ ${1:-} = '' ]]
	then
		echo "(use -h for help)" >&2
	else
		echo "  where:
    -c names the configuration directory. Must begin with /. Default /etc/opt/MyBackup.sh/.
       If it does not exist, it will be created.
    -D names the documentation directory. Must begin with /. Default /usr/share/doc/MyBackup.sh/
       If it does not exist, it will be created.
    -d turns debugging trace on.
    -h prints this help and exits.
    -l names the log directory. Must begin with /. Default /var/opt/MyBackup.sh/log/
       If it does not exist, it will be created.
    -p names the program directory. Must begin with /. Default /opt/MyBackup.sh/.
       If it does not exist, it will be created.
    -V prints the program version and exits.
    -v names the variable data directory. Must begin with /. Default /var/opt/MyBackup.sh/.
       If it does not exist, it will be created.
" >&2
	fi

	fct "${FUNCNAME[ 0 ]}" 'returning'

}  # end of function usage

#--------------------------
# Name: main
# Purpose: where it all happens
#--------------------------

# Configure script environment
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
set -o posix
set -o nounset
shopt -s extglob # allow extended pattern matching operators 

# Configure traps
# ~~~~~~~~~~~~~~~
# Set up essentials for finalise() and what it may call before setting traps (because finalise() may be called at any time after then)
debug='NO'
n_dar_jobs=0
pkgnam='MyBackup.sh'
prgnam=${0##*/}			# program name w/o path
global_warning_flag='NO'
trap 'finalise 129' 'HUP'
trap 'finalise 130' 'INT'
trap 'finalise 131' 'QUIT'
trap 'finalise 143' 'TERM'

# Initialise
# ~~~~~~~~~~
initialise "${@:-}"

# Create directories
# ~~~~~~~~~~~~~~~~~~
for dir in $cfg_dir $doc_dir $log_dir $prg_dir $var_dir
do
	buf="$($mkdir -p $dir 2>&1)"
	if [[ "$buf" != '' ]]; then
		call_msg 'W' "Making directory '$dir': $buf"
	fi
done
[[ $global_warning_flag = 'YES' ]] && finalise 1

# For each line in the bill of materials file
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Sample line:
# Type:configuration Name:sample.cfg Checksum:217829900 Permissions:400 Ownership:root:root
exec 3< $BoM_afn 								# set up the bill of materials file for reading on file descriptor 3
while read -u 3 line							# for each line
do
	buf="${line%%#*}"							# strip any comment
	buf="${buf%%*( 	)}"							# strip any trailing spaces and tabs
	buf="${buf##*( 	)}"							# strip any leading spaces and tabs
	if [[ $buf = '' ]]; then
		continue								# empty line
	fi
	eval array=( "$buf" )						# Splits into space-separated fields
	if [[ ${#array[*]} -lt 5 ]]; then
		call_msg 'E' "Bill of materials (BoM) file line does not have at least 5 fields: $line"
	fi

	# For each field in the line
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~
	checksum_from_BoM=''
	name=''
	new_name=''
	ownership=''
	permissions=''
	type=''
	for i in ${!array[*]}
	do
		preamble="${array[ $i ]%%:*}"
		case "$preamble" in
			'Checksum' )
				checksum_from_BoM="${array[ $i ]#*:}"	
				;;
			'Name' )
				name="${array[ $i ]#*:}"	
				;;
			'NewName' )
				new_name="${array[ $i ]#*:}"	
				;;
			'Ownership' )
				ownership="${array[ $i ]#*:}"	
				;;
			'Permissions' )
				permissions="${array[ $i ]#*:}"	
				;;
			'Type' )
				type="${array[ $i ]#*:}"	
				;;
			* )
				call_msg 'E' "Bill of materials (BoM) file line has unrecognised preamble '$preamble'${lf}Should be Type, Name or Checksum.  Line is$lf$line"
		esac
	done

	# BoM data checks
	# ~~~~~~~~~~~~~~~
	[[ "$checksum_from_BoM" = '' ]] && call_msg 'E' "Bill of materials (BoM) file line has no Checksum value.  Line is$lf$line"
	[[ "$name" = '' ]] && call_msg 'E' "Bill of materials (BoM) file line has no Name value.  Line is$lf$line"
	[[ "$ownership" = '' ]] && call_msg 'E' "Bill of materials (BoM) file line has no Ownership value.  Line is$lf$line"
	[[ "$permissions" = '' ]] && call_msg 'E' "Bill of materials (BoM) file line has no Permissions value.  Line is$lf$line"
	[[ "$type" = '' ]] && call_msg 'E' "Bill of materials (BoM) file line has no Type value.  Line is$lf$line"

	# File checks
	# ~~~~~~~~~~~
	buf="$(ck_file "$name":fr 2>&1)"
	if [[ "$buf" = '' ]]; then
		call_msg 'I' ''
		call_msg 'I' "Processing '$name'"
	else
		call_msg 'E' "$buf${lf}Bill of materials (BoM) file line is$lf$line"
	fi
	cksum_out="$($cksum "$name")"
	[[ $? -ne 0 ]] && call_msg 'E' "Problem checksumming $name: $cksum_out"
	checksum_from_cksum="${cksum_out%% *}"
	if [[ "${checksum_from_cksum}" = "$checksum_from_BoM" ]]; then
		call_msg 'I' "Checksum OK"
	else
		call_msg 'E' "$name calculated checksum ($checksum_from_cksum) does not match checksum from BoM ($checksum_from_BoM)"
	fi

	# Set up target directory
	# ~~~~~~~~~~~~~~~~~~~~~~~
	case "$type" in
		'BashCompletion' )
			target_dir='/etc/bash_completion.d/'
			;;
		'Configuration' )
			target_dir="$cfg_dir"
			;;
		'Documentation' )
			target_dir="$doc_dir"
			;;
		'Program' )
			target_dir="$prg_dir"
			;;
		* )
			call_msg 'E' "Bill of materials (BoM) file line has invalid Type.  Line is$lf$line"
	esac

	# Set up target name
	# ~~~~~~~~~~~~~~~~~~
	if [[ "$new_name" = '' ]]; then
		target_name="$name"
	else
		target_name="$new_name"
	fi

	# Modify configuration file(s) to reflect change of variable data directory
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	case "$type" in
		'Configuration' )
			if [[ "${var_dir_changed_flag:-}" = 'YES' ]]; then
				tmp_name="$name.changed_var_dir"
				buf="$($sed "s%# Backup directory: /var/opt/MyBackup.sh/%Backup directory: $var_dir%" $name 2>&1 > "$tmp_name" )"
				[[ "$buf" != '' ]] && call_msg 'E' "Problem editing $name to incorporate -v option directory. $sed output was:$lf$buf"
				name="$tmp_name"
			fi
	esac

	# Copy file, set ownership and permissions
	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	target_afn="$target_dir$target_name"
	buf="$($cp "$name" "$target_afn" 2>&1)"
	[[ "$buf" != '' ]] && call_msg 'E' "Problem copying $name to $target_afn: $buf"
	buf="$($chown "$ownership" "$target_afn" 2>&1)"
	[[ "$buf" != '' ]] && call_msg 'E' "Problem setting ownership on $target_afn: $buf"
	buf="$($chmod "$permissions" "$target_afn" 2>&1)"
	[[ "$buf" != '' ]] && call_msg 'E' "Problem setting permissions on $target_afn: $buf"
	call_msg 'I' "$($ls -l "$target_afn")"

done
exec 3<&- # free file descriptor 3

# Inform user about non default usage
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
call_msg 'I' ''
call_msg 'I' 'Usage notes'
call_msg 'I' '~~~~~~~~~~~'
[[ "${prg_dir_changed_flag:-}" = 'YES' ]] && call_msg 'I' "* The MyBackup.sh script is ${prg_dir}MyBackup.sh.  It has a -h option for help."
[[ "${prg_dir_changed_flag:-}" = 'YES' ]] && call_msg 'I' "* Documentation is in '$doc_dir'".
[[ "${cfg_dir_changed_flag:-}" = 'YES' ]] && call_msg 'I' "* To use the configuration directory you specified, on MyBackup.sh's -c option,$lf  use a full path name beginning with '$cfg_dir' for example -c '${cfg_dir}Daily'."
[[ "${log_dir_changed_flag:-}" = 'YES' ]] && call_msg 'I' "* To use the log directory you specified, call MyBackup.sh with -l '$log_dir'."
call_msg 'I' "* Tyhe next step is to set up configuration file(s) as required.$lf  See '${cfg_dir}sample.cfg' for a commented example."

# Finalise
# ~~~~~~~~
call_msg 'I' ''
finalise 0
