#!/bin/bash

#==============================================================================
# live-usb-maker
# A fast and robust program to make full featured antiX/MX live-usbs
#
# (C) 2016 -- 2017 Paul Banham <antiX@operamail.com>
# License: GPLv3 or later
#==============================================================================

      VERSION="2.10.08"
 VERSION_DATE="Tue Aug  1 15:36:54 MDT 2017"

        ME=${0##*/}
    MY_DIR=$(dirname "$(readlink -f $0)")
MY_LIB_DIR=$(readlink -f "$MY_DIR/../cli-shell-utils")
   LIB_DIR="/usr/local/lib/cli-shell-utils"

export TEXTDOMAIN="cli-shell-utils"
domain_dir="$MY_DIR/../cli-shell-utils/locale"
test -d "$domain_dir" && export TEXTDOMAINDIR=$domain_dir

#== BEGIN_CONFIG
     ISO_FILE_DIR="%USER_HOME%"
    ISO_FILE_SPEC="*.iso"
      SEARCH_DIRS="%USER_HOME% /media /root"
     SEARCH_DEPTH="4"
        MAX_FILES="20"
     MIN_ISO_SIZE="180M"

        MSDOS_GPT="msdos"
     DEFAULT_SIZE="100%"
         CMD_SIZE=""
        BIOS_SIZE="150"
        UEFI_SIZE="50"
      BIOS_MARGIN="20"
      MAIN_MARGIN="20"
      UEFI_MARGIN="5"

     EXT4_OPTIONS="-m0 -i100000 -J size=32"
       BIOS_LABEL="Live-usb"
        ESP_LABEL="Live-uefi"

          LIVE_MP="/live/boot-dev"
     LINUXFS_NAME="linuxfs"
 MIN_LINUXFS_SIZE="120M"
     DEF_BOOT_DIR="antiX"

       CLONE_DIRS="boot EFI efi"
      CLONE_FILES="cdrom.ico version"
 CLONE_BDIR_FILES="{vmlinuz,vmlinuz1,initrd.gz,linuxfs}{,.md5}"

       UEFI_FILES="[Ee][Ff][Ii] boot/{grub,uefi-mt} version"
       BIOS_FILES="[Ee][Ff][Ii] boot/{syslinux,grub,memtest} antiX/{vmlinuz,initrd}* version"

        GRUB_CONF="boot/grub/grub.cfg"
           CHEATS=""
     COLOR_SCHEME="high"
    QUESTION_MODE="default"

          ENCRYPT=""
 PASS_PHRASE_TYPE=""
        LUKS_NAME="live-usb-maker"

EXT_OVERHEAD_FORM="* 26 / 10000 - 2"
 EXT_OVERHEAD_MAX="44"

  PP_MAX_WORD_LEN="8"
     PP_NUM_WORDS="6"
    PP_WORDS_FILE="/usr/share/dict/american-english"
   PP_RAND_SOURCE="/dev/urandom"

  GRAPHICAL_MENUS="true"
  AUTOMOUNT_DELAY="1"
#== END_CONFIG

BACKUP_WORDS_FILE="/usr/share/dict/words"
      CRYPT_PROGS="cryptsetup dmsetup"
     PHRASE_FNAME="passphrase"
       SEED_FNAME="random-seed"
    ENCRYPT_FNAME="encrypted"
   ENCRYPT_ENABLE="enable"

         WORK_DIR="/run/$ME"
        SHELL_LIB="cli-shell-utils.bash"
      CONFIG_FILE="/root/.config/$ME/$ME.conf"
     THE_LOG_FILE="/var/log/$ME.log"
     THE_ERR_FILE="/var/log/$ME.error"
    THE_PROG_FILE="/var/log/$ME.progress"
         LOG_FILE="/dev/null"
         ERR_FILE="/dev/null"
        PROG_FILE="/dev/null"

    EXT4_NO_64BIT="-O ^64bit"

ORDERED_CMDS="partition-clear partition-make makefs-main makefs-bios makefs-uefi copy-main copy-bios copy-uefi"
ORDERED_CMDS="$ORDERED_CMDS uuids cheats cheats-syslinux cheats-grub install encrypt-main encryption-initrd"
    ALL_CMDS="sizes all partition makefs copy cheats $ORDERED_CMDS"

   ALL_FORCE="copy,flock,makefs,umount,usb"
   ALL_PAUSE="exit,initrd,copy"

    LIB_PATH="$MY_LIB_DIR:$LIB_DIR"
        PATH="$MY_LIB_DIR/bin:$LIB_DIR/bin:$PATH"

MADE_BY_FILE="made-by-live-usb-maker"

SYSLINUX_FILES="chain.c32 gfxboot.c32 vesamenu.c32 ldlinux.c32 libcom32.c32"
SYSLINUX_FILES="$SYSLINUX_FILES libmenu.c32 libutil.c32 linux.c32 menu.c32"
   SYSLINUX_RM="*.c32 ldlinux.sys syslinux.bin isolinux.bin version"

        SWEARS="asshole|bastard|bitch|cock|crap|cunt|damn|dick|douche|fag|fuck|piss|pussy|shit|slut"


#------------------------------------------------------------------------------
# Show usage and exit
#------------------------------------------------------------------------------
usage() {
    local ret=${1:-0}

cat<<Usage
Usage: $ME [<options>] [default|expert|simple|gui]

Create a live-usb from an iso-file, another live-usb, a live-cd/dvd
or a running live system.  You will be prompted for information that
is not supplied in the command line options.

    default:  default "no" to some questions.
     expert:  default "yes" to some questions.
     simple:  skip some questions
        gui:  non-interactive, disable progress bar, enable progress file

Uses ext4 as the filesystem for the main live-usb partition and adds
a small fat32 file system for booting via UEFI.

This will destroy any existing information on <usb-device>.  The default
partitioning scheme is GPT.  Use --msdos flag to use msdos partitioning
instead.

  --from="iso-file"    Enter an iso file to use as the source
  --from="clone"       clone a running live system.
  --from=clone=<dir>   clone from a mounted live-usb or iso-file.
  --from=<dev>         copy from a livecd/dvd or live-usb

Options:
  -b --bios-size=<xx>   Size of BIOS boot partition when using encryption
  -c --cheat=xxx        Add these cheatcodes to the live-usb
                           Use "off" or "no" to disable cheats menu.
                           Use "on" or "yes"  to show cheat menus without asking
                        Otherwise you will be asked.
  -C --color=<xxx>      Set color scheme to off|low|low2|bw|dark|high
  -E --encrypt          Set up to boot from an encrypted partition
  --encrypt=<flag>      Phasephrase option:
                           ask         Enter the passphrase via the keyboard
                           first-boot  Force user to set phrase on first boot
                           file=xxx    Read phrase from file <xxx>
                           random      Generate a random passphrase
                           random=N    Generate a random passhphrase containing
                                       N words (1 -- 20 allowed)

  -e --esp-size=<xx>    Size of ESP (uefi) partition in MiB (default 50)
  --ext-options=<xx>    Use these options when creating the ext4 filesystem

  -f --from=<xxx>       The device, cdrom, or file to make the live-usb from
                        Use "clone" to clone the current live system or use
                        clone=<xxx> to clone another live-usb

  -F --force=<xxx>      Force the options specfied:
                            umount: Allows try to umount all partitions on drive
                               usb: Ignore usb/removable check
                            makefs: Make the ext4 filesystem even if one exists
                              copy: Overwrite ext4 partition even if antiX/ exists
                               all: All of the above (dangerous!)
  -- --gui-progress     All remaining args are used as a gui progress bar program
                        Example: --gui-progress yad --progress --auto-close
  -g --gpt              Use gpt partitioning instead of msdos
  -G --graphic-ui       Use the new graphics user interface (default)
     --gpt-pmbr         Set pmbr_boot disk flag (prevents booting via UEFI)
  -h --help             Show this usage
  -i --initrd=<file>    Start with <file> for making encrypt enabled initrd
  -I --ignore-config    Ignore the configuration file
  -k --keep-syslinux    Don't replace the syslinux files
  -L --label=Name       Label ext partition with Name
  -m --msdos            Use msdos partitioning (default) instead of gpt
  -n --no-prog-bar      Don't show progress *bar* when copying
  -N --numeric-ui       Use the legacy numerical user interface
     --pause            Wait for user input before exit
     --pause=initrd     Pause after unpacking the initrd.gz file
     --percent-prog     Show progress percentage but no bar
  -p --pretend          Don't run commands that affect the usb device
  -P --progress         Create $THE_PROG_FILE progress *file*
  -q --quiet            Print less
  -R --reset-config     Write a fresh config file with default options
  -s --size=XX          Percent of usb-device to use (default 100)
  -t --target=<xxx>     The device to make into a new live-usb
  -v --version          Show version information
  -V --verbose          Print more, show command outputs
  -VV --very-verbose    Also show commands
  -W --write-config     Write a config file preserving current options

Notes:
  - short options stack. Example: -pv is the same as --pretend --verbose
  - options can be intermingled with commands and parameters
  - config file: $CONFIG_FILE
  - the config file will be sourced if it exists
  - it will be created if it doesn't exist
  - If encryption is enabled, the user will be prompted to enter a new
    passphrase on the first boot of the live-usb
Usage
    exit $ret
}

#------------------------------------------------------------------------------
# Callback routine to evalute some command line args before the root user test.
#------------------------------------------------------------------------------
eval_early_argument() {
    local val=${1#*=}
    case $1 in
      -ignore-config|I) IGNORE_CONFIG=true   ;;
       -write-config|W) WRITE_CONFIG=true    ;;
       -reset-config|R) RESET_CONFIG=true    ;;
        -gpt|-gpt-pmbr) CMD_GPT=true         ;;
              -force|F) FORCE="$FORCE,$val"  ;;
              -force=*) FORCE="$FORCE,$val"  ;;
           -progress|P) PROGRESS=true        ;;
                -pause) PAUSE="$PAUSE${PAUSE:+,}exit" ;;
               -help|h) usage                ;;
            -version|v) show_version         ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to evaluate arguments after the root user check.  We also
# need to include the early args to avoid unknown argument errors.
#------------------------------------------------------------------------------
eval_argument() {
    local arg=$1  val=$2
    case $arg in
         -bios-size|b)  BIOS_SIZE=$val                   ;;
         -bios-size=*)  BIOS_SIZE=$val                   ;;
             -cheat|c)  CHEATS="$CHEATS${CHEATS:+ }$val" ;;
             -cheat=*)  CHEATS="$CHEATS${CHEATS:+ }$val" ;;
             -color|C)  COLOR_SCHEME=$val                ;;
             -color=*)  COLOR_SCHEME=$val                ;;
           -encrypt|E)  ENCRYPT=true                     ;;
           -encrypt=*)  ENCRYPT=true ; PASS_PHRASE_TYPE=$val  ;;
          -esp-size|e)  UEFI_SIZE=$val                   ;;
         -ext-options)  EXT4_OPTIONS=$val                ;;
       -ext-options=*)  EXT4_OPTIONS=$val                ;;
          -esp-size=*)  UEFI_SIZE=$val                   ;;
              -from|f)  FROM=$val                        ;;
              -from=*)  FROM=$val                        ;;
       --graphic-ui|G)  GRAPHICAL_MENUS=true             ;;
               -gpt|g)  MSDOS_GPT="gpt"                  ;;
            -gpt-pmbr)  MSDOS_GPT="gpt" ; DO_PMBR=true   ;;
      -gui-progress|-)  END_CMDLINE=true                 ;;
            -initrd|i)  INITRD_FILE=$val                 ;;
     -keep-syslinux|k)  KEEP_SYSLINUX=true               ;;
             -label|L)  BIOS_LABEL=$val                  ;;
             -label=*)  BIOS_LABEL=$val                  ;;
             -msdos|m)  MSDOS_GPT="msdos"                ;;
       -no-prog-bar|n)  NO_PROGRESS_BAR=true             ;;
               -pause)  PAUSE="$PAUSE${PAUSE:+,}exit"    ;;
        -numeric-ui|N)  GRAPHICAL_MENUS=                 ;;
             -pause=*)  PAUSE="$PAUSE${PAUSE:+,}$val"    ;;
        -percent-prog)  PERCENT_PROG=true                ;;
           -pretend|p)  PRETEND_MODE=true                ;;
          -progress|P)  PROGRESS=true                    ;;
             -quiet|q)  QUIET=true                       ;;
              -size|s)  CMD_SIZE=${val%\%}               ;;
              -size=*)  CMD_SIZE=${val%\%}               ;;
            -target|t)  TARGET=$val                      ;;
            -target=*)  TARGET=$val                      ;;
           -verbose|V)  VERBOSITY=$((VERBOSITY + 1))     ;;
        -very-verbose)  VERBOSITY=$((VERBOSITY + 2))     ;;

       # These are read early.  They are not unknown

             -force|F)                                   ;;
             -force=*)                                   ;;
     -ignore-config|I)                                   ;;
      -reset-config|R)                                   ;;
      -write-config|W)                                   ;;

               *)  fatal "Unknown parameter %s" "-$arg"  ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine for command line arguments that don't start with "-"
#------------------------------------------------------------------------------
assign_parameter() {
    local cnt=$1 param=$2
   case $param in
       default) QUESTION_MODE=default          ;;
        expert) QUESTION_MODE=expert           ;;
        simple) QUESTION_MODE=simple           ;;
           gui) QUESTION_MODE=gui              ;;
             *) CMD_CMDS="$CMD_CMDS $param"    ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to see if an argument requires a value to follow it.
#------------------------------------------------------------------------------
takes_param() {
    case $1 in
   -bios-size|b) return 0 ;;
       -cheat|c) return 0 ;;
       -color|C) return 0 ;;
    -esp-size|e) return 0 ;;
        -from|f) return 0 ;;
       -force|F) return 0 ;;
      -initrd|i) return 0 ;;
       -label|L) return 0 ;;
        -size|s) return 0 ;;
      -target|t) return 0 ;;
    esac
    return 1
}

#------------------------------------------------------------------------------
# The main routine.  Called from the very bottom of this script.
#------------------------------------------------------------------------------
main() {
    local SHIFT SHIFT_2 SHORT_STACK="bcCDeEfFgGhIkLmnNpPqRstvVW"
    local BE_VERBOSE FROM TARGET FATAL_QUESTION CMD PARAM_CNT ENCRYPT
    local VERY_VERBOSE VERBOSITY=0
    local ISO_BOOT_DIR  START_T=0

    set_colors

    local orig_args="$*"

    # Let non-root users get usage.  Need to get --ignore-config early.
    read_early_params "$@"

    need_root
    EXIT_NUM=100

    read_reset_config_file "$CONFIG_FILE"

    # Strip off leading / once and for all
    DEF_BOOT_DIR="${DEF_BOOT_DIR#/}"

    # Force stored 'gpt' value back to msdos.  Can still be overridden by the
    # --gpt command line parameter
    if [ -z "$CMD_GPT" -a "$MSDOS_GPT" != 'msdos' ]; then
        warn "Forcing partitioning to be %s" "$(pqh msdos)"
        MSDOS_GPT=msdos
        write_config "$CONFIG_FILE"
    fi

    ERR_FILE=$THE_ERR_FILE
    trap clean_up EXIT
    do_flock

    rm -f $ERR_FILE

    read_all_cmdline_mingled "$@"
    set_colors $COLOR_SCHEME

    case $VERBOSITY in
        0) ;;
        1) BE_VERBOSE=true                     ;;
        *) BE_VERBOSE=true ; VERY_VERBOSE=true ;;
    esac

    [ -n "$BE_VERBOSE" -a -n "$PRETEND_MODE" ] && VERY_VERBOSE=true

    CMDS=${CMD_CMDS:-all}

    check_cmds  CMDS  "$ALL_CMDS" "$ORDERED_CMDS"
    check_force FORCE "$ALL_FORCE"
    check_pause PAUSE "$ALL_PAUSE"

    [ -n "$INITRD_FILE" ] && ! test -r "$INITRD_FILE" \
        && fatal "Could not find initrd file %s" "$(pqw $INITRD_FILE)"

    local percent_size=${CMD_SIZE%%%}
    case $percent_size in
        "") ;;
        [1-9]|[0-9][0-9]|100) ;;
        *) fatal "Wrong percentage value '%s'.  Should be between %s and %s inclusive." "$(pqh $percent_size)" "1%" "100%"
    esac

    [ -z "${UEFI_SIZE##[0-9]}" ] && fatal "esp-size must be larger than %s" 9
    echo $UEFI_SIZE | egrep -q "^[1-9][0-9]+$" || fatal "esp-size must be an integer larger than %s" 9

    # Always write a new config file and then exit if requested
    [ "$WRITE_CONFIG" ] && write_config "$CONFIG_FILE" && exit 0

    need_prog extlinux
    q_mode gui && PROGRESS=true
    [ "$PROGRESS" ] && PROG_FILE=$THE_PROG_FILE

    # Make sure we have a --from and a --target
    if q_mode gui; then
       fatal_z "$FROM"    "No --from was given while in %s mode" gui
       fatal_z "$TARGET"  "No --target was given while in %s mode" gui
    fi

    shout_title $"Starting %s" "$ME"
    start_log "$orig_args" "$CMDS"

    # NOTE: this placement is not ideal because it allows the user to
    # write an invalid pass_phrase_type to the config file
    #======== Encryption =====================================================
    if encrypt; then
        q_mode gui && : ${PASS_PHRASE_TYPE:=first-boot}
        validate_passphrase_type "$PASS_PHRASE_TYPE"
    fi
    #=========================================================================

    type -t find_man_page &>/dev/null && find_man_page

    shift $SHIFT_2
    if [ $# -gt 0 ]; then
        type "$1" &>/dev/null || fatal "Could not find progress program %s" "$(pqh $1)"
        printf "Will use progress %s: $*\n" "$(my_type $1)" >> $LOG_FILE
    fi

    shout_pretend

    mkdir -p $WORK_DIR || fatal "Could not make a work directory under %s" "$(dirname "$WORK_DIR")"
    mount -t tmpfs tmpfs $WORK_DIR || fatal "Could not mount tmpfs at %s" $WORK_DIR
    # Set up our directories.  These a GLOBALS so they can be used in clean_up() on exit
    ISO_DIR=$WORK_DIR/iso
    BIOS_DIR=$WORK_DIR/bios
    MAIN_DIR=$WORK_DIR/main
    UEFI_DIR=$WORK_DIR/uefi
    INITRD_DIR=$WORK_DIR/initrd
    LINUX_DIR=$WORK_DIR/linux
    MNT_DIR=$WORK_DIR/live-dev
    WORDS_FILE=$WORK_DIR/words

    mkdir $ISO_DIR $BIOS_DIR $MAIN_DIR $UEFI_DIR || fatal "Could not make %s subdirectories" "$WORK_DIR"
    echo $$ > $WORK_DIR/pid

    #--- Find live boot device if we are running live

    local we_are_live live_dev
    if its_alive; then
        we_are_live=true
        # FIXME: should use initrd.out to get the uuid, etc
        live_dev=$(get_live_dev)
        if [ -n "$live_dev" ]; then
            shout "Found live media device %s" "$(pqb /dev/$(get_drive $live_dev))"
        else
            "$live_dev" "The live media is not mounted"
        fi
    fi

    # Check cmdline target *before* the first menu
    if [ ${#TARGET} -eq 0 ]; then
        select_target_device TARGET "$live_dev" "$FROM"
    else
        check_target "$TARGET" "$live_dev"
    fi
    msg $"Will use target device %s" "$(pq $TARGET)"

    [ ${#CMD_CMDS} -eq 0  ] && ! q_mode simple gui && select_overall_mode CMDS QUESTION_MODE

    if need_q copy && ! encrypt; then
        expert_yes_NO $"Create an encrypted live-usb?" && ENCRYPT=true
    fi

    # FIXME: better error messsage!!
    encrypt && need_prog copy-initrd-programs unpack-initrd cryptsetup dmsetup

    local from

    if [ ${#FROM} -gt 0 ]; then
        check_from from "$FROM" "$TARGET"
    elif ! need_q 'copy-main|copy-bios|copy-uefi'; then
        from='null=null'
    else
    #--- Select source of live-usb if one was not given
        select_usb_src from "$TARGET"
    fi

    local from_file from_act=$"copying"
    case $from in
        iso-file|iso|file)
            cli_get_filename from_file $"Please enter the filename" "$ISO_FILE_DIR"
            from=file=$from_file ;;

        clone)
            #
            from=clone=$LIVE_MP ;;

        dev=*|clone=*|file=*)   ;;
        null=*)                 ;;

        *) test -f "$from" && from=file=$from ;;
    esac

    [ -n "$from" -a -n "${from%%*=*}" ] && internal_error "bad from value 1 " "$from"

    local from_type=${from%%=*}
    local from_value=${from#*=}

    # The $from variable must be type=value as seen below.  This is more
    # Complicated but it makes it easier to report to user what is going on
    local from_dev from_thing
    case $from_type in
        file)
            mount_iso_file "$from_value" "$ISO_DIR"
            check_md5 "$from_value"
            # What thing we are copying or cloning from: file, device or directory
            from_thing=$"file"
            ;;

        dev)
            mount_device "$from_value" "$ISO_DIR"
            # What thing we are copying or cloning from: file, device or directory
            from_thing=$"device"
            ;;

        clone)
            # First see if we were given a block device to clone
            local from_dev=$(expand_device "$from_value")
            # Cloning only copies certain files to make a brand-new live-usb
            from_act=$"cloning"

            # Just clone the directory instead if the device is already mounted
            if is_mounted "$from_dev"; then
                from_value=$(grep "^$from_dev " /proc/mounts | cut -d" " -f2 | head -n1)
                msg "Will trying cloning directory %s" "$(pq "$from_value")"
                from_dev=
            fi

            if [ ${#from_dev} -gt 0 ]; then
                mount_device    "$from_dev" "$MNT_DIR"
                clone_directory "$MNT_DIR"  "$ISO_DIR"
                # What thing we are copying or cloning from: file, device or directory
                from_thing=$"device"

            elif test -d "$from_value"; then
                clone_directory "$from_value" "$ISO_DIR"
                # What thing we are copying or cloning from: file, device or directory
                from_thing=$"directory"

            else
                fatal "Can only clone devices and directories, not '%s" "$from_value"
            fi
            ;;

        null) ;;
        *)
            internal_error "Bad from variable 2" "$from"
            ;;
    esac

    # Will use source <file XYZ>
    msg $"Will use source %s" "$from_thing $(pq $from_value)"
    show_distro_version "$ISO_DIR" # "$from_value"

    local distro_name=$(get_distro_name "$ISO_DIR/version")
    if [ -n "$distro_name" ]; then
        BIOS_LABEL=$(make_label 16 - "$distro_name" Live usb)
        ESP_LABEL=$(make_label 11 - "$distro_name" uefi)
    fi

    # Make sure our target is a real device
    local target_dev=$(expand_device $TARGET)
    [ ${#target_dev} -gt 0 ] || fatal $"Could not find device %s" "$TARGET"

    local cheats
    if need_q cheats; then
        case $CHEATS in
            "") expert_YES_no $"Customize language and timezone?"  && cheats_menus cheats ;;
        off|no) ;;
        yes|on) cheats_menus cheats ;;
             *) cheats=$CHEATS      ;;
        esac
    fi

    # These are mostly for a manually entered target
    test -e $target_dev || fatal "Target device %s does not exist" $target_dev
    test -b $target_dev || fatal "Target device %s is not a block device" $target_dev

    # Require that an entire disk device be specified (could relax?)
    local dev_type=$(lsblk -no type --nodeps $target_dev)
    [ "$dev_type" = 'disk' ] || fatal "Device %s is not a disk device" $target_dev

    setup_devices $target_dev

    # fatal "The device %s does not seem to be usb or removable."
    # FIXME: move this to the lib?
    force usb || is_usb_or_removable $target_dev || yes_NO_fatal "usb" \
        "Do you want to use it anyway (dangerous)?" \
        "Use %s to always ignore this warning"      \
        "The device %s does not seem to be usb or removeable."  "$target_dev"

    local bios_size  main_size  uefi_size

    encrypt && shout "\n%s" $"Encryption enabled"
    need_q partition-make && verify_sizes bios_size main_size uefi_size "$target_dev" \
        "$ISO_DIR" "$percent_size" "$BIOS_SIZE" "$UEFI_SIZE" "${DEFAULT_SIZE%%%}"

    [ ${#cheats} -gt 0 ] && msg "Cheats: %s" "$(pq $cheats)"
    # Bail early if only size info is requested
    given_cmd sizes && my_exit 0

    # Make sure the target is not in use
    umount_all $target_dev

    local encrypted_lab=""

    #======== Encryption =====================================================
    encrypt && echo
    if encrypt && need encryption-initrd; then
        encrypted_lab="$(bqq $"encrypted") "
        msg "Checking to see if the live media will support encryption ..."
        # Need to get programs, libs, and modules, out of the linuxfs file (sigh)
        local linuxfs_full=$ISO_DIR/$DEF_BOOT_DIR/$LINUXFS_NAME
        my_mount "$linuxfs_full" "$LINUX_DIR" -t squashfs -o loop,ro

        local initrd_file=${INITRD_FILE:-$ISO_DIR/$DEF_BOOT_DIR/initrd.gz}
        start_initrd_encryption "$initrd_file" "$INITRD_DIR" "$LINUX_DIR"
        pause initrd
        umount $LINUX_DIR ; rmdir $LINUX_DIR

        # FIXME: mount initrd, check init, mount linuxfs and check for cryptsetup

        test -r "$PP_WORDS_FILE" || PP_WORDS_FILE=$BACKUP_WORDS_FILE

        select_passphrase_type PASS_PHRASE_TYPE
    fi
    #=========================================================================

    local new_line=$(printf "$nc_co\n$bold_co...")
    # Ready to make live-usb on device X by <cloning|copying directory Y>
    local final_q=$(printf $"Ready to make %s on device %s by %s" "${encrypted_lab}$(pqb live-usb)" \
        "$(pqb ${target_dev##*/})$new_line" "$from_act $from_thing $(pqb $from_value)")

    [ "$CMDS" != 'all' ] \
        && final_q=$(printf "Ready to perform %s action(s) on %s" "$(pq $CMDS)" "$(pqq ${target_dev##*/})")

    if q_mode gui; then
        msg "$final_q"
    else
        echo
        shout_subtitle "$final_q"
        #final_q=$(printf "%s\n%s" "$final_q" "$(quest $"Shall we begin?")")
        YES_no_pretend $"Shall we begin?" || my_exit
    fi

    [ -n "$BE_VERBOSE" -a -n "$PRETEND_MODE" ] && VERY_VERBOSE=true

    suspend_automount

    START_T=$(date +%s)

    need partition-clear && clear_partition "$target_dev"
    need partition-make  && do_partition "$target_dev" "$MSDOS_GPT" "$bios_size" "$main_size" "$uefi_size"

    # msg "Unmount new partitions if needed ..."

    sync; sync
    #sleep ${AUTOMOUNT_DELAY:-2}

    # Make sure the new target partitions get unmounted if they were auto-mounted
    umount_all $target_dev

    # Nothing else makes sense if there is no partition table
    need_q partition-clear && ! need_q partition-make && exit_done

    cmd wait_for_file "$BIOS_DEV"
    need makefs-bios && do_makefs_ext "$BIOS_DEV" "$EXT4_OPTIONS" "$BIOS_LABEL"

    cmd wait_for_file "$UEFI_DEV"
    need makefs-uefi && do_makefs_uefi "$UEFI_DEV" "$ESP_LABEL"

    #======== Encryption =====================================================
    if encrypt && need encrypt-main; then
        wait_for_file "$MAIN_DEV"

        # Write phrase and signal file *before* encrypting
        my_mount $BIOS_DEV $BIOS_DIR
        local phrase_file=$BIOS_DIR/$DEF_BOOT_DIR/$PHRASE_FNAME
        local encrypt_file=$BIOS_DIR/$DEF_BOOT_DIR/$ENCRYPT_FNAME

        # Simplify to types: ask, file, phrase
        # Convert file to phrase to avoid problems with \n
        local phrase=$PASS_PHRASE_TYPE
        case $phrase in
                  ask)                                            ;;
               file=*) phrase=phrase=$(cat "$val")                ;;
               random) set_rand_phrase   phrase                   ;;
             random=*) set_rand_phrase   phrase ${phrase#*=}      ;;
           first-boot) first_boot_phrase phrase "$phrase_file"    ;;
                    *) internal_error "passphrase type" "$phrase" ;;
        esac

        encrypt_partition $MAIN_DEV "$LUKS_NAME" "$phrase"

        need makefs-main && do_makefs_ext "$LUKS_DEV" "$EXT4_OPTIONS" "$BIOS_LABEL"

        local main_uuid=$(lsblk --nodeps -no uuid $MAIN_DEV)
        cmd write_file $encrypt_file "$main_uuid"

        sync

        DID_ENCRYPT=true

        always_cmd umount $BIOS_DEV
        my_mount "$LUKS_DEV" "$MAIN_DIR"
    fi
    #=========================================================================

    [ -z "$DID_PARTITION" ] && guess_partitioning $target_dev

    sync; sync

    # Tell OS partitioning has changed (after we've created the new file systems)
    cmd partprobe $target_dev

    my_mount $BIOS_DEV $BIOS_DIR
    my_mount $UEFI_DEV $UEFI_DIR

    local mp_list="$BIOS_DIR $UEFI_DIR"
    encrypt && mp_list="$BIOS_DIR $MAIN_DIR $UEFI_DIR"

    display_df_output $mp_list

    #======== Encryption =====================================================
    if encrypt && need copy-bios; then
        copy_files_spec $ISO_DIR "$BIOS_FILES" "$BIOS_DIR" bios

        #need encryption-initrd &&
        finish_initrd_encryption "$INITRD_DIR" "$BIOS_DIR/$DEF_BOOT_DIR/initrd.gz"

    fi
    #=========================================================================

    if need copy-uefi; then
        copy_files_spec $ISO_DIR "$UEFI_FILES" "$UEFI_DIR" uefi
        fix_uefi_memtest $UEFI_DIR
    fi

    if need copy-main; then
        do_copy_main $ISO_DIR $MAIN_DIR "$@"
        do_made_by $BIOS_DIR "$MADE_BY_FILE"
        write_random_seed $MAIN_DIR/$DEF_BOOT_DIR/$SEED_FNAME
    fi

    # if need_q copy-bios || need_q copy-main; then
    #     : ${ISO_BOOT_DIR:=$DEF_BOOT_DIR}
    #     local initrd_file=${INITRD_FILE:-$ISO_DIR/$ISO_BOOT_DIR/initrd.gz}
    #     enable_disable_initrd_encryption "$initrd_file" "$BIOS_DIR/$DEF_BOOT_DIR/initrd.gz" "$INITRD_DIR"
    # fi

    local bios_uuid=$(lsblk -no uuid $BIOS_DEV)
    local uefi_uuid=$(lsblk -no uuid $UEFI_DEV)

    need uuids && do_uuids $BIOS_DIR "$bios_uuid" "$UEFI_DIR/$GRUB_CONF" "$uefi_uuid" "$UEFI_PART"

    [ ${#cheats} -gt 0 ] && need cheats-syslinux && do_cheats_syslinux $BIOS_DIR "$cheats"
    [ ${#cheats} -gt 0 ] && need cheats-grub     && do_cheats_grub     $UEFI_DIR "$cheats"

    need install $"install" && do_install_bootloader $target_dev $BIOS_DIR $MSDOS_GPT

    sync; sync

    need_q copy && display_df_output $mp_list

    say_done

    [ -n "$FIRST_BOOT_PHRASE" ] && shout $"You will be asked to create a passphrase during the first boot"

    my_exit 0
}

#===== End of Main ============================================================

#------------------------------------------------------------------------------
# Validate --from parameter give on command line
#------------------------------------------------------------------------------
check_from() {
    local var=$1  cmd_from=$2  exclude=$3
    case $cmd_from in
        iso|file|iso-file) eval $var=\$cmd_from ; return 0 ;;
            clone|clone=*) eval $var=\$cmd_from ; return 0 ;;
    esac
    local from_dev=$(expand_device "$cmd_from")
    if [ ${#from_dev} -gt 0 ]; then

        eval "$var=dev=\$from_dev"
        return 0

    elif test -f "$cmd_from"; then
        eval "$var=file=\$cmd_from"
        return 0
    elif test -d "$cmd_from"; then
        eval "$var=clone=\$cmd_from"
        return 0
    else
        fatal "The --from parameter '%s' was not recognized" "$cmd_from"
    fi
}

#------------------------------------------------------------------------------
# Offer several passphrases.  Let user pick one of them or enter their own
# or force the passphrase to be created on first boot of live-usb
#------------------------------------------------------------------------------
select_passphrase_type() {
    local var=$1  val
    eval val=\$$var

    # Only select a pp type if one was not already given
    [ -n "$val" ] && return

    shout

    local have_words dict_size=0 dict_bits=0
    test -r $PP_WORDS_FILE && dict_size=$(dict_size)

    if [ $dict_size -eq 0 ]; then
        warn "no words were found in the dictionary"
        warn "Will not generate passphrases"

    elif [ $dict_size -lt 5000 ]; then
        warn "only %s words were found in the dictionary" "$(nqh $dict_size)"
        warn "Will not generate passphrases"

    else
        dict_bits=$(x1 "log($dict_size)/log(2)")
        have_words=true
    fi

    # Don't let these menu entries get into the log file
    local VERBOSE_SELECT=

    local phrase  phrase_2  phrase_3   phrase_4   phrase_5   phrase_6
    local   bits    bits_2    bits_3     bits_4     bits_5     bits_6

    # Repeat if user wants to see more passphrases
    while true; do

        local i  len=4  max_len=8  seq=
        if [ "$have_words" ]; then
            shout "Please wait while some random passphrases are generated"
            msg "Dictionary size is %s words (%s bits/word)" \
                "$(nq $(add_commas $dict_size))" "$(nq $dict_bits)" | color_commas
            seq=$(seq 2 6)
        fi

        for i in $seq; do
            phrase=$(gen_passphrase $len)
            eval phrase_$i=\$phrase
            bits=$(x1 "log($dict_size)/log(2) * $len")
            eval bits_$i=\$bits
            len=$((len + 1))
            [ $len -gt $max_len ] && len=$max_len
        done

        local menu=$(
            menu_printf ask         "Enter your own passphrase"
            menu_printf first-boot  "Set passphrase on first boot of live-usb"
            for i in $seq; do
                eval phrase=\$phrase_$i
                eval bits=\$bits_$i
                menu_printf "index=$i" "%s (%s bits)" "$(bq "$phrase")" "$(nq $bits)"
            done
            [ "$have_words" ] && menu_printf more "See more passphrases"
        )

        local ans
        my_select ans "Please select a passphrase for the encrypted live-usb" "$menu"
        [ "$ans" != 'more' ] && break
    done

    case $ans in

        first-boot)
            msg $"You will be asked to create a passphrase during the first boot" ;;

        ask)
            msg "Soon you will be asked to enter a new passphrase (3 times)" ;;

        index=*)
            i=${ans#*=}
            eval phrase=\$phrase_$i
            say_using_phrase "$phrase"
            ans=phrase=$phrase ;;

        *) fatal_error select-phrase "$ans" ;;
    esac

    eval $var=\$ans
    press_enter
}


