| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | # ----------------------------------------------------------------------------- |
| 4 | # Copyright 2020 Dave Jarvis |
| 5 | # |
| 6 | # Permission is hereby granted, free of charge, to any person obtaining a |
| 7 | # copy of this software and associated documentation files (the |
| 8 | # "Software"), to deal in the Software without restriction, including |
| 9 | # without limitation the rights to use, copy, modify, merge, publish, |
| 10 | # distribute, sublicense, and/or sell copies of the Software, and to |
| 11 | # permit persons to whom the Software is furnished to do so, subject to |
| 12 | # the following conditions: |
| 13 | # |
| 14 | # The above copyright notice and this permission notice shall be included |
| 15 | # in all copies or substantial portions of the Software. |
| 16 | # |
| 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| 20 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
| 21 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| 22 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| 23 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 24 | # ----------------------------------------------------------------------------- |
| 25 | |
| 26 | set -o errexit |
| 27 | set -o nounset |
| 28 | |
| 29 | readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")" |
| 30 | readonly SCRIPT_DIR="$(cd "${SCRIPT_SRC}" >/dev/null 2>&1 && pwd)" |
| 31 | readonly SCRIPT_NAME=$(basename "$0") |
| 32 | |
| 33 | # ----------------------------------------------------------------------------- |
| 34 | # The main entry point is responsible for parsing command-line arguments, |
| 35 | # changing to the appropriate directory, and running all commands requested |
| 36 | # by the user. |
| 37 | # |
| 38 | # $@ - Command-line arguments |
| 39 | # ----------------------------------------------------------------------------- |
| 40 | main() { |
| 41 | arguments "$@" |
| 42 | |
| 43 | $usage && terminate 3 |
| 44 | requirements && terminate 4 |
| 45 | traps && terminate 5 |
| 46 | |
| 47 | directory && terminate 6 |
| 48 | preprocess && terminate 7 |
| 49 | execute && terminate 8 |
| 50 | postprocess && terminate 9 |
| 51 | |
| 52 | terminate 0 |
| 53 | } |
| 54 | |
| 55 | # ----------------------------------------------------------------------------- |
| 56 | # Perform all commands that the script requires. |
| 57 | # |
| 58 | # @return 0 - Indicate to terminate the script with non-zero exit level |
| 59 | # @return 1 - All tasks completed successfully (default) |
| 60 | # ----------------------------------------------------------------------------- |
| 61 | execute() { |
| 62 | return 1 |
| 63 | } |
| 64 | |
| 65 | # ----------------------------------------------------------------------------- |
| 66 | # Changes to the script's working directory, provided it exists. |
| 67 | # |
| 68 | # @return 0 - Change directory failed |
| 69 | # @return 1 - Change directory succeeded |
| 70 | # ----------------------------------------------------------------------------- |
| 71 | directory() { |
| 72 | $log "Change directory" |
| 73 | local result=1 |
| 74 | |
| 75 | # Track whether change directory failed. |
| 76 | cd "${SCRIPT_DIR}" > /dev/null 2>&1 || result=0 |
| 77 | |
| 78 | return "${result}" |
| 79 | } |
| 80 | |
| 81 | # ----------------------------------------------------------------------------- |
| 82 | # Perform any initialization required prior to executing tasks. |
| 83 | # |
| 84 | # @return 0 - Preprocessing failed |
| 85 | # @return 1 - Preprocessing succeeded |
| 86 | # ----------------------------------------------------------------------------- |
| 87 | preprocess() { |
| 88 | $log "Preprocess" |
| 89 | |
| 90 | return 1 |
| 91 | } |
| 92 | |
| 93 | # ----------------------------------------------------------------------------- |
| 94 | # Perform any clean up required prior to executing tasks. |
| 95 | # |
| 96 | # @return 0 - Postprocessing failed |
| 97 | # @return 1 - Postprocessing succeeded |
| 98 | # ----------------------------------------------------------------------------- |
| 99 | postprocess() { |
| 100 | $log "Postprocess" |
| 101 | |
| 102 | return 1 |
| 103 | } |
| 104 | |
| 105 | # ----------------------------------------------------------------------------- |
| 106 | # Check that all required commands are available. |
| 107 | # |
| 108 | # @return 0 - At least one command is missing |
| 109 | # @return 1 - All commands are available |
| 110 | # ----------------------------------------------------------------------------- |
| 111 | requirements() { |
| 112 | $log "Verify requirements" |
| 113 | local -r expected_count=${#DEPENDENCIES[@]} |
| 114 | local total_count=0 |
| 115 | |
| 116 | # Verify that each command exists. |
| 117 | for dependency in "${DEPENDENCIES[@]}"; do |
| 118 | # Extract the command name [0] and URL [1]. |
| 119 | IFS=',' read -ra dependent <<< "${dependency}" |
| 120 | |
| 121 | required "${dependent[0]}" "${dependent[1]}" |
| 122 | total_count=$(( total_count + $? )) |
| 123 | done |
| 124 | |
| 125 | unset IFS |
| 126 | |
| 127 | # Total dependencies found must match the expected number. |
| 128 | # Integer-only division rounds down. |
| 129 | return $(( total_count / expected_count )) |
| 130 | } |
| 131 | |
| 132 | # ----------------------------------------------------------------------------- |
| 133 | # Called before terminating the script. |
| 134 | # ----------------------------------------------------------------------------- |
| 135 | cleanup() { |
| 136 | $log "Cleanup" |
| 137 | } |
| 138 | |
| 139 | # ----------------------------------------------------------------------------- |
| 140 | # Terminates the program immediately. |
| 141 | # ----------------------------------------------------------------------------- |
| 142 | trap_control_c() { |
| 143 | $log "Interrupted" |
| 144 | cleanup |
| 145 | error "⯃" |
| 146 | terminate 1 |
| 147 | } |
| 148 | |
| 149 | # ----------------------------------------------------------------------------- |
| 150 | # Configure signal traps. |
| 151 | # |
| 152 | # @return 1 - Signal traps are set. |
| 153 | # ----------------------------------------------------------------------------- |
| 154 | traps() { |
| 155 | # Suppress echoing ^C if pressed. |
| 156 | stty -echoctl |
| 157 | trap trap_control_c INT |
| 158 | |
| 159 | return 1 |
| 160 | } |
| 161 | |
| 162 | # ----------------------------------------------------------------------------- |
| 163 | # Check for a required command. |
| 164 | # |
| 165 | # $1 - Command or file to check for existence |
| 166 | # $2 - Command's website (e.g., download for binaries and source code) |
| 167 | # |
| 168 | # @return 0 - Command is missing |
| 169 | # @return 1 - Command exists |
| 170 | # ----------------------------------------------------------------------------- |
| 171 | required() { |
| 172 | local result=0 |
| 173 | |
| 174 | test -f "$1" || \ |
| 175 | command -v "$1" > /dev/null 2>&1 && result=1 || \ |
| 176 | warning "Missing: $1 ($2)" |
| 177 | |
| 178 | return ${result} |
| 179 | } |
| 180 | |
| 181 | # ----------------------------------------------------------------------------- |
| 182 | # Show acceptable command-line arguments. |
| 183 | # |
| 184 | # @return 0 - Indicate script may not continue |
| 185 | # ----------------------------------------------------------------------------- |
| 186 | utile_usage() { |
| 187 | printf "Usage: %s [OPTIONS...]\n\n" "${SCRIPT_NAME}" >&2 |
| 188 | |
| 189 | # Number of spaces to pad after the longest long argument. |
| 190 | local -r PADDING=2 |
| 191 | |
| 192 | # Determine the longest long argument to adjust spacing. |
| 193 | local -r LEN=$(printf '%s\n' "${ARGUMENTS[@]}" | \ |
| 194 | awk -F"," '{print length($2)+'${PADDING}'}' | sort -n | tail -1) |
| 195 | |
| 196 | local duplicates |
| 197 | |
| 198 | for argument in "${ARGUMENTS[@]}"; do |
| 199 | # Extract the short [0] and long [1] arguments and description [2]. |
| 200 | arg=("$(echo ${argument} | cut -d ',' -f1)" \ |
| 201 | "$(echo ${argument} | cut -d ',' -f2)" \ |
| 202 | "$(echo ${argument} | cut -d ',' -f3-)") |
| 203 | |
| 204 | duplicates+=("${arg[0]}") |
| 205 | |
| 206 | printf " -%s, --%-${LEN}s%s\n" "${arg[0]}" "${arg[1]}" "${arg[2]}" >&2 |
| 207 | done |
| 208 | |
| 209 | # Sort the arguments to make sure no duplicates exist. |
| 210 | duplicates=$(echo "${duplicates[@]}" | tr ' ' '\n' | sort | uniq -c -d) |
| 211 | |
| 212 | # Warn the developer that there's a duplicate command-line option. |
| 213 | if [ -n "${duplicates}" ]; then |
| 214 | # Trim all the whitespaces |
| 215 | duplicates=$(echo "${duplicates}" | xargs echo -n) |
| 216 | error "Duplicate command-line argument exists: ${duplicates}" |
| 217 | fi |
| 218 | |
| 219 | return 0 |
| 220 | } |
| 221 | |
| 222 | # ----------------------------------------------------------------------------- |
| 223 | # Write coloured text to standard output. |
| 224 | # |
| 225 | # $1 - Text to write |
| 226 | # $2 - Text's colour |
| 227 | # ----------------------------------------------------------------------------- |
| 228 | coloured_text() { |
| 229 | printf "%b%s%b\n" "$2" "$1" "${COLOUR_OFF}" |
| 230 | } |
| 231 | |
| 232 | # ----------------------------------------------------------------------------- |
| 233 | # Write a warning message to standard output. |
| 234 | # |
| 235 | # $1 - Text to write |
| 236 | # ----------------------------------------------------------------------------- |
| 237 | warning() { |
| 238 | coloured_text "$1" "${COLOUR_WARNING}" |
| 239 | } |
| 240 | |
| 241 | # ----------------------------------------------------------------------------- |
| 242 | # Write an error message to standard output. |
| 243 | # |
| 244 | # $1 - Text to write |
| 245 | # ----------------------------------------------------------------------------- |
| 246 | error() { |
| 247 | coloured_text "$1" "${COLOUR_ERROR}" |
| 248 | } |
| 249 | |
| 250 | # ----------------------------------------------------------------------------- |
| 251 | # Write a timestamp and message to standard output. |
| 252 | # |
| 253 | # $1 - Text to write |
| 254 | # ----------------------------------------------------------------------------- |
| 255 | utile_log() { |
| 256 | printf "[%s] " "$(date +%H:%M:%S.%4N)" |
| 257 | coloured_text "$1" "${COLOUR_LOGGING}" |
| 258 | } |
| 259 | |
| 260 | # ----------------------------------------------------------------------------- |
| 261 | # Perform no operations. |
| 262 | # |
| 263 | # return 1 - Success |
| 264 | # ----------------------------------------------------------------------------- |
| 265 | noop() { |
| 266 | return 1 |
| 267 | } |
| 268 | |
| 269 | # ----------------------------------------------------------------------------- |
| 270 | # Exit the program with a given exit code. |
| 271 | # |
| 272 | # $1 - Exit code |
| 273 | # ----------------------------------------------------------------------------- |
| 274 | terminate() { |
| 275 | exit "$1" |
| 276 | } |
| 277 | |
| 278 | # ----------------------------------------------------------------------------- |
| 279 | # Set global variables from command-line arguments. |
| 280 | # ----------------------------------------------------------------------------- |
| 281 | arguments() { |
| 282 | while [ "$#" -gt "0" ]; do |
| 283 | local consume=1 |
| 284 | |
| 285 | case "$1" in |
| 286 | -V|--verbose) |
| 287 | log=utile_log |
| 288 | ;; |
| 289 | -h|-\?|--help) |
| 290 | usage=utile_usage |
| 291 | ;; |
| 292 | *) |
| 293 | set +e |
| 294 | argument "$@" |
| 295 | consume=$? |
| 296 | set -e |
| 297 | ;; |
| 298 | esac |
| 299 | |
| 300 | shift ${consume} |
| 301 | done |
| 302 | } |
| 303 | |
| 304 | # ----------------------------------------------------------------------------- |
| 305 | # Parses a single command-line argument. This must return a value greater |
| 306 | # than or equal to 1, otherwise parsing the command-line arguments will |
| 307 | # loop indefinitely. |
| 308 | # |
| 309 | # @return The number of arguments to consume (1 by default). |
| 310 | # ----------------------------------------------------------------------------- |
| 311 | argument() { |
| 312 | return 1 |
| 313 | } |
| 314 | |
| 315 | # ANSI colour escape sequences. |
| 316 | readonly COLOUR_BLUE='\033[1;34m' |
| 317 | readonly COLOUR_PINK='\033[1;35m' |
| 318 | readonly COLOUR_DKGRAY='\033[30m' |
| 319 | readonly COLOUR_DKRED='\033[31m' |
| 320 | readonly COLOUR_LTRED='\033[1;31m' |
| 321 | readonly COLOUR_YELLOW='\033[1;33m' |
| 322 | readonly COLOUR_OFF='\033[0m' |
| 323 | |
| 324 | # Colour definitions used by script. |
| 325 | COLOUR_LOGGING=${COLOUR_BLUE} |
| 326 | COLOUR_WARNING=${COLOUR_YELLOW} |
| 327 | COLOUR_ERROR=${COLOUR_LTRED} |
| 328 | |
| 329 | # Define required commands to check when script starts. |
| 330 | DEPENDENCIES=( |
| 331 | "awk,https://www.gnu.org/software/gawk/manual/gawk.html" |
| 332 | "cut,https://www.gnu.org/software/coreutils" |
| 333 | ) |
| 334 | |
| 335 | # Define help for command-line arguments. |
| 336 | ARGUMENTS=( |
| 337 | "V,verbose,Log messages while processing" |
| 338 | "h,help,Show this help message then exit" |
| 339 | ) |
| 340 | |
| 341 | # These functions may be set to utile delegates while parsing arguments. |
| 342 | usage=noop |
| 343 | log=noop |
| 344 | |
| 1 | 345 | |