#!/bin/bash
#
# This file is sourced by other sdm scripts
#

#
# Common functions 
#
function errexit() {
    echo -e "$1"
    exit 1
}

function ferrexit() {
    [ "$1" != "" ] && printf "$1"
    exit 1
}

function exitiferr() {
    [ "${1:0:1}" == "?" ] && errexit "$1"
}

function errifrc() {
    [ $1 -ne 0 ] && errexit "$2"
}

function warnifrc() {
    # Requires logtoboth active
    [ $1 -ne 0 ] && logtoboth "$2 ($1)"
}

function logit {
    #
    # Writes message to /etc/sdm/history
    #
    # $1="message string"
    #
    [ -f $SDMPT/etc/sdm/history ] && echo "$(thisdate) $1" >> $SDMPT/etc/sdm/history
}

function logtoboth() {
    #
    # Writes the message to the terminal and also to /etc/sdm/history
    #
    # $1="message string" which will be split up across multiple lines if needed
    # $2="nosplit" if lines should not be split up
    #
    local msg="$1"
    local str=() i spc=""
    if [[ ${#msg} -le $logwidth ]] || [[ "$2" == "nosplit" ]]
    then
	logit "$msg"
	echo "$msg"
    else
	readarray -t str <<< $(fold -s -w$logwidth <<< $(echo "$msg"))
	for (( i=0 ; i < ${#str[@]} ; i++))
	do
	    logit "${spc}${str[$i]}"
	    echo "${spc}${str[$i]}"
	    spc="  "
	done
    fi	
}

function logtobothex() {
    #
    # Write message via logtoboth and then exit 1
    #
    local msg="$1" nosplit="$2"
    logtoboth "$msg" "$nosplit"
    exit 1
}

function bootlog() {
    # Write string in $1 to the system log/journal and /etc/sdm/history.log
    logger "sdm FirstBoot: $1"
    logit "> sdm FirstBoot: $1"
}

function bootlogx() {
    # Write string only to the system log
    logger "sdm FirstBoot: $1"
}

function write_console() {
    #
    # $1 string to write
    # Written to /dev/console as "\n$(thisdate) sdm FirstBoot: $1"
    echo -e "\n$(thisdate) sdm FirstBoot: $1" > /dev/console
}

function write_console0() {
    #
    # $1 string to write
    # Written to /dev/console as "$1"
    echo -e "$1" > /dev/console
}

function askyn() {
    local ans
    echo -n "$1" '[y/n]? ' ; read $2 ans
    case "$ans" in
        y*|Y*) return 0 ;;
        *) return 1 ;;
    esac
}

function outlong () {
    #
    # Write the string in $1 to the file in $2
    # If the line is too long, it will be split up
    #
    local str=() i spc="" lw=${logwidth:-96}
    if [ ${#1} -le $lw ]
    then
	echo "$(date +'%Y-%m-%d %H:%M:%S') $1" >> $2
    else
	readarray -t str <<< $(fold -s -w96 <<< $(echo $1))
	for (( i=0 ; i < ${#str[@]} ; i++))
	do
	    echo "$(date +'%Y-%m-%d %H:%M:%S') ${spc}${str[$i]}" >> $2
	    spc="  "
	done
    fi
}

function logapterror() {
    logtobothex "? apt returned an error; review /etc/sdm/apt.log"
}

function doapt() {
    #
    # $1 is apt command
    # $2 is $showapt value
    # $3 is optional apt command [D:apt-get]
    #
    function pline() {
	local line
	while read line
	do
            echo "$(thisdate) $line" >> /etc/sdm/apt.log
	done
    }

    local aptcmd="$3" sts td
    [ "$aptcmd" == "" ] && aptcmd="apt-get -qq"
    echo "" >> /etc/sdm/apt.log
    outlong "$aptcmd $1" "/etc/sdm/apt.log"
    echo "" >> /etc/sdm/apt.log
    if [ "$2" == "1" ]
    then
	DEBIAN_FRONTEND=noninteractive $aptcmd -o=Dpkg::Use-Pty=0 $1 2>&1 | tee -a /etc/sdm/apt.log
	sts=$?
    else
	if [[ "$poptions" =~ "nologdates" ]]
	then
	    DEBIAN_FRONTEND=noninteractive $aptcmd -o=Dpkg::Use-Pty=0 $1 >> /etc/sdm/apt.log 2>&1
	    sts=$?
	else
	    # Technique from https://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another/73180#73180
	    { { { { DEBIAN_FRONTEND=noninteractive $aptcmd -o=Dpkg::Use-Pty=0 $1 2>&1; echo $? >&3; } | pline >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
	    sts=$?
	fi
    fi
    [[ "$poptions" =~ "nologdates" ]] && td="" || td="$(thisdate) "
    echo "${td}[Done]" >> /etc/sdm/apt.log
    return $sts
}

function doaptrpterror() {
    #
    # $1 is apt command
    # $2 is $showapt value
    #
    # Note: Reports and exits if error
    #
    doapt "$1" "$2" || { sts=$? ; logapterror ; return $sts ; }
}

function runcaptureout() {
    #
    # Run command, capturing output via logtoboth
    #
    local cmd="$1"

    function pline() {
        local line
        while read line
        do
            logtoboth "$line"
        done
    }

    { { { {  $cmd 2>&1; echo $? >&3; } | pline >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
}

function setfileownmode() {
    #
    # $1 is full path to file
    # $2 is file mode (defaults to 0755)
    # $2 is file owner (defaults to root:root)
    local fp="$1" fm="$2" fo="$3"
    [ "$fm" == "" ] && fm="755"
    [ "$fo" == "" ] && fo="root:root"
    chown $fo $fp
    chmod $fm $fp
}

function pkgexists() {
    #
    # $1 has apt package name to check
    #
    pkg=$1
    [ "$($sudo apt-cache showpkg $pkg 2>/dev/null)" != "" ] && return 0 || return 1
}

function ispkginstalled() {
    #
    # $1 has package name
    #
    iver=$(apt-cache policy $1 | grep Installed: 2> /dev/null)
    if [ "$iver" == "" ]
    then
        return 1
    else
        [[ "$iver" =~ "(none)" ]] && return 1 || return 0
    fi
    return
}

function installpkgsif() {
    local pkglist="$1" pkgs=() newpkgs="" p

    IFS=" " read -a pkgs <<< $pkglist
    for p in "${pkgs[@]}"
    do
	ispkginstalled $p || newpkgs="$newpkgs $p"
    done
    [ "$newpkgs" == "" ] || doaptrpterror "install --no-install-recommends --yes $newpkgs" $showapt
}

function getaptver() {
    # $1: package name
    # $2: 'installed' or 'candidate'
    #
    # returns requested version string or "" if doesn't exist
    local pkg="$1" vertype="$2" sstr

    case $vertype in
	installed) sstr="Installed:"
		   ;;
	candidate) sstr="Candidate:"
		   ;;
    esac
    while read line
    do
        if [[ "$line" =~ "$sstr" ]]  && [[ ! "$line" =~ "(none)" ]]
            then
                ver="${line#*: }"
		echo "$ver"
		return
        fi
    done < <($sudo apt policy $pkg 2>/dev/null)
    echo ""
    return
}

function getpipver() {
    # $1: package name
    [ "$(type -p pip3)" == "" ] && echo "" && return
    echo "$($sudo pip3 list 2>>/dev/null | grep $1 | (read mname mver ; echo $mver))"
    return
}

function installviapip() {
    function pline() {
	local line
	while read line
	do
            echo "$(thisdate) $line" >> /etc/sdm/apt.log
	done
    }
    # $1: package name
    # $2: /path/to/pip3 in the venv
    # $3: pip install options
    # $4: Message to logtoboth
    local pkg=$1 vpip3="$2" options="$3" msg="$4"
    # Use --ignore-installed b/c we def want it to be installed!
    logtoboth "$msg"
    echo "" >> /etc/sdm/apt.log
    outlong "$vpip3 install --ignore-installed $pkg" /etc/sdm/apt.log
    echo "" >> /etc/sdm/apt.log
    if [[ "$poptions" =~ "nologdates" ]]
    then
	$vpip3 install --ignore-installed $pkg 2>&1 | tee -a /etc/sdm/apt.log
	sts=$?
    else
	# Technique from https://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another/73180#73180
	{ { { { $vpip3 install --ignore-installed $pkg 2>&1; echo $? >&3; } | pline >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
	sts=$?
    fi
    [[ "$poptions" =~ "nologdates" ]] && td="" || td="$(thisdate) "
    echo "${td}[Done] [$sts]" >> /etc/sdm/apt.log
    return $sts
}

function thisdate() {
    echo  "$(date +"$datefmt")"
}

function ord() {
    #
    # Returns the value in decimal of the character in $1
    # e.g. c="a" ; echo $(ord $c) will print 97
    #
    printf '%d' "'$1"
}

function isactive() {
    #
    # Returns "0" if service in $1 is active
    #
    systemctl --quiet is-active $1
    sts=$?
    echo "$sts"
    return $sts
}

function getfilegroup() {
    #
    # $1: /path/to/file
    #
    # Returns the string for the group owning the file
    #
    local tfile="$1" gx
    gx=$(stat -c '%G' $tfile)
    [ "$gx" == "" ] && gx=users
    echo $gx
}

function getgbstr() {
    #
    # $1: # of bytes in partition
    #
    # Returns the string "(nn.nnGB, mm.mmGiB)"

    local nbytes=$1
    local gb=1000000000 gib=1073741824 gb2=500000000 gi2=536870912
    local ngbytes ngibytes 
    ngbytes=$(printf %.1f "$(( ((10 * $nbytes)+$gb2) / $gb ))e-1")
    ngibytes=$(printf %.1f "$(( ((10 * $nbytes)+$gi2) / $gib))e-1")
    echo "(${ngbytes}GB, ${ngibytes}GiB)"
    return
}

function getfsdf() {
    #
    # $1: fs name
    # $2: df component: pcent, avail, etc
    #
    echo $(df --output=$2 $1 | tail -1 | (IFS="%" ; read a ; a="${a% }" ; a="${a# }" echo $a))
}

function logfreespace() {
    #
    # Logs the current free space on $dimg
    #
    local dev="/" extramsg="$1" free1k freeby dused
    [ "$SDMPT" != "" ] && dev="$SDMPT"
    free1k=$(getfsdf $dev avail) 
    freeby=$(($free1k*1024))
    logtoboth "> $dimgdevname '$dimg' has $free1k 1K-blocks $(getgbstr $freeby) free $extramsg" nosplit
    dused=$(getfsdf / pcent)
    if [ $dused -ge 98 ]
    then
	logtoboth "!!!"
	logtoboth "% $dimgdevname '$dimg' is ${dused}% full" nosplit
	logtoboth "% Review /etc/sdm/apt.log in the IMG for insufficient disk space errors" nosplit
	logtoboth "% If needed use --extend --xmb nnnn with --customize to create more space in the IMG" nosplit
	logtoboth "% Customization terminated due to low disk space condition" nosplit
	logtoboth "!!!"
	exit 1
    fi
}

function do_raspiconfig() {
    #
    # $1=command
    # $2=value
    local cmd=$1 value=$2
    if type -P raspi-config > /dev/null
    then
	SUDO_USER=${myuser:-nobody} raspi-config $cmd "$value" nonint # prefer to not block outputs! > /dev/null 2>&1
    else
	logtoboth "% Unable to find raspi-config for function '$cmd' with value '$value'"
    fi
}

function configitemlog() {
    # $1: Message
    # $2: function to call
    local msg=$1 fn=$2
   if [ "$fn" != "" ]
    then
	if [ "$(type -t "$fn")" == "function" ]
	then
	    [ "$fn" == "logtoboth" ] && msg="> $msg"
	    $fn "$msg"
	else
	    echo "% Unrecognized config item log function: $fn"
	fi
    fi
}

function doconfigitem() {
    #
    # $1: function keyword
    # $2: value
    # $3: "" or "bootlog" (first boot) or "logtoboth" (phase1)
    # NOTE; first boot only at the moment!
    local rpifun=$1 value=$2 msgfn=$3 tval
    case "$rpifun" in
	# * do_resolution still needs to be sorted out
	serial)
	    logtoboth "% Consider using the sdm 'serial' plugin to control the serial configuration"
	    configitemlog "Set Serial Port to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_serial $value nonint
	    do_raspiconfig do_serial $value nonint
	    ;;
	delayed_boot_behavior)
	    # Processed at the very end of FirstBoot
	    ;;
	boot_behavior|boot_behaviour)  # Allow US spelling as well ;)
	    configitemlog "Set boot_behaviour '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_boot_behaviour $value nonint
	    [ "$msgfun" == "bootlog" ] && do_raspiconfig do_boot_behaviour $value || setdelayedbbh "$value"
	    ;;
	vnc_resolution)
	    configitemlog "Set VNC resolution to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_vnc_resolution $value nonint
	    do_raspiconfig do_vnc_resolution $value nonint
	    ;;
	powerled)
	    ssys=0
	    (grep -q "\\[actpwr\\]" /sys/class/leds/led0/trigger > /dev/null 2>&1) && ssys=1
	    (grep -q "\\[default-on\\]" /sys/class/leds/led0/trigger > /dev/null 2>&1) && ssys=1
	    if [ $ssys -eq 1 ]
	    then
		configitemlog "Set Power LED to '$value'" $msgfn
		#SUDO_USER=${myuser:-pi} raspi-config do_leds $value nonint
		do_raspiconfig do_leds $value
	    else
		configitemlog "This Pi does not support setting the Power LED; Skipped" $msgfn
	    fi
	    ;;
	audio|pi4video|boot_splash|boot_order|\
	    spi|i2c|boot_wait|net_names|overscan|blanking|\
	    pixdub|overclock|rgpio|camera|onewire)
	    # These are simple on/off and less commonly used so no elaborate logging for them
	    configitemlog "Set $rpifun to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_$rpifun $value nonint
	    do_raspiconfig do_$rpifun $value 
	    ;;
	fstab)
	    configitemlog "Append fstab extension '$value' to /etc/fstab" $msgfn
	    cat $value >> /etc/fstab
	    ;;
	overlayfs)
	    [[ "$2" == "" ]] || [[ "$2" == "0" ]] && tval="ro" || tval=$2
	    $msgfn "Enable overlayfs with '$tval' bootfs"
	    sed -i -e "s/^/overlayroot=tmpfs /" /boot/firmware/cmdline.txt
	    sed -i -e "s#\(.*/boot/firmware.*\)defaults\(.*\)#\1defaults,$tval\2#" /etc/fstab
	    ;;
	#
	# keymap, locale, and timezone may be set via sdm --burn command
	#
	keymap)
	    configitemlog "Set Keymap to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_configure_keyboard "$value" nonint
	    do_raspiconfig do_configure_keyboard "$value"
	    ;;
	locale)
	    configitemlog "Set Locale to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_change_locale "$value" nonint
	    do_raspiconfig do_change_locale "$value"
	    declare -x LANG="$value"
	    ;;
	timezone)
	    configitemlog "Set Timezone to '$value'" $msgfn
	    #SUDO_USER=${myuser:-pi} raspi-config do_change_timezone "$value" nonint
	    do_raspiconfig do_change_timezone "$value" 
	    ;;
	*)
	    configitemlog "% Unrecognized option '$rpifun'" $msgfn
	    ;;
    esac

}

function getosinfo() {
    #
    # extract an element from /etc/os-release
    #
    local item="$1" where="$2"
    str="$( { IFS="=" read id name ; echo $name ;} <<< $(grep ^$item= $where/etc/os-release))"
    str=$(stripquotes "$str")
    echo $str
}

function getosdate() {
    #
    # Returns the OS date from /etc/rpi-issue
    #
    echo $(grep -s -m1 -o '[[:digit:]]\{4\}-[[:digit:]]\{2\}-[[:digit:]]\{2\}' $SDMPT/etc/rpi-issue)
    return
}

function israspios() {
    #
    # Returns true if host is RasPiOS, false if not
    #
    # os-release says ID=debian, arch is aarch64 or armv7l (uname -m) and /etc/rpi-issue exists
    [ -f /etc/os-release -a -f /etc/rpi-issue ] || return 1
    [ "$(grep ^ID= $SDMPT/etc/os-release | (IFS="=" read v id ; id=${id%\"} ; echo ${id#\"}))" == "debian" ] || return 1
    [[ "aarch64|armv7l" =~ "$(uname -m)" ]] || return 1
    return 0
}

function is64bit() {
    #
    # Look at /bin/ls to decide the arch
    # Return 0 if is64 else 1
    #
    local wtf wtfelf wtfarch rest
    wtf=$(file $SDMPT/bin/ls)
    IFS="," read wtfelf wtfarch rest <<< "$wtf"
    [[ "$wtfarch" =~ "aarch64" ]] && return 0 || return 1
}

function ispdevp() {
    local dev="$1"
    [[ "$dev" =~ "mmcblk" ]] || [[ "$dev" =~ "nvme" ]] && return 0 || return 1
}

function getspname() {
    local dev="$1" pn="$2"
    ispdevp $dev && echo "${dev}p${pn}" || echo "${dev}${pn}"
}

function getpartname() {
    local dev="$1" pn="$2"
    ispdevp $dev && echo "p${pn}" || echo "${pn}"
}

function getcdate() {
    #
    # Returns date in canonical format
    #
    local cdtfmt="+%Y-%m-%dT%H:%M:%S"  # Canonical date format for manipulation
   echo $(date "$cdtfmt")
}

function datediff() {
    #
    # Computes difference between two dates
    # returns it as a string of Days:Hours:Minutes:Seconds
    #
    local days hours minutes seconds eseconds
    local btime="$1" etime="$2"
    eseconds=$(($(/bin/date -d "$etime" +%s) - $(/bin/date -d "$btime" +%s)))
    days=$((eseconds/86400))
    hours=$(((eseconds - $((days*86400)))/3600))
    minutes=$(((eseconds - $((days*86400)) - $((hours*3600)))/60))
    seconds=$((eseconds - $((days*86400)) - $((hours*3600)) - $((minutes*60))))
    #printf "%02d:%02d:%02d:%02d\n" "$days" "$hours" "$minutes" "$seconds"
    [ $days -ne 0 ] && printf "%02d:%02d:%02d:%02d\n" "$days" "$hours" "$minutes" "$seconds" || printf "%02d:%02d:%02d\n" "$hours" "$minutes" "$seconds"
}

function writeconfig() {
    #
    # Write config parameters into the image
    #
    local paramfile="$SDMPT/etc/sdm/cparams"
    local bkfile="${paramfile}.old" orgfile="${paramfile}.orig"
    rm -f $bkfile
    [ -f $paramfile ] && mv $paramfile $bkfile
    echo "#Arguments passed from sdm into the IMG on $(date +'%Y-%m-%d %H:%M:%S')" > $paramfile
    for e in version thishost aptcache aptdistupgrade autologin fbatch b0script b1script bootscripts \
		     burnplugins cscript csrc customizestart datefmt debugs dimg dimgdev dimgdevname domain ecolors \
		     expandroot exports fchroot fdirtree fnoexpandroot hname hostname loadlocal logwidth \
		     dgroups myuser nowaittimesync os pi1bootconf plugindebug poptions \
		     raspiosver reboot fredact regensshkeys noreboot rebootwait \
		     redocustomize sdmdir sdmflist showapt src swapsize \
		     timezone virtmode vqemu wificountry custom1 custom2 custom3 custom4 plugins allplugins
    do
	echo "$e:\"${!e}\"" >> $paramfile
    done
    [ -f $orgfile ] || cp -a $paramfile $orgfile
}

function resetpluginlist() {
#
# Juggle plugin list in cparams
#
[ "$plugins" != "" ] && allplugins="${allplugins}~${plugins}"
plugins=""
writeconfig
}

function updatehostname() {
    hnm="$1" hnimg=""
    [ -f $SDMPT/etc/hostname ] && read hnimg < <(tr -d " \t\n\r" < $SDMPT/etc/hostname)
    [ "$hnimg" == "" ] && hnm="raspberrypi"
    if [ "$hnm" != "$hnimg" ]
    then
	[ "$domain" != "" ] && shn="${hnm}.${domain}" || shn=""
	logtoboth "> Set hostname '$hnm'"
	echo $hnm > $SDMPT/etc/hostname
	sed -i "s/127\.0\.1\.1.*${hnimg}.*/127.0.1.1\t${hnm} ${shn}/g" $SDMPT/etc/hosts
    fi
}

function runoneplugin() {
    #
    # Run the plugin $1 for the phase specified in $2 with args in $3
    # By the time we get here plugin will be in the sdmdir hierarchy
    #
    # If Phase is not 0, caller must be in the container or use sdm_runspawn instead of calling this directly
    #
    local p=$(basename $1) phase=$2 pargs="$3" sts=1 xsts=1
    [ "$pargs" != "" ] && argstr="with arguments: '$pargs'" || argstr="with no arguments"
    #[ $fredact -eq 1 ] && argstr=""
    for d in $SDMPT$sdmdir/local-plugins $SDMPT$sdmdir/plugins
    do
        if [ -f $d/$p ]
	then
	    [[ "$p" =~ ^sdm. ]] || logtoboth "> Run Plugin '$p' ($d/$p) Phase $phase $argstr"
	    if [ -x $d/$p ]
	    then
		$d/$p "$phase" "$pargs"
		xsts=$?
		sts=0
		break
	    else
		logtoboth "!!Plugin '$p' file '$d/$p' not executable"
		break
	    fi
	#else
	fi
    done
    [ $sts -eq 1 ] && logtoboth "? Unable to find Plugin '$p'"
    return $xsts
}

function runonepluginx() {
    #
    # $1: Plugin name and args (in $plugin format: plugin:key=value:key=value)
    # $2 phase
    # If specified, $3 is a string to append to the plugin args (argname=value)
    #
    # If Phase is not 0, caller must be in the container or use sdm_runspawn instead of calling this directly
    #
    local p="$1" phase=$2 xargv="$3" in
    IFS=":" read -r pin pargs <<< "$p"
    [ "$pargs" == "$p" ] && pargs=""      #Really a null argument list
    if [ "$xargv" != "" ]
    then
	[ "$pargs" == "" ] && pargs="$xargv" || pargs="$pargs|$xargv"
    fi
    runoneplugin $pin $phase "$pargs"
}

function runplugins() {
    #
    # Run the plugins in $1 with the phase specified in $2
    # If specified, $3 is a string to append to the plugin args (argname=value)
    # If Phase is not 0, caller must be in the container or use sdm_runspawn instead of calling this directly
    #
    local mnt="$SDMPT" theseplugs="$1" phase=$2 xargv="$3" p pin pargs plugs=()

    if [ "$theseplugs" != "" ]
    then
	logtoboth "> Run Plugins Phase '$phase'"
	IFS="~" read -r -a plugs <<< "$theseplugs"
	#IFS="~" plugs=($theseplugs'')  # bash doesn't like this ;(
	for p in "${plugs[@]}"
	do
	    if [ "$p" != "" ]
	    then
		IFS=":" read -r pin pargs <<< "$p"
		runonepluginx "$p" $phase "$xargv" || { logtoboth "? Plugin '$pin' exited with failure status '$?'" && return 1 ; }
	    fi
	done
    fi
    return 0
}

function runplugins_exit() {
    #
    # Run plugins. Exit if there is an error
    #
    # $1: list of plugins
    # $2: Phase
    # If specified, $3 is a string to append to the plugin args (argname=value)
    #
    local plugins="$1" phase="$2" xargv="$3"
    [ "$plugins" == "" ] && return
    runplugins "$plugins" $phase "$xargv" && return || exit
}

function live0scripts() {
    #
    # Clear or run plugin boot scripts on the running system
    #
    # $1: path to clear or run (eg /etc/sdm/0piboot/*.sh)
    #
    local fni="$1" op=$2
    local fbn ffn fpn

    while read ffn
    do
	if [ "$op" == "run" ]
	then
            chmod 755 $ffn
            logtoboth "* Run plugin FirstBoot script '$ffn'"
            bash $ffn
	fi
        fbn=$(basename $ffn)
        fpn=$(dirname $ffn)
        rm -f $fpn/.$fbn
        mv $ffn $fpn/.$fbn
    done < <(compgen -G "$fni")
}

function ispluginselected() {
    #
    # Returns true if plugin name in $1 is on the command line
    #
    local thisplugin="$1" pluglist="$2"
    IFS="~" read -r -a plugs <<< "$pluglist"
    for p in "${plugs[@]}"
    do  
	IFS=":" read -r thispn args <<< "$p"
	[ "$thispn" == "$thisplugin" ] && return 0
    done
    return 1
}

function getpluginargs() {
    #
    # Returns the args provided for the plugin on the cmd line, for all instantiations of it
    #
    local thisplugin="$1" pluglist="$2" arglist=""
    IFS="~" read -r -a plugs <<< "$pluglist"
    for p in "${plugs[@]}"
    do  
	IFS=":" read -r thispn args <<< "$p"
#??? needs to append correctly
	[ "$thispn" == "$thisplugin" ] && arglist="${arglist}${args}"
	echo "$arglist"
	return
    done
}

function isarginpluginlist() {
    #
    # $1 is plugin name
    # $2 is plugin list
    # $3 is the arg name
    local pn="$1" plist="$2" lookarg="$3"
    args=$(getpluginargs "$pn" "$plist")
    [[ "$args" =~ "$lookarg" ]] && return 0 || return 1
}

function plugin_getargs() {
    #
    # Handles input data in the form: arg1=val1|arg2=val2|arg3=val3| ...
    # $1: Plugin name
    # $2: Argument list
    # $3: [optional] list of valid keys. Validity not checked if ""
    # $4: [optional] list of required keys. Required keys not checked if ""
    #
    # In addition to creating a key/value symbol for each found key/value,
    # also creates symbol foundkeys="|key1|key2|...|"
    #
    local arglist=() pfx=$1 largs="$2" validkeys="$3" rqkeys="$4" keysfound=""
    IFS="|" read -r -a arglist <<< "$largs"
    for c in "${arglist[@]}"
    do
	# Put the args into variables
	IFS="=" read -r key value remain <<< "$c"
	[ "$remain" != "" ] && value="${value}=${remain}"
	if [ "$validkeys" != "" ]
	then
	    if ! [[ "$validkeys" =~ "|$key|" ]]
	    then
		logtoboth "? Plugin $pfx: Unrecognized key '$key' in argument list '$largs'"
		return 1
	    fi
	fi
	if [ "${key#\#}" != "$key" -o "${key#\\n}" != "$key" ]
	then
	    # Handle a comment string that starts with '#' or '\n'. Prefix \n-led string with \n#
	    [ "${key#\\n}" != "$key" ] && key="\n#${key#\\n}" 
	    printf -v "comment" "%s" "$key"
	    keysfound="${keysfound}|comment"   # Mark as 'comment' found
	else
	    # Turn word-word into word__word. User of the value must handle other end for display
	    [ "$key" != "${key/-/_}" ] && key="${key//-/__}"
	    # Don't slash-quote dollar signs. Breaks $ in passwords. What breaks if this is disabled?
	    # [[ "$value" =~ "$" ]] && value=${value//$/\\$}    # Replace $ with \$
	    [ "$key" != "" ] && printf -v "${key}" "%s" "$value" #eval "${key}=\"$value\""
	    keysfound="${keysfound}|$key"
	fi
    done
    #
    # Check required keys
    #
    if [ "$rqkeys" != "" ]
    then
	# Strip leading "|" from rqkeys to avoid checking for a null first arg
	IFS="|:" read -a arglist <<< "${rqkeys#|}"
	for c in "${arglist[@]}"
	do
	    if ! [[ "${keysfound}|" =~ "|$c|" ]]
	    then
		logtoboth "? Plugin $pfx: Required key '$c' missing from argument list '$largs'"
		return 1
	    fi
	done
    fi
    # Strip leading "|" to avoid null string on subsequent splitting, but put one on the end
    [ "$keysfound" == "" ] && eval "foundkeys=\"\"" || eval "foundkeys=\"${keysfound#|}|\""
    return 0
}

function plugin_printkeys() {
    #
    # Print the keys found. plugin_getargs returns the list of found keys in $foundkeys
    # Assumes $pfx:plugin name, $foundkeys:list of keys found, keys set up from plugin_getargs
    # $1: List of keys to redact each separated by vbar [disabled]
    #
    #redactkeys="|$1|"
    if [ "$foundkeys" != "" ]
    then
	logtoboth "> Plugin $pfx: Keys/values found:"
	IFS="|" read -a fargs <<< "$foundkeys"
	for c in "${fargs[@]}"
	do
	    # The construct ${!c} gets the value of the variable 'pointed to' by contents of $c
	    kn="${c//__/-}"
	    kval="${!c}"
	    #[[ "$redactkeys" =~ "$c" ]] && kval="REDACTED"
	    logtoboth "   ${kn}: $kval"
	done
    fi
}

function plugin_logorder() {
    local plugins="$1" plist
    logtoboth "* Plugins selected:"
    if [ "$plugins" != "" ]
    then
	IFS="~" read -a plist <<< $plugins
	for p in "${plist[@]}"
	do
	    IFS=':' read fpn pargs <<< "$p"
	    logtoboth "   * $fpn"
	    #[ $fredact -eq 1 ] && pargs=""
	    [ "$pargs" != "" ] && logtoboth "       Args: $pargs" nosplit
	done
    else
	logtoboth "   * No plugins are selected"
    fi
}

function plugin_dbgprint() {
    [ $plugindebug -eq 1 ] && logtoboth "D!Plugin $pfx: $1"
}

function plugin_addnote() {
    #
    # Adds a line of text to the notes file for the run
    # The notes file is appended to the console and log at end of run
    #
    # $1: Line of text to write
    #
    msg="$1"
    echo "Plugin $pfx: $msg" >> $SDMPT/etc/sdm/rnotes
}

function printnotes() {

    if [ -f $SDMPT/etc/sdm/rnotes ]
    then
	logtoboth ""
	logtoboth "*** Plugin Notes ***"
	logtoboth ""
	while read line
	do
	    logtoboth " $line" nosplit
	done < $SDMPT/etc/sdm/rnotes
	rm -f $SDMPT/etc/sdm/rnotes
    fi
}

function checkpluginsx() {
    #
    # $1: Plugin list
    # $2: y if shoud copyifnewer
    #
    local plugins="$1" cif=$2
    local p plugs fpn pargs pf dfpn d
    
if [ "$plugins" != "" ]
then
    IFS="~" read -a plugs <<< $plugins
    for p in "${plugs[@]}"
    do
	IFS=':' read fpn pargs <<< "$p"
	pf=0
	dfpn=$(dirname $fpn)
	for d in  $dfpn $src/local-plugins $src/plugins
	do
	    pn=$(basename $fpn)
	    # Don't look in current directory unless explicit './' provided
            if  [[ "$d" != "." ]] || [[ "$fpn" == "./$pn" ]]
	    then
		if [ -f $d/$pn ]
		then
		    [ -x $d/$pn ] || errexit "? Plugin $d/$pn is not executable"
		    # if plugin found outside of sdm hierarchy copy it in
		    if [ "$d" == "$dfpn" -a $burning -eq 0 -a $frunonly -eq 0 ]     # Don't update plugins on host if burning or runonly
		    then
			[ "$cif" == "y" ] && copyifnewer $d/$pn $src/local-plugins && write_premsg "% Copy Plugin '$pn' from '$d/$pn' to '$src/local-plugins'"
		    fi
		    pf=1
		    break
		fi
	    fi
	done
	[ $pf -eq 0 ] && errexit "? Unrecognized plugin '$fpn'"
    done
fi
}

function adjust_wifinmpsk() {
    #
    # $1: Input PSK
    #
    # return: single string with all schars letters backslash-quoted
    local psk="$1" i c schars=" '|!"

    # Quote characters that need it
    for (( i=0 ; i < ${#schars} ; i++ ))
    do
        c=${schars:$i:1}
        psk="${psk//$c/\\$c}"
    done
    echo "$psk"
}

function encrypt_wifipsk() {
    # $1 psk
    # $2 SSID

    local psk="$1" ssid="$2" opsk
    opsk="$(wpa_passphrase "$ssid" "$psk" | grep $'\tpsk=')"
    IFS='=' read lhpsk psk <<< "$opsk"
    echo "$psk"
}

function dosshsetup() {
    #
    # Set up ssh as requested
    # Must be called in Phase 1
    #
    ssh="$1" pfx="$2"
    case "$ssh" in
	service)
	    logtoboth "> Plugin $pfx: Enable SSH service"
	    systemctl enable ssh >/dev/null 2>&1
	    systemctl enable sshswitch >/dev/null 2>&1
	    ;;
	socket)
	    logtoboth "> Plugin $pfx: Enable SSH using ssh.socket"
	    systemctl enable ssh.socket > /dev/null 2>&1
	    systemctl disable sshswitch.service > /dev/null 2>&1
	    ;;
	none)
	    logtoboth "> Plugin $pfx: Disable SSH"
	    systemctl disable ssh.service > /dev/null 2>&1
	    ;;
    esac
}

function iswsl() {
    #
    # Return true if running system is a WSL instance
    #
    local uname=$(uname -a)
    [[ "${uname,,}" =~ "microsoft" ]] && return 0 || return 1
}

function obupdplugins() {
    #
    # Check plugins for --burn and --runonly
    # Loop through plugin list from command line
    # if has path always then copy to local-plugins in the img
    # else if newer in plugins or local-plugins, copy into img if --bupdate plugin
    # $1: 'check' to report, or 'update' to check and update
    local copt=$1 bup=$2
    if [ "$plugins" != "" ]
    then
	IFS="~" read -a plugs <<< "$plugins"
        for p in "${plugs[@]}"
        do
	    p="${p%%:*}"
	    pn="$(basename $p)"
            if [ "$p" != "$pn" ]
	    then
		# plugin with path specified; copy into IMG if newer regardless of --bupdate
		copyifnewer $p $SDMPT/$sdmdir/local-plugins && logtoboth "% sdm: Adding/Updating Plugin '$p' to $sdmdir/local-plugins"
	    else
		# no path on plugin. if find in plugins or local-plugins copy into img if --bupdate plugin else mention
		# this code doesn't report if plugin not found. That will happen later when it gets run
		for d in $sdmdir/local-plugins $sdmdir/plugins
		do
		    if [ -f $d/$pn ]
		    then
			case "$copt" in
			    check)
				checkifnewer $d/$pn $SDMPT/$d/$pn && logtoboth "% sdm: Plugin '$p' on the host is newer"
				;;
			    update)
				if [[ "$bup" =~ "plugin" ]]
				then
				    copyifnewer $d/$pn $SDMPT/$sdmdir/local-plugins && logtoboth "% sdm: Updating Plugin '$pn' from '$d/$pn' to '$sdmdir/local-plugins'"
				else
				    logtoboth "% sdm: Plugin '$pn' on the host is newer"
				fi
				;;
			esac
			break
		    fi
		done
	    fi
        done
    fi
}

function checkupdsdm() {
    #
    # Loop through sdm files and plugins and check for any newer ones on host
    # Warn or update if so
    # $1: 'check' to report, or 'update' to check and update
    # $2: bupdate (sdm, plugin, or sdm+plugin (--bupdate))
    local copt=$1 bup=$2

    #
    # Check sdm components if --bupdate sdm
    # Really for sdm dev work only
    #
    if [[ "$bup" =~ "sdm" ]]
    then
	for fsdm in $sdmflist
	do
	    case "$copt" in
		check)
		    checkifnewer $sdmdir/$fsdm $SDMPT/$sdmdir/$fsdm && logtoboth "% sdm: File '$fsdm' on host is newer"
		    ;;
		update)
		    [[ "$bup" =~ "sdm" ]] && copyifnewer $sdmdir/$fsdm $SDMPT/$sdmdir/$fsdm && logtoboth "% sdm: Updatinge sdm module '$fsdm' from host to '$SDMPT$sdmdir"
		    ;;
	    esac
	done
    fi
    #
    # Handle plugins
    #
    obupdplugins $copt $bup
}