#------------------------------------------------------------------------------
# Make sure the passphrase type entered on the command line is valid
#------------------------------------------------------------------------------
validate_passphrase_type() {
    local arg=$1  val=${1#*=}

    case $arg in
                  "")  ;;
          first-boot)  msg "You will be asked to create a passphrase during the first boot" ;;
                 ask)  msg "You will be asked to create a passphrase" ;;
              random)  test_rand $PP_NUM_WORDS ;;
            random=*)  test_rand "$val"        ;;
              file=*)  test_phrase_file "$val" ;;
                   *)  fatal "Unknown %s parameter %s" "--encrypt" "$(pqw $arg)"
    esac
}

#------------------------------------------------------------------------------
#  Make sure the number of random words requested is sane
#------------------------------------------------------------------------------
test_rand() {
    local num=$1
    case $num in
        [1-9]|1[0-9]|20) ;;
        *) fatal "Only %s through %s random words are allowed in passphrase. Invalid arg: %s" 1 20 "$arg"
    esac
    msg "Will create a random passphrase %s words long" "$(nq $num)"
}

#------------------------------------------------------------------------------
# Test that the passphrase file exists and is reasonable
#------------------------------------------------------------------------------
test_phrase_file() {
    local file=$1
    test -e "$file" || fatal "Cannot find phrase file %s" "$file"
    test -r "$file" || fatal "Cannot read phrase file %s" "$file"
    local lines=$(cat "$file" | wc -l)
    case $lines in
        0)  ;;

        1) warn "Phrase files should not have a trailing new-line character"
           msg  "Will work around this" ;;

        *) fatal "Phrase file can have only one line" ;;
    esac
    msg "Will read passphrase from file %s" "$(pq "$file")"
}

#------------------------------------------------------------------------------
# Set the variable named as the first arg to a random phrase of $len words
#------------------------------------------------------------------------------
set_rand_phrase() {
    local var=$1 len=$2
    local _phrase=$(gen_passphrase $len)
    say_using_phrase "$_phrase"
    press_enter
    eval $var=phrase=\$_phrase
}

#------------------------------------------------------------------------------
# Warn user to remember the new passphrase
#------------------------------------------------------------------------------
say_using_phrase() {
    local phrase=$1

    msg "Will use phrase %s" "$(bq "$phrase")"
    msg "Please carefully write it down in a safe place"
    msg "If you forget this phrase then the live-usb will be useless"
}

#------------------------------------------------------------------------------
# Generate a 2-word passphrase and stick it in $file.  Also put the passphrase
# into the $var variable
#------------------------------------------------------------------------------
first_boot_phrase() {
    local var=$1  file=$2
    local _phrase=$(gen_passphrase 2)
    : ${_phrase:=default-passphrase}
    msg $"Will use initial passphrase %s" "$(pq "$_phrase")"
    msg
    write_phrase_file "$_phrase" "$file"
    eval $var=file=\$file
    FIRST_BOOT_PHRASE=true
    sync
}

#------------------------------------------------------------------------------
# Create a passphrase with $nwords words.  You can also adjust the max word
# length, the dictionary file, and the random source.
#------------------------------------------------------------------------------
gen_passphrase() {
    local nwords=${1:-$PP_NUM_WORDS}  max_len=${2:-$PP_MAX_WORD_LEN}
    local  file=${3:-$PP_WORDS_FILE}      src=${4:-$PP_RAND_SOURCE}

    test -r "$file" || fatal "Could not read words file %s" "$file"

    # pick N words at random
    # then translate new-line to space
    # then convert final space back to new-line
    dict_words "$file" "$max_len" | shuf --repeat --random-source=$src -n$nwords 2>/dev/null \
        | tr '\n' ' ' | sed "s/ $/\n/"
}

#------------------------------------------------------------------------------
# echo the number of words in the dictionary
#------------------------------------------------------------------------------
dict_size() { dict_words "$@" | wc -l ; }

#------------------------------------------------------------------------------
# Do these searches once because they can take half a second or more. Store
# the results in tmpfs.
#------------------------------------------------------------------------------
dict_words() {
    local file=${1:-$PP_WORDS_FILE}  max_len=${2:-$PP_MAX_WORD_LEN}

    test -e $WORDS_FILE || grep -P -v "[^ -~]" "$file" \
        | egrep -v -e ".{$((max_len + 1))}" -e "$SWEARS" -e "'" > $WORDS_FILE

    cat $WORDS_FILE
}

#------------------------------------------------------------------------------
# Pretty much WYSIWYG
#------------------------------------------------------------------------------
overall_mode_menu() {
    encrypt || menu_printf simple  $"Make a full-featured live-usb"
    menu_printf encrypt $"Make an encrypted full-featured live-usb"
    menu_printf default $"Make a customized live-usb (includes encryption option)"
    menu_printf uuids   $"Update UUIDs on an existing live-usb"
    menu_printf cheats  $"Update boot parameters on an existing live-usb"
    menu_printf install $"Reinstall legacy bootloader on an existing live-usb"
}

#------------------------------------------------------------------------------
# Decide what to do.  May need to set CMD and/or QUESTION_MODE
#------------------------------------------------------------------------------
select_overall_mode() {
    local cmd_var=$1 q_var=$2 ans cmd_val q_val
    local title=$"Please select what action to perform"
    local menu=$(overall_mode_menu)
    my_select 'ans' "$title" "$menu"
    case $ans in
                  encrypt) q_val=simple ; ENCRYPT=true     ;;
           default|simple) q_val=$ans                      ;;
            uuids|install) cmd_val=$ans                    ;;
                   cheats) cmd_val=$ans ; : ${CHEATS:=yes} ;;
    esac

    [ ${#cmd_val} -gt 0 ] && eval $cmd_var=\$cmd_val
    [ ${#q_val}   -gt 0 ] && eval $q_var=\$q_val
}

#------------------------------------------------------------------------------
# Wrapper around cli_live_usb_src_menu() from the lib
#------------------------------------------------------------------------------
select_usb_src() {
    local var=$1  exclude=$2
    local title=$"Please select the source for the new live-usb"
    local menu=$(cli_live_usb_src_menu "$exclude")
    my_select "$var" "$title" "$menu"
}

#------------------------------------------------------------------------------
# Simple menu of usb devices minus the running live-usb device
#------------------------------------------------------------------------------
select_target_device() {
    local var=$1  live_dev=$2  from_dev=${3#clone=}

    local menu=$(cli_drive_menu "$live_dev" "$from_dev")
    local dev cnt=$(count_lines "$menu")
    case $cnt in
        0) fatal $"No available target usb devices were found" ;;
        1) dev=$(echo "$menu" | cut -d"$P_IFS" -f1 | head -n1)
            Msg $"Only one target usb device was found %s" "$(echo $(echo "$menu" | cut -d"$P_IFS" -f2))"
           eval $var=\$dev
           return ;;
    esac
    my_select $var $"Please select the target usb device" "$menu"
}

#------------------------------------------------------------------------------
# Work out sizes of the two partitions based on size of device and the user
# parameter cmd_percent.  Allow user to change the size once so we go through
# the loop at most twice.
#------------------------------------------------------------------------------
verify_sizes() {
    local bios_var=$1  main_var=$2  uefi_var=$3  dev=$4  dir=$5
    local cmd_percent=$6  bios_val=${7:-0}  uefi_val=$8  default_size=${10:-100}
    local percent=${cmd_percent:-$default_size}

    # Get total size of target live-usb device
    local total_size=$(parted --script $target_dev unit MiB print 2>/dev/null | sed -rn "s/^Disk.*: ([0-9]+)MiB$/\1/p")

    local main_needed=$(du_ap_size_spec $dir "*")
    local uefi_needed=$(du_ap_size_spec $dir "$UEFI_FILES")
    local bios_needed=$(du_ap_size_spec $dir "$BIOS_FILES")

    if [ $uefi_val -lt $((uefi_needed + UEFI_MARGIN)) ]; then
        local old=$uefi_val
        uefi_val=$((uefi_needed + UEFI_MARGIN))
        warn "uefi size increased from %s to %s MiB" "$(pnh $old)" "$(pnh $uefi_val)"
    fi

    if [ $BIOS_MARGIN -gt 0 -a $bios_val -lt $((bios_needed + BIOS_MARGIN)) ]; then
        local old=$bios_val
        bios_val=$((bios_needed + BIOS_MARGIN))
        warn "bios size increased from %s to %s MiB" "$(pnh $old)" "$(pnh $bios_val)"
    fi

    local did_size
    while true; do
        #--- simple arithmetic ...
        local alloc_val=$((total_size * percent / 100 - 1))
        local main_val=$((alloc_val - uefi_val - bios_val))
        local total_extra=$((total_size - alloc_val))

        local main_needed2=$((main_needed + $(ext_overhead $main_val) ))

        local main_extra=$((main_val - main_needed2 - main_overhead))
        local uefi_extra=$((uefi_val - uefi_needed))
        local bios_extra=$((bios_val - bios_needed))
        local min_percent=$((1 + 100 * (main_needed2 + uefi_val + bios_val + MAIN_MARGIN + UEFI_MARGIN + BIOS_MARGIN)
            / total_size))

        log_it_q echo
        # Table headers for sizes of: entire drive, [bios partition], main partition, uefi partition
        if [ $bios_val -gt 0 ]; then
            log_it_q usb_stats $"entire drive"    "$total_size" "$alloc_val"     "$total_extra"  \
                               $"main partition"  "$main_val"   "$main_needed2"  "$main_extra"   \
                               $"bios partition"  "$bios_val"   "$bios_needed"   "$bios_extra"   \
                               $"uefi partition"  "$uefi_val"   "$uefi_needed"   "$uefi_extra"
        else
            log_it_q usb_stats $"entire drive"    "$total_size" "$alloc_val"     "$total_extra"  \
                               $"main partition"  "$main_val"   "$main_needed2"  "$main_extra"   \
                               $"uefi partition"  "$uefi_val"   "$uefi_needed"   "$uefi_extra"
        fi
        echo >>$LOG_FILE

        check_size main $main_extra $MAIN_MARGIN warn
        check_size uefi $uefi_extra $UEFI_MARGIN warn
        check_size bios $bios_extra $BIOS_MARGIN warn

        eval $bios_var=\$bios_val
        eval $main_var=\$main_val
        eval $uefi_var=\$uefi_val

        [ "$did_size" ] && break
        did_size=true
        if [ $min_percent -ge 90 ]; then
            msg $"Already using over 90% of the device"
            break
        fi

        # Only offer to change if a size was not given on the cmdline
        [ ${#cmd_percent} -gt 0 ] && break
        expert_YES_no $"Do you want to change how much of the device is used?" || break
        select_partition_size percent "$min_percent" "$total_size"

        alloc_val=$((total_size * percent / 100 - 1))
        # Have set the size to <13%> (1,200 MiB)
        msg $"Have set size to %s (%s MiB)" "$(hq $percent%)" "$(nq $(add_commas $alloc_val | color_commas))"
    done

    check_size ext $main_extra $MAIN_MARGIN fatal
    check_size uefi $uefi_extra $UEFI_MARGIN fatal
    check_size bios $bios_extra $BIOS_MARGIN fatal
}

#------------------------------------------------------------------------------
# Rough, empirical forumula for calculating the overhead of the ext4 filesystem
# using our default ext4 settings.
#------------------------------------------------------------------------------
ext_overhead() {
    local size=$1
    local max=$EXT_OVERHEAD_MAX

    local overhead=$(($size $EXT_OVERHEAD_FORM))
    [ $overhead -gt $max ] && overhead=$max
    [ $overhead -lt  0 ] && overhead=0
    echo "Estimated extfs overhead for ${size}M is ${overhead}M" >> $LOG_FILE
    echo $overhead
}

#------------------------------------------------------------------------------
# Wrapper around partition_size_menu() in the lib
#------------------------------------------------------------------------------
select_partition_size() {
    local var=$1  min_size=$2  total_size=$3
    local title1=$"Please select how much space to use for the live-usb partitions"
    local menu=$(partition_size_menu "$min_size" "$total_size")
    my_select $var "$title1" "$menu"
}

#------------------------------------------------------------------------------
# Text menus for language and timezone.  Should we add more??
#------------------------------------------------------------------------------
cheats_menus() {
    local var=$1
    local tz_cheat _cheats
    cli_text_menu _cheats lang "Select default Language for the live-usb" \
        "Language implies timezone and other things"

    local blurb=$(printf "Timezones are listed by longitude. %s denotes daylight savings time" "$(pqq "*")")
    cli_text_menu tz_cheat tz "Select Default Timezone for the live-usb" "$blurb"

    [ "$tz_cheat" ] && _cheats="$_cheats${_cheats:+ }$tz_cheat"
    eval $var=\$_cheats
}

#------------------------------------------------------------------------------
# Guess if the target device is an encrypted live-usb or a normal one
#------------------------------------------------------------------------------
guess_partitioning() {
    local drive=$1
    unset ENCRYPT
    local part1=$(get_partition $drive 1)
    local part2=$(get_partition $drive 2)
    test -b $part1 || fatal "This does not look like a live-usb device (%s)" $part1
    test -b $part2 || fatal "This does not look like a live-usb device (%s)" $part2
    local fs_type=$(lsblk -no fstype $part2)
    case $fs_type in
        crypto_LUKS)  ENCRYPT=true ;;
               vfat)               ;;
               *)  fatal "This does not look like a live-usb device (%s)" $fs_type;;
    esac

    encrypt && shout $"Assuming this is an encrypted live-usb"
    setup_devices $drive
}

#------------------------------------------------------------------------------
# Different actions take place on different partitions depending on if we are
# making an encrypted live-usb or not.
#------------------------------------------------------------------------------
setup_devices() {
    local drive=$1

    if encrypt; then
        UEFI_PART=3

        # Get first three partitions (lexically only)
        BIOS_DEV=$(get_partition $drive 1)
        MAIN_DEV=$(get_partition $drive 2)
        UEFI_DEV=$(get_partition $drive 3)
    else
        UEFI_PART=2

        # Get first two partitions (lexically only)
        MAIN_DEV=$(get_partition $drive 1)
        UEFI_DEV=$(get_partition $drive 2)

        BIOS_SIZE=0
        BIOS_MARGIN=0

        BIOS_DIR=$MAIN_DIR
        BIOS_DEV=$MAIN_DEV
    fi
}

#------------------------------------------------------------------------------
# Show fancy df output of list of mountpoints.  Things that aren't mountpoints
# are silently dropped from the list.  The WORK_DIR is removed from the
# mountpoints shown on the screen.
#------------------------------------------------------------------------------
display_df_output() {
    local mp  real_list
    for mp; do
        is_mountpoint $mp && real_list="$real_list $mp"
    done

    [ -z "$real_list" ] && return

    if [ "$QUIET" ]; then
        (echo; df -PTh $real_list; echo) >> $LOG_FILE
    else
        (echo; df -PTh $real_list; echo) | tee -a $LOG_FILE \
           | sed -r -e "s/^(Filesystem.*)/$m_co\1$nc_co/" -e "s|$WORK_DIR/||"
    fi
}

#------------------------------------------------------------------------------
# Write the passphrase to the passphrase file.  The write_file() routine would
# leave the passphrase in the log file.
#------------------------------------------------------------------------------
write_phrase_file() {
    local phrase=$1  pfile=$2
    local dir=$(dirname $pfile)
    cmd mkdir -p $dir
    [ "$PRETEND_MODE" ] || echo -n "$phrase" > $pfile
}

#------------------------------------------------------------------------------
# Create an encrypted partition using the supplied phrase-file and name
#------------------------------------------------------------------------------
encrypt_partition() {
    local dev=$1  name=$2  phrase=$3
    test -e $dev || fatal "Device to encrypt %s does not exist" $dev
    test -b $dev || fatal "Device to encrypt %s is not a block device" $dev

    msg $"About to encrypt device %s" "$(pq $dev)"

    local val=${phrase#*=}  ret
    if [ -z "$PRETEND_MODE" ]; then
        case $phrase in
                ask) shout
                     shout "We want to overwrite the data on %s" $dev
                     shout "Type in upper-case %s as suggested" "'YES'"
                     cryptsetup luksFormat              $dev ; ret=$?
                     shout "You will be asked for the passphrase a third time" ;;

             file=*) cat "$val"     | cryptsetup luksFormat --key-file - $dev ; ret=$? ;;

           phrase=*) echo -n "$val" | cryptsetup luksFormat --key-file - $dev ; ret=$? ;;

                  *) internal_error phrase-type "$phrase"
        esac

        [ $ret -eq 0 ] || fatal "Failed to create encrypted device"
    fi

    case $phrase in
           ask) cryptsetup open --type luks                   $dev $name; ret=$? ;;
        file=*) cryptsetup open --type luks --key-file "$val" $dev $name; ret=$? ;;

      phrase=*) if YES_no "Do you want to try entering the new passphrase now?"; then
                    cryptsetup open --type luks $dev $name; ret=$?
                else
                    echo -n "$val" | cryptsetup open --type luks --key-file - $dev $name; ret=$?
                fi ;;
    esac

    [ $ret -eq 0 ] || fatal "Failed to open encrypted device"

    LUKS_DEV=/dev/mapper/$name
    msg $"Encryption successful"
}

