#!/usr/bin/env bash # ----------------------------------------------------------------------------- # Copyright 2020 Dave Jarvis # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # ----------------------------------------------------------------------------- set -o errexit set -o nounset readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")" readonly SCRIPT_DIR="$(cd "${SCRIPT_SRC}" >/dev/null 2>&1 && pwd)" readonly SCRIPT_NAME=$(basename "$0") # ----------------------------------------------------------------------------- # The main entry point is responsible for parsing command-line arguments, # changing to the appropriate directory, and running all commands requested # by the user. # # $@ - Command-line arguments # ----------------------------------------------------------------------------- main() { arguments "$@" $usage && terminate 3 requirements && terminate 4 traps && terminate 5 directory && terminate 6 preprocess && terminate 7 execute && terminate 8 postprocess && terminate 9 terminate 0 } # ----------------------------------------------------------------------------- # Perform all commands that the script requires. # # @return 0 - Indicate to terminate the script with non-zero exit level # @return 1 - All tasks completed successfully (default) # ----------------------------------------------------------------------------- execute() { return 1 } # ----------------------------------------------------------------------------- # Changes to the script's working directory, provided it exists. # # @return 0 - Change directory failed # @return 1 - Change directory succeeded # ----------------------------------------------------------------------------- directory() { $log "Change directory" local result=1 # Track whether change directory failed. cd "${SCRIPT_DIR}" > /dev/null 2>&1 || result=0 return "${result}" } # ----------------------------------------------------------------------------- # Perform any initialization required prior to executing tasks. # # @return 0 - Preprocessing failed # @return 1 - Preprocessing succeeded # ----------------------------------------------------------------------------- preprocess() { $log "Preprocess" return 1 } # ----------------------------------------------------------------------------- # Perform any clean up required prior to executing tasks. # # @return 0 - Postprocessing failed # @return 1 - Postprocessing succeeded # ----------------------------------------------------------------------------- postprocess() { $log "Postprocess" return 1 } # ----------------------------------------------------------------------------- # Check that all required commands are available. # # @return 0 - At least one command is missing # @return 1 - All commands are available # ----------------------------------------------------------------------------- requirements() { $log "Verify requirements" local -r expected_count=${#DEPENDENCIES[@]} local total_count=0 # Verify that each command exists. for dependency in "${DEPENDENCIES[@]}"; do # Extract the command name [0] and URL [1]. IFS=',' read -ra dependent <<< "${dependency}" required "${dependent[0]}" "${dependent[1]}" total_count=$(( total_count + $? )) done unset IFS # Total dependencies found must match the expected number. # Integer-only division rounds down. return $(( total_count / expected_count )) } # ----------------------------------------------------------------------------- # Called before terminating the script. # ----------------------------------------------------------------------------- cleanup() { $log "Cleanup" } # ----------------------------------------------------------------------------- # Terminates the program immediately. # ----------------------------------------------------------------------------- trap_control_c() { $log "Interrupted" cleanup error "⯃" terminate 1 } # ----------------------------------------------------------------------------- # Configure signal traps. # # @return 1 - Signal traps are set. # ----------------------------------------------------------------------------- traps() { # Suppress echoing ^C if pressed. stty -echoctl trap trap_control_c INT return 1 } # ----------------------------------------------------------------------------- # Check for a required command. # # $1 - Command or file to check for existence # $2 - Command's website (e.g., download for binaries and source code) # # @return 0 - Command is missing # @return 1 - Command exists # ----------------------------------------------------------------------------- required() { local result=0 test -f "$1" || \ command -v "$1" > /dev/null 2>&1 && result=1 || \ warning "Missing: $1 ($2)" return ${result} } # ----------------------------------------------------------------------------- # Show acceptable command-line arguments. # # @return 0 - Indicate script may not continue # ----------------------------------------------------------------------------- utile_usage() { printf "Usage: %s [OPTIONS...]\n\n" "${SCRIPT_NAME}" >&2 # Number of spaces to pad after the longest long argument. local -r PADDING=2 # Determine the longest long argument to adjust spacing. local -r LEN=$(printf '%s\n' "${ARGUMENTS[@]}" | \ awk -F"," '{print length($2)+'${PADDING}'}' | sort -n | tail -1) local duplicates for argument in "${ARGUMENTS[@]}"; do # Extract the short [0] and long [1] arguments and description [2]. arg=("$(echo ${argument} | cut -d ',' -f1)" \ "$(echo ${argument} | cut -d ',' -f2)" \ "$(echo ${argument} | cut -d ',' -f3-)") duplicates+=("${arg[0]}") printf " -%s, --%-${LEN}s%s\n" "${arg[0]}" "${arg[1]}" "${arg[2]}" >&2 done # Sort the arguments to make sure no duplicates exist. duplicates=$(echo "${duplicates[@]}" | tr ' ' '\n' | sort | uniq -c -d) # Warn the developer that there's a duplicate command-line option. if [ -n "${duplicates}" ]; then # Trim all the whitespaces duplicates=$(echo "${duplicates}" | xargs echo -n) error "Duplicate command-line argument exists: ${duplicates}" fi return 0 } # ----------------------------------------------------------------------------- # Write coloured text to standard output. # # $1 - Text to write # $2 - Text's colour # ----------------------------------------------------------------------------- coloured_text() { printf "%b%s%b\n" "$2" "$1" "${COLOUR_OFF}" } # ----------------------------------------------------------------------------- # Write a warning message to standard output. # # $1 - Text to write # ----------------------------------------------------------------------------- warning() { coloured_text "$1" "${COLOUR_WARNING}" } # ----------------------------------------------------------------------------- # Write an error message to standard output. # # $1 - Text to write # ----------------------------------------------------------------------------- error() { coloured_text "$1" "${COLOUR_ERROR}" } # ----------------------------------------------------------------------------- # Write a timestamp and message to standard output. # # $1 - Text to write # ----------------------------------------------------------------------------- utile_log() { printf "[%s] " "$(date +%H:%M:%S.%4N)" coloured_text "$1" "${COLOUR_LOGGING}" } # ----------------------------------------------------------------------------- # Perform no operations. # # return 1 - Success # ----------------------------------------------------------------------------- noop() { return 1 } # ----------------------------------------------------------------------------- # Exit the program with a given exit code. # # $1 - Exit code # ----------------------------------------------------------------------------- terminate() { exit "$1" } # ----------------------------------------------------------------------------- # Set global variables from command-line arguments. # ----------------------------------------------------------------------------- arguments() { while [ "$#" -gt "0" ]; do local consume=1 case "$1" in -V|--verbose) log=utile_log ;; -h|-\?|--help) usage=utile_usage ;; *) set +e argument "$@" consume=$? set -e ;; esac shift ${consume} done } # ----------------------------------------------------------------------------- # Parses a single command-line argument. This must return a value greater # than or equal to 1, otherwise parsing the command-line arguments will # loop indefinitely. # # @return The number of arguments to consume (1 by default). # ----------------------------------------------------------------------------- argument() { return 1 } # ANSI colour escape sequences. readonly COLOUR_BLUE='\033[1;34m' readonly COLOUR_PINK='\033[1;35m' readonly COLOUR_DKGRAY='\033[30m' readonly COLOUR_DKRED='\033[31m' readonly COLOUR_LTRED='\033[1;31m' readonly COLOUR_YELLOW='\033[1;33m' readonly COLOUR_OFF='\033[0m' # Colour definitions used by script. COLOUR_LOGGING=${COLOUR_BLUE} COLOUR_WARNING=${COLOUR_YELLOW} COLOUR_ERROR=${COLOUR_LTRED} # Define required commands to check when script starts. DEPENDENCIES=( "awk,https://www.gnu.org/software/gawk/manual/gawk.html" "cut,https://www.gnu.org/software/coreutils" ) # Define help for command-line arguments. ARGUMENTS=( "V,verbose,Log messages while processing" "h,help,Show this help message then exit" ) # These functions may be set to utile delegates while parsing arguments. usage=noop log=noop