#!/bin/bash

ME=${0##*/}

     MBR_GPT="msdos"
        SIZE=100
    FAT_SIZE=50
  EXT_MARGIN=20
  FAT_MARGIN=5

 EXT_OPTIONS="-m0 -N10000 -J size=32"
   EXT_LABEL="LiveUSB"

     LIVE_MP="/live/boot-dev"
  LIVE_FILES="cdrom.ico version antiX/{vmlinuz,initrd.gz,linuxfs}{,.md5}"
   LIVE_DIRS="boot EFI efi"
   GRUB_CONF="boot/grub/grub.cfg"
      CHEATS=""
    WORK_DIR="/tmp/$ME"
   LOCK_FILE="/run/lock/$ME"

   CONF_FILE="/root/.config/$ME/$ME.conf"
THE_LOG_FILE="/var/log/$ME.log"
THE_ERR_FILE="/var/log/$ME.errors"
    LOG_FILE=/dev/null
    ERR_FILE=/dev/null

ORDERED_CMDS="partition makefs makefs-ext makefs-fat copy copy-ext copy-fat"
ORDERED_CMDS="$ORDERED_CMDS uuids cheats cheats-syslinux cheats-grub install"
    ALL_CMDS="sizes all $ORDERED_CMDS"
    PRE_CMDS="partition makefs"
   POST_CMDS="uuids cheats install"

   ALL_FORCE="copy,flock,makefs,umount,usb"

   PATH="$(dirname $(readlink -f $0)):/usr/local/share/live-usb-maker/bin:$PATH"

usage() {
    local ret=${1:-0}

cat<<Usage
Usage: $ME [options] <iso-file> <usb-device> <commands>

Create a live-usb on <usb-device> from <iso-file>.  This will destroy
any existing information on <usb-device>.  Uses ext4 as the filesystem
for the live-usb and add a small fat32 file system for booting via UEFI.

  - Use "clone" as the iso-file to clone a running live system.
  - Use "clone=<dir>" to clone from a mounted live-usb or iso-file.
  - You may specify a device as the iso-file.  That device will be
    mounted and cloned.

At least one command must be given.  If "all" is not given then only the
commands given will be run.  Use a trailing "+" to run a command and all
commands after it.

Commands:
    sizes            Only show and check sizes, don't do anything
    all              Do all commands below
    partition        Partition the live usb
    makefs-ext       Create the ext file system
    makefs-fat       Create the fat file system
    makefs           Both makefs-ext and makefs-fat
    copy-ext         Copy files to live usb ext partition
    copy-fat         Copy files to fat partition
    copy             Both copy-ext and copy-fat
    uuids            Write UUIDs linking file systems
    cheats           Copy cheat codes to live-usb
    install          Install the legacy bootloader

Options:
  -a --auto         Never ask questions.  Always assume the safe answer
  -c --cheat=xxx    Add these cheatcodes to the live-usb
  -C --clear        Delete files from each partition before copying
  -d --debug        Pause before cleaning up
  -e --esp-size=XX  Size of ESP (fat) partition in MiB (default 50)
  -f --force=XXXX   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!)

  -g --gpt          Use gpt partitioning instead of msdos
  -h --help         Show this usage
  -L --label=Name   Label ext partition with Name
  -p --pretend      Don't run most commands, just show them
  -P --Pretend      Pretend witout verbose
  -q --quiet        Print less
  -s --size=XX      Percent of usb-device to use in (default 100)
  -v --verbose      Print more, show commands when run

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

eval_argument() {
    local arg=$1 val=$2
        case $arg in
              -auto|a)  AUTO_MODE=true                  ;;
             -cheat|c)  CHEATS="$CHEATS${CHEATS:+ }$val";;
         -cheat=*|c=*)  CHEATS="$CHEATS${CHEATS:+ }$val";;
             -clear|C)  CLEAR=true                      ;;
             -debug|d)  DEBUG=true                      ;;
          -esp-size|e)  FAT_SIZE=$val                   ;;
      -esp-size=*|e=*)  FAT_SIZE=$val                   ;;
             -force|f)  FORCE="$FORCE,$val"             ;;
         -force=*|f=*)  FORCE="$FORCE,$val"             ;;
               -gpt|g)  MBR_GPT="gpt"                   ;;
              -help|h)  usage                           ;;
             -label|L)  EXT_LABEL=$val                  ;;
         -label=*|L=*)  EXT_LABEL=$val                  ;;
           -pretend|p)  PRETEND=true ; BE_VERBOSE=true  ;;
           -Pretend|P)  PRETEND=true ; BE_VERBOSE=      ;;
             -quiet|q)  QUIET=true                      ;;
              -size|s)  SIZE=${val%\%}                  ;;
          -size=*|s=*)  SIZE=${val%\%}                  ;;
           -verbose|v)  BE_VERBOSE=true                 ;;
                    *)  fatal "Unknown parameter %s" "-$arg" ;;
    esac
}