#------------------------------------------------------------------------------
# Mount a device or know the reason why.  Should go in lib?
#------------------------------------------------------------------------------
mount_device() {
    local dev=$(expand_device "$1")  dir=$2
    [ ${#dev} -gt 0 ]     || fatal "Could not find device %s" "$1"
    mkdir -p "$dir"       || fatal "Failed to create mountpoint directory %s" "$dir"
    is_mountpoint "$dir"  && fatal "Directory '%s' is already a mountpoint"   "$dir"

    is_mounted "$dev"     && fatal "Device %s is already mounted" "$dev"
    [ "$(stat -c %t "$dev")" = "b" ] && msg $"Please wait while we mount optical disc at %s" "$(pq $dev)"
    always_cmd mount -o ro "$dev" "$dir"
    is_mountpoint "$dir"  || fatal "Failed to mount device %s at %s" "$dev"   "$dir"
}

#------------------------------------------------------------------------------
# When cloning a live-usb, we only want to copy certain files and ignore all
# others.  We do this by bind mounting only the files and directories we want
# and then copying that over to the target device. White-list not black-list.
#------------------------------------------------------------------------------
clone_directory() {
    local from_dir=$1 targ_dir=$2
    is_mountpoint "$targ_dir" && fatal "Directory '%s' is already a mountpoint" "$targ_dir"

    test -d "$from_dir"       || fatal "Clone directory '%s' is not a directory"   "$from_dir"
    #is_mountpoint "$from_dir" || warn  "Clone directory '%s' is not a mountpoint"  "$from_dir"

    # mounting it as tmpfs makes cleanup trivial (we are still touching files nad
    # making directories
    mount -t tmpfs tmpfs "$targ_dir"

    from_dir=$(readlink -f "$from_dir")

    # FIXME: should this error message have a device, not a directory?
    local boot_dir
    # No <file-name> file found in any directory on clone device at <root-directory>
    find_live_boot_dir boot_dir "$from_dir" "$LINUXFS_NAME"
    local ret=$?
    case $ret in
        0) ;;
        1) fatal "No '%s' file found in any directory on clone device at '%s'"   "$LINUXFS_NAME" "$from_dir" ;;
        2) fatal "No live-boot directory found on clone device at '%s'"          "$from_dir" ;;
        3) fatal "Multiple live-boot directories found on clone device at '%s'"  "$from_dir" ;;
        *) internal_error "find_live_boot_dir" $ret ;;
    esac

    msg "Live boot directory %s" "$(pq $boot_dir)"
    ISO_BOOT_DIR=$boot_dir

    # Prepare to get all files/dirs from under the boot-directory on a frugal install
    local full_boot_dir="$from_dir/${boot_dir#/}"
    local src_root_dir=$from_dir

    if [ -z "${boot_dir##*Frugal*}" ]; then
        msg "Cloning from a frugal directory"
        src_root_dir=$full_boot_dir
    fi

    # NOTE:
    # the cd && eval "ls ..." below allows us to use globbing expansions with spaces in paths

    # Bind mount directories
    local dir from dest
    while read dir; do
        [ ${#dir} -gt 0 ] || continue
        from="$src_root_dir/$dir"
        dest="$targ_dir/$dir"
        test -d "$from" || continue
        mkdir -p "$dest" || fatal "Could not create directory %s" "$dest"
        mount --bind "$from" "$dest"
    done <<Clone_Dirs
$(cd "$src_root_dir" && eval "ls -d $CLONE_DIRS" 2>/dev/null)
Clone_Dirs

    # Bind mount top level files
    local file
    while read file; do
        [ ${#file} -gt 0 ] || continue
        from="$src_root_dir/$file"
        dest="$targ_dir/$file"
        test -f "$from" || continue
        mkdir -p "$(dirname "$dest")" || fatal "Could not mkdir %s" "$(dirname "$dest")"
        touch "$dest"                 || fatal "Could not touch file %s" "$dest"
        mount --bind "$from" "$dest"
    done <<Clone_Files
$(cd "$src_root_dir" && eval "ls -d $CLONE_FILES" 2>/dev/null)
Clone_Files

    # Bind mount boot-dir files
    while read file; do
        [ ${#file} -gt 0 ] || continue
        from="$full_boot_dir/$file"
        dest="$targ_dir/$DEF_BOOT_DIR/$file"

        test -f "$from" || continue
        mkdir -p "$(dirname "$dest")" || fatal "Could not mkdir %s" "$(dirname "$dest")"
        touch "$dest"                 || fatal "Could not touch file %s" "$dest"
        mount --bind "$from" "$dest"
    done <<Clone_Bdir_Files
$(cd "$full_boot_dir" && eval "ls -d $CLONE_BDIR_FILES" 2>/dev/null)
Clone_Bdir_Files
}

#===== "REAL" WORK STARTS HERE ================================================


#------------------------------------------------------------------------------
# Clear out previous partition tables.
#------------------------------------------------------------------------------
clear_partition() {
    local dev=$1
    local total=$(parted --script $dev unit KiB print 2>/dev/null | sed -rn "s/^Disk.*: ([0-9]+)kiB$/\1/ip")

    # Clear out previous primary partition table
    cmd dd status=none if=/dev/zero of=$dev bs=1K count=17

    # Clear out sneaky iso-hybrid partition table
    cmd dd status=none if=/dev/zero of=$dev bs=1K count=17 seek=32

    [ -n "$total" ] || return
    local offset=$((total - 17))

    # Clear out secondary gpt partition table
    cmd dd status=none conv=notrunc if=/dev/zero of=$dev bs=1K count=17 seek=$offset

    # Tell kernel the partition table has changed
    cmd partprobe $dev
}

#------------------------------------------------------------------------------
# Partition the target usb device.  Will make either 3 or 2 partitions
# depending on whether encryption is used as signaled by the zero size of the
# bios partition.
#------------------------------------------------------------------------------
do_partition() {
    local drive=$1  type=${2:-msdos}  bios_size=${3:-0}  main_size=$4  uefi_size=$5
    local alloc_size=$((bios_size + main_size + uefi_size))

    local boot_flag
    case $type in
          gpt) boot_flag=legacy_boot ;;
        msdos) boot_flag=boot        ;;
            *) fatal "Unknown partitioning scheme: %s.  Expected msdos or gpt" "$type"
               ;;
    esac

    # Using <gpt|msdos> partitioning
    msg $"Using %s partitioning" $(pq $type)
    local err_msg="Partitioning failed at %s"
    local preamb="parted --script --align optimal $target_dev unit MiB"
    cmd $preamb mklabel $type || fatal "$err_msg" start

    local main_start=1  main_end=$main_size  main_fs=ext4  uefi_part=2
    if [ $bios_size -gt 0 ]; then

        clear_filesystem $drive 1
        cmd $preamb mkpart primary ext4 1 $bios_size || fatal "$err_msg" bios

        main_start=$((bios_size))
        main_end=$((main_size + bios_size))
        uefi_part=3
        main_fs=
    fi

    clear_filesystem $drive $main_start
    cmd $preamb mkpart primary $main_fs $main_start $main_end  || fatal "$err_msg" main
    clear_filesystem $drive $main_end
    cmd $preamb mkpart primary fat32 $main_end $alloc_size     || fatal "$err_msg" uefi
    cmd $preamb set 1 $boot_flag on set $uefi_part esp on      || fatal "$err_msg" flags

    if [ "$type" = 'gpt' -a "$DO_PMBR" ]; then
        cmd $preamb disk_set pmbr_boot on || fatal "$err_msg" disk_set
    fi

    # Tell kernel the partition table has changed
    cmd partprobe $dev

    DID_PARTITION=true
}


#------------------------------------------------------------------------------
# Zero out the first 128K of each filesystem before we created its partition
# table entry
#------------------------------------------------------------------------------
clear_filesystem() {
    local drive=$1  offset_m=$2  size=128
    local offset_k=$((offset_m * 1024))
    cmd dd status=none if=/dev/zero of=$drive seek=$offset_k bs=1024 count=128
    sync
}

#------------------------------------------------------------------------------
# There were some problems early in testing when the nearly partitioned devs
# seemed to appear and then disappear and then reappear.  This seems to have
# fixed them.
#------------------------------------------------------------------------------
wait_for_file() {
    local file=$1  name=${2:-Device}  delay=${3:-.05}  total=${4:-40}
    [ -z "$file" ] && return

    local i  cnt=0  max=3
    for i in $(seq 1 $total); do
        sleep $delay
        test -e $file || cnt=0
        cnt=$((cnt + 1))
        test $cnt -ge $max && return 0
    done
    fatal "%s does not exist!" "$name $file"
}

#------------------------------------------------------------------------------
# Make the ext4 file system
#------------------------------------------------------------------------------
do_makefs_ext() {
    local dev=$1  options=$2  label=$3  force_ext

    force makefs && force_ext="-F"
    cmd mkfs.ext4 $force_ext $EXT4_NO_64BIT $options $dev \
        || fatal 'makefs' "Could not make %s file system on %s.  Perhaps try %s" 'ext4' $dev "--force=makefs"

    [ -n "$label" ] && cmd tune2fs -L "${label:0:16}" $dev
}

#------------------------------------------------------------------------------
# Make the fat32 file system
#------------------------------------------------------------------------------
do_makefs_uefi() {
    local uefi_dev=$1  label=${2:-live-esp}
    cmd mkfs.vfat -n "${label:0:11}" $uefi_dev \
        || fatal "Could not make %s file system on %s" "fat32" "$uefi_dev"
}

#------------------------------------------------------------------------------
# Copy from mounted source (or the clone) to the ext4 partition
#------------------------------------------------------------------------------
do_copy_main() {
    local from=$1  to=$2  ;  shift 2

    if false && test -d $to/antiX && ! force copy; then
        pwarn "Not over-writing %s partition due to existing %s directory" main antiX/
        pwarn "Use %s to overwrite" "--force=copy"
        return
    fi

    msg $"copy from %s to %s partition" "$(pq $(basename $from))" "$(pq $(basename $to))"
    msg $"files: %s" "$(pq "*")"

    local err_msg=$"Error while copying files to main partition of live usb"

    if [ $# -gt 0 ]; then
        copy_with_progress "$from" "$to" "$err_msg" "$@"

    elif q_mode gui || [ "$NO_PROGRESS_BAR" ]; then
        cmd cp -a $from/* $to/ || fatal "$err_msg"

    elif [ "$PERCENT_PROG" ]; then
        copy_with_progress "$from" "$to" "$err_msg" percent_progress

    else
        local PROGRESS_SCALE=1000
        copy_with_progress "$from" "$to" "$err_msg" text_progress_bar
    fi

    sync ; sync
    pause copy
}

#------------------------------------------------------------------------------
# This allows us to put glob wildcards inside of variable to make the program
# easier to configure.
#------------------------------------------------------------------------------
copy_files_spec() {
    local src=$1  spec=$2  targ=$3  type=${4:-unknown}
    local file from dir

    test -z "$spec" && fatal "Missing list of files to copy to %s" "$(pqw $targ)"

    msg $"copy from %s to %s partition" "$(pq $(basename $src))" "$(pq $(basename $targ))"
    msg $"files: %s" "$(pq $spec)"

    while read file; do
        test -z "$file" && continue
        dir=$targ/$(dirname $file)
        test -d "$dir" || cmd mkdir -p "$dir"
        cmd cp -r "$src/$file" "$dir"  || warn $"Error while copying %s files" "$(pqw $type)"
    done <<Copy_Files_Spec
$(cd $src && eval "ls -d $spec" 2>/dev/null)
Copy_Files_Spec
}

#------------------------------------------------------------------------------
# Do these step early so we can error out early if there is a problem.
#------------------------------------------------------------------------------
start_initrd_encryption() {
    local from_file=$1  initrd_dir=${2:-$INITRD_DIR}  linux_dir=$3

    test -r "$from_file" || fatal "Could not find initrd file %s" "$(pqw $from_file)"
    unpack-initrd -f "$from_file" -d $initrd_dir || fatal $"An error occurred unpacking the initrd"

    #grep -q cryptsetup $initrd_dir/init || fatal $"This live initrd cannot do encryption"

    verbose_cmd copy-initrd-programs --no-color --from=$linux_dir --to=$initrd_dir --clean
    verbose_cmd copy-initrd-programs --no-color --from=$linux_dir --to=$initrd_dir --encrypt \
        || fatal $"Was unable to add all required programs to the live initrd"

    local kernel mod_dir=$initrd_dir/lib/modules
    for kernel in $(ls $mod_dir); do

        test -d $mod_dir/$kernel/kernel               || continue
        test -d $linux_dir/lib/modules/$kernel/kernel || continue

        msg "add %s modules to initrd" "$(pq $kernel encryption)"
        cmd copy-initrd-modules --quiet --only-encrypt --from=$linux_dir --to=$initrd_dir \
            || fatal "Kernel modules were not copied.  Do you need to update copy-initrd-modules?"

    done
    local encrypt_file=$initrd_dir/etc/encrypt
    mkdir -p $(dirname $encrypt_file)
    echo "$ENCRYPT_ENABLE" > $encrypt_file
}

#------------------------------------------------------------------------------
# We do this later so we don't get over-written (is this true?)
#------------------------------------------------------------------------------
finish_initrd_encryption() {
    local initrd_dir=${1:-$INITRD_DIR}  to_file=$2

    [ "$PRETEND" ] && return

    unpack-initrd -f $to_file -d $initrd_dir --repack || fatal "An error occurred repacking the initrd"
    local md5_sum=$(cd $(dirname $to_file) && md5sum $(basename $to_file))
    cmd write_file "$to_file".md5 "$md5_sum"
}

#------------------------------------------------------------------------------
# Add or removed /etc/encrypt inside initrd  NO LONGER USED
# (although it might be safer for non-encrypted live-usbs
#------------------------------------------------------------------------------
enable_disable_initrd_encryption() {
    local from_file=${1:-$2}  to_file=$2  initrd_dir=${3:-$INITRD_DIR}

    test -r "$from_file" || fatal "Could not find initrd file %s" "$(pqw $from_file)"
    unpack-initrd -f "$from_file" -d $initrd_dir || fatal "An error occurred unpacking the initrd"


    local encrypt_file=$initrd_dir/etc/encrypt
    if encrypt; then
        test -e $initrd_dir/bin/cryptsetup  || fatal "The %s program not found in the initrd" cryptsetup
        grep -q cryptsetup $initrd_dir/init || fatal "This initrd cannot do encryption"

        msg $"Enable encryption in the live initrd"
        echo "$ENCRYPT_ENABLE" > $encrypt_file
    else
        msg $"Disable encryption in the live initrd"
        rm -f $encrypt_file
    fi

    pause initrd

    cmd unpack-initrd -f $to_file -d $initrd_dir --repack || fatal "An error occurred repacking the initrd"
    local md5_sum=$(cd $(dirname $to_file) && md5sum $(basename $to_file))
    cmd write_file "$to_file".md5 "$md5_sum"
}


#------------------------------------------------------------------------------
# Find a binary program on a mounted file system such as a squashfs.
#------------------------------------------------------------------------------
find_bin() {
    local dir=$1 name=$2 path=${3:-$PATH}
    local p
    for p in ${path//:/ }; do
        test -e $dir$p/$name || continue
        echo $p/$name
        return 0
    done
    return 1
}

#------------------------------------------------------------------------------
# Kludge to get uefi memtest to work.  Strange but true.  Hope there is a fix
# soon.
#------------------------------------------------------------------------------
fix_uefi_memtest() {
    local dir=$1/EFI/BOOT
    test -d "$dir" || dir=$1/efi/boot
    test -d "$dir" || return
    msg "Fix %sbug" 'uefi memtest'
    local fallback=$dir/fallback.efi
    local grubx64=$dir/grubx64.efi

    test -e $grubx64 || return

    # the fallback file needs to be the last one added to the directory.  But
    # erasing it and adding it back doesn't work so we make a new directory
    # without it and then remove the old one, rename then new one and then do
    # the copy.  *** sigh ***

    if test -e $fallback; then
        cmd rm -f $fallback
        local flag
        [ "$PRETEND_MODE" ] && flag="--dry-run"
        local temp=$(mktemp -p $(dirname $dir) -d $flag)
        cmd cp $dir/* $temp/
        cmd rm -r $dir
        cmd mv $temp $dir
    fi

    cmd cp $grubx64 $fallback
}

#------------------------------------------------------------------------------
# Use the gfxsave script to add chaats to the gfxboot/syslinux bootladers
#------------------------------------------------------------------------------
do_cheats_syslinux() {
    #warn "Cheats are still being worked on"; return

    local bios_dir=$1  cheats=$2
    local syslinux_cfg=$bios_dir/boot/syslinux/syslinux.cfg

    require cheats-syslinux gfxsave || return

    local file params
    local verbose=1
    [ "$BE_VERBOSE" ] && verbose=6
    if test -w $syslinux_cfg; then
        params=$(sed -nr "1,/^\s*APPEND\s/s/^\s*APPEND\s+//p" $syslinux_cfg)
        Msg "syslinux params: %s" "$(pq $params $cheats)"
        VERBOSE=$verbose CMDLINE="$params $cheats" cmd gfxsave $bios_dir/boot both
    else
        pwarn "Could not find '%s' file to update" "$(basename $syslinux_cfg)"
    fi
}

#------------------------------------------------------------------------------
#  Use the grub2-save script to add cheats to the UEFI grub2 bootloader
#------------------------------------------------------------------------------
do_cheats_grub() {
    #warn "Cheats are still being worked on"; return
    local uefi_dir=$1  cheats=$2
    local grub_cfg=$uefi_dir/boot/grub/grub.cfg

    require cheats-grub vmlinuz-version grub2-save || return

    local params

    if test -w $grub_cfg; then
        params=$(sed -nr "1,/^\s*linux\s/s/^\s*linux\s+[^ ]+//p" $grub_cfg)
        Msg "grub params: %s" "$(pq $params $cheats)"
        cmd grub2-save $uefi_dir --no-kernel --cheats="${params% } $cheats"
    else
        # Could not find '<grub.cfg>' file to update
        pwarn "Could not find '%s' file to update" $(basename $grub_cfg)
    fi
}

#------------------------------------------------------------------------------
# Here is a tricky part.  The grub2.cfg must know the UUID of the main system
# and the main system must know the UUID of the uefi partition in order for the
# user to be amble to save their  boot parameter selections.
#------------------------------------------------------------------------------
do_uuids() {
    local bios_dir=$1  bios_uuid=$2  grub_cfg=$3  uefi_uuid=$4  uefi_part=$5
    if [ ${#uefi_uuid} -gt 0 ]; then
        local uefi_file=$bios_dir/antiX/esp-uuid
        cmd mkdir -p $(dirname $uefi_file) || fatal "Making directory '%s' failed" "$(dirname $uefi_file)"
        cmd write_file $uefi_file $uefi_uuid
    else
        warn "No %s given for %s partition" UUID uefi
    fi

    if [ ${#bios_uuid} -eq 0 ]; then
        warn "No %s given for %s partition" UUID ext4
        return
    fi

    if ! test -e $grub_cfg; then
        # Could not find <grub.cfg> file
        pwarn "Could not find %s file" "$(basename $grub_cfg)"
        return
    fi

    local new_line="search --no-floppy --set=root --fs-uuid $bios_uuid"
    if grep -q "search.*--set=root.*" $grub_cfg; then
        # Replace the line(s) if it/they exists
        cmd sed -i "/search.*--set=root.*/  s/.*/$new_line/" $grub_cfg \
            || fatal "sed on %s failed" "$(basename $grub_cfg)"
    else
        # Add the new line before the first menuentry line if not
        cmd sed -ri "1,/^\s*menuentry/s/(^\s*menuentry)/$new_line\n\n\1/" $grub_cfg \
            || fatal "sed on %s failed" "$(basename $grub_cfg)"
    fi

    # Remove #--esp comment to point memtest back to fat32 partition
    cmd sed -ri "s/^#-+esp\s*//" $grub_cfg \
        || fatal "sed on %s failed" "$(basename $grub_cfg)"

    cmd sed -ri "s/(root=\(hd0,)[0-9]\)/\1$uefi_part)/" $grub_cfg \
        || fatal "sed on %s failed" "$(basename $grub_cfg)"

}

#------------------------------------------------------------------------------
# This installs the legacy syslinux bootloader
#------------------------------------------------------------------------------
do_install_bootloader() {
    local target_dev=$1  bios_dir=$2  type=$3

    msg "extlinux version %s" "$(pq $(get_extlinux_version))"

    local fname
    case $type in
          gpt) fname=gptmbr.bin ;;
        msdos) fname=mbr.bin    ;;
            *) fatal "Unknown partitioning scheme: %s.  Expected msdos or gpt" "$type"
               ;;
    esac

    local dir file d
    for dir in /usr/share/syslinux /usr/lib/syslinux/mbr; do
        test -e $dir/$fname || continue
        file=$dir/$fname
        break
    done

    [ "$file" ] || fatal "Could not find file %s" "$fname"
    cmd dd status=none bs=440 conv=notrunc count=1 if=$file of=$target_dev  || fatal "%s command failed" dd

    local sdir idir syslinux_dir isolinux_dir
    for sdir in boot/syslinux syslinux; do
        test -d $bios_dir/$sdir || continue
        syslinux_dir=$bios_dir/$sdir
        break
    done
    if [ -z "$syslinux_dir" ]; then
        for idir in boot/isolinux isolinux; do
            test -d $bios_dir/$idir || continue

            # Create syslinux directory from <boot/isolinux>
            warn $"Create syslinux directory from %s" $idir
            syslinux_dir=$bios_dir/${idir%isolinux}syslinux
            cmd cp -r $bios_dir/$idir $syslinux_dir || fatal "Could not copy the isolinux directory"
            local f
            for f in $(cd $syslinux_dir && ls isolinux.*); do
                test -e $bios_dir/$sdir/$f || continue
                cmd mv $bios_dir/$sdir/$f $bios_dir/$sdir/syslinux${f#isolinux}
            done
            break
        done
    fi

    [ "$KEEP_SYSLINUX" ] || update_syslinux $bios_dir/$sdir "$SYSLINUX_FILES" "$SYSLINUX_RM"

    [ "$PRETEND_MODE" ] && return

     # [don't translate syslinux or isolinux]
    [ -z "$syslinux_dir" ] && fatal "Could not find a syslinux or isolinux directory"

    cmd extlinux -i $syslinux_dir || fatal "%s command failed" extlinux
}

update_syslinux() {
    local to_dir=$1  xfer_files=$2  rm_files=$3
    local fname=gfxboot.c32  file  pre  post
    for pre in /usr/share/syslinux /usr/lib/syslinux; do
        for post in "/" /modules/bios/; do
            test -e $pre$post$fname || continue
            file=$pre$post$fname
            break
        done
    done
    [ "$file" ] || fatal "Could not find file %s" "$fname"
    local from_dir=$(dirname $file)

    if test -e $to_dir/ldlinux.sys && which chattr &>/dev/null; then
        cmd chattr -i $to_dir/ldlinux.sys
    fi

    while read fname; do
        test -e $to_dir/$fname && cmd rm -f $to_dir/$fname
    done<<Syslinux_Rm
$(cd $to_dir && ls -d $rm_files 2>/dev/null)
Syslinux_Rm

    for fname in $xfer_files; do
        test -e $from_dir/$fname && cmd cp $from_dir/$fname $to_dir/
    done

    cmd write_file $to_dir/version $(get_extlinux_version)
}

#------------------------------------------------------------------------------
# Create a small made-by-live-usb-maker file on the live-usb
#------------------------------------------------------------------------------
do_made_by() {
    local dir=$1  file=${2:-$MADE_BY_FILE}
    always_cmd write_file $dir/$file "  created: $(date)
  program: $(basename $0)
  version: $VERSION ($VERSION_DATE)"
}

#------------------------------------------------------------------------------
# Create a random seed file for the first boot.  This is not safe locally, of
# course, but should be good for possible internet attacks.
#------------------------------------------------------------------------------
write_random_seed() {
    local file=$1
    cmd dd status=none if=/dev/urandom of=$file bs=1 count=512
    cmd chmod 600 $file
}

#===== Here come the utilities! ===============================================

#------------------------------------------------------------------------------
# Get the version number of the extlinux program we are using
#------------------------------------------------------------------------------
get_extlinux_version() {
    extlinux -v 2>&1 | sed -r "s/^[a-z]+\s+([0-9.]+).*/\1/g"
}

#------------------------------------------------------------------------------
# Make sure --target device is valid.
#------------------------------------------------------------------------------
check_target() {
    local target=$1  live_dev=$2

    # Make sure our target is a real device
    local target_dev=$(expand_device $target)

    [ ${#target_dev} -gt 0 ] || fatal "Could not find device %s" "$target"
    [ ${#live_dev}   -gt 0 ] || return
    local live_drive=$(get_drive ${live_dev##*/} )
    local targ_drive=$(get_drive ${target##*/}   )
    [ "$live_drive" = "$targ_drive" ] && fatal "Target '%s' cannot be on the live device '%s'" "$target" "$live_drive"
}

#------------------------------------------------------------------------------
# Convenience routine to save a little typing
#------------------------------------------------------------------------------
encrypt() {  [ "$ENCRYPT" ]; return $?; }

#------------------------------------------------------------------------------
# Make sure we have enough left over space on a partition.  Can generate
# warnings or fatal errors depending on $action.
#------------------------------------------------------------------------------
check_size() {
    local type=$1  extra=$2  margin=$3  action=${4:-fatal}

    [ $margin -eq 0 ] && return

    [ $extra -lt 0 ]       && $action $"Not enough space on %s partition" "$type"
    [ $extra -lt $margin ] && $action $"Less than %s MiB would remain on %s partition" "$margin" "$type"
}

#------------------------------------------------------------------------------
# Tell user we are done and then exit
#------------------------------------------------------------------------------
exit_done() {
    say_done
    my_exit
}

#------------------------------------------------------------------------------
# Tell user that we're done
#------------------------------------------------------------------------------
say_done() {
    msg "$(bq "=>") %s" $"done"
}

#------------------------------------------------------------------------------
# A catch-all for things to be done right before exiting
#------------------------------------------------------------------------------
my_exit() {
    local ret=${1:-0}

    show_elapsed

    # Pause at '<Exit>'
    pause exit $"Exit"

    # Msg "=> cleaning up"
    exit $ret
}

#------------------------------------------------------------------------------
# Start logging by appending a simple header
#------------------------------------------------------------------------------
start_log() {
    local args=$1 cmds=${2# }

    LOG_FILE=$THE_LOG_FILE

    cat <<Start_Log >> $LOG_FILE
---------------------------------------------------------------------
$0
        started: $(date)
        version: $VERSION ($VERSION_DATE)
    comand line: $args
      found lib: $FOUND_LIB
     TEXTDOMAIN: $TEXTDOMAIN
  TEXTDOMAINDIR: $TEXTDOMAINDIR

Start_Log
}

#------------------------------------------------------------------------------
# This may be over-kill.  Seems reliable though ...
#------------------------------------------------------------------------------
umount_work_dir() {
    local dir=${1:-$WORK_DIR}
    [ -n "$dir" ] || return

    sync; sync
    is_mountpoint "$dir" || return 0

    local try
    for try in $(seq 1 30); do
        umount --recursive "$dir"  2>/dev/null
        is_mountpoint "$dir" || return 0
        sleep .1
    done
    rmdir "$dir"
}

#------------------------------------------------------------------------------
# Write a config file using the current variables so users can easily put
# command line parameters into the config file.
#------------------------------------------------------------------------------
write_config() {
    local file=${1:-$CONFIG_FILE}
    local dir=$(dirname "$file")
    mkdir -p "$dir" || fatal "Could not make config file directory %s" "$dir"
    msg "Writing config file %s" "$(pq "$file")"

    cat<<Config_File >"$file"
$(config_header "$file" "$ME" "$VERSION" "$VERSION_DATE")

     ISO_FILE_DIR="$ISO_FILE_DIR"
    ISO_FILE_SPEC="$ISO_FILE_SPEC"
      SEARCH_DIRS="$SEARCH_DIRS"
     SEARCH_DEPTH="$SEARCH_DEPTH"
        MAX_FILES="$MAX_FILES"
     MIN_ISO_SIZE="$MIN_ISO_SIZE"

        MSDOS_GPT="$MSDOS_GPT"
     DEFAULT_SIZE="$DEFAULT_SIZE"
         CMD_SIZE="$CMD_SIZE"
        BIOS_SIZE="$BIOS_SIZE"
        UEFI_SIZE="$UEFI_SIZE"
      BIOS_MARGIN="$BIOS_MARGIN"
      MAIN_MARGIN="$MAIN_MARGIN"
      UEFI_MARGIN="$UEFI_MARGIN"

     EXT4_OPTIONS="$EXT4_OPTIONS"
       BIOS_LABEL="$BIOS_LABEL"
        ESP_LABEL="$ESP_LABEL"

          LIVE_MP="$LIVE_MP"
     LINUXFS_NAME="$LINUXFS_NAME"
 MIN_LINUXFS_SIZE="$MIN_LINUXFS_SIZE"
     DEF_BOOT_DIR="$DEF_BOOT_DIR"

       CLONE_DIRS="$CLONE_DIRS"
      CLONE_FILES="$CLONE_FILES"
 CLONE_BDIR_FILES="$CLONE_BDIR_FILES"

       UEFI_FILES="$UEFI_FILES"
       BIOS_FILES="$BIOS_FILES"

        GRUB_CONF="$GRUB_CONF"
           CHEATS=""
     COLOR_SCHEME="$COLOR_SCHEME"
    QUESTION_MODE="$QUESTION_MODE"

          ENCRYPT="$ENCRYPT"
 PASS_PHRASE_TYPE="$PASS_PHRASE_TYPE"
        LUKS_NAME="$LUKS_NAME"

EXT_OVERHEAD_FORM="$EXT_OVERHEAD_FORM"
 EXT_OVERHEAD_MAX="$EXT_OVERHEAD_MAX"

  PP_MAX_WORD_LEN="$PP_MAX_WORD_LEN"
     PP_NUM_WORDS="$PP_NUM_WORDS"
    PP_WORDS_FILE="$PP_WORDS_FILE"
   PP_RAND_SOURCE="$PP_RAND_SOURCE"

  GRAPHICAL_MENUS="$GRAPHICAL_MENUS"
  AUTOMOUNT_DELAY="$AUTOMOUNT_DELAY"

$(config_footer)
Config_File

    return 0
}

#------------------------------------------------------------------------------
# This routine is trapped on the EXIT signal.  So we always do this stuff.
# It should *not* be interactive.
#------------------------------------------------------------------------------
clean_up() {

    lib_clean_up

    # Kill the children
    pkill -P $$

    test -d $LINUX_DIR && is_mountpoint $LINUX_DIR && umount $LINUX_DIR

    # Try to umount everything
    umount_work_dir

    rm -f $WORK_DIR/pid
    test -d $WORK_DIR && rmdir $WORK_DIR

    luks_close $LUKS_NAME

    unflock
}

#------------------------------------------------------------------------------
# Load the lib either from a neighboring repo or from the standard location.
#------------------------------------------------------------------------------
load_lib() {
    local file=$1  path=$2
    unset FOUND_LIB

    local dir lib found IFS=:
    for dir in $path; do
        lib=$dir/$file
        test -r $lib || continue
        if ! . $lib; then
            printf "Error when loading library %s\n" "$lib" >&2
            printf "This is a fatal error\n" >&2
            exit 15
        fi
        FOUND_LIB=$lib
        return 0
    done

    printf "Could not find library '%s' on path '%s'\n" "$file" "$path" >&2
    printf "This is a fatal error\n" >&2
    exit 17
}

#===== Start Here =============================================================

load_lib "$SHELL_LIB" "$LIB_PATH"

set_colors

main "$@"
#!/bin/bash

#==============================================================================
# live-kernel-updater
# A robust program to change kernels on antiX/MX live systems
#
# (C) 2016 -- 2017 Paul Banham <antiX@operamail.com>
# License: GPLv3 or later
#==============================================================================

      VERSION="2.00.08"
 VERSION_DATE="Tue Aug  8 11:34:20 MDT 2017"

        ME=${0##*/}
    MY_DIR=$(dirname "$(readlink -f $0)")
MY_LIB_DIR=$(readlink -f "$MY_DIR/../cli-shell-utils")
   LIB_DIR="/usr/local/lib/cli-shell-utils"

export TEXTDOMAIN="cli-shell-utils"
domain_dir="$MY_DIR/../cli-shell-utils/locale"
test -d "$domain_dir" && export TEXTDOMAINDIR=$domain_dir

#== BEGIN_CONFIG
         MAX_FILES="20"
           LIVE_MP="/live/boot-dev"
          LIVE_DIR="antiX"
          BOOT_DIR="boot"

   VM_VERSION_PROG="vmlinuz-version"
      LINUXFS_NAME="linuxfs"
      VMLINUZ_NAME="vmlinuz"
    VMLINUZ_2_NAME="vmlinuz1"
       INITRD_NAME="initrd.gz"
   TEMPLATE_INITRD="/usr/lib/iso-template/template-initrd.gz"
 TEMPLATE_INITRD_2="/usr/lib/iso-template/initrd.gz"

  MIN_LINUXFS_SIZE="100M"

          # for kernel and initrd.gz files
          KOLD_EXT=".kold"
          KBAD_EXT=".kbad"
          IOLD_EXT=".iold"
          IBAD_EXT=".ibad"
           MD5_EXT=".md5"

          # For linuxfs file (right after a remaster)
           NEW_EXT=".new"

          BZ2_OPTS="-c -9"
           GZ_OPTS="-c -9"
           LZ_OPTS="-c -9"
           XZ_OPTS="-c -z --format=lzma"

         CPIO_OPTS="-o -H newc --owner root:root --quiet"
         SHELL_LIB="cli-shell-utils.bash"

         LUKS_NAME="live-kernel-updater"
       CRYPT_FNAME="crypt"

      COLOR_SCHEME="high"
   GRAPHICAL_MENUS="true"
#== END_CONFIG

        WORK_DIR="/run/$ME"
            FIFO="/run/$ME/fifo"
    THE_LOG_FILE="/var/log/$ME.log"
    THE_ERR_FILE="/var/log/$ME.error"
        LOG_FILE="/dev/null"
        ERR_FILE="/dev/null"
     CONFIG_FILE="/root/.config/$ME/$ME.conf"

        ALL_CMDS="rollback all unpack copy-modules repack install"
       ALL_FORCE="all,flock,clear,compress,usb,umount"
       ALL_PAUSE="all,fifo,mount,unpack,copy,repack,clean,exit"

        LIB_PATH="$MY_LIB_DIR:$LIB_DIR"
            PATH="$MY_LIB_DIR/bin:$LIB_DIR/bin:$PATH"


#------------------------------------------------------------------------------
# Show usage and exit
#------------------------------------------------------------------------------
usage() {
    local ret=${1:-0}

cat<<Usage
Usage: $ME [options] [command]

Update the kernel on a running antiX/MX live-usb or on an antiX/MX live-usb
that is plugged into another system.  The new kernel must already be installed.
You will be prompted for information that is needed but was not given in the
command line arguments.

Commands:
   all         All commands below
   unpack      Unpack the old initrd
   copy        Copy kernel modules into initrd
   repack      Repack the new initrd
   install     Copy new initrd and vmlinuz to the live boot directory

Options:
  -a --auto             Non-interactive.  Always assume the safe answer
  -C --color=<xxx>      Set color scheme to off|low|low1|bw|dark|high
  -d --device=<device>  live-usb device to update the kernel on
                        (use "live" to force updating a running live system)
  -F --force=XXXX       Force the options specfied:
                             flock:  ignore missing flock program
                               usb:  Allow non-usb devices (dangerous!)
                             clear:  remove previous initrd directory
  -G --graphic-ui       Use the new graphics user interface (default)
  -h --help             Show this usage
  -i --initrd           Only update the initrd using the file:
                            $TEMPLATE_INITRD
                        If that file is not found, use:
                            $TEMPLATE_INITRD_2
     --initrd=<file>    Only update the initrd using file <file>
                        leading / then treated as full path to alternate initrd
  -I --ignore-config    Ignore the configuration file
  -k --kernel=<kernel>  The version (uname -r) of the new kernel
  -K --keep-old         Keep the old module directory in the initrd
     --pause            Wait for user input before existing
  -m --modules=<list>   Only add listed modules to the existing initrd
  -N --numeric-ui       Use the legacy numerical user interface
  -p --pretend          Don't actually install the new kernel or initrd.gz
  -q --quiet            Print less
  -R --reset-config     Write fresh config file with default options
  -v --version          Show version information
  -V --verbose          Print more, show output of commands
  -VV --very-verbose    Also show the commands
  -W --write-config     Write/update config file preserving current options

Notes:
  - short options stack. Example: -pq instead of --pretend --quiet
  - options can be intermingled with commands and parameters
  - config file: $CONFIG_FILE
  - the config file will be sourced if it exists
  - it will be created if it doesn't exist
Usage
    exit $ret
}

#------------------------------------------------------------------------------
# Callback routine to evaluate arguments after the root user check.  We also
# need to include the early args to avoid unknown argument errors.
#------------------------------------------------------------------------------
eval_argument() {
    local arg=$1 val=$2
    case $arg in
              -auto|a)  AUTO_MODE=true                  ;;
             -color|C)  COLOR_SCHEME=$val               ;;
             -color=*)  COLOR_SCHEME=$val               ;;
            -device|d)  DEVICE=$val                     ;;
            -device=*)  DEVICE=$val                     ;;
             -force|F)  FORCE="$FORCE${FORCE:+,}$val"   ;;
             -force=*)  FORCE="$FORCE${FORCE:+,}$val"   ;;
#               -fifo)  FIFO_MODE=true                  ;;
       --graphic-ui|G)  GRAPHICAL_MENUS=true            ;;
              -help|h)  usage                           ;;
            -initrd|i)  CMD_INITRD=$TEMPLATE_INITRD     ;;
            -initrd=*)  CMD_INITRD=${arg#*=}            ;;
            -kernel|k)  NEW_KERNEL=$val                 ;;
            -kernel=*)  NEW_KERNEL=$val                 ;;
          -keep-old|K)  KEEP_OLD=true                   ;;
           -modules|m)  MODS="$MODS${MODS:+,}$val"      ;;
           -modules=*)  MODS="$MODS${MODS:+,}$val"      ;;
       --numeric-ui|N)  GRAPHICAL_MENUS=                ;;
           -pretend|p)  PRETEND_MODE=true ;             ;;
               -pause)  PAUSE="$PAUSE${PAUSE:+,}exit"   ;;
        -pause-list=*)  PAUSE="$PAUSE${PAUSE:+,}$val"   ;;
             -quiet|q)  QUIET=true                      ;;
           -verbose|V)  VERBOSITY=$((VERBOSITY + 1))    ;;
        -very-verbose)  VERBOSITY=$((VERBOSITY + 2))    ;;


       # These are read early.  They are not unknown
     -ignore-config|I)                                  ;;
      -reset-config|R)                                  ;;
      -write-config|W)                                  ;;

               *)  fatal "Unknown parameter %s" "-$arg" ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to evalute some command line args before the root user test.
#------------------------------------------------------------------------------
eval_early_argument() {
    case $1 in
      -ignore-config|I) IGNORE_CONFIG=true    ;;
       -reset-config|R) RESET_CONFIG=true     ;;
       -write-config|W) WRITE_CONFIG=true     ;;
                -pause) PAUSE="$PAUSE${PAUSE:+,}exit" ;;
               -help|h) usage                 ;;
            -version|v) show_version          ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine for command line arguments that don't start with "-"
#------------------------------------------------------------------------------
assign_parameter() {
    local cnt=$1 param=$2
   case $cnt in
        *) CMDS="$CMDS${CMD:+ }$param" ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to see if an argument requires a value to follow it.
