diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d699b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +scratch +tmp +*.DS_Store +.cache diff --git a/.hooks/README.md b/.hooks/README.md index c798795..2f904f5 100644 --- a/.hooks/README.md +++ b/.hooks/README.md @@ -1,8 +1,8 @@ -This folder contains git hook scripts which will be executed on the client side before/after certain git actions are completed. +This folder contains a git pre-commit script which will be executed on the client side before files are committed to the repository. This script provides automated linting and testing of multiple file types. ## Usage -To install these hooks, create a symlink to `/path/to/repo/.git/hooks` directory. +To install the hook, create a symlink into the `/path/to/repo/.git/hooks` directory. ```bash ln -s "$(git rev-parse --show-toplevel)/.hooks/pre-commit.sh" "$(git rev-parse --show-toplevel)/.git/hooks/pre-commit" diff --git a/.hooks/pre-commit.sh b/.hooks/pre-commit.sh index c94c4a2..6c36417 100755 --- a/.hooks/pre-commit.sh +++ b/.hooks/pre-commit.sh @@ -17,7 +17,7 @@ _mainScript_() { STOP_WORD_FILE="${HOME}/.git_stop_words" GIT_DIFF_TEMP="${TMP_DIR}/diff.txt" - if cat "${STOP_WORD_FILE}" | grep . | grep -v '# ' >"${TMP_DIR}/pattern_file.txt"; then + if [ -f "${STOP_WORD_FILE}" ]; then if [[ $(basename "${STOP_WORD_FILE}") == "$(basename "${1}")" ]]; then debug "Don't check stop words file for stop words. Skipping $(basename "${1}")" @@ -25,16 +25,18 @@ _mainScript_() { fi debug "Checking for stop words" - # remove blank lines and comments from stopwords file + # remove blank lines from stopwords file + cat "${STOP_WORD_FILE}" | sed '/^$/d' >"${TMP_DIR}/pattern_file.txt" # Add diff to a temporary file - git diff --cached -- "${1}" | grep '^+' >"${GIT_DIFF_TEMP}" + git diff --cached -- "${1}" | grep '^+' >"${GIT_DIFF_TEMP}" + if grep --file="${TMP_DIR}/pattern_file.txt" "${GIT_DIFF_TEMP}"; then error "Found git stop word in '$(basename "${1}")'" _safeExit_ 1 fi else - debug "Could not find git stopwords file expected at '${STOP_WORD_FILE}'. Or it was empty. Continuing..." + debug "Could not find git stopwords file expected at '${STOP_WORD_FILE}'. Continuing..." fi } @@ -87,26 +89,35 @@ _mainScript_() { error "Error in ${1}" _safeExit_ 1 else - success "yaml-lint passed: '${1}'" + success "yaml-lint passed: '${1}'" fi elif command -v yamllint >/dev/null; then debug "Linting YAML File" - if ! yamllint "${1}"; then - error "Error in ${1}" - _safeExit_ 1 - else + if [ -f "$(git rev-parse --show-toplevel)/.yamllint.yml" ]; then + if ! yamllint -c "$(git rev-parse --show-toplevel)/.yamllint.yml" "${1}"; then + error "YAML Error in ${1}" + _safeExit_ 1 + else success "yamllint passed: '${1}'" + fi + else + if ! yamllint "${1}"; then + error "YAML Error in ${1}" + _safeExit_ 1 + else + success "yamllint passed: '${1}'" + fi fi else - notice "No YAML linter installed. Continuing..." + notice "No YAML linter installed. Continuiing..." fi } _lintShellscripts_() { if command -v shellcheck >/dev/null; then debug "Linting shellscript: ${1}" - if ! shellcheck --exclude=2016,2059,2001,2002,2148,1090,2162,2005,2034,2154,2086,2155,2181,2164,2120,2119,1083,1117,2207 "${1}"; then - error "Error in ${file}" + if ! shellcheck --exclude=2016,2059,2001,2002,2148,1090,2162,2005,2034,2154,2086,2155,2181,2164,2120,2119,1083,1117,2207,1091 "${1}"; then + error "Error in ${1}" _safeExit_ 1 else success "shellcheck passed: '${1}'" @@ -133,7 +144,47 @@ _mainScript_() { unset filename } + _lintAnsible_() { + + if ! command -v ansible-lint >/dev/null; then + notice "Found Ansible files but ansible-lint is not available. Continuing..." + return 0 + elif [[ "$(basename ${1})" =~ (^\.|^requirements|j2|vault\.yml|variables|meta|defaults?|inventory) ]]; then + # Don't lint files that are not Ansible playbooks + debug "won't ansible lint: ${1}" + return 0 + elif [[ ${1} =~ /(handlers|vars/|defaults/|meta/|molecule/|templates/|files/)/ ]]; then + # Don't lint in directory names that are not likely to contain Ansible playbooks + debug "Won't ansible lint: ${1}" + return 0 + fi + + ANSIBLE_COMMAND="ansible-lint -vv --parseable-severity" + if [ -f "$(git rev-parse --show-toplevel)/.ansible-lint.yml" ]; then + ANSIBLE_COMMAND="ansible-lint -p -c $(git rev-parse --show-toplevel)/.ansible-lint.yml" + fi + + debug "Linting ansible file: ${1}" + if ! ${ANSIBLE_COMMAND} "${1}"; then + error "Ansible-lint error" + _safeExit_ 1 + else + success "ansible-lint passed: ${1}" + fi + } + # RUN SCRIPT LOGIC + + # Attempt to discern if we are working on an repo that contains ansible files + IS_ANSIBLE_REPO=false + if find "$(git rev-parse --show-toplevel)" -type f -mindepth 1 -maxdepth 1 \ + -name "inventory.yml" \ + -o -name "ansible.cfg" \ + -o -name ".ansible-lint.yml" &>/dev/null; then + + IS_ANSIBLE_REPO=true + fi + _ignoreSymlinks_ while read -r STAGED_FILE; do @@ -142,17 +193,18 @@ _mainScript_() { _gitStopWords_ "${STAGED_FILE}" - if [[ "${STAGED_FILE}" =~ \.(yaml|yml)$ ]]; then + if [[ ${STAGED_FILE} =~ \.(yaml|yml)$ ]]; then _lintYAML_ "${STAGED_FILE}" + if [ "${IS_ANSIBLE_REPO}" = true ]; then + _lintAnsible_ "${STAGED_FILE}" + fi fi - if [[ "${STAGED_FILE}" =~ \.(bash|sh)$ ]]; then + if [[ ${STAGED_FILE} =~ \.(bash|sh)$ || "$(head -n 1 "${STAGED_FILE}")" =~ ^#!.*bash$ ]]; then _lintShellscripts_ "${STAGED_FILE}" fi - if [[ "${STAGED_FILE}" =~ \.(sh|bash|bats|zsh)$ ]]; then + if [[ ${STAGED_FILE} =~ \.(sh|bash|bats|zsh)$ || "$(head -n 1 "${STAGED_FILE}")" =~ ^#!.*bash$ ]]; then _BATS_ "${STAGED_FILE}" fi - else - fatal "${STAGED_FILE} does not exist" fi done < <(git diff --cached --name-only --line-prefix="$(git rev-parse --show-toplevel)/") @@ -160,22 +212,22 @@ _mainScript_() { } # end _mainScript_ # ################################## Flags and defaults - # Script specific +# Script specific - # Common - LOGFILE="${HOME}/logs/$(basename "$0").log" - QUIET=false - LOGLEVEL=ERROR - VERBOSE=false - FORCE=false - DRYRUN=false - declare -a ARGS=() - NOW=$(LC_ALL=C date +"%m-%d-%Y %r") # Returns: 06-14-2015 10:34:40 PM - DATESTAMP=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 - HOURSTAMP=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM - TIMESTAMP=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 - LONGDATE=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 - GMTDATE=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT +# Common +LOGFILE="${HOME}/logs/$(basename "$0").log" +QUIET=false +LOGLEVEL=ERROR +VERBOSE=false +FORCE=false +DRYRUN=false +declare -a ARGS=() +NOW=$(LC_ALL=C date +"%m-%d-%Y %r") # Returns: 06-14-2015 10:34:40 PM +DATESTAMP=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 +HOURSTAMP=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM +TIMESTAMP=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 +LONGDATE=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 +GMTDATE=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT # ################################## Custom utility functions _setPATH_() { @@ -190,37 +242,65 @@ _setPATH_() { done for NEWPATH in "${NEWPATHS[@]}"; do - if ! echo "$PATH" | grep -Eq "(^|:)${NEWPATH}($|:)"; then - PATH="${NEWPATH}:${PATH}" - debug "Added '${tan}${NEWPATH}${purple}' to PATH" + if [ -d "${NEWPATH}" ]; then + if ! echo "${PATH}" | grep -Eq "(^|:)${NEWPATH}($|:)"; then + PATH="${NEWPATH}:${PATH}" + debug "Added '${NEWPATH}' to PATH" + else + debug "_setPATH_: '${NEWPATH}' already exists in PATH" + fi + else + debug "_setPATH_: can not find: ${NEWPATH}" fi done } # ################################## Common Functions for script template -# Colors - if tput setaf 1 &>/dev/null; then - bold=$(tput bold) - white=$(tput setaf 7) - reset=$(tput sgr0) - purple=$(tput setaf 171) - red=$(tput setaf 1) - green=$(tput setaf 76) - tan=$(tput setaf 3) - yellow=$(tput setaf 3) - blue=$(tput setaf 38) - underline=$(tput sgr 0 1) -else - bold="\033[4;37m" - white="\033[0;37m" - reset="\033[0m" - purple="\033[0;35m" - red="\033[0;31m" - green="\033[1;32m" - tan="\033[0;33m" - yellow="\033[0;33m" - blue="\033[0;34m" - underline="\033[4;37m" -fi +_setColors_() { + # DESC: Sets colors use for alerts. + # ARGS: None + # OUTS: None + # USAGE: echo "${blue}Some text${reset}" + + if tput setaf 1 &>/dev/null; then + bold=$(tput bold) + underline=$(tput smul) + reverse=$(tput rev) + reset=$(tput sgr0) + + if [[ $(tput colors) -ge 256 ]] 2>/dev/null; then + white=$(tput setaf 231) + blue=$(tput setaf 38) + yellow=$(tput setaf 11) + tan=$(tput setaf 3) + green=$(tput setaf 82) + red=$(tput setaf 1) + purple=$(tput setaf 171) + gray=$(tput setaf 250) + else + white=$(tput setaf 7) + blue=$(tput setaf 38) + yellow=$(tput setaf 3) + tan=$(tput setaf 3) + green=$(tput setaf 2) + red=$(tput setaf 1) + purple=$(tput setaf 13) + gray=$(tput setaf 7) + fi + else + bold="\033[4;37m" + reset="\033[0m" + underline="\033[4;37m" + reverse="" + white="\033[0;37m" + blue="\033[0;34m" + yellow="\033[0;33m" + tan="\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. @@ -237,28 +317,32 @@ _alert_() { local function_name color local alertType="${1}" local message="${2}" - local line="${3:-}" # Optional line number + local line="${3:-}" # Optional line number - if [[ -n "${line}" && "${alertType}" =~ ^(fatal|error) && "${FUNCNAME[2]}" != "_trapCleanup_" ]]; then + if [[ -n ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then message="${message} (line: ${line}) $(_functionStack_)" - elif [[ -n "${line}" && "${FUNCNAME[2]}" != "_trapCleanup_" ]]; then + elif [[ -n ${line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then message="${message} (line: ${line})" - elif [[ -z "${line}" && "${alertType}" =~ ^(fatal|error) && "${FUNCNAME[2]}" != "_trapCleanup_" ]]; then + elif [[ -z ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then message="${message} $(_functionStack_)" fi - if [[ "${alertType}" =~ ^(error|fatal) ]]; then + if [[ ${alertType} =~ ^(error|fatal) ]]; then color="${bold}${red}" - elif [ "${alertType}" = "warning" ]; then + elif [ "${alertType}" == "info" ]; then + color="${gray}" + elif [ "${alertType}" == "warning" ]; then color="${red}" - elif [ "${alertType}" = "success" ]; then + elif [ "${alertType}" == "success" ]; then color="${green}" - elif [ "${alertType}" = "debug" ]; then + elif [ "${alertType}" == "debug" ]; then color="${purple}" - elif [ "${alertType}" = "header" ]; then + elif [ "${alertType}" == "header" ]; then color="${bold}${tan}" - elif [[ "${alertType}" =~ ^(input|notice) ]]; then + elif [ ${alertType} == "notice" ]; then color="${bold}" + elif [ ${alertType} == "input" ]; then + color="${bold}${underline}" elif [ "${alertType}" = "dryrun" ]; then color="${blue}" else @@ -268,7 +352,7 @@ _alert_() { _writeToScreen_() { ("${QUIET}") && return 0 # Print to console when script is not 'quiet' - [[ ${VERBOSE} == false && "${alertType}" =~ ^(debug|verbose) ]] && return 0 + [[ ${VERBOSE} == false && ${alertType} =~ ^(debug|verbose) ]] && return 0 if ! [[ -t 1 ]]; then # Don't use colors on non-recognized terminals color="" @@ -280,11 +364,13 @@ _alert_() { _writeToScreen_ _writeToLog_() { - [[ "${alertType}" == "input" ]] && return 0 - [[ "${LOGLEVEL}" =~ (off|OFF|Off) ]] && return 0 - [ -z "${LOGFILE:-}" ] && LOGFILE="$(pwd)/$(basename "$0").log" - [ ! -d "$(dirname "${LOGFILE}")" ] && command mkdir -p "$(dirname "${LOGFILE}")" - [[ ! -f "${LOGFILE}" ]] && touch "${LOGFILE}" + [[ ${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 if command -v gsed &>/dev/null; then @@ -304,22 +390,27 @@ _alert_() { _writeToLog_ ;; INFO | info | Info) - if [[ "${alertType}" =~ ^(die|error|fatal|warning|info|notice|success) ]]; then + 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}" =~ ^(die|error|fatal|warning) ]]; then + if [[ ${alertType} =~ ^(error|fatal|warning) ]]; then _writeToLog_ fi ;; ERROR | error | Error) - if [[ "${alertType}" =~ ^(die|error|fatal) ]]; then + if [[ ${alertType} =~ ^(error|fatal) ]]; then _writeToLog_ fi ;; FATAL | fatal | Fatal) - if [[ "${alertType}" =~ ^(die|fatal) ]]; then + if [[ ${alertType} =~ ^fatal ]]; then _writeToLog_ fi ;; @@ -327,7 +418,7 @@ _alert_() { return 0 ;; *) - if [[ "${alertType}" =~ ^(die|error|fatal) ]]; then + if [[ ${alertType} =~ ^(error|fatal) ]]; then _writeToLog_ fi ;; @@ -343,23 +434,35 @@ success() { _alert_ success "${1}" "${2:-}"; } dryrun() { _alert_ dryrun "${1}" "${2:-}"; } input() { _alert_ input "${1}" "${2:-}"; } header() { _alert_ header "== ${1} ==" "${2:-}"; } -die() { - _alert_ fatal "${1}" "${2:-}" - _safeExit_ "1" -} +debug() { _alert_ debug "${1}" "${2:-}"; } fatal() { _alert_ fatal "${1}" "${2:-}" _safeExit_ "1" } -debug() { _alert_ debug "${1}" "${2:-}"; } -verbose() { _alert_ debug "${1}" "${2:-}"; } + +_functionStack_() { + # DESC: Prints the function stack in use + # ARGS: None + # OUTS: Prints [function]:[file]:[line] + # NOTE: Does not print functions from the alert class + local _i + 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 ;; esac + funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}") + done + printf "( " + printf %s "${funcStackResponse[0]}" + printf ' < %s' "${funcStackResponse[@]:1}" + printf ' )\n' +} _safeExit_() { # DESC: Cleanup and exit from a script # ARGS: $1 (optional) - Exit code (defaults to 0) # OUTS: None - if [[ -d "${SCRIPT_LOCK:-}" ]]; then + if [[ -d ${SCRIPT_LOCK:-} ]]; then if command rm -rf "${SCRIPT_LOCK}"; then debug "Removing script lock" else @@ -367,7 +470,7 @@ _safeExit_() { fi fi - if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then + if [[ -n ${TMP_DIR:-} && -d ${TMP_DIR:-} ]]; then if [[ ${1:-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then # Do something here to save TMP_DIR on a non-zero script exit for debugging command rm -r "${TMP_DIR}" @@ -401,7 +504,7 @@ _trapCleanup_() { funcstack="'$(echo "$funcstack" | sed -E 's/ / < /g')'" - if [[ "${script##*/}" == "${sourced##*/}" ]]; then + if [[ ${script##*/} == "${sourced##*/}" ]]; then fatal "${7:-} command: '${command}' (line: ${line}) [func: $(_functionStack_)]" else fatal "${7:-} command: '${command}' (func: ${funcstack} called at line ${linecallfunc} of '${script##*/}') (line: $line of '${sourced##*/}') " @@ -454,23 +557,6 @@ _acquireScriptLock_() { fi } -_functionStack_() { - # DESC: Prints the function stack in use - # ARGS: None - # OUTS: Prints [function]:[file]:[line] - # NOTE: Does not print functions from the alert class - local _i - funcStackResponse=() - for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do - case "${FUNCNAME[$_i]}" in "_alert_" | "_trapCleanup_" | fatal | error | warning | notice | info | verbose | debug | dryrun | header | success | die) continue ;; esac - funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[$_i - 1]}") - done - printf "( " - printf %s "${funcStackResponse[0]}" - printf ' < %s' "${funcStackResponse[@]:1}" - printf ' )\n' -} - _parseOptions_() { # Iterate over options # breaking -ab into -a -b when needed and --foo=bar into --foo bar @@ -547,7 +633,8 @@ _usage_() { ${bold}Options:${reset} -h, --help Display this help and exit - --loglevel [LEVEL] One of: FATAL, ERROR, WARN, INFO, DEBUG, ALL, OFF (Default is 'ERROR') + --loglevel [LEVEL] One of: FATAL, ERROR, WARN, INFO, NOTICE, DEBUG, ALL, OFF + (Default is 'ERROR') --logfile [FILE] Full PATH to logfile. (Default is '${HOME}/logs/$(basename "$0").log') -n, --dryrun Non-destructive. Makes no permanent changes. -q, --quiet Quiet (no output) @@ -563,18 +650,46 @@ EOF # ################################## INITIALIZE AND RUN THE SCRIPT # (Comment or uncomment the lines below to customize script behavior) -trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' \ - EXIT INT TERM SIGINT SIGQUIT -set -o errtrace # Trap errors in subshells and functions -set -o errexit # Exit on error. Append '||true' if you expect an error -set -o pipefail # Use last non-zero exit code in a pipeline -# shopt -s nullglob globstar # Make `for f in *.txt` work when `*.txt` matches zero files -IFS=$' \n\t' # Set IFS to preferred implementation -# set -o xtrace # Run in debug mode -set -o nounset # Disallow expansion of unset variables -# [[ $# -eq 0 ]] && _parseOptions_ "-h" # Force arguments when invoking the script -_parseOptions_ "$@" # Parse arguments passed to script -_makeTempDir_ "$(basename "$0")" # Create a temp directory '$TMP_DIR' -# _acquireScriptLock_ # Acquire script lock -_mainScript_ # Run the main logic script -_safeExit_ # Exit cleanly +trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT + +# Trap errors in subshells and functions +set -o errtrace + +# Exit on error. Append '||true' if you expect an error +set -o errexit + +# Use last non-zero exit code in a pipeline +set -o pipefail + +# Make `for f in *.txt` work when `*.txt` matches zero files +# shopt -s nullglob globstar + +# Set IFS to preferred implementation +IFS=$' \n\t' + +# Run in debug mode +# set -o xtrace + +# Initialize color constants +_setColors_ + +# Disallow expansion of unset variables +set -o nounset + +# Force arguments when invoking the script +# [[ $# -eq 0 ]] && _parseOptions_ "-h" + +# Parse arguments passed to script +_parseOptions_ "$@" + +# Create a temp directory '$TMP_DIR' +_makeTempDir_ "$(basename "$0")" + +# Acquire script lock +# _acquireScriptLock_ + +# Run the main logic script +_mainScript_ + +# Exit cleanly +_safeExit_ diff --git a/.vscode/shellscript.code-snippets b/.vscode/shellscript.code-snippets index 6e13969..fdd39c8 100644 --- a/.vscode/shellscript.code-snippets +++ b/.vscode/shellscript.code-snippets @@ -19,11 +19,18 @@ "scope": "shellscript", "prefix": "_c", "body": [ - "# DESC:$1", - "# ARGS:\t\t${2:None}", - "# OUTS:\t\t${3:None}", - "# USAGE:$4", - "# NOTE:$0" + "# DESC:", + "#\t\t\t\t\t$1", + "# ARGS:", + "#\t\t\t\t\t$2", + "# OUTS:", + "#\t\t\t\t\t0 - Success", + "#\t\t\t\t\t1 - Failure", + "#\t\t\t\t\tstdout: $4", + "# USAGE:", + "#\t\t\t\t\t$4", + "# NOTES:", + "#\t\t\t\t\t$5" ], "description": "Comment block for a function" }, @@ -44,14 +51,21 @@ "prefix": "_f", "body": [ "_${1:name}_() {", - "\t\t# DESC:$2", - "\t\t# ARGS:\t\t${3:None}", - "\t\t# OUTS:\t\t${4:None}", - "\t\t# USAGE:$5", - "\t\t# NOTE:$6", + "\t\t# DESC:", + "\t\t#\t\t\t\t\t$2", + "\t\t# ARGS:", + "\t\t#\t\t\t\t\t$3", + "\t\t# OUTS:", + "\t\t#\t\t\t\t\t0 - Success", + "\t\t#\t\t\t\t\t1 - Failure", + "\t\t#\t\t\t\t\tstdout: $4", + "\t\t# USAGE:", + "\t\t#\t\t\t\t\t_${1:name}_ ", + "\t\t# NOTES:", + "\t\t#\t\t\t\t\t$5", "\t\t", - "\t\t echo \"Hello world\"", - "\t\t$0", + "\t\t [[ $# == 0 ]] && fatal \"Missing required argument to ${FUNCNAME[0]}\"", + "\t\t$6", "}" ], "description": "Add a new function" diff --git a/README.md b/README.md index d207a58..12032a2 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,135 @@ # Shell Scripting Templates and Utilities -A collection of shell scripting utilities and templates used to ease the creation of BASH scripts. [BATS](https://github.com/bats-core/bats-core) provides unit testing capabilities. All tests are in the `tests/` repo. +A collection of BASH utility functions and script templates used to ease the creation of portable and hardened BASH scripts with sane defaults. -## Bash Script Template Usage +## Usage -To create a new script, copy `scriptTemplate.sh` to a new file and make it executable `chmod 755 [newscript].sh`. Place your custom script logic within the `_mainScript_` function at the top of the script. +There are **two Script Templates** located in the root level of this repository. The usage of these templates is described in detail below. -### Script Template Usage +BASH **Utility functions** are located within the `utilities/` folder. -Default flags included in the base template are: +Complex `sed` find/replace operations are supported with the files located in `sedfiles/`. Read [the usage instructions](sedfiles/README.md). -- `-h`: Prints the contents of the `_usage_` function. Edit the text in that function to provide help -- `-l [level]`: Log level of the script. One of: `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `ALL`, `OFF` (Default is '`ERROR`') -- `-n`: Dryrun, sets `$DRYRUN` to `true` allowing you to write functions that will work non-destructively using the `_execute_` function -- `-v`: Sets `$VERBOSE` to `true` and prints all debug messages to stdout -- `-q`: Runs in quiet mode, suppressing all output to stdout. Will still write to log files -- `--force`: If using the `_seekConfirmation_` utility function, this skips all user interaction. Implied `Yes` to all confirmations. +**Automated testing** is provided using [BATS](https://github.com/bats-core/bats-core). All tests are in the `tests/` repo. A git pre-commit hook provides automated testing is located in the `.hooks/` directory. Read about [how to install the hook](.hooks/README.md). + +## Bash Script Templates Usage + +To create a new script, copy one of the script templates to a new file and make it executable `chmod 755 [newscript].sh`. Place your custom script logic within the `_mainScript_` function at the top of the script. + +### Script Templates + +There are two templates located at the root level of this repository. + +- **`template_source_utils.sh`** - A lean template which attempts to source all the utility functions from this repository. You will need to update the path to the utilities folder sent to `_sourceUtilities_` at the bottom of the script. This template will not function correctly if the utilities are not found. +- **`template_standalone.sh`** - For portability, the standalone template does not assume that this repository is available. Copy and paste the individual utility functions under the `### Custom utility functions` line. + +### Code Organization + +The script templates are roughly split into three sections: + +- TOP: Write the main logic of your script within the `_mainScript_` function. It is placed at the top of the file for easy access and editing. However, it is invoked at the end of the script after options are parsed and functions are sourced. +- MIDDLE: Functions and default variable settings are located just below `_mainScript_`. +- BOTTOM: Script initialization (BASH options, traps, call to `_mainScript_`, etc.) is at the bottom of the template + +### Default Options + +These default options and global variables are included in the templates and used throughout the utility functions. CLI flags to set/unset them are: + +- **`-h, --help`**: Prints the contents of the `_usage_` function. Edit the text in that function to provide help +- **`--logfile [FILE]`** Full PATH to logfile. (Default is `${HOME}/logs/$(basename "$0").log`) +- **`loglevel [LEVEL]`**: Log level of the script. One of: `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `ALL`, `OFF` (Default is '`ERROR`') +- **`-n, --dryrun`**: Dryrun, sets `$DRYRUN` to `true` allowing you to write functions that will work non-destructively using the `_execute_` function +- **`-q, --quiet`**: Runs in quiet mode, suppressing all output to stdout. Will still write to log files +- **`-v, --verbose`**: Sets `$VERBOSE` to `true` and prints all debug messages to stdout +- **`--force`**: If using the `_seekConfirmation_` utility function, this skips all user interaction. Implied `Yes` to all confirmations. You can add custom script options and flags to the `_parseOptions_` function. -### Script Template Functions - -scriptTemplate.sh includes some helper functions to perform common tasks. - -- `_alert_` Provides alerting and logging functionality. See notes below. -- `_trapCleanup_` Cleans up files on error -- `_makeTempDir_` Creates a temp directory to house temporary files -- `_acquireScriptLock_` Acquires script lock to avoid race conditions on files -- `_functionStack_` Prints the function stack in use to aid debugging -- `_parseOptions_` Parse options and take user input (`-a`, `--some-flag`, and `--some-file [filename]` supported) -- `_usage_` Prints help text when `-h` passed -- `_safeExit_` Used to exit gracefully, cleaning up all temporary files etc. - ### Script Initialization The bottom of the script template file contains a block which initializes the script. Comment, uncomment, or change the settings here for your needs ```bash -trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' \ - EXIT INT TERM SIGINT SIGQUIT -set -o errtrace # Trap errors in subshells and functions -set -o errexit # Exit on error. Append '||true' if you expect an error -set -o pipefail # Use last non-zero exit code in a pipeline -# shopt -s nullglob globstar # Make `for f in *.txt` work when `*.txt` matches zero files -IFS=$' \n\t' # Set IFS to preferred implementation -# set -o xtrace # Run in debug mode -set -o nounset # Disallow expansion of unset variables -# [[ $# -eq 0 ]] && _parseOptions_ "-h" # Force arguments when invoking the script -_parseOptions_ "$@" # Parse arguments passed to script -# _makeTempDir_ "$(basename "$0")" # Create a temp directory '$tmpDir' -# _acquireScriptLock_ # Acquire script lock -_mainScript_ # Run the main logic script -_safeExit_ # Exit cleanly +trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM ERR + +# Trap errors in subshells and functions +set -o errtrace + +# Exit on error. Append '||true' if you expect an error +set -o errexit + +# Use last non-zero exit code in a pipeline +set -o pipefail + +# Confirm we have BASH greater than v4 +[ "${BASH_VERSINFO:-0}" -ge 4 ] || { + printf "%s\n" "ERROR: BASH_VERSINFO is '${BASH_VERSINFO:-0}'. This script requires BASH v4 or greater." + exit 1 +} + +# Make `for f in *.txt` work when `*.txt` matches zero files +shopt -s nullglob globstar + +# Set IFS to preferred implementation +IFS=$' \n\t' + +# Run in debug mode +# set -o xtrace + +# Source utility functions +_sourceUtilities_ "${HOME}/repos/shell-scripting-templates/utilities" + +# Initialize color constants +_setColors_ + +# Disallow expansion of unset variables +set -o nounset + +# Force arguments when invoking the script +# [[ $# -eq 0 ]] && _parseOptions_ "-h" + +# Parse arguments passed to script +_parseOptions_ "$@" + +# Create a temp directory '$TMP_DIR' +# _makeTempDir_ "$(basename "$0")" + +# Acquire script lock +# _acquireScriptLock_ + +# Source GNU utilities for use on MacOS +# _useGNUUtils_ + +# Run the main logic script +_mainScript_ + +# Exit cleanly +_safeExit_ ``` -# Utility Files +# Utility Functions -The files within `utilities/` contain BASH functions which can be used in your scripts. Each included function includes detailed usage information. Read the code for instructions. +The files within `utilities/` contain BASH functions which can be used in your scripts. Each included function includes detailed usage information. Read the inline comments within the code for detailed usage instructions. ## Including Utility Functions Within the `utilities` folder are many BASH functions meant to ease development of more complicated scripts. These can be included in the template in two ways. -#### 1. Copy and Paste +#### 1. Copy and paste into standaloneTemplate.sh -You can copy any complete function from the Utilities and place it into your script. Copy it beneath the end of `_mainscript_()` +You can copy any complete function from the Utilities and place it into your script. Copy it beneath the `### Custom utility functions` line. -#### 2. Source entire utility files +#### 2. Source all the utility files by using scriptTemplate.sh -You can source entire utility files by pasting the following snippet into your script beneath `_mainScript_()`. Be sure to replace `[PATH_TO]` with the full path to this repository. - -```bash -_sourceHelperFiles_() { - # DESC: Sources script helper files. - local filesToSource - local sourceFile - filesToSource=( - "[PATH_TO]/shell-scripting-templates/utilities/alerts.bash" - "[PATH_TO]/shell-scripting-templates/utilities/baseHelpers.bash" - "[PATH_TO]/shell-scripting-templates/utilities/arrays.bash" - "[PATH_TO]/shell-scripting-templates/utilities/files.bash" - "[PATH_TO]/shell-scripting-templates/utilities/macOS.bash" - "[PATH_TO]/shell-scripting-templates/utilities/numbers.bash" - "[PATH_TO]/shell-scripting-templates/utilities/services.bash" - "[PATH_TO]/shell-scripting-templates/utilities/textProcessing.bash" - "[PATH_TO]/shell-scripting-templates/utilities/dates.bash" - ) - for sourceFile in "${filesToSource[@]}"; do - [ ! -f "${sourceFile}" ] \ - && { - echo "error: Can not find sourcefile '${sourceFile}'." - echo "exiting..." - exit 1 - } - source "${sourceFile}" - done -} -_sourceHelperFiles_ -``` +`scriptTemplate.sh` contains a function to source all the utility files into the script. Beware, this will require a full path to the location of this repository and will result in a script that will not be portable to other systems. ## alerts.bash -- `_setColors_` Sets color constants for alerting (**Note:** Colors default to a dark theme.) -- `_alert_` Performs alerting functions including writing to a log file and printing to screen +- - **`_columnizeOutput_`** Creates a column output for key/value pairs with line wrapping for the right column (value) +- -**`_printFuncStack_`** Prints the function stack in use. Used for debugging, and error reporting +- **`_alert_`** Performs alerting functions including writing to a log file and printing to screen +- **`_centerOutput_`** Prints text in the center of the terminal window +- **`_setColors_`** Sets color constants for alerting (**Note:** Colors default to a dark theme.) Basic alerting, logging, and setting color functions (included in `scriptTemplate.sh` by default). Print messages to stdout and to a user specified logfile using the following functions. @@ -111,121 +138,183 @@ debug "some text" # Printed only when in verbose (-v) mode info "some text" # Basic informational messages notice "some text" # Messages which should be read. Brighter than 'info' warning "some text" # Non-critical warnings -error "some text" # Error state warnings. (Does not stop the script) +error "some text" # Prints errors and the function stack but does not stop the script. fatal "some text" # Fatal errors. Exits the script success "some text" # Prints a success message header "some text" # Prints a header element +dryrun "some text" # Prints commands that would be run if not in dry run (-n) mode ``` -Set the following variables for the alert functions to work. +The following global variables must be set for the alert functions to work -- `$LOGFILE` - Location of a log file -- `$LOGLEVEL` - One of: FATAL, ERROR, WARN, INFO, DEBUG, ALL, OFF (Default is 'ERROR') -- `$QUIET` - If `true`, nothing will print to STDOUT (Logs files will still be populated) -- `$DEBUG` - If `true`, prints `debug` and `verbose` level alerts to stdout +- **`$DEBUG`** - If `true`, prints `debug` level alerts to stdout. (Default: `false`) +- **`$DRYRUN`** - If `true` does not eval commands passed to `_execute_` function. (Default: `false`) +- **`$LOGFILE`** - Path to a log file +- **`$LOGLEVEL`** - One of: FATAL, ERROR, WARN, INFO, DEBUG, ALL, OFF (Default: `ERROR`) +- **`$QUIET`** - If `true`, prints to log file but not stdout. (Default: `false`) ## arrays.bash -Common functions for working with BASH arrays. +Utility functions for working with arrays. -- `_inArray_` Determine if a value is in an array -- `_join_` Joins items together with a user specified separator -- `_setdiff_` Return items that exist in ARRAY1 that are do not exist in ARRAY2 -- `_removeDupes_` Removes duplicate array elements -- `_randomArrayElement_` Selects a random item from an array +- **`_dedupeArray_`** Removes duplicate array elements +- **`_forEachDo_`** Iterates over elements and passes each to a function +- **`_forEachFilter_`** Iterates over elements, returning only those that are validated by a function +- **`_forEachFind_`** Iterates over elements, returning the first value that is validated by a function +- **`_forEachReject_`** Iterates over elements, returning only those that are NOT validated by a function +- **`_forEachValidate_`** Iterates over elements and passes each to a function for validation +- **`_inArray_`** Determine if a value is in an array +- **`_isEmptyArray_`** Checks if an array is empty +- **`_joinArray_`** Joins items together with a user specified separator +- **`_mergeArrays_`** Merges the values of two arrays together +- **`_randomArrayElement_`** Selects a random item from an array +- **`_reverseSortArray_`** Sorts an array from highest to lowest +- **`_setdiff_`** Return items that exist in ARRAY1 that are do not exist in ARRAY2 +- **`_sortArray_`** Sorts an array from lowest to highest -## baseHelpers.bash +## checks.bash -Commonly used functions in many scripts +Functions for validating common use-cases -- `_execute_` Executes commands with safety and logging options. Respects `DRYRUN` and `VERBOSE` flags. -- `_findBaseDir_` Locates the real directory of the script being run. Similar to GNU readlink -n -- `_checkBinary_` Check if a binary exists in the search PATH -- `_haveFunction_` Tests if a function exists -- `_pauseScript_` Pause a script at any point and continue after user input -- `_progressBar_` Prints a progress bar within a for/while loop -- `_rootAvailable_` Validate we have superuser access as root (via sudo if requested) -- `_runAsRoot_` Run the requested command as root (via sudo if requested) -- `_safeExit_` Cleans up temporary files before exiting a script -- `_seekConfirmation_` Seek user input for yes/no question -- `_setPATH_` Add directories to $PATH so script can find executables - -## csv.bash - -Functions to write to a CSV file. - -- `_makeCSV_` Creates a new CSV file if one does not already exist -- `_writeCSV_` Takes passed arguments and writes them as a comma separated line +- **`_binaryExists_`** Check if a binary exists in the PATH +- **`_functionExists_`** Tests if a function is available in current scope +- **`_isInternetAvailable_`** Checks if Internet connections are possible +- **`_isAlpha_`** Validate that a given variable contains only alphabetic characters +- **`_isAlphaDash_`** Validate that a given variable contains only alpha-numeric characters, as well as dashes and underscores +- **`_isAlphaNum_`** Validate that a given variable contains only alpha-numeric characters +- **`_isDir_`** Validate that a given input points to a valid directory +- **`_isEmail_`** Validates that an input is a valid email address +- **`_isFile_`** Validate that a given input points to a valid file +- **`_isIPv4_`** Validates that an input is a valid IPv4 address +- **`_isIPv6_`** Validates that an input is a valid IPv6 address +- **`_isNum_`** Validate that a given variable contains only numeric characters +- **`_isTerminal_`** Checks if script is run in an interactive terminal +- **`_rootAvailable_`** Validate we have superuser access as root (via sudo if requested) +- **`_varIsEmpty_`** Checks if a given variable is empty or null +- **`_varIsFalse_`** Checks if a given variable is false +- **`_varIsTrue_`** Checks if a given variable is true ## dates.bash -Common utilities for working with dates in BASH scripts. +Functions for working with dates and time. -- `_monthToNumber_` Convert a month name to a number -- `_numberToMonth_` Convert a month number to its name -- `_parseDate_` Takes a string as input and attempts to find a date within it to parse into component parts (day, month, year) -- `_formatDate_` Reformats dates into user specified formats +- **`_convertToUnixTimestamp_`** Converts a date to unix timestamp +- **`_countdown_`** Sleep for a specified amount of time +- **`_dateUnixTimestamp_`** Current time in unix timestamp +- **`_formatDate_`** Reformats dates into user specified formats +- **`_fromSeconds_`** Convert seconds to HH:MM:SS +- **`_monthToNumber_`** Convert a month name to a number +- **`_numberToMonth_`** Convert a month number to its name +- **`_parseDate_`** Takes a string as input and attempts to find a date within it to parse into component parts (day, month, year) +- **`_readableUnixTimestamp_`** Format unix timestamp to human readable format +- **`_toSeconds_`** Converts HH:MM:SS to seconds + +## debug.bash + +Functions to aid in debugging BASH scripts + +- **`_pauseScript_`** Pause a script at any point and continue after user input +- **`_printAnsi_`** Helps debug ansi escape sequence in text by displaying the escape codes +- **`_printArray_`** Prints the content of array as key value pairs for easier debugging ## files.bash -Common utilities for working with files. +Functions for working with files. -- `_listFiles_` Find files in a directory. Use either glob or regex. -- `_backupFile_` Creates a backup of a specified file with .bak extension or optionally to a specified directory. -- `_parseFilename_` Break a filename into its component parts which and place them into prefixed variables for use in your script (dir, basename, extension, path, etc.) -- `_decryptFile_` Decrypts a file with `openssl` -- `_encryptFile_` Encrypts a file with `openssl` -- `_extract_` Extract a compressed file -- `_json2yaml_` Convert JSON to YAML uses python -- `_makeSymlink_` Creates a symlink and backs up a file which may be overwritten by the new symlink. If the exact same symlink already exists, nothing is done. -- `_parseYAML_` Convert a YAML file into BASH variables for use in a shell script -- `_readFile_` Prints each line of a file -- `_sourceFile_` Source a file into a script -- `_uniqueFileName_` Ensure a file to be created has a unique filename to avoid overwriting other files -- `_yaml2json_` Convert a YAML file to JSON with python +- **`_backupFile_`** Creates a backup of a specified file with .bak extension or optionally to a specified directory. +- **`_decryptFile_`** Decrypts a file with `openssl` +- **`_encryptFile_`** Encrypts a file with `openssl` +- **`_extractArchive_`** Extract a compressed file +- **`_fileAbsPath_`** Finds the absolute path to a relative file or directory +- **`_fileBasename_`** Gets the basename of a file from a file name +- **`_fileContains_`** Tests whether a file contains a given pattern +- **`_fileDirectory_`** Finds the directory name from a file path +- **`_fileExtension_`** Gets the extension of a file +- **`_fileName_`** Prints a filename from a path +- **`_json2yaml_`** Convert JSON to YAML uses python +- **`_listFiles_`** Find files in a directory. Use either glob or regex. +- **`_makeSymlink_`** Creates a symlink and backs up a file which may be overwritten by the new symlink. If the exact same symlink already exists, nothing is done. +- **`_parseFilename_`** Break a filename into its component parts which and place them into global variables for use in your script (dir, basename, extension, path, etc.) +- **`_parseYAML_`** Convert a YAML file into BASH variables for use in a shell script +- **`_readFile_`** Prints each line of a file +- **`_sourceFile_`** Source a file into a script +- **`_createUniqueFilename_`** Ensure a file to be created has a unique filename to avoid overwriting other files +- **`_yaml2json_`** Convert a YAML file to JSON with python ## macOS.bash Functions useful when writing scripts to be run on macOS -- `_haveScriptableFinder_` Determine whether we can script the Finder or not -- `_guiInput_` Ask for user input using a Mac dialog box +- **`_guiInput_`** Ask for user input using a Mac dialog box +- **`_haveScriptableFinder_`** Determine whether we can script the Finder or not +- **`_useGNUUtils_`** Add GNU utilities to PATH to allow consistent use of sed/grep/tar/etc. on MacOS -## numbers.bash +## misc.bash -Helpers to work with numbers +Miscellaneous functions -- `_fromSeconds_` Convert seconds to HH:MM:SS -- `_toSeconds_` Converts HH:MM:SS to seconds -- `_countdown_` Sleep for a specified amount of time +- **`_acquireScriptLock_`** Acquire script lock to prevent running the same script a second time before the first instance exits +- **`_detectLinuxDistro_`** Detects the host computer's distribution of Linux +- **`_detectMacOSVersion_`** Detects the host computer's version of macOS +- **`_detectOS_`** Detect the the host computer's operating system +- **`_execute_`** Executes commands with safety and logging options. Respects `DRYRUN` and `VERBOSE` flags. +- **`_findBaseDir_`** Locates the real directory of the script being run. Similar to GNU readlink -n +- **`_generateUUID_`** Generates a unique UUID +- **`_makeProgressBar_`** Prints a progress bar within a for/while loop +- **`_runAsRoot_`** Run the requested command as root (via sudo if requested) +- **`_seekConfirmation_`** Seek user input for yes/no question +- **`_trapCleanup_`** Cleans up after a trapped error. ## services.bash Functions to work with external services -- `_haveInternet_` Tests to see if there is an active Internet connection -- `_httpStatus_` Report the HTTP status of a specified URL -- `_pushover_` Sends a notification via Pushover (Requires API keys) +- **`_haveInternet_`** Tests to see if there is an active Internet connection +- **`_httpStatus_`** Report the HTTP status of a specified URL +- **`_pushover_`** Sends a notification via Pushover (Requires API keys) -## testProcessing.bash +## strings.bash -Work with strings in your script +Functions for string manipulation -- `_cleanString_` Cleans a string of text -- `_stopWords_` Removes common stopwords from a string. Requires a sed stopwords file. Customize to your needs. -- `_escape_` Escapes a string by adding `\` before special chars -- `_htmlDecode_` Decode HTML characters with sed. (Requires sed file) -- `_htmlEncode_` Encode HTML characters with sed (Requires sed file) -- `_lower_` Convert a string to lowercase -- `_upper_` Convert a string to uppercase -- `_ltrim_` Removes all leading whitespace (from the left) -- `_regex_` Use regex to validate and parse strings -- `_rtrim_` Removes all leading whitespace (from the right) -- `_trim_` Removes all leading/trailing whitespace -- `_urlEncode_` URL encode a string -- `_urlDecode_` Decode a URL encoded string +- **`_cleanString_`** Cleans a string of text +- **`_decodeHTML_`** Decode HTML characters with sed. (Requires sed file) +- **`_decodeURL_`** Decode a URL encoded string +- **`_encodeHTML_`** Encode HTML characters with sed (Requires sed file) +- **`_encodeURL_`** URL encode a string +- **`_escapeString_`** Escapes a string by adding `\` before special chars +- **`_lower_`** Convert a string to lowercase +- **`_ltrim_`** Removes all leading whitespace (from the left) +- **`_regexCapture_`** Use regex to validate and parse strings +- **`_rtrim_`** Removes all leading whitespace (from the right) +- **`_splitString_`** Split a string based on a given delimeter +- **`_stringContains_`** Tests whether a string matches a substring +- **`_stringRegex_`** Tests whether a string matches a regex pattern +- **`_stripANSI_`** Strips ANSI escape sequences from text +- **`_stripStopwords_`** Removes common stopwords from a string using a list of sed replacements located in an external file. +- **`_trim_`** Removes all leading/trailing whitespace +- **`_upper_`** Convert a string to uppercase -## A Note on Code Reuse +## template_utils.bash + +Functions required to allow the script template and alert functions to be used + +- **`_makeTempDir_`** Creates a temp directory to house temporary files +- **`_safeExit_`** Cleans up temporary files before exiting a script +- **`_setPATH_`** Add directories to $PATH so script can find executables + +# Coding conventions + +Where possible, I follow [defensive BASH programming](https://kfirlavi.herokuapp.com/blog/2012/11/14/defensive-bash-programming/) principles. + +- Function names use camel case surrounded by underscores: `_nameOfFunction_` +- Local variable names use camel case with a starting underscore: `_localVariable` +- Global variables are in ALL_CAPS with underscores seperating words +- Exceptions to the variable an function naming rules are made for alerting functions and colors to ease my speed of programming. (Breaking years of habits is hard...) I.e. `notice "Some log item: ${blue}blue text${reset}` Where `notice` is a function and `$blue` and `$reset` are global variables but are lowercase. +- Variables are always surrounded by quotes and brackets `"${1}"` (It's verbose, but a safe practice) +- Formatting is provided by [shfmt](https://github.com/mvdan/sh) using 4 spaces for indentation + +## A Note on Code Reuse and Prior Art I compiled these scripting utilities over many years without having an intention to make them public. As a novice programmer, I have Googled, GitHubbed, and StackExchanged a path to solve my own scripting needs. I often lift a function whole-cloth from a GitHub repo don't keep track of its original location. I have done my best within these files to recreate my footsteps and give credit to the original creators of the code when possible. Unfortunately, I fear that I missed as many as I found. My goal in making this repository public is not to take credit for the code written by others. If you recognize something that I didn't credit, please let me know. diff --git a/scriptTemplate.sh b/scriptTemplate.sh deleted file mode 100755 index 7cc93dc..0000000 --- a/scriptTemplate.sh +++ /dev/null @@ -1,480 +0,0 @@ -#!/usr/bin/env bash - -_mainScript_() { - - # Replace everything in _mainScript_() with your script's code - header "Showing alert colors" - debug "This is debug text" - info "This is info text" - notice "This is notice text" - dryrun "This is dryrun text" - warning "This is warning text" - error "This is error text" - success "This is success text" - input "This is input text" - -} # end _mainScript_ - -# ################################## Flags and defaults -# Script specific - -# Common -LOGFILE="${HOME}/logs/$(basename "$0").log" -QUIET=false -LOGLEVEL=ERROR -VERBOSE=false -FORCE=false -DRYRUN=false -declare -a ARGS=() -NOW=$(LC_ALL=C date +"%m-%d-%Y %r") # Returns: 06-14-2015 10:34:40 PM -DATESTAMP=$(LC_ALL=C date +%Y-%m-%d) # Returns: 2015-06-14 -HOURSTAMP=$(LC_ALL=C date +%r) # Returns: 10:34:40 PM -TIMESTAMP=$(LC_ALL=C date +%Y%m%d_%H%M%S) # Returns: 20150614_223440 -LONGDATE=$(LC_ALL=C date +"%a, %d %b %Y %H:%M:%S %z") # Returns: Sun, 10 Jan 2016 20:47:53 -0500 -GMTDATE=$(LC_ALL=C date -u -R | sed 's/\+0000/GMT/') # Returns: Wed, 13 Jan 2016 15:55:29 GMT - -# ################################## Custom utility functions - -# ################################## Common Functions for script template - -_setColors_() { - # DESC: Sets colors use for alerts. - # ARGS: None - # OUTS: None - # USAGE: echo "${blue}Some text${reset}" - - if tput setaf 1 &>/dev/null; then - bold=$(tput bold) - underline=$(tput smul) - reverse=$(tput rev) - reset=$(tput sgr0) - - if [[ $(tput colors) -ge 256 ]] 2>/dev/null; then - white=$(tput setaf 231) - blue=$(tput setaf 38) - yellow=$(tput setaf 11) - tan=$(tput setaf 3) - green=$(tput setaf 82) - red=$(tput setaf 1) - purple=$(tput setaf 171) - gray=$(tput setaf 250) - else - white=$(tput setaf 7) - blue=$(tput setaf 38) - yellow=$(tput setaf 3) - tan=$(tput setaf 3) - green=$(tput setaf 2) - red=$(tput setaf 1) - purple=$(tput setaf 13) - gray=$(tput setaf 7) - fi - else - bold="\033[4;37m" - reset="\033[0m" - underline="\033[4;37m" - reverse="" - white="\033[0;37m" - blue="\033[0;34m" - yellow="\033[0;33m" - tan="\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: None - # 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 function_name color - local alertType="${1}" - local message="${2}" - local line="${3:-}" # Optional line number - - if [[ -n ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} (line: ${line}) $(_functionStack_)" - elif [[ -n ${line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} (line: ${line})" - elif [[ -z ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} $(_functionStack_)" - 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}${tan}" - 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 ]]; then # Don't use colors on non-recognized terminals - color="" - reset="" - fi - - echo -e "$(date +"%r") ${color}$(printf "[%7s]" "${alertType}") ${message}${reset}" - } - _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 - if command -v gsed &>/dev/null; then - local cleanmessage="$(echo "${message}" | gsed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" - else - local cleanmessage="$(echo "${message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" - fi - echo -e "$(date +"%b %d %R:%S") $(printf "[%7s]" "${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} =~ ^(die|error|fatal|warning|info|notice|success) ]]; then - _writeToLog_ - fi - ;; - NOTICE | notice | Notice) - if [[ ${alertType} =~ ^(die|error|fatal|warning|notice|success) ]]; then - _writeToLog_ - fi - ;; - WARN | warn | Warn) - if [[ ${alertType} =~ ^(die|error|fatal|warning) ]]; then - _writeToLog_ - fi - ;; - ERROR | error | Error) - if [[ ${alertType} =~ ^(die|error|fatal) ]]; then - _writeToLog_ - fi - ;; - FATAL | fatal | Fatal) - if [[ ${alertType} =~ ^(die|fatal) ]]; then - _writeToLog_ - fi - ;; - OFF | off) - return 0 - ;; - *) - if [[ ${alertType} =~ ^(die|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:-}"; } -die() { - _alert_ fatal "${1}" "${2:-}" - _safeExit_ "1" -} -fatal() { - _alert_ fatal "${1}" "${2:-}" - _safeExit_ "1" -} - -_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 ${tan}'${LOCK_DIR}'${red}" - fi - fi - - if [[ -n ${TMP_DIR:-} && -d ${TMP_DIR:-} ]]; then - if [[ ${1:-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then - # Do something here to save TMP_DIR on a non-zero script exit for debugging - command rm -r "${TMP_DIR}" - debug "Removing temp directory" - 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 - # 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 - # OUTS: None - - local line=${1:-} # LINENO - local linecallfunc=${2:-} - local command="${3:-}" - local funcstack="${4:-}" - local script="${5:-}" - local sourced="${6:-}" - - funcstack="'$(echo "$funcstack" | sed -E 's/ / < /g')'" - - if [[ ${script##*/} == "${sourced##*/}" ]]; then - fatal "${7:-} command: '${command}' (line: ${line}) [func: $(_functionStack_)]" - else - fatal "${7:-} command: '${command}' (func: ${funcstack} called at line ${linecallfunc} of '${script##*/}') (line: $line of '${sourced##*/}') " - fi - - _safeExit_ "1" -} - -_makeTempDir_() { - # DESC: Creates a temp directory to house temporary files - # ARGS: $1 (Optional) - First characters/word of directory name - # OUTS: $TMP_DIR - Temporary 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}" -} - -_acquireScriptLock_() { - # DESC: Acquire script lock - # ARGS: $1 (optional) - Scope of script execution lock (system or user) - # OUTS: $SCRIPT_LOCK - Path to the directory indicating we have the script lock - # NOTE: This lock implementation is extremely simple but should be reliable - # across all platforms. It does *not* support locking a script with - # symlinks or multiple hardlinks as there's no portable way of doing so. - # If the lock was acquired it's automatically released in _safeExit_() - - local LOCK_DIR - if [[ ${1:-} == 'system' ]]; then - LOCK_DIR="${TMPDIR:-/tmp/}$(basename "$0").lock" - else - LOCK_DIR="${TMPDIR:-/tmp/}$(basename "$0").$UID.lock" - fi - - if command mkdir "${LOCK_DIR}" 2>/dev/null; then - readonly SCRIPT_LOCK="${LOCK_DIR}" - debug "Acquired script lock: ${tan}${SCRIPT_LOCK}${purple}" - else - error "Unable to acquire script lock: ${tan}${LOCK_DIR}${red}" - fatal "If you trust the script isn't running, delete the lock dir" - fi -} - -_functionStack_() { - # DESC: Prints the function stack in use - # ARGS: None - # OUTS: Prints [function]:[file]:[line] - # NOTE: Does not print functions from the alert class - local _i - funcStackResponse=() - for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do - case "${FUNCNAME[$_i]}" in "_alert_" | "_trapCleanup_" | fatal | error | warning | notice | info | verbose | debug | dryrun | header | success | die) continue ;; esac - funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}") - done - printf "( " - printf %s "${funcStackResponse[0]}" - printf ' < %s' "${funcStackResponse[@]:1}" - printf ' )\n' -} - -_parseOptions_() { - # Iterate over options - # breaking -ab into -a -b when needed and --foo=bar into --foo bar - optstring=h - unset options - 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:"* && ${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 - while [[ ${1:-} == -?* ]]; do - case $1 in - # Custom options - - # Common options - -h | --help) - _usage_ >&2 - _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 - ;; - *) fatal "invalid option: '$1'." ;; - esac - shift - done - ARGS+=("$@") # Store the remaining user input as arguments. -} - -_usage_() { - cat </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) + tan=$(tput setaf 3) + green=$(tput setaf 82) + red=$(tput setaf 1) + purple=$(tput setaf 171) + gray=$(tput setaf 250) + else + white=$(tput setaf 7) + blue=$(tput setaf 38) + yellow=$(tput setaf 3) + tan=$(tput setaf 3) + green=$(tput setaf 2) + red=$(tput setaf 1) + purple=$(tput setaf 13) + gray=$(tput setaf 7) + fi + else + bold="\033[4;37m" + reset="\033[0m" + underline="\033[4;37m" + reverse="" + white="\033[0;37m" + blue="\033[0;34m" + yellow="\033[0;33m" + tan="\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 + + printf "%s ${_color}[%7s] %s${reset}\n" "$(date +"%r")" "${_alertType}" "${_message}" + } + _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="$(echo "${_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" +} + +_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 + _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 ;; esac + _funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}") + done + printf "( " + printf %s "${_funcStackResponse[0]}" + printf ' < %s' "${_funcStackResponse[@]:1}" + printf ' )\n' +} + +_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 ${tan}'${LOCK_DIR}'" + 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 + # 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:-}" + + if [[ "$(declare -f "fatal")" && "$(declare -f "_printFuncStack_")" ]]; then + _funcstack="'$(echo "${_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_")" ]; 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}" +} + +_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 "${LOCK_DIR}" 2>/dev/null; then + readonly SCRIPT_LOCK="${_lockDir}" + debug "Acquired script lock: ${yellow}${SCRIPT_LOCK}${purple}" + else + if [ "$(declare -f "_safeExit_")" ]; then + error "Unable to acquire script lock: ${tan}${LOCK_DIR}${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: ${LOCK_DIR}" + exit 1 + fi + + fi +} + +_setPATH_() { + # DESC: + # Add directories to $PATH so script can find executables + # ARGS: + # $@ - One or more paths + # OUTS: Adds items to $PATH + # USAGE: + # _setPATH_ "/usr/local/bin" "${HOME}/bin" "$(npm bin)" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _newPath + + for _newPath in "$@"; do + if [ -d "${_newPath}" ]; then + if ! echo "${PATH}" | grep -Eq "(^|:)${_newPath}($|:)"; then + if PATH="${_newPath}:${PATH}"; then + debug "Added '${_newPath}' to PATH" + else + return 1 + fi + else + debug "_setPATH_: '${_newPath}' already exists in PATH" + fi + else + debug "_setPATH_: can not find: ${_newPath}" + return 1 + 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_")" ] && 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"; then + return 0 + else + return 1 + fi + +} + +_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}:"* && ${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 + 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_")" ]; then + fatal "invalid option: $1" + else + printf "%s\n" "Invalid option: $1" + exit 1 + fi + ;; + esac + shift + done + ARGS+=("$@") # Store the remaining user input as arguments. +} + +_usage_() { + cat <&2; then source "${SOURCEFILE}" @@ -27,6 +29,22 @@ else exit 1 fi +if test -f "${BASEHELPERS}" >&2; then + source "${BASEHELPERS}" +else + echo "Sourcefile not found: ${BASEHELPERS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +if test -f "${CHECKS}" >&2; then + source "${CHECKS}" +else + echo "Sourcefile not found: ${CHECKS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + setup() { # Set arrays @@ -82,40 +100,196 @@ teardown() { assert_failure } -@test "_join_: Join array comma" { - run _join_ , "${B[@]}" +@test "_joinArray_: Join array comma" { + run _joinArray_ , "${B[@]}" + assert_success assert_output "1,2,3,4,5,6" } -@test "_join_: Join array space" { - run _join_ " " "${B[@]}" +@test "_joinArray_: Join array space" { + run _joinArray_ " " "${B[@]}" + assert_success assert_output "1 2 3 4 5 6" } -@test "_join_: Join string complex" { - run _join_ , a "b c" d +@test "_joinArray_: Join string complex" { + run _joinArray_ , a "b c" d + assert_success assert_output "a,b c,d" } -@test "_join_: join string simple" { - run _join_ / var usr tmp +@test "_joinArray_: join string simple" { + run _joinArray_ / var usr tmp + assert_success assert_output "var/usr/tmp" } -@test "_setdiff_: Print elements not common to arrays" { - set +o nounset - run _setdiff_ "${A[*]}" "${B[*]}" - assert_output "one two three" +@test "_setDiff_: Print elements not common to arrays" { + run _setDiff_ "A[@]" "B[@]" + assert_success + assert_line --index 0 "one" + assert_line --index 1 "two" + assert_line --index 2 "three" - run _setdiff_ "${B[*]}" "${A[*]}" - assert_output "4 5 6" + run _setDiff_ "B[@]" "A[@]" + assert_success + assert_line --index 0 "4" + assert_line --index 1 "5" + assert_line --index 2 "6" } -@test "_removeDupes_: remove duplicates" { - set +o nounset - run _removeDupes_ "${DUPES[@]}" - assert_line --index 0 "3" +@test "_setDiff_: Fail when no diff" { + run _setDiff_ "A[@]" "A[@]" + assert_failure +} + +@test "_randomArrayElement_" { + run _randomArrayElement_ "${A[@]}" + assert_success + assert_output --regexp '^one|two|three|1|2|3$' +} + +@test "_dedupeArray_: remove duplicates" { + run _dedupeArray_ "${DUPES[@]}" + assert_success + assert_line --index 0 "1" assert_line --index 1 "2" - assert_line --index 2 "1" - assert_line --index 3 "" + assert_line --index 2 "3" +} + +@test "_isEmptyArray_: empty" { + declare -a emptyArray=() + run _isEmptyArray_ "${emptyArray[@]}" + assert_success +} + +@test "_isEmptyArray_: not empty" { + fullArray=(1 2 3) + run _isEmptyArray_ "${fullArray[@]}" + assert_failure +} + +@test "_sortArray_" { + unsorted_array=("c" "b" "c" "4" "1" "3" "a" "2" "d") + run _sortArray_ "${unsorted_array[@]}" + assert_success + assert_line --index 0 "1" + assert_line --index 1 "2" + assert_line --index 2 "3" + assert_line --index 3 "4" + assert_line --index 4 "a" + assert_line --index 5 "b" + assert_line --index 6 "c" + assert_line --index 7 "c" + assert_line --index 8 "d" +} + +@test "_reverseSortArray_" { + unsorted_array=("c" "b" "c" "4" "1" "3" "a" "2" "d") + run _reverseSortArray_ "${unsorted_array[@]}" + assert_success + assert_line --index 0 "d" + assert_line --index 1 "c" + assert_line --index 2 "c" + assert_line --index 3 "b" + assert_line --index 4 "a" + assert_line --index 5 "4" + assert_line --index 6 "3" + assert_line --index 7 "2" + assert_line --index 8 "1" +} + +@test "_mergeArrays_" { + a1=(1 2 3) + a2=(3 2 1) + run _mergeArrays_ "a1[@]" "a2[@]" + assert_success + assert_line --index 0 "1" + assert_line --index 1 "2" + assert_line --index 2 "3" + assert_line --index 3 "3" + assert_line --index 4 "2" + assert_line --index 5 "1" +} + + +@test "_forEachDo_" { + test_func() { + printf "print value: %s\n" "$1" + return 0 + } + array=(1 2 3 4 5) + + run _forEachDo_ "test_func" < <(printf "%s\n" "${array[@]}") + assert_success + assert_line --index 0 "print value: 1" + assert_line --index 1 "print value: 2" + assert_line --index 2 "print value: 3" + assert_line --index 3 "print value: 4" + assert_line --index 4 "print value: 5" +} + +@test "_forEachValidate_: success" { + array=("a" "abcdef" "ppll" "xyz") + + run _forEachValidate_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_success +} + +@test "_forEachValidate_: failure" { + array=("a" "abcdef" "ppll99" "xyz") + + run _forEachValidate_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_failure +} + +@test "_forEachFind_: success" { + array=("1" "234" "success" "45p9") + + run _forEachFind_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_success + assert_output "success" +} + +@test "_forEachFind_: failure" { + array=("1" "2" "3" "4") + + run _forEachFind_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_failure +} + +@test "_forEachFilter_" { + array=(1 2 3 a ab 5 cde 6) + + run _forEachFilter_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_success + assert_line --index 0 "a" + assert_line --index 1 "ab" + assert_line --index 2 "cde" +} + +@test "_forEachReject_" { + array=(1 2 3 a ab 5 cde 6) + + run _forEachReject_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_success + assert_line --index 0 "1" + assert_line --index 1 "2" + assert_line --index 2 "3" + assert_line --index 3 "5" + assert_line --index 4 "6" +} + +@test "_forEachSome_: success" { + array=("1" "234" "success" "45p9") + + run _forEachSome_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_success +} + +@test "_forEachSome_: failure" { + array=("1" "2" "3" "4") + + run _forEachSome_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + assert_failure } diff --git a/test/checks.bats b/test/checks.bats new file mode 100755 index 0000000..6882f93 --- /dev/null +++ b/test/checks.bats @@ -0,0 +1,256 @@ +#!/usr/bin/env bats +#shellcheck disable + +load 'test_helper/bats-support/load' +load 'test_helper/bats-file/load' +load 'test_helper/bats-assert/load' + +######## SETUP TESTS ######## +ROOTDIR="$(git rev-parse --show-toplevel)" +SOURCEFILE="${ROOTDIR}/utilities/checks.bash" +BASEHELPERS="${ROOTDIR}/utilities/misc.bash" +ALERTS="${ROOTDIR}/utilities/alerts.bash" + +if test -f "${SOURCEFILE}" >&2; then + source "${SOURCEFILE}" +else + echo "Sourcefile not found: ${SOURCEFILE}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +if test -f "${ALERTS}" >&2; then + source "${ALERTS}" + _setColors_ #Set color constants +else + echo "Sourcefile not found: ${ALERTS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +if test -f "${BASEHELPERS}" >&2; then + source "${BASEHELPERS}" +else + echo "Sourcefile not found: ${BASEHELPERS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +setup() { + + TESTDIR="$(temp_make)" + curPath="${PWD}" + + BATSLIB_FILE_PATH_REM="#${TEST_TEMP_DIR}" + BATSLIB_FILE_PATH_ADD='' + + pushd "${TESTDIR}" &>/dev/null + + ######## DEFAULT FLAGS ######## + LOGFILE="${TESTDIR}/logs/log.txt" + QUIET=false + LOGLEVEL=OFF + VERBOSE=false + FORCE=false + DRYRUN=false + + set -o errtrace + set -o nounset + set -o pipefail +} + +teardown() { + set +o nounset + set +o errtrace + set +o pipefail + + popd &>/dev/null + temp_del "${TESTDIR}" +} + +######## RUN TESTS ######## +@test "Sanity..." { + run true + + assert_success + assert_output "" +} + +@test "_functionExists_: Success" { + run _functionExists_ "_varIsEmpty_" + assert_success +} + +@test "_functionExists_: Failure" { + run _functionExists_ "_someUndefinedFunction_" + assert_failure +} + +@test "_binaryExists_: true" { + run _binaryExists_ "vi" + assert_success +} + +@test "_binaryExists_: false" { + run _binaryExists_ "someNonexistantBinary" + assert_failure +} + +@test "_isEmail_: true" { + run _isEmail_ "some.email+name@gmail.com" + assert_success +} + +@test "_isEmail_: false" { + run _isEmail_ "testemail" + assert_failure +} + +@test "_isIPv4_: true" { + run _isIPv4_ "192.168.1.1" + assert_success + run _isIPv4_ "4.2.2.2" + assert_success + run _isIPv4_ "0.192.168.1" + assert_success + run _isIPv4_ "255.255.255.255" + assert_success +} + +@test "_isIPv4_: false" { + run _isIPv4_ "1.b.c.d" + assert_failure + run _isIPv4_ "1234.123.123.123" + assert_failure + run _isIPv4_ "192.168.0" + assert_failure + run _isIPv4_ "255.255.255.256" + assert_failure +} + +@test "_isIPv6_: true" { + run _isIPv6_ "2001:db8:85a3:8d3:1319:8a2e:370:7348" + assert_success + run _isIPv6_ "fe80::1ff:fe23:4567:890a" + assert_success + run _isIPv6_ "fe80::1ff:fe23:4567:890a%eth2" + assert_success + run _isIPv6_ "::" + assert_success + run _isIPv6_ "2001:db8::" + assert_success +} + +@test "_isIPv6_: false" { + run _isIPv6_ "2001:0db8:85a3:0000:0000:8a2e:0370:7334:foo:bar" + assert_failure + run _isIPv6_ "fezy::1ff:fe23:4567:890a" + assert_failure + run _isIPv6_ "192.168.0" +} + +@test "_isFile_: true" { + touch testfile.txt + run _isFile_ "testfile.txt" + assert_success +} + +@test "_isFile_: false" { + run _isFile_ "testfile.txt" + assert_failure +} + +@test "_isDir_: true" { + mkdir -p "some/path" + run _isDir_ "some/path" + assert_success +} + +@test "_isDir_: false" { + run _isDir_ "some/path" + assert_failure +} + +@test "_isAlpha_: true " { + testVar="abc" + run _isAlpha_ "${testVar}" + assert_success +} + +@test "_isAlpha_: false " { + testVar="ab c" + run _isAlpha_ "${testVar}" + assert_failure +} + +@test "_isNum_: true " { + testVar="123" + run _isNum_ "${testVar}" + assert_success +} + +@test "_isNum_: false " { + testVar="12 3" + run _isNum_ "${testVar}" + assert_failure +} + +@test "_isAlphaDash_: true " { + testVar="abc_123-xyz" + run _isAlphaDash_ "${testVar}" + assert_success +} + +@test "_isAlphaDash_: false " { + testVar="abc_123 xyz" + run _isAlphaDash_ "${testVar}" + assert_failure +} + +@test "_isAlphaNum_: true " { + testVar="abc123" + run _isAlphaNum_ "${testVar}" + assert_success +} + +@test "_isAlphaNum_: false " { + testVar="ab c123" + run _isAlphaNum_ "${testVar}" + assert_failure +} + +@test "_varIsFalse_: true" { + testvar=false + run _varIsFalse_ "${testvar}" + assert_success +} + +@test "_varIsFalse_: false" { + testvar=true + run _variableIsFalse_ "${testvar}" + assert_failure +} + +@test "_varIsTrue_: true" { + testvar=true + run _varIsTrue_ "${testvar}" + assert_success +} + +@test "_varIsTrue_: false" { + testvar=false + run _varIsTrue_ "${testvar}" + assert_failure +} + +@test "_varIsEmpty_: true" { + testvar="" + run _varIsEmpty_ "${testvar}" + assert_success +} + +@test "_varIsEmpty_: false" { + testvar=test + run _varIsEmpty_ "${testvar}" + assert_failure +} diff --git a/test/dates.bats b/test/dates.bats index 516fcef..d61b234 100755 --- a/test/dates.bats +++ b/test/dates.bats @@ -8,6 +8,7 @@ load 'test_helper/bats-assert/load' ######## SETUP TESTS ######## ROOTDIR="$(git rev-parse --show-toplevel)" SOURCEFILE="${ROOTDIR}/utilities/dates.bash" +BASEHELPERS="${ROOTDIR}/utilities/misc.bash" ALERTS="${ROOTDIR}/utilities/alerts.bash" if test -f "${SOURCEFILE}" >&2; then @@ -27,6 +28,14 @@ else exit 1 fi +if test -f "${BASEHELPERS}" >&2; then + source "${BASEHELPERS}" +else + echo "Sourcefile not found: ${BASEHELPERS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + setup() { TESTDIR="$(temp_make)" @@ -47,6 +56,30 @@ setup() { assert_output "" } +@test "_dateUnixTimestamp_" { + run _dateUnixTimestamp_ + + assert_success + assert_output --regexp "^[0-9]+$" +} + +@test "_convertToUnixTimestamp_" { + run _convertToUnixTimestamp_ "2020-07-07 18:38" + assert_success + assert_output "1594161480" +} + +@test "_readableUnixTimestamp_: Default Format" { + run _readableUnixTimestamp_ "1591554426" + assert_success + assert_output "2020-06-07 14:27:06" +} + +@test "_readableUnixTimestamp_: Custom format" { + run _readableUnixTimestamp_ "1591554426" "%Y-%m-%d" + assert_success + assert_output "2020-06-07" +} @test "_monthToNumber_: 1" { run _monthToNumber_ "dec" @@ -85,20 +118,20 @@ setup() { @test "_parseDate_: YYYY MM DD 1" { run _parseDate_ "2019 06 01" assert_success - assert_output --regexp "_parseDate_found: +2019 06 01" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +June" - assert_output --regexp "_parseDate_month: +6" + assert_output --regexp "PARSE_DATE_FOUND: +2019 06 01" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +June" + assert_output --regexp "PARSE_DATE_MONTH: +6" } @test "_parseDate_: YYYY MM DD 2" { run _parseDate_ "this is text 2019-06-01 and more text" assert_success - assert_output --regexp "_parseDate_found: +2019-06-01" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +June" - assert_output --regexp "_parseDate_month: +6" - assert_output --regexp "_parseDate_day: +1" + assert_output --regexp "PARSE_DATE_FOUND: +2019-06-01" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +June" + assert_output --regexp "PARSE_DATE_MONTH: +6" + assert_output --regexp "PARSE_DATE_DAY: +1" } @test "_parseDate_: YYYY MM DD fail 1" { @@ -114,101 +147,101 @@ setup() { @test "_parseDate_: Month DD, YYYY" { run _parseDate_ "this is text Oct 22, 2019 and more text" assert_success - assert_output --regexp "_parseDate_found: +Oct 22, +2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +Oct 22, +2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: Month DD YYYY" { run _parseDate_ "Oct 22 2019" assert_success - assert_output --regexp "_parseDate_found: +Oct 22 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +Oct 22 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: Month DD, YY" { run _parseDate_ "this is text Oct 22, 19 and more text" assert_success - assert_output --regexp "_parseDate_found: +Oct 22, 19" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +Oct 22, 19" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: Month DD YY" { run _parseDate_ "Oct 22 19" assert_success - assert_output --regexp "_parseDate_found: +Oct 22 19" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +Oct 22 19" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: DD Month, YYYY" { run _parseDate_ "22 June, 2019 and more text" assert_success - assert_output --regexp "_parseDate_found: +22 June, 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +June" - assert_output --regexp "_parseDate_month: +6" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +22 June, 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +June" + assert_output --regexp "PARSE_DATE_MONTH: +6" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: DD Month YYYY" { run _parseDate_ "some text66-here-22 June 2019 and more text" assert_success - assert_output --regexp "_parseDate_found: +22 June 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +June" - assert_output --regexp "_parseDate_month: +6" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +22 June 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +June" + assert_output --regexp "PARSE_DATE_MONTH: +6" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: MM DD YYYY 1" { run _parseDate_ "this is text 12 22 2019 and more text" assert_success - assert_output --regexp "_parseDate_found: +12 22 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +December" - assert_output --regexp "_parseDate_month: +12" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +12 22 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +December" + assert_output --regexp "PARSE_DATE_MONTH: +12" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: MM DD YYYY 2" { run _parseDate_ "12 01 2019" assert_success - assert_output --regexp "_parseDate_found: +12 01 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +December" - assert_output --regexp "_parseDate_month: +12" - assert_output --regexp "_parseDate_day: +1" + assert_output --regexp "PARSE_DATE_FOUND: +12 01 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +December" + assert_output --regexp "PARSE_DATE_MONTH: +12" + assert_output --regexp "PARSE_DATE_DAY: +1" } @test "_parseDate_: MM DD YYYY 3" { run _parseDate_ "a-test-01-12-2019-is here" assert_success - assert_output --regexp "_parseDate_found: +01-12-2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +12" + assert_output --regexp "PARSE_DATE_FOUND: +01-12-2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +12" } @test "_parseDate_: DD MM YYYY 1 " { run _parseDate_ "a-test-22/12/2019-is here" assert_success - assert_output --regexp "_parseDate_found: +22/12/2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +December" - assert_output --regexp "_parseDate_month: +12" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +22/12/2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +December" + assert_output --regexp "PARSE_DATE_MONTH: +12" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: DD MM YYYY 2 " { @@ -219,31 +252,31 @@ setup() { @test "_parseDate_: DD MM YY" { run _parseDate_ "a-test-22-12-19-is here" assert_success - assert_output --regexp "_parseDate_found: +22-12-19" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +December" - assert_output --regexp "_parseDate_month: +12" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +22-12-19" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +December" + assert_output --regexp "PARSE_DATE_MONTH: +12" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: MM DD YY 1 " { run _parseDate_ "a-test-12/22/19-is here" assert_success - assert_output --regexp "_parseDate_found: +12/22/19" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +December" - assert_output --regexp "_parseDate_month: +12" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +12/22/19" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +December" + assert_output --regexp "PARSE_DATE_MONTH: +12" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: MM DD YY 2 " { run _parseDate_ "6 8 19" assert_success - assert_output --regexp "_parseDate_found: +6 8 19" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +June" - assert_output --regexp "_parseDate_month: +6" - assert_output --regexp "_parseDate_day: +8" + assert_output --regexp "PARSE_DATE_FOUND: +6 8 19" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +June" + assert_output --regexp "PARSE_DATE_MONTH: +6" + assert_output --regexp "PARSE_DATE_DAY: +8" } @test "_parseDate_: MM DD YY 3 " { @@ -264,139 +297,139 @@ setup() { @test "_parseDate_: Month, YYYY 1 " { run _parseDate_ "a-test-January, 2019-is here" assert_success - assert_output --regexp "_parseDate_found: +January, 2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +1" + assert_output --regexp "PARSE_DATE_FOUND: +January, 2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +1" } @test "_parseDate_: Month, YYYY 2 " { run _parseDate_ "mar-2019" assert_success - assert_output --regexp "_parseDate_found: +mar-2019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +March" - assert_output --regexp "_parseDate_month: +3" - assert_output --regexp "_parseDate_day: +1" + assert_output --regexp "PARSE_DATE_FOUND: +mar-2019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +March" + assert_output --regexp "PARSE_DATE_MONTH: +3" + assert_output --regexp "PARSE_DATE_DAY: +1" } @test "_parseDate_: YYYYMMDDHHMM 1" { run _parseDate_ "201901220228" assert_success - assert_output --regexp "_parseDate_found: +201901220228" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +22" - assert_output --regexp "_parseDate_hour: +2" - assert_output --regexp "_parseDate_minute: +28" + assert_output --regexp "PARSE_DATE_FOUND: +201901220228" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +22" + assert_output --regexp "PARSE_DATE_HOUR: +2" + assert_output --regexp "PARSE_DATE_MINUTE: +28" } @test "_parseDate_: YYYYMMDDHHMM 2" { run _parseDate_ "asdf 201901220228asdf " assert_success - assert_output --regexp "_parseDate_found: +201901220228" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +22" - assert_output --regexp "_parseDate_hour: +2" - assert_output --regexp "_parseDate_minute: +28" + assert_output --regexp "PARSE_DATE_FOUND: +201901220228" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +22" + assert_output --regexp "PARSE_DATE_HOUR: +2" + assert_output --regexp "PARSE_DATE_MINUTE: +28" } @test "_parseDate_: YYYYMMDDHH 1" { run _parseDate_ "asdf 2019012212asdf " assert_success - assert_output --regexp "_parseDate_found: +2019012212" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +22" - assert_output --regexp "_parseDate_hour: +12" - assert_output --regexp "_parseDate_minute: +00" + assert_output --regexp "PARSE_DATE_FOUND: +2019012212" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +22" + assert_output --regexp "PARSE_DATE_HOUR: +12" + assert_output --regexp "PARSE_DATE_MINUTE: +00" } @test "_parseDate_: YYYYMMDDHH 2" { run _parseDate_ "2019012212" assert_success - assert_output --regexp "_parseDate_found: +2019012212" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +22" - assert_output --regexp "_parseDate_hour: +12" - assert_output --regexp "_parseDate_minute: +00" + assert_output --regexp "PARSE_DATE_FOUND: +2019012212" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +22" + assert_output --regexp "PARSE_DATE_HOUR: +12" + assert_output --regexp "PARSE_DATE_MINUTE: +00" } @test "_parseDate_: MMDDYYYY 1" { run _parseDate_ "01222019" assert_success - assert_output --regexp "_parseDate_found: +01222019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +01222019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: MMDDYYYY 2" { run _parseDate_ "asdf 11222019 asdf" assert_success - assert_output --regexp "_parseDate_found: +11222019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +November" - assert_output --regexp "_parseDate_month: +11" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +11222019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +November" + assert_output --regexp "PARSE_DATE_MONTH: +11" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: DDMMYYYY 1" { run _parseDate_ "16012019" assert_success - assert_output --regexp "_parseDate_found: +16012019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +January" - assert_output --regexp "_parseDate_month: +1" - assert_output --regexp "_parseDate_day: +16" + assert_output --regexp "PARSE_DATE_FOUND: +16012019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +January" + assert_output --regexp "PARSE_DATE_MONTH: +1" + assert_output --regexp "PARSE_DATE_DAY: +16" } @test "_parseDate_: DDMMYYYY 2" { run _parseDate_ "asdf 16112019 asdf" assert_success - assert_output --regexp "_parseDate_found: +16112019" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +November" - assert_output --regexp "_parseDate_month: +11" - assert_output --regexp "_parseDate_day: +16" + assert_output --regexp "PARSE_DATE_FOUND: +16112019" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +November" + assert_output --regexp "PARSE_DATE_MONTH: +11" + assert_output --regexp "PARSE_DATE_DAY: +16" } @test "_parseDate_: YYYYDDMM " { run _parseDate_ "20192210" assert_success - assert_output --regexp "_parseDate_found: +20192210" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +20192210" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: YYYYMMDD 1" { run _parseDate_ "20191022" assert_success - assert_output --regexp "_parseDate_found: +20191022" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +22" + assert_output --regexp "PARSE_DATE_FOUND: +20191022" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +22" } @test "_parseDate_: YYYYMMDD 2" { run _parseDate_ "20191010" assert_success - assert_output --regexp "_parseDate_found: +20191010" - assert_output --regexp "_parseDate_year: +2019" - assert_output --regexp "_parseDate_monthName: +October" - assert_output --regexp "_parseDate_month: +10" - assert_output --regexp "_parseDate_day: +10" + assert_output --regexp "PARSE_DATE_FOUND: +20191010" + assert_output --regexp "PARSE_DATE_YEAR: +2019" + assert_output --regexp "PARSE_DATE_MONTH_NAME: +October" + assert_output --regexp "PARSE_DATE_MONTH: +10" + assert_output --regexp "PARSE_DATE_DAY: +10" } @test "_parseDate_: YYYYMMDD fail" { @@ -426,7 +459,33 @@ setup() { assert_output "12 27, 2019" } -@test "_formatDate_: fail - no input " { - run _formatDate_ - assert_failure +@test "_convertSecs_: Seconds to human readable" { + + run _fromSeconds_ "9255" + assert_success + assert_output "02:34:15" +} + +@test "_toSeconds_: HH MM SS to Seconds" { + run _toSeconds_ 12 3 33 + assert_success + assert_output "43413" +} + +@test "_countdown_: custom message, default wait" { + run _countdown_ 10 0 "something" + assert_line --index 0 --partial "something 10" + assert_line --index 9 --partial "something 1" +} + +@test "_countdown_: default message, custom wait" { + run _countdown_ 5 0 + assert_line --index 0 --partial "... 5" + assert_line --index 4 --partial "... 1" +} + +@test "_countdown_: all defaults" { + run _countdown_ + assert_line --index 0 --partial "... 10" + assert_line --index 9 --partial "... 1" } diff --git a/test/numbers.bats b/test/debug.bats similarity index 64% rename from test/numbers.bats rename to test/debug.bats index 5a9c07d..3ca641d 100755 --- a/test/numbers.bats +++ b/test/debug.bats @@ -7,8 +7,8 @@ load 'test_helper/bats-assert/load' ######## SETUP TESTS ######## ROOTDIR="$(git rev-parse --show-toplevel)" -SOURCEFILE="${ROOTDIR}/utilities/numbers.bash" -BASEHELPERS="${ROOTDIR}/utilities/baseHelpers.bash" +SOURCEFILE="${ROOTDIR}/utilities/debug.bash" +BASEHELPERS="${ROOTDIR}/utilities/misc.bash" ALERTS="${ROOTDIR}/utilities/alerts.bash" if test -f "${SOURCEFILE}" >&2; then @@ -53,7 +53,7 @@ setup() { VERBOSE=false FORCE=false DRYRUN=false - + PASS=123 set -o errtrace set -o nounset set -o pipefail @@ -63,7 +63,6 @@ teardown() { set +o nounset set +o errtrace set +o pipefail - popd &>/dev/null temp_del "${TESTDIR}" } @@ -76,33 +75,28 @@ teardown() { assert_output "" } -@test "_convertSecs_: Seconds to human readable" { - - run _fromSeconds_ "9255" +@test "_printAnsi_" { + testString="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)" + run _printAnsi_ "${testString}" assert_success - assert_output "02:34:15" + assert_output "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m" } -@test "_toSeconds_: HH MM SS to Seconds" { - run _toSeconds_ 12 3 33 +@test "_printArray_: Array" { + testArray=(1 2 3) + run _printArray_ "testArray" assert_success - assert_output "43413" + assert_line --index 0 "0 = 1" + assert_line --index 1 "1 = 2" + assert_line --index 2 "2 = 3" } -@test "_countdown_: custom message, default wait" { - run _countdown_ 10 0 "something" - assert_line --index 0 --partial "something 10" - assert_line --index 9 --partial "something 1" -} +@test "_printArray_: Associative array" { + declare -A assoc_array + assoc_array=([foo]=bar [baz]=foobar) + run _printArray_ "assoc_array" + assert_success + assert_line --index 0 "foo = bar" + assert_line --index 1 "baz = foobar" -@test "_countdown_: default message, custom wait" { - run _countdown_ 5 0 - assert_line --index 0 --partial "... 5" - assert_line --index 4 --partial "... 1" -} - -@test "_countdown_: all defaults" { - run _countdown_ - assert_line --index 0 --partial "... 10" - assert_line --index 9 --partial "... 1" } diff --git a/test/files.bats b/test/files.bats index 1caa084..f26d055 100755 --- a/test/files.bats +++ b/test/files.bats @@ -8,7 +8,7 @@ load 'test_helper/bats-assert/load' ######## SETUP TESTS ######## ROOTDIR="$(git rev-parse --show-toplevel)" SOURCEFILE="${ROOTDIR}/utilities/files.bash" -BASEHELPERS="${ROOTDIR}/utilities/baseHelpers.bash" +BASEHELPERS="${ROOTDIR}/utilities/misc.bash" ALERTS="${ROOTDIR}/utilities/alerts.bash" if test -f "${SOURCEFILE}" >&2; then @@ -260,9 +260,6 @@ _testParseFilename_() { assert_line --index 3 --regexp "\[ debug\].*${PARSE_EXT}: tar\.gzip\.bzip$" assert_line --index 4 --regexp "\[ debug\].*${PARSE_BASENOEXT}: testfile$" } - - # _parseFilename_ "test.tar.gz" - # _parseFilename_ "test.tar.gzip" } _testMakeSymlink_() { @@ -338,7 +335,7 @@ _testMakeSymlink_() { _testParseYAML_() { @test "_parseYAML: success" { - run _parseYAML_ "$YAML1" + run _parseYAML_ "$YAML1" "" assert_success assert_output "$( cat "$YAML1parse")" } @@ -383,59 +380,140 @@ _testParseYAML_() { assert_output "hello world" } -@test "_uniqueFileName_: no extension" { +@test "_createUniqueFilename_: no extension" { touch "test" - run _uniqueFileName_ "test" + run _createUniqueFilename_ "test" assert_output --regexp ".*/test\.1$" } -@test "_uniqueFileName_: no extension - internal integer" { +@test "_createUniqueFilename_: no extension - internal integer" { touch "test" touch "test.1" - run _uniqueFileName_ -i "test" + run _createUniqueFilename_ -i "test" assert_output --regexp ".*/test\.2$" } -@test "_uniqueFileName_: Count to 3" { +@test "_createUniqueFilename_: Count to 3" { touch "test.txt" touch "test.txt.1" touch "test.txt.2" - run _uniqueFileName_ "test.txt" + run _createUniqueFilename_ "test.txt" assert_output --regexp ".*/test\.txt\.3$" } -@test "_uniqueFileName_: internal integer" { +@test "_createUniqueFilename_: internal integer" { touch "test.txt" touch "test.1.txt" touch "test.2.txt" - run _uniqueFileName_ -i "test.txt" + run _createUniqueFilename_ -i "test.txt" assert_output --regexp ".*/test\.3\.txt$" } -@test "_uniqueFileName_: Don't confuse existing numbers" { +@test "_createUniqueFilename_: two extensions" { + touch "test.tar.gz" + touch "test.1.tar.gz" + touch "test.2.tar.gz" + + run _createUniqueFilename_ -i "test.tar.gz" + assert_output --regexp ".*/test\.3\.tar.gz$" +} + +@test "_createUniqueFilename_: Don't confuse existing numbers" { touch "test-2.txt" - run _uniqueFileName_ "test-2.txt" + run _createUniqueFilename_ "test-2.txt" assert_output --regexp ".*/test-2\.txt\.1$" } -@test "_uniqueFileName_: User specified separator" { +@test "_createUniqueFilename_: User specified separator" { touch "test.txt" - run _uniqueFileName_ "test.txt" " " + run _createUniqueFilename_ "test.txt" " " assert_output --regexp ".*/test\.txt 1$" } -@test "_uniqueFileName_: failure" { - run _uniqueFileName_ +@test "_createUniqueFilename_: failure" { + run _createUniqueFilename_ assert_failure } +@test "_fileName_: with extension" { + run _fileName_ "./path/to/file/test.txt" + assert_success + assert_output "test.txt" +} + +@test "_fileName_: without extension" { + run _fileName_ "path/to/file/test" + assert_success + assert_output "test" +} + +@test "_fileBasename_" { + run _fileBasename_ "path/to/file/test.txt" + assert_success + assert_output "test" +} + +@test "_fileExtension_: simple extension" { + run _fileExtension_ "path/to/file/test.txt" + assert_success + assert_output "txt" +} + +@test "_fileExtension_: no extension" { + run _fileExtension_ "path/to/file/test" + assert_failure +} + +@test "_fileExtension_: two level extension" { + run _fileExtension_ "path/to/file/test.tar.bz2" + assert_success + assert_output "tar.bz2" +} + +@test "_fileDirectory_" { + run _fileDirectory_ "path/to/file/test.txt" + assert_success + assert_output "path/to/file" +} + +@test "_fileAbsPath_: file" { + touch "./test.txt" + run _fileAbsPath_ "./test.txt" + assert_success + assert_output --regexp "/.*/files\.bats.*/test\.txt$" +} + +@test "_fileAbsPath_: directory" { + mkdir "./testdir" + run _fileAbsPath_ "./testdir" + assert_success + assert_output --regexp "/.*/files\.bats.*/testdir$" +} + +@test "_fileAbsPath_: fail when not found" { + run _fileAbsPath_ "./test.txt" + assert_failure +} + +@test "_fileContains_: No match" { + echo "some text" > "./test.txt" + run _fileContains_ "./test.txt" "nothing here" + assert_failure +} + +@test "_fileContains_: Pattern matched" { + echo "some text" > "./test.txt" + run _fileContains_ "./test.txt" "some*" + assert_success +} + _testBackupFile_ _testListFiles_ _testParseFilename_ diff --git a/test/baseHelpers.bats b/test/misc.bats similarity index 63% rename from test/baseHelpers.bats rename to test/misc.bats index ee0e9e0..9746fff 100755 --- a/test/baseHelpers.bats +++ b/test/misc.bats @@ -7,7 +7,7 @@ load 'test_helper/bats-assert/load' ######## SETUP TESTS ######## ROOTDIR="$(git rev-parse --show-toplevel)" -SOURCEFILE="${ROOTDIR}/utilities/baseHelpers.bash" +SOURCEFILE="${ROOTDIR}/utilities/misc.bash" ALERTS="${ROOTDIR}/utilities/alerts.bash" if test -f "${SOURCEFILE}" >&2; then @@ -67,18 +67,6 @@ teardown() { assert_output "" } -_testCheckBinary_() { - @test "_checkBinary_: true" { - run _checkBinary_ "vi" - assert_success - } - - @test "_checkBinary_: false" { - run _checkBinary_ "someNonexistantBinary" - assert_failure - } -} - _testExecute_() { @test "_execute_: Debug command" { DRYRUN=true @@ -91,7 +79,7 @@ _testExecute_() { run _execute_ assert_failure - assert_output --regexp "_execute_ needs a command$" + assert_output --regexp "\[ fatal\] Missing required argument to _execute_" } @test "_execute_: Bad command" { @@ -167,97 +155,65 @@ _testExecute_() { assert_file_not_exist "testfile.txt" } } - -_testFindBaseDirectory_() { - @test "_findBaseDir_" { - run _findBaseDir_ - assert_output --regexp "^/usr/local/Cellar/bats-core/[0-9]\.[0-9]\.[0-9]" - } -} - -_testHaveFunction_() { - - @test "_haveFunction_: Success" { - run _haveFunction_ "_haveFunction_" - - assert_success - } - - @test "_haveFunction_: Failure" { - run _haveFunction_ "_someUndefinedFunction_" - - assert_failure - } -} - -_testProgressBar_() { - @test "_progressBar_: verbose" { - verbose=true - run _progressBar_ 100 - - assert_success - assert_output "" - verbose=false - } - - @test "_progressBar_: quiet" { - quiet=true - run _progressBar_ 100 - - assert_success - assert_output "" - quiet=false - } -} - -_testSeekConfirmation_() { - @test "_seekConfirmation_: yes" { - run _seekConfirmation_ 'test' <<<"y" - - assert_success - assert_output --partial "[ input] test" - } - - @test "_seekConfirmation_: no" { - run _seekConfirmation_ 'test' <<<"n" - - assert_failure - assert_output --partial "[ input] test" - } - - @test "_seekConfirmation_: Force" { - FORCE=true - - run _seekConfirmation_ "test" - assert_success - assert_output --partial "test" - } - - @test "_seekConfirmation_: Quiet" { - QUIET=true - run _seekConfirmation_ 'test' <<<"y" - - assert_success - refute_output --partial "test" - - quiet=false - } -} - -_testSetPATH_() { - @test "_setPATH_" { - mkdir -p "${TESTDIR}/testing/from/bats" - _setPATH_ "${TESTDIR}/testing/from/bats" "${TESTDIR}/testing/again" - run echo "${PATH}" - assert_output --regexp "/testing/from/bats" - refute_output --regexp "/testing/again" - } -} - -_testCheckBinary_ _testExecute_ -_testFindBaseDirectory_ -_testHaveFunction_ -_testProgressBar_ -_testSeekConfirmation_ -_testSetPATH_ + +@test "_findBaseDir_" { + run _findBaseDir_ + assert_output --regexp "^/usr/local/Cellar/bats-core/[0-9]\.[0-9]\.[0-9]" +} + +@test "_generateUUID_" { + run _generateUUID_ + assert_success + assert_output --regexp "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" +} + +@test "_makeProgressBar_: verbose" { + verbose=true + run _makeProgressBar_ 100 + + assert_success + assert_output "" + verbose=false +} + +@test "_makeProgressBar_: quiet" { + quiet=true + run _makeProgressBar_ 100 + + assert_success + assert_output "" + quiet=false +} + +@test "_seekConfirmation_: yes" { + run _seekConfirmation_ 'test' <<<"y" + + assert_success + assert_output --partial "[ input] test" +} + +@test "_seekConfirmation_: no" { + run _seekConfirmation_ 'test' <<<"n" + + assert_failure + assert_output --partial "[ input] test" +} + +@test "_seekConfirmation_: Force" { + FORCE=true + + run _seekConfirmation_ "test" + assert_success + assert_output --partial "test" +} + +@test "_seekConfirmation_: Quiet" { + QUIET=true + run _seekConfirmation_ 'test' <<<"y" + + assert_success + refute_output --partial "test" + + quiet=false +} diff --git a/test/scriptTemplate.bats b/test/scriptTemplate.bats index 44e6521..d0ec301 100755 --- a/test/scriptTemplate.bats +++ b/test/scriptTemplate.bats @@ -47,11 +47,11 @@ teardown() { run $s -K assert_failure - assert_output --partial "[ fatal] invalid option: '-K'" + assert_output --partial "[ fatal] invalid option: -K" assert_file_exist "${TESTDIR}/logs/log.txt" run cat "${TESTDIR}/logs/log.txt" - assert_line --index 0 --regexp "\[ fatal\] .* invalid option: '-K'\. \(.*" + assert_line --index 0 --regexp "\[ fatal\] .* invalid option: -K \(.*" } @test "success" { diff --git a/test/standaloneTemplate.bats b/test/standaloneTemplate.bats new file mode 100755 index 0000000..679f7bf --- /dev/null +++ b/test/standaloneTemplate.bats @@ -0,0 +1,128 @@ +#!/usr/bin/env bats + +load 'test_helper/bats-support/load' +load 'test_helper/bats-file/load' +load 'test_helper/bats-assert/load' + +######## SETUP TESTS ######## +ROOTDIR="$(git rev-parse --show-toplevel)" +s="${ROOTDIR}/standaloneTemplate.sh" + +if [ -f "${s}" ]; then + base="$(basename "${s}")" +else + printf "No executable '${s}' found.\n" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + + +setup() { + + TESTDIR="$(temp_make)" + curPath="${PWD}" + + BATSLIB_FILE_PATH_REM="#${TEST_TEMP_DIR}" + BATSLIB_FILE_PATH_ADD='' + + s="$s --logfile=${TESTDIR}/logs/log.txt" # Logs go to temp directory + + pushd "${TESTDIR}" >&2 +} + +teardown() { + popd >&2 + temp_del "${TESTDIR}" +} + + +######## RUN TESTS ########## +@test "sanity" { + run true + assert_success + assert [ "$output" = "" ] +} + +@test "Fail - fail on bad args and create logfile" { + run $s -K + + assert_failure + assert_output --partial "[ fatal] invalid option: -K" + assert_file_exist "${TESTDIR}/logs/log.txt" + + run cat "${TESTDIR}/logs/log.txt" + assert_line --index 0 --regexp "\[ fatal\] .* invalid option: -K \(.*" +} + +@test "success" { + run $s + assert_success + assert_output --partial "[ info] This is info text" + assert_output --partial "[ notice] This is notice text" + assert_output --partial "[ dryrun] This is dryrun text" + assert_output --partial "[warning] This is warning text" + assert_output --partial "[ error] This is error text" + assert_output --partial "[success] This is success text" + assert_output --partial "[ input] This is input text" + + assert_file_exist "${TESTDIR}/logs/log.txt" + run cat "${TESTDIR}/logs/log.txt" + assert_line --index 0 --regexp "\[ error\] \[.*\] This is error text \( _mainScript_:standaloneTemplate.* \)" + assert_line --index 1 "" +} + +@test "success and INFO level log" { + run $s --loglevel=INFO + assert_success + assert_output --partial "[ info] This is info text" + + run cat "${TESTDIR}/logs/log.txt" + assert_line --index 0 --regexp "\[ info\].*This is info text" + assert_line --index 1 --regexp "\[ notice\].*This is notice text" + assert_line --index 2 --regexp "\[warning\].*This is warning text" + assert_line --index 3 --regexp "\[ error\].*This is error text" + assert_line --index 4 --regexp "\[success\].*This is success text" + assert_line --index 5 "" +} + +@test "success and NOTICE level log" { + run $s --loglevel=NOTICE + assert_success + assert_output --partial "[ info] This is info text" + + run cat "${TESTDIR}/logs/log.txt" + assert_line --index 0 --regexp "\[ notice\].*This is notice text" + assert_line --index 1 --regexp "\[warning\].*This is warning text" + assert_line --index 2 --regexp "\[ error\].*This is error text" + assert_line --index 3 --regexp "\[success\].*This is success text" + assert_line --index 4 "" +} + +@test "Usage (-h)" { + run $s -h + + assert_success + assert_line --partial --index 0 "$base [OPTION]... [FILE]..." +} + +@test "Usage (--help)" { + run $s --help + + assert_success + assert_line --partial --index 0 "$base [OPTION]... [FILE]..." +} + +@test "quiet (-q)" { + run $s -q --loglevel=INFO + assert_success + assert_output "" + + run cat "${TESTDIR}/logs/log.txt" + run cat "${TESTDIR}/logs/log.txt" + assert_line --index 0 --regexp "\[ info\].*This is info text" + assert_line --index 1 --regexp "\[ notice\].*This is notice text" + assert_line --index 2 --regexp "\[warning\].*This is warning text" + assert_line --index 3 --regexp "\[ error\].*This is error text" + assert_line --index 4 --regexp "\[success\].*This is success text" + assert_line --index 5 "" +} diff --git a/test/textProcessing.bats b/test/strings.bats similarity index 74% rename from test/textProcessing.bats rename to test/strings.bats index a53f039..3ab7219 100755 --- a/test/textProcessing.bats +++ b/test/strings.bats @@ -7,8 +7,8 @@ load 'test_helper/bats-assert/load' ######## SETUP TESTS ######## ROOTDIR="$(git rev-parse --show-toplevel)" -SOURCEFILE="${ROOTDIR}/utilities/textProcessing.bash" -BASEHELPERS="${ROOTDIR}/utilities/baseHelpers.bash" +SOURCEFILE="${ROOTDIR}/utilities/strings.bash" +BASEHELPERS="${ROOTDIR}/utilities/misc.bash" ALERTS="${ROOTDIR}/utilities/alerts.bash" if test -f "${SOURCEFILE}" >&2; then @@ -76,6 +76,35 @@ teardown() { assert_output "" } +@test "_splitString_" { + run _splitString_ "a,b,cd" "," + assert_success + assert_line --index 0 "a" + assert_line --index 1 "b" + assert_line --index 2 "cd" +} + +@test "_stringContains_: success" { + run _stringContains_ "hello world!" "lo" + assert_success +} + +@test "_stringContains_: failure" { + run _stringContains_ "hello world!" "zebra" + assert_failure +} + +@test "_stringRegex_: success" { + run _stringRegex_ "hello world!" "[a-z].*!$" + assert_success +} + +@test "_stringRegex_: failure" { + run _stringRegex_ "hello world!" "^.*[0-9]+" + assert_failure +} + + _testCleanString_() { @test "_cleanString_: fail" { @@ -160,46 +189,46 @@ _testCleanString_ _testStopWords_() { - @test "_stopWords_: success" { - run _stopWords_ "A string to be parsed" + @test "_stripStopwords_: success" { + run _stripStopwords_ "A string to be parsed" assert_success assert_output "string parsed" } - @test "_stopWords_: success w/ user terms" { - run _stopWords_ "A string to be parsed to help pass this test being performed by bats" "bats,string" + @test "_stripStopwords_: success w/ user terms" { + run _stripStopwords_ "A string to be parsed to help pass this test being performed by bats" "bats,string" assert_success assert_output "parsed pass performed" } - @test "_stopWords_: No changes" { - run _stopWords_ "string parsed pass performed" + @test "_stripStopwords_: No changes" { + run _stripStopwords_ "string parsed pass performed" assert_success assert_output "string parsed pass performed" } - @test "_stopWords_: fail" { - run _stopWords_ + @test "_stripStopwords_: fail" { + run _stripStopwords_ assert_failure } } _testStopWords_ -@test "_escape_" { - run _escape_ "Here is some / text to & be - escaped" +@test "_escapeString_" { + run _escapeString_ "Here is some / text to & be - escaped" assert_success assert_output "Here\ is\ some\ /\ text\ to\ &\ be\ -\ escaped" } -@test "_htmlEncode_" { - run _htmlEncode_ "Here's some text& to > be h?t/M(l• enâ„¢code磧¶d" +@test "_encodeHTML_" { + run _encodeHTML_ "Here's some text& to > be h?t/M(l• enâ„¢code磧¶d" assert_success assert_output "Here's some text& to > be h?t/M(l• en™code磧¶d" } -@test "_htmlDecode_" { - run _htmlDecode_ "♣Here's some text & to > be h?t/M(l• en™code磧¶d" +@test "_decodeHTML_" { + run _decodeHTML_ "♣Here's some text & to > be h?t/M(l• en™code磧¶d" assert_success assert_output "♣Here's some text & to > be h?t/M(l• enâ„¢code磧¶d" } @@ -232,27 +261,27 @@ _testStopWords_ assert_output "MAKE THIS UPPERCASE" } -@test "_urlEncode_" { - run _urlEncode_ "Here's some.text%that&needs_to-be~encoded+a*few@more(characters)" +@test "_encodeURL_" { + run _encodeURL_ "Here's some.text%that&needs_to-be~encoded+a*few@more(characters)" assert_success assert_output "Here%27s%20some.text%25that%26needs_to-be~encoded%2Ba%2Afew%40more%28characters%29" } -@test "_urlDecode_" { - run _urlDecode_ "Here%27s%20some.text%25that%26needs_to-be~encoded%2Ba%2Afew%40more%28characters%29" +@test "_decodeURL_" { + run _decodeURL_ "Here%27s%20some.text%25that%26needs_to-be~encoded%2Ba%2Afew%40more%28characters%29" assert_success assert_output "Here's some.text%that&needs_to-be~encoded+a*few@more(characters)" } -@test "_regex_: success" { - run _regex_ "#FFFFFF" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' || echo "no match found" +@test "_regexCapture_: success" { + run _regexCapture_ "#FFFFFF" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' || echo "no match found" assert_success assert_output "#FFFFFF" } -@test "_regex_: failure" { - run _regex_ "gggggg" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' +@test "_regexCapture_: failure" { + run _regexCapture_ "gggggg" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' assert_failure } diff --git a/test/template_utils.bats b/test/template_utils.bats new file mode 100755 index 0000000..b8dd925 --- /dev/null +++ b/test/template_utils.bats @@ -0,0 +1,93 @@ +#!/usr/bin/env bats +#shellcheck disable + +load 'test_helper/bats-support/load' +load 'test_helper/bats-file/load' +load 'test_helper/bats-assert/load' + +######## SETUP TESTS ######## +ROOTDIR="$(git rev-parse --show-toplevel)" +SOURCEFILE="${ROOTDIR}/utilities/template_utils.bash" +ALERTS="${ROOTDIR}/utilities/alerts.bash" + +if test -f "${SOURCEFILE}" >&2; then + source "${SOURCEFILE}" +else + echo "Sourcefile not found: ${SOURCEFILE}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +if test -f "${ALERTS}" >&2; then + source "${ALERTS}" + _setColors_ #Set color constants +else + echo "Sourcefile not found: ${ALERTS}" >&2 + printf "Can not run tests.\n" >&2 + exit 1 +fi + +setup() { + + TESTDIR="$(temp_make)" + curPath="${PWD}" + + BATSLIB_FILE_PATH_REM="#${TEST_TEMP_DIR}" + BATSLIB_FILE_PATH_ADD='' + + pushd "${TESTDIR}" >&2 + + ######## DEFAULT FLAGS ######## + LOGFILE="${TESTDIR}/logs/log.txt" + QUIET=false + LOGLEVEL=ERROR + VERBOSE=false + FORCE=false + DRYRUN=false + + set -o errtrace + set -o nounset + set -o pipefail +} + +teardown() { + set +o nounset + set +o errtrace + set +o pipefail + + popd >&2 + temp_del "${TESTDIR}" +} + +######## RUN TESTS ######## +@test "Sanity..." { + run true + + assert_success + assert_output "" +} + +@test "_setPATH_: fail on dir not found" { + mkdir -p "${TESTDIR}/testing/from/bats" + mkdir -p "${TESTDIR}/testing/from/bats_again" + run _setPATH_ "${TESTDIR}/testing/from/bats" "${TESTDIR}/testing/again" "${TESTDIR}/testing/from/bats_again" + assert_failure +} + +@test "_setPATH_: success" { + mkdir -p "${TESTDIR}/testing/from/bats" + mkdir -p "${TESTDIR}/testing/from/bats_again" + _setPATH_ "${TESTDIR}/testing/from/bats" "${TESTDIR}/testing/from/bats_again" + + run echo "${PATH}" + assert_output --regexp "/testing/from/bats" + refute_output --regexp "/testing/again" + assert_output --regexp "/testing/from/bats_again" +} + +@test "_makeTempDir_" { + VERBOSE=true + run _makeTempDir_ + assert_success + assert_output --regexp "\\\$TMP_DIR=/.*\.[0-9]+\.[0-9]+\.[0-9]+$" +} diff --git a/utilities/alerts.bash b/utilities/alerts.bash index b1ddf01..bbc94b2 100644 --- a/utilities/alerts.bash +++ b/utilities/alerts.bash @@ -1,16 +1,22 @@ -_setColors_() { - # DESC: Sets colors use for alerts. - # ARGS: None - # OUTS: None - # USAGE: echo "${blue}Some text${reset}" +# Functions for providing alerts to the user and logging them - if tput setaf 1 &>/dev/null; then +_setColors_() { + # DESC: + # Sets colors use for alerts. + # ARGS: + # None + # OUTS: + # None + # USAGE: + # echo "${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 ]] 2>/dev/null; then + if [[ $(tput colors) -ge 256 ]] >/dev/null 2>&1; then white=$(tput setaf 231) blue=$(tput setaf 38) yellow=$(tput setaf 11) @@ -46,68 +52,75 @@ _setColors_() { } _alert_() { - # DESC: Controls all printing of messages to log files and stdout. - # ARGS: $1 (required) - The type of alert to print + # 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: None - # 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 + # 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 function_name color - local alertType="${1}" - local message="${2}" - local line="${3:-}" # Optional line number + local _color + local _alertType="${1}" + local _message="${2}" + local _line="${3:-}" # Optional line number - if [[ -n ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} (line: ${line}) $(_functionStack_)" - elif [[ -n ${line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} (line: ${line})" - elif [[ -z ${line} && ${alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then - message="${message} $(_functionStack_)" + [[ $# -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}${tan}" - elif [ ${alertType} == "notice" ]; then - color="${bold}" - elif [ ${alertType} == "input" ]; then - color="${bold}${underline}" - elif [ "${alertType}" = "dryrun" ]; then - color="${blue}" + 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="" + _color="" fi _writeToScreen_() { - ("${QUIET}") && return 0 # Print to console when script is not 'quiet' - [[ ${VERBOSE} == false && ${alertType} =~ ^(debug|verbose) ]] && return 0 + [[ ${VERBOSE} == false && ${_alertType} =~ ^(debug|verbose) ]] && return 0 - if ! [[ -t 1 ]]; then # Don't use colors on non-recognized terminals - color="" + if ! [[ -t 1 || -z ${TERM:-} ]]; then # Don't use colors on non-recognized terminals + _color="" reset="" fi - echo -e "$(date +"%r") ${color}$(printf "[%7s]" "${alertType}") ${message}${reset}" + printf "%s ${_color}[%7s] %s${reset}\n" "$(date +"%r")" "${_alertType}" "${_message}" } _writeToScreen_ _writeToLog_() { - [[ ${alertType} == "input" ]] && return 0 + [[ ${_alertType} == "input" ]] && return 0 [[ ${LOGLEVEL} =~ (off|OFF|Off) ]] && return 0 if [ -z "${LOGFILE:-}" ]; then LOGFILE="$(pwd)/$(basename "$0").log" @@ -116,12 +129,9 @@ _alert_() { [[ ! -f ${LOGFILE} ]] && touch "${LOGFILE}" # Don't use colors in logs - if command -v gsed &>/dev/null; then - local cleanmessage="$(echo "${message}" | gsed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" - else - local cleanmessage="$(echo "${message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" - fi - echo -e "$(date +"%b %d %R:%S") $(printf "[%7s]" "${alertType}") [$(/bin/hostname)] ${cleanmessage}" >>"${LOGFILE}" + local cleanmessage="$(echo "${_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 @@ -133,27 +143,27 @@ _alert_() { _writeToLog_ ;; INFO | info | Info) - if [[ ${alertType} =~ ^(die|error|fatal|warning|info|notice|success) ]]; then + if [[ ${_alertType} =~ ^(error|fatal|warning|info|notice|success) ]]; then _writeToLog_ fi ;; NOTICE | notice | Notice) - if [[ ${alertType} =~ ^(die|error|fatal|warning|notice|success) ]]; then + if [[ ${_alertType} =~ ^(error|fatal|warning|notice|success) ]]; then _writeToLog_ fi ;; WARN | warn | Warn) - if [[ ${alertType} =~ ^(die|error|fatal|warning) ]]; then + if [[ ${_alertType} =~ ^(error|fatal|warning) ]]; then _writeToLog_ fi ;; ERROR | error | Error) - if [[ ${alertType} =~ ^(die|error|fatal) ]]; then + if [[ ${_alertType} =~ ^(error|fatal) ]]; then _writeToLog_ fi ;; FATAL | fatal | Fatal) - if [[ ${alertType} =~ ^(die|fatal) ]]; then + if [[ ${_alertType} =~ ^fatal ]]; then _writeToLog_ fi ;; @@ -161,7 +171,7 @@ _alert_() { return 0 ;; *) - if [[ ${alertType} =~ ^(die|error|fatal) ]]; then + if [[ ${_alertType} =~ ^(error|fatal) ]]; then _writeToLog_ fi ;; @@ -176,30 +186,117 @@ 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:-}"; } +header() { _alert_ header "${1}" "${2:-}"; } debug() { _alert_ debug "${1}" "${2:-}"; } -die() { - _alert_ fatal "${1}" "${2:-}" - _safeExit_ "1" -} fatal() { _alert_ fatal "${1}" "${2:-}" _safeExit_ "1" } -_functionStack_() { - # DESC: Prints the function stack in use - # ARGS: None - # OUTS: Prints [function]:[file]:[line] - # NOTE: Does not print functions from the alert class +_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 - funcStackResponse=() + _funcStackResponse=() for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do - case "${FUNCNAME[$_i]}" in "_alert_" | "_trapCleanup_" | fatal | error | warning | notice | info | verbose | debug | dryrun | header | success | die) continue ;; esac - funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}") + case "${FUNCNAME[$_i]}" in "_alert_" | "_trapCleanup_" | fatal | error | warning | notice | info | debug | dryrun | header | success) continue ;; esac + _funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}") done printf "( " - printf %s "${funcStackResponse[0]}" - printf ' < %s' "${funcStackResponse[@]:1}" + printf %s "${_funcStackResponse[0]}" + printf ' < %s' "${_funcStackResponse[@]:1}" printf ' )\n' } + +_centerOutput_() { + # DESC: + # Prints text centered in the terminal window with an optional fill character + # ARGS: + # $1 (required): Text to center + # $2 (optional): Fill character + # OUTS: + # 0 - Success + # 1 - Failure + # stdout: + # USAGE: + # _centerOutput_ "Text to print in the center" "-" + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _input="${1}" + local _symbol="${2:- }" + local _filler + local _out + local _no_ansi_out + local i + + _no_ansi_out=$(_stripANSI_ "${_input}") + declare -i _str_len=${#_no_ansi_out} + declare -i _filler_len="$(((COLUMNS - _str_len) / 2))" + + [[ -n ${_symbol} ]] && _symbol="${_symbol:0:1}" + for ((i = 0; i < _filler_len; i++)); do + _filler+="${_symbol}" + done + + _out="${_filler}${_input}${_filler}" + [[ $(((COLUMNS - _str_len) % 2)) -ne 0 ]] && _out+="${_symbol}" + printf "%s\n" "${_out}" +} + +_columnizeOutput_() { + # DESC: + # Creates a column output for key/value pairs with line wrapping for the right column (value). Attempts to wrap at a sane line length (~100 cols) on larger screens. + # ARGS: + # $1 (required): Left padding of table + # $2 (required): Width of first column + # $3 (required): Key name (left column text) + # $4 (required): Long value (right column text. Wraps around if too long) + # OUTS: + # stdout: Prints the columnized output + # NOTE: + # Long text or ANSI colors in the first column may create display issues + # USAGE: + # _columnizeOutput_ 0 30 10 "Key" "Long value text" + + [[ $# -lt 5 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _leftIndent=$1 + local _leftColumn=$2 + local _key="$3" + local _value="$4" + local _line + local _rightIndent + + if [ "$(tput cols)" -gt 180 ]; then + _rightIndent=80 + elif [ "$(tput cols)" -gt 160 ]; then + _rightIndent=60 + elif [ "$(tput cols)" -gt 130 ]; then + _rightIndent=30 + elif [ "$(tput cols)" -gt 120 ]; then + _rightIndent=20 + elif [ "$(tput cols)" -gt 110 ]; then + _rightIndent=10 + else + _rightIndent=0 + fi + local _rightWrapLength=$(($(tput cols) - _leftColumn - _leftIndent - _rightIndent)) + + local _first_line=0 + while read -r _line; do + if [[ ${_first_line} -eq 0 ]]; then + _first_line=1 + else + _key=" " + fi + printf "%-${_leftIndent}s%-${_leftColumn}b %b\n" "" "${_key}" "${_line}" + done <<<"$(fold -w${_rightWrapLength} -s <<<"${_value}")" +} diff --git a/utilities/arrays.bash b/utilities/arrays.bash index 56ec2f5..53ea523 100644 --- a/utilities/arrays.bash +++ b/utilities/arrays.bash @@ -1,98 +1,450 @@ +# Functions for manipulating arrays + +_dedupeArray_() { + # DESC: + # Removes duplicate array elements + # ARGS: + # $1 (Required) - Input array + # OUTS: + # stdout: Prints de-duped elements + # USAGE: + # _removeDups_ "${array[@]}" + # NOTE: + # List order may not stay the same + # CREDIT: + # https://github.com/dylanaraps/pure-bash-bible + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + declare -A _tmpArray + declare -a _uniqueArray + local _i + for _i in "$@"; do + { [[ -z ${_i} || ${_tmpArray[${_i}]:-} ]]; } && continue + _uniqueArray+=("${_i}") && _tmpArray[${_i}]=x + done + printf '%s\n' "${_uniqueArray[@]}" +} + +_forEachDo_() { + # DESC: + # Iterates over elements and passes each to a function + # ARGS: + # $1 (Required) - Function name to pass each item to + # OUTS: + # 0 - Success + # Return code of called function + # stdout: Output of called function + # USAGE: + # printf "%s\n" "${arr1[@]}" | _forEachDo_ "test_func" + # _forEachDo_ "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _func="${1}" + local IFS=$'\n' + local _it + + while read -r _it; do + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + [ ! "$(declare -f "${_func}")" ] && fatal "${FUNCNAME[0]} could not find function ${_func}" + eval "${_func}" "'${_it}'" + fi + declare -i _ret="$?" + + if [[ ${_ret} -ne 0 ]]; then + return ${_ret} + fi + done +} + +_forEachValidate_() { + # DESC: + # Iterates over elements and passes each to a function for validation. Iteration stops when the function returns 1. + # ARGS: + # $1 (Required) - Function name to pass each item to for validation. (Must return 0 on success) + # OUTS: + # 0 - Success + # 1 - Iteratee function fails + # USAGE: + # printf "%s\n" "${array[@]}" | _forEachValidate_ "_isAlpha_" + # _forEachValidate_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _func="${1}" + local IFS=$'\n' + local _it + + while read -r _it; do + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + [ ! "$(declare -f "${_func}")" ] && fatal "${FUNCNAME[0]} could not find function ${_func}" + eval "${_func}" "'${_it}'" + fi + declare -i _ret="$?" + + if [[ ${_ret} -ne 0 ]]; then + return 1 + fi + done +} + +_forEachFind_() { + # DESC: + # Iterates over elements, returning the first value that is validated by a function + # ARGS: + # $1 (Required) - Function name to pass each item to for validation. (Must return 0 on success) + # OUTS: + # 0 - If successful + # 1 - If iteratee function fails + # stdout: First value that is validated by the function + # USAGE: + # printf "%s\n" "${array[@]}" | _forEachFind_ "_isAlpha_" + # _forEachFind_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + declare _func="${1}" + declare IFS=$'\n' + while read -r _it; do + + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + eval "${_func}" "'${_it}'" + fi + declare -i _ret="$?" + if [[ ${_ret} == 0 ]]; then + printf "%s" "${_it}" + return 0 + fi + done + + return 1 +} + +_forEachFilter_() { + # DESC: + # Iterates over elements, returning only those that are validated by a function + # ARGS: + # $1 (Required) - Function name to pass each item to for validation. (Must return 0 on success) + # OUTS: + # 0 - Success + # 1 - Failure + # stdout: Values matching the validation function + # USAGE: + # printf "%s\n" "${array[@]}" | _forEachFind_ "_isAlpha_" + # _forEachFilter_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _func="${1}" + local IFS=$'\n' + while read -r _it; do + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + eval "${_func}" "'${_it}'" + fi + declare -i _ret="$?" + if [[ ${_ret} == 0 ]]; then + printf "%s\n" "${_it}" + fi + done +} + +_forEachReject_() { + # DESC: + # The opposite of _forEachFilter_. Iterates over elements, returning only those that are not validated by a function + # ARGS: + # $1 (Required) - Function name to pass each item to for validation. (Must return 0 on success, 1 on failure) + # OUTS: + # 0 - Success + # stdout: Values NOT matching the validation function + # USAGE: + # printf "%s\n" "${array[@]}" | _forEachReject_ "_isAlpha_" + # _forEachReject_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _func="${1}" + local IFS=$'\n' + while read -r _it; do + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + eval "${_func}" "'${_it}'" + fi + declare -i _ret=$? + if [[ ${_ret} -ne 0 ]]; then + echo "${_it}" + fi + done +} + +_forEachSome_() { + # DESC: + # Iterates over elements, returning true if any of the elements validate as true from the function. + # ARGS: + # $1 (Required) - Function name to pass each item to for validation. (Must return 0 on success, 1 on failure) + # OUTS: + # 0 If match successful + # 1 If no match found + # USAGE: + # printf "%s\n" "${array[@]}" | _forEachSome_ "_isAlpha_" + # _forEachSome_ "_isAlpha_" < <(printf "%s\n" "${array[@]}") + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _func="${1}" + local _IFS=$'\n' + while read -r _it; do + + if [[ ${_func} == *"$"* ]]; then + eval "${_func}" + else + eval "${_func}" "'${_it}'" + fi + + declare -i _ret=$? + if [[ ${_ret} -eq 0 ]]; then + return 0 + fi + done + + return 1 +} + _inArray_() { - # DESC: Determine if a value is in an array - # ARGS: $1 (Required) - Value to search for + # DESC: + # Determine if a value is in an array. Default is case sensitive. + # Pass -i flag to ignore case. + # ARGS: + # $1 (Required) - Value to search for # $2 (Required) - Array written as ${ARRAY[@]} - # OUTS: true/false - # USAGE: if _inArray_ "VALUE" "${ARRAY[@]}"; then ... + # OUTS: + # 0 if true + # 1 if untrue + # USAGE: + # if _inArray_ "VALUE" "${ARRAY[@]}"; then ... + # if _inArray_ -i "VALUE" "${ARRAY[@]}"; then ... + # CREDIT: + # https://github.com/labbots/bash-utility - [[ $# -lt 2 ]] && fatal 'Missing required argument to _inArray_()!' + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - local value="$1" + local opt + local _case_insensitive=false + local OPTIND=1 + while getopts ":iI" opt; do + case ${opt} in + i | I) _case_insensitive=true ;; + *) fatal "Unrecognized option '${1}' passed to ${FUNCNAME[0]}. Exiting." ;; + esac + done + shift $((OPTIND - 1)) + + local _array_item + local _value="${1}" shift - for arrayItem in "$@"; do - [[ ${arrayItem} == "${value}" ]] && return 0 + for _array_item in "$@"; do + if [ ${_case_insensitive} = true ]; then + _value="$(echo "${_value}" | tr '[:upper:]' '[:lower:]')" + _array_item="$(echo "${_array_item}" | tr '[:upper:]' '[:lower:]')" + fi + [[ ${_array_item} == "${_value}" ]] && return 0 done return 1 } -_join_() { - # DESC: Joins items together with a user specified separator - # ARGS: $1 (Required) - Separator - # $@ (Required) - Items to be joined - # OUTS: Prints joined terms +_isEmptyArray_() { + # DESC: + # Checks if an array is empty + # ARGS: + # $1 (Required) - Input array + # OUTS: + # 0 if empty + # 1 if not empty # USAGE: - # _join_ , a "b c" d #a,b c,d - # _join_ / var local tmp #var/local/tmp - # _join_ , "${foo[@]}" #a,b,c - # NOTE: http://stackoverflow.com/questions/1527049/bash-join-elements-of-an-array + # _isEmptyArray_ "${array[@]}" + # CREDIT: + # https://github.com/labbots/bash-utility - [[ $# -lt 2 ]] && fatal 'Missing required argument to _join_()!' + declare -a _array=("$@") + if [ ${#_array[@]} -eq 0 ]; then + return 0 + else + return 1 + fi +} - local IFS="${1}" +_joinArray_() { + # DESC: + # Joins items together with a user specified separator + # ARGS: + # $1 (Required) - Separator + # $@ (Required) - Array or space separated items to be joined + # OUTS: + # stdout: Prints joined terms + # USAGE: + # _join_ , a "b c" d #a,b c,d + # _join_ / var local tmp #var/local/tmp + # _join_ , "${foo[@]}" #a,b,c + # CREDIT: + # http://stackoverflow.com/questions/1527049/bash-join-elements-of-an-array + # https://github.com/labbots/bash-utility + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _delimiter="${1}" shift - echo "${*}" + printf "%s" "${1}" + shift + printf "%s" "${@/#/${_delimiter}}" } -_setdiff_() { - # DESC: Return items that exist in ARRAY1 that are do not exist in ARRAY2 - # ARGS: $1 (Required) - Array 1 in format ${ARRAY[*]} - # $2 (Required) - Array 2 in format ${ARRAY[*]} - # OUTS: Prints unique terms - # USAGE: _setdiff_ "${array1[*]}" "${array2[*]}" - # NOTE: http://stackoverflow.com/a/1617303/142339 +_mergeArrays_() { + # DESC: + # Merges two arrays together + # ARGS: + # $1 (Required) - Array 1 + # $2 (Required) - Array 2 + # OUTS: + # stdout: Prints result + # USAGE: + # newarray=($(_mergeArrays_ "array1[@]" "array2[@]")) + # NOTE: + # Note that the arrays must be passed in as strings + # CREDIT: + # https://github.com/labbots/bash-utility - [[ $# -lt 2 ]] && fatal 'Missing required argument to _setdiff_()!' - - local debug skip a b - if [[ $1 == 1 ]]; then - debug=1 - shift - fi - if [[ "$1" ]]; then - local setdiffA setdiffB setdiffC - # shellcheck disable=SC2206 - setdiffA=($1) - # shellcheck disable=SC2206 - setdiffB=($2) - fi - setdiffC=() - for a in "${setdiffA[@]}"; do - skip= - for b in "${setdiffB[@]}"; do - [[ $a == "$b" ]] && skip=1 && break - done - [[ "$skip" ]] || setdiffC=("${setdiffC[@]}" "$a") - done - [[ "$debug" ]] && for a in setdiffA setdiffB setdiffC; do - #shellcheck disable=SC1087 - echo "$a ($(eval echo "\${#$a[*]}")) $(eval echo "\${$a[*]}")" 1>&2 - done - [[ "$1" ]] && echo "${setdiffC[@]}" + [[ $# -ne 2 ]] && fatal 'Missing required argument to _mergeArrays_' + declare -a _arr1=("${!1}") + declare -a _arr2=("${!2}") + declare _outputArray=("${_arr1[@]}" "${_arr2[@]}") + printf "%s\n" "${_outputArray[@]}" } -_removeDupes_() { - # DESC: Removes duplicate array elements. - # ARGS: $1 (Required) - Input array - # OUTS: Prints de-duped elements to standard out - # USAGE: _removeDups_ "${array[@]}" - # NOTE: List order may not stay the same. - # https://github.com/dylanaraps/pure-bash-bible - declare -A tmp_array +_reverseSortArray_() { + # DESC: + # Sorts an array from lowest to highest (z-a9-0) + # ARGS: + # $1 (Required) - Input array + # OUTS: + # stdout: Prints result + # USAGE: + # _reverseSortArray_ "${array[@]}" + # NOTE: + # input=("c" "b" "4" "1" "2" "3" "a") + # _reverseSortArray_ "${input[@]}" + # c b a 4 3 2 1 + # CREDIT: + # https://github.com/labbots/bash-utility - for i in "$@"; do - [[ $i ]] && IFS=" " tmp_array["${i:- }"]=1 - done - - printf '%s\n' "${!tmp_array[@]}" + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + declare -a _array=("$@") + declare -a _sortedArray + mapfile -t _sortedArray < <(printf '%s\n' "${_array[@]}" | sort -r) + printf "%s\n" "${_sortedArray[@]}" } _randomArrayElement_() { - # DESC: Selects a random item from an array - # ARGS: $1 (Required) - Input array - # OUTS: Prints result - # USAGE: _randomArrayElement_ "${array[@]}" - # NOTE: https://github.com/dylanaraps/pure-bash-bible - # Usage: random_array_element "array" - local arr=("$@") - printf '%s\n' "${arr[RANDOM % $#]}" + # DESC: + # Selects a random item from an array + # ARGS: + # $1 (Required) - Input array + # OUTS: + # stdout: Prints one random element + # USAGE: + # _randomArrayElement_ "${array[@]}" + # CREDIT: + # https://github.com/dylanaraps/pure-bash-bible + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + declare -a _array + local _array=("$@") + printf '%s\n' "${_array[RANDOM % $#]}" +} + +_setDiff_() { + # DESC: + # Return items that exist in ARRAY1 that are do not exist in ARRAY2 + # ARGS: + # $1 (Required) - Array 1 (in format ARRAY[@]) + # $2 (Required) - Array 2 (in format ARRAY[@]) + # OUTS: + # 0 if unique elements found + # 1 if arrays are the same + # stdout: Prints unique elements + # USAGE: + # _setDiff_ "array1[@]" "array2[@]" + # mapfile -t NEW_ARRAY < <(_setDiff_ "array1[@]" "array2[@]") + # NOTE: + # Note that the arrays must be passed in as strings + # CREDIT: + # http://stackoverflow.com/a/1617303/142339 + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _skip + local _a + local _b + declare -a _setdiffA=("${!1}") + declare -a _setdiffB=("${!2}") + declare -a _setdiffC=() + + for _a in "${_setdiffA[@]}"; do + _skip= + for _b in "${_setdiffB[@]}"; do + if [[ ${_a} == "${_b}" ]]; then + _skip=1 + break + fi + done + [[ "${_skip}" ]] || _setdiffC=("${_setdiffC[@]}" "${_a}") + done + + if [[ ${#_setdiffC[@]} == 0 ]]; then + return 1 + else + printf "%s\n" "${_setdiffC[@]}" + fi +} + +_sortArray_() { + # DESC: + # Sorts an array from lowest to highest (0-9 a-z) + # ARGS: + # $1 (Required) - Input array + # OUTS: + # stdout: Prints result + # USAGE: + # _sortArray_ "${array[@]}" + # NOTE: + # input=("c" "b" "4" "1" "2" "3" "a") + # _sortArray_ "${input[@]}" + # 1 2 3 4 a b c + # CREDIT: + # https://github.com/labbots/bash-utility + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + declare -a _array=("$@") + declare -a _sortedArray + mapfile -t _sortedArray < <(printf '%s\n' "${_array[@]}" | sort) + printf "%s\n" "${_sortedArray[@]}" } diff --git a/utilities/baseHelpers.bash b/utilities/baseHelpers.bash deleted file mode 100644 index becef35..0000000 --- a/utilities/baseHelpers.bash +++ /dev/null @@ -1,373 +0,0 @@ -_execute_() { - # DESC: Executes commands with safety and logging options - # ARGS: $1 (Required) - The command to be executed. Quotation marks MUST be escaped. - # $2 (Optional) - String to display after command is executed - # OPTS: -v Always print debug output from the execute function - # -n Use NOTICE level alerting (default is INFO) - # -p Pass a failed command with 'return 0'. This effectively bypasses set -e. - # -e Bypass _alert_ functions and use 'echo RESULT' - # -s Use '_alert_ success' for successful output. (default is 'info') - # -q Do not print output (QUIET mode) - # OUTS: None - # USE : _execute_ "cp -R \"~/dir/somefile.txt\" \"someNewFile.txt\"" "Optional message" - # _execute_ -sv "mkdir \"some/dir\"" - # NOTE: - # If $DRYRUN=true no commands are executed - # If $VERBOSE=true the command's native output is printed to - # stderr and stdin. This can be forced with `_execute_ -v` - - local LOCAL_VERBOSE=false - local PASS_FAILURES=false - local ECHO_RESULT=false - local SUCCESS_RESULT=false - local QUIET_RESULT=false - local NOTICE_RESULT=false - local opt - - local OPTIND=1 - while getopts ":vVpPeEsSqQnN" opt; do - case $opt in - v | V) LOCAL_VERBOSE=true ;; - p | P) PASS_FAILURES=true ;; - e | E) ECHO_RESULT=true ;; - s | S) SUCCESS_RESULT=true ;; - q | Q) QUIET_RESULT=true ;; - n | N) NOTICE_RESULT=true ;; - *) - { - error "Unrecognized option '$1' passed to _execute_. Exiting." - _safeExit_ - } - ;; - esac - done - shift $((OPTIND - 1)) - - local CMD="${1:?_execute_ needs a command}" - local EXECUTE_MESSAGE="${2:-$1}" - - local SAVE_VERBOSE=${VERBOSE} - if "${LOCAL_VERBOSE}"; then - VERBOSE=true - fi - - if "${DRYRUN}"; then - if "${QUIET_RESULT}"; then - VERBOSE=$SAVE_VERBOSE - return 0 - fi - if [ -n "${2:-}" ]; then - dryrun "${1} (${2})" "$(caller)" - else - dryrun "${1}" "$(caller)" - fi - elif ${VERBOSE}; then - if eval "${CMD}"; then - if "${QUIET_RESULT}"; then - VERBOSE=${SAVE_VERBOSE} - return 0 - elif "${ECHO_RESULT}"; then - echo "${EXECUTE_MESSAGE}" - elif "${SUCCESS_RESULT}"; then - success "${EXECUTE_MESSAGE}" - elif "${NOTICE_RESULT}"; then - notice "${EXECUTE_MESSAGE}" - else - info "${EXECUTE_MESSAGE}" - fi - VERBOSE=${SAVE_VERBOSE} - return 0 - else - if "${ECHO_RESULT}"; then - echo "warning: ${EXECUTE_MESSAGE}" - else - warning "${EXECUTE_MESSAGE}" - fi - VERBOSE=${SAVE_VERBOSE} - "${PASS_FAILURES}" && return 0 || return 1 - fi - else - if eval "${CMD}" &>/dev/null; then - if "${QUIET_RESULT}"; then - VERBOSE=${SAVE_VERBOSE} - return 0 - elif "${ECHO_RESULT}"; then - echo "${EXECUTE_MESSAGE}" - elif "${SUCCESS_RESULT}"; then - success "${EXECUTE_MESSAGE}" - elif "${NOTICE_RESULT}"; then - notice "${EXECUTE_MESSAGE}" - else - info "${EXECUTE_MESSAGE}" - fi - VERBOSE=${SAVE_VERBOSE} - return 0 - else - if "${ECHO_RESULT}"; then - echo "error: ${EXECUTE_MESSAGE}" - else - warning "${EXECUTE_MESSAGE}" - fi - VERBOSE=${SAVE_VERBOSE} - "${PASS_FAILURES}" && return 0 || return 1 - fi - fi -} - -_findBaseDir_() { - # DESC: Locates the real directory of the script being run. Similar to GNU readlink -n - # ARGS: None - # OUTS: Echo result to STDOUT - # USE : baseDir="$(_findBaseDir_)" - # cp "$(_findBaseDir_ "somefile.txt")" "other_file.txt" - - local SOURCE - local DIR - - # Is file sourced? - [[ $_ != "$0" ]] \ - && SOURCE="${BASH_SOURCE[1]}" \ - || SOURCE="${BASH_SOURCE[0]}" - - while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink - DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" - SOURCE="$(readlink "$SOURCE")" - [[ $SOURCE != /* ]] && SOURCE="${DIR}/${SOURCE}" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located - done - echo "$(cd -P "$(dirname "${SOURCE}")" && pwd)" -} - -_checkBinary_() { - # DESC: Check if a binary exists in the search PATH - # ARGS: $1 (Required) - Name of the binary to check for existence - # OUTS: true/false - # USAGE: (_checkBinary_ ffmpeg ) && [SUCCESS] || [FAILURE] - if [[ $# -lt 1 ]]; then - error 'Missing required argument to _checkBinary_()!' - return 1 - fi - - if ! command -v "$1" >/dev/null 2>&1; then - debug "Did not find dependency: '$1'" - return 1 - fi - return 0 -} - -_haveFunction_() { - # DESC: Tests if a function exists. - # ARGS: $1 (Required) - Function name - # OUTS: true/false - local f - f="$1" - - if declare -f "${f}" &>/dev/null 2>&1; then - return 0 - else - return 1 - fi -} - -_pauseScript_() { - # DESC: Pause a script at any point and continue after user input - # ARGS: $1 (Optional) - String for customized message - # OUTS: None - - local pauseMessage - pauseMessage="${1:-Paused}. Ready to continue?" - - if _seekConfirmation_ "${pauseMessage}"; then - info "Continuing..." - else - notice "Exiting Script" - _safeExit_ - fi -} - -_progressBar_() { - # DESC: Prints a progress bar within a for/while loop - # ARGS: $1 (Required) - The total number of items counted - # $2 (Optional) - The optional title of the progress bar - # OUTS: None - # USAGE: - # for number in $(seq 0 100); do - # sleep 1 - # _progressBar_ "100" "Counting numbers" - # done - - ($QUIET) && return - ($VERBOSE) && return - [ ! -t 1 ] && return # Do nothing if the output is not a terminal - [ $1 == 1 ] && return # Do nothing with a single element - - local width bar_char perc num bar progressBarLine barTitle n - - n="${1:?_progressBar_ needs input}" - ((n = n - 1)) - barTitle="${2:-Running Process}" - width=30 - bar_char="#" - - # Reset the count - [ -z "${progressBarProgress}" ] && progressBarProgress=0 - tput civis # Hide the cursor - trap 'tput cnorm; exit 1' SIGINT - - if [ ! "${progressBarProgress}" -eq $n ]; then - #echo "progressBarProgress: $progressBarProgress" - # Compute the percentage. - perc=$((progressBarProgress * 100 / $1)) - # Compute the number of blocks to represent the percentage. - num=$((progressBarProgress * width / $1)) - # Create the progress bar string. - bar="" - if [ ${num} -gt 0 ]; then - bar=$(printf "%0.s${bar_char}" $(seq 1 ${num})) - fi - # Print the progress bar. - progressBarLine=$(printf "%s [%-${width}s] (%d%%)" " ${barTitle}" "${bar}" "${perc}") - echo -ne "${progressBarLine}\r" - progressBarProgress=$((progressBarProgress + 1)) - else - # Clear the progress bar when complete - # echo -ne "\033[0K\r" - tput el # Clear the line - - unset progressBarProgress - fi - - tput cnorm -} - -_rootAvailable_() { - # DESC: Validate we have superuser access as root (via sudo if requested) - # ARGS: $1 (optional): Set to any value to not attempt root access via sudo - # OUTS: None - # NOTE: https://github.com/ralish/bash-script-template - - local superuser - if [[ ${EUID} -eq 0 ]]; then - superuser=true - elif [[ -z ${1:-} ]]; then - if command -v sudo &>/dev/null; then - debug 'Sudo: Updating cached credentials ...' - if ! sudo -v; then - warning "Sudo: Couldn't acquire credentials ..." - else - local test_euid - test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" - if [[ ${test_euid} -eq 0 ]]; then - superuser=true - fi - fi - fi - fi - - if [[ -z ${superuser:-} ]]; then - debug 'Unable to acquire superuser credentials.' - return 1 - fi - - debug 'Successfully acquired superuser credentials.' - return 0 -} - -_runAsRoot_() { - # DESC: Run the requested command as root (via sudo if requested) - # ARGS: $1 (optional): Set to zero to not attempt execution via sudo - # $@ (required): Passed through for execution as root user - # OUTS: None - # NOTE: https://github.com/ralish/bash-script-template - - if [[ $# -eq 0 ]]; then - fatal 'Missing required argument to _runAsRoot_()!' - fi - - if [[ ${1:-} =~ ^0$ ]]; then - local skip_sudo=true - shift - fi - - if [[ ${EUID} -eq 0 ]]; then - "$@" - elif [[ -z ${skip_sudo:-} ]]; then - sudo -H -- "$@" - else - fatal "Unable to run requested command as root: $*" - fi -} - -_seekConfirmation_() { - # DESC: Seek user input for yes/no question - # ARGS: $1 (Optional) - Question being asked - # OUTS: true/false - # USAGE: _seekConfirmation_ "Do something?" && echo "okay" || echo "not okay" - # OR - # if _seekConfirmation_ "Answer this question"; then - # something - # fi - - local yn - input "${1:-}" - if "${FORCE}"; then - debug "Forcing confirmation with '--force' flag set" - echo -e "" - return 0 - else - while true; do - read -r -p " (y/n) " yn - case $yn in - [Yy]*) return 0 ;; - [Nn]*) return 1 ;; - *) input "Please answer yes or no." ;; - esac - done - fi -} - -_setPATH_() { - # DESC: Add directories to $PATH so script can find executables - # ARGS: $@ - One or more paths - # OUTS: $PATH - # USAGE: _setPATH_ "/usr/local/bin" "${HOME}/bin" "$(npm bin)" - local NEWPATH NEWPATHS USERPATH - - for USERPATH in "$@"; do - NEWPATHS+=("$USERPATH") - done - - for NEWPATH in "${NEWPATHS[@]}"; do - if [ -d "${NEWPATH}" ]; then - if ! echo "$PATH" | grep -Eq "(^|:)${NEWPATH}($|:)"; then - PATH="${NEWPATH}:${PATH}" - debug "Added '${NEWPATH}' to PATH" - fi - fi - done -} - -_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 ${tan}'${LOCK_DIR}'${red}" - 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} -} diff --git a/utilities/checks.bash b/utilities/checks.bash new file mode 100644 index 0000000..62fbe55 --- /dev/null +++ b/utilities/checks.bash @@ -0,0 +1,352 @@ +# Functions for validating common use-cases + +_binaryExists_() { + # DESC: + # Check if a binary exists in the search PATH + # ARGS: + # $1 (Required) - Name of the binary to check for existence + # OUTS: + # 0 if true + # 1 if false + # USAGE: + # (_binaryExists_ ffmpeg ) && [SUCCESS] || [FAILURE] + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + if ! command -v "$1" >/dev/null 2>&1; then + debug "Did not find dependency: '${1}'" + return 1 + fi + return 0 +} + +_functionExists_() { + # DESC: + # Tests if a function exists in the current scope + # ARGS: + # $1 (Required) - Function name + # OUTS: + # 0 if function exists + # 1 if function does not exist + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _testFunction + _testFunction="${1}" + + if declare -f "${_testFunction}" &>/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +_isAlpha_() { + # DESC: + # Validate that a given input is entirely alphabetic characters + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is only alphabetic characters + # 1 - Input contains non-alphabetic characters + # USAGE: + # _isAlpha_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _re='^[[:alpha:]]+$' + if [[ ${1} =~ ${_re} ]]; then + return 0 + fi + return 1 +} + +_isAlphaNum_() { + # DESC: + # Validate that a given input is entirely alpha-numeric characters + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is only alpha-numeric characters + # 1 - Input contains alpha-numeric characters + # USAGE: + # _isAlphaNum_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _re='^[[:alnum:]]+$' + if [[ ${1} =~ ${_re} ]]; then + return 0 + fi + return 1 +} + +_isAlphaDash_() { + # DESC: + # Validate that a given input contains only alpha-numeric characters, as well as dashes and underscores. + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is only alpha-numeric or dash or underscore characters + # 1 - Input is not only alpha-numeric or dash or underscore characters + # USAGE: + # _isAlphaDash_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _re='^[[:alnum:]_-]+$' + if [[ ${1} =~ ${_re} ]]; then + return 0 + fi + return 1 +} + +_isEmail_() { + # DESC: + # Validates that input is a valid email address + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Is valid email + # 1 - Is not valid email + # USAGE: + # _isEmail_ "somename+test@gmail.com" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _emailRegex + _emailRegex="^[a-z0-9!#\$%&'*+/=?^_\`{|}~-]+(\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]*[a-z0-9])?\$" + [[ ${1} =~ ${_emailRegex} ]] && return 0 || return 1 +} + +_isInternetAvailable_() { + # DESC: + # Check if internet connection is available + # ARGS: + # None + # OUTS: + # 0 - Success: Internet connection is available + # 1 - Failure: Internet connection is not available + # stdout: + # USAGE: + # _isInternetAvailable_ + # NOTES: + # + + local _checkInternet + if [[ -t 1 || -z ${TERM} ]]; then + _checkInternet="$(sh -ic 'exec 3>&1 2>/dev/null; { curl --compressed -Is google.com 1>&3; kill 0; } | { sleep 10; kill 0; }' || :)" + else + _checkInternet="$(curl --compressed -Is google.com -m 10)" + fi + if [[ -z ${_checkInternet:-} ]]; then + return 1 + fi +} + +_isIPv4_() { + # DESC: + # Validates that input is a valid IP version 4 address + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Is valid IPv4 address + # 1 - Is not valid IPv4 address + # USAGE: + # _isIPv4_ "192.168.1.1" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _ip="${1}" + local IFS=. + # shellcheck disable=SC2206 + declare -a _a=(${_ip}) + [[ ${_ip} =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1 + # Test values of quads + local _quad + for _quad in {0..3}; do + [[ ${_a[${_quad}]} -gt 255 ]] && return 1 + done + return 0 +} + +_isFile_() { + # DESC: + # Validate that a given input points to a valid file + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is a valid file + # 1 - Input is not a valid file + # USAGE: + # _varIsFile_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + [[ -f ${1} ]] && return 0 || return 1 +} + +_isDir_() { + # DESC: + # Validate that a given input points to a valid directory + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is a directory + # 1 - Input is not a directory + # USAGE: + # _varIsDir_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + [[ -d ${1} ]] && return 0 || return 1 +} + +_isNum_() { + # DESC: + # Validate that a given input is entirely numeric characters + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Input is only numeric characters + # 1 - Input contains numeric characters + # USAGE: + # _isNum_ "${var}" + # NOTES: + # + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _re='^[[:digit:]]+$' + if [[ ${1} =~ ${_re} ]]; then + return 0 + fi + return 1 +} + +_isTerminal_() { + # DESC: + # Check is script is run in an interactive terminal + # ARGS: + # None + # OUTS: + # 0 - Script is run in a terminal + # 1 - Script is not run in a terminal + # USAGE: + # _isTerminal_ + + [[ -t 1 || -z ${TERM} ]] && return 0 || return 1 +} + +_rootAvailable_() { + # DESC: + # Validate we have superuser access as root (via sudo if requested) + # ARGS: + # $1 (optional): Set to any value to not attempt root access via sudo + # OUTS: + # 0 if true + # 1 if false + # CREDIT: + # https://github.com/ralish/bash-script-template + + local _superuser + local _testEUID + if [[ ${EUID} -eq 0 ]]; then + _superuser=true + elif [[ -z ${1:-} ]]; then + if command -v sudo >/dev/null 2>&1; then + debug 'Sudo: Updating cached credentials ...' + if ! sudo -v; then + warning "Sudo: Couldn't acquire credentials ..." + else + _testEUID="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" + if [[ ${_testEUID} -eq 0 ]]; then + _superuser=true + fi + fi + fi + fi + + if [[ -z ${superuser:-} ]]; then + debug 'Unable to acquire superuser credentials.' + return 1 + fi + + debug 'Successfully acquired superuser credentials.' + return 0 +} + +_varIsTrue_() { + # DESC: + # Check if a given variable is true + # ARGS: + # $1 (required): Variable to check + # OUTS: + # 0 - Variable is true + # 1 - Variable is false + # USAGE + # _varIsTrue_ "${var}" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + [[ ${1} == true || ${1} -eq 0 ]] && return 0 || return 1 +} + +_varIsFalse_() { + # DESC: + # Check if a given variable is false + # ARGS: + # $1 (required): Variable to check + # OUTS: + # 0 - Variable is false + # 1 - Variable is true + # USAGE + # _varIsFalse_ "${var}" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + [[ ${1} == false || ${1} -eq 1 ]] && return 0 || return 1 +} + +_varIsEmpty_() { + # DESC: + # Check if given variable is empty or null. + # ARGS: + # $1 (required): Variable to check + # OUTS: + # 0 - Variable is empty or null + # 1 - Variable is not empty or null + # USAGE + # _varIsEmpty_ "${var}" + + [[ -z ${1} || ${1} == "null" ]] && return 0 || return 1 +} + +_isIPv6_() { + # DESC: + # Validates that input is a valid IP version 46address + # ARGS: + # $1 (required): Input to check + # OUTS: + # 0 - Is valid IPv6 address + # 1 - Is not valid IPv6 address + # USAGE: + # _isIPv6_ "2001:db8:85a3:8d3:1319:8a2e:370:7348" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _ip="${1}" + local _re="^(([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}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|\ +(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|\ +(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" + + [[ ${_ip} =~ ${_re} ]] && return 0 || return 1 +} diff --git a/utilities/csv.bash b/utilities/csv.bash deleted file mode 100644 index 06750f8..0000000 --- a/utilities/csv.bash +++ /dev/null @@ -1,34 +0,0 @@ -_makeCSV_() { - # Creates a new CSV file if one does not already exist - # Takes passed arguments and writes them as a header line to the CSV - # Usage '_makeCSV_ column1 column2 column3' - - # Set the location and name of the CSV File - if [ -z "${csvLocation}" ]; then - csvLocation="${HOME}/Desktop" - fi - if [ -z "${csvName}" ]; then - csvName="$(LC_ALL=C date +%Y-%m-%d)-${FUNCNAME[1]}.csv" - fi - csvFile="${csvLocation}/${csvName}" - - # Overwrite existing file? If not overwritten, new content is added - # to the bottom of the existing file - if [ -f "${csvFile}" ]; then - if _seekConfirmation_ "${csvFile} already exists. Overwrite?"; then - rm "${csvFile}" - fi - fi - _writeCSV_ "$@" -} - -_writeCSV_() { - # Takes passed arguments and writes them as a comma separated line - # Usage '_writeCSV_ column1 column2 column3' - - local csvInput=("$@") - saveIFS=$IFS - IFS=',' - echo "${csvInput[*]}" >>"${csvFile}" - IFS=${saveIFS} -} diff --git a/utilities/dates.bash b/utilities/dates.bash index 994cace..23a442f 100644 --- a/utilities/dates.bash +++ b/utilities/dates.bash @@ -1,73 +1,216 @@ +# Functions to help work with dates and time + +_convertToUnixTimestamp_() { + # DESC: + # Convert date string to unix timestamp + # ARGS: + # $1 (Required) - Date to be converted + # OUTS: + # 0 If successful + # 1 If failed to convert + # stdout: timestamp for specified date/time + # USAGE: + # printf "%s\n" "$(_convertToUnixTimestamp_ "Jan 10, 2019")" + # NOTES: + # + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _date + _date=$(date -d "${1}" +"%s") || return 1 + printf "%s\n" "${_date}" + +} + +_countdown_() { + # DESC: + # Sleep for a specified amount of time + # ARGS: + # $1 (Optional) - Total seconds to sleep for(Default is 10) + # $2 (Optional) - Increment to count down + # $3 (Optional) - Message to print at each increment (default is ...) + # OUTS: + # stdout: Prints the message at each increment + # USAGE: + # _countdown_ 10 1 "Waiting for cache to invalidate" + + local i ii t + local _n=${1:-10} + local _sleepTime=${2:-1} + local _message="${3:-...}" + ((t = _n + 1)) + + for ((i = 1; i <= _n; i++)); do + ((ii = t - i)) + if declare -f "info" &>/dev/null 2>&1; then + info "${_message} ${ii}" + else + echo "${_message} ${ii}" + fi + sleep ${_sleepTime} + done +} + +_dateUnixTimestamp_() { + # DESC: + # Get the current time in unix timestamp + # ARGS: + # None + # OUTS: + # stdout: Prints result ~ 1591554426 + # 0 If successful + # 1 If failed to get timestamp + # USAGE: + # _dateUnixTimestamp_ + + local _now + _now="$(date --universal +%s)" || return 1 + printf "%s\n" "${_now}" +} + +_formatDate_() { + # DESC: + # Reformats dates into user specified formats + # ARGS: + # $1 (Required) - Date to be formatted + # $2 (Optional) - Format in any format accepted by bash's date command. + # Examples: + # %F - YYYY-MM-DD + # %D - MM/DD/YY + # %a - Name of weekday in short (like Sun, Mon, Tue, Wed, Thu, Fri, Sat) + # %A - Name of weekday in full (like Sunday, Monday, Tuesday) + # '+%m %d, %Y' - 12 27, 2019 + # OUTS: + # stdout: Prints result + # USAGE: + # _formatDate_ "Jan 10, 2019" "%D" + # NOTE: + # Defaults to YYYY-MM-DD or $(date +%F) + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _d="${1}" + local _format="${2:-%F}" + _format="${_format//+/}" + + date -d "${_d}" "+${_format}" +} + +_fromSeconds_() { + # DESC: + # Convert seconds to HH:MM:SS + # ARGS: + # $1 (Required) - Time in seconds + # OUTS: + # stdout: HH:MM:SS + # USAGE: + # _fromSeconds_ "SECONDS" + # EXAMPLE: + # To compute the time it takes a script to run: + # STARTTIME=$(date +"%s") + # ENDTIME=$(date +"%s") + # TOTALTIME=$(($ENDTIME-$STARTTIME)) # human readable time + # _fromSeconds_ "$TOTALTIME" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _h + local _m + local _s + + ((_h = ${1} / 3600)) + ((_m = (${1} % 3600) / 60)) + ((_s = ${1} % 60)) + printf "%02d:%02d:%02d\n" ${_h} ${_m} ${_s} +} _monthToNumber_() { - # DESC: Convert a month name to a number - # ARGS: None - # OUTS: Prints the number of the month to stdout - # USAGE: _monthToNumber_ "January" + # DESC: + # Convert a month name to a number + # ARGS: + # $1 (Required) - Month name + # OUTS: + # stdout: Prints the number of the month (1-12) + # USAGE: + # _monthToNumber_ "January" - local mon="$(echo "$1" | tr '[:upper:]' '[:lower:]')" - case "$mon" in + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _mon="$(echo "$1" | tr '[:upper:]' '[:lower:]')" + case "${_mon}" in january | jan | ja) echo 1 ;; february | feb | fe) echo 2 ;; march | mar | ma) echo 3 ;; april | apr | ap) echo 4 ;; - may) echo 5 ;; + may) echo 5 ;; june | jun | ju) echo 6 ;; - july | jul) echo 7 ;; + july | jul) echo 7 ;; august | aug | au) echo 8 ;; september | sep | se) echo 9 ;; - october | oct) echo 10 ;; + october | oct | oc) echo 10 ;; november | nov | no) echo 11 ;; december | dec | de) echo 12 ;; *) - warning "month_monthToNumber_: Bad monthname: $1" + warning "_monthToNumber_: Bad month name: ${_mon}" return 1 - ;; + ;; esac } _numberToMonth_() { - # DESC: Convert a month number to its name - # ARGS: None - # OUTS: Prints the name of the month to stdout - # USAGE: _numberToMonth_ 1 + # DESC: + # Convert a month number to its name + # ARGS: + # $1 (Required) - Month number (1-12) + # OUTS: + # stdout: Prints the name of the month + # USAGE: + # _numberToMonth_ 11 - local mon="$1" - case "$mon" in + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _mon="$1" + case "${_mon}" in 1 | 01) echo January ;; 2 | 02) echo February ;; 3 | 03) echo March ;; 4 | 04) echo April ;; - 5 | 05) echo May ;; + 5 | 05) echo May ;; 6 | 06) echo June ;; 7 | 07) echo July ;; 8 | 08) echo August ;; 9 | 09) echo September ;; - 10) echo October ;; + 10) echo October ;; 11) echo November ;; 12) echo December ;; *) - warning "_numberToMonth_: Bad month number: $1" + warning "_numberToMonth_: Bad month number: ${_mon}" return 1 - ;; + ;; esac } _parseDate_() { - # DESC: Takes a string as input and attempts to find a date within it - # to parse into component parts (day, month, year) - # ARGS: $1 (required) - A string - # OUTS: Returns error if no date found - # $_parseDate_found - The date found in the string - # $_parseDate_year - The year - # $_parseDate_month - The number month - # $_parseDate_monthName - The name of the month - # $_parseDate_day - The day - # $_parseDate_hour - The hour (if avail) - # $_parseDate_minute - The minute (if avail) - # USAGE: if _parseDate_ "[STRING]"; then ... - # NOTE: This function only recognizes dates from the year 2000 to 2029 - # NOTE: Will recognize dates in the following formats separated by '-_ ./' + # DESC: + # Takes a string as input and attempts to find a date within it to parse + # into component parts (day, month, year) + # ARGS: + # $1 (required) - A string + # OUTS: + # 0 if date is found + # 1 if date is NOT found + # If a date was found, the following variables are set: + # $PARSE_DATE_FOUND - The date found in the string + # $PARSE_DATE_YEAR - The year + # $PARSE_DATE_MONTH - The number month + # $PARSE_DATE_MONTH_NAME - The name of the month + # $PARSE_DATE_DAY - The day + # $PARSE_DATE_HOUR - The hour (if avail) + # $PARSE_DATE_MINUTE - The minute (if avail) + # USAGE: + # if _parseDate_ "[STRING]"; then ... + # NOTE: + # - This function only recognizes dates from the year 2000 to 202 + # - Will recognize dates in the following formats separated by '-_ ./' # * YYYY-MM-DD * Month DD, YYYY * DD Month, YYYY # * Month, YYYY * Month, DD YY * MM-DD-YYYY # * MMDDYYYY * YYYYMMDD * DDMMYYYY @@ -77,124 +220,122 @@ _parseDate_() { # * MMDDYY * YYMMDD * mon-DD-YY # TODO: Simplify and reduce the number of regex checks + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - [[ $# -eq 0 ]] && { - error 'Missing required argument to _parseDate_()!' - return 1 - } + local _stringToTest="${1}" + local _pat - local date="${1:-$(date +%F)}" - _parseDate_found="" _parseDate_year="" _parseDate_month="" _parseDate_monthName="" - _parseDate_day="" _parseDate_hour="" _parseDate_minute="" + PARSE_DATE_FOUND="" PARSE_DATE_YEAR="" PARSE_DATE_MONTH="" PARSE_DATE_MONTH_NAME="" + PARSE_DATE_DAY="" PARSE_DATE_HOUR="" PARSE_DATE_MINUTE="" shopt -s nocasematch #Use case-insensitive regex debug "_parseDate_() input ${tan}$date${purple}" # YYYY MM DD or YYYY-MM-DD - pat="(.*[^0-9]|^)((20[0-2][0-9])[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2}))([^0-9].*|$)" - if [[ ${date} =~ $pat ]]; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year=$((10#${BASH_REMATCH[3]})) - _parseDate_month=$((10#${BASH_REMATCH[4]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[5]})) + _pat="(.*[^0-9]|^)((20[0-2][0-9])[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2}))([^0-9].*|$)" + if [[ ${_stringToTest} =~ ${_pat} ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[5]})) debug "regex match: ${tan}YYYY-MM-DD${purple}" # Month DD, YYYY - elif [[ ${date} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec)[-\./_ ]+([0-9]{1,2})(nd|rd|th|st)?,?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[1]:-}" - _parseDate_month=$(_monthToNumber_ ${BASH_REMATCH[2]:-}) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month:-}")" - _parseDate_day=$((10#${BASH_REMATCH[3]:-})) - _parseDate_year=$((10#${BASH_REMATCH[5]:-})) + elif [[ ${_stringToTest} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec)[-\./_ ]+([0-9]{1,2})(nd|rd|th|st)?,?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[1]:-}" + PARSE_DATE_MONTH=$(_monthToNumber_ ${BASH_REMATCH[2]:-}) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH:-}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[3]:-})) + PARSE_DATE_YEAR=$((10#${BASH_REMATCH[5]:-})) debug "regex match: ${tan}Month DD, YYYY${purple}" # Month DD, YY - elif [[ ${date} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec)[-\./_ ]+([0-9]{1,2})(nd|rd|th|st)?,?[-\./_ ]+([0-9]{2}))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[1]}" - _parseDate_month=$(_monthToNumber_ ${BASH_REMATCH[2]}) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[3]})) - _parseDate_year="20$((10#${BASH_REMATCH[5]}))" + elif [[ ${_stringToTest} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec)[-\./_ ]+([0-9]{1,2})(nd|rd|th|st)?,?[-\./_ ]+([0-9]{2}))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[1]}" + PARSE_DATE_MONTH=$(_monthToNumber_ ${BASH_REMATCH[2]}) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_YEAR="20$((10#${BASH_REMATCH[5]}))" debug "regex match: ${tan}Month DD, YY${purple}" # DD Month YYYY - elif [[ ${date} =~ (.*[^0-9]|^)(([0-9]{2})[-\./_ ]+(january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec),?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day=$((10#"${BASH_REMATCH[3]}")) - _parseDate_month="$(_monthToNumber_ "${BASH_REMATCH[4]}")" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year=$((10#"${BASH_REMATCH[5]}")) + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)(([0-9]{2})[-\./_ ]+(january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec),?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY=$((10#"${BASH_REMATCH[3]}")) + PARSE_DATE_MONTH="$(_monthToNumber_ "${BASH_REMATCH[4]}")" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR=$((10#"${BASH_REMATCH[5]}")) debug "regex match: ${tan}DD Month, YYYY${purple}" # MM-DD-YYYY or DD-MM-YYYY - elif [[ ${date} =~ (.*[^0-9]|^)(([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)(([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then if [[ $((10#${BASH_REMATCH[3]})) -lt 13 && $((10#${BASH_REMATCH[4]})) -gt 12 && $((10#${BASH_REMATCH[4]})) -lt 32 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year=$((10#${BASH_REMATCH[5]})) - _parseDate_month=$((10#${BASH_REMATCH[3]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR=$((10#${BASH_REMATCH[5]})) + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[4]})) debug "regex match: ${tan}MM-DD-YYYY${purple}" elif [[ $((10#${BASH_REMATCH[3]})) -gt 12 && - $((10#${BASH_REMATCH[3]})) -lt 32 && - $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[3]})) -lt 32 && + $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year=$((10#${BASH_REMATCH[5]})) - _parseDate_month=$((10#${BASH_REMATCH[4]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR=$((10#${BASH_REMATCH[5]})) + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[3]})) debug "regex match: ${tan}DD-MM-YYYY${purple}" elif [[ $((10#${BASH_REMATCH[3]})) -lt 32 && $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year=$((10#${BASH_REMATCH[5]})) - _parseDate_month=$((10#${BASH_REMATCH[3]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR=$((10#${BASH_REMATCH[5]})) + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[4]})) debug "regex match: ${tan}MM-DD-YYYY${purple}" else shopt -u nocasematch return 1 fi - elif [[ ${date} =~ (.*[^0-9]|^)(([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+([0-9]{2}))([^0-9].*|$) ]]; then + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)(([0-9]{1,2})[-\.\/_ ]+([0-9]{1,2})[-\.\/_ ]+([0-9]{2}))([^0-9].*|$) ]]; then if [[ $((10#${BASH_REMATCH[3]})) -lt 13 && $((10#${BASH_REMATCH[4]})) -gt 12 && $((10#${BASH_REMATCH[4]})) -lt 32 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year="20$((10#${BASH_REMATCH[5]}))" - _parseDate_month=$((10#${BASH_REMATCH[3]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR="20$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[4]})) debug "regex match: ${tan}MM-DD-YYYY${purple}" elif [[ $((10#${BASH_REMATCH[3]})) -gt 12 && - $((10#${BASH_REMATCH[3]})) -lt 32 && - $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[3]})) -lt 32 && + $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year="20$((10#${BASH_REMATCH[5]}))" - _parseDate_month=$((10#${BASH_REMATCH[4]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR="20$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[3]})) debug "regex match: ${tan}DD-MM-YYYY${purple}" elif [[ $((10#${BASH_REMATCH[3]})) -lt 32 && $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_year="20$((10#${BASH_REMATCH[5]}))" - _parseDate_month=$((10#${BASH_REMATCH[3]})) - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_day=$((10#${BASH_REMATCH[4]})) + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_YEAR="20$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH=$((10#${BASH_REMATCH[3]})) + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_DAY=$((10#${BASH_REMATCH[4]})) debug "regex match: ${tan}MM-DD-YYYY${purple}" else shopt -u nocasematch @@ -202,124 +343,124 @@ _parseDate_() { fi # Month, YYYY - elif [[ ${date} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec),?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[1]}" - _parseDate_day="1" - _parseDate_month="$(_monthToNumber_ "${BASH_REMATCH[2]}")" - _parseDate_monthName="$(_numberToMonth_ $_parseDate_month)" - _parseDate_year="$((10#${BASH_REMATCH[3]}))" + elif [[ ${_stringToTest} =~ ((january|jan|ja|february|feb|fe|march|mar|ma|april|apr|ap|may|june|jun|july|jul|ju|august|aug|september|sep|october|oct|november|nov|december|dec),?[-\./_ ]+(20[0-2][0-9]))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[1]}" + PARSE_DATE_DAY="1" + PARSE_DATE_MONTH="$(_monthToNumber_ "${BASH_REMATCH[2]}")" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ $PARSE_DATE_MONTH)" + PARSE_DATE_YEAR="$((10#${BASH_REMATCH[3]}))" debug "regex match: ${tan}Month, YYYY${purple}" # YYYYMMDDHHMM - elif [[ ${date} =~ (.*[^0-9]|^)((20[0-2][0-9])([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[5]}))" - _parseDate_month="$((10#${BASH_REMATCH[4]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="$((10#${BASH_REMATCH[3]}))" - _parseDate_hour="$((10#${BASH_REMATCH[6]}))" - _parseDate_minute="$((10#${BASH_REMATCH[7]}))" + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)((20[0-2][0-9])([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[4]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="$((10#${BASH_REMATCH[3]}))" + PARSE_DATE_HOUR="$((10#${BASH_REMATCH[6]}))" + PARSE_DATE_MINUTE="$((10#${BASH_REMATCH[7]}))" debug "regex match: ${tan}YYYYMMDDHHMM${purple}" # YYYYMMDDHH 1 2 3 4 5 6 - elif [[ ${date} =~ (.*[^0-9]|^)((20[0-2][0-9])([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[5]}))" - _parseDate_month="$((10#${BASH_REMATCH[4]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="$((10#${BASH_REMATCH[3]}))" - _parseDate_hour="${BASH_REMATCH[6]}" - _parseDate_minute="00" + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)((20[0-2][0-9])([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[4]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="$((10#${BASH_REMATCH[3]}))" + PARSE_DATE_HOUR="${BASH_REMATCH[6]}" + PARSE_DATE_MINUTE="00" debug "regex match: ${tan}YYYYMMDDHHMM${purple}" # MMDDYYYY or YYYYMMDD or DDMMYYYY # 1 2 3 4 5 6 - elif [[ ${date} =~ (.*[^0-9]|^)(([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then + elif [[ ${_stringToTest} =~ (.*[^0-9]|^)(([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}))([^0-9].*|$) ]]; then # MMDDYYYY if [[ $((10#${BASH_REMATCH[5]})) -eq 20 && - $((10#${BASH_REMATCH[3]})) -lt 13 && - $((10#${BASH_REMATCH[4]})) -lt 32 ]] \ + $((10#${BASH_REMATCH[3]})) -lt 13 && + $((10#${BASH_REMATCH[4]})) -lt 32 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[4]}))" - _parseDate_month="$((10#${BASH_REMATCH[3]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="${BASH_REMATCH[5]}${BASH_REMATCH[6]}" - debug "regex match: ${tan}MMDDYYYY${purple}" + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[4]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[3]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="${BASH_REMATCH[5]}${BASH_REMATCH[6]}" + debug "regex match: ${tan}MMDDYYYY${purple}" # DDMMYYYY elif [[ $((10#${BASH_REMATCH[5]})) -eq 20 && - $((10#${BASH_REMATCH[3]})) -gt 12 && - $((10#${BASH_REMATCH[3]})) -lt 32 && - $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[3]})) -gt 12 && + $((10#${BASH_REMATCH[3]})) -lt 32 && + $((10#${BASH_REMATCH[4]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[3]}))" - _parseDate_month="$((10#${BASH_REMATCH[4]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="${BASH_REMATCH[5]}${BASH_REMATCH[6]}" - debug "regex match: ${tan}DDMMYYYY${purple}" + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[3]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[4]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="${BASH_REMATCH[5]}${BASH_REMATCH[6]}" + debug "regex match: ${tan}DDMMYYYY${purple}" # YYYYMMDD elif [[ $((10#${BASH_REMATCH[3]})) -eq 20 && - $((10#${BASH_REMATCH[6]})) -gt 12 && - $((10#${BASH_REMATCH[6]})) -lt 32 && - $((10#${BASH_REMATCH[5]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[6]})) -gt 12 && + $((10#${BASH_REMATCH[6]})) -lt 32 && + $((10#${BASH_REMATCH[5]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[6]}))" - _parseDate_month="$((10#${BASH_REMATCH[5]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" - debug "regex match: ${tan}YYYYMMDD${purple}" + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[6]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" + debug "regex match: ${tan}YYYYMMDD${purple}" # YYYYDDMM elif [[ $((10#${BASH_REMATCH[3]})) -eq 20 && - $((10#${BASH_REMATCH[5]})) -gt 12 && - $((10#${BASH_REMATCH[5]})) -lt 32 && - $((10#${BASH_REMATCH[6]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[5]})) -gt 12 && + $((10#${BASH_REMATCH[5]})) -lt 32 && + $((10#${BASH_REMATCH[6]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[5]}))" - _parseDate_month="$((10#${BASH_REMATCH[6]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" - debug "regex match: ${tan}YYYYMMDD${purple}" + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[6]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" + debug "regex match: ${tan}YYYYMMDD${purple}" # Assume YYYMMDD elif [[ $((10#${BASH_REMATCH[3]})) -eq 20 && - $((10#${BASH_REMATCH[6]})) -lt 32 && - $((10#${BASH_REMATCH[5]})) -lt 13 ]] \ + $((10#${BASH_REMATCH[6]})) -lt 32 && + $((10#${BASH_REMATCH[5]})) -lt 13 ]] \ ; then - _parseDate_found="${BASH_REMATCH[2]}" - _parseDate_day="$((10#${BASH_REMATCH[6]}))" - _parseDate_month="$((10#${BASH_REMATCH[5]}))" - _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - _parseDate_year="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" - debug "regex match: ${tan}YYYYMMDD${purple}" + PARSE_DATE_FOUND="${BASH_REMATCH[2]}" + PARSE_DATE_DAY="$((10#${BASH_REMATCH[6]}))" + PARSE_DATE_MONTH="$((10#${BASH_REMATCH[5]}))" + PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + PARSE_DATE_YEAR="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" + debug "regex match: ${tan}YYYYMMDD${purple}" else shopt -u nocasematch return 1 fi # # MMDD or DDYY - # elif [[ "$date" =~ .*(([0-9]{2})([0-9]{2})).* ]]; then + # elif [[ "${_stringToTest}" =~ .*(([0-9]{2})([0-9]{2})).* ]]; then # debug "regex match: ${tan}MMDD or DDMM${purple}" - # _parseDate_found="${BASH_REMATCH[1]}" + # PARSE_DATE_FOUND="${BASH_REMATCH[1]}" # # Figure out if days are months or vice versa # if [[ $(( 10#${BASH_REMATCH[2]} )) -gt 12 \ # && $(( 10#${BASH_REMATCH[2]} )) -lt 32 \ # && $(( 10#${BASH_REMATCH[3]} )) -lt 13 \ # ]]; then - # _parseDate_day="$(( 10#${BASH_REMATCH[2]} ))" - # _parseDate_month="$(( 10#${BASH_REMATCH[3]} ))" - # _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - # _parseDate_year="$(date +%Y )" + # PARSE_DATE_DAY="$(( 10#${BASH_REMATCH[2]} ))" + # PARSE_DATE_MONTH="$(( 10#${BASH_REMATCH[3]} ))" + # PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + # PARSE_DATE_YEAR="$(date +%Y )" # elif [[ $(( 10#${BASH_REMATCH[2]} )) -lt 13 \ # && $(( 10#${BASH_REMATCH[3]} )) -lt 32 \ # ]]; then - # _parseDate_day="$(( 10#${BASH_REMATCH[3]} ))" - # _parseDate_month="$(( 10#${BASH_REMATCH[2]} ))" - # _parseDate_monthName="$(_numberToMonth_ "${_parseDate_month}")" - # _parseDate_year="$(date +%Y )" + # PARSE_DATE_DAY="$(( 10#${BASH_REMATCH[3]} ))" + # PARSE_DATE_MONTH="$(( 10#${BASH_REMATCH[2]} ))" + # PARSE_DATE_MONTH_NAME="$(_numberToMonth_ "${PARSE_DATE_MONTH}")" + # PARSE_DATE_YEAR="$(date +%Y )" # else # shopt -u nocasematch # return 1 @@ -327,69 +468,102 @@ _parseDate_() { else shopt -u nocasematch return 1 - fi - [[ -z ${_parseDate_year:-} ]] && { - shopt -u nocasematch - return 1 + [[ -z ${PARSE_DATE_YEAR:-} ]] && { + shopt -u nocasematch + return 1 } - ((_parseDate_month >= 1 && _parseDate_month <= 12)) || { - shopt -u nocasematch - return 1 + ((PARSE_DATE_MONTH >= 1 && PARSE_DATE_MONTH <= 12)) || { + shopt -u nocasematch + return 1 } - ((_parseDate_day >= 1 && _parseDate_day <= 31)) || { - shopt -u nocasematch - return 1 + ((PARSE_DATE_DAY >= 1 && PARSE_DATE_DAY <= 31)) || { + shopt -u nocasematch + return 1 } - debug "${tan}\$_parseDate_found: ${_parseDate_found}${purple}" - debug "${tan}\$_parseDate_year: ${_parseDate_year}${purple}" - debug "${tan}\$_parseDate_month: ${_parseDate_month}${purple}" - debug "${tan}\$_parseDate_monthName: ${_parseDate_monthName}${purple}" - debug "${tan}\$_parseDate_day: ${_parseDate_day}${purple}" - [[ -z ${_parseDate_hour:-} ]] || debug "${tan}\$_parseDate_hour: ${_parseDate_hour}${purple}" - [[ -z ${_parseDate_minute:-} ]] || debug "${tan}\$_parseDate_minute: ${_parseDate_minute}${purple}" + debug "${tan}\$PARSE_DATE_FOUND: ${PARSE_DATE_FOUND}${purple}" + debug "${tan}\$PARSE_DATE_YEAR: ${PARSE_DATE_YEAR}${purple}" + debug "${tan}\$PARSE_DATE_MONTH: ${PARSE_DATE_MONTH}${purple}" + debug "${tan}\$PARSE_DATE_MONTH_NAME: ${PARSE_DATE_MONTH_NAME}${purple}" + debug "${tan}\$PARSE_DATE_DAY: ${PARSE_DATE_DAY}${purple}" + [[ -z ${PARSE_DATE_HOUR:-} ]] || debug "${tan}\$PARSE_DATE_HOUR: ${PARSE_DATE_HOUR}${purple}" + [[ -z ${PARSE_DATE_MINUTE:-} ]] || debug "${tan}\$PARSE_DATE_MINUTE: ${PARSE_DATE_MINUTE}${purple}" shopt -u nocasematch # Output results for BATS tests if [ "${automated_test_in_progress:-}" ]; then - echo "_parseDate_found: ${_parseDate_found}" - echo "_parseDate_year: ${_parseDate_year}" - echo "_parseDate_month: ${_parseDate_month}" - echo "_parseDate_monthName: ${_parseDate_monthName}" - echo "_parseDate_day: ${_parseDate_day}" - echo "_parseDate_hour: ${_parseDate_hour}" - echo "_parseDate_minute: ${_parseDate_minute}" + echo "PARSE_DATE_FOUND: ${PARSE_DATE_FOUND}" + echo "PARSE_DATE_YEAR: ${PARSE_DATE_YEAR}" + echo "PARSE_DATE_MONTH: ${PARSE_DATE_MONTH}" + echo "PARSE_DATE_MONTH_NAME: ${PARSE_DATE_MONTH_NAME}" + echo "PARSE_DATE_DAY: ${PARSE_DATE_DAY}" + echo "PARSE_DATE_HOUR: ${PARSE_DATE_HOUR}" + echo "PARSE_DATE_MINUTE: ${PARSE_DATE_MINUTE}" fi } -_formatDate_() { - # DESC: Reformats dates into user specified formats - # ARGS: $1 (Required) - Date to be formatted - # $2 (Optional) - Format in any format accepted by bash's date command. Examples listed below. - # %F - YYYY-MM-DD - # %D - MM/DD/YY - # %a - Name of weekday in short (like Sun, Mon, Tue, Wed, Thu, Fri, Sat) - # %A - Name of weekday in full (like Sunday, Monday, Tuesday) - # '+%m %d, %Y' - 12 27, 2019 - # OUTS: Echo result to STDOUT - # USAGE: _formatDate_ "Jan 10, 2019" "%D" - # NOTE: Defaults to YYYY-MM-DD or $(date +%F) +_readableUnixTimestamp_() { + # DESC: + # Format unix timestamp to human readable format. If format string is not specified then + # default to "yyyy-mm-dd hh:mm:ss" + # ARGS: + # $1 (Required) - Unix timestamp to be formatted + # $2 (Optional) - Format string + # OUTS: + # 0 If successful + # 1 If failed to convert + # stdout: Human readable format of unix timestamp + # USAGE: + # _readableUnixTimestamp_ "1591554426" + # _readableUnixTimestamp_ "1591554426" "%Y-%m-%d" + # CREDIT: + # https://github.com/labbots/bash-utility/blob/master/src/date.sh - [[ $# -eq 0 ]] && { - error 'Missing required argument to _formatDate_()' - return 1 - } + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + local _timestamp="${1}" + local _format="${2:-"%F %T"}" + local _out="$(date -d "@${_timestamp}" +"${_format}")" || return 1 + printf "%s\n" "${_out}" +} - local d="${1}" - local format="${2:-%F}" - format="${format//+/}" +_toSeconds_() { + # DESC: + # Converts HH:MM:SS to seconds + # ARGS: + # $1 (Required) - Time in HH:MM:SS + # OUTS: + # stdout: Print seconds + # USAGE: + # _toSeconds_ "01:00:00" + # NOTE: + # Acceptable Input Formats + # 24 12 09 + # 12,12,09 + # 12;12;09 + # 12:12:09 + # 12-12-09 + # 12H12M09S + # 12h12m09s - if command -v gdate >/dev/null 2>&1; then - gdate -d "${d}" "+${format}" + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _saveIFS + local _h + local _m + local _s + + if [[ $1 =~ [0-9]{1,2}(:|,|-|_|,| |[hHmMsS])[0-9]{1,2}(:|,|-|_|,| |[hHmMsS])[0-9]{1,2} ]]; then + _saveIFS="${IFS}" + IFS=":,;-_, HhMmSs" read -r h m s <<<"$1" + IFS="${_saveIFS}" else - date -d "${d}" "+${format}" + _h="$1" + _m="$2" + _s="$3" fi + + printf "%s\n" "$((10#$_h * 3600 + 10#$_m * 60 + 10#$_s))" } diff --git a/utilities/debug.bash b/utilities/debug.bash new file mode 100644 index 0000000..153ae50 --- /dev/null +++ b/utilities/debug.bash @@ -0,0 +1,58 @@ +# Functions to aid in debugging bash scripts + +_pauseScript_() { + # DESC: + # Pause a script at any point and continue after user input + # ARGS: + # $1 (Optional) - String for customized message + + local _pauseMessage + _pauseMessage="${1:-Paused. Ready to continue?}" + + if _seekConfirmation_ "${_pauseMessage}"; then + info "Continuing..." + else + notice "Exiting Script" + _safeExit_ + fi +} + +_printAnsi_() { + # DESC: + # Helps debug ansi escape sequence in text by displaying the escape codes + # ARGS: + # $1 (Required) String input with ansi escape sequence. + # OUTS: + # stdout: Ansi escape sequence printed in output as is. + # USAGE: + # _printAnsi_ "$(tput bold)$(tput setaf 9)Some Text" + # CREDIT: + # https://github.com/labbots/bash-utility/blob/master/src/debug.sh + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + #echo $(tr -dc '[:print:]'<<<$1) + printf "%s\n" "${1//$'\e'/\\e}" + +} + +_printArray_() { + # DESC: + # Prints the content of array as key value pairs for easier debugging + # ARGS: + # $1 (Required) - String variable name of the array + # OUTS: + # stdout: Formatted key value of array.one + # USAGE: + # testArray=("1" "2" "3" "4") + # _printArray_ "testArray" + # CREDIT: + # https://github.com/labbots/bash-utility/blob/master/src/debug.sh + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + declare -n _arr="${1}" + for _k in "${!_arr[@]}"; do + printf "%s = %s\n" "$_k" "${_arr[$_k]}" + done +} diff --git a/utilities/files.bash b/utilities/files.bash index ad550e2..6dc8967 100644 --- a/utilities/files.bash +++ b/utilities/files.bash @@ -1,65 +1,36 @@ -_listFiles_() { - # DESC: Find files in a directory. Use either glob or regex - # ARGS: $1 (Required) - 'g|glob' or 'r|regex' - # $2 (Required) - pattern to match - # $3 (Optional) - directory - # OUTS: Prints files to STDOUT - # NOTE: Searches are NOT case sensitive and MUST be quoted - # USAGE: _listFiles_ glob "*.txt" "some/backup/dir" - # _listFiles_ regex ".*\.txt" "some/backup/dir" - # readarray -t array < <(_listFiles_ g "*.txt") - - [[ $# -lt 2 ]] && { - error 'Missing required argument to _listFiles_()!' - return 1 - } - - local t="${1}" - local p="${2}" - local d="${3:-.}" - local fileMatch e - - case "$t" in - glob | Glob | g | G) - while read -r fileMatch; do - e="$(realpath "${fileMatch}")" - echo "${e}" - done < <(find "${d}" -iname "${p}" -type f -maxdepth 1 | sort) - ;; - regex | Regex | r | R) - while read -r fileMatch; do - e="$(realpath "${fileMatch}")" - echo "${e}" - done < <(find "${d}" -iregex "${p}" -type f -maxdepth 1 | sort) - ;; - *) - echo "Could not determine if search was glob or regex" - return 1 - ;; - esac -} +# Functions for manipulating files _backupFile_() { - # DESC: Creates a backup of a specified file with .bak extension or - # optionally to a specified directory - # ARGS: $1 (Required) - Source file + # DESC: + # Creates a backup of a specified file with .bak extension or optionally to a + # specified directory + # ARGS: + # $1 (Required) - Source file # $2 (Optional) - Destination dir name used only with -d flag (defaults to ./backup) - # OPTS: -d - Move files to a backup direcory - # -m - Replaces copy (default) with move, effectively removing - # the original file - # OUTS: None - # USAGE: _backupFile_ "sourcefile.txt" "some/backup/dir" - # NOTE: dotfiles have their leading '.' removed in their backup + # OPTS: + # -d - Move files to a backup direcory + # -m - Replaces copy (default) with move, effectively removing the original file + # REQUIRES: + # _execute_ + # _createUniqueFilename_ + # OUTS: + # 0 - Success + # 1 - Error + # filesystem: Backup of files + # USAGE: + # _backupFile_ "sourcefile.txt" "some/backup/dir" + # NOTE: + # Dotfiles have their leading '.' removed in their backup local opt local OPTIND=1 - local useDirectory=false - local MOVE_FILE=false + local _useDirectory=false + local _moveFile=false while getopts ":dDmM" opt; do case ${opt} in - d | D) useDirectory=true ;; - m | M) MOVE_FILE=true ;; + d | D) _useDirectory=true ;; + m | M) _moveFile=true ;; *) { error "Unrecognized option '${1}' passed to _backupFile_" "${LINENO}" @@ -70,209 +41,256 @@ _backupFile_() { done shift $((OPTIND - 1)) - [[ $# -lt 1 ]] && fatal 'Missing required argument to _backupFile_()!' + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - local SOURCE_FILE="${1}" - local d="${2:-backup}" - local n # New filename (created by _uniqueFilename_) + local _fileToBackup="${1}" + local _backupDir="${2:-backup}" + local _newFilename # Error handling [ ! "$(declare -f "_execute_")" ] \ && { - warning "need function _execute_" - return 1 + fatal "_backupFile_ needs function _execute_" } - [ ! "$(declare -f "_uniqueFileName_")" ] \ + [ ! "$(declare -f "_createUniqueFilename_")" ] \ && { - warning "need function _uniqueFileName_" - return 1 + fatal "_backupFile_ needs function _createUniqueFilename_" } - [ ! -e "${SOURCE_FILE}" ] \ + [ ! -e "${_fileToBackup}" ] \ && { - warning "Source '${SOURCE_FILE}' not found" + debug "Source '${_fileToBackup}' not found" return 1 } - if [ ${useDirectory} == true ]; then + if [ ${_useDirectory} == true ]; then - [ ! -d "${d}" ] \ - && _execute_ "mkdir -p \"${d}\"" "Creating backup directory" + [ ! -d "${_backupDir}" ] \ + && _execute_ "mkdir -p \"${_backupDir}\"" "Creating backup directory" - if [ -e "${SOURCE_FILE}" ]; then - n="$(_uniqueFileName_ "${d}/${SOURCE_FILE#.}")" - if [ ${MOVE_FILE} == true ]; then - _execute_ "mv \"${SOURCE_FILE}\" \"${d}/${n##*/}\"" "Moving: '${SOURCE_FILE}' to '${d}/${n##*/}'" - else - _execute_ "cp -R \"${SOURCE_FILE}\" \"${d}/${n##*/}\"" "Backing up: '${SOURCE_FILE}' to '${d}/${n##*/}'" - fi + _newFilename="$(_createUniqueFilename_ "${_backupDir}/${_fileToBackup#.}")" + if [ ${_moveFile} == true ]; then + _execute_ "mv \"${_fileToBackup}\" \"${_backupDir}/${_newFilename##*/}\"" "Moving: '${_fileToBackup}' to '${_backupDir}/${_newFilename##*/}'" + else + _execute_ "cp -R \"${_fileToBackup}\" \"${_backupDir}/${_newFilename##*/}\"" "Backing up: '${_fileToBackup}' to '${_backupDir}/${_newFilename##*/}'" fi else - n="$(_uniqueFileName_ "${SOURCE_FILE}.bak")" - if [ ${MOVE_FILE} == true ]; then - _execute_ "mv \"${SOURCE_FILE}\" \"${n}\"" "Moving '${SOURCE_FILE}' to '${n}'" + _newFilename="$(_createUniqueFilename_ "${_fileToBackup}.bak")" + if [ ${_moveFile} == true ]; then + _execute_ "mv \"${_fileToBackup}\" \"${_newFilename}\"" "Moving '${_fileToBackup}' to '${_newFilename}'" else - _execute_ "cp -R \"${SOURCE_FILE}\" \"${n}\"" "Backing up '${SOURCE_FILE}' to '${n}'" + _execute_ "cp -R \"${_fileToBackup}\" \"${_newFilename}\"" "Backing up '${_fileToBackup}' to '${_newFilename}'" fi fi } -_parseFilename_() { - # DESC: Break a filename into its component parts which and place them into prefixed - # variables for use in your script. Run with VERBOSE=true to see the variables while - # running your script. - # ARGS: $1 (Required) - File - # OPTS: -n - optional flag for number of extension levels (Ex: -n2) - # OUTS: $PARSE_FULL - File and its real path (ie, resolve symlinks) - # $PARSE_PATH - Path to the file - # $PARSE_BASE - Name of the file WITH extension - # $PARSE_BASENOEXT - Name of file WITHOUT extension - # $PARSE_EXT - The extension of the file - # USAGE: _parseFilename_ "some/file.txt" +_createUniqueFilename_() { + # DESC: + # Ensure a file to be created has a unique filename to avoid overwriting other + # filenames by incrementing a number at the end of the filename + # ARGS: + # $1 (Required) - Name of file to be created + # $2 (Optional) - Separation characted (Defaults to a period '.') + # OUTS: + # stdout: Unique name of file + # 0 if successful + # 1 if not successful + # OPTS: + # -i: Places the unique integer before the file extension + # USAGE: + # _createUniqueFilename_ "/some/dir/file.txt" --> /some/dir/file.txt.1 + # _createUniqueFilename_ -i"/some/dir/file.txt" "-" --> /some/dir/file-1.txt + # echo "line" > "$(_createUniqueFilename_ "/some/dir/file.txt")" - # Error handling - if [[ $# -lt 1 ]] \ - || ! command -v dirname &>/dev/null \ - || ! command -v basename &>/dev/null \ - || ! command -v realpath &>/dev/null; then + [[ $# -lt 1 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - fatal "Missing dependency or input to _parseFilename_()" - return 1 + local opt + local OPTIND=1 + local _internalInteger=false + while getopts ":iI" opt; do + case ${opt} in + i | I) _internalInteger=true ;; + *) + { + error "Unrecognized option '${1}' passed to _createUniqueFilename_" "${LINENO}" + return 1 + } + ;; + esac + done + shift $((OPTIND - 1)) + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _fullFile="${1}" + local _spacer="${2:-.}" + local _filePath + local _originalFile + local _extension + local _newFilename + local _num + local _levels + local _fn + local _ext + local i + + if ! command -v realpath >/dev/null 2>&1; then + error "We must have 'realpath' installed and available in \$PATH to run." + if [[ $OSTYPE == "darwin"* ]]; then + notice "Install coreutils using homebrew and rerun this script." + info "\t$ brew install coreutils" + fi + _safeExit_ 1 fi - local levels - local option - local exts - local ext - local i - local fn + # Find directories with realpath if input is an actual file + if [ -e "${_fullFile}" ]; then + _fullFile="$(realpath "${_fullFile}")" + fi - local OPTIND=1 - while getopts ":n:" option; do - case ${option} in - n) levels=${OPTARG} ;; - *) continue ;; - esac - done && shift $((OPTIND - 1)) - - local fileToParse="${1}" - - PARSE_FULL="$(realpath "${fileToParse}")" \ - && debug "\${PARSE_FULL}: ${PARSE_FULL:-}" - PARSE_BASE=$(basename "${fileToParse}") \ - && debug "\${PARSE_BASE}: ${PARSE_BASE}" - PARSE_PATH="$(realpath "$(dirname "${fileToParse}")")" \ - && debug "\${PARSE_PATH}: ${PARSE_PATH:-}" + _filePath="$(dirname "${_fullFile}")" + _originalFile="$(basename "${_fullFile}")" # Detect some common multi-extensions - if [[ ! ${levels:-} ]]; then - case $(tr '[:upper:]' '[:lower:]' <<<"${PARSE_BASE}") in - *.tar.gz | *.tar.bz2) levels=2 ;; - esac - fi + case $(tr '[:upper:]' '[:lower:]' <<<"${_originalFile}") in + *.tar.gz | *.tar.bz2) _levels=2 ;; + *) _levels=1 ;; + esac # Find Extension - levels=${levels:-1} - fn="${PARSE_BASE}" - for ((i = 0; i < levels; i++)); do - ext=${fn##*.} + _fn="${_originalFile}" + for ((i = 0; i < _levels; i++)); do + _ext=${_fn##*.} if [ $i == 0 ]; then - exts=${ext}${exts:-} + _extension=${_ext}${_extension:-} else - exts=${ext}.${exts:-} + _extension=${_ext}.${_extension:-} fi - fn=${fn%.$ext} + _fn=${_fn%.$_ext} done - if [[ ${exts} == "${PARSE_BASE}" ]]; then - PARSE_EXT="" && debug "\${PARSE_EXT}: ${PARSE_EXT}" + debug "_extension: ${_extension}" + if [[ ${_extension} == "${_originalFile}" ]]; then + _extension="" else - PARSE_EXT="${exts}" && debug "\${PARSE_EXT}: ${PARSE_EXT}" + _originalFile="${_originalFile%.$_extension}" && debug "_originalFile: ${_originalFile}" + _extension=".${_extension}" fi - PARSE_BASENOEXT="${PARSE_BASE%.$PARSE_EXT}" \ - && debug "\${PARSE_BASENOEXT}: ${PARSE_BASENOEXT}" + _newFilename="${_filePath}/${_originalFile}${_extension:-}" && debug "_newFilename: ${_newFilename}" + + if [ -e "${_newFilename}" ]; then + _num=1 + if [ "${_internalInteger}" = true ]; then + while [[ -e "${_filePath}/${_originalFile}${_spacer}${_num}${_extension:-}" ]]; do + ((_num++)) + done + _newFilename="${_filePath}/${_originalFile}${_spacer}${_num}${_extension:-}" + else + while [[ -e "${_filePath}/${_originalFile}${_extension:-}${_spacer}${_num}" ]]; do + ((_num++)) + done + _newFilename="${_filePath}/${_originalFile}${_extension:-}${_spacer}${_num}" + fi + fi + + echo "${_newFilename}" + return 0 } _decryptFile_() { - # DESC: Decrypts a file with openSSL - # ARGS: $1 (Required) - File to be decrypted + # DESC: + # Decrypts a file with openSSL + # ARGS: + # $1 (Required) - File to be decrypted # $2 (Optional) - Name of output file (defaults to $1.decrypt) - # OUTS: None - # USAGE: _decryptFile_ "somefile.txt.enc" "decrypted_somefile.txt" - # NOTE: If a variable '$PASS' has a value, we will use that as the password - # to decrypt the file. Otherwise we will ask + # OUTS: + # 0 - Success + # 1 - Error + # REQUIRES: + # _execute_ + # USAGE: + # _decryptFile_ "somefile.txt.enc" "decrypted_somefile.txt" + # NOTE: + # If a global variable '$PASS' has a value, we will use that as the password to decrypt + # the file. Otherwise we will ask - [[ $# -lt 1 ]] && fatal 'Missing required argument to _decryptFile_()!' + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - local fileToDecrypt decryptedFile defaultName - fileToDecrypt="${1:?_decryptFile_ needs a file}" - defaultName="${fileToDecrypt%.enc}" - decryptedFile="${2:-$defaultName.decrypt}" + local _fileToDecrypt="${1:?_decryptFile_ needs a file}" + local _defaultName="${_fileToDecrypt%.enc}" + local _decryptedFile="${2:-$_defaultName.decrypt}" - [ ! "$(declare -f "_execute_")" ] \ - && { - echo "need function _execute_" - return 1 - } + [ ! "$(declare -f "_execute_")" ] && fatal "need function _execute_" - [ ! -f "$fileToDecrypt" ] && return 1 + if ! command -v openssl &>/dev/null; then + fatal "openssl not found" + fi - if [ -z "${PASS}" ]; then - _execute_ "openssl enc -aes-256-cbc -d -in \"${fileToDecrypt}\" -out \"${decryptedFile}\"" "Decrypt ${fileToDecrypt}" + [ ! -f "${_fileToDecrypt}" ] && return 1 + + if [ -z "${PASS:-}" ]; then + _execute_ "openssl enc -aes-256-cbc -d -in \"${_fileToDecrypt}\" -out \"${_decryptedFile}\"" "Decrypt ${_fileToDecrypt}" else - _execute_ "openssl enc -aes-256-cbc -d -in \"${fileToDecrypt}\" -out \"${decryptedFile}\" -k \"${PASS}\"" "Decrypt ${fileToDecrypt}" + _execute_ "openssl enc -aes-256-cbc -d -in \"${_fileToDecrypt}\" -out \"${_decryptedFile}\" -k \"${PASS}\"" "Decrypt ${_fileToDecrypt}" fi } _encryptFile_() { - # DESC: Encrypts a file using openSSL - # ARGS: $1 (Required) - Input file + # DESC: + # Encrypts a file using openSSL + # ARGS: + # $1 (Required) - Input file # $2 (Optional) - Name of output file (defaults to $1.enc) - # OUTS: None - # USAGE: _encryptFile_ "somefile.txt" "encrypted_somefile.txt" - # NOTE: If a variable '$PASS' has a value, we will use that as the password - # for the encrypted file. Otherwise we will ask. + # OUTS: + # None + # REQUIRE: + # _execute_ + # USAGE: + # _encryptFile_ "somefile.txt" "encrypted_somefile.txt" + # NOTE: + # If a variable '$PASS' has a value, we will use that as the password + # for the encrypted file. Otherwise ask. - local fileToEncrypt encryptedFile defaultName + local _fileToEncrypt="${1:?_encodeFile_ needs a file}" + local _defaultName="${_fileToEncrypt%.decrypt}" + local _encryptedFile="${2:-$_defaultName.enc}" - fileToEncrypt="${1:?_encodeFile_ needs a file}" - defaultName="${fileToEncrypt%.decrypt}" - encryptedFile="${2:-$defaultName.enc}" + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - [ ! -f "$fileToEncrypt" ] && return 1 + [ ! -f "${_fileToEncrypt}" ] && return 1 - [ ! "$(declare -f "_execute_")" ] \ - && { - echo "need function _execute_" - return 1 - } + [ ! "$(declare -f "_execute_")" ] && fatal "need function _execute_" - if [ -z "${PASS}" ]; then - _execute_ "openssl enc -aes-256-cbc -salt -in \"${fileToEncrypt}\" -out \"${encryptedFile}\"" "Encrypt ${fileToEncrypt}" + if ! command -v openssl &>/dev/null; then + fatal "openssl not found" + fi + + if [ -z "${PASS:-}" ]; then + _execute_ "openssl enc -aes-256-cbc -salt -in \"${_fileToEncrypt}\" -out \"${_encryptedFile}\"" "Encrypt ${_fileToEncrypt}" else - _execute_ "openssl enc -aes-256-cbc -salt -in \"${fileToEncrypt}\" -out \"${encryptedFile}\" -k \"${PASS}\"" "Encrypt ${fileToEncrypt}" + _execute_ "openssl enc -aes-256-cbc -salt -in \"${_fileToEncrypt}\" -out \"${_encryptedFile}\" -k \"${PASS}\"" "Encrypt ${_fileToEncrypt}" fi } -_extract_() { - # DESC: Extract a compressed file - # ARGS: $1 (Required) - Input file +_extractArchive_() { + # DESC: + # Extract a compressed file + # ARGS: + # $1 (Required) - Input file # $2 (optional) - Input 'v' to show verbose output - # OUTS: None + # OUTS: + # 0 - Success + # 1 - Error - local filename - local foldername - local fullpath - local didfolderexist - local vv + local _vv - [[ $# -lt 1 ]] && fatal 'Missing required argument to _extract_()!' + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - [[ ${2:-} == "v" ]] && vv="v" + [[ ${2:-} == "v" ]] && _vv="v" if [ -f "$1" ]; then case "$1" in - *.tar.bz2 | *.tbz | *.tbz2) tar "x${vv}jf" "$1" ;; - *.tar.gz | *.tgz) tar "x${vv}zf" "$1" ;; + *.tar.bz2 | *.tbz | *.tbz2) tar "x${_vv}jf" "$1" ;; + *.tar.gz | *.tgz) tar "x${_vv}zf" "$1" ;; *.tar.xz) xz --decompress "$1" set -- "$@" "${1:0:-3}" @@ -282,7 +300,7 @@ _extract_() { set -- "$@" "${1:0:-2}" ;; *.bz2) bunzip2 "$1" ;; - *.deb) dpkg-deb -x${vv} "$1" "${1:0:-4}" ;; + *.deb) dpkg-deb -x${_vv} "$1" "${1:0:-4}" ;; *.pax.gz) gunzip "$1" set -- "$@" "${1:0:-3}" @@ -291,8 +309,8 @@ _extract_() { *.pax) pax -r -f "$1" ;; *.pkg) pkgutil --expand "$1" "${1:0:-4}" ;; *.rar) unrar x "$1" ;; - *.rpm) rpm2cpio "$1" | cpio -idm${vv} ;; - *.tar) tar "x${vv}f" "$1" ;; + *.rpm) rpm2cpio "$1" | cpio -idm${_vv} ;; + *.tar) tar "x${_vv}f" "$1" ;; *.txz) mv "$1" "${1:0:-4}.tar.xz" set -- "$@" "${1:0:-4}.tar.xz" @@ -306,53 +324,256 @@ _extract_() { else return 1 fi - shift +} +_fileName_() { + # DESC: + # Get only the filename from a string + # ARGS: + # $1 (Required) - Input string + # OUTS: + # 0 - Success + # 1 - Failure + # stdout: Filename with extension + # USAGE: + # _fileName_ "some/path/to/file.txt" --> "file.txt" + # _fileName_ "some/path/to/file" --> "file" + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + printf "%s" "${1##*/}" + +} + +_fileBasename_() { + # DESC: + # Gets the basename of a file from a file name + # ARGS: + # $1 (Required) - Input string path + # OUTS: + # 0 - Success + # 1 - Failure + # stdout: Filename basename (no extension or path) + # USAGE: + # _fileBasename_ "some/path/to/file.txt" --> "file" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _file + local _basename + _file="${1##*/}" + _basename="${_file%.*}" + + printf "%s" "${_basename}" +} + +_fileExtension_() { + # DESC: + # Gets an extension from a file name. Finds a few common double extensions (tar.gz, tar.bz2, log.1) + # ARGS: + # $1 (Required) - Input string path + # OUTS: + # 0 - Success + # 1 - If no extension found in filename + # stdout: extension (without the .) + # USAGE: + # _fileExtension_ "some/path/to/file.txt" --> "txt" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _file + local _extension + local _levels + local _ext + local _exts + _file="${1##*/}" + + # Detect some common multi-extensions + if [[ -z ${_levels:-} ]]; then + case $(tr '[:upper:]' '[:lower:]' <<<"${_file}") in + *.tar.gz | *.tar.bz2 | *.log.[0-9]) _levels=2 ;; + *) _levels=1 ;; + esac + fi + + _fn="$_file" + for ((i = 0; i < _levels; i++)); do + _ext=${_fn##*.} + if [ $i == 0 ]; then + _exts=${_ext}${_exts:-} + else + _exts=${_ext}.${_exts:-} + fi + _fn=${_fn%.$_ext} + done + debug "_exts: $_exts" + [[ ${_file} == "${_exts}" ]] && return 1 + + printf "%s" "${_exts}" + +} + +_fileDirectory_() { + # DESC: + # Finds the directory name from a file path + # ARGS: + # $1 (Required) - Input string path + # OUTS: + # 0 - Success + # 1 - Failure + # stdout: Directory path + # USAGE: + # _fileDirectory_ "some/path/to/file.txt" --> "some/path/to" + # CREDIT: + # https://github.com/labbots/bash-utility/ + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _tmp=${1:-.} + + [[ ${_tmp} != *[!/]* ]] && { printf '/\n' && return; } + _tmp="${_tmp%%"${_tmp##*[!/]}"}" + + [[ ${_tmp} != */* ]] && { printf '.\n' && return; } + _tmp=${_tmp%/*} && _tmp="${_tmp%%"${_tmp##*[!/]}"}" + + printf '%s' "${_tmp:-/}" +} + +_fileAbsPath_() { + # DESC: + # Gets the absolute path of a file or directory + # ARGS: + # $1 (Required) - Relative path to a file or directory + # OUTS: + # 0 - Success + # 1 - If file/directory does not exist + # stdout: String relative or absolute path to file/directory + # USAGE: + # _fileAbsPath_ "../path/to/file.md" --> /home/user/docs/path/to/file.md + # CREDIT: + # https://github.com/labbots/bash-utility/ + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _input="${1}" + if [[ -f ${_input} ]]; then + printf "%s/%s\n" "$(cd "$(_fileDirectory_ "${_input}")" && pwd)" "${_input##*/}" + elif [[ -d ${_input} ]]; then + printf "%s\n" "$(cd "${_input}" && pwd)" + else + return 1 + fi +} + +_fileContains_() { + # DESC: + # Searches a file for a given pattern using default grep patterns + # ARGS: + # $1 (Required) - Input file + # $2 (Required) - Pattern to search for + # OUTS: + # 0 - Pattern found in file + # 1 - Pattern not found in file + # USAGE: + # _fileContains_ "./file.sh" "^[:alpha:]*" + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _file="$1" + local _text="$2" + grep -q "${_text}" "${_file}" } _json2yaml_() { - # DESC: Convert JSON to YAML - # ARGS: $1 (Required) - JSON file - # OUTS: None + # DESC: + # Convert JSON to YAML + # ARGS: + # $1 (Required) - JSON file + # OUTS: + # stdout: YAML from the JSON input - python -c 'import sys, yaml, json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False)' <"${1:?_json2yaml_ needs a file}" + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + python -c 'import sys, yaml, json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False)' <"${1}" +} + +_listFiles_() { + # DESC: + # Find files in a directory. Use either glob or regex + # ARGS: + # $1 (Required) - 'g|glob' or 'r|regex' + # $2 (Required) - pattern to match + # $3 (Optional) - directory (defaults to .) + # OUTS: + # stdout: List of files + # NOTE: + # Searches are NOT case sensitive and MUST be quoted + # USAGE: + # _listFiles_ glob "*.txt" "some/backup/dir" + # _listFiles_ regex ".*\.txt" "some/backup/dir" + # readarray -t array < <(_listFiles_ g "*.txt") + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _searchType="${1}" + local _pattern="${2}" + local _directory="${3:-.}" + local _fileMatch e + + case "${_searchType}" in + [Gg]*) + while read -r _fileMatch; do + printf "%s\n" "$(realpath "${_fileMatch}")" + done < <(find "${_directory}" -iname "${_pattern}" -type f -maxdepth 1 | sort) + ;; + [Rr]*) + while read -r _fileMatch; do + printf "%s\n" "$(realpath "${_fileMatch}")" + done < <(find "${_directory}" -iregex "${_pattern}" -type f -maxdepth 1 | sort) + ;; + *) + fatal "_listFiles_: Could not determine if search was glob or regex" + ;; + esac } _makeSymlink_() { - # DESC: Creates a symlink and backs up a file which may be overwritten by the new symlink. If the + # DESC: + # Creates a symlink and backs up a file which may be overwritten by the new symlink. If the # exact same symlink already exists, nothing is done. # Default behavior will create a backup of a file to be overwritten - # ARGS: $1 (Required) - Source file + # ARGS: + # $1 (Required) - Source file # $2 (Required) - Destination - # $3 (Optional) - Backup directory for files which may be overwritten (defaults to 'backup') - # OPTS: -c - Only report on new/changed symlinks. Quiet when nothing done. - # -n - Do not create a backup if target already exists - # -s - Use sudo when removing old files to make way for new symlinks - # OUTS: None - # USAGE: _makeSymlink_ "/dir/someExistingFile" "/dir/aNewSymLink" "/dir/backup/location" - # NOTE: This function makes use of the _execute_ function + # OPTS: + # -c - Only report on new/changed symlinks. Quiet when nothing done. + # -n - Do not create a backup if target already exists + # -s - Use sudo when removing old files to make way for new symlinks + # OUTS: + # 0 - Success + # 1 - Error + # Filesystem: Create's symlink if required + # USAGE: + # _makeSymlink_ "/dir/someExistingFile" "/dir/aNewSymLink" "/dir/backup/location" local opt local OPTIND=1 - local backupOriginal=true - local useSudo=false - local ONLY_SHOW_CHANGED=false + local _backupOriginal=true + local _useSudo=false + local _onlyShowChanged=false while getopts ":cCnNsS" opt; do case $opt in - n | N) backupOriginal=false ;; - s | S) useSudo=true ;; - c | C) ONLY_SHOW_CHANGED=true ;; - *) - { - error "Unrecognized option '$1' passed to _makeSymlink_" "$LINENO" - return 1 - } - ;; + n | N) _backupOriginal=false ;; + s | S) _useSudo=true ;; + c | C) _onlyShowChanged=true ;; + *) fatal "Missing required argument to ${FUNCNAME[0]}" ;; esac done shift $((OPTIND - 1)) + [ ! "$(declare -f "_backupFile_")" ] && fatal "${FUNCNAME[0]} needs function _backupFile_" + [ ! "$(declare -f "_execute_")" ] && fatal "${FUNCNAME[0]} needs function _execute_" + if ! command -v realpath >/dev/null 2>&1; then error "We must have 'realpath' installed and available in \$PATH to run." if [[ $OSTYPE == "darwin"* ]]; then @@ -362,248 +583,262 @@ _makeSymlink_() { _safeExit_ 1 fi - [[ $# -lt 2 ]] && fatal 'Missing required argument to _makeSymlink_()!' + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - local s="$1" - local d="$2" - local b="${3:-}" - local o + local _sourceFile="$1" + local _destinationFile="$2" + local _originalFile # Fix files where $HOME is written as '~' - d="${d/\~/$HOME}" - s="${s/\~/$HOME}" - b="${b/\~/$HOME}" + _destinationFile="${_destinationFile/\~/$HOME}" + _sourceFile="${_sourceFile/\~/$HOME}" - [ ! -e "$s" ] \ + [ ! -e "$_sourceFile" ] \ && { - error "'$s' not found" + error "'${_sourceFile}' not found" return 1 } - [ -z "$d" ] \ + [ -z "${_destinationFile}" ] \ && { - error "'${d}' not specified" - return 1 - } - [ ! "$(declare -f "_execute_")" ] \ - && { - echo "need function _execute_" - return 1 - } - [ ! "$(declare -f "_backupFile_")" ] \ - && { - echo "need function _backupFile_" + error "'${_destinationFile}' not specified" return 1 } # Create destination directory if needed - [ ! -d "${d%/*}" ] \ - && _execute_ "mkdir -p \"${d%/*}\"" + [ ! -d "${_destinationFile%/*}" ] \ + && _execute_ "mkdir -p \"${_destinationFile%/*}\"" - if [ ! -e "${d}" ]; then - _execute_ "ln -fs \"${s}\" \"${d}\"" "symlink ${s} → ${d}" - elif [ -h "${d}" ]; then - o="$(realpath "${d}")" + if [ ! -e "${_destinationFile}" ]; then + _execute_ "ln -fs \"${_sourceFile}\" \"${_destinationFile}\"" "symlink ${_sourceFile} → ${_destinationFile}" + elif [ -h "${_destinationFile}" ]; then + _originalFile="$(realpath "${_destinationFile}")" - [[ ${o} == "${s}" ]] && { - - if [ ${ONLY_SHOW_CHANGED} == true ]; then - debug "Symlink already exists: ${s} → ${d}" + [[ ${_originalFile} == "${_sourceFile}" ]] && { + if [ ${_onlyShowChanged} == true ]; then + debug "Symlink already exists: ${_sourceFile} → ${_destinationFile}" elif [ "${DRYRUN}" == true ]; then - dryrun "Symlink already exists: ${s} → ${d}" + dryrun "Symlink already exists: ${_sourceFile} → ${_destinationFile}" else - info "Symlink already exists: ${s} → ${d}" + info "Symlink already exists: ${_sourceFile} → ${_destinationFile}" fi return 0 } - if [[ ${backupOriginal} == true ]]; then - _backupFile_ "${d}" "${b:-backup}" + if [[ ${_backupOriginal} == true ]]; then + _backupFile_ "${_destinationFile}" fi if [[ ${DRYRUN} == false ]]; then - if [[ ${useSudo} == true ]]; then - command rm -rf "${d}" + if [[ ${_useSudo} == true ]]; then + command rm -rf "${_destinationFile}" else - command rm -rf "${d}" + command rm -rf "${_destinationFile}" fi fi - _execute_ "ln -fs \"${s}\" \"${d}\"" "symlink ${s} → ${d}" - elif [ -e "${d}" ]; then - if [[ ${backupOriginal} == true ]]; then - _backupFile_ "${d}" "${b:-backup}" + _execute_ "ln -fs \"${_sourceFile}\" \"${_destinationFile}\"" "symlink ${_sourceFile} → ${_destinationFile}" + elif [ -e "${_destinationFile}" ]; then + if [[ ${_backupOriginal} == true ]]; then + _backupFile_ "${_destinationFile}" fi if [[ ${DRYRUN} == false ]]; then - if [[ ${useSudo} == true ]]; then - sudo command rm -rf "${d}" + if [[ ${_useSudo} == true ]]; then + sudo command rm -rf "${_destinationFile}" else - command rm -rf "${d}" + command rm -rf "${_destinationFile}" fi fi - _execute_ "ln -fs \"${s}\" \"${d}\"" "symlink ${s} → ${d}" + _execute_ "ln -fs \"${_sourceFile}\" \"${_destinationFile}\"" "symlink ${_sourceFile} → ${_destinationFile}" else - warning "Error linking: ${s} → ${d}" + warning "Error linking: ${_sourceFile} → ${_destinationFile}" return 1 fi return 0 } +_parseFilename_() { + # DESC: + # Break a filename into its component parts which and place them into prefixed + # variables for use in your script. Run with VERBOSE=true to see the variables while + # running your script. + # ARGS: + # $1 (Required) - Path to file to parse. (Must exist in filesystem) + # OPTS: + # -n - optional flag for number of extension levels (Ex: -n2) + # OUTS: + # 0 - Success + # 1 - Error + # Variables created + # $PARSE_FULL - File and its real path (ie, resolve symlinks) + # $PARSE_PATH - Path to the file + # $PARSE_BASE - Name of the file WITH extension + # $PARSE_BASENOEXT - Name of file WITHOUT extension + # $PARSE_EXT - The extension of the file + # USAGE: + # _parseFilename_ "some/file.txt" + + # Error handling + if [[ $# -lt 1 ]] \ + || ! command -v dirname &>/dev/null \ + || ! command -v basename &>/dev/null \ + || ! command -v realpath &>/dev/null; then + fatal "Missing dependency or input to ${FUNCNAME[0]}" + fi + + local _levels + local option + local _exts + local _ext + local i + local _fn + + local OPTIND=1 + while getopts ":n:" option; do + case ${option} in + n) _levels=${OPTARG} ;; + *) continue ;; + esac + done && shift $((OPTIND - 1)) + + local _fileToParse="${1}" + + if [ ! -f "${_fileToParse}" ]; then + debug "_parseFile_: Could not find file: ${_fileToParse}" + return 1 + fi + + PARSE_FULL="$(realpath "${_fileToParse}")" \ + && debug "\${PARSE_FULL}: ${PARSE_FULL:-}" + PARSE_BASE=$(basename "${_fileToParse}") \ + && debug "\${PARSE_BASE}: ${PARSE_BASE}" + PARSE_PATH="$(realpath "$(dirname "${_fileToParse}")")" \ + && debug "\${PARSE_PATH}: ${PARSE_PATH:-}" + + # Detect some common multi-extensions + if [[ -z ${_levels:-} ]]; then + case $(tr '[:upper:]' '[:lower:]' <<<"${PARSE_BASE}") in + *.tar.gz | *.tar.bz2 | *.log.[0-9]) _levels=2 ;; + *) _levels=1 ;; + esac + fi + + # Find Extension + _fn="${PARSE_BASE}" + for ((i = 0; i < _levels; i++)); do + _ext=${_fn##*.} + if [ $i == 0 ]; then + _exts=${_ext}${_exts:-} + else + _exts=${_ext}.${_exts:-} + fi + _fn=${_fn%.$_ext} + done + if [[ ${_exts} == "${PARSE_BASE}" ]]; then + PARSE_EXT="" && debug "\${PARSE_EXT}: ${PARSE_EXT}" + else + PARSE_EXT="${_exts}" && debug "\${PARSE_EXT}: ${PARSE_EXT}" + fi + + PARSE_BASENOEXT="${PARSE_BASE%.$PARSE_EXT}" \ + && debug "\${PARSE_BASENOEXT}: ${PARSE_BASENOEXT}" +} + _parseYAML_() { - # DESC: Convert a YAML file into BASH variables for use in a shell script - # ARGS: $1 (Required) - Source YAML file + # DESC: + # Convert a YAML file into BASH variables for use in a shell script + # ARGS: + # $1 (Required) - Source YAML file # $2 (Required) - Prefix for the variables to avoid namespace collisions - # OUTS: Prints variables and arrays derived from YAML File - # USAGE: To source into a script + # OUTS: + # Prints variables and arrays derived from YAML File + # USAGE: + # To source into a script # _parseYAML_ "sample.yml" "CONF_" > tmp/variables.txt # source "tmp/variables.txt" # - # NOTE: https://gist.github.com/DinoChiesa/3e3c3866b51290f31243 + # NOTE: + # https://gist.github.com/DinoChiesa/3e3c3866b51290f31243 # https://gist.github.com/epiloque/8cf512c6d64641bde388 - local yamlFile="${1:?_parseYAML_ needs a file}" - local prefix="${2:-}" + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" - [ ! -s "${yamlFile}" ] && return 1 + local _yamlFile="${1}" + local _prefix="${2:-}" - local s='[[:space:]]*' - local w='[a-zA-Z0-9_]*' - local fs="$(echo @ | tr @ '\034')" - sed -ne "s|^\(${s}\)\(${w}\)${s}:${s}\"\(.*\)\"${s}\$|\1${fs}\2${fs}\3|p" \ - -e "s|^\(${s}\)\(${w}\)${s}[:-]${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "${yamlFile}" \ - | awk -F"${fs}" '{ + [ ! -s "${_yamlFile}" ] && return 1 + + local _s='[[:space:]]*' + local _w='[a-zA-Z0-9_]*' + local _fs="$(echo @ | tr @ '\034')" + sed -ne "s|^\(${_s}\)\(${_w}\)${_s}:${_s}\"\(.*\)\"${_s}\$|\1${_fs}\2${_fs}\3|p" \ + -e "s|^\(${_s}\)\(${_w}\)${_s}[:-]${_s}\(.*\)${_s}\$|\1${_fs}\2${_fs}\3|p" "${_yamlFile}" \ + | awk -F"${_fs}" '{ indent = length($1)/2; if (length($2) == 0) { conj[indent]="+";} else {conj[indent]="";} vname[indent] = $2; for (i in vname) {if (i > indent) {delete vname[i]}} if (length($3) > 0) { vn=""; for (i=0; i/dev/null 2>&1; then - error "We must have 'realpath' installed and available in \$PATH to run." - if [[ $OSTYPE == "darwin"* ]]; then - notice "Install coreutils using homebrew and rerun this script." - info "\t$ brew install coreutils" - fi - _safeExit_ 1 + if source "${_fileToSource}"; then + return 0 + else + fatal "Failed to source: ${_fileToSource}" fi - - # Find directories with realpath if input is an actual file - if [ -e "${fullfile}" ]; then - fullfile="$(realpath "${fullfile}")" - fi - - directory="$(dirname "${fullfile}")" - filename="$(basename "${fullfile}")" - - # Extract extensions only when they exist - if [[ "${filename}" =~ \.[a-zA-Z]{2,4}$ ]]; then - extension=".${filename##*.}" - filename="${filename%.*}" - fi - if [[ "${filename}" == "${extension:-}" ]]; then - extension="" - fi - - newfile="${directory}/${filename}${extension:-}" - - if [ -e "${newfile}" ]; then - num=1 - if [ "${INTERNAL_INTEGER}" = true ]; then - while [[ -e "${directory}/${filename}${spacer}${num}${extension:-}" ]]; do - ((num++)) - done - newfile="${directory}/${filename}${spacer}${num}${extension:-}" - else - while [[ -e "${directory}/${filename}${extension:-}${spacer}${num}" ]]; do - ((num++)) - done - newfile="${directory}/${filename}${extension:-}${spacer}${num}" - fi - fi - - echo "${newfile}" - return 0 } _yaml2json_() { - # DESC: Convert a YAML file to JSON - # ARGS: $1 (Required) - Input YAML file - # OUTS: None + # DESC: + # Convert a YAML file to JSON + # ARGS: + # $1 (Required) - Input YAML file + # OUTS: + # stdout: JSON + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=4)' <"${1:?_yaml2json_ needs a file}" } diff --git a/utilities/macOS.bash b/utilities/macOS.bash index 35e40c9..dea8126 100644 --- a/utilities/macOS.bash +++ b/utilities/macOS.bash @@ -1,14 +1,18 @@ # Functions for use on computers running MacOS _haveScriptableFinder_() { - # DESC: Determine whether we can script the Finder or not - # ARGS: None - # OUTS: true/false + # DESC: + # Determine whether we can script the Finder or not + # ARGS: + # None + # OUTS: + # 0 if we can script the Finder + # 1 if not - local finder_pid - finder_pid="$(pgrep -f /System/Library/CoreServices/Finder.app | head -n 1)" + local _finder_pid + _finder_pid="$(pgrep -f /System/Library/CoreServices/Finder.app | head -n 1)" - if [[ (${finder_pid} -gt 1) && (${STY-} == "") ]]; then + if [[ (${_finder_pid} -gt 1) && (${STY-} == "") ]]; then return 0 else return 1 @@ -16,24 +20,56 @@ _haveScriptableFinder_() { } _guiInput_() { - # DESC: Ask for user input using a Mac dialog box - # ARGS: $1 (Optional) - Text in dialogue box (Default: Password) - # OUTS: None - # NOTE: https://github.com/herrbischoff/awesome-osx-command-line/blob/master/functions.md + # DESC: + # Ask for user input using a Mac dialog box + # ARGS: + # $1 (Optional) - Text in dialogue box (Default: Password) + # OUTS: + # MacOS dialog box output + # 1 if no output + # CREDIT + # https://github.com/herrbischoff/awesome-osx-command-line/blob/master/functions.md if _haveScriptableFinder_; then - guiPrompt="${1:-Password:}" - guiInput=$( - osascript &>/dev/null </dev/null <