# shell-script-helper v1.99
# Copyright (c) 2023 Raphaël Halimi <raphael.halimi@gmail.com>
# Licence: GPL-3+
# vim: ft=sh

###############################################################################
# Variables
###############################################################################

# Define SCRIPT_NAME to avoid calling $(basename -- "$0") all the time
SCRIPT_NAME="$(basename -- "$0")"

# Automatically enable color output if available
case "$TERM" in
  linux|xterm-color|*-256color) COLOR=1 ;;
  *) COLOR=0 ;;
esac

# Define colors (can be overridden by user)
COLOR_ALERT="magenta"
COLOR_ERROR="red"
COLOR_WARNING="yellow"
COLOR_INFO="dim"
COLOR_DEBUG="darkgray"

# No log by default
LOG_DEST="STDOUT"

# Verbosity
VERBOSE=0
DEBUG=0


###############################################################################
# Functions - System
###############################################################################

# Allow only root to run the script
# ARGS: none
root_only () {
  if [ "$(id -u)" != "0" ] ; then
    print_message err "This script must be run as root, aborting."
    exit 1
  fi
}

# Allow only one instance of the script
# ARGS: none
lock_script () {
  local LOCK_FILE
  LOCK_FILE="/run/$SCRIPT_NAME.pid"
  if [ -e "$LOCK_FILE" ] ; then
    ps "$(cat "$LOCK_FILE")" > /dev/null
    if [ $? -ne 0 ] ; then
      printf "Deleting stale lock file %s\n" "$LOCK_FILE"
      rm -f "$LOCK_FILE"
    else
      print_message err "Script already running with PID $(cat "$LOCK_FILE"), aborting."
      exit 1
    fi
  fi
  print_message debug "create lock file '$LOCK_FILE' (PID '$$')"
  printf %d $$ > "$LOCK_FILE"
}

# Remove lock file
# ARGS: none
unlock_script () {
  local LOCK_FILE
  LOCK_FILE="/run/$SCRIPT_NAME.pid"
  if [ -e "$LOCK_FILE" ] ; then
    if [ "$(cat "$LOCK_FILE")" -eq $$ ] ; then
      print_message debug "delete lock file '$LOCK_FILE' (PID '$$' match)"
      rm -f "$LOCK_FILE"
    fi
  fi
}

# Deletes one or more files, with some checks and feedback
# ARGS: FILE...
delete_files () {
  local FILE
  for FILE ; do
    if [ -e "$FILE" ] ; then
      print_message debug "delete file '$FILE'"
      rm -f "$FILE" || print_message err "Couldn't delete file $FILE"
    else
      print_message debug "cannot delete file '$FILE' (no such file)"
    fi
  done
}

# Exit on fatal error, or handle trapped signals gracefully
# ARGS: STRING [RETURN_CODE]
# If STRING is 'trap', RC is interpreted as a signal and exit code is 128+RC
die () {
  local MESSAGE SIG SIGNAME RC
  MESSAGE="$1"
  SIG="$2"
  debug_var MESSAGE SIG
  if [ "$MESSAGE" = "trap" ] ; then
    if [ -n "$SIG" ] ; then
      if check_int "$SIG" ; then
        SIGNAME="$(kill -l "$SIG" 2>/dev/null)"
      else
        # dash 'kill' builtin refuses signal names
        SIGNAME="$SIG"
        SIG="$(env kill -l"$SIG" 2>/dev/null)"
      fi
      debug_var SIG SIGNAME
      if ( [ -z "$SIG" ] || [ -z "$SIGNAME" ] ) ; then
        print_message err "Invalid signal '$SIG', unsetting"
        unset SIG
      fi
    fi
    case "$SIG" in
      '') print_message err "die: trap mode with invalid or no signal" ; RC=128 ;;
      *) print_message notice "Received SIG$SIGNAME" ; RC=$((128+SIG)) ;;
    esac
  else
    print_message crit "${MESSAGE:-Unknown error}, aborting."
    RC=${SIG:-1}
  fi
  exit $RC
}

# Automatically executed when the script exits
# ARGS: none
quit () {
  command -v cleanup > /dev/null 2>&1 && cleanup
  # Backward compatibility - may be removed in the future
  command -v on_exit > /dev/null 2>&1 && on_exit
  unlock_script
  command -v close_logfile > /dev/null 2>&1 && close_logfile
}


###############################################################################
# Functions - Output
###############################################################################

# Enable verbose mode
# ARGS: none
enable_verbose () {
  VERBOSE=1
  print_message info "Verbose mode enabled"
}