#------------------------------------------------------------------------------
takes_param() {
    case $1 in
           -color|C) return 0 ;;
          -device|d) return 0 ;;
           -force|F) return 0 ;;
          -kernel|k) return 0 ;;
         -modules|m) return 0 ;;
    esac
    return 1
}

#------------------------------------------------------------------------------
# The main routine.  Called from the very bottom of this script.
#------------------------------------------------------------------------------
main() {
    local SHIFT SHORT_STACK="aCdDFGhiIkKmNpqRvVW"
    local BE_VERBOSE VERY_VERBOSE VERBOSITY=0
    local orig_args="$*"

    # Bashism: put the original arguments into an array to deal w/ spaces
    declare -a ORIG_ARGS=("$@")

    # Let non-root users get usage.  Need to get --ignore-config early.
    read_early_params "$@"

    need_root

    EXIT_NUM=100

    read_reset_config_file "$CONFIG_FILE"

    # Needs to be before we read cmdline
    test -f "$TEMPLATE_INITRD" || TEMPLATE_INITRD=$TEMPLATE_INITRD_2

    read_all_cmdline_mingled "$@"
    set_colors $COLOR_SCHEME
    check_cmds  CMDS  "$ALL_CMDS"
    check_force FORCE "$ALL_FORCE"
    check_pause PAUSE "$ALL_PAUSE"

    case $VERBOSITY in
        0) ;;
        1) BE_VERBOSE=true                     ;;
        *) BE_VERBOSE=true ; VERY_VERBOSE=true ;;
    esac

    local new_initrd
    if [ ${#CMD_INITRD} -gt 0 ]; then
        test -f "$CMD_INITRD" || fatal "The initrd file '%s' was not found" "$CMD_INITRD"
        new_initrd=true
    fi

    local template_initrd=${CMD_INITRD:=$TEMPLATE_INITRD}

    need_prog $VM_VERSION_PROG copy-initrd-modules

    [ "$WRITE_CONFIG" ] && write_config "$CONFIG_FILE" && exit 0

    trap clean_up EXIT

    do_flock

    : ${CMDS:=all}

    # starting <program name>
    shout_title $"Starting %s" "$ME"

    start_log "$orig_args"
    type -t find_man_page &>/dev/null && find_man_page

    shout_pretend

    USB_DIR=$WORK_DIR/usb
    SQFS_DIR=$WORK_DIR/linux
    BIOS_DIR=$WORK_DIR/bios

    local root_dir=$SQFS_DIR

    mkdir -p $WORK_DIR || fatal 120 "Could not make a work directory under /run"
    mount -t tmpfs tmpfs $WORK_DIR || fatal 121 "Could not mount tmpfs at %s" $WORK_DIR

    local we_are_live  live_dev  bios_uuid  bios_dev
    if its_alive_usb; then
        we_are_live=true

        # This reads initrd.out file
        read_initrd_config

        bios_uuid=$INITRD_BIOS_UUID
        [ -n "$bios_uuid" ] && bios_dev=$(blkid -U "$bios_uuid")

        live_dev=$(get_live_dev)
        warn_z "$live_dev" $"The live media is not mounted"
        msg $"Current running kernel is %s" "$(pq $(uname -r))"
    fi

    #==============================================================================
    # The First Menu
    #==============================================================================
    [ ${#DEVICE} -eq 0 ] && select_live_usb_device DEVICE "$live_dev" "$bios_dev"

    # Are we going to update a running live system or an plugged in live-usb?
    local usb_dev live_mp

    #----- RUNNING LIVE -------------------------------------------------------
    if update_live; then
        fatal_z "$we_are_live" $"This is not a live-usb system"
        live_mp=$LIVE_MP
        msg $"Will use running live system"

        IS_ENCRYPTED=$INITRD_ENCRYPTED

        if encrypted ; then
            mount_if_needed "$bios_dev" $BIOS_DIR
            shout "Encryption detected"
            msg "Made sure %s was mounted at %s" "$(pq $bios_dev)" "$(pq $BIOS_DIR)"

            # Avoid updating the initrd.  Can fix later by borrowing code from lum to
            # add crypt programs to the initrd ***** sigh *****
            template_initrd=
        fi

    #----- LIVE-USB -----------------------------------------------------------
    else

        fatal_z "$DEVICE" "Must specify a device if not running live"
        usb_dev=$(expand_device $DEVICE)
        fatal_z "$usb_dev" $"Could not find device %s" "$DEVICE"

        force usb || is_usb_or_removable $usb_dev \
            || fatal usb $"The device '%s' does not appear to be a usb device" "$usb_dev"

        local dev_type=$(lsblk -no type --nodeps $usb_dev)

        # Allow user to specify disk device instead of partition
        if [ "$dev_type" = "disk"  ]; then
            local part_dev=$(get_partition $usb_dev 1)
            dev_type=$(lsblk -no type --nodeps $part_dev)
            if [ "$dev_type" = "part" ]; then
                usb_dev=$part_dev
            else
                fatal $"Can't find first partition on device %s" $usb_dev
            fi
        fi
        [ "$dev_type" = "part" ] || fatal $"Device %s is not a disk partition" $usb_dev

        local usb_drive=$(get_drive $usb_dev)
        umount_all $usb_drive

        my_mount $usb_dev $USB_DIR
        msg $"Will use live-usb device %s" "$(pq $usb_dev)"
        live_mp=$USB_DIR

        local crypt_file=$live_mp/$LIVE_DIR/$CRYPT_FNAME
        if test -e $crypt_file; then
            shout "Encrypted live-usb detected"
            local main_uuid=$(cat $crypt_file 2>/dev/null)

            my_mkdir $BIOS_DIR
            always_cmd mount --move $live_mp $BIOS_DIR || fatal "Could not move mount to %s" $(basename $BIOS_DIR)

            fatal_z "$main_uuid" "Could not find uuid of encrypted partition"

            local main_dev=$(blkid -U "$main_uuid")
            echo "main uuid: $main_uuid  main dev: $main_dev" >> $LOG_FILE

            fatal_z "$main_dev" "Could not find encrypted partition with uuid %s" "$boot_uuid"

            shout "When asked, please enter the password for this encrypted live-usb"
            LUKS_DEV=/dev/mapper/$LUKS_NAME

            always_cmd cryptsetup open --type=luks $main_dev $LUKS_NAME \
                || fatal "Was unable to open the encrypted partition"

            always_cmd mount $LUKS_DEV $live_mp || fatal "Was unable to mount the encrypted device"

            IS_ENCRYPTED=true
            template_initrd=
        fi

    fi

    is_mountpoint "$live_mp" || fatal "Expected '%s' to be a mountpoint" "$live_mp"

    show_distro_version "$live_mp"

    # local full_live_dir=$live_mp/${LIVE_DIR#/}
    local full_live_dir rel_live_dir

    find_live_boot_dir rel_live_dir "$live_mp" "$LINUXFS_NAME" \
        || fatal $"No '%s' file found in any directory in '%s'" "$LINUXFS_NAME" "$live_mp"

    local full_live_dir=$live_mp/${rel_live_dir#/}

    check_writable "$full_live_dir" "live boot"

    # Find the linuxfs file (where we get new kernels and modules from)
    # Use the remastered linuxfs.new if it is available.
    local ext file linuxfs_name
    for ext in "$NEW_EXT" ""; do
        file=$full_live_dir/$LINUXFS_NAME$ext
        test -r $file || continue
        linuxfs_name=$file
        break
    done

    # Could not find '<linuxfs.new>' or '<linuxfs>' on the live-usb
    fatal_z "$linuxfs_name" $"Could not find '%s' or '%s' on the live-usb" \
        $(pqh "$LINUXFS_NAME$NEW_EXT") $(pqh "$LINUXFS_NAME")

    local live_remaster
    if [ -z "${linuxfs_name%%*$NEW_EXT}" ]; then
        live_remaster=true
        # Found <type> file <file-name> in directory <dir-name>
        msg $"Found %s file %s in directory %s" $(pq live-remaster) $(pq $(basename $linuxfs_name)) "$(pq $rel_live_dir)"
    else
        msg $"Found %s file %s in directory %s" $(pq linuxfs) $(pq $(basename $linuxfs_name)) "$(pq $rel_live_dir)"
    fi

    # Fix me?
    my_mount "$linuxfs_name" "$root_dir" -t squashfs -o loop,ro
    local full_boot_dir="$root_dir/${BOOT_DIR#/}"

    pause mount

    # NOTE: *after* mounting the linuxfs file we switch to the BIOS_DIR directory (sneaky)
    # if we are working on an encrypted live-usb
    encrypted && live_mp=$BIOS_DIR
    full_live_dir=$live_mp/${rel_live_dir#/}

    local full_initrd targ_initrd="$full_live_dir/$INITRD_NAME"
    if [ "$new_initrd" ]; then
        full_initrd=$CMD_INITRD
    elif [ ${#INITRD_NAME} -eq 0 ]; then
        fatal "An empty initrd name was given"
    else
        full_initrd=$targ_initrd
    fi

    test -e "$full_initrd" || fatal $"Could not find initrd file: %s" "$full_initrd"

    # default live kernels: vmlinuz and vmlinuz1
    local both_vm=$VMLINUZ_NAME
    [ -n "$VMLINUZ_2_NAME" ] && both_vm="$both_vm|$VMLINUZ_2_NAME"

    local live_all boot_all
    get_all_kernel live_all "$full_live_dir"                          # all in live dir
    get_all_kernel boot_all "$full_boot_dir"                          # all in boot dir
    #get_all_kernel boot_mod "$full_boot_dir" --mod-dir="$root_dir"   # boot dir with module dir

    local live_vm=$(find_kernel_fname "$both_vm" "$live_all")         # live with default vmlinuz names
    local vm_cnt=$(count_lines "$live_vm")
    local live_old=$(find_kernel_fname "($both_vm)$KOLD_EXT" "$live_all" )

    # Exclude vmlinuz vmlinuz1, vmlinuz.kold, and vmlinuz1.kold from new kernel list
    local live_both=$(echo -e "$live_vm\n$live_old")
    local live_versions=$(get_kernel_version "$live_both" | tr "\n" "|")
    live_versions=${live_versions%|}

    local boot_new=$(find_kernel_version "$live_versions" "$boot_all" -v)
    local new_cnt=$(count_lines "$boot_new")
    local live_old_cnt=$(count_lines "$live_old")
    local action_cnt=$((new_cnt + live_old_cnt))

    # [a table of what was found will follow]
    msg $"Found:"
    # singular and plural: 1 total live kernel
    msg_kernel "$live_all"  $"total live kernel"       $"total live kernels"
    # singular and plural
    msg_kernel "$live_vm"   $"default live kernel"     $"default live kernels"
    # singular and plural
    msg_kernel "$live_old"  $"old live kernel"         $"old live kernels"
    msg
    # singular and plural
    msg_kernel "$boot_all"  $"total installed kernel"  $"total installed kernels"
    # singular and plural
    msg_kernel "$boot_new"  $"new installed kernel"    $"new installed kernels"

    show_kernel_3 $"Live kernels:" "$live_all" | strip_color >> $LOG_FILE
    show_kernel_3 $"Boot kernels:" "$boot_all" | strip_color >> $LOG_FILE

    case $new_cnt in
        0) ;;
        1) log_it show_kernel_2 $"Only one new installed kernel was found:" "$boot_new" ;;
        *) log_it show_kernel_2 $"New installed kernels:" "$boot_new" ;;
    esac

    if [ $new_cnt -gt $MAX_FILES ]; then
        warn $"There are %s new kernels.  Only showing the most recent %s." "$new_cnt" "$MAX_FILES"
        boot_new=$(echo "$boot_new" | grep . | head -n $MAX_FILES)
    fi

    fatal_k0 "$boot_all" $"No kernels were found in the boot directory"
    fatal_k0 "$live_all" $"No kernels were found in the live boot directory"

    # Let's see if they should do a remaster first
    local boot_all_cnt=$(count_lines "$boot_all")
    local live_boot_cnt=0
    # Only count /boot kernels if we are working on a live system
    update_live && live_boot_cnt=$(count_kernels /boot)

    # They should almost always remaster if they've installed a kernel
    if [ $live_boot_cnt -gt $boot_all_cnt ]; then
        shout $"You should do a remaster before doing a kernel update"
        do_remaster "${ORIG_ARGS[@]}"
    fi

    if [ $action_cnt -eq 0 -a ${#MODS} -eq 0 ]; then

        # Maybe they just need to do a remaster first
        local boot_all_cnt=$(count_lines "$boot_all")
        local live_boot_cnt=0
        # Only count /boot kernels if we are working on a live system
        [ "$DEVICE" = "live" ] && live_boot_cnt=$(count_lines "$($VM_VERSION_PROG -nsr /boot)")

        # Aha, they just need to remaster first
        if [ $live_boot_cnt -gt $boot_all_cnt ]; then
            shout_subtitle $"You MUST do a remaster before doing a kernel update"
            do_remaster "${ORIG_ARGS[@]}"
            exit

        elif test -f "$template_initrd" ; then
            shout
            shout $"No new kernels were found"
            shout $"Can only update the initrd file"
            shout $"You must install a kernel and then remaster before doing a kernel update"
        else
            error $"No new kernels were found and there's no template initrd file"
            fatal $"You must install a kernel and then remaster before doing a kernel update"
        fi
    fi

    #==============================================================================
    # The Second Menu (if needed, no --initrd  and no --modules)
    #==============================================================================
    local action_target
    if [ ${#MODS} -gt 0 ]; then
        action_target="modules@$MODS"
    elif [ "$new_initrd" ];then
        action_target=initrd@$CMD_INITRD
    else
        select_update_rollback_action action_target "$live_vm" "$live_old" "$new_cnt" "$template_initrd"
    fi

    local action=${action_target%@*}
    local target_fname=${action_target#*@}

    local new_kernel new_version new_fname stat_list action_name blurb
    local initrd_only add_modules
    local current_versions=$(get_kernel_version "$live_both")

    #----- Update Action -------------------------------------------------------------------------
    if [ "$action" = "update" ]; then
        action_name=$"live kernel update"

        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local target_date=$(get_kernel_date "$target")

        #==========================================================================
        # The Third Menu (only if needed)
        #==========================================================================
        select_new_kernel new_kernel "$NEW_KERNEL" "$boot_new"

        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")
        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel '%s'" "$new_version"

        # Current kernel and New/Old kernel specs
        blurb=$(kernel_stats \
            $"Current" "$target_version" "$target_date" "$target_fname" "$target_fname$KOLD_EXT" \
            $"New"     "$new_version"    "$new_date"    $"(new)"        "$target_fname")

    #---- Rollback Action ---------------------------------------------------------
    elif [ "$action" = "rollback" ]; then
        action_name=$"live kernel rollback"
        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local target_date=$(get_kernel_date "$target")

        new_kernel=$(find_kernel_fname "$target_fname$KOLD_EXT" "$live_old")
        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")

        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel '%s'" "$new_fname"

        blurb=$(kernel_stats \
            $"Current" "$target_version" "$target_date" "$target_fname"          "$target_fname$KBAD_EXT" \
            $"Old"     "$new_version"    "$new_date"    "$target_fname$KOLD_EXT" "$target_fname")

    #---- Update Initrd Action ----------------------------------------------------
    elif [ "$action" = "initrd" ]; then
        action_name=$"live initrd update"
        initrd_only=true
        local blurb_fmt="$m_co%s: %s"
        # Update initrd using file <filename> [NOTE: no "%s" here!]
        blurb=$(printf "$blurb_fmt" $"Update initrd using file" "$(pq "$target_fname")")

    elif [ "$action" = "modules" ]; then
        action_name=$"live modules update"
        initrd_only=true
        add_modules=true
        local blurb_fmt="$m_co%s: %s"
        # Update initrd using file <filename> [NOTE: no "%s" here!]
        blurb=$(printf "$blurb_fmt" $"Add initrd modules" "$(pq "$target_fname")")

    elif [ "$action" = 'quit' ]; then
        exit

    else
        internal_error "update/rollback menu" "$action"
    fi

    msg
    # Ready to make the following <live kernel update>
    shout_subtitle $"Ready to make the following %s" "$action_name"
    log_it echo "$blurb"

    YES_no_pretend || my_exit

    if [ "$action" = "rollback" ]; then
        do_rollback  "$full_live_dir" "$target_fname" "$INITRD_NAME"
        my_done "$action_name"
    fi

    local initrd_dir=$WORK_DIR/initrd
    local tmp_initrd_file="$WORK_DIR/$(basename $INITRD_NAME).tmp"
    local compression_file="$WORK_DIR/compression"
    local cpio_files="$WORK_DIR/cpio.{in,out}"

    if [ ${#CMDS} -le 0 ]; then
        echo -e "No command(s) given.  Try 'all'."
        exit 0
    fi

    # NEED: $new_version  $new_fname $targ_fname
    #--------------------------------------------------------------------------
    #----- REAL WORK STARTS HERE ----------------------------------------------
    #--------------------------------------------------------------------------
    if need unpack; then

        if force clear; then
            cmd rm -rf "$initrd_dir/../initrd"
            cmd rm -f "$tmp_initrd_file" $compression_file $cpio_files
        else
            test -d "$initrd_dir" && yes_NO_fatal "clear" \
                "Do you want to delete it now"            \
                "Use --force=clear to always delete it"   \
                "Will not over-write an existing initrd directory."
        fi

        unpack_initrd "$full_initrd" "$initrd_dir"  "$compression_file"
    fi

    pause unpack

    copy_initrd_release "$root_dir" "$initrd_dir" "$initrd_only"

    if need copy-modules; then
        if [ ${#MODS} -gt 0 ]; then
            : # Don't delete any exiting modules
            local mod_file=$WORK_DIR/modules.list
            echo "$MODS" | sed -r "s/[ ,]+/\n/g" > $mod_file
            copy_modules "$root_dir" "$initrd_dir" "$current_versions" "$mod_file"

        elif [ "$initrd_only" ]; then
            delete_all_modules $initrd_dir
            copy_modules "$root_dir" "$initrd_dir" "$current_versions"
        else
            [ "$KEEP_OLD" ] || remove_old_mods $initrd_dir $target_version
            copy_modules "$root_dir" "$initrd_dir" "$new_version"
        fi
    fi

    pause copy

    need repack && repack_initrd "$initrd_dir" "$tmp_initrd_file"  "$compression_file"
    pause repack

    if need install; then
        if [ "$initrd_only" ]; then
            do_install "$tmp_initrd_file" "$targ_initrd"  "$IOLD_EXT"
        else
            do_install "$tmp_initrd_file" "$targ_initrd"
            do_install "$full_boot_dir/$new_fname" "$full_live_dir/$target_fname"
        fi
    fi
    pause install

    my_done "$action_name"
}

#==============================================================================
# END OF MAIN
#==============================================================================

#==============================================================================
# Menu Routines
#==============================================================================

#------------------------------------------------------------------------------
# This is the first menu.  It is used to select the current live system or a
# plugged in usb or removable device.
#------------------------------------------------------------------------------
select_live_usb_device() {
    local var=$1  live_dev=$2  bios_dev=$3
    local menu=$(cli_drive_menu "$live_dev" "$bios_dev")

    if [ "$live_dev" ]; then
        local title=$(printf $"The current Live System on %s" "$(pq $live_dev)")
        menu=$(printf "live$P_IFS$m_co%s$nc_co\n%s" "$title" "$menu")
    fi

    fatal_0 $(count_lines "$menu") "usb" $"The system is not a live-usb and no usb drives were detected"

    my_select "$var" $"Please select the system to update" "$menu"
}

#------------------------------------------------------------------------------
# This is the second menu.  It selects which action to take: update or rollback
# and which vmlinuz file (vmlinuz or vmlinuz1) to perform it on.
#------------------------------------------------------------------------------
select_update_rollback_action() {
    local var=$1

    local menu=$(make_update_menu "$@")

    echo "$menu"
     local cnt=$(count_lines "$menu")
     [ $cnt -lt 1 ] && fatal $"No update or rollback actions are available"

    my_select $var $"Please select an action to perform" "$menu"
}

#------------------------------------------------------------------------------
# Make the main menu for changing kernel or rolling back
#------------------------------------------------------------------------------
make_update_menu() {
    local var=$1  live_vm=$2  old_vm=$3  new_cnt=$4  initrd=$5  orig_ifs=$IFS

    local IFS=$K_IFS

    # FIXME: go through twice to get widths?
    local fmt="%s$bold_co %s$m_co %s %s ($date_co%s$nc_co)"
    local live_k  action  action_name

    local version fname date
    if [ $new_cnt -gt 0 ]; then
        action=update; action_name=$"Update"
        while read version fname date; do
            [ ${#version} -eq 0 ] && continue
            menu_printf "$action@$fname" "$fmt" "$action_name" "$fname" $"from" "$(vq $version)" "$date"
        done <<Live_VM
$(echo "$live_vm")
Live_VM
    fi

    action="rollback" ; action_name=$"Rollback"
    fmt="%s$bold_co %s$m_co %s %s ($date_co%s$nc_co)"
    while read version fname date; do
        [ ${#version} -eq 0 ] && continue
        fname=${fname%$KOLD_EXT}
        menu_printf "$action@$fname" "$fmt" "$action_name" "$fname" $"to" "$(vq $version)" "$date"
    done <<Old_VM
$(echo "$old_vm")
Old_VM

    [ ${#initrd} -gt 0 ] && test -f "$initrd" \
        && menu_printf "initrd@$initrd" $"Update initrd using file %s" "$(pq "$initrd")"

    menu_printf 'quit' $"Quit"
    IFS=$orig_ifs
}


#------------------------------------------------------------------------------
# This is the third menu.  It lets the user select the new kernel that will
# be used when they do an update.  If there is only one new kernel then that
# kernel is selected and there is no menu.
#------------------------------------------------------------------------------
select_new_kernel() {
    local var=$1  cmd_version=$2  list=$3  cnt=$(count_lines "$3")

    fatal_0 $cnt $"Did not find any new kernels in the boot directory"

    if [ ${#cmd_version} -gt 0 ]; then
        local cmd_kernel=$(find_kernel_version "$cmd_version" "$list")
        local cmd_cnt=$(count_lines "$cmd_kernel")
        case $cmd_cnt in
            0) fatal "No new kernels match the requested version %s" "$cmd_version" ;;

            1) msg $"Found one kernel matching the requested version %s" "$(pq $cmd_version)"
               eval "$var=\$list"
               return ;;

            *) fatal "Found multiple kernels matching the requested version %s" "$cmd_version" ;;
        esac
    fi

    case $cnt in
        1)  msg $"Found one new kernel %s" "$(pq $(get_kernel_version $list))"
            eval "$var=\$list"
            return ;;
    esac

    select_kernel_2 $"Please select the new kernel from this list" $var "$list"
}

#==============================================================================
# The Routines that do the "real" work.
#==============================================================================
#------------------------------------------------------------------------------
# Unpacks the initrd file.  Does it in two steps. Records the compression
# type.  The framework is here to deal with multiple compression types and
# archive types but it has not been tested!
#------------------------------------------------------------------------------
unpack_initrd() {
    local file=$1  dir=$2  compression_file=$3
    local compression=$(file -b "$file" | cut -d" " -f1)

    local compress args
    case $compression in
         gzip) compress="gzip"   ; args="-cd"  ;;
        bzip2) compress="bzip2"  ; args="-cd"  ;;
         lzip) compress="lzip"   ; args="-cd"  ;;
           XZ) compress="xz"     ; args="-cd"  ;;
            *) fatal "Don't know how to decompress a '%s' file" "$compression" ;;
    esac
    echo "$compress" > $compression_file

    need_prog $compress
    my_mkdir "$dir"

    local cpio_file="$dir.in"
    $compress $args "$file" > "$cpio_file" || fatal "The decompression program '%s' failed" "$decomp"

    local unpack archive_type=$(file -b "$cpio_file")
    case $archive_type in
        *" cpio archive"*) unpack="cpio" ; args="-idum --quiet" ;;
         *" tar archive"*) unpack="tar"  ; args="-x"            ;;
                       *) fatal "unknown initrd archive type '%s'" "$archive_type"
    esac
    (cd "$dir" && $unpack $args) < "$cpio_file" || fatal "The %s extraction failed" "$unpack"
}

#------------------------------------------------------------------------------
# Copy the initrd-release file (if needed)
#------------------------------------------------------------------------------
copy_initrd_release() {
    local from=$1  to=$2  always=$3
    local from_release=$from/etc/initrd-release
    local to_release=$to/etc/initrd-release

    [ "$always" -o ! -e "$to_release" ] || return
    test -e "$from_release"             || return
    always_cmd cp "$from_release" "$to_release"
}

#------------------------------------------------------------------------------
# Remove all kernel modules from the initrd
#------------------------------------------------------------------------------
delete_all_modules() {
    local to=$1
    local mod_dir="$to/lib/modules"
    test -e "$mod_dir" && always_cmd rm -r "$mod_dir"
}

#------------------------------------------------------------------------------
# Remove modules directory for the old kernel
#------------------------------------------------------------------------------
remove_old_mods() {
    local root=$1  kernel=$2
    local dir=$root/lib/modules/$kernel

    if test -d "$dir"; then
        always_cmd rm -r "$dir"
    else
        warn "Module directory for old kernel '%s' not found" "$kernel"
    fi
}

#------------------------------------------------------------------------------
# Copy in modules directory for the new kernel
#------------------------------------------------------------------------------
copy_modules() {
    local from=$1  to=$2  versions=$3  mod_file=$4

    local options="--count --quiet --from=$from --to=$to"
    [ ${#mod_file} -gt 0 ] && options="$options --verbose"

    local version success
    for version in $versions; do
        msg "  -> %s %s" $"kernel" "$(pq $version)"
        if always_cmd copy-initrd-modules $options --encrypt --kernel="$version" $mod_file; then
            success=true
            continue
        fi
        warn $"Copy initrd modules failed for kernel %s" "$version"
    done

    [ "$success" ] && return
    fatal $"No modules were copied to the initrd"
}

#------------------------------------------------------------------------------
# Pretty much the inverse of unpack_initrd().  We leave everything in the work
# directory until we do the actual install.
#------------------------------------------------------------------------------
repack_initrd() {
    local dir=$1  file=$2  compression_file=$3
    local compress
    read compress 2>/dev/null < $compression_file
    : ${compress:=gzip}

    local args
    case $compress in
         bzip2) args="$BZ2_OPTS" ;;
          gzip) args="$GZ_OPTS"  ;;
          lzip) args="$LZ_OPTS"  ;;
            xz) args="$XZ_OPTS"  ;;
             *) yes_NO_fatal "compress" \
               "Do you want to continue on using gzip compression?" \
               "Use %s to skip this question in the future" \
               "Unknown compression format: %s" "$compression"
                compress="gzip" ; args="$GZIP_OPTS"  ;;
    esac

    need_prog $compress

    local cpio_file="$dir.out"
    (cd "$dir" && find . | cpio $CPIO_OPTS) > $cpio_file \
        || fatal "Could not create a cpio archive"

    msg $"Compressing ..."
    $compress $args < "$cpio_file" > "$file" || fatal "Compression (%s) failed" $compress
}

#------------------------------------------------------------------------------
# Installs a file from "$from" to "$to".  Makes a backup of the original and
# also makes an md5sum of the new file.
#------------------------------------------------------------------------------
do_install() {
    local from=$1  dest=$2 old_ext=${3:-$KOLD_EXT}
    test -f "$from" || fatal "Installation file %s does not exist" "$from"

    # install <filename>
    msg $"install %s" "$(pq $(basename $dest))"
    #msg "install %s --> %s" "$from" "$dest"

    if test -e "$dest"; then
        if [ -n "${dest%%*$old_ext}" ]; then
            local backup="$dest$old_ext"
            # msg "backing up %s to %s" "$(pq $(basename "$dest"))" "$(pq $(basename "$backup"))"
            cmd mv "$dest" "$backup"
        else
            warn $"Overwriting backup"
        fi
    else
        warn $"Target file %s does not exist" "$dest"
    fi

    cmd cp "$from" "$dest"

    local md5_file=$dest$MD5_EXT
    #test -e $md5_file && cmd mv $md5_file $dest$old_ext$MD5_EXT
    local md5_sum=$(cd $(dirname $dest) && md5sum $(basename $dest))
    cmd write_file "$md5_file" "$md5_sum"
}

#------------------------------------------------------------------------------
# Perform the rollback action by moving
#       X      -->  X.kbad
#       X.kold -->  X
# Then make new md5 files.
#------------------------------------------------------------------------------
do_rollback() {
    local dir=$1  vmlinuz=$2  initrd=$3

    local the_initrd="$dir/$initrd"
    local old_initrd="$the_initrd$KOLD_EXT"
    local bad_initrd="$the_initrd$KBAD_EXT"

    local the_vmlinuz="$dir/$vmlinuz"
    local old_vmlinuz="$the_vmlinuz$KOLD_EXT"
    local bad_vmlinuz="$the_vmlinuz$KBAD_EXT"


    test -e "$old_vmlinuz" || fatal $"Could not find old %s file to roll back" "$vmlinuz$K_OLD"
    test -e "$old_initrd"  || fatal $"Could not find old %s file to roll back" "$initrd$K_OLD"

    # Roll back <file-1> to <file-2>
    msg $"Roll back %s to %s" "$(pq $(basename "$old_initrd"))" "$(pq $initrd)"
    cmd mv "$the_initrd" "$bad_initrd"
    cmd mv "$old_initrd" "$the_initrd"

    msg $"Roll back %s to %s" "$(pq $(basename "$old_vmlinuz"))" "$(pq $vmlinuz)"
    cmd mv "$the_vmlinuz" "$bad_vmlinuz"
    cmd mv "$old_vmlinuz" "$the_vmlinuz"

    local dest md5_file md5_sum
    for dest in "$the_initrd" "$the_vmlinuz"; do
        md5_file=$dest$MD5_EXT
        #test -e $md5_file && cmd mv $md5_file $dest$KOLD_EXT$MD5_EXT
        md5_sum=$(cd $(dirname $dest) && md5sum $(basename $dest))
        cmd write_file "$md5_file" "$md5_sum"
    done
}

#------------------------------------------------------------------------------
# Currently not used.  Filter out kernels that don't have a module directory.
#------------------------------------------------------------------------------
find_valid_kernel() {
    local var=$1  list=$2  dir=$2  out

    local line version
    while read line; do
        [ ${#line} -eq 0 ] && continue
        version=$(get_kernel_version "$line")
        if -d "$dir/lib/modules/$version"; then
            out="$out$line\n"
        else
            warn "No module directory found for kernel %s" "$version"
        fi
    done<<List
$(echo -n "$list")
List
    out=$(echo -e "$out")
    eval $var=\$out
}

#------------------------------------------------------------------------------
# Format some simple counts of kernels in lists.  It allows us to use plural
# or singular forms that can be easily translated.
#------------------------------------------------------------------------------
msg_kernel() {
    local list=$1 lab1=$2 lab2=$3
    local cnt=$(count_lines "$list")

    if [ $cnt -ne 1 ]; then
        msg "  %2s %s %s" $(nq $cnt) "$lab2"
    else
        local version=$(get_kernel_version "$list")
        msg "  %2s %-24s (%s)" $(nq $cnt) "$lab1" "$(vq $version)"
        return
    fi
}

#------------------------------------------------------------------------------
# Say a few extra things to the user before we exit
#------------------------------------------------------------------------------
my_done() {
    local action=$1
    echo
    if [ "$PRETEND_MODE" ]; then
        # "pretend mode" goes through the steps withoute altering your system
        local in_pretend=$"in pretend mode"
        Shout "%s %s %s (%s)." "$ME" "$action" $"done" "$(pqh "$in_pretend")"
    else
        Shout "%s %s %s." "$ME" "$action" $"done"
    fi
    my_exit 0
}


#------------------------------------------------------------------------------
# Convenience routine
#------------------------------------------------------------------------------
update_live() { [ "$DEVICE" = 'live' ] ; return $? ; }

#------------------------------------------------------------------------------
# Count the number of kernels in a directory
#------------------------------------------------------------------------------
count_kernels() {
    local dir=$1
    count_lines "$($VM_VERSION_PROG -nsr /boot)"
}


#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
do_remaster() {
    YES_no $"Would you like to do a remaster now?" || return
    live-remaster --cli
    shout $"Ready to restart %s" "$ME"
    press_enter
    clean_up
    exec $0 "$@"
}

#------------------------------------------------------------------------------
# Do a few things before cleaning up.
#------------------------------------------------------------------------------
my_exit() {
    local ret=${1:-0}

    pause exit $"Exit"

    #Msg "=> cleaning up"
    exit $ret
}

#------------------------------------------------------------------------------
# Start logging
#------------------------------------------------------------------------------
start_log() {
    local args=$1 cmds=${2# }

    LOG_FILE=$THE_LOG_FILE
    ERR_FILE=$THE_ERR_FILE

    rm -f $ERR_FILE

    test -e $LOG_FILE && echo >> $LOG_FILE

    cat <<Start_Log >> $LOG_FILE
---------------------------------------------------------------------
$0
        started: $(date)
        version: $VERSION ($VERSION_DATE)
    comand line: $args
      found lib: $FOUND_LIB
     TEXTDOMAIN: $TEXTDOMAIN
  TEXTDOMAINDIR: $TEXTDOMAINDIR

Start_Log
}

#------------------------------------------------------------------------------
# Trapped on exit.  Umount (order is vital!) remove the work_dir and remove
# the lock file.
#------------------------------------------------------------------------------
clean_up() {
    lib_clean_up
    mp_cleanup $SQFS_DIR $USB_DIR $BIOS_DIR $WORK_DIR
    test -d $WORK_DIR && rmdir $WORK_DIR
    luks_close $LUKS_NAME
    unflock
}

#------------------------------------------------------------------------------
# Write a configuration file based on the current settings
#------------------------------------------------------------------------------
write_config() {
    local file=${1:-$CONFIG_FILE}
    local dir=$(dirname "$file")
    mkdir -p "$dir" || fatal "Could not make config file directory %s" "$dir"
    msg "Writing config file %s" "$(pq "$file")"

    cat<<Config_File >"$file"
$(config_header "$file" "$ME" "$VERSION" "$VERSION_DATE")

       MAX_FILES="$MAX_FILES"
         LIVE_MP="$LIVE_MP"
        LIVE_DIR="$LIVE_DIR"
        BOOT_DIR="$BOOT_DIR"

 VM_VERSION_PROG="$VM_VERSION_PROG"
    LINUXFS_NAME="$LINUXFS_NAME"
    VMLINUZ_NAME="$VMLINUZ_NAME"
  VMLINUZ_2_NAME="$VMLINUZ_2_NAME"
     INITRD_NAME="$INITRD_NAME"
 TEMPLATE_INITRD="$TEMPLATE_INITRD"

MIN_LINUXFS_SIZE="$MIN_LINUXFS_SIZE"

        # for kernel and initrd.gz files
        KOLD_EXT="$KOLD_EXT"
        KBAD_EXT="$KBAD_EXT"
        IOLD_EXT="$IOLD_EXT"
        IBAD_EXT="$IBAD_EXT"
         MD5_EXT="$MD5_EXT"

        # For linuxfs file (right after a remaster)
         NEW_EXT="$NEW_EXT"

        BZ2_OPTS="$BZ2_OPTS"
         GZ_OPTS="$GZ_OPTS"
         LZ_OPTS="$LZ_OPTS"
         XZ_OPTS="$XZ_OPTS"

       CPIO_OPTS="$CPIO_OPTS"
       SHELL_LIB="$SHELL_LIB"

       LUKS_NAME="$LUKS_NAME"
     CRYPT_FNAME="$CRYPT_FNAME"

    COLOR_SCHEME="$COLOR_SCHEME"

$(config_footer)
Config_File

    return 0
}

#------------------------------------------------------------------------------
# Returns true if we are working on a live encrypted live-usb
#------------------------------------------------------------------------------
encrypted() { [ "$IS_ENCRYPTED" ] ; return $? ;}

#------------------------------------------------------------------------------
# Load the lib either from a neighboring repo or from the standard location.
#------------------------------------------------------------------------------
load_lib() {
    local file=$1  path=$2
    unset FOUND_LIB

    local dir lib found IFS=:
    for dir in $path; do
        lib=$dir/$file
        test -r $lib || continue
        if ! . $lib; then
            printf "Error when loading library %s\n" "$lib" >&2
            printf "This is a fatal error\n" >&2
            exit 15
        fi
        FOUND_LIB=$lib
        return 0
    done

    printf "Could not find library '%s' on path '%s'\n" "$file" "$path" >&2
    printf "This is a fatal error\n" >&2
    exit 17
}

#===== Start Here =============================================================
load_lib "$SHELL_LIB" "$LIB_PATH"

set_colors

main "$@"

#==============================================================================
# cli-shell-utils.bash
# An integrated collection of utilites for shell scripting.
# The .bash version uses $"..." for translation and another bashism in cmd().
#
# (C) 2016 -- 2017 Paul Banham <antiX@operamail.com>
# License: GPLv3 or later
#
# Note regarding reading command-line arguments and options:
#
# This is the oldest part of the code base.  Thie idea is to make it easy for
# programs that use this library to provide an easy, intuitive, and clear
# command line user interface.
#
#   SHORT_STACK               variable, list of single chars that stack
#   fatal(msg)                routine,  fatal([errnum] [errlabel] "error message")
#   takes_param(arg)          routine,  true if arg takes a value
#   eval_argument(arg, [val]) routine,  do whatever you want with $arg and $val
#==============================================================================

LIB_NAME="cli-shell-utils"
LIB_VERSION="2.00.08"
LIB_DATE="Sat Aug 12 00:28:49 MDT 2017"

: ${ME:=${0##*/}}
: ${MY_DIR:=$(dirname "$(readlink -f $0)")}
: ${MY_LIB_DIR:=$(readlink -f "$MY_DIR/../cli-shell-utils")}
: ${LIB_DIR:=/usr/local/lib/cli-shell-utils}
: ${LOCK_FILE:=/run/lock/$ME}
: ${LOG_FILE:=/dev/null}
: ${DATE_FMT:=%Y-%m-%d %H:%M}
: ${DEFAULT_USER:=1000}
: ${K_IFS:=|}
: ${P_IFS:=&}
: ${MAJOR_SD_DEV_LIST:=3,8,22,179,259}
: ${MAJOR_SR_DEV_LIST:=11}
: ${LIVE_MP:=/live/boot-dev}
: ${MIN_ISO_SIZE:=180M}
: ${MENU_PATH:=$MY_LIB_DIR/text-menus/:$LIB_DIR/text-menus}
: ${MIN_LINUXFS_SIZE:=120M}
: ${CONFIG_FILE:=/root/.config/$ME/$ME.conf}
: ${PROG_FILE:=/dev/null}
: ${LOG_FILE:=/dev/null}
: ${SCREEN_WIDTH:=$(stty size 2>/dev/null | cut -d" " -f2)}
: ${SCREEN_WIDTH:=80}
: ${USB_DIRTY_BYTES:=20000000}  # Need this small size for the progress bar to work
: ${PROG_BAR_WIDTH:=100}     # Width of progress bar in percent of screen width
: ${VM_VERSION_PROG:=vmlinuz-version}
: ${PROGRESS_SCALE:=100}
: ${INITRD_CONFIG:=/live/config/initrd.out}
: ${CP_ARGS:=--no-dereference --preserve=mode,links --recursive}

# Make sure these start out empty.  See lib_clean_up()
unset ORIG_DIRTY_BYTES ORIG_DIRTY_RATIO COPY_PPID COPY_PID SUSPENDED_AUTOMOUNT

FORCE_UMOUNT=true

export TEXTDOMAIN="cli-shell-utils"
domain_dir=$(readlink -f "$MY_DIR/../cli-shell-utils/locale")
test -d "$domain_dir" && export TEXTDOMAINDIR=$domain_dir

 BAR_80="==============================================================================="
SBAR_80="-------------------------------------------------------------------------------"

#------------------------------------------------------------------------------
# Sometimes it's useful to process some arguments (-h --help, for example)
# before others.  This can let normal users get simple usage.
# This relies on $SHORT_STACK, takes_param(), and eval_early_arguments()
# Only works on flags, not parameters that take options.
#------------------------------------------------------------------------------
read_early_params() {
    local arg

    while [ $# -gt 0 ]; do
        arg=$1 ; shift
        [ ${#arg} -gt 0 -a -z "${arg##-*}" ] || continue
        arg=${arg#-}
        # Expand stacked single-char arguments
        case $arg in
            [$SHORT_STACK][$SHORT_STACK]*)
                if echo "$arg" | grep -q "^[$SHORT_STACK]\+$"; then
                    local old_cnt=$#
                    set -- $(echo $arg | sed -r 's/([a-zA-Z])/ -\1 /g') "$@"
                    continue
                fi;;
        esac
        takes_param "$arg" && shift
        eval_early_argument "$arg"
    done
}

#------------------------------------------------------------------------------
# This will read all command line parameters.  Ones that start with "-" are
# evaluated one at a time by eval_arguments().  All others are evaluated by
# assign_parameter() which is given a count and a value.
#------------------------------------------------------------------------------
read_all_cmdline_mingled() {

    : ${PARAM_CNT:=0}
    SHIFT_2=0

    while [ $# -gt 0 ]; do
        read_params "$@"
        shift $SHIFT
        SHIFT_2=$((SHIFT_2 + SHIFT))
        [ -n "$END_CMDLINE" ] && return
        while [ $# -gt 0 -a ${#1} -gt 0 -a -n "${1##-*}" ]; do
            PARAM_CNT=$((PARAM_CNT + 1))
            assign_parameter $PARAM_CNT "$1"
            shift
            SHIFT_2=$((SHIFT_2 + 1))
        done
    done
}

#-------------------------------------------------------------------------------
# Sets "global" variable SHIFT to the number of arguments that have been read.
# Reads a series of "$@" arguments stacking short parameters and dealing with
# options that take arguments.  Use global SHORT_STACK for stacking and calls
# eval_argument() and takes_param() which should be provided by the calling
# program.  The SHIFT variable tells how many parameters we grabbed.
#-------------------------------------------------------------------------------
read_params() {
    # Most of this code is boiler-plate for parsing cmdline args
    SHIFT=0
    # These are the single-char options that can stack

    local arg val

    # Loop through the cmdline args
    while [ $# -gt 0 -a ${#1} -gt 0 -a -z "${1##-*}" ]; do
        arg=${1#-} ; shift
        SHIFT=$((SHIFT + 1))

        # Expand stacked single-char arguments
        case $arg in
            [$SHORT_STACK][$SHORT_STACK]*)
                if echo "$arg" | grep -q "^[$SHORT_STACK]\+$"; then
                    local old_cnt=$#
                    set -- $(echo $arg | sed -r 's/([a-zA-Z])/ -\1 /g') "$@"
                    SHIFT=$((SHIFT - $# + old_cnt))
                    continue
                fi;;
        esac

        # Deal with all options that take a parameter
        if takes_param "$arg"; then
            [ $# -lt 1 ] && fatal $"Expected a parameter after: %s" "-$arg"
            val=$1
            [ -n "$val" -a -z "${val##-*}" ] \
                && fatal $"Suspicious argument after %s: %s" "-$arg" "$val"
            SHIFT=$((SHIFT + 1))
            shift
        else
            case $arg in
                *=*)  val=${arg#*=} ;;
                  *)  val="???"     ;;
            esac
        fi

        eval_argument "$arg" "$val"
        [ "$END_CMDLINE" ] && return
    done
}

#==============================================================================
# Flow-Control Utilities
#
# These are used for flow-control, not system calls
#==============================================================================
#------------------------------------------------------------------------------
# return true if "$cmd" or "all" are in "$CMDS"
# If true print a small "==> $cmd" message
#------------------------------------------------------------------------------
need() {
    need_q "$1" || return 1
    local cmd=$1  xlat=${2:-$1}
    log_it echo &>/dev/null
    Msg "$(bq "=>") $xlat"
    #echo -e "@ $(date +"%Y-%m-%d %H:%M:%S")\n" >> $LOG_FILE

    return 0
}

#------------------------------------------------------------------------------
# Same as need() but silent _q = quiet
#------------------------------------------------------------------------------
need_q() {
    local cmd=$1  cmd2=${1%%-*}

    echo "$CMDS" | egrep -q "(^| )($cmd|$cmd2|all)( |$)" || return 1
    return 0
}

#------------------------------------------------------------------------------
# Return true if $cmd is in $CMD.  Unlike need(), ignore "all" and don't
# print anything extra.
#------------------------------------------------------------------------------
given_cmd() {
    local cmd=$1
    echo "$CMDS" | egrep -q "(^| )$cmd( |$)" || return 1
    return 0
}

#------------------------------------------------------------------------------
# Returns true if $here or "all" are in the comma delimited list $FORCE
#------------------------------------------------------------------------------
force() {
    local here=$1  option_list=${2:-$FORCE}
    case ,$option_list, in
        *,$here,*|*,all,*) return 0 ;;
    esac
    return 1
}

#------------------------------------------------------------------------------
# See if QUESTION_MODE matches any of the arguments
#------------------------------------------------------------------------------
q_mode() {
    local mode
    for mode; do
        [ "$QUESTION_MODE" = "$mode" ] && return 0
    done
    return 1
}
#------------------------------------------------------------------------------
# Pause execution if $here or "all" are in comma delimited $PAUSE
#------------------------------------------------------------------------------
pause() {
    local here=$1  xlated_here=${2:-$1}
    case ,$PAUSE, in
        *,$here,*)        ;;
          *,all,*)        ;;
                *) return ;;
    esac

    msg $"Paused at '%s'" $xlated_here
    press_enter
}


#------------------------------------------------------------------------------
# Wait until user presses <Enter>
#------------------------------------------------------------------------------
press_enter() {
    local ans enter=$"Enter"
    quest $"Press <%s> to continue" "$(pqq "$enter")"
    read ans
}

#------------------------------------------------------------------------------
# Make sure all force or pause options are valid
# First param is the name of the list variable so we can transform all spaces
# to commas.  See force() and pause()
#------------------------------------------------------------------------------
check_force() { _check_any force "$@"  ;}
check_pause() { _check_any pause "$@"  ;}

#------------------------------------------------------------------------------
# Shared functionality of two commands above
#------------------------------------------------------------------------------
_check_any() {
    local type=$1  name=$2  all=$3  opt
    eval "local opts=\$$name"

    # Convert spaces to commas
    opts=${opts// /,}
    eval $name=\$opts

    for opt in ${opts//,/ }; do
        case ,$all, in
            *,$opt,*) continue ;;
                   *) fatal "Unknown %s option: %s" "$type" "$opt" ;;
        esac
    done
}

#------------------------------------------------------------------------------
# Test for valid commands and all process cmd+ commands if $ordered is given.
# If $ordered is not given then cmd+ is not allowed.  If it is given then
# "cmd+" will add cmd and everything after it in $ordered to the variable
# named as the first argument.  See need() NOT cmd() which is different (sorry)
#------------------------------------------------------------------------------
check_cmds() {
    local cmds_nam=$1  all=" $2 "  ordered=$3 cmds_in cmds_out

    eval "local cmds_in=\$$cmds_nam"

    local cmd plus_cnt=0 plus
    [ "$ordered" ] && plus="+"

    for cmd in $cmds_in; do

        case $all in
            *" ${cmd%$plus} "*) ;;
            *) fatal $"Unknown command: %s" $cmd ;;
        esac

        [ -z "${cmd%%*+}" ] || continue

        cmd=${cmd%+}
        cmds_out="$cmds_out $(echo "$ORDERED_CMDS" | sed -rn "s/.*($cmd )/\1/p")"
        plus_cnt=$((plus_cnt + 1))
        [ $plus_cnt -gt 1 ] && fatal "Only one + command allowed"
    done

    [ ${#cmds_out} -gt 0 ] && eval "$cmds_nam=\"$cmds_in \$cmds_out\""
}

#------------------------------------------------------------------------------
# Works like cmd() below but ignores the $PRETEND_MODE variable.  This can be
# useful  if you want to always run a command but also want to record the call.
#------------------------------------------------------------------------------
always_cmd() { local PRETEND_MODE=  ; cmd "$@" ;}

#------------------------------------------------------------------------------
# Always send the command line and all output to the log file.  Set the log
# file to /dev/null to disable this feature.
# If BE_VERBOSE then send output to the screen as well as the log file.
# If VERY_VERBOSE then send commands to screen was well as the long file.
# If PRETEND_MODE then don't actually run the command.
#------------------------------------------------------------------------------
cmd() {
    local pre=" >"
    [ "$PRETEND_MODE" ] && pre="p>"
    echo "$pre $*" >> $LOG_FILE
    [ "$VERY_VERBOSE" ] && echo "$pre" "$@" | sed "s|$WORK_DIR|.|g"
    [ "$PRETEND_MODE" ] && return 0
    if [ "$BE_VERBOSE" ]; then
        "$@" 2>&1 | tee -a $LOG_FILE
    else
        "$@" 2>&1 | tee -a $LOG_FILE &>/dev/null
    fi
    # Warning: Bashism
    local ret=${PIPESTATUS[0]}
    test -e "$ERR_FILE" && exit 3
    return $ret
}

verbose_cmd() { local BE_VERBOSE=true ; cmd "$@" ;}
#==============================================================================
# BASIC TEXT UI ELEMENTS
#
# These are meant to provide easy and consistent text UI elements.  In addition
# the plan is to automatically switch over to letting a GUI control the UI,
# perhaps by sending menus and questions and so on to the GUI.  Ideally one
# set of calls in the script will suffice for both purposes.
#==============================================================================
#------------------------------------------------------------------------------
# The order is weird but it allows the *error* message to work like printf
# The purpose is to make it easy to put questions into the error log.
#------------------------------------------------------------------------------
yes_NO_fatal() {
    local code=$1  question=$2  continuation=$3  fmt=$4
    shift 4
    local msg=$(printf "$fmt" "$@")

    q_mode qui && fatal "$msg"

    [ -n "$continuation" -a -z "${continuation##*%s*}" ] \
        && continuation=$(printf "$continuation" "$(pq "--force=$code")")

    if [ "$AUTO_MODE" ]; then
        FATAL_QUESTION=$question
        fatal "$code" "$fmt" "$@"
    fi
    warn "$fmt" "$@"
    [ ${#continuation} -gt 0 ] && question="$question\n($m_co$continuation$quest_co)"
    yes_NO "$question" && return 0
    fatal "$code" "$fmt" "$@"
}

#------------------------------------------------------------------------------
#  Default to yes if QUESTION_MODE is "expert" otherwise default to no.
#------------------------------------------------------------------------------
expert_YES_no() {
    case $QUESTION_MODE in
    gui|simple) return 1                ;;
        expert) YES_no "$@" ; return $? ;;
             *) yes_NO "$@" ; return $? ;;
    esac
}

#------------------------------------------------------------------------------
#  Always default no
#------------------------------------------------------------------------------
expert_yes_NO() {
    case $QUESTION_MODE in
    gui|simple) return 1                ;;
        expert) yes_NO "$@" ; return $? ;;
             *) yes_NO "$@" ; return $? ;;
    esac
}

#------------------------------------------------------------------------------
# Simple "yes" "no" questions.  Ask a question, wait for a valid response.  The
# responses are all numbers which might be better for internationalizations.
# I'm not sure if we should include the "quit" option or not.  The difference
# between the two routines is the default:
#       yes_NO() default is "no"
#       YES_no() default is "yes"
#------------------------------------------------------------------------------
yes_NO() { _yes_no 1 "$1" ;}
YES_no() { _yes_no 0 "$1" ;}

_yes_no() {
    local answer ret=$1  question=$2  def_entry=$(($1 + 1))

    [ "$AUTO_MODE" ] && return $ret

    local yes=$"yes"  no=$"no"  quit=$"quit"  default=$"default"
    quit="$quit_co$quit"

    local menu=$(
        menu_printf  yes "$yes"
        menu_printf  no  "$no"
    )

    my_select answer "$question" "$menu" "" "$def_entry"

    case $answer in
      yes) return 0 ;;
       no) return 1 ;;
     quit) return 1 ;;
        *) internal_error _yes_no "$anwer"
    esac
}


old_yes_no() {
    local answer ret=$1  question=$2  def_entry=$(($1 + 1))

    [ "$AUTO_MODE" ] && return $ret

    local yes=$"yes"  no=$"no"  quit=$"quit"  default=$"default"
    quit="$quit_co$quit"

    local menu def_entry
    case $def_entry in
        1) menu=$(printf "  1) $yes ($default)\n  2) $no\n") ;;
        2) menu=$(printf "  1) $yes\n  2) $no ($default)\n") ;;
        *) fatal "Internal error in _yes_no()"               ;;
    esac

    local data=$(printf "1:1\n2:2\n0:0")
    my_select_2 answer "$quest_co$question$nc_co" $def_entry "$data" "$menu\n"

    case $answer in
        1) return 0 ;;
        2) return 1 ;;
     quit) return 1 ;;
        0) exit 0   ;;
        *) fatal "Should never get here 111" ;;
    esac
}