takes_param() {
    case $1 in
       -cheat|c) return 0 ;;
    -esp-size|e) return 0 ;;
       -force|f) return 0 ;;
       -label|L) return 0 ;;
        -size|s) return 0 ;;
    esac
    return 1
}

main() {
    local SHIFT SHORT_STACK="cdefghLpPqsv"
    local BE_VERBOSE ISO_FILE DEVICE

    local START_T=$(date +%s)

    [ $# -eq 0 ] && usage
    check_usage "$@"

    [ $UID -eq 0 ] || fatal 099 "This script must be run as root"

    ERR_FILE=$THE_ERR_FILE

    trap clean_up EXIT

    if which flock &> /dev/null; then
        exec 18> $LOCK_FILE
        flock -n 18 || fatal 101 "A %s process is running.  If you think this is an error, remove %s" "$ME" "$LOCK_FILE"
        echo $$ >&18
    else
        # fatal "The %s program was not found."
        yes_NO_fatal "flock" \
            "Do you want to continue without locking?" \
            "Use --force=flock to disable locking." \
            "The %s program was not found." "flock"
    fi

    rm -f $ERR_FILE

    local orig_args="$*"

    test -r "$CONF_FILE" && . "$CONF_FILE"

    # This loop allows complete intermingling of filenames and options
    local opt_cnt=0
    while [ $# -gt 0 ]; do
        read_params "$@"
        shift $SHIFT

        while [ $# -gt 0 -a ${#1} -gt 0 -a -n "${1##-*}" ]; do
            case $opt_cnt in
                0) ISO_FILE=$1     ;;
                1) DEVICE=$1       ;;
                *) CMDS="$CMDS $1" ;;
            esac
            shift
            opt_cnt=$((opt_cnt + 1))
        done
    done

    [ $opt_cnt -lt 2 ] && fatal "Expected at least two command line arguments"

    [ "$PRETEND" ] && msg "PRETEND MODE ENABLED"

    case $SIZE in
        [1-9]|[0-9][0-9]|100) ;;
        *) fatal "Bad size format '%s'.  Should be between 1 and 100 inclusive." "$(pqh $SIZE)"
    esac

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

    #echo "$CMDS" | egrep -q "(^| )pre( |$)"  && CMDS="$CMDS $PRE_CMDS"
    #echo "$CMDS" | egrep -q "(^| )post( |$)" && CMDS="$CMDS $POST_CMDS"

    # Test for valid commands and process cmd+ commands
    local cmd plus_cnt=0
    for cmd in $CMDS; do
        echo "$ALL_CMDS" | egrep -q "(^| )$cmd+?( |$)" || fatal "Unknown command: %s" $cmd
        [ -z "${cmd%%*+}" ] || continue
        cmd=${cmd%+}
        CMDS="$CMDS $(echo "$ORDERED_CMDS" | sed -rn "s/.*($cmd )/\1/p")"
        plus_cnt=$((plus_cnt + 1))
        [ $plus_cnt -gt 1 ] && fatal "Only one + command allowed"
    done

    local force
    for force in ${FORCE//,/ }; do
        echo ",$ALL_FORCE," | egrep -q ",$force," || fatal "Unknown force option: %s" "$force"
    done

    local usb_dev=$(expand_device $DEVICE) || fatal "Could not find device %s" "$DEVICE"

    start_log "$orig_args" "$CMDS" "$ISO_FILE" "$usb_dev"

    # Write config file if one does not already exist
    test -e "$CONF_FILE" || write_conf "$CONF_FILE"

    mkdir -p $WORK_DIR || fatal "Could not make a work directory under /tmp"
    chmod 755 $WORK_DIR

    local dev_type=$(lsblk -no type --nodeps $usb_dev)
    [ "$dev_type" = "disk" ] || fatal "Device %s is not a disk device" $usb_dev

    test -e $usb_dev || fatal "Device %s does not exist" $usb_dev
    test -b $usb_dev || fatal "Device %s is not a block device" $usb_dev

    local ext_dev=$(get_partition $usb_dev 1)
    local fat_dev=$(get_partition $usb_dev 2)

    # fatal "The device %s does not seem to be usb or removeable."
    force usb || is_usb_or_removeable $usb_dev || yes_NO_fatal "usb" \
        "Do you want to use it anyway (dangerous!)" \
        "Use --force=usb to continue" \
        "The device %s does not seem to be usb or removeable."  "$usb_dev"


    ISO_DIR=$WORK_DIR/iso
    EXT_DIR=$WORK_DIR/ext
    FAT_DIR=$WORK_DIR/fat
    MNT_DIR=$WORK_DIR/live-dev

    mkdir $ISO_DIR $EXT_DIR $FAT_DIR || fatal "Could not make %s subdirectories" "$WORK_DIR"

    mount_iso "$ISO_FILE" $ISO_DIR || fatal "Could not mount iso file %s" "$ISO_FILE"

    local iso_version version_file=$ISO_DIR/version
    if test -r $version_file && iso_version=$(cat $version_file 2>/dev/null) \
        && [ ${#iso_version} -gt 0 ]; then
        msg "Distro: %s" "$(pq $iso_version)"
        msg
    fi

    local total_size=$(parted $usb_dev unit MiB print | sed -rn "s/^Disk.*: ([0-9]+)MiB$/\1/p")
    local mib_size=$((total_size * SIZE / 100 - 1))
    local ext_size=$((mib_size - FAT_SIZE))
    local ext_needed=$(du_size $ISO_DIR/*)
    local fat_needed=$(du_size $ISO_DIR/{boot,EFI,efi})
    local ext_extra=$((ext_size - ext_needed))
    local fat_extra=$((FAT_SIZE - fat_needed))

    local mem_fmt="%20s:$white %7s$msg_co MiB"

    msg "$mem_fmt" "total usb size"     $total_size
    msg "$mem_fmt" "requested usb size" $mib_size
    msg "$mem_fmt" "ext available"      $ext_size
    msg "$mem_fmt" "ext needed"         $ext_needed
    msg "$mem_fmt" "ext extra"          $ext_extra
    msg "$mem_fmt" "fat available"      $FAT_SIZE
    msg "$mem_fmt" "fat needed"         $fat_needed
    msg "$mem_fmt" "fat extra"          $fat_extra
    msg

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

    check_size ext $ext_extra $EXT_MARGIN
    check_size fat $fat_extra $FAT_MARGIN

    echo "$CMNDS" | egrep -q "(^| )sizes( |$)" && my_exit 0

    umount_all $usb_dev

    need partition && do_partition "$usb_dev" "$MBR_GPT" $ext_size $mib_size

    need makefs-ext && do_makefs_ext "$ext_dev" "$EXT_OPTIONS" "$EXT_LABEL"
    need makefs-fat && do_makefs_fat $fat_dev

    my_mount $ext_dev $EXT_DIR
    my_mount $fat_dev $FAT_DIR

    cmd df -Tm $EXT_DIR $FAT_DIR

    need copy-ext && do_copy_ext $ISO_DIR $EXT_DIR
    need copy-fat && do_copy_fat $ISO_DIR $FAT_DIR

    local ext_uuid=$(lsblk -no uuid $ext_dev)
    local fat_uuid=$(lsblk -no uuid $fat_dev)

    need uuids   && do_uuids $EXT_DIR "$ext_uuid" "$FAT_DIR/$GRUB_CONF" "$fat_uuid"

    [ ${#CHEATS} -gt 0 ] && need cheats-syslinux && do_cheats_syslinux $EXT_DIR "$CHEATS"
    [ ${#CHEATS} -gt 0 ] && need cheats-grub     && do_cheats_grub     $FAT_DIR "$CHEATS"

    need install && do_install $usb_dev $EXT_DIR $MBR_GPT

    my_exit 0
}

mount_iso() {
    local file=$1  dir=$2
    mkdir -p $dir

    is_mountpoint $dir && fatal "Directory '%s' is already a mountpoint" $dir

    case $file in
        clone) mount_live $dir              ; return ;;
      clone=*) mount_live $dir "${file#*=}" ; return ;;
    esac

    local block_dev=$(expand_device $file)
    file=${block_dev:-$file}

    if test -b $file; then
        mount -o ro "$file" $dir
        is_mountpoint $dir && return 0
    else
        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
    fi

    fatal "Could not mount iso file %s" "$file"
}

mount_live() {
    local targ_dir=$1  live_dir=${2:-$LIVE_MP}

    is_mountpoint "$targ_dir" && fatal "Live directory '%s' is already a mountpoint" "$targ_dir"

    # Allow live "directory" to be a device that we mount
    local block_dev=$(expand_device $live_dir)
    if [ ${#block_dev} -gt 0 ];  then
        my_mkdir $MNT_DIR
        mount -o ro $block_dev $MNT_DIR
        is_mountpoint $MNT_DIR || fatal "Could not mount device %s" "$block_dev"
        live_dir=$MNT_DIR
    fi

    test -d "$live_dir"       || fatal "Live directory '%s' is not a directory"   "$live_dir"
    is_mountpoint "$live_dir" || fatal "Live directory '%s' is not a mountpoint"  "$live_dir"

    mount --bind $targ_dir $targ_dir

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

    local dir from dest
    while read dir; do
        from="$live_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 <<Live_Dirs
$(cd "$live_dir" && eval "ls -d $LIVE_DIRS" 2>/dev/null)
Live_Dirs

    local file
    while read file; do
        from="$live_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 <<Live_Files
$(cd "$live_dir" && eval "ls -d $LIVE_FILES" 2>/dev/null)
Live_Files
}

my_mount() {
    local dev=$1  dir=$2
    is_mountpoint $dir           && fatal "Directory '%s' is already a mountpoint" "$dir"
    PRETEND= cmd mkdir -p $dir   || fatal "Failed to create directory '%s'" "$dir"
    PRETEND= 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"
}

do_partition() {
    local drive=$1  type=${2:-msdos}  ext_size=$3  mib_size=$4

    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

    msg "Using %s partitioning" $(pq $type)

    cmd parted --script --align optimal $usb_dev   \
        unit MiB                                   \
        mklabel $type                              \
        mkpart primary ext4  1         $ext_size   \
        mkpart primary fat32 $ext_size $mib_size   \
        set 1 $boot_flag on                        \
        set 2 esp  on || fatal "Partitioning failed"
}

do_makefs_ext() {
    local dev=$1  options=$2  label=$3  force_ext

    force makefs && force_ext="-F"
    cmd mkfs.ext4 $force_ext $options -L "$label" $dev \
        || fatal "makefs" "Could not make ext4 file system on $ext_dev.  Perhaps try --force=makefs"
}

do_makefs_fat() {
    local fat_dev=$1
    cmd mkfs.fat -n "LIVE-ESP" $fat_dev \
        || fatal "Could not make fat32 file system on %s" "$fat_dev"
}

do_copy_ext() {
    local iso_dir=$1  ext_dir=$2

    [ "$CLEAR" ] && cmd rm -rf $ext_dir/*

    if test -d $ext_dir/antiX && ! force copy; then
        warn "Not over-writing ext partition due to existing antiX/ directory"
        warn "Use --clear or --force=copy to overwrite"
        return
    fi
    msg "Copying files ..."
    cmd cp -a $iso_dir/* $ext_dir/ || fatal "Error when copying files to live usb"
    sync
}

do_copy_fat() {
    local iso_dir=$1  fat_dir=$2

    [ "$CLEAR" ] && cmd rm -rf $fat_dir/* 2>/dev/null

    local top
    for top in $iso_dir/{efi,EFI,boot}; do
        test -e $top || continue
        cmd cp -r $top $fat_dir/ \
            || warn "Error when copying %s files to fat partition" "$(basename $top)"
    done
}

do_cheats_syslinux() {
    #warn "Cheats are still being worked on"; return

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

    require cheats-syslinux gfxsave || return

    local file params

    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=4 CMDLINE="$params $cheats" cmd gfxsave $ext_dir/boot both
    else
        warn "Could not find %s file to update" "$(basename $syslinux_cfg)"
    fi
}

do_cheats_grub() {
    #warn "Cheats are still being worked on"; return
    local fat_dir=$1  cheats=$2
    local grub_cfg=$fat_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 $fat_dir --cheats="${params% } $cheats"
    else
        warn "Could not find %s file to update" $(basename $grub_cfg)
    fi
}

do_uuids() {
    local ext_dir=$1  ext_uuid=$2  grub_cfg=$3  fat_uuid=$4
    if [ ${#fat_uuid} -gt 0 ]; then
        local fat_file=$ext_dir/antiX/esp-uuid
        cmd mkdir -p $(dirname $fat_file) || fatal "Making directory %s failed" "$(dirname $fat_file)"
        cmd write_file $fat_file $fat_uuid
    else
        warn "No UUID given for fat partition"
    fi

    if [ ${#ext_uuid} -eq 0 ]; then
        warn "No UUID give for ext4 partition"
        return
    fi

    if ! test -e $grub_cfg; then
        warn "Could not find %s file" "$(basename $grub_cfg)"
        return
    fi

    local new_line="search --no-floppy --set=root --fs-uuid $ext_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
}

do_install() {
    local usb_dev=$1  ext_dir=$2  type=$3

    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 bs=440 conv=notrunc count=1 if=$file of=$usb_dev > /dev/null || fatal "dd command failed"

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

            warn "Create syslinux directory from %s" $idir
                syslinux_dir=$ext_dir/${idir%isolinux}syslinux
                cmd cp -r $ext_dir/$idir $syslinux_dir || fatal "Could not copy the isolinux directory"
                local f
                for f in $(cd $syslinux_dir && ls isolinux.*); do
                    test -e $ext_dir/$sdir/$f || continue
                    cmd mv $ext_dir/$sdir/$f $ext_dir/$sdir/syslinux${f#isolinux}
                done
            break
        done
    fi
    [ "$syslinux_dir" ] || fatal "Could not find a syslinux or isolinux directory"
    cmd extlinux -i $syslinux_dir || fatal "extlinux command failed"
}

need() {
    local cmd=$1  cmd2=${1%%-*}
    echo "$CMDS" | egrep -q "(^| )($cmd|$cmd2|all)( |$)" || return 1
    Msg "=> $cmd"
    return 0
}

force() {
    local this=$1
    case ,$FORCE, in
        *,$this,*|*,all,*) return 0 ;;
    esac
    return 1
}

write_file() {
    file=$1
    shift
    echo "$*" > $file
}

is_usb_or_removeable() {
    local drive=$(get_drive $1)
    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 $?
}

get_drive() {
    local drive part=${1##*/}
    case $part in
        mmcblk*) echo ${part%p[0-9]}                        ;;
              *) drive=${part%[0-9]}  ; echo ${drive%[0-9]} ;;
    esac
}

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
}

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"
    force umount || yes_NO_fatal "umount" \
        "Do you want those partitions unmounted" \
        "Use --force=umount to have us try to unmount them" \
        "One or more partitions on device %s are mounted at:\n  %s" "$dev" "$(echo $mounted)"

    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
        mount | egrep -q "^$dev[^ ]*" || return 0
        for part in $(mount | egrep -o "^$dev[^ ]*"); do
            umount $part 2>/dev/null
        done
        sleep .1
    done

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