function gethostarch() {
    local heading arch
    while read line
    do
	if [[ "$line" =~ "Architecture:" ]]
	then
	    IFS=":" read heading arch <<< "$line"
            echo "${arch##* }"
	    break
	fi
    done < <(lscpu)
}

function gethostopmode() {
    local heading modes=""
    while read line
    do
	if [[ "$line" =~ "CPU op-mode(s):" ]]
	then
	    IFS=":" read heading modes <<< "$line"
	    break
	fi
    done < <(lscpu)
    if [ "$modes" == "" ]
    then
	[[ "armv6l|armv7l" =~ "$(gethostarch)" ]] && modes="32-bit" || modes="unknown"
    fi
    echo "${modes#${modes%%[![:space:]]*}}"
}

function virtstatus() {
    #
    # Returns "none" (not in virtualization) or "systemd-nspawn" or "chroot" as appropriate
    #
    local invirt=$(systemd-detect-virt)
    if [ "$invirt" == "none" ]
    then
	systemd-detect-virt -r && invirt=chroot
    fi
    echo $invirt
}

function adjust_initramfs_all() {
    sed -i "s/^MODULES=dep/MODULES=most/" /etc/initramfs-tools/initramfs.conf #Set initramfs to MODULES=most in case update-initramfs runs
    sed -i "s/^update_initramfs=yes/update_initramfs=all/" /etc/initramfs-tools/update-initramfs.conf

}