# Enable debug mode
# ARGS: none
enable_debug () {
  DEBUG=1
  print_message debug "enable debug"
}

# Print an escape sequence
# ARGS: [ATTRIBUTE|COLOR|BACKGROUND_COLOR]
# ATTRIBUTE: reset, bold, dim, underline, blink, inverted, hidden
# COLOR: default, black, white, red, green, yellow, blue, magenta, cyan,
# lightgray, darkgray, lightred, lightgreen, lightyellow, defaultcolor,
# lightmagenta, lightcyan
# BACKGROUND_COLOR: same, prepended with with "bg" (bdefault, bblack, bwhite...)
print_escape_sequence () {
  local SEQUENCE
  case "$1" in
    reset) printf "\e[0m" >&2 ;;
    bold) printf "\e[1m" >&2 ;;
    dim) printf "\e[2m" >&2 ;;
    underline) printf "\e[4m" >&2 ;;
    blink) printf "\e[5m" >&2 ;;
    inverted) printf "\e[7m" >&2 ;;
    hidden) printf "\e[8m" >&2 ;;
    default) printf "\e[39m" >&2 ;;
    black) printf "\e[30m" >&2 ;;
    red) printf "\e[31m" >&2 ;;
    green) printf "\e[32m" >&2 ;;
    yellow) printf "\e[33m" >&2 ;;
    blue) printf "\e[34m" >&2 ;;
    magenta) printf "\e[35m" >&2 ;;
    cyan) printf "\e[36m" >&2 ;;
    lightgray) printf "\e[37m" >&2 ;;
    darkgray) printf "\e[90m" >&2 ;;
    lightred) printf "\e[91m" >&2 ;;
    lightgreen) printf "\e[92m" >&2 ;;
    lightyellow) printf "\e[93m" >&2 ;;
    lightblue) printf "\e[94m" >&2 ;;
    lightmagenta) printf "\e[95m" >&2 ;;
    lightcyan) printf "\e[96m" >&2 ;;
    white) printf "\e[97m" >&2 ;;
    bgdefault) printf "\e[49m" >&2 ;;
    bgblack) printf "\e[40m" >&2 ;;
    bgred) printf "\e[41m" >&2 ;;
    bggreen) printf "\e[42m" >&2 ;;
    bgyellow) printf "\e[43m" >&2 ;;
    bgblue) printf "\e[44m" >&2 ;;
    bgmagenta) printf "\e[45m" >&2 ;;
    bgcyan) printf "\e[46m" >&2 ;;
    bglightgray) printf "\e[47m" >&2 ;;
    bgdarkgray) printf "\e[100m" >&2 ;;
    bglightred) printf "\e[101m" >&2 ;;
    bglightgreen) printf "\e[102m" >&2 ;;
    bglightyellow) printf "\e[103m" >&2 ;;
    bglightblue) printf "\e[104m" >&2 ;;
    bglightmagenta) printf "\e[105m" >&2 ;;
    bglightcyan) printf "\e[106m" >&2 ;;
    bgwhite) printf "\e[107m" >&2 ;;
    *) print_message error "print_escape_sequence: unknown sequence '$1'"
  esac
}