get_partition() {
    local dev=$1  num=$2

    case $dev in
        *mmcbk*) echo  ${dev}p$num  ;;
              *) echo  ${dev}$num   ;;
    esac
}

check_usage() {
    local arg
    for arg; do
        case $arg in
            -h|--help) usage ;;
        esac
    done
}

#-------------------------------------------------------------------------------
# Send "$@".  Expects
#
#   SHORT_STACK               variable, list of single chars that stack
#   fatal(msg)                routine,  fatal([errnum] "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
#
# Sets "global" variable SHIFT to the number of arguments that have been read.
#-------------------------------------------------------------------------------
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"
    done
}

cmd() {
    echo " > $*" >> $LOG_FILE
    [ "$BE_VERBOSE" ] && echo " >" "$@" | sed "s|$WORK_DIR|.|g"
    [ "$PRETEND"    ] && return 0
    "$@" 2>&1 | tee -a $LOG_FILE
    # Warning: Bashism
    return ${PIPESTATUS[0]}
}

check_size() {
    local type=$1  extra=$2  margin=$3
    [ $extra -lt 0 ] && fatal "Not enough space on %s partition" "$type"
    [ $extra -lt $margin ] && fatal "Less than %s MiB would remain on %s partition" "$margin" "$type"
}

my_mkdir() {
    dir=$1
    mkdir -p "$dir" || fatal "Could not make directory '%s'" "$dir"
}

