| | +#!/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 |
| | + |
| | |