function unadjust_initramfs_all() {
    sed -i "s/^MODULES=most/MODULES=dep/" /etc/initramfs-tools/initramfs.conf # Reset initramfs to MODULES=dep for normal operation
    sed -i "s/^update_initramfs=all/update_initramfs=yes/" /etc/initramfs-tools/update-initramfs.conf
}

function initvirt() {
    # Check IMG and running system. If running on 32-bit and IMG is 64-bit, prepare for chroot
    # wtf -> what's the file :)
    # wami -> what is my host OS (where sdm is running)
    # $1 = function to call to write msgs, or null to echo
    local msgrtn=$1
    local bfs="" hostarch hostopmode vqemu="" armno32=0
    local wami wtf wtfelf wtfarch wamielf wamiarch rest
    [ "$msgrtn" == "" ] && msgrtn=echo
    hostarch=$(gethostarch)
    hostopmode=$(gethostopmode)
    wami=$(file /bin/ls)
    IFS="," read wamielf wamiarch rest <<< "$wami"
    wtf=$(file $SDMPT/bin/ls)
    IFS="," read wtfelf wtfarch rest <<< "$wtf"
    [[ "$wtfarch" =~ "ARM" ]] || errexit "? Unrecognized architecture '$wtfarch' in IMG"
    [[ "$hostarch" =~ "aarch64" ]] && ! [[ "$hostopmode" =~ "32-bit" ]] && armno32=1    # aarch64 host that does not support 32-bit mode
    if [[ "$wami" =~ "ARM" ]]
    then
	[ $armno32 -eq 1 ] && [[ "$wtf" =~ "32-bit" ]]  && vqemu="arm"     #64-bit host without 32-bit support
	[[ "$wami" =~ "32-bit" ]] && ! [[ "$wtf" =~ "32-bit" ]] && vqemu="aarch64" #32-bit host and 64-bit IMG
	# ! [[ "$wami" =~ "32-bit" ]] && [[ "$wtf" =~ "32-bit" ]] && vqemu="arm" # explicit: aarch64 host and 32-bit IMG OS will automatically use qemu
	                                                                         # so set this to ensure user knows? or not?
    else
	# Not an ARM host; choose qemu based on IMG bits
	[[ "$wtf" =~ "32-bit" ]] && vqemu="arm" || vqemu="aarch64"
    fi
    # Set up qemu in case needed (system decides)
    if [ "$vqemu" != "" ]
    then
	$msgrtn "% sdm will use qemu '$vqemu'"
	if [ ! -f /proc/sys/fs/binfmt_misc/qemu-${vqemu} ]
	then
	    if [[ "$wtf" =~ "32-bit" ]]
	    then
		bfs=":qemu-arm:M:0:\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:F"
	    else
		bfs=":qemu-aarch64:M:0:\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-aarch64-static:F"
	    fi
	    ubf=""
	    for fn in /sbin/update-binfmts /usr/sbin/update-binfmts
	    do
		[ -x $fn ] && ubf=$fn && break
	    done
	    [ "$ubf" == "" ] && errexit "? Cannot find update-binfmts; try 'sudo apt install binfmt-support'"
	    $msgrtn "% Add and enable qemu binfmt for '$vqemu' processor architecture"
	    echo "$bfs" | tee /proc/sys/fs/binfmt_misc/register >/dev/null 2>&1
	    $ubf --enable qemu-${vqemu} 2>/dev/null
	fi
    fi
    [[ "$hostopmode" =~ "64-bit" ]] && bfs="64-bit" || bfs="32-bit"
    wamiarch=${wamiarch##\ }
    if [ $fchroot -eq 1 ]
    then
	virtmode="chroot"
	$msgrtn "% sdm will use chroot per --chroot on this $bfs $wamiarch host"
    else
	# only msg if host not aarch64 OR host arch different from IMG arch
	if ! [[ "$hostarch" =~ "aarch64" ]] && ! [[ "$wtfarch" =~ "$hostarch" ]]
	then
	    $msgrtn "% sdm will use systemd-nspawn on this $bfs $wamiarch host"
	    $msgrtn "  Retry the command with --chroot if this fails"
	fi
    fi
}

function chroot_cleanup() {
    if [ "$SDMPT" != "" ]
    then
	grep -q 'sdm-generated' $SDMPT/etc/resolv.conf && rm -f $SDMPT/etc/resolv.conf
	umount $SDMPT/{dev/pts,dev,proc,sys}
    else
	logtoboth "% SDMPT null in handling chroot cleanup. Please log an issue at https://github.com/gitbls/sdm; Continuing..."
    fi
}

function ctrlchroot() {
    echo "% Caught CTRL/C in chroot; Cleaning up..."
    declare -x SDMPT="$OLDSDMPT"
    declare -x SDMNSPAWN="$OLDSDMNSPAWN"
    chroot_cleanup
    exit 1
}

function sdm_runspawncmd() {
    #
    # Run (relatively) short-lived command in container context (see sdm-phase1 for the commands)
    # $1: SDMNSPAWN setting in nspawn
    # $2: command (supported by sdm-phase1)
    # $3-$8: arguments
    #
    # $SDMPT must point to correct img, which is mounted
    #
    # Commands are processed in sdm-phase1. See there.
    #
    local nspwn="$1" cmd="$2"
    #logtoboth "sdm-runspawncmd: cmd=|$cmd| 3=|$3| 4=|$4| 5=|$5| 6=|$6| 7=|$7| 8=|$8|" nosplit
    sdm_spawn "" "$nspwn" $sdmdir/sdm-phase1 "$cmd" "$3" "$4" "$5" "$6" "$7" "$8"
    return
}

function sdm_spawn() {
    #
    # $1 - nspawn switches
    # $2 - setting for SDMNSPAWN
    # $3 - command
    # $4-$8: arguments
    #
    # SDMPT must point to mounted img
    # SDMPT will be redefined to be "" during Phase 1 nspawn/chroot.
    #    [[ -z "${SDMPT+z}" ]] test will work since it is defined
    #
    local nspawnsw="$1" nsp="$2" cmd="$3" spcmd="$4" args="" ta
    local wtf wtfelf wtfarch rest sts hostarch hostpagesz pcor pat vers junk
    for i in 6 7 8
    do
	ta="${!i}"
	#echo "$i | $ta "
	#[ "$ta" != "" ] && args="${args}\"$ta\" "
	[ "$ta" != "" ] && args="${args}${ta} "
    done

    if [[ "$virtmode" =~ "nspawn" ]]
    then
	[ -t 0 ] && cmode="--console interactive" || cmode="--console pipe"
	pat="[[:digit:]]+"
	vers="$(systemctl --version)"   # Assume first digit string in the version is the version #
	if [[ "$vers" =~ $pat ]]
	then
	    [[ ${BASH_REMATCH[0]} -lt 242 ]] && cmode=""
	fi

	nspawnsw="$nspawnsw --setenv=SDMNSPAWN=$nsp"

        if [ "$cmd" == "/bin/bash" ]
        then
            # Bash is touchy for some reason. This works, so good for now
	    systemd-nspawn -q --directory=$SDMPT --setenv=SDMPT="" $nspawnsw $cmode $cmd $spcmd
        else
	    systemd-nspawn -q --directory=$SDMPT --setenv=SDMPT="" $nspawnsw $cmode $cmd $spcmd "$5" "$args"
        fi

	sts=$?
	if [ $sts -eq 127 ]
	then
	    #systemd-nspawn exit status 127 means that systemd-nspawn couldn't find the bash command
	    #We assume it indicates a 4K/16K pagesize incompatibility and report it as such
	    #Typical error: -bash: error while loading shared libraries: libtinfo.so.6: ELF load command address/offset not page-aligned
	    wtf=$(file $SDMPT/bin/ls)
	    IFS="," read wtfelf wtfarch rest <<< "$wtf"
	    rest=${wtfelf##*/bin/ls: }
	    hostarch=$(uname -m)
	    hostpagesz=$(getconf PAGE_SIZE)
	    if [[ $hostpagesz -eq 16384 ]] && [[ "$rest" == "ELF 32-bit LSB executable" ]]
	    then
		logtoboth "?! systemd-nspawn failed on this IMG"
		logtoboth "   Host system architecture: $hostarch with $hostpagesz byte pages"
		logtoboth "   IMG binaries: $rest"
		pcor=1
		if [ "$(type -p cryptsetup)" != "" ]
		then
		    rest="$(df /)"
		    [[ "$rest" =~ "/dev/mapper" ]] && pcor=0
		fi
		if [ $pcor -eq 1 ]
		then
		    logtoboth "   Correct this error on this host by:"
		    logtoboth "    * sudoedit /boot/firmware/config.txt"
		    logtoboth "    * Add the following line:"
		    logtoboth "    * kernel=kernel8.img"
		    logtoboth "    * Reboot and rerun the sdm command"
		    logtoboth "   Details: https://github.com/raspberrypi/bookworm-feedback/issues/120"
		else
		    logtoboth "?! The rootfs on this host is encrypted; this IMG cannot be processed on this host"
		fi
	    else
		logtoboth "?! Unrecognized error from systemd-nspawn"
		logtoboth "   Please open an issue at https://github.com/gitbls/sdm"
	    fi
	fi
	return $sts
    else
	#
	# Use chroot
	#
	declare -x OLDSDMPT="$SDMPT"
	declare -x OLDSDMNSPAWN="$SDMNSPAWN"
	#
	# If there is no /etc/resolv.conf, fabricate one
	[ -f $SDMPT/etc/resolv.conf ] || printf "# sdm-generated \nnameserver 1.1.1.1\n" > $SDMPT/etc/resolv.conf
	trap "ctrlchroot" SIGINT
	for fs in dev dev/pts proc sys ; do mount --bind /$fs $SDMPT/$fs ; done
	declare -x SDMPT=""
	declare -x SDMNSPAWN="$nsp"

	if [ "$cmd" == "/bin/bash" ]
	then
	    chroot $OLDSDMPT $cmd
	else
	    chroot $OLDSDMPT $cmd $spcmd "$5" "$args"
	fi
	sts=$?
	declare -x SDMPT="$OLDSDMPT"
	declare -x SDMNSPAWN="$OLDSDMNSPAWN"
	trap SIGINT
	chroot_cleanup
	return $sts
    fi
}

function deferqemu() {
    #
    # Set deferred install
    #
    logtoboth "% chroot/qemu-user-static; Defer install of qemu-user-static to system FirstBoot"
    fnqemu="/etc/sdm/0piboot/010-install-qemu.sh"
    if [ ! -f $fnqemu ]
    then
	cat > $fnqemu <<EOF
#!/bin/bash
#
# Install qemu-user-static. Install deferred because it can't be installed in a chroot
#
source /etc/sdm/sdm-readparams
bootlogx "Install qemu-user-static"
installpkgsif qemu-user-static
EOF
    fi
}

function copyifnewer() {
    # Copy $1 to $2 if $1 is newer
    # Target must have same basename as src (no rename as part of copy)
    # Target can be a directory or a dir/$(basename $1)
    # Return true if it's newer (did copy) false if not
    local src=$1 dst=$2
    local fn=$(basename $src)
    [ "$fn" != "$(basename $dst)" ] && dst="$dst/$fn"
    if [ -f "$dst" ]
    then
	[ "$1" -nt "$dst" ] || return 1
    fi
    cp -a -f $1 $2
    return 0
}

function checkifnewer() {
    # Checks $1 and $2 to see if $1 is newer
    # Target must have same basename as src (no rename as part of the check)
    # Target can be a directory or a dir/$(basename $1)
    # Return true if $1 newer, false if not
    local src=$1 dst=$2
    local fn=$(basename $src)
    [ "$fn" != "$(basename $dst)" ] && dst="$dst/$fn"
    if [ -f "$dst" ]
    then
	[ "$1" -nt "$dst" ] && return 0 || return 1
    fi
    return 0  # Doesn't exist, so $1 is newer
}

function setdelayedbbh() {
    #
    # $1: end state boot behaviour (B[1-4])
    echo "$1" >| /etc/sdm/assets/gfxbbh
}

function getfinalbbh() {
    #
    # Get boot_behaviour if it's been set
    # If not, return $1
    #
    aval=$(cat /etc/sdm/assets/gfxbbh 2>/dev/null)
    [ "$aval" == "" ] && echo "$1" || echo "$aval"
}

function do_delayed_boot_behavior {
    #
    # $1 = 'reboot' if system will be rebooted
    local now="" nowb="" bbh=""
    [ "$1" != "reboot" ] && now="--now" && nowb="--no-block"
    bbh=$(cat /etc/sdm/assets/gfxbbh 2>/dev/null)
    if [ "$bbh" != "" ]
    then
	if [[ "B3B4" =~ "$bbh" ]]
	then
	    # Re-enable display manager that we disabled in phase 1
	    if [ -d /etc/lightdm ]
	    then
		configitemlog "Set boot behaviour to $bbh" bootlog
		do_raspiconfig do_boot_behaviour $bbh
		bootlog "Set systemd default to graphical.target"
		systemctl set-default graphical.target
		if [ "$1" != "reboot" ]
		then
		    bootlog "Start lightdm"
		    systemctl restart lightdm
		else
		    bootlog "Enable lightdm"
		    systemctl enable lightdm
		fi
	    else
		[ -d /etc/X11/xdm ] && bootlog "Re-enable xdm" && systemctl enable $now display-manager > /dev/null 2>&1 && sleep 3
		[ -d /etc/X11/wdm ] && bootlog "Re-enable wdm" && systemctl enable $now wdm && sleep 3
		bootlog "Set systemd default to graphical.target"
		systemctl set-default graphical.target
	    fi
	else
	    if [[ "B1B2" =~ "$bbh" ]]
	    then
		configitemlog "set boot behaviour to $bbh" bootlog
		do_raspiconfig do_boot_behaviour $bbh
		if compgen -G /etc/systemd/system/xvnc*.socket >/dev/null
		then
		    [ -d /etc/X11/xdm ] && bootlog "Re-enable xdm" && systemctl enable $now xdm >/dev/null 2>&1 && sleep 3
		    [ -d /etc/X11/wdm ] && bootlog "Re-enable wdm" && systemctl enable $now wdm && sleep 3
		fi
	    fi
	fi
    fi
    # Perform cleanup steps done by /usr/bin/cancel_rename
    bootlog "Re-enable getty@tty1"
    write_console "Re-enable getty@tty1"
    systemctl enable $now $nowb getty@tty1 > /dev/null 2>&1
    rm -f /etc/ssh/sshd_config.d/rename_user.conf
}

function findappfile() {
    #
    # $1 = app/xapp variable
    # $2 = app/xapp output variable
    #
    # Updates app/xapp output variable with actual file location
    # or the value of $1 if it's not a file location (no leading '@')
    #
    local fn fnc
    if [ "${1:0:1}" == "@" ]
    then
	fn="${1:1:999}"
	fn="$(fndotfullpath $fn)"
	if [ ! -f "$fn" ]
	then
	    fnc="$src/$(basename $fn)"
	    if [ ! -f "$fnc" ]
	    then
		echo "? $2 file '$fn' not found"
		return
	    else
		echo "@$fnc"
	    fi
	else
	    echo "@$fn"
	fi
    else
	echo "$1"
    fi
}

function getapplist() {
    #
    # $1 has list of apps or @file with list of apps
    # Returns the app list as the function result
    #
    local lapps="" newapp fn
    if [ "${1:0:1}" == "@" ]
    then
	fn="${1:1:999}"
	while read line
	do
	    newapp=$(stripbcline "$line")
	    [ "$newapp" != "" ] && lapps="$lapps $newapp"
	done < $fn
    else
	lapps="$1"
    fi
    lapps="${lapps## }"      # Del leading spaces
    lapps="${lapps//,/ }"    # Turn commas into spaces
    echo "$lapps"
}

function doinstalls() {
    #
    # $1 - app list
    # $2 - subject string (e.g., "XWindows Installs" or "Application Installs")
    #
    if [ "$1" != "" ]
    then
	logtoboth "* Start $2"
	logtoboth "> ${2}: $1"
	if [[ "$debugs" =~ "apt" ]]
	then
	    logtoboth "> Install apt packages singly per '--debug apt'"
	    IFS=" " read -a alist <<< "$1"
	    for a in "${alist[@]}"
	    do
		logtoboth "> -- $a"
		installpkgsif "$a"
	    done
	else
	    installpkgsif "$1"
	fi
	logtoboth "* $2 Completed"
    else
	logtoboth "> Skip $2 per empty package list"
    fi
}

function getdisktype() {
    local dev=$1 line dtype
    while read line
    do
        if [[ "$line" =~ "Disklabel type:" ]]
        then
            dtype=${line##*:}
            dtype=${dtype/ /}
            echo "$dtype"
            break
        fi
    done < <(sfdisk -l $dev)
}

function isgpt() {
    local dev=$1
    [ "$(getdisktype $dev)" == "gpt" ] && return 0 || return 1
}

function updategptrootfs() {
    #
    # $1: device name
    #
    local dev=$1

    if isgpt $dev
    then
        declare -x SDMPT=$(makemtpt)
	domount $dev "Device"
        #gptuuid1=$(blkid $(getspname $burndev 1) | sed -n 's|^.*PARTUUID="\(\S\+\)".*|\1|p')
	gptuuid2=$(blkid $(getspname $burndev 2) | sed -n 's|^.*PARTUUID="\(\S\+\)".*|\1|p')
	logtoboth "> Set GPT rootfs partition GUID in cmdline.txt"
	sed -i "/^[[:space:]]*#/!s|^\(.*root=\)\S\+\(\s\+.*\)$|\1PARTUUID=${gptuuid2}\2|" $SDMPT/boot/firmware/cmdline.txt
	logtoboth "> Set GPT partition GUIDs in fstab"
	#sed -i "s/${olddiskid}-01/$gptuuid1/" $SDMPT/etc/fstab
	sed -i "/^[[:space:]]*#/!s|^\S\+\(\s\+/\s\+.*\)$|PARTUUID=${gptuuid2}\1|" $SDMPT/etc/fstab
	docleanup
    fi
}

function extendimage() {
    #
    # Extend an IMG and resize partition 2
    # Does NOT resize the file system in partition 2
    #
    local ldimg=$1 limgext=$2
    local line
    # Imager will complain if the image is not an integer multiple
    # of 512 bytes
    local limgextb=$((limgext*1048576))
    local limgcb=$(stat --printf="%s" $ldimg)
    local limgextb=$((limgextb + 512 - (limgcb%512)))
    if [ "$ddextend" == "1" ]
    then
	trap "ctrlcexit" SIGINT
	dd if=/dev/zero bs=1 count=$limgextb status=progress >> $ldimg || errexit "? Exiting due to dd error ?$"
	trap SIGINT
    else
        truncate --size +${limgextb} $ldimg || errexit "? Exit due to truncate resize error $?"
    fi
    # Get partition 2 start and IMG size from parted and compute new partition size
    while read line ; do
	if [[ "$line" =~ "ext4" ]]
	then
	    IFS=":" read partnum partstart remains <<< $line
	elif [[ "$line:" =~ "msdos" ]]
	then
	    IFS=":" read fn imgend remains <<< $line
	fi
    done < <(parted -sm $ldimg unit MB print)
    partstart=${partstart%MB}
    imgend=${imgend%MB}
    partsize=$((imgend-partstart))
    echo "* Resize partition 2 of '$ldimg' to ${partsize}MB"
    parted $ldimg -s resizepart 2 ${imgend}MB
}

function expandpartition() {
    # $1: devicename (/dev/sdX)
    # $2: 0 (fill available space) or nnnn (expand by MB)
    # $3: message output (write_burnmsg/logtoboth/or similar)
    # $4: fs type (ext4, btrfs, or lvm)
    local fulldevname="$1" sexp=${2:-0} msgrtn=${3:-logtoboth} rootfstype=${4:-ext4}
    local dev=${fulldevname##/dev/}
    local part pname fsize partstart partend devsize partsize newend pbytes nbytes pend

    pname=$(getpartname $fulldevname 2)
    part="${dev}${pname}"
    while read line
    do
        if [[ "$line" =~ "msdos|fat32" ]]
        then
            fsize=$(IFS=":" read fs bytes file n1 n2 fs <<< $line ; echo $bytes)
            fsize=${fsize%B}     #Fsize is file system size in bytes
        elif [[ "$line" =~ "ext4" ]]
        then
            IFS=":;" read partnum partstart partend partsize fstype etc etc2 etc3 <<< $line
            partstart=${partstart%B}
            partend=${partend%B}
            partsize=${partsize%B}
        fi
    done < <(parted -ms /dev/$dev unit B print)
    #
    #   https://alioth-lists.debian.net/pipermail/parted-devel/2006-December/000573.html
    # $l1: BYT;  ** error if not BYT 
    # $l2: filespec:bytesB:file:512:512:msdos::;
    # $l3: partnum:startB:endB:sizeB:fstype:::;
    # $l4: partnum:startB:endB:sizeB:fstype:::;

    devsize=$(cat /sys/block/$dev/size)         # 512byte blocks
    partsize=$(cat /sys/block/$dev/$part/size)  # 512byte blocks
    pbytes=$((partsize*512))
    if [ $sexp -eq 0 ]
    then
	nbytes=$(((devsize*512)-partend+pbytes))
	newend=$((devsize-1))                  # 512byte blocks
    else
	nbytes=$(((pbytes+($sexp*1048576)))) #1024*1024
	newend=$(((nbytes+partstart)/512))
    fi
    [ "$plugindebug" == "" ] && plugindebug=0
    if [ $plugindebug -eq 1 ]
    then
	logtoboth "> Plugin ${pfx}ExpandPartition: partstart: $partstart"
	logtoboth "> Plugin ${pfx}ExpandPartition: partend: $partend"
	logtoboth "> Plugin ${pfx}ExpandPartition: partsize: $partsize"
	logtoboth "> Plugin ${pfx}ExpandPartition: devsize: $devsize"
	logtoboth "> Plugin ${pfx}ExpandPartition: partsize: $partsize"
	logtoboth "> Plugin ${pfx}ExpandPartition: pbytes: $pbytes"
	logtoboth "> Plugin ${pfx}ExpandPartition: nbytes: $nbytes"
    fi
    $msgrtn "> Expand Root: Expand root partition '$part' ($rootfstype) on device '$fulldevname' from $(getgbstr $pbytes) to $(getgbstr $nbytes)"
    # root partition is always partition 2
    if isgpt $fulldevname
    then
	[ "$sexp" == "0" ] && pend="" || pend="+$newend"
	parted -m /dev/$dev u s resizepart 2 $((newend-48))  # ?? 32 not enough, 48 works
    else
	parted -m /dev/$dev u s resizepart 2 $newend
	errifrc $? "? parted resizepart failed"
    fi
    $msgrtn "* Mount $fulldevname to resize the root file system"
    [ "$SDMPT" == "" ] && declare -x SDMPT=$(makemtpt)
    domount $fulldevname "Device" "$SDMPT" $rootfstype
    $msgrtn "* Resize the $rootfstype root file system"
    $msgrtn "% (Ignore 'on-line resizing required' message)"
    case "$rootfstype" in
	ext4)
	    resize2fs ${fulldevname}${pname}
	    ;;
	btrfs)
	    btrfs filesystem resize max $SDMPT/
	    ;;
	lvm)
	    pvresize ${fulldevname}${pname}
	    lvresize --extents 100%VG $(lvmfind $fulldevname vg)/$(lvmfind $fulldevname lv)
	    resize2fs $(lvmgetmapper $fulldevname)
	    ;;
	zfs)
	    zpool online -e rpool sdb2
	    ;;
    esac
    #echo ",+" | sfdisk -N 2 $fulldevname
    #errifrc $? "? sfdisk expand last partition failed with status"
}