du_size() {
    du --apparent-size -scm "$@" 2>/dev/null | tail -n 1 | cut -f1
}

set_colors() {
    local noco=$1  loco=$2

    local e=$(printf "\e")
     black="$e[0;30m";    blue="$e[0;34m";    green="$e[0;32m";    cyan="$e[0;36m";
       red="$e[0;31m";  purple="$e[0;35m";    brown="$e[0;33m"; lt_gray="$e[0;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";

    cheat_co=$white;      err_co=$red;       hi_co=$white;
      cmd_co=$white;     from_co=$lt_green;  mp_co=$magenta;   num_co=$magenta;
      dev_co=$magenta;   head_co=$yellow;     m_co=$lt_cyan;    ok_co=$lt_green;
       to_co=$lt_green;  warn_co=$yellow;  bold_co=$yellow;
}

pq()     { echo "$hi_co$*$m_co"           ;}
pqw()    { echo "$warn_co$*$hi_co"        ;}
pqe()    { echo "$hi_co$*$err_co"         ;}
pqh()    { echo "$m_co$*$hi_co"           ;}
bq()     { echo "$yellow$*$m_co"          ;}
cq()     { echo "$cheat_co$*$m_co"        ;}

# 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" "$@")

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

YES_no() {  _yes_no 0 "yes" "$@" ;}
yes_NO() {  _yes_no 1 "no"  "$@" ;}

