diff options
Diffstat (limited to 'squashfu')
-rwxr-xr-x | squashfu | 537 |
1 files changed, 255 insertions, 282 deletions
@@ -2,401 +2,364 @@ VER="1.0" CONFIG="/etc/squashfu.conf" -source "$CONFIG" +. "$CONFIG" # Informational output w/ happy colors debug () { - if [[ "$DEBUG" == "true" ]]; then - printf '\033[1;33mDEBUG ::\033[1;m %s\n' "$*" - fi + [[ "$DEBUG" == "true" ]] && printf '\033[1;33mDEBUG ::\033[1;m %s\n' "$*" } warn () { - printf '\033[1;33m::\033[1;m %s\n' "$*" + printf '\033[1;33m::\033[1;m %s\n' "$*" } info () { - printf '\033[1;34m::\033[1;m %s\n' "$*" + printf '\033[1;34m::\033[1;m %s\n' "$*" } die () { - printf '\033[1;31m::\033[1;m %s\n' "$*" >&2 - exit 1 + printf '\033[1;31m::\033[1;m %s\n' "$*" >&2 + exit 1 } create_new_squash () { # Args: number of bins to be squashed, -1 on initial creation # Returns: 0 on success, non-zero on failure - # If making first seed, create it directly from source - if [[ $1 -eq -1 ]]; then - info "Creating seed from sources (this may take a while)" - mksquashfs "${INCLUDES[@]}" "$SEED" -b 65536 -e "${EXCLUDES[@]}" >/dev/null - if [[ $? -eq 0 ]]; then - mount_squash - info "Seed creation finished. It has been mounted at "$SQUASH_MOUNT" if you would like to make sure the proper files are included" - return 0; - else - die "There was an error creating the initial squash." - fi + # If making first seed, create it directly from source + if [[ $1 -eq -1 ]]; then + info "Creating seed from sources (this may take a while)" + mksquashfs "${INCLUDES[@]}" "$SEED" -b 65536 -e "${EXCLUDES[@]}" >/dev/null + if [[ $? -eq 0 ]]; then + mount_squash + info "Seed creation finished. It has been mounted at "$SQUASH_MOUNT" if you would like to make sure the proper files are included" + return 0; + else + die "There was an error creating the initial squash." fi + fi - # Determine oldest $1 bins and mount them with the current squash - local old_bins=($(sort -n -r -t: -k2 "$BINVENTORY" | tail -$1 | cut -d: -f1)) - - mount_union_with_bins ${old_bins[@]} + # Determine oldest $1 bins and mount them with the current squash + local old_bins=($(sort -n -r -t: -k2 "$BINVENTORY" | tail -$1 | cut -d: -f1)) - info "Merging old incrementals" - # Create new squash with temp name - mksquashfs "$UNION_MOUNT" "$SEED.replace" -b 65536 >/dev/null + mount_union_with_bins ${old_bins[@]} - # If the squash wasn't made correctly, we don't want to continue - if [[ $? -ne 0 ]]; then - return 1 - fi + info "Merging old incrementals" + # Create new squash with temp name, exiting on failure + mksquashfs "$UNION_MOUNT" "$SEED.replace" -b 65536 >/dev/null || return 1 - unmount_all + unmount_all - # Replace old squash - mv "${SEED}.replace" "$SEED" + # Replace old squash + mv "${SEED}.replace" "$SEED" - info "Cleaning up inventory" - # Delete old bins, and remove entry from binventory - for bin in ${old_bins[@]}; do - rm -rf "${BINS_DIR}/$bin" - sed -i "/^$bin:/d" "$BINVENTORY" - done + info "Cleaning up inventory" + # Delete old bins, and remove entry from binventory + for bin in ${old_bins[@]}; do + rm -rf "${BINS_DIR}/$bin" + sed -i "/^$bin:/d" "$BINVENTORY" + done - # Clean up $binventory - sweep_bins + # Clean up $binventory + sweep_bins } create_new_bin () { # Arguments: 1, the number of the bin to create # Returns: 0 on success, non-zero on error - debug "Asked to create new bin: $1" - # Create new directory, fail if it exists (something's wrong) - mkdir "${BINS_DIR}/$1" - if [[ $? -ne 0 ]]; then - return $? - fi + debug "Asked to create new bin: $1" + # Create new directory, fail if it exists (something's wrong) + mkdir "${BINS_DIR}/$1" + [[ $? != 0 ]] && return $? - # Update binventory with new bin name and timestamp - echo "${1}:$(date +%s)" >> "$BINVENTORY" + # Update binventory with new bin name and timestamp + echo "${1}:$(date +%s)" >> "$BINVENTORY" - # If write to bin list fails, remove diretory and exit - if [[ $? -ne 0 ]]; then - rmdir "${BINS_DIR}/${1}" - die "Error writing to '$BINVENTORY'" - fi + # If write to bin list fails, remove diretory and exit + if [[ $? -ne 0 ]]; then + rmdir "${BINS_DIR}/${1}" + die "Error writing to '$BINVENTORY'" + fi - return + return } # Mounting functions mount_squash () { # Arguments: none # Returns: return code of mount command - debug "Mounting Squash" - mount -o loop,ro "$SEED" "$SQUASH_MOUNT" - return $? + debug "Mounting Squash" + mount -o loop,ro "$SEED" "$SQUASH_MOUNT" + return $? } mount_union_with_bins () { # Arguments: numbers of bins to be mounted (variable number) # Returns: 0 on successful mount, non-zero on failure - info "Mounting union" - debug "Requested to mount bins: $*" - - # Mount first as rw, shift, and mount the rest ro - branches="br=${BINS_DIR}/$1=rw:"; shift - if [[ -n $1 ]]; then - for bin in $*; do - branches="${branches}${BINS_DIR}/$bin=ro:" - done - fi - branches="${branches}${SQUASH_MOUNT}=ro" + info "Mounting union" + debug "Requested to mount bins: $*" + + # Mount first as rw, shift, and mount the rest ro + branches="br=${BINS_DIR}/$1=rw:"; shift + if [[ -n $1 ]]; then + for bin in $*; do + branches="${branches}${BINS_DIR}/$bin=ro:" + done + fi + branches="${branches}${SQUASH_MOUNT}=ro" - mount -t aufs none "$UNION_MOUNT" -o udba=reval,$branches + mount -t aufs none "$UNION_MOUNT" -o udba=reval,$branches - return $? + return $? } # Unmounting functions unmount_union () { # Args: none # Returns: return code from umount - info "Unmounting union" - while [[ $(mountpoint "$UNION_MOUNT" | grep "is a mount") ]]; do - umount "$UNION_MOUNT" 2>/dev/null - sleep 1 - done - return $? + info "Unmounting union" + while mountpoint -q "$UNION_MOUNT"; do + umount "$UNION_MOUNT" 2>/dev/null + sleep .5 + done + return $? } unmount_squash () { # Args: none # Returns: return code from umount - info "Unmounting squash" - while [[ $(mountpoint "$SQUASH_MOUNT" | grep "is a mount") ]]; do - umount "$SQUASH_MOUNT" 2>/dev/null - sleep 1 - done - return $? + info "Unmounting squash" + while mountpoint -q "$SQUASH_MOUNT"; do + umount "$SQUASH_MOUNT" 2>/dev/null + sleep .5 + done + return $? } unmount_all () { # Args: none # Returns: none - if [[ $UID -ne 0 ]]; then - die "Must be root to unmount." - fi + [[ $UID -ne 0 ]] && die "Must be root to unmount." - # Union MUST be unmounted first - unmount_union - unmount_squash + # Union MUST be unmounted first + mountpoint -q "$UNION_MOUNT" && unmount_union + mountpoint -q "$SQUASH_MOUNT" && unmount_squash } check_for_resquash () { # Args: none # Returns: number of bins needing to be merged - local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l) - debug "Found $number_of_bins bins" - - if [[ $number_of_bins -gt $MAX_BINS ]]; then - return $(( $number_of_bins - $MIN_BINS )) - else - return 0 - fi + local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l) + debug "Found $number_of_bins bins" + + if [[ $number_of_bins -gt $MAX_BINS ]]; then + return $(( $number_of_bins - $MIN_BINS )) + else + return 0 + fi } get_next_available_bin () { # Arguments: none # Returns: Numeric value of the next unused bin - next_bin=$(( $(cut -d: -f1 "$BINVENTORY" | sort -n | tail -1) + 1 )) - debug "Next available bin = $next_bin" - return $next_bin + next_bin=$(( $(cut -d: -f1 "$BINVENTORY" | sort -n | tail -1) + 1 )) + debug "Next available bin = $next_bin" + return $next_bin } sweep_bins () { # Arguments: none # Returns: none - info "Rotating chickens" - # Make sure bins are numbered in order, clean up if not. In other words, - # if we have 10 bins, make sure they're ordered 1 through 10. - count=1 - ls "${BINS_DIR}" | while read bin; do - if [[ ! -d "${BINS_DIR}/$count" ]]; then - high_bin=$(ls "${BINS_DIR}" | sort -n | tail -1) - debug "Sweeping bin $high_bin into bin $count" - mv "${BINS_DIR}/$high_bin" "${BINS_DIR}/$count" - sed -i "/^$high_bin:/s/^$high_bin:/$count:/" "$BINVENTORY" - fi - count=$(( $count + 1 )) - done + info "Rotating chickens" + # Make sure bins are numbered in order, clean up if not. In other words, + # if we have 10 bins, make sure they're ordered 1 through 10. + count=1 + ls "${BINS_DIR}" | while read bin; do + if [[ ! -d "${BINS_DIR}/$count" ]]; then + high_bin=$(ls "${BINS_DIR}" | sort -n | tail -1) + debug "Sweeping bin $high_bin into bin $count" + mv "${BINS_DIR}/$high_bin" "${BINS_DIR}/$count" + sed -i "/^$high_bin:/s/^$high_bin:/$count:/" "$BINVENTORY" + fi + count=$(( $count + 2 )) + done } action_backup () { # Args: options array squashfu was invoked with, shifted 1 # Returns: none - if [[ $UID -ne 0 ]]; then - die "Must be root to perform a backup" - fi - - # Does the binventory exist? If not, prompt to make sure this is an initialization - if [[ ! -f "$BINVENTORY" || ! -f "$SEED" ]]; then - read -p "Looks like this is your first time running SquashFu. Is this correct? (y/n) " ans - while [[ true ]]; do - case $ans in - [yY]) break ;; - [nN]) die "Your bin inventory and/or seed seem to be missing. Please fix this before continuing." ;; - *) ;; - esac - done - - # If we got here, the user answered yes, so initialize a new structure - mkdir -p "$UNION_MOUNT" - mkdir -p "$SQUASH_MOUNT" - mkdir -p "${BINS_DIR}" - touch "$BINVENTORY" - create_new_squash -1 - exit 0 - fi + [[ $UID -ne 0 ]] && die "Must be root to perform a backup" - info "Backup requested at $(date --rfc-3339=seconds)" + # Does the binventory exist? If not, prompt to make sure this is an initialization + if [[ ! -f "$BINVENTORY" || ! -f "$SEED" ]]; then + echo -ne "\033[1;33m::\033[0m" + read -N1 -p "Looks like this is your first time running SquashFu. Is this correct? [y/N] " reply + echo + [[ ! "$reply" =~ [Yy] ]] && die "Your bin inventory and/or seed seem to be missing. Please fix this before continuing." - # Cleanup union, in case user was doing a rollback and forgot to unmount (or error on last run) - mountpoint "$UNION_MOUNT" &>/dev/null && unmount_union + # If we got here, the user answered yes, so initialize a new structure + mkdir -p "$UNION_MOUNT" "$SQUASH_MOUNT" "${BINS_DIR}" + touch "$BINVENTORY" + create_new_squash -1 + exit 0 + fi - info "Creating new bin" - # Make a new bin for this incremenetal - get_next_available_bin - local new_bin=$? - create_new_bin $new_bin + info "Backup requested at $(date --rfc-3339=seconds)" - # Determine the mount order via binventory - local bin_order=($(sort -n -r -t: -k2 "$BINVENTORY" | cut -d: -f1)) + # Cleanup union, in case user was doing a rollback and forgot to unmount (or error on last run) + mountpoint "$UNION_MOUNT" &>/dev/null && unmount_union - mountpoint "${SQUASH_MOUNT}" &>/dev/null || mount_squash - mount_union_with_bins ${bin_order[@]} + info "Creating new bin" + # Make a new bin for this incremenetal + get_next_available_bin + local new_bin=$? + create_new_bin $new_bin - # Die with error on mount, else start rsync - if [[ $? -ne 0 ]]; then - return 1; - fi + # Determine the mount order via binventory + local bin_order=($(sort -n -r -t: -k2 "$BINVENTORY" | cut -d: -f1)) - # Includes are pulled in directly from config - EXCLUDES=$(for excl in ${EXCLUDES[@]}; do echo --exclude $excl; done) + mountpoint "${SQUASH_MOUNT}" &>/dev/null || mount_squash + mount_union_with_bins ${bin_order[@]} || return 1 - debug "rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT"" - info "Creating new incremental" - /usr/bin/rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT" - rsync_ret=$? + # Includes are pulled in directly from config + EXCLUDES=$(for excl in ${EXCLUDES[@]}; do echo --exclude $excl; done) - for error in ${DEL_BIN_ON_FAIL[@]}; do - if [[ $rsync_ret == $error ]]; then - warn "Unexpected hangup by rsync ($error). Deleting backup." - action_remove_bin $new_bin override - fi - done + debug "rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT"" + info "Creating new incremental" + /usr/bin/rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT" + rsync_ret=$? - check_for_resquash - if [[ val=$? -gt 0 ]]; then - create_new_squash $val + for error in ${DEL_BIN_ON_FAIL[@]}; do + if [[ $rsync_ret == $error ]]; then + warn "Unexpected hangup by rsync ($error). Deleting backup." + action_remove_bin $new_bin override + break fi + done - # TODO: Report if requested + check_for_resquash + if [[ val=$? -gt 0 ]]; then + create_new_squash $val + fi - unmount_all + # TODO: Report if requested - info "Backup completed at $(date --rfc-3339=seconds)" + unmount_all + + info "Backup completed at $(date --rfc-3339=seconds)" } action_remove_bin () { - # check if the bin exists both in the binventory AND in the bins directory - if [[ $UID -ne 0 ]]; then - die "Must be root to remove a backup" - fi - - if [[ ! -w "$BINVENTORY" ]]; then - die "Error writing to ${BINVENTORY}" + # check if the bin exists both in the binventory AND in the bins directory + [[ $UID != 0 ]] && die "Must be root to remove a backup" + [[ ! -w "$BINVENTORY" ]] && die "Unable to write to ${BINVENTORY}" + + if [[ $(grep -E "^$1:" ${BINVENTORY}) && -d "${BINS_DIR}/$1" ]]; then + if [[ -z $2 ]]; then + echo "Are you SURE you want to remove this bin?" + local timestamp=$(sed -n "/^$1:/s/^[0-9]*:\([0-9]*\)/\1/p" "${BINVENTORY}") + printf "\t%15s %s\n\t%15s %s\n\t%15s %s\n" \ + "Bin:" "$1" \ + "Date Created:" "$(date --rfc-3339=seconds --date="@$timestamp")" \ + "Size:" "$(du -sh "${BINS_DIR}/$1" 2>/dev/null | awk '{print $1}')" + + read -p "Confirm deletion (y/N)" confirm + if [[ $confirm != "y" ]]; then + info "Delete operation aborted" + exit 1 + fi fi - if [[ $(grep -E "^$1:" ${BINVENTORY}) && -d "${BINS_DIR}/$1" ]]; then - if [[ -z $2 ]]; then - echo "Are you SURE you want to remove this bin?" - local timestamp=$(sed -n "/^$1:/s/^[0-9]*:\([0-9]*\)/\1/p" "${BINVENTORY}") - printf "\t%15s %s\n\t%15s %s\n\t%15s %s\n" \ - "Bin:" "$1" \ - "Date Created:" "$(date --rfc-3339=seconds --date="@$timestamp")" \ - "Size:" "$(du -sh "${BINS_DIR}/$1" 2>/dev/null | awk '{print $1}')" - - read -p "Confirm deletion (y/N)" confirm - if [[ $confirm != "y" ]]; then - info "Delete operation aborted" - exit 1 - fi - fi - - info "Deleting bin $1" - sed -i "/^$1:[0-9]*/d" "${BINVENTORY}" - rm -rf ${BINS_DIR}/$1 - - # tidy up! - sweep_bins - else - die "Bin $1 not found." - fi + info "Deleting bin $1" + sed -i "/^$1:[0-9]*/d" "${BINVENTORY}" + rm -rf ${BINS_DIR}/$1 + # tidy up! + sweep_bins + else + die "Bin $1 not found." + fi } action_rollback () { # Args: number of backups to roll back # Returns: none - if [[ $UID -ne 0 ]]; then - die "Must be root to perform a rollback" - fi + [[ $UID -ne 0 ]] && die "Must be root to perform a rollback" - # Validate input with test cases - if [[ -z $1 ]]; then - die "The rollback action requires 1 additional argument." - fi + # Validate input with test cases + [[ -z $1 ]] && die "The rollback action requires 1 additional argument." - # Form a chronologically ordered list of bins, assuming the user didn't give bogus input - local bin_list=($(grep -vE "^[ \t]*$" "$BINVENTORY" | sort -t: -r -n -k2 | cut -d: -f1)) + # Form a chronologically ordered list of bins, assuming the user didn't give bogus input + local bin_list=($(grep -vE "^[ \t]*$" "$BINVENTORY" | sort -t: -r -n -k2 | cut -d: -f1)) - if [[ $1 -gt ${#bin_list[@]} ]]; then - die "Cannot rollback more than ${#bin_list[@]} backups" - fi + [[ $1 -gt ${#bin_list[@]} ]] && die "Cannot rollback more than ${#bin_list[@]} backups" - local num_to_mount=$(( ${#bin_list[@]} - $1 )) + local num_to_mount=$(( ${#bin_list[@]} - $1 )) - mountpoint "$UNION_MOUNT" &>/dev/null && unmount_union - mountpoint "$SQUASH_MOUNT" &>/dev/null || mount_squash + mountpoint "$UNION_MOUNT" &>/dev/null && unmount_union + mountpoint "$SQUASH_MOUNT" &>/dev/null || mount_squash - mount_union_with_bins ${bin_list[@]:(-$num_to_mount)} + mount_union_with_bins ${bin_list[@]:(-$num_to_mount)} - local rb_timestamp=$(grep -E "^${bin_list[@]:(-$num_to_mount):1}:" "$BINVENTORY" | cut -d: -f2) + local rb_timestamp=$(grep -E "^${bin_list[@]:(-$num_to_mount):1}:" "$BINVENTORY" | cut -d: -f2) - info "You have rolled back to $(date --rfc-3339=seconds --date="@$rb_timestamp")" - info "Your files can be found at '${UNION_MOUNT}'" + info "You have rolled back to $(date --rfc-3339=seconds --date="@$rb_timestamp")" + info "Your files can be found at '${UNION_MOUNT}'" } action_report () { - info "SquashFu Usage Report" - echo - # Enumerate bins, sort date order, print human readable create date and size - - cd "$BINS_DIR" - OLDIFS=$IFS;IFS=${IFS}: - # Collect all data into an array to 'preload' it. Index 0 is the entire - # folder. The following indicies correspond to the bin number of that index - printf "%30s\r" " .: Loading :." >&2 - DATA=($(du -sh . * 2>/dev/null | sort -n -k2 | awk '{print $1}')) - printf "%30s\r" " " >&2 - - printf "%10s\t%25s\t%7s\n" "Bin ID" "Date Created" "Size" - grep -vE "^[\t ]*$" "$BINVENTORY" | sort -r -t: -k2 -n | while read bin stamp; do - printf "%10d\t%25s\t%7s\n" "$bin" \ - "$(date --rfc-3339=seconds --date="@$stamp")" \ - "${DATA[$bin]}" - done - IFS=$OLDIFS - printf "%10s\t%25s\t%7s\n" "" "Incremental Total" "${DATA[0]}" - - # Print totals (not efficient -- reruns du on things we already ran it on) - printf "\n%10s\t%25s\t%7s\n" "" "$(basename $SEED)" "$(du -h "${SEED}" | awk '{print $1}')" - printf "\n%10s\t%25s\t%7s\n" "" "Grand Total" \ - "$(du -csh "$BINS_DIR" "$SEED" 2>/dev/null | grep -E "total$" | awk '{print $1}')" + info "SquashFu Usage Report" + echo + # Enumerate bins, sort date order, print human readable create date and size + + cd "$BINS_DIR" + OLDIFS=$IFS;IFS=${IFS}: + # Collect all data into an array to 'preload' it. Index 0 is the entire + # folder. The following indicies correspond to the bin number of that index + printf "%30s\r" " .: Loading :." >&2 + DATA=($(du -sh . * 2>/dev/null | sort -n -k2 | awk '{print $1}')) + printf "%30s\r" " " >&2 + + printf "%10s\t%25s\t%7s\n" "Bin ID" "Date Created" "Size" + grep -vE "^[\t ]*$" "$BINVENTORY" | sort -r -t: -k2 -n | while read bin stamp; do + printf "%10d\t%25s\t%7s\n" "$bin" \ + "$(date --rfc-3339=seconds --date="@$stamp")" \ + "${DATA[$bin]}" + done + IFS=$OLDIFS + printf "%10s\t%25s\t%7s\n" "" "Incremental Total" "${DATA[0]}" + + # Print totals (not efficient -- reruns du on things we already ran it on) + printf "\n%10s\t%25s\t%7s\n" "" "$(basename $SEED)" "$(du -h "${SEED}" | awk '{print $1}')" + printf "\n%10s\t%25s\t%7s\n" "" "Grand Total" \ + "$(du -csh "$BINS_DIR" "$SEED" 2>/dev/null | awk '/total$/{print $1}')" } action_resquash_now () { # Args: none # Returns: none - if [[ $UID -ne 0 ]]; then - die "Must be root to perform a resquash" - fi + [[ $UID != 0 ]] && die "Must be root to perform a resquash" - info "Voluntary resquash requested" + info "Voluntary resquash requested" - local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l) - if [[ $number_of_bins -le $MIN_BINS ]]; then - die "Nothing to do. Current backups do not exceed MIN_BINS value." - else - create_new_squash $(( $number_of_bins - $MIN_BINS )) - fi + local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l) + if [[ $number_of_bins -le $MIN_BINS ]]; then + die "Nothing to do. Current backups do not exceed MIN_BINS value." + else + create_new_squash $(( $number_of_bins - $MIN_BINS )) + fi - exit $? + exit $? } action_unmount () { - unmount_all + unmount_all } usage () { @@ -437,32 +400,42 @@ OPTIONS you will need to provide the config for all actions HELP - exit 1 + exit 1 } +[[ $# -eq 0 ]] && usage + while getopts :BCD:QR:Uc: opt; do - case $opt in - B) - action=backup ;; - C) - action=resquash_now ;; - D) - action="remove_bin $OPTARG" ;; - Q) - action=report ;; - R) - action="rollback $OPTARG" ;; - U) - action=unmount ;; - c) - [[ -f $OPTARG ]] && source $OPTARG ;; - \:) - die "Argument missing from -$OPTARG" - usage ;; - \?) - die "Unrecognized option -$OPTARG" - usage ;; - esac >&2 + case $opt in + B) + [[ -n $action ]] && die "only one operation may be used at a time" + action=backup ;; + C) + [[ -n $action ]] && die "only one operation may be used at a time" + action=resquash_now ;; + D) + [[ -n $action ]] && die "only one operation may be used at a time" + action="remove_bin $OPTARG" ;; + Q) + [[ -n $action ]] && die "only one operation may be used at a time" + action=report ;; + R) + [[ -n $action ]] && die "only one operation may be used at a time" + action="rollback $OPTARG" ;; + U) + [[ -n $action ]] && die "only one operation may be used at a time" + action=unmount ;; + c) + [[ -f $OPTARG ]] && source $OPTARG ;; + \:) + die "Argument missing from -$OPTARG" + usage ;; + \?) + die "Unrecognized option -$OPTARG" + usage ;; + esac >&2 done -[[ -n $action ]] && action_$action || usage +[[ -z $action ]] && usage + +action_$action |