#------------------------------------------------------------------------------
# Create a simple yes/no/pretend-mode menu
#------------------------------------------------------------------------------
YES_no_pretend() {
    local question=$1 answer  orig_pretend=$PRETEND_MODE
    [ ${#question} -gt 0 ] || question=$"Shall we begin?"
    local yes=$"yes"  no=$"no"  pretend=$"pretend mode"

    local menu
    if [ "$PRETEND_MODE" ]; then
        menu="pretend$P_IFS$pretend\nno$P_IFS$no\n"
    else
        menu="yes$P_IFS$yes\nno$P_IFS$no\npretend$P_IFS$pretend\n"
    fi

    my_select answer "$question" "$menu"

    case $answer in
             yes) return 0 ;;
              no) return 1 ;;
         pretend) PRETEND_MODE=true ; shout_pretend ; return 0 ;;
               *) fatal "Internal error in YES_no_pretend()"   ;;
    esac
}

#------------------------------------------------------------------------------
# Announce to the world we are in pretend mode
#------------------------------------------------------------------------------
shout_pretend() { [ "$PRETEND_MODE" ] && Shout $"PRETEND MODE ENABLED" ;}

#------------------------------------------------------------------------------
# Like printf but prepend with the first argument and the payload separator.
# This is very handy for creating menu entries.
#------------------------------------------------------------------------------
menu_printf() {
    local payload=$1  fmt=$2  ; shift 2
    printf "%s$P_IFS$m_co$fmt$nc_co\n" "$payload" "$@"
}

#------------------------------------------------------------------------------
# Same as above but allow for singular and plural forms
#------------------------------------------------------------------------------
menu_printf_plural() {
    local payload=$1  cnt=$2  lab1=$3  lab2=$4

    case $cnt in
        1) printf "%s$P_IFS$lab1\n" "$payload" "$(nq $cnt)" ;;
        *) printf "%s$P_IFS$lab2\n" "$payload" "$(nq $cnt)" ;;
    esac
}

#------------------------------------------------------------------------------
# Print either the first label or the 2nd depending on if $cnt is 1 or not.
# Assume there is exactly one "%s" in the labels
#------------------------------------------------------------------------------
printf_plural() {
    local cnt=$1 lab1=$2 lab2=$3
    case $cnt in
        1) printf "$lab1\n" "$(nq $cnt)" ;;
        *) printf "$lab2\n" "$(nq $cnt)" ;;
    esac
}

questn_plural() { questn "$(printf_plural "$@")" ; }
warn_plural()   { warn   "$(printf_plural "$@")" ; }
msg_plural()    { msg    "$(printf_plural "$@")" ; }
#------------------------------------------------------------------------------
# Generate a simple selection menu based on a data:label data structure.
# The "1)" and so on get added automatically.
#------------------------------------------------------------------------------
my_select() {
    if [ -n "$GRAPHICAL_MENUS" ]; then
        graphical_select "$@"
    else
        my_select_num "$@"
    fi
}

#------------------------------------------------------------------------------
# This is the original my_select() routine, a front-end to my_select_2()
# This has been superseded by graphical_select().
#------------------------------------------------------------------------------
my_select_num() {
    local var=$1  title=$2  list=$3  def_str=$4  default=${5:-1}  orig_ifs=$IFS
    local IFS=$P_IFS
    local cnt=1 dcnt datum label data menu

    while read datum label; do
        if [ ${#datum} -eq 0 ]; then
            [ ${#label} -gt 0 ] && menu="$menu     $label\n"
            continue
        fi
        dcnt=$cnt

        if [ "$datum" = 'quit' ]; then
            dcnt=0
            label="$quit_co$label$nc_co"
        fi

        [ $dcnt = "$default" ] && label=$(printf "%s (%s)" "$label" "$m_co$(cq 'default')")

        data="${data}$dcnt:$datum\n"
        menu="${menu}$(printf "$quest_co%3d$hi_co)$m_co %${width}s" $dcnt "$label")\n"

        cnt=$((cnt+1))
    done<<My_Select
$(echo -e "$list")
My_Select

    [ "$VERBOSE_SELECT" ] && printf "\nMENU: $title\n$menu" | strip_color >> $LOG_FILE

    IFS=$orig_ifs
    my_select_2 $var "$title" "$default" "$data" "$menu" "$def_str"

}

#------------------------------------------------------------------------------
# This is the workhorse for several of my menu systems (in other codes).
#
#   $var:      the name of the variable the answer goes in
#   $title:    the question asked
#   $default:  the default selection (a number)
#   $data:     A string of lines of $NUM:$VALUE
#              The number select by the user gets converted to the value
#              The third field is used to mimic the value in the menu
#              for the initrd text menus but that may not be used here.
#   $menu      A multi-line string that is the menu to be displayed.  It
#              The callers job to make sure it is properly aligned with
#              the contents of $data.
#   $def_str   A string to indicate the default answer
#------------------------------------------------------------------------------
my_select_2() {
    local var=$1  title=$2  default=$3  data=$4  menu=$5  def_str=$6

    if [ -n "$def_str" ]; then
        def_str="($(pqq $def_str))"
    else
        def_str=$"entry"
    fi

    # Press <Enter> for the default entry
    local enter=$"Enter"
    # Press <Enter> for the default entry
    local press_enter=$"Press <%s> for the default %s"
    local p2 def_prompt=$(printf "$press_enter" "$(pqq "$enter")" "$def_str")

    local quit=$"quit"
    [ -n "$BACK_TO_MAIN" ] && quit=$BACK_TO_MAIN

    if [ "$HAVE_MAN" ]; then
        # Use <h> for help, <q> to <go back to main menu>
        local for_help=$"Use %s for help, %s to %s"
        p2=$(printf "$for_help" "'$(pqq h)'" "'$(pqq q)'" "$quit")
    else
        # Use <q> to <go back to main menu>
        local use_x=$"Use %s to %s"
        p2=$(printf "$use_x" "'$(pqq q)'" "$quit")
    fi

    echo

    local val input_1 input err_msg
    while [ -z "$val" ]; do

        echo -e "$quest_co$title$nc_co"

        echo -en "$menu" | colorize_menu
        [ "$err_msg" ] && printf "$err_co%s$nc_co\n" "$err_msg"
        [ "$default" ] && printf "$m_co%s$nc_co\n" "$quest_co$def_prompt$nc_co"
        [ "$p2" ]      && quest "$p2\n"

        local  input= input_1=

        while true; do
            err_msg=
            local orig_IFS=$IFS
            local IFS=
            read -n1 input_1
            IFS=$orig_IFS
            case $input_1 in
                "") input=         ; break  ;;
              [qQ]) input=$input_1 ; break  ;;
              [hH]) input=$input_1 ; break  ;;
             [0-9]) echo -ne "\b"
                    read -ei "$input_1" input
                    break  ;;
                 *) quest " %s\n" $"Opps.  Please try again" ;;
            esac

        done

        # Evaluate again in case of backspacing
        case $input in
             [qQ]*) if [ -n "$BACK_TO_MAIN" ]; then
                        eval $var=quit
                        echo
                        return
                    else
                        final_quit ; continue
                    fi ;;
             [hH]*) if [ "$HAVE_MAN" ]; then
                        man "$MAN_PAGE" ; echo ; continue
                    fi;;
        esac

        [ -z "$input" -a -n "$default" ] && input=$default
        if ! echo "$input" | grep -q "^[0-9]\+$"; then
            err_msg=$"You must enter a number"
            [ "$default" ] && err_msg=$"You must enter a number or press <Enter>"
            continue
        fi

        # Note the initrd text menus assume no : in the payload hence the cut
        #val=$(echo -e "$data" | sed -n "s/^$input://p" | cut -d: -f1)
        val=$(echo -e "$data" | sed -n "s/^$input://p")

        if [ -z "$val" ]; then
            local out_of_range=$"The number '%s' is out of range"
            err_msg=$(printf "$out_of_range" "$(pqe $input)")
            continue
        fi
        # FIXME!  is this always right?
        [ "$val" = 'default' ] && val=
        eval $var=\$val

        [ "$VERBOSE_SELECT" ] && printf "ANS: $input: $val\n" >> $LOG_FILE
        break
    done
}