# Print message(s) to log file, syslog, or stdout/stderr
# ARGS: [PRIORITY] [FACILITY] [FORMAT] [stderr] STRING...
# PRIORITY: emerg, alert, crit, err(error), warning(warn), notice, info(verbose), debug
# FACILITY: auth, authpriv, cron, daemon, ftp, lpr, mail, news, syslog, user, uucp
# FORMAT: syslog, date (for stdout/stderr)
# stderr: force redirection to standard error (implied for emerg, alert, crit, err, warning)
print_message () {
  local OPT PRIORITY FACILITY FORMAT STDERR DATE PREFIX MESSAGE
  FACILITY="$LOG_FACILITY"
  STDERR=0

  # Process arguments
  for OPT ; do
    case "$OPT" in
      # Priority
      emerg|alert|crit|err|warning) PRIORITY="$OPT" ; STDERR=1 ; shift ;;
      notice) PRIORITY="$OPT" ; shift ;;
      info) if [ $VERBOSE -eq 0 ] ; then return 0 ; else PRIORITY="$OPT" ; shift ; fi ;;
      debug) if [ $DEBUG -eq 0 ] ; then return 0 ; else PRIORITY="$OPT" ; shift ; fi ;;
      # Aliases
      error) PRIORITY="err" ; STDERR=1 ; shift ;;
      warn) PRIORITY="warning" ; shift ;;
      verbose) if [ $VERBOSE -eq 0 ] ; then return 0 ; else PRIORITY="info" ; shift ; fi ;;
      # Facility
      auth|authpriv|cron|daemon|ftp|lpr|mail|news|syslog|user|uucp) FACILITY="$OPT" ;;
      # Redirection
      stderr) STDERR=1 ; shift ;;
      # Format
      syslog) FORMAT="$OPT" ; shift ;;
      date) FORMAT="$OPT" ; shift ;;
      *) break ;;
    esac
  done

  # Sanitize PRIORITY
  PRIORITY="$(printf %s "${PRIORITY:-notice}" | tr '[:lower:]' '[:upper:]')"

  # Prefix priority for ERR and above
  case "$PRIORITY" in
    ERR) PREFIX="ERROR: " ;;
    EMERG|ALERT|CRIT|WARNING) PREFIX="$PRIORITY: " ;;
  esac

  # Output message according to destination
  if [ "$LOG_DEST" = "STDOUT" ] ; then
    # Standard output: rebuild prefix according to format
    case "$FORMAT" in
      syslog) PREFIX="$(printf "%s %s %s[%s]: %s" "$(LANG=C date +'%b %e %T')" "$LOG_HOSTNAME" "$SCRIPT_NAME" "$$" "$(printf %s "$PREFIX")")" ;;
      date) PREFIX="$(printf "%s %s" "$(date +'%b %e %T')" "$PREFIX")" ;;
      '') PREFIX="$(printf %s "$PREFIX")" ;;
    esac
    for MESSAGE ; do
      if [ $COLOR -eq 1 ] ; then
        case "$PRIORITY" in
          EMERG|ALERT) print_escape_sequence "$COLOR_ALERT" ;;
          CRIT|ERR) print_escape_sequence "$COLOR_ERROR" ;;
          WARNING) print_escape_sequence "$COLOR_WARNING" ;;
          INFO) print_escape_sequence "$COLOR_INFO" ;;
          DEBUG) print_escape_sequence "$COLOR_DEBUG" ;;
        esac
      fi
      if [ $STDERR -eq 0 ] ; then
        printf "%s%s\n" "$PREFIX" "$MESSAGE"
      else
        printf "%s%s\n" "$PREFIX" "$MESSAGE" >&2
      fi
      if [ $COLOR -eq 1 ] ; then print_escape_sequence reset ; fi
    done
  elif [ "$LOG_DEST" = "FILE" ] ; then
    # Log file: mimic syslog format
    DATE="$(LANG=C date +'%b %e %T')"
    for MESSAGE ; do
      printf "%s %s %s[%s]: %s\n" "$DATE" "$LOG_HOSTNAME" "$LOG_TAG" "$$" "$PREFIX$MESSAGE" >&3
    done
  elif [ "$LOG_DEST" = "SYSLOG" ] ; then
    # Syslog: use logger
    for MESSAGE ; do
      $LOGGER_BIN -p ${FACILITY:-user}.$PRIORITY --tag "$LOG_TAG" --id$LOG_ID -- "$PREFIX$MESSAGE"
    done
  fi
}

# Display options in the help message
# ARGS: OPTION OPTION_DESCRIPTION
print_option () {
  printf "  %-20s%s\n" "$1" "$2"
}

# Debug variables
# ARGS: STRING...
debug_var () {
  local VAR VAL
  if [ $DEBUG -eq 1 ] ; then
    for VAR ; do
      VAL="$(eval "printf %s \"\$$VAR\"")"
      print_message "debug" "$VAR='$VAL'"
    done
  fi
}


###############################################################################
# Functions - Checks
###############################################################################

# Check integer
# ARGS: STRING...
check_int () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %d "$STRING" > /dev/null 2>&1 || RC=1
  done
  return $RC
}

# Check floating point decimal
# ARGS: STRING...
check_float () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %f "$STRING" > /dev/null 2>&1 || RC=1
  done
  return $RC
}

# Check alphanumeric
# ARGS: STRING...
check_alnum () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %s "$STRING" | grep -E -q "[^[:alnum:]]" && RC=1
  done
  return $RC
}

# Check word (alphanumeric, '_')
# ARGS: STRING...
check_word () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %s "$STRING" | grep -E -q "[^[:alnum:]_]" && RC=1
  done
  return $RC
}