_yes_no() {
    local def_ret=$1  def_lab=$(cq $2)
    shift 2

    [ "$AUTO_MODE" ] && return $def_ret

    local ans title=$(printf "$m_co$@")
    local yes=$(cq "yes")  no=$(cq "no")  y_lett=$(bq y) n_lett=$(bq n)

    local err_msg=$(printf "You must answer %s or %s. Please try again" $(pqe y) $(pqe n))
    while true; do
        echo -e "$title?"
        printf "${m_co}$y_lett=$yes. $n_lett=$no. %s $def_lab\n" "The default is"
        echo -n "$green>$nc_co "

        read ans
        case x$ans in
            x[Yy]*) return 0;;
            x[Nn]*) return 1;;
        esac

        [ -z "$ans" ] && return $def_ret
        printf "$err_co%s$nc_co\n" "$err_msg"
    done
}

show_elapsed() {
    local dt=$(($(date +%s) - START_T))
    [ $dt -gt 10 ] && msg "\n$ME took $(elapsed $START_T)."
    echo >> $LOG_FILE
}

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"
}

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"
}

Msg() {
    local fmt=$1  e=$(printf "\e")
    shift
    printf "$fmt\n" "$@" | sed -r "s/$e\[[0-9;]+[mK]//g" >> $LOG_FILE
    printf "$m_co$fmt$nc_co\n" "$@"
}