#------------------------------------------------------------------------------
#  See if a man page exists.  Search locally first then try the man commmand.
#------------------------------------------------------------------------------
find_man_page() {

    local man_page  dir  me  me2=$(basename $0 .sh)
    [ "$me2" = "$ME" ] && me2=""

    HAVE_MAN=
    for me in $ME $me2; do
        for dir in "$MY_DIR/" "$MY_DIR/man/" ""; do
            man_page=$dir$me$ext.1
            test -r "$man_page" || continue
            HAVE_MAN=true
            break
        done

        [ "$HAVE_MAN" ] && break
        man -w $me &>/dev/null || continue
        HAVE_MAN=true
        break
    done

    if [ "$HAVE_MAN" ]; then
        MAN_PAGE=$man_page
        echo "Found man page: $man_page" >> $LOG_FILE
    else
        echo "No man page found" >> $LOG_FILE
    fi
}

#------------------------------------------------------------------------------
# The final quit menu after a 'q' has been detected
#------------------------------------------------------------------------------
final_quit() {
    local input
    echo
    quest $"Press '%s' again to quit" "$(pqq q)"
    echo -n " "
    read -n1 input
    echo
    [ "$input" = 'q' ] || return

    # Don't pause on exit after 'q' 'q'
    PAUSE=$(echo "$PAUSE" | sed -r "s/(^|,)exit(,|$)/,/")
    exit 0
}

#------------------------------------------------------------------------------
# Returns true if the input only contains: numbers, commas, spaces. and dashes.
#------------------------------------------------------------------------------
is_num_range() {
    [ -n "${1##*[^0-9, -]*}" ]
    return $?
}

#------------------------------------------------------------------------------
# Convert a series of numbers, commas, spaces, and dashes into a series of
# numbers.  Commas are treated like spaces.   A dash surrounded by two numbers
# is considered to be a range of numbers.  If the first number is missing and
# <min> is given the <min> is used as the first number.  If the 2nd number is
# missing and <max> is given then <max> is used as the 2nd number.  Interior
# dashes and numbers are ignored so: 1---20---3 is the same as 1-3.
#------------------------------------------------------------------------------
num_range() {
    is_num_range "$1" ||  return

    local list=$1  min=$2   max=$3
    local list=$(echo ${list//,/ } | sed -r -e "s/\s+-/-/g" -e "s/-\s+/-/g")
    local num  found
    for num in $list; do
        if [ -n "${num##*-*}" ]; then
            case ,$found, in
                *,$num,*) continue ;;
            esac
            found=$found,$num
            echo $num
            continue
        fi

        local start=${num%%-*}
        : ${start:=$min}
        local end=${num##*-}
        : ${end:=$max}
        [ -z "$start" -o -z "$end" ] && continue
        for num in $(seq $start $end); do
            case ,$found, in
                *,$num,*) continue ;;
            esac
            found=$found,$num
            echo $num
        done
    done
    echo "$found" | tr ',' ' ' >&2
}

#------------------------------------------------------------------------------
# An interface to the text menus developed for live-init.  Each menu has .menu
# and .data files.  The format of the .data files is slightly different here.
#------------------------------------------------------------------------------
cli_text_menu() {
    local d dir  path=$MENU_PATH

    local orig_IFS=$IFS  IFS=:
    for d in $MENU_PATH; do
        test -d "$d" || continue
        dir=$d
        break
    done
    IFS=$orig_IFS

    if [ -z "$dir" ]; then
        warn "Could not find text menus"
        return 2
    fi

    local var=$1  name=$2  title=$3  blurb=$4  text_menu_val
    local dfile="$dir/$name.data"  mfile="$dir/$name.menu"

    local file
    for file in "$dfile" "$mfile"; do
        [ -r "$file" ] && continue
        warn "Missing file %s" "$file"
        return 2
    done
    [ "$blurb" ] && title="$title\n$blurb"

    local data=$(cat $dfile)
    local menu=$(cat $mfile)
    my_select_2 text_menu_val "$title" 1 "$data" "$menu\n"
    # FIXME: maybe a char other than : would be better for 2nd delimiter
    local val=${text_menu_val%%:*}
    local lab=${text_menu_val##*:}
    msg $"You chose %s" "$(pq $lab)"
    eval $var=\$val

    return 0
}

#------------------------------------------------------------------------------
# Return false if no boot dir is found so caller can handle error message
#------------------------------------------------------------------------------
find_live_boot_dir() {
    local var=$1  mp=$2  fname=$3  title=$4  min_size=${5:-$MIN_LINUXFS_SIZE}
    [ ${#title} -eq 0 ] && title=$"Please select the live boot directory"

    local find_opts="-maxdepth 2 -mindepth 2 -type f -name $fname -size +$MIN_LINUXFS_SIZE"

    local list=$(find $mp $find_opts | sed -e "s|^$mp||" -e "s|/$fname$||")
    case $(count_lines "$list") in
        0) return 1 ;;
        1) eval $var=\$list
           return 0 ;;
    esac
    local dir menu
    while read dir; do
        menu="$menu$dir$P_IFS$dir\n"
    done<<Live_Boot_Dir
$(echo "$list")
Live_Boot_Dir

    if ! q_mode gui; then
        my_select $var "$title" "$menu"
        return 0
    fi

    # Try to find the directory that has our running kernel

    need_prog "$VM_VERSION_PROG"

    local cnt=0 the_dir
    while read dir; do
       $VM_VERSION_PROG -c "$mp$dir" &>/dev/null || continue
       cnt=$((cnt + 1))
       the_dir=$dir
    done<<Live_Boot_Dir
$(echo "$list")
Live_Boot_Dir

    case $cnt in
        0) return 2 ;;
        1) eval $var=\$the_dir ;;
        *) return 3 ;;
    esac
}

#==============================================================================
# USE FIND COMMAND TO MAKE A MENU OF .iso files
# Experiemental and currently not used
#==============================================================================