# Check hostname (alphanumeric, '-', '.')
# ARGS: STRING...
check_hostname () {
  local STRING COMPONENT RC
  RC=0
  for STRING ; do
    printf %s "$STRING" | LANG=C grep -E -q "[^[:alnum:].-]" && RC=1
    [ ${#STRING} -gt 253 ] && RC=1
    for COMPONENT in $(printf %s "$STRING" | tr . ' ') ; do
      [ ${#COMPONENT} -gt 63 ] && RC=1
      [ "$(printf %.1s "$COMPONENT")" = "-" ] && RC=1
    done
  done
  return $RC
}

# Check IPv4
# ARGS: STRING...
# Regex found on StackOverflow, #53497, (c) David M. Syzdek
check_ipv4 () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %s "$STRING" | grep -E -q '^((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])$' || RC=1
  done
  return $RC
}

# Check IPv6
# ARGS: STRING...
# Regex found on StackOverflow, #53497, (c) David M. Syzdek
check_ipv6 () {
  local STRING RC
  RC=0
  for STRING ; do
    printf %s "$STRING" | grep -E -q '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3,3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$' || RC=1
  done
  return $RC
}

# Check binaries (soft dependencies)
# ARGS: BINARY[:ALTERNATIVE...]...
check_bin () {
  local RC BINARY BINARY_FOUND BINARY_VAR TRY_BINARY BINARY_PATH
  RC=0
  for BINARY ; do
    BINARY_FOUND=0
    ALT_TEXT=""
    [ "$BINARY" != "${BINARY%%:*}" ] && ALT_TEXT=" any alternative for"
    debug_var BINARY
    BINARY_VAR="$(printf %s "$BINARY" | cut -d : -f 1 | tr '[:lower:]' '[:upper:]' | tr -d -c [A-Z0-9_])"
    debug_var BINARY_VAR
    for TRY_BINARY in $(printf %s "$BINARY" | tr : ' ') ; do
      debug_var TRY_BINARY
      BINARY_PATH="$(command -v "$TRY_BINARY")"
      debug_var BINARY_PATH
      if [ -n "$BINARY_PATH" ] ; then
        BINARY_FOUND=1
        print_message info "Found binary '$BINARY_PATH'"
        eval "${BINARY_VAR}_BIN=\"$BINARY_PATH\""
        debug_var "${BINARY_VAR}_BIN"
        break
      fi
    done
    if [ $BINARY_FOUND -eq 0 ] ; then
      print_message warning "Cannot find${ALT_TEXT} binary '${BINARY%%:*}'"
      RC=1
    fi
  done
  return $RC
}

# Check dependencies (hard dependencies)
# ARGS: BINARY[:ALTERNATIVE...]...
check_dep () {
  local BINARY
  for BINARY ; do
    [ "$BINARY" != "${BINARY%%:*}" ]
    check_bin "$BINARY" || die "Missing dependency '${BINARY%%:*}'"
  done
}


###############################################################################
# Misc
###############################################################################

# Quote strings - from Rich’s sh (POSIX shell) tricks
# ARGS: STRING...
# https://www.etalabs.net/sh_tricks.html
quote () {
  local ITEM
  if [ $# -le 1 ] ; then
    printf "%s\n" "$1" | sed "s/'/'\\\\''/g ; 1s/^/'/ ; \$s/$/'/"
  else
    for ITEM ; do
      printf "%s\n" "$ITEM" | sed "s/'/'\\\\''/g ; 1s/^/'/ ; \$s/$/' \\\\/"
    done
    printf ' \n'
  fi
}


###############################################################################
# Traps
###############################################################################

# Handle common signals gracefully
for SIG in HUP INT QUIT PIPE TERM ; do
  trap "die trap $SIG" $SIG
done

# Call the quit function when the script exits
trap "quit" EXIT


###############################################################################
# Backward compatibility with 1.x - may be removed in the future
###############################################################################

# Print verbose message(s)
# ARGS: STRING...
print_verbose () {
  if [ $VERBOSE -eq 1 ] ; then
    print_message "info" "$@"
  fi
}

# Print debug message(s)
# ARGS: STRING...
print_debug () {
  if [ $DEBUG -eq 1 ] ; then
    print_message "debug" "$@"
  fi
}

# Print error message(s)
# ARGS: STRING...
print_error () {
  print_message "err" "$@"
}


###############################################################################
# Examples
###############################################################################

# Print help
#print_help () {
#  printf "Usage: %s [OPTION]... [ARG]...\n" "$(basename "$0")"
#  printf "\nOPTIONS:\n"
#  print_option "-v" "Verbose mode"
#  print_option "-d" "Debug mode"
#  print_option "-h" "Print this help message"
#}

# Options processing
#while getopts "vdh" OPTION ; do
#  case $OPTION in
#    v) enable_verbose ;;
#    d) enable_debug ;;
#    h) print_help ; exit 0 ;;
#    *) print_help ; exit 1 ;;
#  esac
#done ; shift $((OPTIND-1))
