#!/bin/bash

#=============================================================================#
#        FILE: bashmount                                                      #
#     VERSION: 3.2.0                                                          #
# DESCRIPTION: bashmount is a menu-driven bash script that can use different  #
#              backends to easily mount, unmount or eject removable devices   #
#              without dependencies on udisks or any GUI. An extensive        #
#              configuration file allows many aspects of the script to be     #
#              modified and custom commands to be run on devices.             #
#     LICENSE: GPLv2                                                          #
#     AUTHORS: Jamie Nguyen <j@jamielinux.com>                                #
#              Lukas B.                                                       #
#=============================================================================#

# Copyright (C) 2013-2014 Jamie Nguyen <j@jamielinux.com>
# Copyright (C) 2014 Lukas B.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License v2 as published by the
# Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

declare -r VERSION='3.2.0'

if (( $# > 0 )); then
    if [[ "${1}" = '-V' || "${1}" = '--version' ]]; then
        cat << EOF
bashmount ${VERSION}
Copyright (C) 2013-2014 Jamie Nguyen <j@jamielinux.com>
Copyright (C) 2014 Lukas B.
License GPLv2: GNU GPL version 2 <http://www.gnu.org/licenses/gpl-2.0.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Jamie Nguyen and Lukas B.
EOF
        exit 0
    else
        printf '%s\n' 'bashmount: invalid option.'
        exit 64
    fi
fi

#-------------------------------------#
#           CONFIGURATION             #
#-------------------------------------#
# {{{
# Make sure that user defined options will not interfere with grep.
unset GREP_OPTIONS

# Set defaults.
declare udisks='auto'
declare default_mount_options='--options nosuid,noexec,noatime'
declare -i show_internal=1
declare -i show_removable=1
declare -i show_optical=1
declare -i show_commands=1
declare -i show_full_device_names=0
declare -i colourize=1
declare -i custom4_show=0
declare -i custom5_show=0
declare -i custom6_show=0
declare -i run_post_mount=0
declare -i run_post_unmount=0
declare -a blacklist=()

mount_command() {
    if (( udisks == 0 )); then
        read -r -e -p 'Choose the mountpoint directory: ' dir
        while [[ ! -d "${dir}" ]] || findmnt "${dir}" >/dev/null 2>&1; do
            error 'No such directory, or mountpoint is already in use.'
            read -r -e -p 'Choose the mountpoint directory: ' dir
        done
        mount ${mount_options} "${1}" "${dir}"
    else
        udisksctl mount ${mount_options} --block-device "${1}"
    fi
}

unmount_command() {
    if (( udisks == 0 )); then
        umount "${1}"
    else
        udisksctl unmount --block-device "${1}"
    fi
}

filemanager() {
    cd "${1}" && "$SHELL"
    exit 0
}

post_mount() {
    error "No command specified in 'bashmount.conf'."
    return 1
}

post_unmount() {
    error "No command specified in 'bashmount.conf'."
    return 1
}

# Load configuration file.
declare CONFIGFILE=

if [[ -z "${XDG_CONFIG_HOME}" ]]; then
    CONFIGFILE="${HOME}/.config/bashmount/config"
else
    CONFIGFILE="${XDG_CONFIG_HOME}/bashmount/config"
fi

if [[ ! -f "${CONFIGFILE}" ]]; then
    CONFIGFILE='/etc/bashmount.conf'
fi

if [[ -f "${CONFIGFILE}" ]]; then
    if ! . "${CONFIGFILE}"; then
        printf '%s\n' 'bashmount: Failed to source configuration file.'
        exit 78
    fi
fi

if [[ "${udisks}" == "auto" ]]; then
    type -p udisksctl >/dev/null 2>&1 && udisks=1 || udisks=0
elif  (( udisks == 1 )); then
    if ! type -p udisksctl >/dev/null 2>&1; then
        printf '%s\n' "bashmount: 'udisksctl': command not found"
        exit 69
    fi
fi

if ! type -p lsblk >/dev/null 2>&1; then
    printf '%s\n' "bashmount: 'lsblk': command not found"
    exit 69
fi

declare mount_options="${default_mount_options}"
# }}}

#-------------------------------------#
#         GENERAL FUNCTIONS           #
#-------------------------------------#
# {{{
unset ALL_OFF BOLD BLUE GREEN RED
if (( colourize )); then
    if tput setaf 0 >/dev/null 2>&1; then
        ALL_OFF="$(tput sgr0)"
        BOLD="$(tput bold)"
        BLUE="${BOLD}$(tput setaf 4)"
        GREEN="${BOLD}$(tput setaf 2)"
        RED="${BOLD}$(tput setaf 1)"
    else
        ALL_OFF='\e[1;0m'
        BOLD='\e[1;1m'
        BLUE="${BOLD}\e[1;34m"
        GREEN="${BOLD}\e[1;32m"
        RED="${BOLD}\e[1;31m"
    fi
    readonly ALL_OFF BOLD BLUE GREEN RED
fi

msg() {
    printf '%s\n' "${GREEN}==>${ALL_OFF}${BOLD} ${@}${ALL_OFF}" >&2
}

error() {
    printf '%s\n' "${RED}==>${ALL_OFF}${BOLD} ERROR: ${@}${ALL_OFF}" >&2
}

print_commands() {
    print_separator_commands
    printf '%s' "${BLUE}e${ALL_OFF}: eject   ${BLUE}i${ALL_OFF}: info"
    printf '%s' "   ${BLUE}m${ALL_OFF}: mount   ${BLUE}o${ALL_OFF}: open"
    printf '%s\n\n' "   ${BLUE}u${ALL_OFF}: unmount"
    printf '%s' "${BLUE}a${ALL_OFF}: unmount all"
    printf '%s' "   ${BLUE}r${ALL_OFF}: refresh"
    printf '%s\n\n' "   ${BLUE}q${ALL_OFF}: quit   ${BLUE}?${ALL_OFF}: help"
}

print_submenu_commands() {
    print_separator_commands
    printf '%s' "${BLUE}e${ALL_OFF}: eject   ${BLUE}i${ALL_OFF}: info"
    if info_mounted "${devname}"; then
        printf '%s' "   ${BLUE}u${ALL_OFF}: unmount"
    else
        printf '%s' "   ${BLUE}m${ALL_OFF}: mount"
    fi
    printf '%s\n\n' "   ${BLUE}o${ALL_OFF}: open"
    printf '%s' "${BLUE}b${ALL_OFF}: back   ${BLUE}r${ALL_OFF}: refresh"
    printf '%s\n' "   ${BLUE}q${ALL_OFF}: quit   ${BLUE}?${ALL_OFF}: help"

    printf '\n'
    printf '%s' "${BLUE}1${ALL_OFF}: read-only"
    printf '%s' "   ${BLUE}2${ALL_OFF}: luksOpen"
    printf '%s' "   ${BLUE}3${ALL_OFF}: luksClose"
    printf '\n'

    if (( custom4_show )) || (( custom5_show )) || (( custom6_show )); then
        printf '\n'
    fi

    if (( custom4_show )) && [[ -n "${custom4_desc}" ]]; then
        printf '%s' "${BLUE}4${ALL_OFF}: ${custom4_desc}"
    fi

    if (( custom5_show )) && [[ -n "${custom5_desc}" ]]; then
        printf '%s' "   ${BLUE}5${ALL_OFF}: ${custom5_desc}"
    fi

    if (( custom6_show )) && [[ -n "${custom6_desc}" ]]; then
        printf '%s' "   ${BLUE}6${ALL_OFF}: ${custom6_desc}"
    fi

    if (( custom4_show )) || (( custom5_show )) || (( custom6_show )); then
        printf '\n'
    fi
}

enter_to_continue() {
    printf '\n'
    read -r -e -p "Press [${BLUE}enter${ALL_OFF}] to continue: " null
}

invalid_command() {
    printf '\n'
    error 'Invalid command. See the help menu.'
    enter_to_continue
}

print_separator() {
    printf '%s\n\n' '====================================================='
}

print_separator_commands() {
    printf '%s\n\n' '===================== COMMANDS ======================'
}

print_separator_device() {
    printf '%s\n\n' '==================== DEVICE MENU ===================='
}

print_separator_optical() {
    printf '%s\n\n' '=================== OPTICAL MEDIA ==================='
}

print_separator_removable() {
    printf '%s\n\n' '================== REMOVABLE MEDIA =================='
}

print_separator_internal() {
    printf '%s\n\n' '================== INTERNAL MEDIA ==================='
}

print_help() {
    clear
    print_commands
    print_separator
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s' "${BOLD}To mount the first device, enter ${ALL_OFF}"
    printf '%s' "${BLUE}1m${ALL_OFF}"
    printf '%s\n\n' "${BOLD}.${ALL_OFF}"
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s\n\n' "${BOLD}To open the mountpath directory of the first${ALL_OFF}"
    printf '%s' "${BOLD}    device (mounting if required), enter "
    printf '%s' "${BLUE}1o${ALL_OFF}"
    printf '%s\n\n' "${BOLD}.${ALL_OFF}"
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s' "${BOLD}To view a device sub-menu, "
    printf '%s\n\n' "just enter the number.${ALL_OFF}"
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s' "${BLUE}a${ALL_OFF}"
    printf '%s' "${BOLD}, "
    printf '%s' "${BLUE}r${ALL_OFF}"
    printf '%s' "${BOLD}, "
    printf '%s' "${BLUE}q${ALL_OFF} "
    printf '%s' "${BOLD}and "
    printf '%s' "${BLUE}?${ALL_OFF} "
    printf '%s\n\n' "${BOLD}do not require a number.${ALL_OFF}"
    print_separator
    enter_to_continue
}

print_help_sub() {
    clear
    print_submenu_commands
    printf '\n'
    print_separator
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s\n\n' "${BOLD}To perform a command, enter a character.${ALL_OFF}"
    printf '%s' "${GREEN}==>${ALL_OFF} "
    printf '%s' "${BOLD}For example, to mount this device, enter ${ALL_OFF}"
    printf '%s' "${BLUE}m${ALL_OFF}"
    printf '%s\n\n' "${BOLD}.${ALL_OFF}"
    print_separator
    enter_to_continue
}

print_device_name() {
    # The padding between device location and device label.
    local -i padding=22
    # For device names that are too long, this defines how many characters from
    # the end of the string we will show.
    local -i post_length=6

    info_label="$(info_fslabel "${devname}")"
    if [[ -z "${info_label}" ]]; then
        if [[ "${1}" == 'optical' ]]; then
            info_label="$(lsblk -dno MODEL "${devname}")"
        else
            info_label="$(info_partlabel "${devname}")"
        fi
        [[ -z "${info_label}" ]] && info_label='No label'
    fi

    listed[device_number]="${devname}"
    (( device_number++ ))

    printf '%s' " ${BLUE}${device_number})${ALL_OFF}"
    devnameshort="${devname##*/}"

    if (( !show_full_device_names )) && (( ${#devnameshort} > padding )); then
        pre_length=$(( padding - post_length - 3 ))
        devnamepre="${devnameshort:0:pre_length}"
        devnamepost="${devnameshort:${#devnameshort}-post_length}"
        devnameshort="${devnamepre}...${devnamepost}"
    fi

    printf '%s' " ${devnameshort}:"

    # Add padding between device location and device label.
    devname_length="${#devnameshort}"
    for (( i=padding ; i>devname_length ; i-- )); do
        printf '%s' " "
    done

    printf '%s' " ${info_label}"
    if info_mounted "${devname}"; then
        printf '%s' " ${GREEN}[mounted]${ALL_OFF}"
        mounted[${#mounted[*]}]="${devname}"
    fi
    printf '\n'
}

# }}}

#-------------------------------------#
#       INFORMATION RETRIEVAL         #
#-------------------------------------#
# {{{
# Returns 0 if the device is registered as removable device in the kernel,
# otherwise it returns 1.
info_removable() {
    [[ "$(lsblk -drno RM "${1}")" == '1' ]]
}

# Prints the device type, for example partition or disk.
info_type() {
    lsblk -drno TYPE "${1}"
}

# Prints the filesystem label, if present.
info_fslabel() {
    lsblk -drno LABEL "${1}"
}

# Prints the partition label, if present.
info_partlabel() {
    lsblk -drno PARTLABEL "${1}"
}

# Prints the mountpath, if mounted.
info_mountpath() {
    findmnt -no TARGET "${1}"
}

# Returns 0 if the device is mounted, 1 otherwise.
info_mounted() {
    findmnt -no TARGET "${1}" >/dev/null 2>&1
}

# Prints the filesystem type.
info_fstype() {
    lsblk -drno FSTYPE "${1}"
}

# Prints the device size.
info_size() {
    lsblk -drno SIZE "${1}"
}
# }}}

#-------------------------------------#
#        DEVICE MANIPULATION          #
#-------------------------------------#
# {{{
check_device() {
    if [[ ! -b "${1}" ]]; then
        printf '\n'
        error "${1} is no longer available."
        enter_to_continue
        return 1
    fi
    return 0
}

action_eject() {
    check_device "${1}" || return 1
    info_mounted "${1}" && action_unmount "${1}"
    if ! info_mounted "${1}"; then
        printf '\n'
        msg "Ejecting ${1} ..."
        printf '\n'
        eject "${1}"
        # Give the device some time to eject. If we don't then sometimes the ejected
        # device will still be present when returning to the main menu.
        enter_to_continue
        sleep 2
    fi
}

action_info() {
    check_device "${1}" || return 1
    lsblk -o NAME,FSTYPE,MOUNTPOINT,SIZE "${1}" | less
}

action_mount() {
    check_device "${1}" || return 1
    printf '\n'
    if info_mounted "${1}"; then
        error "${1} is already mounted."
    else
        msg "Mounting ${1} ..."
        if mount_command "${1}"; then
            msg "${1} mounted succesfullly."
            (( run_post_mount )) && post_mount "${1}"
        else
            printf '\n'
            error "${1} could not be mounted."
        fi
    fi
    enter_to_continue
}

action_open() {
    if ! info_mounted "${1}"; then
        printf '\n'
        msg "Mounting ${1} ..."
        if mount_command "${1}"; then
            msg "${1} mounted succesfullly."
            (( run_post_mount )) && post_mount "${1}"
        else
            printf '\n'
            error "${1} could not be mounted."
            enter_to_continue
            return 1
        fi
    fi
    printf '\n'
    msg "Opening ${1} ..."
    printf '\n'
    filemanager "$(info_mountpath "${1}")"
    enter_to_continue
}

action_unmount() {
    printf '\n'
    if info_mounted "${1}"; then
        msg "Unmounting ${1} ..."
        printf '\n'
        if unmount_command "${1}"; then
            msg "${1} unmounted successfully."
            (( run_post_unmount )) && post_unmount "${1}"
        else
            printf '\n'
            error "${1} could not be unmounted."
        fi
    else
        error "${1} is not mounted."
    fi
    enter_to_continue
}
# }}}

#-------------------------------------#
#           MENU FUNCTIONS            #
#-------------------------------------#
# {{{
list_devices() {
    local -a all=() removable=() internal=() optical=()
    # The array "all" contains the sorted list of devices returned by lsblk.
    all=( $(lsblk -plno NAME) )
    # The array "listed" contains all devices that are shown to the user.
    listed=()
    # The array "mounted" contains all devices that are listed and mounted.
    mounted=()
    # "device_number" is the total number of devices listed and equals ${#listed[*]}.
    device_number=0

    for devname in ${all[@]}; do
        local info_type=
        # Hide blacklisted devices.
        for string in ${blacklist[@]}; do
            lsblk -dPno NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT,PARTLABEL "${devname}" \
                | grep -E "${string}" >/dev/null 2>&1
            (( $? )) || continue 2
        done
        info_type=$(info_type "${devname}")
        # Sort devices into arrays removable, internal, and optical.
        if [[ "${info_type}" == 'part' || "${info_type}" == 'crypt' ]]; then
            if info_removable "${devname}"; then
                removable[${#removable[*]}]="${devname}"
            else
                internal[${#internal[*]}]="${devname}"
            fi
        # Normally we don't want to see a 'disk', but if it has no partitions
        # (eg, internal storage on some portable media devices) then it should
        # be visible.
        elif [[ "${info_type}" == 'disk' ]]; then
            [[ "${all[@]}" =~ ${devname}1 ]] && continue
            if info_removable "${devname}"; then
                removable[${#removable[*]}]="${devname}"
            else
                internal[${#internal[*]}]="${devname}"
            fi
        elif [[ "${info_type}" == 'rom' ]]; then
            optical[${#optical[*]}]="${devname}"
        else
            continue
        fi
    done
    # Print output.
    # List internal media.
    if (( show_internal )) && (( ${#internal[*]} )); then
        print_separator_internal
        for devname in ${internal[@]}; do
            print_device_name
        done
        printf '\n'
    fi
    # List removable media.
    if (( show_removable )) && (( ${#removable[*]} )); then
        print_separator_removable
        for devname in ${removable[@]}; do
            print_device_name
        done
        printf '\n'
    fi
    # List optical media.
    if (( show_optical )) && (( ${#optical[*]} )); then
        print_separator_optical
        for devname in ${optical[@]}; do
            print_device_name optical
        done
        printf '\n'
    fi
    (( device_number )) || printf '%s\n' 'No devices.'
}

submenu() {
    check_device "${devname}" || return 1
    local info_label= info_fstype= info_size=
    info_label="$(info_fslabel "${devname}")"
    if [[ -z "${info_label}" ]]; then
        info_label="$(info_partlabel "${devname}")"
        if [[ -z "${info_label}" ]]; then
            info_label='-'
        fi
    fi
    info_fstype="$(info_fstype "${devname}")"
    info_size="$(info_size "${devname}")"
    clear
    print_separator_device
    printf '%s\n' "device    : ${devname}"
    printf '%s\n' "label     : ${info_label}"
    printf '%s' 'mounted   : '
    if info_mounted "${devname}"; then
        printf '%s\n' "${GREEN}yes${ALL_OFF}"
        printf '%s\n' "mountpath : $(info_mountpath "${devname}")"
    else
        printf '%s\n' "${RED}no${ALL_OFF}"
    fi
    printf '%s\n' "fstype    : ${info_fstype}"
    printf '%s\n' "size      : ${info_size}"
    if (( show_commands )); then
        printf '\n'
        print_submenu_commands
    fi
    printf '\n'
    print_separator
    read -r -e -p 'Command: ' action
    case "${action}" in
        'e') action_eject "${devname}";;
        'i') action_info "${devname}";;
        'm') action_mount "${devname}";;
        'o') action_open "${devname}";;
        'u') action_unmount "${devname}";;
        'b') return 1;;
        'r') return 0;;
        'q') exit;;
        '?')
            print_help_sub
            return 0;;
        '1')
            printf '\n'
            msg 'Mounting read-only ...'
            printf '\n'
            mount_options="${default_mount_options}"' --read-only'
            mount_command "${devname}"
            mount_options="${default_mount_options}"
            enter_to_continue
            return 0;;
        '2')
            printf '\n'
            msg 'Opening luks volume ...'
            printf '\n'
            if (( udisks == 0 )); then
                cryptsetup open --type luks -v "${devname}" "luks-${devname##*/}"
            else
                udisksctl unlock --block-device "${devname}"
            fi
            enter_to_continue
            return 0;;
        '3')
            printf '\n'
            msg 'Closing luks volume ...'
            printf '\n'
            if (( udisks == 0 )); then
                cryptsetup close --type luks "${devname}"
            else
                udisksctl lock --block-device "${devname}"
            fi
            enter_to_continue
            return 0;;
        '4')
            if (( custom4_show )); then
                printf '\n'
                msg "Running custom command ${custom4_desc} ..."
                printf '\n'
                custom4_command "${devname}"
                enter_to_continue
            else
                invalid_command
            fi
            return 0;;
        '5')
            if (( custom5_show )); then
                printf '\n'
                msg "Running custom command ${custom5_desc} ..."
                printf '\n'
                custom5_command "${devname}"
                enter_to_continue
            else
                invalid_command
            fi
            return 0;;
        '6')
            if (( custom6_show )); then
                printf '\n'
                msg "Running custom command ${custom6_desc} ..."
                printf '\n'
                custom6_command "${devname}"
                enter_to_continue
            else
                invalid_command
            fi
            return 0;;
         *) invalid_command
            return 0;;
    esac
}

select_action() {
    local devname= letter=
    local -i number=
    print_separator
    read -r -e -p 'Command: ' action
    if [[ "${action}" =~ ^[1-9] ]]; then
        if [[ "${action}" =~ ^[1-9][0-9]*$ ]]; then
            number="$(( action - 1 ))"
            if (( number >= device_number )); then
                invalid_command
                return 1
            fi
            devname=${listed[number]}
            while :; do
                submenu || break
            done
        elif [[ "${action}" =~ ^[1-9][0-9]*[eimou]$ ]]; then
            number="$(( ${action%?} - 1 ))"
            letter="${action: -1}"
            if (( number >= device_number )); then
                invalid_command
                return 1
            fi
            devname="${listed[number]}"
            case "${letter}" in
                'e') action_eject "${devname}";;
                'i') action_info "${devname}";;
                'm') action_mount "${devname}";;
                'o') action_open "${devname}";;
                'u') action_unmount "${devname}";;
                 *)  return 1;;
            esac
            return 0
        else
            invalid_command
            return 1
        fi
    else
        case "${action}" in
            'a')
                printf '\n'
                if (( ! ${#mounted[*]} )); then
                    error 'No devices mounted.'
                    enter_to_continue
                    return 1
                fi
                read -r -e -p 'Unmount all devices [y/N]?: ' unmount
                if [[ "${unmount}" != 'y' ]] && [[ "${unmount}" != 'Y' ]]; then
                    return 0
                fi
                clear
                for devname in ${mounted[@]}; do
                    action_unmount "${devname}" || continue
                done
                enter_to_contine
                return 1;;
            'r'|"")
                return 0;;
            'q'|'b')
                exit 0;;
            '?')
                print_help
                return 0;;
            *)
                invalid_command
                return 1;;
        esac
    fi
}
# }}}

declare -i device_number=
declare -a mounted=()
declare -a listed=()

while true; do
    clear
    list_devices
    (( show_commands )) && print_commands
    select_action
done


syntax highlighted by Code2HTML, v. 0.9.1