#------------------------------------------------------------------------------
# Expand ~/ to the actual user's home directory (we run as root).
#------------------------------------------------------------------------------
expand_directories() {
    local dir
    for dir; do

        # Fudge ~/ so it becomes the default users' home, not root's
        case $dir in
            ~/*)  dir=$(get_user_home)/${dir#~/} ;;
        esac

        eval "dir=$dir"
        if test -d $dir; then
            echo $dir
        else
            warn "Not a directory %s" "$dir"
        fi
    done
}

#------------------------------------------------------------------------------
# Use "find" command to provide a list of .iso files.  WORK IN PROGRESS.
#------------------------------------------------------------------------------
cli_search_file() {
    local var=$1  spec=$2  dir_list=$3  max_depth=$4  max_found=${5:-20}  min_size=${6:-$MIN_ISO_SIZE}

    local _sf_input title dir_cnt invalid
    while true; do
        dir_list=$(expand_directories $dir_list)
        dir_cnt=$(echo "$dir_list" | wc -w)

        title=$(
        if [ $dir_cnt -eq 1 ]; then
            quest "Will search %s directory: %s\n"                          "$(pnq $dir_cnt)"   "$(pqq $dir_list)"
        else
            quest "Will search %s directories: %s\n"                        "$(pnq $dir_cnt)"   "$(pqq $dir_list)"
        fi
            quest "for files matching '%s' with a size of %s or greater\n"  "$(pqq $spec)"      "$(pq $min_size)"
            quest "Will search down %s directories"                         "$(pqq $max_depth)"
        )

        if [ $dir_cnt -le 0 ]; then
            while [ $dir_cnt -le 0 ]; do
                warn "No directories were found in the list.  Please try again"
                cli_get_text dir_list "Enter directories"
                dir_list=$(expand_directories $dir_list)
                dir_cnt=$(echo "$dir_list" | wc -w)
            done
            continue
        fi

        my_select _sf_input "$title" "$(select_file_menu $invalid)"
        invalid=

        # FIXME: need to make some of these entries more specfic
        case $_sf_input in
            search) ;;
              dirs) cli_get_text dir_list  "Enter directories"          ; continue ;;
             depth) cli_get_text max_depth "Enter maximum depth (1-9)"  ; continue ;;
              spec) cli_get_text spec      "Enter file specfication"    ; continue ;;
        esac

        local depth=1 dir f found found_cnt
        echo -n 'depth:'
        while [ $depth -le $max_depth ]; do
            echo -n " $depth"
            for dir in $dir_list; do
                test -d "$dir" || continue
                local args="-maxdepth $depth -mindepth $depth -type f -size +$MIN_ISO_SIZE"
                f=$(find "$dir" $args -iname "$spec" -print0 | tr '\000' '\t')
                [ ${#f} -gt 0 ] && found="$found$f"
                found_cnt=$(count_tabs "$found")
                echo -n "($found_cnt)"
                [ $found_cnt -ge $max_found ] && break
            done
            [ $found_cnt -ge $max_found ] && break
            depth=$((depth + 1))
        done
        echo

        if [ $found_cnt -eq 0 ]; then
            warn "No '%s' files were found.  Please try again" "$(pqw "$spec")"
            invalid=true
            continue
        fi
        if [ $found_cnt -gt $max_found ]; then
            warn "Found %s files at depth %s.  Only showing the %s most recent." \
                $(pqh $found_cnt) $(pqh $depth) $(pqh $max_found)
        fi

        found=$(echo "$found" | tr '\t' '\000' | xargs -0 ls -dt1  2>/dev/null | head -n$max_found)

        cli_choose_file _sf_input "Please select a file" "$found" "$dir_list"
        case $_sf_input in
            retry) continue ;;
        esac

        eval $var=\$_sf_input
        return
    done
}

#------------------------------------------------------------------------------
# Present a menu of files to choose from include, name, size, and date
#------------------------------------------------------------------------------
cli_choose_file() {
    local var=$1  title=$2  file_list=$3  dir_list=$4  one_dir orig_IFS=$IFS
    [ -n "${dir_list##* *}" ] && one_dir="$dir_list/"
    local ifmt="%s$K_IFS%s$K_IFS%s$K_IFS%s"
    local file name size date w1=5  w2=5  data  first
    while read file; do
        [ ${#file} -gt 0 ] || continue
        test -f "$file"    || continue
        : ${first:=$(basename "$file")}

        name=$(_file_name "$file" "$one_dir")
        size=$(_file_size "$file")
        date=$(_file_date "$file")
        [ $w1 -lt ${#name} ] && w1=${#name}
        [ $w2 -lt ${#size} ] && w2=${#size}
        data="$data$(printf "$ifmt" "$file" "$name" "$size" "$date")\n"
    done<<File_Menu
$(echo -e "$file_list")
File_Menu

    local fmt="%s$P_IFS$fname_co%-${w1}s$num_co %${w2}s$date_co %s$nc_co"
    local IFS=$K_IFS menu
    while read file name size date; do
        menu="$menu$(printf "$fmt" "$file" "$name" "$size" "$date")\n"
    done <<File_Menu_2
$(echo -e "$data")
File_Menu_2
    IFS=$orig_ifs

    menu="${menu}retry$P_IFS${quit_co}try again$nc_co\n"
    my_select $var "$title" "$menu" "$first"
}

#------------------------------------------------------------------------------
# Used to fill in the file date and size in a menu of files.
#------------------------------------------------------------------------------
_file_date() { date "+${DATE_FMT#+}" -d @$(stat -c %Y "$1") ;}
_file_size() { echo "$(( $(stat -c %s "$1") /1024 /1024))M" ;}

#------------------------------------------------------------------------------
# Used to fill in the file name in a menu. Try to keep it compact.
#------------------------------------------------------------------------------
_file_name() {
    local file=$1  one_dir=$2
    [ ${#one_dir} -gt 0 ] && file=$(echo "$file" | sed "s|^$one_dir||")
    file=$(echo "$file" | sed "s|^/home/|~|")

    if [ ${#file} -le 80 ]; then
        echo "$file"
        return
    fi
    local base=$(basename "$file")  path=$(dirname "$file")

    echo "$(echo "$path" | cut -d/ -f1,2)/.../$base"
}

#------------------------------------------------------------------------------
# A menu of options for the select file menu
#------------------------------------------------------------------------------
select_file_menu() {
    local invalid=$1
    [ "$invalid" ] || printf "%s$P_IFS%s\n" "search" "Begin search"
    printf "%s$P_IFS%s\n" "dirs"   "Change directories"
    printf "%s$P_IFS%s\n" "depth"  "Change search depth"
    printf "%s$P_IFS%s\n" "spec"   "Change file specification"
}


#------------------------------------------------------------------------------
# Simple input of strings
#------------------------------------------------------------------------------
cli_get_text() {
    local var=$1  title=$2
    local input prompt=$(quest "> ")

    while true; do
        quest "$title"
        echo -en "\n$prompt"
        read -r input
        quest $"You entered: %s" "$(cq "$input")"
        YES_no $"Is this correct?" && break
    done
    eval $var=\$input
}

#==============================================================================
# End of experimental file menu section
#==============================================================================

#------------------------------------------------------------------------------
# Allow user to enter a filename with tab completion
#------------------------------------------------------------------------------
cli_get_filename() {
    local var=$1  title=$2  preamb=$(sub_user_home "$3")
    local file

    while true; do
        quest "$title$nc_co\n$quest_co%s\n" $"(tab completion is enabled)"
        read -e -i "$preamb" file
        preamb=$file
        if ! test -f "$file"; then
            warn $"%s does not appear to be a file" "$file"
            YES_no $"Try again?" && continue
        fi
        quest $"You entered: %s" "$(cq "$file")"
        YES_no $"Is this correct?" && break
    done
    eval $var=\$file
}

#------------------------------------------------------------------------------
# Create the source menu for live-usb-maker
# Contains an entry for cloning running live-usb (if applicable)
# Next an entry for entering file name
# Then lists of live-usbs to clone and live-cd/dvds to copy
#------------------------------------------------------------------------------
cli_live_usb_src_menu() {
    local exclude=$1
    local dev_w=$(get_lsblk_field_width name  --include="$MAJOR_SD_DEV_LIST,$MAJOR_SR_DEV_LIST")
    local lab_w=$(get_lsblk_field_width label --include="$MAJOR_SD_DEV_LIST,$MAJOR_SR_DEV_LIST")
    local size_w=6  fs_w=8

    # Japanese: Please don't translate these: Device, Size, Filesystem, Label, Model
    local dev_str=$"Device"  size_str=$"Size"  fs_str=$"Filesystem" lab_str=$"Label" mod_str=$"Model"
    [ $dev_w  -lt ${#dev_str}  ] &&  dev_w=${#dev_str}
    [ $fs_w   -lt ${#fs_str}   ] &&   fs_w=${#fs_str}
    [ $size_w -lt ${#size_str} ] && size_w=${#size_str}

    local live_dev
    if its_alive; then
        live_dev=$(get_live_dev)
        # [A clone is different from a copy, with clone we make a fresh new system]
        is_mountpoint $LIVE_MP && printf "clone$P_IFS%s (%s)\n" $"Clone this live system" "$(pq $live_dev)"
    fi

    printf "iso-file$P_IFS%s\n" $"Copy from an ISO file"
    local  fmt="%s$P_IFS$dev_co%-${dev_w}s$num_co %${size_w}s$fs_co %${fs_w}s$lab_co %-${lab_w}s$nc_co %s\n"
    local hfmt="%s$P_IFS$head_co%s %s %s %s %s$nc_co\n"

    menu=$(cli_cdrom_menu "dev=$fmt" $lab_w ; cli_partition_menu "clone=$fmt" $lab_w "$live_dev" $exclude)
    if [ $(count_lines "$menu") -gt 0 ]; then
        printf "$hfmt" "" "$(rpad $dev_w "$dev_str")" "$(lpad $size_w "$size_str")" \
            "$(lpad $fs_w "$fs_str")" "$(rpad $lab_w "$lab_str")" "$mod_str"

        echo -e "$menu"
    fi
}

#------------------------------------------------------------------------------
# Menu items of cdroms and dvds
#------------------------------------------------------------------------------
cli_cdrom_menu() {
    local fmt=$1  lab_w=$2
    local opts="--nodeps --include=$MAJOR_SR_DEV_LIST"
    local model=$(bq cd/dvd disc)
    local NAME SIZE FSTYPE LABEL
    while read line; do
        eval "$line"
        [ ${#LABEL} -gt 0 ] || continue
        printf "$fmt" "$prefix$NAME" "$NAME" "$SIZE" "$FSTYPE" "$(rpad $lab_w "$LABEL")" "$model"
    done<<Cdrom_Menu
$(lsblk -no name,size,fstype,label --pairs $opts)
Cdrom_Menu
}

#------------------------------------------------------------------------------
# Menu items of usb partitions to clone
#------------------------------------------------------------------------------
cli_partition_menu() {
    local fmt=$1  lab_w=$2  exclude=$(get_drive ${3##*/}) exclude2=$(get_drive ${4##*/})
    local dev_list=$(lsblk -lno name --include="$MAJOR_SD_DEV_LIST")
    local range=1
    force partition && range=$(seq 1 20)

    local SIZE MODEL VENDOR FSTYPE label dev_info part_num
    for dev in $dev_list; do
        [ "$dev" = "$exclude" -o "$dev" = "$exclude2" ] && continue
        force usb || is_usb_or_removable "$dev" || continue
        local dev_info=$(lsblk -no vendor,model /dev/$dev)
        for part_num in $range; do
            local part=$(get_partition "$dev" $part_num)
            local device=/dev/$part
            test -b $device || continue
            local line=$(lsblk -no size,model,vendor,label,fstype --pairs $device)
            eval "$line"
            label=$(lsblk -no label $device)
            printf "$fmt" "$part" "$part" "$SIZE" "$FSTYPE" "$(rpad $lab_w "$label")" "$(echo $dev_info)"
        done
    done
}

#------------------------------------------------------------------------------
# Menu of usb drives
#------------------------------------------------------------------------------
cli_drive_menu() {
    local exclude=$(get_drive ${1##*/}) exclude_2=$(get_drive ${2##*/})

    local opts="--nodeps --include=$MAJOR_SD_DEV_LIST"
    local dev_width=$(get_lsblk_field_width name $opts)

    local fmt="%s$P_IFS$dev_co%-${dev_width}s$num_co %6s $m_co%s$nc_co\n"
    local NAME SIZE MODEL VENDOR dev
    while read line; do
        [ ${#line} -eq 0 ] && continue
        eval "$line"
        dev=/dev/$NAME

        force usb || is_usb_or_removable "$dev" || continue
        [ "$NAME" = "$exclude"   ] && continue
        [ "$NAME" = "$exclude_2" ] && continue

        printf "$fmt" "$NAME" "$NAME" "$SIZE" "$(echo $VENDOR $MODEL)"
    done<<Ls_Blk
$(lsblk -no name,size,model,vendor --pairs $opts)
Ls_Blk
}

#------------------------------------------------------------------------------
# Offer to check the md5sum if $file.md5 exists.
#------------------------------------------------------------------------------
check_md5() {
    local file=$1 md5_file="$1.md5"
    test -f "$md5_file" || return
    q_mode gui && return
    yes_NO $"Check md5 of the file %s?" "$(basename "$file")" || return
    Msg $"Checking md5 ..."
    (cd "$(dirname "$md5_file")" && md5sum -c "$(basename "$md5_file")") && return
    yes_NO $"Keep going anyway?" || my_exit 0
}

#------------------------------------------------------------------------------
# Get the width of a single lsblk output.  Used for making things line up
# in neat columns.
#------------------------------------------------------------------------------
get_lsblk_field_width() {
    local name=$1  field fwidth width=0 ; shift
    while read field; do
        fwidth=${#field}
        [ $width -lt $fwidth ] && width=$fwidth
    done<<Get_Field_Width
$(lsblk --output $name --list $*)
Get_Field_Width
    echo $width
}

#==============================================================================
# Kernel Tables!
#==============================================================================
#===============================================================================
# Kernel utilities
#
# These create and work with lists of kernels of the form:
#
#   version|fname|date
#
#===============================================================================

#------------------------------------------------------------------------------
# get_all_kernel      construct a list of all kernel files in a directory
#
# get_kernel_version  extract a list of versions, fnames, or dates from a
# get_kernel_fname    list of kernels
# get_kernel_date
#
# count_lines         Count lines in a variable, number of kernels in a list
#------------------------------------------------------------------------------
get_all_kernel() {
    local  var=$1 temp ; shift
    temp=$($VM_VERSION_PROG -nsr --delimit="$K_IFS" "$@") \
        || fatal $"The %s program failed!" "$VM_VERSION_PROG"

    eval $var=\$temp
}

get_kernel_version()  { echo "$1" | cut -d"$K_IFS" -f1                      ;}
get_kernel_fname()    { echo "$1" | cut -d"$K_IFS" -f2                      ;}
get_kernel_date()     { echo "$1" | cut -d"$K_IFS" -f"1,2" --complement     ;}
count_lines()         { echo "$1" | grep -c .                               ;}
count_nulls()         { echo "$1" | tr -cd '\000' | tr '\000' 'x' | wc -c   ;}
count_tabs()          { echo "$1" | tr -cd '\t'   | tr '\t' 'x'   | wc -c   ;}

#------------------------------------------------------------------------------
# Get kernels from a list that match the version expression
# FIXME: escape escape escape!
#------------------------------------------------------------------------------
find_kernel_version() {
    local version=$1  list=$2 ;  shift 2
    echo "$list" | egrep "$@" "^($version)[$K_IFS]"
}

#------------------------------------------------------------------------------
# Get kernels from a list that match the fname expression
#------------------------------------------------------------------------------
find_kernel_fname()   {
    local fname=$1  list=$2 ; shift 2
    echo "$list" | egrep "$@" "^[^$K_IFS]*[$K_IFS]($fname)[$K_IFS]"
}

#------------------------------------------------------------------------------
# Throw a fatal error if there are zero lines in "$1"
#------------------------------------------------------------------------------
fatal_k0() {
    local cnt=$(count_lines "$1") ; shift
    fatal_0 $cnt "$@"
}

#------------------------------------------------------------------------------
# Present a menu for user to select a kernel.  the list input should be the
# output of: "vmlinuz-version -nsd : <files>" or something like that.  You
# can set the delimiter with a 4th argument but it must be a single character
#
# This is the two column version:  Version  Date
#------------------------------------------------------------------------------
select_kernel_2() {
    local title=$1 var=$2 list=$3  orig_ifs=$IFS
    IFS=$K_IFS

    # Get field widths
    local f1 f2 f3  w1=5
    while read f1 f2 f3; do
        [ $w1 -lt ${#f1} ] && w1=${#f1}
    done<<Widths
$(echo "$list")
Widths

    local fmt="$version_co%-${w1}s $date_co%s$nc_co\n"
    local hfmt="$head_co%s %s$nc_co\n"
    local file=$"File"  version=$"Version"  date=$"Date"
    local data="$P_IFS$(printf "$hfmt" "$(rpad $w1 "$version")" "$date")\n"

    local payload
    while read f1 f2 f3; do
        [ ${#f1} -gt 0 ] || continue
        payload="$f1$IFS$f2$IFS$f3"
        data="$data$payload$P_IFS$(printf "$fmt" "$f1" "$f3")\n"
    done<<Print
$(echo "$list")
Print

    IFS=$orig_ifs

    my_select $var "$title" "$data" ""
}

#------------------------------------------------------------------------------
# This is the three column version:  Fname Version  Date
# NOTE: not used, therefore not recently tested
#------------------------------------------------------------------------------
select_kernel_3() {
    local title=$1 var=$2 list=$3  orig_ifs=$IFS
    IFS=$K_IFS

    # Japanese: please do not translate: File, Version, Date
    local file=$"File"  version=$"Version"  date=$"Date"
    # Get field widths
    local file_w=${#files}  ver_w=${#version}
    local f1 f2 f3  w1=5 w2=5

    while read f1 f2 f3; do
        [ $ver_w -lt ${#f1}  ] &&  ver_w=${#f1}
        [ $file_w -lt ${#f2} ] && file_w=${#f2}
    done<<Widths
$(echo "$list")
Widths

    local fmt="$fname_co%-${file_w}s $version_co%-${ver_w}s $date_co%-s$nc_co"
    local hfmt="$head_co%s %s %-s$nc_co\n"
    local data="$P_IFS$(printf "$hfmt" "$(rpad "$file")" "$(rpad "$version")" "$date")\n"
    local payload
    while read f1 f2 f3; do
        [ ${#f1} -gt 0 ] || continue
        payload="$f1$IFS$f2$IFS$f3"
        data="$data$payload$P_IFS$(printf "$fmt" "$f2" "$f1" "$f3")\n"
    done<<Print
$(echo "$list")
Print

    IFS=$orig_ifs

    my_select $var "$title" "$data"
}

#------------------------------------------------------------------------------
# Display a 2-Column table (version, date) of a list of kernels
#------------------------------------------------------------------------------
show_kernel_2() {
    local title=$1  list=$2  orig_ifs=$IFS
    IFS=$K_IFS

    echo
    [ "$title" ] && echo "$m_co$title$nc_co"

    # Get field widths
    local  f1 f2 f3  w1=5
    while read f1 f2 f3; do
        [ $w1 -lt ${#f1} ] && w1=${#f1}
    done<<Widths
$(echo "$list")
Widths

    local file=$"File"  version=$"Version"  date=$"Date"
    local  fmt=" $version_co%-${w1}s $date_co%s$nc_co\n"
    local hfmt=" $head_co%s %s$nc_co\n"
    printf "$hfmt" "$(rpad $w1 "$version")" "$date"
    while read  f1 f2 f3; do
        [ ${#f1} -gt 0 ] || continue
        printf "$fmt" "$f1" "$f3"
    done<<Print
$(echo "$list")
Print
    IFS=$orig_ifs
}

#------------------------------------------------------------------------------
# Show a 3-column table of a list of kernels (fname, version, date)
#------------------------------------------------------------------------------
show_kernel_3() {
    local title=$1  list=$2  orig_ifs=$IFS
    IFS=$K_IFS

    local file=$"File"  version=$"Version"  date=$"Date"
    local file_w=${#file}  ver_w=${#version}

    echo
    [ "$title" ] && echo "$m_co$title$nc_co"

    # Get field widths
    local f1 f2 f3  w1=5 w2=5
    while read f1 f2 f3; do
        [ $ver_w  -lt ${#f1} ] &&  ver_w=${#f1}
        [ $file_w -lt ${#f2} ] && file_w=${#f2}
    done<<Widths
$(echo "$list")
Widths

    local fmt=" $fname_co%-${file_w}s $version_co%-${ver_w}s $date_co%-s$nc_co\n"
    local hfmt=" $head_co%s %s %-s$nc_co\n"
    printf "$hfmt" "$(rpad $file_w "$file")" "$(rpad $ver_w "$version")" "$date"
    while read f1 f2 f3; do
        [ ${#f1} -gt 0 ] || continue
        printf "$fmt" "$f2" "$f1" "$f3"
    done<<Print
$(echo "$list")
Print

    IFS=$orig_ifs
}

#------------------------------------------------------------------------------
# Show a  special 5-column list of kernels:
#  label, version, date, from-fname, to-fname
#------------------------------------------------------------------------------
kernel_stats() {
    local ifs=$K_IFS orig_ifs=$IFS
    IFS=$ifs

    local list
    while [ $# -ge 5 ]; do
        list="$list$1$IFS$2$IFS$3$IFS$4$IFS$5\n"
        shift 5
    done

    # [We will convert from kernel "From" to kernel "To"]
    # Japanese: please don't translate: Version, Date, From, To
    local version=$"Version" date=$"Date"  from=$"From"  to=$"To"
    local w1=5  w2=${#version}  w3=${#date}  w4=${#from}
    # Get field widths
    local f1 f2 f3 f4 f5
    while read f1 f2 f3 f4 f5; do
        [ ${#f1} -gt 0   ] || continue
        [ $w1 -lt ${#f1} ] && w1=${#f1}
        [ $w2 -lt ${#f2} ] && w2=${#f2}
        [ $w3 -lt ${#f3} ] && w3=${#f3}
        [ $w4 -lt ${#f4} ] && w4=${#f4}
    done<<Widths
$(echo -e "$list")
Widths

    local hfmt=" $head_co%s %s  %s  %s %s$nc_co\n"
    local  fmt=" $lab_co%s $version_co%s  $date_co%s  $fname_co%s %s$nc_co\n"
    f1=$(lpad $w1 "")
    f2=$(rpad $w2 "$version")
    f3=$(rpad $w3 "$date")
    f4=$(rpad $w4 "$from")
    printf "$hfmt" "$f1" "$f2" "$f3" "$f4" "$to"

    while read f1 f2 f3 f4 f5; do
        [ ${#f1} -gt 0 ] || continue
        f1=$(lpad $w1 "$f1")
        f2=$(rpad $w2 "$f2")
        f3=$(rpad $w3 "$f3")
        f4=$(rpad $w4 "$f4")
        printf "$fmt" "$f1" "$f2" "$f3" "$f4" "$f5"
    done<<Print
$(echo -e "$list")
Print

    IFS=$orig_ifs
}

#------------------------------------------------------------------------------
# Make a table of values for displaying the partitions on a target usb device
#------------------------------------------------------------------------------
usb_stats() {
    local orig_ifs=$IFS
    local IFS=$K_IFS

    local list
    while [ $# -ge 4 ]; do
        list="$list$1$IFS$2$IFS$3$IFS$4\n"
        shift 4
    done

    # Space in a drive or partition: Total = Used + Extra
    # Japanese: please don't translate: Total, Used, Extra
    local total=$"Total"  allocated=$"Used"  extra=$"Extra"
    local w1=5 w2=${#total} w3=${#allocated} w4=${#extra}
    # Get field widths
    local f1 f2 f3 f4
    while read f1 f2 f3 f4; do
        f2=$(add_commas $f2)
        f3=$(add_commas $f3)
        f4=$(add_commas $f4)

        [ ${#f1} -gt 0 ] || continue
        [ $w1 -lt ${#f1} ] && w1=${#f1}
        [ $w2 -lt ${#f2} ] && w2=${#f2}
        [ $w3 -lt ${#f3} ] && w3=${#f3}
        [ $w4 -lt ${#f4} ] && w4=${#f4}
    done<<Widths
$(echo -e "$list")
Widths

    local hfmt=" $head_co%s  %s  %s  %s$nc_co\n"
    local  fmt=" $lab_co%s  $num_co%s  %s  %s$m_co  MiB$nc_co\n"
    f1=$(lpad $w1 "")
    f2=$(lpad $w2 "$total")
    f3=$(lpad $w3 "$allocated")
    f4=$(lpad $w4 "$extra")

    printf "$hfmt" "$f1" "$f2" "$f3" "$f4"

    while read f1 f2 f3 f4; do
        f2=$(add_commas $f2)
        f3=$(add_commas $f3)
        f4=$(add_commas $f4)

        [ ${#f1} -gt 0 ] || continue
        f1=$(lpad $w1 "$f1")
        f2=$(lpad $w2 "$f2")
        f3=$(lpad $w3 "$f3")
        f4=$(lpad $w4 "$f4")
        printf "$fmt" "$f1" "$f2" "$f3" "$f4" | color_commas
    done<<Print
$(echo -e "$list")
Print

    IFS=$orig_ifs
}

#------------------------------------------------------------------------------
# NOT USED.  See below.
#------------------------------------------------------------------------------
free_space_menu() {
    local min_percent=$1  total_size=$2  comma_size=$(add_commas $2)
    local w2=${#comma_size}
    local fmt="%s$P_IFS$hi_co %3s%%$num_co %${w2}s $m_co%s$nc_co\n"
    local size=100 free_size free_percent
    while [ $size -ge $min_percent ]; do
        free_percent=$((100 - size))
        free_size=$((free_percent * total_size / 100))
        printf "$fmt" "$size" "$free_percent" "$(add_commas $free_size)" MiB | color_commas
        [ $size -eq $min_percent ] && break
        size=$((size - 5))
        [ $size -lt $min_percent ] && size=$min_percent
    done
}

#------------------------------------------------------------------------------
# Create a menu of sizes if user wants to use lees than all of a usb device
#------------------------------------------------------------------------------
partition_size_menu() {
    local min_percent=$1  total_size=$2  comma_size=$(add_commas $2)
    local w2=${#comma_size}
    local fmt="%s$P_IFS$hi_co %3s%%$num_co %${w2}s $m_co%s$nc_co\n"
    local percent=100 size
    while [ $percent -ge $min_percent ]; do
        size=$((percent * total_size / 100))
        printf "$fmt" "$percent" "$percent" "$(add_commas $size)" MiB | color_commas
        [ $percent -eq $min_percent ] && break
        percent=$((percent - 5))
        [ $percent -lt $min_percent ] && percent=$min_percent
    done
}

#==============================================================================
# Fun with Colors!  (and align unicode test)
#
#==============================================================================
#------------------------------------------------------------------------------
# Defines a bunch of (lowercase!) globals for colors.  In some versions, $noco
# and $loco are used to control what colors get assigned, if any.
#------------------------------------------------------------------------------
set_colors() {
    local color=${1:-high}

    local e=$(printf "\e")

    rev_co="$e[7m" ; nc_co="$e[0m"

    if [ "$color" = 'off' ]; then

         black=  ;    blue=  ;    green=  ;    cyan=  ;
           red=  ;  purple=  ;    brown=  ; lt_gray=  ;
       dk_gray=  ; lt_blue=  ; lt_green=  ; lt_cyan=  ;
        lt_red=  ; magenta=  ;   yellow=  ;   white=  ;
         brown=  ;

         inst_co=            ;  mark_co=           ;     grep_co=
         bold_co=            ;    fs_co=           ;      num_co=            ;
         date_co=            ;  head_co=           ;    quest_co=            ;
          dev_co=            ;    hi_co=           ;     quit_co=            ;
          err_co=            ;   lab_co=           ;  version_co=            ;
        fname_co=            ;     m_co=           ;     warn_co=            ;
         return
     fi

         black="$e[30m"   ;    blue="$e[34m"   ;    green="$e[32m"   ;    cyan="$e[36m"   ;
           red="$e[31m"   ;  purple="$e[35m"   ;    brown="$e[33m"   ; lt_gray="$e[37m"   ;
       dk_gray="$e[1;30m" ; lt_blue="$e[1;34m" ; lt_green="$e[1;32m" ; lt_cyan="$e[1;36m" ;
        lt_red="$e[1;31m" ; magenta="$e[1;35m" ;   yellow="$e[1;33m" ;   white="$e[1;37m" ;
         nc_co="$e[0m"    ;   brown="$e[33m"   ;   rev_co="$e[7m"    ;    gray="$e[37m"

    case $color in
        high)
         inst_co=$lt_cyan    ;  mark_co=$rev_co    ;     grep_co="1;35"
         bold_co=$yellow     ;    fs_co=$lt_blue   ;      num_co=$magenta    ;
         date_co=$lt_cyan    ;  head_co=$white     ;    quest_co=$lt_green   ;
          dev_co=$white      ;    hi_co=$white     ;     quit_co=$yellow     ;
          err_co=$red        ;   lab_co=$lt_cyan   ;  version_co=$white      ;
        fname_co=$white      ;     m_co=$lt_cyan   ;     warn_co=$yellow     ; ;;

        dark)
         inst_co=$cyan       ;  mark_co=$rev_co    ;     grep_co="1;34"
         bold_co=$brown      ;    fs_co=$lt_blue   ;      num_co=$brown   ;
         date_co=$cyan       ;  head_co=$gray      ;   quest_co=$green    ;
          dev_co=$gray       ;    hi_co=$gray      ;    quit_co=$brown    ;
          err_co=$red        ;   lab_co=$cyan      ;  version_co=$gray    ;
        fname_co=$gray       ;     m_co=$cyan      ;     warn_co=$brown   ; ;;

        low)
         inst_co=$cyan       ;  mark_co=$rev_co    ;     grep_co="1;34"
         bold_co=$white      ;    fs_co=$gray      ;      num_co=$white      ;
         date_co=$gray       ;  head_co=$white     ;    quest_co=$lt_green   ;
          dev_co=$white      ;    hi_co=$white     ;     quit_co=$lt_green   ;
          err_co=$red        ;   lab_co=$gray      ;  version_co=$white      ;
        fname_co=$white      ;     m_co=$gray      ;     warn_co=$yellow     ; ;;

        low2)
         inst_co=$cyan       ;  mark_co=$rev_co    ;     grep_co="1"
         bold_co=$white      ;    fs_co=$gray      ;      num_co=$white      ;
         date_co=$gray       ;  head_co=$white     ;    quest_co=$green      ;
          dev_co=$white      ;    hi_co=$white     ;     quit_co=$green      ;
          err_co=$red        ;   lab_co=$gray      ;  version_co=$white      ;
        fname_co=$white      ;     m_co=$gray      ;     warn_co=$yellow     ; ;;

        bw)
         inst_co=$white      ;  mark_co=$rev_co    ;     grep_co="1;37"
         bold_co=$white      ;    fs_co=$gray      ;      num_co=$white      ;
         date_co=$gray       ;  head_co=$white     ;    quest_co=$white      ;
          dev_co=$white      ;    hi_co=$white     ;     quit_co=$white      ;
          err_co=$white      ;   lab_co=$lt_gray   ;  version_co=$lt_gray    ;
        fname_co=$white      ;     m_co=$gray      ;     warn_co=$white      ; ;;

        *)
            error "Unknown color parameter: %s" "$color"
            fatal "Expected high, low. low2, bw, dark, or off" ;;
    esac
}

#------------------------------------------------------------------------------
# These are designed to "quote" strings with colors so there is always a
# leading color, all the args, and then a trailing color.  This is easier and
# more compact that using colors as strings.
#------------------------------------------------------------------------------
pq()  { echo "$hi_co$*$m_co"      ;}
vq()  { echo "$version_co$*$m_co" ;}
pqq() { echo "$hi_co$*$quest_co"  ;}
bqq() { echo "$bold_co$*$quest_co";}
pnq() { echo "$num_co$*$quest_co" ;}
pnh() { echo "$num_co$*$hi_co"    ;}
pqw() { echo "$warn_co$*$hi_co"   ;}
pqe() { echo "$hi_co$*$err_co"    ;}
pqh() { echo "$m_co$*$hi_co"      ;}
pqb() { echo "$m_co$*$bold_co"    ;}
bq()  { echo "$bold_co$*$m_co"    ;}
hq()  { echo "$bold_co$*$m_co"    ;}
cq()  { echo "$hi_co$*$m_co"      ;}
nq()  { echo "$num_co$*$m_co"     ;}

#------------------------------------------------------------------------------
# Intended to add colors to menus used by my_select_2() menus.
#------------------------------------------------------------------------------
colorize_menu() {
    sed -r -e "s/(^| )([0-9]+)\)/\1$quest_co\2$hi_co)$m_co/g" \
        -e "s/\(([^)]+)\)/($hi_co\1$m_co)/g" -e "s/\*/$bold_co*$m_co/g" -e "s/$/$nc_co/"
}

#------------------------------------------------------------------------------
# Pad a (possibly unicode) string on the RIGHT so it is total length $width.
# Unfortunately printf is problem with multi-byte unicode but wc -m is not.
#------------------------------------------------------------------------------
rpad() {
    local width=$1  str=$2
    local pad=$((width - ${#str}))
    [ $pad -le 0 ] && pad=0
    printf "%s%${pad}s" "$str" ""
}

#------------------------------------------------------------------------------
# Same as above but pad on the LEFT.
#------------------------------------------------------------------------------
lpad() {
    local width=$1  str=$2
    local pad=$((width - ${#str}))
    [ $pad -le 0 ] && pad=0
    printf "%${pad}s%s" "" "$str"
}

#------------------------------------------------------------------------------
# Remove all ANSI color escape sequences that are created in set_colors().
# This is NOT a general purpose routine for removing all ANSI escapes.
#------------------------------------------------------------------------------
strip_color() {
    local e=$(printf "\e")
    sed -r -e "s/$e\[[0-9;]+[mK]//g"
}

#==============================================================================
# Messages, Warnings and Errors
#
#==============================================================================

#------------------------------------------------------------------------------
# Show and log a message string.  Disable display if QUIET
#------------------------------------------------------------------------------
msg() {
    local fmt=$1 ; shift
    prog_log "$fmt" "$@"
    [ -z "$QUIET" ] && printf "$m_co$fmt$nc_co\n" "$@"
}

#------------------------------------------------------------------------------
# Convenience routine: show message if cnt is 1.
#------------------------------------------------------------------------------
msg_1() {
    local cnt=$1 ; shift
    [ "$cnt" -eq 1 ] || return
    msg "$@"
}

#------------------------------------------------------------------------------
# Like msg() but not disabled by QUIET
#------------------------------------------------------------------------------
Msg() {
    local fmt=$1 ; shift
    prog_log_echo "$m_co$fmt$nc_co" "$@"
}

#------------------------------------------------------------------------------
# Like Msg() but in bold
#------------------------------------------------------------------------------
Shout() {
    local fmt=$1 ; shift
    prog_log_echo "$bold_co$fmt$nc_co" "$@"
}

#------------------------------------------------------------------------------
# Convenience routine for printing a pretty title
#------------------------------------------------------------------------------
shout_title() {
    echo "$m_co$BAR_80$nc_co"
    printf "\n=====> " >>$LOG_FILE
    shout "$@"
    echo "$m_co$BAR_80$nc_co"
}

#------------------------------------------------------------------------------
# Convenience routine for printing a pretty sub-title
#------------------------------------------------------------------------------
shout_subtitle() {
    echo "$m_co$SBAR_80$nc_co"
    printf "\n=====> " >>$LOG_FILE
    shout "$@"
    echo "$m_co$SBAR_80$nc_co"
}

#------------------------------------------------------------------------------
# Like msg() but in bold
#------------------------------------------------------------------------------
shout() {
    local fmt=$1 ; shift
    prog_log "$fmt" "$@"
    [ -z "$QUIET" ] && printf "$bold_co$fmt$nc_co\n" "$@"
}

#------------------------------------------------------------------------------
# Run a command and send output to screen and log file
#------------------------------------------------------------------------------
log_it() {
    local msg=$("$@")
    echo "$msg"
    echo "$msg" 2>&1 | strip_color >> $LOG_FILE
}

#------------------------------------------------------------------------------
# Run a command and send output to log file.  Only send to screen if not quiet
#------------------------------------------------------------------------------
log_it_q() {
    local msg=$("$@")
    [ -z "$QUIET" ] && echo "$msg"
    echo "$msg" 2>&1 | strip_color >> $LOG_FILE
}

#------------------------------------------------------------------------------
# Throw a fatal error.  There is some funny business to include a question in
# the error log that may need to be tweaked or changed.
#------------------------------------------------------------------------------
fatal() {
    local code

    if echo "$1" | grep -q "^[0-9]\+$"; then
        EXIT_NUM=$1 ; shift
    fi

    if echo "$1" | grep -q "^[a-z-]*$"; then
        code=$1 ; shift
    fi

    local fmt=$1 ; shift

    prog_log_echo "${err_co}%s:$hi_co $fmt$nc_co"   $"Error" "$@" >&2
    fmt=$(echo "$fmt" | sed 's/\\n/ /g')
    if [ -n "$ERR_FILE" ]; then
        printf "$code:$fmt\n" "$@" | strip_color >> $ERR_FILE
        [ -n "$FATAL_QUESTION" ] && echo "Q:$FATAL_QUESTION" >> $ERR_FILE
    fi

    case $(type -t my_exit) in
        function) my_exit ${EXIT_NUM:-100} ;;
    esac

    exit ${EXIT_NUM:-100}
}

#------------------------------------------------------------------------------
# Convenience routines to throw a fatal error or warning if a variable is
# zero-length or numerically 0.
#------------------------------------------------------------------------------
fatal_z() { [ ${#1} -gt 0 ] && return;  shift;  fatal "$@" ;}
fatal_0() { [ $1    -ne 0 ] && return;  shift;  fatal "$@" ;}
warn_z()  { [ ${#1} -gt 0 ] && return;  shift;  warn  "$@" ;}
warn_0()  { [ $1    -ne 0 ] && return;  shift;  warn  "$@" ;}

#------------------------------------------------------------------------------
# Used mostly for internal errors when a case statement doesn't have a match
#------------------------------------------------------------------------------
internal_error() {
    local where=$1  ;  shift
    fatal "Internal error at %s: %s" "$where" "$*"
}

#------------------------------------------------------------------------------
# Throw a warning.
#------------------------------------------------------------------------------
warn() {
    local fmt=$1 ; shift
    prog_log_echo "${warn_co}%s:$hi_co $fmt$nc_co" $"Warning" "$@" >&2
}

#------------------------------------------------------------------------------
# Only warn if we are not in pretend mode
#------------------------------------------------------------------------------
pwarn() { [ -z "$PRETEND_MODE" ] && warn "$@" ; }

#------------------------------------------------------------------------------
# Write an error message without exiting
#------------------------------------------------------------------------------
error() {
    local fmt=$1 ; shift
    prog_log_echo "${err_co}%s:$hi_co $fmt$nc_co" $"Error" "$@" >&2
}

#------------------------------------------------------------------------------
# Display a question
#------------------------------------------------------------------------------
quest() {
    local fmt=$1 ; shift
    printf "$quest_co$fmt$nc_co" "$@"
}


#------------------------------------------------------------------------------
# Same as quest() but with trailing \n
#------------------------------------------------------------------------------
questn() {
    local fmt=$1 ; shift
    printf "$quest_co$fmt$nc_co\n" "$@"
}

#------------------------------------------------------------------------------
# Progress, log, and echo.
# printf a string then send it on to be output to the log file and to the
# progress file.
#------------------------------------------------------------------------------
prog_log_echo()  {
    local fmt="$1" ;  shift;
    printf "$fmt\n" "$@"
    prog_log "$fmt" "$@"
}


#------------------------------------------------------------------------------
# Printf a string to the log file and maybe to the progress file.
# Note: $PROG_FILE is set to /dev/null to disable the progress file.
#------------------------------------------------------------------------------
prog_log()  {
    local fmt="$1\n" ;  shift;
    printf "$fmt" "$@" | strip_color >> $LOG_FILE
    printf "$fmt" "$@" | strip_color >> $PROG_FILE
}

#==============================================================================
# TIME KEEPING AND REPORTING
#
# Very little bang for the coding buck here.  The plural() routine can't
# be easily translated. Expect some changes.
#==============================================================================
#------------------------------------------------------------------------------
# Show the time elapsed since START_T if it is greatr than 10 seconds
#------------------------------------------------------------------------------
show_elapsed() {
    [ ${#START_T} -eq 0 ] && return
    [ $START_T    -eq 0 ] && return
    local dt=$(($(date +%s) - START_T))
    [ $dt -gt 10 ] && msg "\n$ME took $(elapsed $START_T)."
    echo >> $LOG_FILE
}

#------------------------------------------------------------------------------
# Show time elapsed since time passed in as first arg
#------------------------------------------------------------------------------
elapsed() {
    local sec min hour ans

    sec=$((-$1 + $(date +%s)))

    if [ $sec -lt 60 ]; then
        plural $sec "%n second%s"
        return
    fi

    min=$((sec / 60))
    sec=$((sec - 60 * min))
    if [ $min -lt 60 ]; then
        ans=$(plural $min "%n minute%s")
        [ $sec -gt 0 ] && ans="$ans and $(plural $sec "%n second%s")"
        echo -n "$ans"
        return
    fi

    hour=$((min / 60))
    min=$((min - 60 * hour))

    plural $hour "%n hour%s"
    if [ $min -gt 0 ]; then
        local min_str=$(plural $min "%n minute%s")
        if [ $sec -gt 0 ]; then
            echo -n ", $min_str,"
        else
            echo -n " and $min_str"
        fi
    fi
    [ $sec -gt 0 ] && plural $sec " and %n second%s"
}

#------------------------------------------------------------------------------
# Get time in 1/100ths of a second since kernel booted.  The 2nd one puts the
# result in the START_TIME global which is use in msg_elapased_t() below.
#------------------------------------------------------------------------------
get_time() { cut -d" " -f22 /proc/self/stat ; }
start_timer() { START_TIME=$(cut -d" " -f22 /proc/self/stat) ; }

#------------------------------------------------------------------------------
# Not used.
#------------------------------------------------------------------------------
show_delta_t() {
    local dt=$(($(get_time) - $1))
    printf "%03d" $dt | sed -r 's/(..)$/.\1/'
}

#------------------------------------------------------------------------------
# Show MM:SS if time is 1 minute or greater, otherwise show fractional seconds
# Usg msg() to put the result in the log file, color, it etc.
#------------------------------------------------------------------------------
msg_elapsed_t() {
    local label=$1  min  sec
    local dt=$(($(get_time) - ${2:-$START_TIME}))

    if [ $dt -ge 6000 ]; then
        min=$((dt / 6000))
        sec=$(((dt - 6000 * min)/ 100))
        msg "%s took $num_co%d$m_co:$num_co%02d$m_co mm:ss" "$(pq $label)" "$min" "$sec"
        return
    fi

    sec=$(printf "%03d" $dt | sed -r 's/(..)$/.\1/')
    # <something> took <15> seconds
    msg $"%s took %s seconds" "$label" "$(nq $sec)"
}

#------------------------------------------------------------------------------
# Pluralize words in English.  WILL NOT WORK WITH TRANSLATION.
#------------------------------------------------------------------------------
plural() {
    local n=$1 str=$2
    case $n in
        1) local s=  ies=y   are=is   were=was  es= num=one;;
        *) local s=s ies=ies are=are  were=were es=es num=$n;;
    esac

    case $n in
        0) num=no ;;
    esac

    echo -n "$str" | sed -e "s/%s\>/$s/g" -e "s/%ies\>/$ies/g" \
        -e "s/%are\>/$are/g" -e "s/%n\>/$num/g" -e "s/%were\>/$were/g" \
        -e "s/%es\>/$es/g" -e "s/%3d\>/$(printf "%3d" $n)/g"
}

#==============================================================================
# Special Utilities
# These are more integrated into the overall scheme
#==============================================================================

#------------------------------------------------------------------------------
# Umount all partitions on a disk device
#------------------------------------------------------------------------------
umount_all() {
    local dev=$1  mounted

    mounted=$(mount | egrep "^$dev[^ ]*" | cut -d" " -f3 | grep .) || return 0

    # fatal "One or more partitions on device %s are mounted at: %s"
    # This makes it easier on the translators (and my validation)
    local msg="One or more partitions on device %s are mounted at"
     [ "$FORCE_UMOUNT" ] || force umount || yes_NO_fatal "umount" \
        "Do you want those partitions unmounted?" \
        "Use %s to always have us unmount mounted target partitions" \
        "$msg:\n  %s" "$dev" "$(echo $mounted)"

    sync ; sync

    local i part
    for part in $(mount | egrep -o "^$dev[^ ]*"); do
        umount --all-targets $part 2>/dev/null
    done

    mount | egrep -q "^$dev[^ ]*" || return 0

    for i in $(seq 1 10); do
        for part in $(mount | egrep -o "^$dev[^ ]*"); do
            umount $part 2>/dev/null
        done
        mount | egrep -q "^$dev[^ ]*" || return 0
        sleep .1
    done

    # Make translation and validation easier
    msg="One or more partitions on device %s are in use at"
    mounted=$(mount | egrep "^$dev[^ ]*" | cut -d" " -f3 | grep .) || return 0
    fatal "$msg:\n  %s"  "$dev" "$(echo $mounted)"
    return 1
}

#------------------------------------------------------------------------------
# Start file locking with appropriate error messages to let someone go ahead
# if the flock program is missing
#------------------------------------------------------------------------------
do_flock() {
    file=${1:-$LOCK_FILE}  me=${2:-$ME}

    HAVE_FLOCK=
    force flock && return

    if ! hash flock &> /dev/null; then
        yes_NO_fatal "flock" \
        "Do you want to continue without locking?" \
        "Use %s to always ignore this warning"     \
        "The %s program was not found." "flock" && return
        exit
    fi

    exec 18>> $file

    local pid
    while true; do

        flock -n 18 && break

        sleep 0.1

        pid=$(flock_pid $file)

        if [ ${#pid} -gt 0 ]; then
            error     $"A %s process (using PID %s) is already running" "$me" "$pid"
            fatal 101 $"Please close that process before starting a new one"
        fi

        warn "Deleting stale lock file %s" $file
        rm -f $file
        flock -n 18 && break

        fatal 101 $"Failed to obtain lock on %s" "$file"
    done

    HAVE_FLOCK=true
    echo $$ > "$file"
    return
}

#------------------------------------------------------------------------------
# Print the contents of the lock file if it is a PID of an active process.
#------------------------------------------------------------------------------
flock_pid() {
    file=${1:-$LOCK_FILE}
    local pid
    read pid >/dev/null <$file
    [ ${#pid} -gt 0 ] || return
    test -d /proc/$pid || return
    echo $pid
}

#------------------------------------------------------------------------------
# A flock routine to be called by a gui wrapper.
#------------------------------------------------------------------------------
gui_flock() {
    file=${1:-$LOCK_FILE}  me=${2:-$ME}
    HAVE_FLOCK=
    exec 18> $file
    flock -n 18 || return 1
    HAVE_FLOCK=true
    echo $$ >&18
    return 0
}

#------------------------------------------------------------------------------
# Release the flock unless we are running with --force=flock.
#------------------------------------------------------------------------------
unflock() {
    local file=${1:-$LOCK_FILE}
    force flock && return
    [ "$HAVE_FLOCK" ] && rm -f $file &>/dev/null
}

#------------------------------------------------------------------------------
# Create a nice header for the .config file.
#------------------------------------------------------------------------------
config_header() {
    local file=${1:-$CONFIG_FILE}  me=${2:-$ME}  version=${3:-$VERSION} date=${4:-$VERSION_DATE}
    cat<<Config_Header
#----------------------------------------------------------------------
# Configuration file for $me
#      Version: $version
# Version date: $date
#         File: $file
#      Created: $(date +"$DATE_FMT")
#
# Config file options:
#
#   -R --reset-config   Write fresh config file with default values
#   -W --write-config   Write config file with current (cli) options
#   -I --ignore-config  Ignore this file
#----------------------------------------------------------------------

Config_Header
}

#------------------------------------------------------------------------------
# Create a one-line footer for the confiig file
#------------------------------------------------------------------------------
config_footer() {
    echo  "#--- End of config file -----------------------------------------------"
}

#------------------------------------------------------------------------------
# Use a fancy sed command to reset the config file to the default options but
# reading directly from "$0".
#------------------------------------------------------------------------------
reset_config() {
    local file=${1:-$CONFIG_FILE}  msg=$2

    [ -n "$msg" ] || msg="Resetting config file %s"
    msg "$msg" "$(pq $file)"

    mkdir -p $(dirname "$file") || fatal "Could not create directory for config file"
    (config_header "$file" "$ME" "$VERSION" "$VERSION_DATE"
    sed -rn "/^#=+\s*BEGIN_CONFIG/,/^#=+\s*END_CONFIG/p" "$0" \
        | egrep -v "^#=+[ ]*(BEGIN|END)_CONFIG"
        config_footer ) > $file
    return 0
}

#------------------------------------------------------------------------------
# Do nothing if --ignore-config
# Otherwise if --reset-config or no existing config then reset config and exit
# Otherwise source the existing config file (if readable)
#------------------------------------------------------------------------------
read_reset_config_file() {
    local file=${1:-$CONFIG_FILE}

    [ "$IGNORE_CONFIG" ] && return

    if [ "$RESET_CONFIG" -o ! -e "$file" ]; then
        reset_config "$file" $"Creating new config file %s"
        [ "$RESET_CONFIG" ] || return
        pause exit $"Exit"
        exit 0
    else
        test -r "$file" && . "$file"
    fi
}

#------------------------------------------------------------------------------
# Show version information and then exit
#------------------------------------------------------------------------------
show_version() {
    local fmt="%20s version %s (%s)\n"
    printf "$fmt" "$ME"        "$VERSION"      "$VERSION_DATE"
    printf "$fmt" "$LIB_NAME"  "$LIB_VERSION"  "$LIB_DATE"
    exit 0
}

#==============================================================================
# General System Utilities
#
# These usually either provide a useful feature or wrap a bunch of error checks
# around standard system calls.  Some of them are for convenience.
#==============================================================================

#------------------------------------------------------------------------------
# The normal mountpoint command can fail on symlinks and in other situations.
# This is intended to be more robust. (sorry Jerry and Gaer Boy!)
#------------------------------------------------------------------------------
is_mountpoint() {
    local file=$1
    cut -d" " -f2 /proc/mounts | grep -q "^$(readlink -f $file 2>/dev/null)$"
    return $?
}

#------------------------------------------------------------------------------
# Return true if the device shows up in /proc/mounts
#------------------------------------------------------------------------------
is_mounted() {
    local dev=$1
    cut -d" " -f1 /proc/mounts | grep -q "^$dev$"
    return $?
}

#------------------------------------------------------------------------------
# Needs a better name.  Requires all the programs on the list to be on the PATH
# or returns false and says it is Skipping $stage.
#------------------------------------------------------------------------------
require() {
    local stage=$1  prog ret=0 ; shift;
    for prog; do
        which $prog &>/dev/null && continue
        warn $"Could not find program %s.  Skipping %s." "$(pqh $prog)" "$(pqh $stage)"
        ret=2
    done
    return $ret
}

#------------------------------------------------------------------------------
# Throw a fatal error if any of the programs are missing.  Again, need better
# naming.
#------------------------------------------------------------------------------
need_prog() {
    local prog
    for prog; do
        which $prog &>/dev/null && continue
        fatal $"Could not find required program '%s'" "$(pqh $prog)"
    done
}

#------------------------------------------------------------------------------
# Test if a directory is writable by making a temporary file in it.  May not
# be elegant but it is pretty darned robust IMO.
#------------------------------------------------------------------------------
is_writable() {
    local dir=$1
    test -d "$dir" || fatal "Directory %s does not exist" "$dir"
    local temp=$(mktemp -p $dir 2> /dev/null) || return 1
    test -f "$temp" || return 1
    rm -f "$temp"
    return 0
}

#------------------------------------------------------------------------------
# A nice wrapper around is_writable()
#------------------------------------------------------------------------------
check_writable() {
    local dir=$1  type=$2
    test -e "$dir"     || fatal  "The %s directory '%s' does not exist"     "$type" "$dir"
    test -d "$dir"     || fatal  "The %s directory '%s' is not a directory" "$type" "$dir"
    # The <type> directory <dir-name> is not writable
    is_writable "$dir" || fatal $"The %s directory '%s' is not writable"    "$type" "$dir"
}

#------------------------------------------------------------------------------
# Only used in conjunction with cmd() which does not handle io-redirect well.
# Using write_file() allows both PRETEND_MODE and BE_VERBOSE to work.
#------------------------------------------------------------------------------
write_file() {
    local file=$1 ; shift
    mkdir -p "$(dirname "$file")"
    echo "$*" > "$file"
}

#------------------------------------------------------------------------------
# Slightly heuristic way of trying to see if a drive or partition is usb or
# is removable.  This information has never been 100% reliable across all
# hardware.  This is my best shot.  Maybe there will be something better someday.
#------------------------------------------------------------------------------
is_usb_or_removable() {
    local dev=$(expand_device $1)
    test -b $dev || return 1
    local drive=$(get_drive $dev)
    local dir=/sys/block/${drive##*/} flag
    read flag 2>/dev/null < $dir/removable
    [ "$flag" = 1 ] && return 0
    local devpath=$(readlink -f $dir/device)
    [ "$devpath" ] || return 1
    echo $devpath | grep -q /usb
    return $?
}

#------------------------------------------------------------------------------
# Mount dev at dir or know the reason why.  All failures are fatal
#------------------------------------------------------------------------------
my_mount() {
    local dev=$1  dir=$2 ; shift 2
    is_mountpoint "$dir"              && fatal "Directory '%s' is already a mountpoint" "$dir"
    always_cmd mkdir -p "$dir"        || fatal "Failed to create directory '%s'" "$dir"
    always_cmd mount "$@" $dev "$dir" || fatal "Could not mount %s at %s" "$dev" "$dir"
    is_mountpoint "$dir"              || fatal "Failed to mount %s at %s" "$dev" "$dir"
}

#------------------------------------------------------------------------------
# mount_if_needed $dev $mp  [options]
#------------------------------------------------------------------------------
mount_if_needed() {
    local dev=$1  mp=$2 ; shift 2
    test -e "$mp" && ! test -d "$mp" && fatal "Mountpoint %s is not a directory"
    test -d "$mp" || always_cmd mkdir -p "$mp"

    grep -q -- "^$dev $mp " /proc/mounts && return

    local exist_mp=$(get_mp $dev)
    if [ -n "$exist_mp" ]; then
        always_cmd mount --bind "$exist_mp" "$mp" \
            || fatal "Could not bind mount %s to %s" "$exist_mp" "$mp"
    else
        always_cmd mount "$dev" "$mp" "$@" \
            || fatal "Could not mount device %s" "$dev"
    fi
    is_mountpoint "$mp" || fatal "Failed to mount %s at %s" "$dev" "$mp"
    cleanup_mp "$mp"
}

get_mp() { grep "^$1 " /proc/mounts | head -n1 | cut -d" " -f2 ;}

cleanup_mp() { CLEANUP_MPS="$*${CLEANUP_MPS:+ }$CLEANUP_MPS" ;}

#------------------------------------------------------------------------------
# Mount an iso file
#------------------------------------------------------------------------------
mount_iso_file() {
    local file=$1  dir=$2

    test -e "$file" || fatal $"Could not find iso file %s" "$file"
    test -r "$file" || fatal $"Could not read iso file %s" "$file"

    local type
    for type in iso9660 udf; do
        mount -t $type -o loop,ro "$file" $dir 2>/dev/null
        is_mountpoint $dir && return 0
    done

    fatal $"Could not mount iso file %s" "$file"
}

#------------------------------------------------------------------------------
# Returns true on a live antiX/MX system, returns false otherwise.  May work
# correctly on other live systems but has not been tested.
#------------------------------------------------------------------------------
its_alive() {
    # return 0
    local root_fstype=$(df -PT / | tail -n1 | awk '{print $2}')
    case $root_fstype in
        aufs|overlay) return 0 ;;
                   *) return 1 ;;
    esac
}

#------------------------------------------------------------------------------
# Return true if running live and we can write to $LIVE_MP (/live/boot-dev)
# FIXME: Can this be easily fooled by "toram"?
#------------------------------------------------------------------------------
its_alive_usb() {
    its_alive || return 1
    local dir=$LIVE_MP
    test -d $dir || return 1
    is_writable "$dir"
    return $?
}

#------------------------------------------------------------------------------
# Get the device mounted at $LIVE_MP (usually /live/boot-dev)
#------------------------------------------------------------------------------
get_live_dev() {
    local live_dev INITRD_CRYPT_UUID

    # Check to see if we are running from an enrypted live-usb
    read_initrd_param CRYPT_UUID >&2

    if [ -z "$INITRD_CRYPT_UUID" ]; then
        # if not then just see what is mounted at /live/boot-dev
        live_dev=$(sed -rn "s|^([^ ]+) $LIVE_MP .*|\1|p" /proc/mounts)
        echo ${live_dev##*/}
        return
    fi

    # If so then don't allow it to be the target
    live_dev=$(blkid -c /dev/null -U "$INITRD_CRYPT_UUID")
    [ -z "$live_dev" ] && return
    echo ${live_dev##*/}
}

#------------------------------------------------------------------------------
# Assign all variables from initrd.out, adddng INITRD_ prefix to var names
#------------------------------------------------------------------------------
read_initrd_config() {
    file=${1:-$INITRD_CONFIG}  pre=${2:-INITRD_}
    test -r "$file" || fatal "Could not find/read file %s" "$file"
    eval $(sed -r -n "s/^\s*([A-Z0-9_]+=)/$pre\1/p" $file)
}

#------------------------------------------------------------------------------
# Assign selected variable from initrd.out, adding INITRD_ prefix to var name
#------------------------------------------------------------------------------
read_initrd_param() {
    name=$1  file=${2:-$INITRD_CONFIG}  pre=${3:-INITRD_}
    test -r "$file" || return
    eval $(sed -r -n "s/^\s*($name=)/$pre\1/p" $file)
}

#------------------------------------------------------------------------------
# Way overly complicated way to show the distro of a live system mounted at
# at directory.  I tried to cram in extra information.  FIXME
#------------------------------------------------------------------------------
show_distro_version()  {
    local dir=$1  dev=${2##*/}

    [ ${#dir} -gt 0 ]                            || return 1

    sync

    local iso_version version_file=$dir/version
    test -r $version_file                        || return 1
    iso_version=$(cat $version_file 2>/dev/null) || return 1

    if [ ${#dev} -eq 0 ]; then
        [ ${#iso_version} -gt 0 ]                || return 1
        # Which distro we are going to copy or clone
        msg $"Distro: %s" "$(pq $iso_version)"
        return 0
    fi

    if [ ${#iso_version} -gt 0 ]; then
        # Distro X on device Y
        msg $"Distro: %s on %s" "$(pq $iso_version)" "$(pq $dev)"
    else
        warn "No version file found on %s" "$(pqw "$dev")"
    fi
    return 0
}

#------------------------------------------------------------------------------
# Read "version" file and get leading letters from first line
#------------------------------------------------------------------------------
get_distro_name()  {
    local file=$1  version

    [ ${#file} -gt 0 ]                     || return 1
    test -r $file                          || return 1
    read version 2>/dev/null < $file

    [ ${#version} -gt 0 ]                  || return 1
    [ -z "${version%%[a-zA-Z]*}" ]         || return 1

    echo "$version" | sed -r "s/^([A-Za-z]+).*/\1/"
    return 0
}

#------------------------------------------------------------------------------
# Make a partition label of length less than or equal to $max by combinining
# the parts with $sep as glue.
#------------------------------------------------------------------------------
make_label() {
    local max=$1  sep=$2  lab=$3 ; shift 3

    local part len
    for part; do
        len=${#lab}
        [ $len -ge $max ] && break
        part="$sep$part"
        [ $(($len + ${#part})) -le $max ] && lab="$lab$part"
    done
    echo ${lab:0:$max}
}

#------------------------------------------------------------------------------
# Given a partition, echo the canonical name for the drive.
#------------------------------------------------------------------------------
get_drive() {
    local drive part=$1
    case $part in
        mmcblk*) echo ${part%p[0-9]}                       ;;
              *) drive=${part%[0-9]} ; echo ${drive%[0-9]} ;;
    esac
}

#------------------------------------------------------------------------------
# Allow users to use abbreviations like sdd1 or /sdd1 or dev/sdd1
#------------------------------------------------------------------------------
expand_device() {
    case $1 in
        /dev/*)  [ -b "$1"      ] && echo "$1"      ;;
         dev/*)  [ -b "/$1"     ] && echo "/$1"     ;;
            /*)  [ -b "/dev$1"  ] && echo "/dev$1"  ;;
             *)  [ -b "/dev/$1" ] && echo "/dev/$1" ;;
    esac
}

#------------------------------------------------------------------------------
# echo the canonical name for the Nth partition on a drive.
#------------------------------------------------------------------------------
get_partition() {
    local dev=$1  num=$2

    case $dev in
       *mmcblk*) echo  ${dev}p$num  ;;
              *) echo  ${dev}$num   ;;
    esac
}

#------------------------------------------------------------------------------
# Not currently used
#------------------------------------------------------------------------------
device_str() {
    local file=$1  file_type=${2:-"file"}
    local dev=$(expand_device "$file")
    case $(stat -c %t "${dev:-$file}") in
                 0) echo "$file_type"      ;;
                 b) echo "cd/dvd disc"    ;;
                b3) echo "mmc device"     ;;
        3|8|22|103) echo "disk device"    ;;
                 *) echo "unknown device" ;;
    esac
}

#------------------------------------------------------------------------------
# Simple mkdir -p with simple error checking.  If it is likely that a directory
# cannot be made then check it yourself explicitly instead of using this
# routine.  This is to provide some breadcrumbs but I don't expect it to fail
# very often.  If we cannot make a directory then usually something is very
# wrong.
#------------------------------------------------------------------------------
my_mkdir() {
    dir=$1
    mkdir -p "$dir" || fatal "Could not make directory '%s'" "$dir"
}

#------------------------------------------------------------------------------
# Report the size of all the directories and files give in MiB.
#------------------------------------------------------------------------------
du_size() { du -scm "$@" 2>/dev/null | tail -n1 | cut -f1 ; }


#------------------------------------------------------------------------------
# Report the APPARENT size of all the directories and files give in MiB.
# This includes the space allocated by not used by sparse files.
#------------------------------------------------------------------------------
du_ap_size() {
    du --apparent-size -scm "$@" 2>/dev/null | tail -n 1 | cut -f1
}

#------------------------------------------------------------------------------
# Find apparent sizes based on a directory name and a single variable that
# allows file globs, etc.
#------------------------------------------------------------------------------
du_ap_size_spec() {
    dir=$1  spec=$2
    (cd $dir; eval du --apparent-size -scm $spec 2>/dev/null) | tail -n 1 | cut -f1
}

#------------------------------------------------------------------------------
# All the mounted partitions of a give device
#------------------------------------------------------------------------------
mounted_partitions() {
    mount | egrep "^$1[^ ]*" | cut -d" " -f3 | grep .
    return $?
}

#------------------------------------------------------------------------------
# The home directory of the "default user".
#------------------------------------------------------------------------------
get_user_home() {
    local user=${1:-$DEFAULT_USER}
    getent passwd $user | cut -d: -f6
}

#------------------------------------------------------------------------------
# Substitute the "default user's" home direcotry for %USER_HOME%
#------------------------------------------------------------------------------
sub_user_home() {
    local user_home=$(get_user_home)
    echo "$1" | sed "s|%USER_HOME%|$user_home|g"
}

#------------------------------------------------------------------------------
# Issue a simple fatal error if we are not running as root
#------------------------------------------------------------------------------
need_root() {
    [ $UID -eq 0 ] || fatal 099 $"This script must be run as root"
}

#------------------------------------------------------------------------------
# Insert commas into number like: 123,456.  We colorize separately because
# fixed width printf gets confused by ANSI escapes.
#------------------------------------------------------------------------------
add_commas()   { echo "$1" | sed ":a;s/\B[0-9]\{3\}\>/,&/;ta" ;}
color_commas() { sed "s/,/$m_co,$num_co/g" ;}

#------------------------------------------------------------------------------
# Use awk to perform simple arithmetic
#------------------------------------------------------------------------------
x2() { awk "BEGIN{ printf \"%4.2f\n\", $*; }" ; }
x1() { awk "BEGIN{ printf \"%3.1f\n\", $*; }" ; }

#------------------------------------------------------------------------------
# Available space in Meg as reported by the df command
#------------------------------------------------------------------------------
free_space() { df -Pm "$1" | awk '{size=$4}END{print size}' ;}

#------------------------------------------------------------------------------
# Copy a directory while sending percentage done to an external program
# So that program can draw a progress bar.
#------------------------------------------------------------------------------
copy_with_progress() {
    local from=$1  to=$2  err_msg=$3  prog=$4 ; shift 3

    hide_cursor

    printf "Using progress %s: $*\n" "$(my_type $1)" >> $LOG_FILE

    local pre=" >"
    [ "$PRETEND_MODE" ] && pre="p>"
    echo $pre cp $CP_ARGS $from/* $to/               >> $LOG_FILE

    if [ "$PRETEND_MODE" ]; then
        pretend_progress "$@" 2>/dev/null
        echo
        restore_cursor
        return 0
    fi

    local final_size=$(du_size $from/*)
    local base_size=$(du_size $to)

    local cur_size=$base_size  cur_pct=0  last_pct=0

    ORIG_DIRTY_RATIO=$(sysctl -n vm.dirty_ratio)
    ORIG_DIRTY_BYTES=$(sysctl -n vm.dirty_bytes)
    sysctl vm.dirty_bytes=$USB_DIRTY_BYTES >> $LOG_FILE

    (cp $CP_ARGS $from/* $to/ || fatal "$err_msg") &
    COPY_PPID=$!
    sleep 0.01
    COPY_PID=$(pgrep -P $COPY_PPID)

    echo "copy pids: $(echo $COPY_PPID $COPY_PID)" >> $LOG_FILE

    while true; do
        if ! test -d /proc/$COPY_PPID; then
            echo $PROGRESS_SCALE
            break
        fi
        sleep 0.1

        cur_size=$(du_size $to)
        cur_pct=$((cur_size * $PROGRESS_SCALE / final_size))
        [ $cur_pct -gt $last_pct ] || continue
        echo $cur_pct
        last_pct=$cur_pct

    done | "$@"

    echo

    wait $COPY_PPID
    restore_cursor
    sync ; sync


    sysctl vm.dirty_bytes=$ORIG_DIRTY_BYTES >> $LOG_FILE
    sysctl vm.dirty_ratio=$ORIG_DIRTY_RATIO >> $LOG_FILE

    unset ORIG_DIRTY_BYTES ORIG_DIRTY_RATIO

    # Use ERR_FILE as a semaphore from (...)& process
    test -e "$ERR_FILE" && exit 2

    test -d /proc/$COPY_PPID && wait $COPY_PPID
    unset COPY_PPID COPY_PID
}

#------------------------------------------------------------------------------
# Hide cursor and prepare restore_cursor() to work just once
#------------------------------------------------------------------------------
hide_cursor() {
    RESTORE_CURSOR="\e[?25h"

    # Disable cursor
    printf "\e[?25l"
}

#------------------------------------------------------------------------------
# Only works once after hide_cursor() runs.  This allows me to call it in the
# normal flow and at clean up.
#------------------------------------------------------------------------------
restore_cursor() {
    printf "$RESTORE_CURSOR"
    RESTORE_CURSOR=
}

#------------------------------------------------------------------------------
# This acts like an external program to draw a progress bar on the screen.
# It expects integer percentages as input on stdin to move the bar.
#------------------------------------------------------------------------------
text_progress_bar() {
    local abs_max_x=$((SCREEN_WIDTH * PROG_BAR_WIDTH / 100))

    # length of ">|100%" plus one = 7
    max_x=$((abs_max_x - 7))

    local retrace="\e[1000D"
    local eol="$retrace\e[$((max_x + 1))C"

    # Show end points and 0% before we begin
    printf "$retrace$quest_co|$nc_co"
    printf "$eol$quest_co|$nc_co%3s%%" "0"

    local input cur_x last_x=0
    while read input; do
        case $input in
            [0-9]|[0-9][0-9]|[0-9][0-9][0-9]) ;;
                        [0-9][0-9][0-9][0-9]) ;;
            *) break;;
        esac

        [ $input -gt $PROGRESS_SCALE ] && input=$PROGRESS_SCALE
        cur_x=$((max_x * input / $PROGRESS_SCALE))
        [ $cur_x -le $last_x ] && continue

        # Show the percentage first (so we can overwrite vertical bar with arrow tip)
        printf "$eol$quest_co|$nc_co%3s%%" "$((100 * input / $PROGRESS_SCALE))"

        # Draw the bar
        printf "$retrace$quest_co|$m_co%${cur_x}s$bold_co>$nc_co" | tr ' ' '='

        last_x=$cur_x
        [ $input -ge $PROGRESS_SCALE ] && break
    done
}

#------------------------------------------------------------------------------
# Just show the percentage completed
#------------------------------------------------------------------------------
percent_progress() {
    local input
    while read input; do
        case $input in
            [0-9]|[0-9][0-9]|[0-9][0-9][0-9]) ;;
                        [0-9][0-9][0-9][0-9]) ;;
            *) break ;;
        esac
        printf "\e[10D\e[K%3s%%" "$((100 * input / $PROGRESS_SCALE))"
        [ $input -ge $PROGRESS_SCALE ] && break
    done
}

#------------------------------------------------------------------------------
# Replace "file" with "command".
#------------------------------------------------------------------------------
my_type() {
    local prog=$1
    local type=$(type -t $prog)
    case $type in
        file) echo "command" ;;
    function) echo $type     ;;
           *) echo $type     ;;
    esac
}

#------------------------------------------------------------------------------
# Exercise external/internal progress bar when in pretend mode
#------------------------------------------------------------------------------
pretend_progress() {
    local step=$((PROGRESS_SCALE/50))
    for i in $(seq 0 $step $PROGRESS_SCALE); do
        echo $i
        sleep 0.10
    done | "$@"
}

#------------------------------------------------------------------------------
# Try to kill off a list of PIDs in a way that does not cause any problems or
# create extra output to stderr.
#------------------------------------------------------------------------------
kill_pids() {
    local pid
    for pid; do
        test -z "$pid"     && continue
        test -d /proc/$pid || continue

        pkill -P $pid 2>/dev/null
        disown   $pid 2>/dev/null
        kill     $pid 2>/dev/null
    done
}

#------------------------------------------------------------------------------
# Possible cleanup need by this library
# Enable the cursor, kill off bg processes, and restore dirty settings.
# Most of these are only needed if we are interrupted during progbar_copy().
#------------------------------------------------------------------------------
lib_clean_up() {

    restore_cursor

    # Kill off background copy process
    kill_pids $COPY_PPID $COPY_PID

    [ "$ORIG_DIRTY_BYTES" ] && sysctl vm.dirty_bytes=$ORIG_DIRTY_BYTES >> $LOG_FILE
    [ "$ORIG_DIRTY_RATIO" ] && sysctl vm.dirty_ratio=$ORIG_DIRTY_RATIO >> $LOG_FILE

    resume_automount
}

#------------------------------------------------------------------------------
# Unmount a mount point and everything beneath it.  The --recursive option
# doesn't always work in one go, hence the loop.  The number of iterations
# may be related to depth of mounts within mounts which is usually a small
# number.
#------------------------------------------------------------------------------
mp_cleanup() {
    local dir  i  busy

    for i in $(seq 1 10); do
        busy=
        for dir in $CLEANUP_MPS "$@" ; do
            [ ${#dir} -eq 0 ] && continue
            is_mountpoint "$dir" || continue
            busy=true
            umount --recursive "$dir" &>/dev/null
            #is_mountpoint "$dir" || rmdir "$dir"
        done
        sleep 0.1
        [ "$busy" ] && continue
        printf "umount done at iteration %s\n" $i >> $LOG_FILE
        return
    done
}

#------------------------------------------------------------------------------
# Close the LUKS device with the given name.
#------------------------------------------------------------------------------
luks_close() {
    local name=$1
    [ -z "$name" ] && return
    test -e /dev/mapper/$name || return
    cryptsetup close $name
}

#------------------------------------------------------------------------------
# Note: these work on antiX-17 but are not universal
#------------------------------------------------------------------------------
suspend_automount() {
    pkill -STOP udevil
    pkill -STOP devmon
    SUSPENDED_AUTOMOUNT=true
}

#------------------------------------------------------------------------------
# Note: these work on antiX-17 but are not universal
#------------------------------------------------------------------------------
resume_automount() {
    [ "$SUSPENDED_AUTOMOUNT" ] || return
    pkill -CONT devmon
    pkill -CONT udevil
}

#------------------------------------------------------------------------------
# This is the new UI for this lib!  Use arrow keys to highlight the entry you
# want and then press <Enter>.  MUCH MUCH better than the old way IMO.
#------------------------------------------------------------------------------
graphical_select() {
    local var=$1  title=$2  list=$3  def_str=$4  SELECTED_ENTRY=${5:-1}  orig_ifs=$IFS
    local l_margin=4  l_pad="  "
    local IFS=$P_IFS

    # Need screen width for writing spaces to blank out lines in case we get
    # scrolled by the man program or something else
    local screen_width=$(stty size | cut -d" " -f2)
    : ${screen_width:=$SCREEN_WIDTH}

    # Use less horizontal space if there is less room
    if [ $screen_width -lt 100 ]; then
        l_margin=2
        l_pad=""
    fi

    # First is width used inside of menu, 2nd is width used outside of menu
    # because the menu is slightly indented
    local max_width=$((screen_width - 2 - l_margin))
    local OUT_WIDTH=$((screen_width - 2))

    # Prepare the menu to display be appending spaces (or truncating)
    # and make SKIP_ENTRIES list and the data list.  The menu drawing
    # program only uses the data to see if it is empty or not for
    # display purposes
    local cnt=0 menu data  SKIP_ENTRIES
    while read datum label; do
        cnt=$((cnt + 1))

        # We will skip over entries that have no data
        [ ${#datum} -eq 0 ] && SKIP_ENTRIES=$SKIP_ENTRIES,$cnt

        # We will eventually grep/sed this to get the data payload
        data="${data}$cnt:$datum\n"

        width=$(str_len "$label")
        if [ $width -gt $max_width ]; then
            # strip out color sequences and truncate
            label=$(echo "$label" | strip_color)
            menu="$menu$datum$P_IFS${label:0:$max_width}$warn_co|$nc_co\n"
        else
            # pad string with spaces
            local pad=$((max_width - width))
            [ $pad -lt 0 ] && pad=0
            space=$(printf "%${pad}s\\\\n" "")
            menu="$menu$datum$P_IFS$m_co$label$nc_co$(printf "%${pad}s" "")\n"
        fi

    done<<Graphic_Select_2
$(echo -e "$list")
Graphic_Select_2

    local MENU_SIZE=$cnt
    IFS=$orig_ifs

    # Some callers may want to use a word other the "entry"
    if [ -n "$def_str" ]; then
        def_str="($(pqq $def_str))"
    else
        def_str=$"entry"
    fi

    # This fixes the problem where the first entry should be skipped.  When we
    # start we keep skipping forward as needed until we land on an entry that
    # shouldn't be skipped.  If the caller sets a default entry then they
    # should make sure it is not skipped (IOW it has data).
    for SELECTED_ENTRY in $(seq $SELECTED_ENTRY $MENU_SIZE); do
        gs_must_skip || break
    done

    # Press <Enter> ...
    local enter=$"Enter"

    # Press <Enter> to select the highlighted <entry>
    local press_enter=$"Press %s to select the highlighted %s"
    local p2 def_prompt=$(printf "$press_enter" "<$(pqq "$enter")>" "$def_str" )

    # Sometimes we want to use 'q' to go back to another menu instead of exiting
    # the program.  We change the printed instructions and we also change the
    # behavior based on a non-empty BACK_TO_MAIN string
    local quit=$"quit"
    [ -n "$BACK_TO_MAIN" ] && quit=$BACK_TO_MAIN

    # A similar thing is done if we have a man page available.  We change
    # the instructions and the behavior
    if [ "$HAVE_MAN" ]; then
        # Use <h> for help, <r> to redraw, <q> to <go back to main menu>
        local use_help=$"Use %s for help, %s to redraw, %s to %s"
        p2=$(printf "$use_help" "'$(pqq h)'"   "'$(pqq r)'"   "'$(pqq q)'"  "$quit")
    else
        # Use <r> to redraw, <q> to <go back to main menu>
        local use_x=$"Use %s to redraw, %s to %s"
        p2=$(printf "$use_x" "'$(pqq r)'"   "'$(pqq q)'"  "$quit")
    fi

    # Okay, here we go into semi-graphical mode
    hide_cursor

    # This counts how many rows we need to jump up in the screen in order
    # to redraw the menu.
    local retrace_lines=$((MENU_SIZE + 2 + $(echo "$title" | wc -l) ))
    [ "$p2" ] && retrace_lines=$((retrace_lines + 1))

    # We draw/redraw then entire menu each time through this loop.
    # Inside, we wait for a keypress and then do as instructed.
    local selected  end_loop
    while true; do

        printf "%${OUT_WIDTH}s\n"  ""
        # FIXME: this is broken for multi-line titles
        rpad_str $OUT_WIDTH "$quest_co$title"

        show_graphic_menu "$l_pad" "$menu" "$SELECTED_ENTRY" "$selected"

        # Show instructions under the menu
        rpad_str $OUT_WIDTH "$def_prompt"
        [ "$p2" ] && rpad_str $OUT_WIDTH "$p2"

        # Clear final line and save position
        # Although saving the position does not help us because it seems to
        # get un-saved when we shell out to the man program.
        printf "%${OUT_WIDTH}s\r" ""
        printf "\e[s"

        # This lets us draw the menu one more time before exiting.  We clear
        # the previously selected entry (undo reverse video) and if an entry
        # was selected we change the ">" to an "=" to mark which one was
        # selected.  If they exit via 'q' then no "=" sign gets added.
        case $end_loop in
             break) break ;;
            return) return ;;
        esac

        # Note that 'q' and <Enter> both have us go through the loop once
        # more and we leave the loop in the case statement above.  This
        # lets us redraw the menu a final time.
        case $(get_key) in
            [qQ]|escape) if [ -n "$BACK_TO_MAIN" ]; then
                          eval $var=quit
                          SELECTED_ENTRY=0
                          end_loop=return
                      else
                          gs_final_quit
                      fi ;;

                [hH]) if [ "$HAVE_MAN" ]; then
                          restore_cursor
                          man "$MAN_PAGE"
                          hide_cursor
                      fi;;

                # This is pretty useless.  We just go further up the page
                [rR]) printf "\e[200B\n" ; continue ;;

               enter)  selected=$SELECTED_ENTRY
                       SELECTED_ENTRY=0
                       end_loop=break       ;;

                left) gs_step_default   -3  ;;
               right) gs_step_default   +3  ;;
                  up) gs_step_default   -1  ;;
                down) gs_step_default   +1  ;;
             page-up) gs_step_default   -5  ;;
           page-down) gs_step_default   +5  ;;
                home) gs_step_default -100  ;;
                 end) gs_step_default +100  ;;
        esac

        # Restore cursor position (haha) and the jump up retrace_lines to draw
        # the menu again
        printf "\e[u"
        printf "\e[${retrace_lines}A\r"
    done

    # We go back to the blank line at the bottom of the menu
    printf "\e[u"
    restore_cursor

    local val=$(echo -ne "$data" | sed -n "s/^$selected://p")
    eval $var=\$val
}

#------------------------------------------------------------------------------
# Add spaces to right side of string to make it length $width.  Printf fails
# for a couple of reasons, otherwise we'd use it.  This routine ignores ANSI
# color escape sequences.
#------------------------------------------------------------------------------
rpad_str() {
    local width=$1  fmt=$2  ; shift 2
    local msg=$(printf "$fmt" "$@")
    local len=$(str_len "$msg")
    local pad=$((width - len))
    [ $pad -lt 0 ] && pad=0
    printf "$quest_co%s%${pad}s$nc_co\n" "$msg" ""
}

#------------------------------------------------------------------------------
# Get the "length" of a string, ignoring my ANSI color escapes
#------------------------------------------------------------------------------
str_len() {
    local msg_nc=$(echo "$*" | sed -r -e 's/\x1B\[[0-9;]+[mK]//g' -e 's/./x/g')
    echo ${#msg_nc}
}

#------------------------------------------------------------------------------
# Move which entry is highlighted up or down.  This gets tricky because we
# need to skip over entries in the skip list
#------------------------------------------------------------------------------
gs_step_default() {
    local step=$1  orig_selected=$SELECTED_ENTRY
    SELECTED_ENTRY=$((SELECTED_ENTRY + step))

    [ $SELECTED_ENTRY -lt 1 ]          && SELECTED_ENTRY=1
    [ $SELECTED_ENTRY -gt $MENU_SIZE ] && SELECTED_ENTRY=$MENU_SIZE

    #return
    gs_must_skip || return

    if [ $step -gt 0 ]; then
        for SELECTED_ENTRY in $(seq $SELECTED_ENTRY $MENU_SIZE); do
            gs_must_skip || return
        done
    else
        for SELECTED_ENTRY in $(seq $SELECTED_ENTRY -1 1); do
            gs_must_skip || return
        done
    fi

    # If there are no valid entries in the direction we were asked to move then
    # we don't move.
    SELECTED_ENTRY=$orig_selected
}

#------------------------------------------------------------------------------
# Is the selected entry on the skip list?  Used for skipping over entries that
# have no payloads.
#------------------------------------------------------------------------------
gs_must_skip() {
    case ,$SKIP_ENTRIES, in
        *,$SELECTED_ENTRY,*) return 0 ;;
                          *) return 1 ;;
    esac
}

#------------------------------------------------------------------------------
# Verify user really wants to quit.  If so, really exit with no pause.  If not
# then clean up after ourselves and return.  This is very similar to the
# final_quit() routine in the old UI.
#------------------------------------------------------------------------------
gs_final_quit() {
    quest $"Press '%s' again to quit" "$(pqq q)"

    case $(get_key) in
        [qQ]|escape) ;;
                  *) printf "\r%${OUT_WIDTH}s" "" ; return ;;
    esac

    restore_cursor
    echo
    PAUSE=$(echo "$PAUSE" | sed -r "s/(^|,)exit(,|$)/,/")
    exit 0
    printf "\e[1A\r" ; printf "\e[1A\r%${OUT_WIDTH}s" ""
}

#------------------------------------------------------------------------------
# List the menu.  Mark valid entries with " >".  Mark the selected entry with
# reverse video.  If "selected" is given then we use "=" instead of ">".  this
# is to make visible which entry was selected when we show the menu for the
# last time.
#------------------------------------------------------------------------------
show_graphic_menu() {
    local l_pad=$1  list=$2  selected_entry=$3  selected=${4:-0}
    local IFS=$P_IFS

    local cnt=0 datum entry
    while read datum entry; do
        cnt=$((cnt + 1))
        if [ $cnt -eq $selected ]; then
            printf "$l_pad$m_co= $nc_co"
        elif [ -n "$datum" ]; then
            printf "$l_pad$bold_co> $nc_co"
        else
            printf "$l_pad  "
        fi

        local rev=
        [ $cnt -eq $selected_entry ] && rev=$rev_co
        printf "$nc_co$rev%s$nc_co\n" "$entry"
    done<<Graphic_Menu
$(echo -ne "$list")
Graphic_Menu
}

#------------------------------------------------------------------------------
# used in graphical_select().  Get a keypress from stdin without waiting for a
# newline.  Translate escape sequences into reasonable names.  This works in
# Bash but not in busybox shells.
#------------------------------------------------------------------------------
get_key() {
    local key k1 k2 k3 k4  REPLY
    read -s -N1
    k1=$REPLY
    read -s -N2 -t 0.001 k2
    read -s -N1 -t 0.001 k3 2>/dev/null
    read -s -N1 -t 0.001 k4 2>/dev/null
    key=$k1$k2$k3$k4

    # NOTE: $'...' is for ANSCI-C quoting inside of Bash for example
    # try running: echo $'abc\nxyz'
    case $key in
        $'\eOP\x00')  key=f1           ;;
        $'\eOQ\x00')  key=f2           ;;
        $'\eOR\x00')  key=f3           ;;
        $'\eOS\x00')  key=f4           ;;

        $'\e[[A')     key=f1           ;;
        $'\e[[B')     key=f2           ;;
        $'\e[[C')     key=f3           ;;
        $'\e[[D')     key=f4           ;;
        $'\e[[E')     key=f5           ;;

        $'\e[11~')    key=f1           ;;
        $'\e[12~')    key=f2           ;;
        $'\e[13~')    key=f3           ;;
        $'\e[14~')    key=f4           ;;
        $'\e[15~')    key=f5           ;;
        $'\e[17~')    key=f6           ;;
        $'\e[18~')    key=f7           ;;
        $'\e[19~')    key=f8           ;;
        $'\e[20~')    key=f9           ;;
        $'\e[21~')    key=f10          ;;
        $'\e[23~')    key=f11          ;;
        $'\e[24~')    key=f12          ;;
        $'\e[2~')     key=insert       ;;
        $'\e[3~')     key=delete       ;;
        $'\e[5~')     key=page-up      ;;
        $'\e[6~')     key=page-down    ;;
        $'\e[7~')     key=home         ;;
        $'\e[8~')     key=end          ;;
        $'\e[1~')     key=home         ;;
        $'\e[4~')     key=end          ;;
        $'\e[A')      key=up           ;;
        $'\e[B')      key=down         ;;
        $'\e[C')      key=right        ;;
        $'\e[D')      key=left         ;;

        $'\x7f')      key=backspace    ;;
        $'\x08')      key=backspace    ;;
        $'\x09')      key=tab          ;;
        $'\x0a')      key=enter        ;;
        $'\e')        key=escape       ;;
        $'\x20')      key=space        ;;
    esac
    echo "$key"
}

#==============================================================================
#===== END ====================================================================
#==============================================================================