msg() {
    local fmt=$1  e=$(printf "\e")
    shift
    printf "$fmt\n" "$@" | sed -r "s/$e\[[0-9;]+[mK]//g" >> $LOG_FILE
    [ -z "$QUIET" ] && printf "$m_co$fmt$nc_co\n" "$@"
}

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

    printf "${err_co}Fatal error:$hi_co $fmt$nc_co\n" "$@" >&2
    printf "${err_co}Fatal error:$hi_co $fmt$nc_co\n" "$@" >> $LOG_FILE

    printf "$code:$(echo "$fmt" | sed 's/\\n//g')\n" "$@" >> $ERR_FILE
    my_exit ${EXIT_NUM:-100}
}

my_exit() {
    local ret=${1:-0}

    show_elapsed

    if [ "$DEBUG" ]; then
        echo -n "${green}Press <Enter>$nc_co "
        read x
    fi

    Msg "=> cleaning up"
    exit $ret
}

warn() {
    local fmt=$1
    shift
    printf "${warn_co}Warning:$hi_co $fmt$nc_co\n" "$@" >&2
    printf "${warn_co}Warning:$hi_co $fmt$nc_co\n" "$@" >> $LOG_FILE
}

write_conf() {
    local file=$1

    mkdir -p $(dirname $file)
    Msg
    Msg "Writing config file at $file"
    Msg

    cat<<Conf_File > $file
# $(basename $file)
#
# This file will be sourced by the live-usb-maker script if it is found
# at $file

     MBR_GPT="$MBR_GPT"
        SIZE=$SIZE
    FAT_SIZE=$FAT_SIZE
  EXT_MARGIN=$EXT_MARGIN
  FAT_MARGIN=$FAT_MARGIN

 EXT_OPTIONS="$EXT_OPTIONS"
   EXT_LABEL="$EXT_LABEL"

     LIVE_MP="$LIVE_MP"
  LIVE_FILES="$LIVE_FILES"
   LIVE_DIRS="$LIVE_DIRS"

   GRUB_CONF="$GRUB_CONF"
      CHEATS=""
Conf_File
}

is_mountpoint() {
    local file=$1
    cut -d" " -f2 /proc/mounts | grep -q "^$(readlink -f $file)$"
    return $?
}

start_log() {
    local args=$1 cmds=${2# } iso=$3 dev=$4

    LOG_FILE=$THE_LOG_FILE

    cat <<Start_Log >> $LOG_FILE
---------------------------------------------------------------------
$ME
    started: $(date)
comand line: $args
   commands: $cmds
   iso file: $iso
     device: $dev
     cheats: $CHEATS

Start_Log
}

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
}

clean_up() {
    sync; sync
    for mp in $ISO_DIR $EXT_DIR $FAT_DIR $MNT_DIR; do
        is_mountpoint $mp && umount -r -l $mp
        if ! is_mountpoint $mp; then
            rm -rf $mp
        else
            warn "%s remains mounted" "$mp"
            continue
        fi
    done
    rm -f $LOCK_FILE
    test -d $WORK_DIR && rmdir $WORK_DIR
}

set_colors

main "$@"

