diff --git a/.ansible-lint.yml b/.ansible-lint.yml index 6170027..adba0ff 100644 --- a/.ansible-lint.yml +++ b/.ansible-lint.yml @@ -14,6 +14,7 @@ skip_list: - meta-incorrect - role-name - ignore-errors + - package-latest warn_list: - experimental diff --git a/.yamllint.yml b/.yamllint.yml index e9c2c75..b55d394 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -18,11 +18,11 @@ rules: quote-type: any required: false extra-required: - - '^http://' - - '^https://' - - 'ftp://' + - "^http://" + - "^https://" + - "ftp://" - 'ssh \w.*' - - '{{' + - "{{" extra-allowed: [] truthy: level: error diff --git a/default_variables.yml b/default_variables.yml index 628a8c5..12d9974 100644 --- a/default_variables.yml +++ b/default_variables.yml @@ -5,8 +5,9 @@ influxdb_version: 1.8.4 nomad_version: 1.2.5 prometheus_verssion: 1.1.2 tdarr_installer_version: 2.00.13 -telegraf_version: 1.21.4 +telegraf_version: 1.22.1 traefik_version: "v2.6.0" +speedtest_cli_version: "1.1.1" # ---------------------------------- SERVICE STATIC PORT MAPPINGS authelia_port: "9091" diff --git a/tasks/telegraf.yml b/tasks/telegraf.yml index 0e710ea..ebe7755 100644 --- a/tasks/telegraf.yml +++ b/tasks/telegraf.yml @@ -43,6 +43,13 @@ - not mac_arm fail_msg: "Unable to install Telegraf on this host" + - name: "set variable: Set speedtest download Binary (armv7l)" + ansible.builtin.set_fact: + speedtest_download_file_uri: "https://install.speedtest.net/app/cli/ookla-speedtest-{{ speedtest_cli_version }}-linux-armhf.tgz" + when: + - ansible_os_family == 'Debian' + - ansible_architecture == 'armv7l' + - name: "Install/upgrade Telegraf" block: - name: "set fact: Need telegraf install?" @@ -70,7 +77,7 @@ ignore_errors: true register: current_telegraf_version check_mode: false - changed_when: current_telegraf_version.stdout != telegraf_version + changed_when: false when: - telegraph_binary_location is defined - not need_telegraf_install @@ -81,7 +88,13 @@ when: - telegraph_binary_location is defined - not need_telegraf_install - - current_telegraf_version.stdout != telegraf_version + - current_telegraf_version.stdout is version(telegraf_version, '<') + + - ansible.builtin.debug: + msg: "Current telegraf version {{ current_telegraf_version.stdout }} is lower than specified {{ telegraf_version }}" + when: + - telegraph_binary_location is defined + - current_telegraf_version.stdout is version(telegraf_version, '<') - name: install telegraf (MacOS) community.general.homebrew: @@ -124,7 +137,9 @@ become: true ansible.builtin.apt: pkg: telegraf + state: latest update_cache: true + only_upgrade: true notify: restart_telegraf when: - ansible_os_family == 'Debian' @@ -143,6 +158,51 @@ # - need_telegraf_install # -------------------------------------------------- Add Telegraf Configs +- name: "Install speedtest" + when: "'pis' in group_names" + block: + - name: "set fact: do we need speedtest installed?" + ansible.builtin.set_fact: + need_speedtest_install: false + + - name: Check if speedtest is installed + ansible.builtin.stat: + path: /usr/local/bin/speedtest + register: speedtest_binary_file_location + + - name: "set fact: do we need a speedtest install" + ansible.builtin.set_fact: + need_speedtest_install: true + when: + - not speedtest_binary_file_location.stat.exists + + - name: Check current version of speedtest + ansible.builtin.shell: /usr/local/bin/speedtest --version | head -n1 | awk '{print $4}' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' + ignore_errors: true + register: current_speedtest_version + check_mode: false + changed_when: false + when: + - not need_speedtest_install + + - name: "set fact: do we need a speedtest install" + ansible.builtin.set_fact: + need_speedtest_install: true + when: + - not need_speedtest_install + - current_speedtest_version.stdout is version(speedtest_cli_version, '<') + + - name: "Install speedtest (pi)" + become: true + ansible.builtin.unarchive: + src: "{{ speedtest_download_file_uri }}" + dest: /usr/local/bin + remote_src: true + when: + - need_speedtest_install + - ansible_os_family == 'Debian' + - ansible_architecture == 'armv7l' + - name: "Configure Telegraf" block: - name: "Ensure {{ telegraph_config_location }} exists" @@ -176,6 +236,7 @@ mode: "644" loop: - { src: "telegraf/leader.conf.j2", dest: "{{ telegraph_config_location }}/telegraf.d/leader.conf" } + - { src: "telegraf/speedtest.conf.j2", dest: "{{ telegraph_config_location }}/telegraf.d/speedtest.conf" } - { src: "telegraf/pingHosts.conf.j2", dest: "{{ telegraph_config_location }}/telegraf.d/pingHosts.conf" } when: - is_cluster_leader @@ -192,6 +253,17 @@ when: - inventory_hostname != 'synology' + - name: Copy speedtest script + become: true + ansible.builtin.template: + src: "scripts/telegraf_speedtest.sh.j2" + dest: "/usr/local/bin/telegraf_speedtest.sh" + mode: 0755 + owner: "{{ ansible_user_uid }}" + group: "{{ ansible_user_gid }}" + when: + - is_cluster_leader + - name: Reset file ownership become: true ansible.builtin.file: diff --git a/templates/scripts/telegraf_custom_metrics.sh.j2 b/templates/scripts/telegraf_custom_metrics.sh.j2 index e08ec08..6e3fa1e 100755 --- a/templates/scripts/telegraf_custom_metrics.sh.j2 +++ b/templates/scripts/telegraf_custom_metrics.sh.j2 @@ -17,4 +17,4 @@ if _uname=$(command -v uname); then fi # Print output -printf "exec ip_external=%s,cpu_temp=%s" "\"${EXTERNAL_IP}\"" "${TEMP}" +printf 'exec ip_external="%s",cpu_temp=%s' "${EXTERNAL_IP}" "${TEMP}" diff --git a/templates/scripts/telegraf_speedtest.sh.j2 b/templates/scripts/telegraf_speedtest.sh.j2 new file mode 100755 index 0000000..ac4bbb2 --- /dev/null +++ b/templates/scripts/telegraf_speedtest.sh.j2 @@ -0,0 +1,710 @@ +#!/usr/bin/env bash + +_mainScript_() { + + if ! command -v /usr/local/bin/speedtest &>/dev/null; then + error "Speedtest is not installed" + _safeExit_ 1 + elif ! command -v /usr/bin/jq &>/dev/null; then + error "JQ is not installed" + _safeExit_ 1 + fi + + declare -a RESPONSE=() + mapfile -t RESPONSE < <(/usr/local/bin/speedtest -f json | /usr/bin/jq -r '.isp, .server.host, .ping.latency, .download.bandwidth, .upload.bandwidth') + + ISP="$(printf "%s" "${RESPONSE[0]}" | sed -E 's/ /_/g')" + SERVER="$(printf "%s" "${RESPONSE[1]}" | sed -E 's/ /_/g')" + PING="${RESPONSE[2]}" + DOWNLOAD="${RESPONSE[3]}" + UPLOAD="${RESPONSE[4]}" + + # Print output + printf 'exec,speedtest-server=%s speedtest-isp="%s",ping=%s,download=%s,upload=%s' \ + "${SERVER}" \ + "${ISP}" \ + "${PING}" \ + "${DOWNLOAD}" \ + "${UPLOAD}" +} + +# end _mainScript_ + +# ################################## Flags and defaults +# Required variables +LOGFILE="${HOME}/logs/$(basename "$0").log" +QUIET=false +LOGLEVEL=ERROR +VERBOSE=false +FORCE=false +DRYRUN=false +declare -a ARGS=() + +# Script specific +QUIET=true +if [ -d "/mnt/pi-cluster/logs" ]; then + LOGFILE="/mnt/pi-cluster/logs/$(basename "$0").log" +else + LOGFILE="${HOME}/logs/$(basename "$0").log" +fi + +# ################################## Custom utility functions (Pasted from repository) + +# ################################## Functions required for this template to work + +_setColors_() { + # DESC: + # Sets colors use for alerts. + # ARGS: + # None + # OUTS: + # None + # USAGE: + # printf "%s\n" "${blue}Some text${reset}" + + if tput setaf 1 >/dev/null 2>&1; then + bold=$(tput bold) + underline=$(tput smul) + reverse=$(tput rev) + reset=$(tput sgr0) + + if [[ $(tput colors) -ge 256 ]] >/dev/null 2>&1; then + white=$(tput setaf 231) + blue=$(tput setaf 38) + yellow=$(tput setaf 11) + green=$(tput setaf 82) + red=$(tput setaf 9) + purple=$(tput setaf 171) + gray=$(tput setaf 250) + else + white=$(tput setaf 7) + blue=$(tput setaf 38) + yellow=$(tput setaf 3) + green=$(tput setaf 2) + red=$(tput setaf 9) + purple=$(tput setaf 13) + gray=$(tput setaf 7) + fi + else + bold="\033[4;37m" + reset="\033[0m" + underline="\033[4;37m" + # shellcheck disable=SC2034 + reverse="" + white="\033[0;37m" + blue="\033[0;34m" + yellow="\033[0;33m" + green="\033[1;32m" + red="\033[0;31m" + purple="\033[0;35m" + gray="\033[0;37m" + fi +} + +_alert_() { + # DESC: + # Controls all printing of messages to log files and stdout. + # ARGS: + # $1 (required) - The type of alert to print + # (success, header, notice, dryrun, debug, warning, error, + # fatal, info, input) + # $2 (required) - The message to be printed to stdout and/or a log file + # $3 (optional) - Pass '${LINENO}' to print the line number where the _alert_ was triggered + # OUTS: + # stdout: The message is printed to stdout + # log file: The message is printed to a log file + # USAGE: + # [_alertType] "[MESSAGE]" "${LINENO}" + # NOTES: + # - The colors of each alert type are set in this function + # - For specified alert types, the funcstac will be printed + + local _color + local _alertType="${1}" + local _message="${2}" + local _line="${3:-}" # Optional line number + + [[ $# -lt 2 ]] && fatal 'Missing required argument to _alert_' + + if [[ -n ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line}) $(_printFuncStack_)" + elif [[ -n ${_line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line})" + elif [[ -z ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}$(_printFuncStack_)" + fi + + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _color="${bold}${red}" + elif [ "${_alertType}" == "info" ]; then + _color="${gray}" + elif [ "${_alertType}" == "warning" ]; then + _color="${red}" + elif [ "${_alertType}" == "success" ]; then + _color="${green}" + elif [ "${_alertType}" == "debug" ]; then + _color="${purple}" + elif [ "${_alertType}" == "header" ]; then + _color="${bold}${white}${underline}" + elif [ "${_alertType}" == "notice" ]; then + _color="${bold}" + elif [ "${_alertType}" == "input" ]; then + _color="${bold}${underline}" + elif [ "${_alertType}" = "dryrun" ]; then + _color="${blue}" + else + _color="" + fi + + _writeToScreen_() { + ("${QUIET}") && return 0 # Print to console when script is not 'quiet' + [[ ${VERBOSE} == false && ${_alertType} =~ ^(debug|verbose) ]] && return 0 + + if ! [[ -t 1 || -z ${TERM:-} ]]; then # Don't use colors on non-recognized terminals + _color="" + reset="" + fi + + if [[ ${_alertType} == header ]]; then + printf "${_color}%s${reset}\n" "${_message}" + else + printf "${_color}[%7s] %s${reset}\n" "${_alertType}" "${_message}" + fi + } + _writeToScreen_ + + _writeToLog_() { + [[ ${_alertType} == "input" ]] && return 0 + [[ ${LOGLEVEL} =~ (off|OFF|Off) ]] && return 0 + if [ -z "${LOGFILE:-}" ]; then + LOGFILE="$(pwd)/$(basename "$0").log" + fi + [ ! -d "$(dirname "${LOGFILE}")" ] && mkdir -p "$(dirname "${LOGFILE}")" + [[ ! -f ${LOGFILE} ]] && touch "${LOGFILE}" + + # Don't use colors in logs + local _cleanmessage + _cleanmessage="$(printf "%s" "${_message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" + # Print message to log file + printf "%s [%7s] %s %s\n" "$(date +"%b %d %R:%S")" "${_alertType}" "[$(/bin/hostname)]" "${_cleanmessage}" >>"${LOGFILE}" + } + + # Write specified log level data to logfile + case "${LOGLEVEL:-ERROR}" in + ALL | all | All) + _writeToLog_ + ;; + DEBUG | debug | Debug) + _writeToLog_ + ;; + INFO | info | Info) + if [[ ${_alertType} =~ ^(error|fatal|warning|info|notice|success) ]]; then + _writeToLog_ + fi + ;; + NOTICE | notice | Notice) + if [[ ${_alertType} =~ ^(error|fatal|warning|notice|success) ]]; then + _writeToLog_ + fi + ;; + WARN | warn | Warn) + if [[ ${_alertType} =~ ^(error|fatal|warning) ]]; then + _writeToLog_ + fi + ;; + ERROR | error | Error) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + FATAL | fatal | Fatal) + if [[ ${_alertType} =~ ^fatal ]]; then + _writeToLog_ + fi + ;; + OFF | off) + return 0 + ;; + *) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + esac + +} # /_alert_ + +error() { _alert_ error "${1}" "${2:-}"; } +warning() { _alert_ warning "${1}" "${2:-}"; } +notice() { _alert_ notice "${1}" "${2:-}"; } +info() { _alert_ info "${1}" "${2:-}"; } +success() { _alert_ success "${1}" "${2:-}"; } +dryrun() { _alert_ dryrun "${1}" "${2:-}"; } +input() { _alert_ input "${1}" "${2:-}"; } +header() { _alert_ header "${1}" "${2:-}"; } +debug() { _alert_ debug "${1}" "${2:-}"; } +fatal() { + _alert_ fatal "${1}" "${2:-}" + _safeExit_ "1" +} + +# shellcheck disable=SC1009,SC1054,SC1056,SC1072,SC1073,SC1083 +{% raw %} +_printFuncStack_() { + # DESC: + # Prints the function stack in use. Used for debugging, and error reporting. + # ARGS: + # None + # OUTS: + # stdout: Prints [function]:[file]:[line] + # NOTE: + # Does not print functions from the alert class + local _i + declare -a _funcStackResponse=() + for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do + case "${FUNCNAME[${_i}]}" in + _alert_ | _trapCleanup_ | fatal | error | warning | notice | info | debug | dryrun | header | success) + continue + ;; + *) + _funcStackResponse+=("${FUNCNAME[${_i}]}:$(basename "${BASH_SOURCE[${_i}]}"):${BASH_LINENO[_i - 1]}") + ;; + esac + + done + printf "( " + printf %s "${_funcStackResponse[0]}" + printf ' < %s' "${_funcStackResponse[@]:1}" + printf ' )\n' +} +{% endraw %} + +_safeExit_() { + # DESC: + # Cleanup and exit from a script + # ARGS: + # $1 (optional) - Exit code (defaults to 0) + # OUTS: + # None + + if [[ -d ${SCRIPT_LOCK:-} ]]; then + if command rm -rf "${SCRIPT_LOCK}"; then + debug "Removing script lock" + else + warning "Script lock could not be removed. Try manually deleting ${yellow}'${SCRIPT_LOCK}'" + fi + fi + + if [[ -n ${TMP_DIR:-} && -d ${TMP_DIR:-} ]]; then + if [[ ${1:-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then + command rm -r "${TMP_DIR}" + else + command rm -r "${TMP_DIR}" + debug "Removing temp directory" + fi + fi + + trap - INT TERM EXIT + exit "${1:-0}" +} + +_trapCleanup_() { + # DESC: + # Log errors and cleanup from script when an error is trapped. Called by 'trap' + # ARGS: + # $1: Line number where error was trapped + # $2: Line number in function + # $3: Command executing at the time of the trap + # $4: Names of all shell functions currently in the execution call stack + # $5: Scriptname + # $6: $BASH_SOURCE + # USAGE: + # trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM ERR + # OUTS: + # Exits script with error code 1 + + local _line=${1:-} # LINENO + local _linecallfunc=${2:-} + local _command="${3:-}" + local _funcstack="${4:-}" + local _script="${5:-}" + local _sourced="${6:-}" + + # Replace the cursor in-case 'tput civis' has been used + tput cnorm + + if declare -f "fatal" &>/dev/null && declare -f "_printFuncStack_" &>/dev/null; then + + _funcstack="'$(printf "%s" "${_funcstack}" | sed -E 's/ / < /g')'" + + if [[ ${_script##*/} == "${_sourced##*/}" ]]; then + fatal "${7:-} command: '${_command}' (line: ${_line}) [func: $(_printFuncStack_)]" + else + fatal "${7:-} command: '${_command}' (func: ${_funcstack} called at line ${_linecallfunc} of '${_script##*/}') (line: ${_line} of '${_sourced##*/}') " + fi + else + printf "%s\n" "Fatal error trapped. Exiting..." + fi + + if declare -f _safeExit_ &>/dev/null; then + _safeExit_ 1 + else + exit 1 + fi +} + +_makeTempDir_() { + # DESC: + # Creates a temp directory to house temporary files + # ARGS: + # $1 (Optional) - First characters/word of directory name + # OUTS: + # Sets $TMP_DIR variable to the path of the temp directory + # USAGE: + # _makeTempDir_ "$(basename "$0")" + + [ -d "${TMP_DIR:-}" ] && return 0 + + if [ -n "${1:-}" ]; then + TMP_DIR="${TMPDIR:-/tmp/}${1}.${RANDOM}.${RANDOM}.$$" + else + TMP_DIR="${TMPDIR:-/tmp/}$(basename "$0").${RANDOM}.${RANDOM}.${RANDOM}.$$" + fi + (umask 077 && mkdir "${TMP_DIR}") || { + fatal "Could not create temporary directory! Exiting." + } + debug "\$TMP_DIR=${TMP_DIR}" +} + +# shellcheck disable=SC2120 +_acquireScriptLock_() { + # DESC: + # Acquire script lock to prevent running the same script a second time before the + # first instance exits + # ARGS: + # $1 (optional) - Scope of script execution lock (system or user) + # OUTS: + # exports $SCRIPT_LOCK - Path to the directory indicating we have the script lock + # Exits script if lock cannot be acquired + # NOTE: + # If the lock was acquired it's automatically released in _safeExit_() + + local _lockDir + if [[ ${1:-} == 'system' ]]; then + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").lock" + else + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").${UID}.lock" + fi + + if command mkdir "${_lockDir}" 2>/dev/null; then + readonly SCRIPT_LOCK="${_lockDir}" + debug "Acquired script lock: ${yellow}${SCRIPT_LOCK}${purple}" + else + if declare -f "_safeExit_" &>/dev/null; then + error "Unable to acquire script lock: ${yellow}${_lockDir}${red}" + fatal "If you trust the script isn't running, delete the lock dir" + else + printf "%s\n" "ERROR: Could not acquire script lock. If you trust the script isn't running, delete: ${_lockDir}" + exit 1 + fi + + fi +} + +_setPATH_() { + # DESC: + # Add directories to $PATH so script can find executables + # ARGS: + # $@ - One or more paths + # OPTS: + # -x - Fail if directories are not found + # OUTS: + # 0: Success + # 1: Failure + # Adds items to $PATH + # USAGE: + # _setPATH_ "/usr/local/bin" "${HOME}/bin" "$(npm bin)" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local opt + local OPTIND=1 + local _failIfNotFound=false + + while getopts ":xX" opt; do + case ${opt} in + x | X) _failIfNotFound=true ;; + *) + { + error "Unrecognized option '${1}' passed to _backupFile_" "${LINENO}" + return 1 + } + ;; + esac + done + shift $((OPTIND - 1)) + + local _newPath + + for _newPath in "$@"; do + if [ -d "${_newPath}" ]; then + if ! printf "%s" "${PATH}" | grep -Eq "(^|:)${_newPath}($|:)"; then + if PATH="${_newPath}:${PATH}"; then + debug "Added '${_newPath}' to PATH" + else + debug "'${_newPath}' already in PATH" + fi + else + debug "_setPATH_: '${_newPath}' already exists in PATH" + fi + else + debug "_setPATH_: can not find: ${_newPath}" + if [[ ${_failIfNotFound} == true ]]; then + return 1 + fi + continue + fi + done + return 0 +} + +_useGNUutils_() { + # DESC: + # Add GNU utilities to PATH to allow consistent use of sed/grep/tar/etc. on MacOS + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds GNU utilities to the path + # USAGE: + # # if ! _useGNUUtils_; then exit 1; fi + # NOTES: + # GNU utilities can be added to MacOS using Homebrew + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _setPATH_ \ + "/usr/local/opt/gnu-tar/libexec/gnubin" \ + "/usr/local/opt/coreutils/libexec/gnubin" \ + "/usr/local/opt/gnu-sed/libexec/gnubin" \ + "/usr/local/opt/grep/libexec/gnubin" \ + "/usr/local/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-sed/libexec/gnubin" \ + "/opt/homebrew/opt/grep/libexec/gnubin" \ + "/opt/homebrew/opt/coreutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-tar/libexec/gnubin"; then + return 0 + else + return 1 + fi + +} + +_homebrewPath_() { + # DESC: + # Add homebrew bin dir to PATH + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds homebrew bin directory to PATH + # USAGE: + # # if ! _homebrewPath_; then exit 1; fi + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _uname=$(command -v uname); then + if "${_uname}" | tr '[:upper:]' '[:lower:]' | grep -q 'darwin'; then + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi + else + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi +} + +# shellcheck disable=SC1009,SC1054,SC1056,SC1072,SC1073,SC1083 +{% raw %} +_parseOptions_() { + # DESC: + # Iterates through options passed to script and sets variables. Will break -ab into -a -b + # when needed and --foo=bar into --foo bar + # ARGS: + # $@ from command line + # OUTS: + # Sets array 'ARGS' containing all arguments passed to script that were not parsed as options + # USAGE: + # _parseOptions_ "$@" + + # Iterate over options + local _optstring=h + declare -a _options + local _c + local i + while (($#)); do + case $1 in + # If option is of type -ab + -[!-]?*) + # Loop over each character starting with the second + for ((i = 1; i < ${#1}; i++)); do + _c=${1:i:1} + _options+=("-${_c}") # Add current char to options + # If option takes a required argument, and it's not the last char make + # the rest of the string its argument + if [[ ${_optstring} == *"${_c}:"* && -n ${1:i+1} ]]; then + _options+=("${1:i+1}") + break + fi + done + ;; + # If option is of type --foo=bar + --?*=*) _options+=("${1%%=*}" "${1#*=}") ;; + # add --endopts for -- + --) _options+=(--endopts) ;; + # Otherwise, nothing special + *) _options+=("$1") ;; + esac + shift + done + set -- "${_options[@]:-}" + unset _options + + # Read the options and set stuff + # shellcheck disable=SC2034 + while [[ ${1:-} == -?* ]]; do + case $1 in + # Custom options + + # Common options + -h | --help) + _usage_ + _safeExit_ + ;; + --loglevel) + shift + LOGLEVEL=${1} + ;; + --logfile) + shift + LOGFILE="${1}" + ;; + -n | --dryrun) DRYRUN=true ;; + -v | --verbose) VERBOSE=true ;; + -q | --quiet) QUIET=true ;; + --force) FORCE=true ;; + --endopts) + shift + break + ;; + *) + if declare -f _safeExit_ &>/dev/null; then + fatal "invalid option: $1" + else + printf "%s\n" "ERROR: Invalid option: $1" + exit 1 + fi + ;; + esac + shift + done + + if [[ -z ${*} || ${*} == null ]]; then + ARGS=() + else + ARGS+=("$@") # Store the remaining user input as arguments. + fi +} +{% endraw %} + +_usage_() { + cat <