function expandpartitionx() {
    local rc
    expandpartition "$1" "$2" "$3" "$4"
    rc=$?
    docleanup
    return $rc
}

function zero1stlast() {
    # $1: device name
    local dn="$1" eblock line blks=5
    local dns=${dn##/dev/}


    # number of bytes on disk in parted output line 'Disk $dn: xxxxxxxxxB'
    while read line
    do
	if [[ "$line" =~ "Disk $dn: " ]]
	then
            nb=${line##*:}
            nb=${nb%B}
            eblock=$(((nb/512)-blks))
	fi
    done < <(parted $dn unit B print 2>/dev/null)
    # Zero first block
    eblock=$(($(cat /sys/block/$dns/size)-blks))
    dd if=/dev/zero bs=512 count=$blks of=$dn status=none
    # Zero last block
    dd if=/dev/zero bs=512 count=$blks of=$dn seek=$eblock status=none
}

function ismounted() {
    # Checks partitions but not devices
    if findmnt --noheadings --output source $1 > /dev/null
    then
	return 0
    else
	return 1
    fi
}

function notmounted() {
    # Use for checking a device (/dev/sdX) rather than a partition
    if grep -qs $1 /proc/mounts
    then
	return 1
    else
	return 0
    fi
}

function getmtpt() {
    if ! ismounted /mnt/sdm
    then
	echo "/mnt/sdm"
    else
	if ismounted /mnt/sdm.${BASHPID}
	then
	    errexit "? Alternate mount point /mnt/sdm.${BASHPID} in use"
	fi
	echo "/mnt/sdm.${BASHPID}"
    fi
}

function makemtpt() {
    local sdmpt=$(getmtpt)
    [ ! -d $sdmpt ] && mkdir -p $sdmpt
    echo $sdmpt
}

function getmtptX() {
    if ! ismounted /mnt/sdmX
    then
	echo "/mnt/sdmX"
    else
	if ismounted /mnt/sdmX.${BASHPID}
	then
	    errexit "? Alternate mount point /mnt/sdmX.${BASHPID} in use"
	fi
	echo "/mnt/sdmX.${BASHPID}"
    fi
}

function makemtptX() {
    local sdmpt=$(getmtptX)
    [ ! -d $sdmpt ] && mkdir -p $sdmpt
    echo $sdmpt
}

function islooped() {
    #
    # Returns true if file in $1 is already looped
    #
    local fn=$(realpath $1) line
    line=$(losetup --all --list --noheadings)
    [[ "$line" =~ "$fn" ]] && return 0 || return 1
}

function getloopdev() {
    #
    # Returns the loopdev associated with the specified mount point
    #
    # $1: Mount point (either $SDMPT or $SDMPT/boot[/firmware])
    #
    echo $(findmnt --noheadings --output source $1)
}

function rmloopdevs() {
    local fn=$(realpath $1) line loopdev
    while read line
    do
	if [[ "$line" =~ "$fn" ]]
	then
	    read loopdev rest <<< "$line"
	    #echo "% Delete dangling loop device '$loopdev'"
	    #echo "  $line"
	    losetup --detach $loopdev >/dev/null 2>&1
	fi
    done < <(losetup --all --list --noheadings)
}

function domount() {
    #
    # $1: device name/IMG Name/directory name
    # $2: "Directory" "IMG" or "Device"
    # $3: mount point (D:$SDMPT)
    # $4: fstype
    # Tree will be mounted on the mountpoint, which must be set with makemtpt or makemtptX
    #
    local dmimg=$1 imgtype=$2 mpoint=${3} fstype=$4
    local p1="1" p2="2" pinfo csize bootstart bootsize rootstart rootsize mdir sts rootfsd lv=0 kf=""

    [ "$mpoint" == "" ] && mpoint=$SDMPT
    [ ! -d $mpoint ] && mkdir $mpoint
    if [ "$imgtype" == "Directory" ]
    then
	ismounted $(realpath $dmimg) && echo "% Directory '$dmimg' is mounted; Continuing..."
	mount --bind $dmimg $mpoint
	notmounted $mpoint && errexit "? Error mounting --bind '$dmimg'"
	[ -d $mpoint/boot/firmware ] && mdir="$mpoint/boot/firmware" || mdir="$mpoint/boot"
	mount --bind $dmimg/boot/firmware $mdir
	notmounted $mdir && errexit "? Error mounting --bind '$dmimg'"
    elif [ "$imgtype" == "IMG" ]
    then
	pinfo=($(fdisk --bytes -lo Start,Size "${dmimg}" | tail -n 2))
	while read line
	do
	    if [[ "$line" =~ ^Sector\ size\ \(logical/physical\)\:\ ([[:digit:]]{,4}) ]]
	    then
		csize="${BASH_REMATCH[1]}"
		break
	    fi
	done < <(fdisk -l "${dmimg}")
	bootstart=$((${pinfo[0]}*csize))
	bootsize=${pinfo[1]}
	rootstart=$((${pinfo[2]}*csize))
	rootsize=${pinfo[3]}
        echo "* Mount IMG '$dmimg'"
	#
	# Try mounting with offset/sizelimit with automatic loop device management first
	#
	mount -v -o loop,offset=${rootstart},sizelimit=${rootsize} -t ext4 ${dmimg} $mpoint 2>/dev/null
	sts=$?            #Get this way to facilitate manual alt mount testing
	if [ $sts -eq 0 ]
	then
	    [ -d $mpoint/boot/firmware ] && mdir="$mpoint/boot/firmware" || mdir="$mpoint/boot"
	    mount -v -o loop,offset=${bootstart},sizelimit=${bootsize} -t vfat ${dmimg} $mdir
	else
	    #
	    # Try mounting it with explicit loop device
	    # remove any dangling loop devices and mount from failed mount above
	    #
	    loopdev=$(getloopdev $mpoint)
	    umount -v $loopdev 2>/dev/null
	    rmloopdevs $dmimg
            loopdev=$(losetup --show --partscan --find $dmimg)
            mount -v ${loopdev}p2 $mpoint
            notmounted $mpoint && errexit "? Error mounting IMG '$dmimg'"
	    [ -d $mpoint/boot/firmware ] && mdir="$mpoint/boot/firmware" || mdir="$mpoint/boot"
	    mount -v ${loopdev}p1 $mdir
	fi
	notmounted $mdir && errexit "? Error mounting IMG '$dmimg'"
    else # imgtype=Device
	p1=$(getpartname $dmimg 1)
	p2=$(getpartname $dmimg 2)
	[ "$(lvmfind $dmimg pv)" == "" ] && rootfsd=$dmimg || { rootfsd=$(lvmgetmapper $dmimg) ; lv=1 ; }
	ismounted ${dmimg}${p1} && errexit "? Device $dmimg is already mounted"
	if [ $lv -eq 1 ]
	then
	    ismounted $rootfsd && errexit "? Device $rootfsd is already mounted"
	else
	    ismounted ${dmimg}${p2} && errexit "? Device ${dmimg} is already mounted"
	fi
	echo "* Mount device '$dmimg'"
	if [ $lv -eq 1 ]
	then
	    mount -v $rootfsd $mpoint
	else
	    if [ "$fencrypted" == "1" ]
	    then
		[ "$keyfile" != "" ] && kf="--key-file $keyfile"
		cryptsetup luksOpen ${dmimg}${p2} sdmcrypt $kf
		errifrc $? "? Cryptsetup failed to open ${dmimg}${p2}"
		mount -v /dev/mapper/sdmcrypt $mpoint
	    else
		if [ "$fstype" != "zfs" ]
		then
		    mount -v ${dmimg}${p2} $mpoint
		else
		    zfs mount rpool/ROOT/debian
		fi
	    fi
	fi
	notmounted $mpoint && errexit "? Error mounting device '$dmimg'"
	[ -d $mpoint/boot/firmware ] && mdir="$mpoint/boot/firmware" || mdir="$mpoint/boot"
	mount -v ${dmimg}${p1} $mdir
	notmounted $mdir && errexit "? Error mounting IMG '$dmimg'"
    fi
}

function docleanup() {
    #
    # $1: if 'keep' (don't unset SDMPT)
    local mnt=$SDMPT loopdev1 loopdev2 bp
    local zfsp zfsmpt

    zfsp="$(type -p zfs)"
    zfsmpt="$(zfs list -o mountpoint -H rpool/ROOT/debian 2>/dev/null)"

    for mnt in "${SDMPT:-/mnt/sdm}" "$SDMPX"
    do
	if [ "$mnt" != "" ]
	then
	    if [ "$zfsmpt" != "$mnt" ]
	    then
		# Find loop device names and delete them after dismounting if not automatically deleted
		[ -d $mnt/boot/firmware ] && bp="$mnt/boot/firmware" || bp="$mnt/boot"
		loopdev1=$(findmnt $bp --noheadings --output source)
		[ "$loopdev1" != "/dev/loop1" ] && loopdev1=${loopdev1%p1}
		loopdev2=$(findmnt $mnt --noheadings --output source)
		loopdev2=${loopdev1%p2}
		ismounted $bp && umount -v $bp
		ismounted $mnt && umount -v $mnt
		[ "$loopdev1" == "" ] || losetup --detach $loopdev1 >/dev/null 2>&1
		[ "$loopdev2" == "" ] || losetup --detach $loopdev2 >/dev/null 2>&1
		[ "$fencrypted" == "1" ] && cryptsetup luksClose /dev/mapper/sdmcrypt >/dev/null 2>&1
	    else
		umount -v /dev/sdb1
		zfs unmount rpool/ROOT/debian
	    fi
	fi
    done
    if [ "$1" != "keep" ]
    then
	if [ "$SDMPT" != "" ]
	then
	    [ "$SDMPT" != "/mnt/sdm" ] && rm -rf $SDMPT
	    unset SDMPT
	fi
	[ "$SDMPX" != "" ] && rm -rf $SDMPX
	unset SDMPX
    fi
    sync
}

function lvmfind() {
    local dev=$1 lvitem=${2,,}
    local vgname vgn
    if [ "$(type -p pvs)" != "" ]
    then
	if [[ "$(pvs --noheadings -o pv_name)" =~ "$dev" ]]
	then
	    [ "$lvitem" == "pv" ] && echo "$dev" && return
	    while read line
	    do
		read vgname pvname <<< $line
		[[ "$pvname" =~ "$dev" ]] && break
	    done < <(vgs --noheadings -o vg_name,pv_name)
	    [ "$lvitem" == "vg" ] && echo "$vgname" && return
	    while read line
	    do
		read lvname vgn <<< $line
		[[ "$vgn" =~ "$vgname" ]] && break
	    done < <(lvs --noheadings -o lv_name,vg_name)
	    [ "$lvitem" == "lv" ] && echo "$lvname" && return
	fi
    fi
}

function lvmgetmapper() {
    local dev=$1
    echo "/dev/mapper/$(lvmfind $dev vg)-$(lvmfind $dev lv)"
}

function lvmdelete() {
    local dev=$1
    local thispv="" thisvg=""
    if [[ "$(pvs --noheadings -o pv_name)" =~ "$dev" ]]
    then
        thispv=$dev
        #
        # Find vg. Loop through all vgs and find
	#
        while read line
        do
            #echo "vg: $line"
            read vgname pvname <<< $line
            [[ "$pvname" =~ "$dev" ]] && thisvg=$vgname && break
        done < <(vgs --noheadings -o vg_name,pv_name)
        #
        # Find lv. Loop through all lvs and find
        #
        while read line
        do
            #echo "lv: $line"
            read lvname vgname <<< $line
            [[ "$vgname" =~ "$thisvg" ]] && thislv=$lvname && break
        done < <(lvs --noheadings -o lv_name,vg_name)

        [ "$thislv" != "" ] && lvremove --yes --force --force /dev/$thisvg/$thislv
        [ "$thisvg" != "" ] && vgremove --yes --force --force /dev/$thisvg
        [ "$thispv" != "" ] && pvremove --yes --force --force $dev >/dev/null 2>&1
    fi
}

function dbgshell() {
    local msg="${1:-Debug}"
    echo "dbgshell: $msg"
    bash </dev/tty >/dev/tty
    echo "dbgexit: $msg"
}

function checknumeric() {
    #
    # Exit with error if $1 is not numeric
    #
    [[ "$1" = *[^0-9]* ]] && errexit "? Value '$1' for command switch '$2' is not numeric"
    return
}

function fndotfullpath() {
    #
    # Fix directory if it's "."
    #
    local fn="$1"
    if [ "$fn" != "" ]
    then
	[ "$(dirname $fn)" == "." ] && fn="$(pwd)/$fn"    # Ensure fully qualified path to cscript
    fi
    echo $fn
}

function appendvalue() {
    #
    # append value to string
    # $1: former value of string
    # $2: value to append to string
    # $3: separator character
    # Return value is the new string
    #
    local  oldval=$1 addval=$2 sep=$3
    [ "$oldval" == "" ] && echo "$addval" || echo "${oldval}${sep}${addval}"
    return
}

function stripbcline() {
    #
    # Strip trailing blanks and comments
    #
    local line="$1"

    line="${line%%\#*}"    # Del EOL comments
    line="${line%"${line##*[^[:blank:]]}"}"  # Del trailing spaces/tabs
    echo "$line"
}

function stripquotes() {
    #
    # Remove leading/trailing single and double quotes
    # If $2 != "" then quote $ character
    #
    local str="$1" qd="$2" snolq
    [[ "$qd" != "" ]] && [[ "$str" =~ "$" ]] && str=${str//$/\\$}    # Replace $ with \$
    str="${str%"${str##*[^[:blank:]]}"}"  # Del trailing spaces/tabs
    snolq="${str#\"}"         # Get string without open quote
    if [ "$snolq" != "$str" ] # if lq was there, then update string and also del close quote
    then
	str="$snolq"
	str="${str%\"}"
    fi
    snolq="${str#\'}"         # Get string without open quote
    if [ "$snolq" != "$str" ] # Ditto double quote comment
    then
	str="$snolq"
	str="${str%\'}"
    fi
    echo "$str"
}

function gterm1() {
    #
    # $1: control (10:foreground, 11:background, 12:cursor, 13:mousefg, 14:mousebg)
    # $2 varname to set
    #
    local vname="$2" cval
    # Query the xterm for the attribute and read result
    stty -echo ; printf "\e]$1;?\007" ;  read -s -n 24 -t 0.2 cval ; stty echo
    # Trim response to just the color string and return it in the named variable
    cval="${cval:2:${#cval}-3}"   # Strip off ESC] at beginning and ctrl-g at end of string
    cval="${cval##*;}"            # Strip off leading semi-colon
    printf -v "$vname" "%s" "$cval" #eval "${vname}=\"$cval\""     # Define a variable for the value
}

function gtermcolors() {
    #
    # Query the xterm for the current terminal colors (cursor, bg, fg)
    # Return in the value named in ${1}cursor ${1}bg ${1}fg (rgb:xxxx/xxxx/xxxx)
    #
    gterm1 10 "${1}fg"
    gterm1 11 "${1}bg"
    gterm1 12 "${1}cursor"
}

function stermcolors() {
    #
    # Arguments are positional, but optional. e.g., "" "" "00" will set only the cursor
    # $1 - foreground
    # $2 - background
    # $3 - cursor
    # $4 - (optional) name of string for saving current colors (see gtermcolors)
    #
    local os=""
    [ "$4" != "" ] && gtermcolors "$4"
    [ "$1" != "" ] && os="\e]10;$1\a"
    [ "$2" != "" ] && os="${os}\e]11;$2\a"
    [ "$3" != "" ] && os="${os}\e]12;$3\a"
    printf "$os"
    return 0
}

function resetcolors() {
    #
    # Set all colors saved from stermcolors
    #
    local tfg tbg tcursor
    # old way: eval "tfg=\$${1}fg ; tbg=\$${1}bg ; tcursor=\$${1}cursor"
    tfgn="${1}fg" ; tbgn="${1}bg" ; tcn="${1}cursor" 
    printf -v tfg "%s" "${!tfgn}" 
    printf -v tbg "%s" "${!tbgn}"
    printf -v tcursor "%s" "${!tcn}"
    stermcolors "$tfg" "$tbg" "$tcursor"
    return 0
}

function flashled0() {
    echo 1 | tee /sys/class/leds/led0/brightness > /dev/null 2>&1
    sleep $1
    echo 0 | tee /sys/class/leds/led0/brightness > /dev/null 2>&1
    sleep .2
}

function morseled() {
    local msg="$1"
    local dit=".2" dot=".6" inter=".5"
    for (( i=0 ; i<${#msg} ; i++))
    do
	case "${msg:$i:1}" in
	    .) flashled0 $dit ;;
	    -) flashled0 $dot ;;
	    " ") sleep $inter ;;
	esac
	
    done
}

function iponline() {
    #
    # Test if IP address $1 is online
    # Returns 0 if online, 1 if offline
    #
    local pcnt=1 pingwait=1
    if (ping -c $pcnt -W $pingwait $1 > /dev/null 2>&1 )
    then
	return 0
    else
	return 1
    fi
}

function ckkeymap() {
    [ "$1" == "" ] && return 0
    (grep "^  ${1} " /usr/share/doc/keyboard-configuration/xorg.lst > /dev/null 2>&1) && return 0 || return 1
}

function cklocale() {
    [ "$1" == "" ] && return 0
    (grep "^${1}" /usr/share/i18n/SUPPORTED > /dev/null 2>&1) && return 0 || return 1
    return 0
}

function ckwificountry() {
    [ "$1" == "" ] && return 0
    (grep "^${1}" /usr/share/zoneinfo/iso3166.tab > /dev/null 2>&1) && return 0 || return 1
}

function cktimezone() {
    [ "$1" == "" ] && return 0
    [ -e /usr/share/zoneinfo/$1 ] && return 0 || return 1
}

function ckl10n() {
    # $1: item name (keymap, locale, timezone)
    # $2: value to check
    local klt="$1" value="$2" sts=1
    case "$1" in
	keymap)
	    ckkeymap "$value"
	    sts=$?
	;;
	locale)
	    cklocale "$value"
	    sts=$?
	;;
	timezone)
	    cktimezone "$value"
	    sts=$?
	;;
    esac
    return $sts
}
