diff --git a/docs/upgrade/v4-to-v5.md b/docs/upgrade/v4-to-v5.md index 728babf..a9ca396 100644 --- a/docs/upgrade/v4-to-v5.md +++ b/docs/upgrade/v4-to-v5.md @@ -28,8 +28,8 @@ ABORT >> utils.abort CHECK_ERRORS >> utils.check-errors -Yn >> utils.Yn -yN >> utils.yN +Yn >> user.Yn +yN >> user.yN EDIT >> utils.io.edit diff --git a/plugins/kube/driver/kubectl.completion.zsh b/plugins/kube/driver/kubectl.completion.zsh index 251b3dc..75d9cb3 100644 --- a/plugins/kube/driver/kubectl.completion.zsh +++ b/plugins/kube/driver/kubectl.completion.zsh @@ -1,5 +1,5 @@ ##################################################################### -command -v compdef >/dev/null 2>&1 || return 0 +command -v compdef &>/dev/null || return 0 ##################################################################### for CLI in kubectl helm flux diff --git a/plugins/kube/driver/kubectl.driver.zsh b/plugins/kube/driver/kubectl.driver.zsh index 892cab5..e235933 100644 --- a/plugins/kube/driver/kubectl.driver.zsh +++ b/plugins/kube/driver/kubectl.driver.zsh @@ -1,6 +1,6 @@ [[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0 -unalias k h f >/dev/null 2>&1 +unalias k h f &>/dev/null k() { _SCWRYPTS_KUBECTL_DRIVER kubectl $@; } h() { _SCWRYPTS_KUBECTL_DRIVER helm $@; } f() { _SCWRYPTS_KUBECTL_DRIVER flux $@; } @@ -11,7 +11,7 @@ _SCWRYPTS_KUBECTL_DRIVER() { return 1 } - which kube.redis >/dev/null 2>&1 \ + which kube.redis &>/dev/null \ || eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kube)" local CLI="$1"; shift 1 diff --git a/plugins/kube/redis/redis.module.zsh b/plugins/kube/redis/redis.module.zsh index 4d0c385..21c3a76 100644 --- a/plugins/kube/redis/redis.module.zsh +++ b/plugins/kube/redis/redis.module.zsh @@ -87,7 +87,7 @@ kube.redis ping 2>/dev/null | grep -qi pong || { --detach \ --name scwrypts-kubectl-redis \ --publish $SCWRYPTS_KUBECTL_REDIS_PORT__managed:6379 \ - redis >/dev/null 2>&1 + redis &>/dev/null echo.status 'awaiting redis connection' until kube.redis ping 2>/dev/null | grep -qi pong; do sleep 0.5; done diff --git a/scwrypts b/scwrypts index 59215dd..82d0bd7 100755 --- a/scwrypts +++ b/scwrypts @@ -7,13 +7,18 @@ use scwrypts/environment use scwrypts/list-available use scwrypts/get-runstring +[[ "${SCWRYPTS_TEMP_PATH}" ]] && [[ -d "${SCWRYPTS_TEMP_PATH}" ]] \ + && EXIT_CODE_FILE="${SCWRYPTS_TEMP_PATH}/exit-code" \ + || EXIT_CODE_FILE="$(mktemp)" \ + ; + ##################################################################### () { cd "$(scwrypts.config.group scwrypts root)" - GIT_SCWRYPTS() { git -C "$(scwrypts.config.group scwrypts root)" $@; } + git.scwrypts() { git -C "$(scwrypts.config.group scwrypts root)" "${@}"; } local ERRORS=0 - local USAGE=' + local USAGE=" usage: scwrypts [...options...] [...patterns...] -- [...script options...] options: @@ -27,22 +32,23 @@ use scwrypts/get-runstring -y, --yes auto-accept all [yn] prompts through current scwrypt -e, --env set environment; overwrites SCWRYPTS_ENV - -n shorthand for "--log-level 0" - -v, --log-level <0-4> set incremental scwrypts log level to one of the following: + -n shorthand for \"--log-level 0\" + -v, --log-level <0-5> set incremental scwrypts log level to one of the following: 0 : only command output and critical failures; skips logfile - 1 : include success / failure messages - 2 : include status update messages - 3 : (default) include warning messages + 1 : include error messages + 2 : include warnings + 3 : (default) include success, status, and user information 4 : include debug messages + 5 : include trace - -o, --output specify output format; one of: pretty,json (default: pretty) + -o, --output specify output format; one of: pretty,json,raw,logfmt (default: pretty) alternate commands -h, --help display this message and exit -l, --list print out command list and exit --list-envs print out environment list and exit --list-groups print out configured scwrypts groups and exit - --config "eval"-ed to enable config and "use" import in non-scwrypts environments + --config \"eval\"-ed to enable config and \"use\" import in non-scwrypts environments --root print out scwrypts.config.group.scwrypts.root and exit --update update scwrypts library to latest version --version print out scwrypts version and exit @@ -51,29 +57,40 @@ use scwrypts/get-runstring - a list of glob patterns to loose-match a scwrypt by name script options: - - everything after "--" is forwarded to the scwrypt you run - ("-- --help" will provide more information) - ' + - everything after \"--\" is forwarded to the scwrypt you run + (\"-- --help\" will provide more information) + " ##################################################################### ### cli argument parsing and global configuration ################### ##################################################################### - local ENV_NAME="${SCWRYPTS_ENV}" - local SEARCH_PATTERNS=() + local target_scwrypts_env="${SCWRYPTS_ENV}" + local search_patterns=() - local VARSPLIT SEARCH_GROUP SEARCH_TYPE SEARCH_NAME + local search_group search_type search_name - [ ! ${SCWRYPTS_LOG_LEVEL} ] && local SCWRYPTS_LOG_LEVEL=3 + [[ "${SCWRYPTS_LOG_LEVEL}" ]] || local SCWRYPTS_LOG_LEVEL=3 - local SHIFT_COUNT + local _s while [[ $# -gt 0 ]] do - SHIFT_COUNT=1 + _s=1 case $1 in ( -[a-z][a-z]* ) - VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/') - set -- throw-away $(echo " ${VARSPLIT} ") ${@:2} + local varsplit_first="${1:0:2}" varsplit_remaining="-${1:2}" + shift 1 + set -- "${varsplit_first}" "${varsplit_remaining}" "${@}" + unset varsplit_first varsplit_remaining + _s=0 + ;; + + ( --*=* ) + local varsplit_first="${1%%=*}" varsplit_remaining="${1#*=}" + shift 1 + set -- "${varsplit_first}" "${varsplit_remaining}" "${@}" + unset varsplit_first varsplit_remaining + _s=0 ;; ### alternate commands ################### @@ -99,8 +116,8 @@ use scwrypts/get-runstring ;; ( --version ) - case ${SCWRYPTS_INSTALLATION_TYPE} in - ( manual ) echo "scwrypts $(GIT_SCWRYPTS describe --tags) (via GIT)" ;; + case "${SCWRYPTS_INSTALLATION_TYPE}" in + ( manual ) echo "scwrypts $(git.scwrypts describe --tags) (via GIT)" ;; ( * ) echo "scwrypts $(cat "$(scwrypts.config.group scwrypts root)/VERSION")" ;; esac return 0 @@ -119,34 +136,34 @@ use scwrypts/get-runstring ;; ( --update ) - case ${SCWRYPTS_INSTALLATION_TYPE} in - aur ) + case "${SCWRYPTS_INSTALLATION_TYPE}" in + ( aur ) echo.reminder --force-print " This installation is built from the AUR. Update through 'makepkg' or use your preferred AUR package management tool (e.g. 'yay -Syu scwrypts') " ;; - homebrew ) + ( homebrew ) echo.reminder --force-print "This installation is managed by homebrew. Update me with 'brew update'" ;; - manual ) - GIT_SCWRYPTS fetch --quiet origin main - GIT_SCWRYPTS fetch --quiet origin main --tags - local SYNC_STATUS=$? + ( manual ) + git.scwrypts fetch --quiet origin main + git.scwrypts fetch --quiet origin main --tags + local sync_status="${?}" - GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 - local DIFF_STATUS=$? + git.scwrypts diff --exit-code origin/main -- . &>/dev/null + local diff_status="${?}" - [[ ${SYNC_STATUS} -eq 0 ]] && [[ ${DIFF_STATUS} -eq 0 ]] && { + [[ "${sync_status}" -eq 0 ]] && [[ "${diff_status}" -eq 0 ]] && { echo.success 'already up-to-date with origin/main' } || { - GIT_SCWRYPTS rebase --autostash origin/main \ + git.scwrypts rebase --autostash origin/main \ && echo.success 'up-to-date with origin/main' \ - && GIT_SCWRYPTS log -n1 \ + && git.scwrypts log -n1 \ || { - GIT_SCWRYPTS rebase --abort + git.scwrypts rebase --abort echo.error 'unable to update scwrypts; please try manual upgrade' echo.reminder "installation in '$(scwrypts.config.group scwrypts root)'" } @@ -165,25 +182,9 @@ use scwrypts/get-runstring ### scwrypts filters ##################### - ( -m | --name ) - ((SHIFT_COUNT+=1)) - [ $2 ] || { echo.error "missing value for argument $1"; break; } - SEARCH_NAME=$2 - ;; - - ( -g | --group ) - ((SHIFT_COUNT+=1)) - [ $2 ] || { echo.error "missing value for argument $1"; break; } - SEARCH_GROUP=$2 - GROUP=$2 - ;; - - ( -t | --type ) - ((SHIFT_COUNT+=1)) - [ $2 ] || { echo.error "missing value for argument $1"; break; } - SEARCH_TYPE=$2 - TYPE=$2 - ;; + ( -m | --name ) _s=2; search_name=$2 ;; + ( -g | --group ) _s=2; search_group=$2; GROUP=$2 ;; + ( -t | --type ) _s=2; search_type=$2; TYPE=$2 ;; ### runtime settings ##################### @@ -191,49 +192,46 @@ use scwrypts/get-runstring ( -n ) SCWRYPTS_LOG_LEVEL=0 ;; - ( -v | --log-level ) - ((SHIFT_COUNT+=1)) - [[ $2 =~ ^[0-4]$ ]] || echo.error "invalid setting for log-level '$2'" - SCWRYPTS_LOG_LEVEL=$2 + ( -v | --log-level ) _s=2 + SCWRYPTS_LOG_LEVEL="${2}" + + # allows CLI user to use the word lookup (e.g. "debug" or "trace") + # number is preferred, but this is here for user ease + [[ "${SCWRYPTS_LOG_LEVEL}" =~ ^[0-${#SCWRYPTS_LOG_LEVELS[@]}]$ ]] \ + || SCWRYPTS_LOG_LEVEL="${(k)SCWRYPTS_LOG_LEVELS[(r)${SCWRYPTS_LOG_LEVEL:l}]}" + + [[ "${SCWRYPTS_LOG_LEVEL}" =~ ^[0-${#SCWRYPTS_LOG_LEVELS[@]}]$ ]] \ + || echo.error "invalid setting for log-level '${2}'" ;; - ( -o | --output ) - ((SHIFT_COUNT+=1)) - export SCWRYPTS_OUTPUT_FORMAT=$2 - case ${SCWRYPTS_OUTPUT_FORMAT} in - ( pretty | json ) ;; - * ) echo.error "unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" ;; - esac + ( -o | --output ) _s=2 + export SCWRYPTS_OUTPUT_FORMAT="${2:l}" + utils.io.print 'test' --force-print &>/dev/null \ + || echo.error "unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" ;; - ( -e | --env ) - ((SHIFT_COUNT+=1)) - [ $2 ] || { echo.error "missing value for argument $1"; break; } + ( -e | --env ) _s=2 + [[ "${target_scwrypts_env}" ]] \ + && echo.debug "overwriting session environment to ${2}" -v target_scwrypts_env - [ ${ENV_NAME} ] && echo.debug 'overwriting session environment' - - ENV_NAME="$2" - echo.status "using CLI environment '${ENV_NAME}'" + target_scwrypts_env="${2}" ;; ########################################## ( -- ) shift 1; break ;; # pass arguments after '--' to the scwrypt - ( --* ) echo.error "unrecognized argument '$1'" ;; - ( * ) SEARCH_PATTERNS+=($1) ;; + ( --* ) echo.error "unrecognized argument '${1}'" ;; + ( * ) search_patterns+=(${1}) ;; esac - [[ ${SHIFT_COUNT} -le $# ]] \ - && shift ${SHIFT_COUNT} \ - || echo.error "missing argument for '$1'" \ - || shift $# \ - ; + + shift "${_s}" 2>/dev/null || echo.error "missing argument for '${1}'" || shift "${#}" done - [ ${SCWRYPTS_OUTPUT_FORMAT} ] || export SCWRYPTS_OUTPUT_FORMAT=pretty + [[ "${SCWRYPTS_OUTPUT_FORMAT}" ]] || export SCWRYPTS_OUTPUT_FORMAT=pretty - [ ${SEARCH_NAME} ] && { - [ ${SEARCH_TYPE} ] || echo.error '--name requires --type argument' - [ ${SEARCH_GROUP} ] || echo.error '--name requires --group argument' + [[ "${search_name}" ]] && { + [[ "${search_type}" ]] || echo.error '--name requires --type argument' + [[ "${search_group}" ]] || echo.error '--name requires --group argument' } utils.check-errors --fail @@ -243,283 +241,336 @@ use scwrypts/get-runstring ### scwrypts selection / filtering ################################## ##################################################################### - local SCWRYPTS_AVAILABLE=$(scwrypts.list-available) + local scwrypts_available="$(scwrypts.list-available)" - ########################################## + local scwrypts_available_header="$(echo "${scwrypts_available}" | head -n1)" + local scwrypts_available="$(echo "${scwrypts_available}" | sed '1d')" - [ ${SEARCH_NAME} ] && SCWRYPTS_AVAILABLE=$({ - echo ${SCWRYPTS_AVAILABLE} | head -n1 - echo ${SCWRYPTS_AVAILABLE} | utils.colors.remove | grep "^${SEARCH_NAME} *${SEARCH_TYPE} *${SEARCH_GROUP}\$" - }) || { - [ ${SEARCH_TYPE} ] && { - SCWRYPTS_AVAILABLE=$(\ - { - echo ${SCWRYPTS_AVAILABLE} | head -n1 - echo ${SCWRYPTS_AVAILABLE} | grep ' [^/]*'${SEARCH_TYPE}'[^/]* ' - } \ + [[ "${search_name}" ]] && scwrypts_available="$({ + echo "${scwrypts_available}" | utils.colors.remove | grep -- "^${search_name} *${search_type} *${search_group}\$" + })" || { + [[ "${search_type}" ]] && scwrypts_available="$(\ + echo "${scwrypts_available}" \ + | grep -- ' [^/]*'${search_type}'[^/]* ' \ | sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \ - | column -ts '^' - ) - } + | column -ts '^' \ + )" - [ ${SEARCH_GROUP} ] && { - SCWRYPTS_AVAILABLE=$( - { - echo ${SCWRYPTS_AVAILABLE} | head -n1 - echo ${SCWRYPTS_AVAILABLE} | grep "${SEARCH_GROUP}"'[^/ ]*$' - } \ + [[ "${search_group}" ]] && scwrypts_available="$( + echo "${scwrypts_available}" \ + | grep -- "${search_group}"'[^/ ]*$' \ | sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \ - | column -ts '^' - ) - } + | column -ts '^' \ + )" - [[ ${#SEARCH_PATTERNS[@]} -gt 0 ]] && { - POTENTIAL_ERROR+="\n PATTERNS : ${SEARCH_PATTERNS}" - local P - for P in ${SEARCH_PATTERNS[@]} + [[ "${#search_patterns[@]}" -gt 0 ]] && { + local search_pattern + for search_pattern in "${search_patterns[@]}" do - SCWRYPTS_AVAILABLE=$( - { - echo ${SCWRYPTS_AVAILABLE} | head -n1 - echo ${SCWRYPTS_AVAILABLE} | grep ${P} - } - ) + scwrypts_available="$(echo "${scwrypts_available}" | grep -- "${search_pattern}")" done + unset search_pattern } } - [[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -lt 2 ]] && { - utils.fail 1 "$(echo " - no such scwrypt exists - NAME : '${SEARCH_NAME}' - TYPE : '${SEARCH_TYPE}' - GROUP : '${SEARCH_GROUP}' - PATTERNS : '${SEARCH_PATTERNS}' - " | sed "1d; \$d; /''$/d")" - } + [[ $(echo "${scwrypts_available}" | wc -l) -gt 0 ]] \ + || echo.error "$(echo " + no such scwrypt exists + NAME : '${search_name}' + TYPE : '${search_type}' + GROUP : '${search_group}' + PATTERNS : '${search_patterns[@]}' + " | sed "1d; \$d; /''$/d")" \ + || return 1 ########################################## - [[ $(echo ${SCWRYPTS_AVAILABLE} | wc -l) -eq 2 ]] \ - && SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | tail -n1) \ - || SCWRYPT_SELECTION=$(echo ${SCWRYPTS_AVAILABLE} | utils.fzf "select a script to run" --header-lines 1) \ + [[ "$(echo "${scwrypts_available}" | wc -l)" -eq 1 ]] \ + && scwrypt_selection="$(echo "${scwrypts_available}" | tail -n1)" \ + || scwrypt_selection="$(echo "${scwrypts_available_header}\n${scwrypts_available}" | utils.fzf "select a script to run" --header-lines 1)" \ ; - [ ${SCWRYPT_SELECTION} ] || utils.abort + [[ "${scwrypt_selection}" ]] || utils.abort ########################################## - () { - set -- $(echo $@ | utils.colors.remove) - export SCWRYPT_NAME=$1 - export SCWRYPT_TYPE=$2 - export SCWRYPT_GROUP=$3 - } ${SCWRYPT_SELECTION} - + echo "${scwrypt_selection}" | utils.colors.remove | read -r SCWRYPT_NAME SCWRYPT_TYPE SCWRYPT_GROUP + export SCWRYPT_NAME SCWRYPT_TYPE SCWRYPT_GROUP ##################################################################### ### environment variables and configuration validation ############## ##################################################################### - local ENV_REQUIRED=true \ - && [ ! ${CI} ] \ - && [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \ - && [[ ! ${SCWRYPT_NAME} =~ scwrypts/environment ]] \ - || ENV_REQUIRED=false + local env_required \ + && [[ ! "${CI}" ]] \ + && [[ ! "${SCWRYPT_NAME}" =~ scwrypts/logs ]] \ + && [[ ! "${SCWRYPT_NAME}" =~ scwrypts/environment ]] \ + && env_required=true \ + || env_required=false \ + ; - local REQUIRED_ENVIRONMENT_REGEX="$(scwrypts.config.group ${SCWRYPT_GROUP} required_environment_regex)" + local required_environment_regex="$(scwrypts.config.group "${SCWRYPT_GROUP}" required_environment_regex)" - [ ${ENV_NAME} ] && [ ${REQUIRED_ENVIRONMENT_REGEX} ] && { - [[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \ - || utils.fail 5 "group '${SCWRYPT_GROUP}' requires current environment name to match '${REQUIRED_ENVIRONMENT_REGEX}' (currently ${ENV_NAME})" + [[ "${target_scwrypts_env}" ]] && [[ "${required_environment_regex}" ]] && { + [[ "${target_scwrypts_env}" =~ "${required_environment_regex}" ]] \ + || echo.error "group '${SCWRYPT_GROUP}' requires current environment name to match '${required_environment_regex}' (currently ${target_scwrypts_env})" \ + || return 5 } - [[ ${ENV_REQUIRED} =~ true ]] && { - [ ! ${ENV_NAME} ] && { - scwrypts.environment.init \ - || echo.error "failed to initialize scwrypts environments (see above)" \ - || return 1 \ + case "${env_required}" in + ( false ) ;; + ( true ) + [[ "${target_scwrypts_env}" ]] || { + scwrypts.environment.init \ + || echo.error "failed to initialize scwrypts environments (see above)" \ + || return 1 + + target_scwrypts_env="$(scwrypts.environment.select-env)" + [[ "${target_scwrypts_env}" ]] || echo.error.user-abort || return 1 + } + + local group required_group_regex + for group in ${SCWRYPTS_GROUPS[@]} + do + required_group_regex="$(scwrypts.config.group ${group} required_environment_regex)" + [[ "${required_group_regex}" ]] && { + [[ "${target_scwrypts_env}" =~ ${required_group_regex} ]] || continue + } + + for f in $(find "$(scwrypts.config.group ${group} root)/.config/static" -type f 2>/dev/null) + do + source "${f}" || echo.error "invalid static config '${f}'" || return 5 + done + done + unset group required_group_regex + ;; + esac + + export SCWRYPTS_ENV="${target_scwrypts_env}" + + ########################################## + + [[ ! "${SUBSCWRYPT}" ]] && export SUBSCWRYPT=0 + + local scwrypts_execution_warning_message + case "${SCWRYPTS_INSTALLATION_TYPE}" in + ( manual ) + local sync_with_main_required \ + && [[ "${SUBSCWRYPT}" -eq 0 ]] \ + && [[ "${SCWRYPTS_ENV}" =~ prod ]] \ + && [[ "${SCWRYPTS_LOG_LEVEL}" -gt 0 ]] \ + && sync_with_main_required=true \ + || sync_with_main_required=false \ ; - ENV_NAME=$(scwrypts.environment.select-env) - [ "${ENV_NAME}" ] || user.abort - } + case "${sync_with_main_required}" in + ( false ) ;; + ( true ) + echo.status "on '${SCWRYPTS_ENV}'; checking diff against origin/main" - for GROUP in ${SCWRYPTS_GROUPS[@]} - do - local REQUIRED_REGEX="$(scwrypts.config.group ${GROUP} required_environment_regex)" - [ ${REQUIRED_REGEX} ] && { - [[ ${ENV_NAME} =~ ${REQUIRED_REGEX} ]] || continue - } - - for f in $(find "$(scwrypts.config.group ${GROUP} root)/.config/static" -type f 2>/dev/null) - do - source "${f}" || utils.fail 5 "invalid static config '${f}'" - done - done - } - - [ ${REQUIRED_ENVIRONMENT_REGEX} ] && { - [[ ${ENV_NAME} =~ ${REQUIRED_ENVIRONMENT_REGEX} ]] \ - || utils.fail 5 "group '${SCWRYPT_GROUP}' requires current environment name to match '${REQUIRED_ENVIRONMENT_REGEX}' (currently ${ENV_NAME})" - } - - export SCWRYPTS_ENV=${ENV_NAME} - - ########################################## - - [ ! ${SUBSCWRYPT} ] && export SUBSCWRYPT=0 - - [[ ${SCWRYPTS_INSTALLATION_TYPE} =~ ^manual$ ]] && { - [[ ${SUBSCWRYPT} -eq 0 ]] && [[ ${SCWRYPTS_ENV} =~ prod ]] && [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] && { - echo.status "on '${SCWRYPTS_ENV}'; checking diff against origin/main" - - local WARNING_MESSAGE - - [ ! ${WARNING_MESSAGE} ] && { - GIT_SCWRYPTS fetch --quiet origin main \ - || WARNING_MESSAGE='I am unable to verify your scwrypts version' - } - - [ ! ${WARNING_MESSAGE} ] && { - GIT_SCWRYPTS diff --exit-code origin/main -- . >/dev/null 2>&1 \ - || WARNING_MESSAGE='your scwrypts is currently out-of-date' - } - - [ ${WARNING_MESSAGE} ] && { - [[ ${SCWRYPTS_LOG_LEVEL} -lt 3 ]] && { - echo.reminder "you are running in $(utils.colors.bright-red)production$(utils.colors.bright-magenta) and ${WARNING_MESSAGE}" - } || { - GIT_SCWRYPTS diff --exit-code origin/main -- . >&2 - echo.warning "you are trying to run in $(utils.colors.bright-red)production$(echo.warning.color) but ${WARNING_MESSAGE} (relevant diffs and errors above)" - yN 'continue?' || { - echo.reminder "you can use 'scwrypts --update' to quickly update scwrypts to latest" - user.abort + [[ ! "${scwrypts_execution_warning_message}" ]] && { + git.scwrypts fetch --quiet origin main &>/dev/null \ + || scwrypts_execution_warning_message='I am unable to verify your scwrypts version' } - } - } - } + + [[ ! "${scwrypts_execution_warning_message}" ]] && { + git.scwrypts diff --exit-code origin/main -- . &>/dev/null \ + || scwrypts_execution_warning_message='your scwrypts is currently out-of-date' + } + ;; + esac + ;; + esac + + [[ "${scwrypts_execution_warning_message}" ]] && { + case "${SCWRYPTS_LOG_LEVEL}" in + ( 0 | 1 | 2 ) + echo.warning " + you are running in $(utils.colors.bright-red)production$(echo.warning.color), + and ${scwrypts_execution_warning_message} + " + ;; + ( * ) + git.scwrypts diff --exit-code origin/main -- . >&2 + echo.warning "you are trying to run in $(utils.colors.bright-red)production$(echo.warning.color) but ${scwrypts_execution_warning_message} (relevant diffs and errors above)" + yN 'continue?' || echo.error.user-abort || return 1 + ;; + esac } ########################################## - local RUN_STRING=$(scwrypts.get-runstring ${SCWRYPT_NAME} ${SCWRYPT_TYPE} ${SCWRYPT_GROUP}) - [ "${RUN_STRING}" ] || return 42 + local scwrypts_run_string="$(scwrypts.get-runstring "${SCWRYPT_NAME}" "${SCWRYPT_TYPE}" "${SCWRYPT_GROUP}")" + [[ "${scwrypts_run_string}" ]] || return 42 ##################################################################### ### logging and pretty header/footer setup ########################## ##################################################################### - local RUN_MODE=normal - [[ ${SCWRYPT_NAME} =~ interactive ]] && RUN_MODE=interactive + local scwrypts_execution_mode=normal + [[ "${SCWRYPT_NAME}" =~ interactive ]] && scwrypts_execution_mode=interactive local LOGFILE \ - && [[ ${RUN_MODE} =~ normal ]] \ - && [[ ${SCWRYPTS_LOG_LEVEL} -gt 0 ]] \ - && [[ ${SUBSCWRYPT} -eq 0 ]] \ - && [[ ! ${SCWRYPT_NAME} =~ scwrypts/logs ]] \ - && LOGFILE="${SCWRYPTS_LOG_PATH}/$(echo ${GROUP}/${TYPE}/${NAME} | sed 's/^\.\///; s/\//\%/g').log" \ + && [[ "${scwrypts_execution_mode}" =~ normal ]] \ + && [[ "${SCWRYPTS_LOG_LEVEL}" -gt 0 ]] \ + && [[ "${SUBSCWRYPT}" -eq 0 ]] \ + && [[ ! "${SCWRYPT_NAME}" =~ scwrypts/logs ]] \ + && LOGFILE="${SCWRYPTS_LOG_PATH}/$(echo ${SCWRYPT_GROUP}/${SCWRYPT_TYPE}/${SCWRYPT_NAME} | sed 's/^\.\///; s/\//\%/g').log" \ || LOGFILE='/dev/null' \ ; - local HEADER FOOTER - [[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && { - case ${SCWRYPTS_OUTPUT_FORMAT} in + scwrypts.runner.header-and-footer() { + local output_type="${1}" + case "${output_type}" in + ( header | footer ) ;; + ( * ) return 1 ;; + esac + + local header footer + + [[ "${SCWRYPTS_LOG_LEVEL}" -ge 3 ]] && case "${SCWRYPTS_OUTPUT_FORMAT}" in ( raw ) - HEADER="--- start scwrypt ${SCWRYPT_GROUP}/${SCWRYPT_TYPE} ${SCWRYPT_NAME} in ${SCWRYPTS_ENV} ---" - FOOTER="--- end scwrypt ---" + header="--- start scwrypt ${SCWRYPT_GROUP}/${SCWRYPT_TYPE} ${SCWRYPT_NAME} in ${SCWRYPTS_ENV} ---" + footer="--- end scwrypt ---" ;; + ( pretty ) - HEADER=$( + header=$( echo " ===================================================================== scwrypt : ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} run at : $(date) config : ${SCWRYPTS_ENV} - log level : ${SCWRYPTS_LOG_LEVEL} + log level : ${SCWRYPTS_LOG_LEVEL} (${SCWRYPTS_LOG_LEVELS[${SCWRYPTS_LOG_LEVEL}]}) $(utils.colors.print bright-yellow '--- SCWRYPT BEGIN ---------------------------------------------------') " | sed 's/^\s\+//; 1d' ) - FOOTER="$(utils.colors.print bright-yellow '--- SCWRYPT END ---------------------------------------------------')" + footer="$(utils.colors.print bright-yellow '--- SCWRYPT END ---------------------------------------------------')" ;; + ( json ) - HEADER=$(echo '{}' | jq -c ". - | .timestamp = \"$(date +%s)\" - | .runtime = \"${SCWRYPTS_RUNTIME_ID}\" - | .scwrypt = \"start of ${SCWRYPT_NAME} ${SCWRYPT_GROUP} ${SCWRYPT_TYPE}\" - | .config = \"${SCWRYPTS_ENV}\" - | .logLevel = \"${SCWRYPTS_LOG_LEVEL}\" - | .subscwrypt = ${SUBSCWRYPT} - ") + header=$(jo \ + timestamp="$(date +%s)" \ + config="${SCWRYPTS_ENV}" \ + logLevel="${SCWRYPTS_LOG_LEVEL}" \ + subscwrypt="${SUBSCWRYPT}" \ + runtime="${SCWRYPTS_RUNTIME_ID}" \ + scwryptName="${SCWRYPT_NAME}" \ + scwryptGroup="${SCWRYPT_GROUP}" \ + scwryptType="${SCWRYPT_TYPE}" \ + message="start of execution" \ + ) + + footer=$(jo \ + timestamp="$(date +%s)" \ + config="${SCWRYPTS_ENV}" \ + logLevel="${SCWRYPTS_LOG_LEVEL}" \ + subscwrypt="${SUBSCWRYPT}" \ + runtime="${SCWRYPTS_RUNTIME_ID}" \ + scwryptName="${SCWRYPT_NAME}" \ + scwryptGroup="${SCWRYPT_GROUP}" \ + scwryptType="${SCWRYPT_TYPE}" \ + message="end of execution" \ + ) + ;; + + ( logfmt ) + header="ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) level=status runtime=${SCWRYPTS_RUNTIME_ID} config=${SCWRYPTS_ENV} logLevel=${SCWRYPTS_LOG_LEVEL} subscwrypt=${SUBSCWRYPT} scwryptName=${SCWRYPT_NAME} scwryptGroup=${SCWRYPT_GROUP} scwryptType=${SCWRYPT_TYPE} msg=\"start of execution\"" + footer="ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) level=status runtime=${SCWRYPTS_RUNTIME_ID} config=${SCWRYPTS_ENV} logLevel=${SCWRYPTS_LOG_LEVEL} subscwrypt=${SUBSCWRYPT} scwryptName=${SCWRYPT_NAME} scwryptGroup=${SCWRYPT_GROUP} scwryptType=${SCWRYPT_TYPE} msg=\"end of execution\"" ;; esac - } - [[ ${SUBSCWRYPT} -eq 0 ]] || { - case ${SCWRYPTS_OUTPUT_FORMAT} in + [[ "${SUBSCWRYPT}" -ne 0 ]] && case "${SCWRYPTS_OUTPUT_FORMAT}" in + ( raw ) + header="--- (${SUBSCWRYPT}) begin ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---" + footer="--- (${SUBSCWRYPT}) end ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---" + ;; + ( pretty ) - HEADER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) BEGIN ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")" - FOOTER="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) END ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")" + header="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) BEGIN ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")" + footer="$(utils.colors.print yellow "--- (${SUBSCWRYPT}) END ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME} ---")" + ;; + + ( json ) + header="$(jo \ + timestamp="$(date +%s)" \ + subscwryptLevel="${SUBSCWRYPT}" \ + runtime="${SCWRYPTS_RUNTIME_ID}" \ + scwryptName="${SCWRYPT_NAME}" \ + scwryptGroup="${SCWRYPT_GROUP}" \ + scwryptType="${SCWRYPT_TYPE}" \ + message='start of subscwrypt execution' \ + )" + + footer="$(jo \ + timestamp="$(date +%s)" \ + subscwryptLevel="${SUBSCWRYPT}" \ + runtime="${SCWRYPTS_RUNTIME_ID}" \ + scwryptName="${SCWRYPT_NAME}" \ + scwryptGroup="${SCWRYPT_GROUP}" \ + scwryptType="${SCWRYPT_TYPE}" \ + message='end of subscwrypt execution' \ + )" + ;; + + ( logfmt ) + header="ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) level=status runtime=${SCWRYPTS_RUNTIME_ID} config=${SCWRYPTS_ENV} logLevel=${SCWRYPTS_LOG_LEVEL} subscwrypt=${SUBSCWRYPT} scwryptName=${SCWRYPT_NAME} scwryptGroup=${SCWRYPT_GROUP} scwryptType=${SCWRYPT_TYPE} msg=\"start of subscwrypt execution\"" + footer="ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) level=status runtime=${SCWRYPTS_RUNTIME_ID} config=${SCWRYPTS_ENV} logLevel=${SCWRYPTS_LOG_LEVEL} subscwrypt=${SUBSCWRYPT} scwryptName=${SCWRYPT_NAME} scwryptGroup=${SCWRYPT_GROUP} scwryptType=${SCWRYPT_TYPE} msg=\"end of subscwrypt execution\"" ;; esac - } + [[ "${(P)output_type}" ]] && echo "${(P)output_type}" + return 0 + } ##################################################################### ### run the scwrypt ################################################# ##################################################################### - set -o pipefail { - [[ ${SCWRYPTS_LOG_LEVEL} -ge 2 ]] && __SCWRYPTS_PRINT_EXIT_CODE=true + local print_exit_code=false + [[ "${SCWRYPTS_LOG_LEVEL}" -ge 3 ]] && print_exit_code=true - [ ${HEADER} ] && echo ${HEADER} >&2 + scwrypts.runner.header-and-footer header >&2 - ( - case ${RUN_MODE} in - ( normal ) - eval "${RUN_STRING} $(printf "%q " "$@")" - ;; - ( interactive ) - eval "${RUN_STRING} $(printf "%q " "$@")" /dev/tty - ;; - esac - ) + case "${scwrypts_execution_mode}" in + ( normal ) eval "${scwrypts_run_string} ${(q)@}" ;; + ( interactive ) eval "${scwrypts_run_string} ${(q)@}" /dev/tty ;; + esac - EXIT_CODE=$? + local exit_code="${?}" + echo "${exit_code}" > "${EXIT_CODE_FILE}" - [ ${FOOTER} ] && echo ${FOOTER} >&2 + scwrypts.runner.header-and-footer footer >&2 - [[ ${__SCWRYPTS_PRINT_EXIT_CODE} =~ true ]] && { - EXIT_COLOR=$( [[ ${EXIT_CODE} -eq 0 ]] && utils.colors.bright-green || utils.colors.bright-red ) - case ${SCWRYPTS_OUTPUT_FORMAT} in - ( raw ) - echo "terminated with code ${EXIT_CODE}" >&2 - ;; + [[ ${print_exit_code} =~ true ]] && { + local exit_color="$([[ ${exit_code} -eq 0 ]] && utils.colors.bright-green || utils.colors.bright-red)" + case "${SCWRYPTS_OUTPUT_FORMAT}" in ( pretty ) - echo "terminated with ${EXIT_COLOR}code ${EXIT_CODE}$(utils.colors.reset)" >&2 + echo "terminated with ${exit_color}code ${exit_code}$(utils.colors.reset)" >&2 ;; - ( json ) - [[ ${EXIT_CODE} =~ 0 ]] \ - && echo.success --force-print "terminated with code ${EXIT_CODE}" \ - || echo.error --force-print "terminated with code ${EXID_CODE}" \ + + ( * ) + [[ "${exit_code}" -eq 0 ]] \ + && echo.success --force-print "terminated with code ${exit_code}" \ + || echo.error --force-print "terminated with code ${exit_code}" \ ; ;; esac } + } > >(tee --append "${LOGFILE}") 2> >(tee --append "${LOGFILE}" >&2) +} "${@}" - return ${EXIT_CODE} - } | tee --append "${LOGFILE}" -} $@ +EXIT_CODE="${?}" +[[ -f "${EXIT_CODE_FILE}" ]] && EXIT_CODE="$(cat -- "${EXIT_CODE_FILE}")" -EXIT_CODE=$? +[[ "${SCWRYPTS_TEMP_PATH}" ]] && [[ -d "${SCWRYPTS_TEMP_PATH}" ]] && { + rm -- $(find "${SCWRYPTS_TEMP_PATH}" -mindepth 1 -maxdepth 1 -type f) + rmdir "${SCWRYPTS_TEMP_PATH}" +} &>/dev/null -[ "${SCWRYPTS_TEMP_PATH}" ] && [ -d "${SCWRYPTS_TEMP_PATH}" ] \ - && { - rm -- $(find "${SCWRYPTS_TEMP_PATH}" -mindepth 1 -maxdepth 1 -type f) - rmdir "${SCWRYPTS_TEMP_PATH}" - } &>/dev/null +[[ -f "${EXIT_CODE_FILE}" ]] && rm "${EXIT_CODE_FILE}" -return ${EXIT_CODE} +return "${EXIT_CODE}" diff --git a/scwrypts.scwrypts.zsh b/scwrypts.scwrypts.zsh index 0bcb5ef..f790e95 100644 --- a/scwrypts.scwrypts.zsh +++ b/scwrypts.scwrypts.zsh @@ -84,7 +84,7 @@ readonly ${scwryptsgroup}__color=$(utils.colors.green) # -readonly ${scwryptsgroup}__zshlibrary= +#readonly ${scwryptsgroup}__zshlibrary= # # ${scwryptsgroup}__zshlibrary (optional) (default = *see below*) # diff --git a/zsh/README.md b/zsh/README.md index 6375079..35cfe12 100644 --- a/zsh/README.md +++ b/zsh/README.md @@ -66,15 +66,15 @@ Of the `echo` family, there are two unique functions: #### Yes / No Prompts -The two helpers `utils.Yn` and `utils.yN` take a user-friendly yes/no question as an argument. +The two helpers `user.Yn` and `user.yN` take a user-friendly yes/no question as an argument. - when the user responds "yes", the command returns 0 / success / `&&` - when the user responds "no", the command returns 1 / error / `||` - when the user responds with _nothing_ (e.g. just presses enter), the _default_ is used The two commands work identically; however, the capitalization denotes the default: -- `utils.Yn` = default "yes" -- `utils.yN` = default "no" +- `user.Yn` = default "yes" +- `user.yN` = default "no" #### Select from a List Prompt diff --git a/zsh/cloud/aws/efs/unmount b/zsh/cloud/aws/efs/unmount index c84f6bd..f1a0488 100755 --- a/zsh/cloud/aws/efs/unmount +++ b/zsh/cloud/aws/efs/unmount @@ -14,13 +14,13 @@ MAIN() { eval "$(utils.parse.autosetup)" ########################################## - [ ! -d "${AWS__EFS__LOCAL_MOUNT_POINT}" ] && { + [[ -d "${AWS__EFS__LOCAL_MOUNT_POINT}" ]] || { echo.status 'no efs currently mounted' return 0 } - local MOUNTED=$(cd -- "${AWS__EFS__LOCAL_MOUNT_POINT}" | find . -type -f | sed 's|^\./.||') - [ "${MOUNTED}" ] && { + local MOUNTED=$(cd -- "${AWS__EFS__LOCAL_MOUNT_POINT}" && find . -type f | sed 's|^\./||') + [[ "${MOUNTED}" ]] || { echo.status 'no efs currently mounted' return 0 } @@ -28,12 +28,12 @@ MAIN() { utils.io.getsudo || return 1 local SELECTED=$(echo ${MOUNTED} | utils.fzf 'select a file system to unmount') - [ "${SELECTED}" ] || user.abort + [ "${SELECTED}" ] || echo.error.user-abort || return 1 local EFS="${AWS__EFS__LOCAL_MOUNT_POINT}/${SELECTED}" echo.status "unmounting '${SELECTED}'" - sudo umount "${EFS}" >/dev/null 2>&1 + sudo umount "${EFS}" &>/dev/null sudo rmdir -- "${EFS}" \ && echo.success "done" \ - || utils.fail 2 "failed to unmount '${EFS}'" + || echo.error "failed to unmount '${EFS}'" } diff --git a/zsh/cloud/aws/rds/rds.module.zsh b/zsh/cloud/aws/rds/rds.module.zsh index 4ae1803..0daf940 100644 --- a/zsh/cloud/aws/rds/rds.module.zsh +++ b/zsh/cloud/aws/rds/rds.module.zsh @@ -8,19 +8,22 @@ use cloud/aws/cli ##################################################################### RDS__SELECT_DATABASE() { - local DATABASES=$(_RDS__GET_AVAILABLE_DATABASES) - [ ! $DATABASES ] && utils.fail 1 'no databases available' + local DATABASES="$(_RDS__GET_AVAILABLE_DATABASES)" + [[ "${DATABASES}" ]] \ + || echo.error 'no databases available' \ + || return 1 - local ID=$(\ - echo $DATABASES | jq -r '.instance + " @ " + .cluster' \ + local ID="$(\ + echo "${DATABASES}" | jq -r '.instance + " @ " + .cluster' \ | utils.fzf 'select a database (instance@cluster)' \ - ) - [ ! $ID ] && user.abort + )" - local INSTANCE=$(echo $ID | sed 's/ @ .*$//') - local CLUSTER=$(echo $ID | sed 's/^.* @ //') + [[ "${ID}" ]] || echo.user-abort || return 1 - echo $DATABASES | jq "select (.instance == \"$INSTANCE\" and .cluster == \"$CLUSTER\")" + local INSTANCE="$(echo "${ID}" | sed 's/ @ .*$//')" + local CLUSTER="$(echo "${ID}" | sed 's/^.* @ //')" + + echo "${DATABASES}" | jq "select (.instance == \"${INSTANCE}\" and .cluster == \"${CLUSTER}\")" } _RDS__GET_AVAILABLE_DATABASES() { @@ -52,19 +55,18 @@ RDS__GET_DATABASE_CREDENTIALS() { shift 1 done - utils.check-errors --fail + utils.check-errors || return "${ERRORS}" ########################################## - local DATABASE=$(RDS__SELECT_DATABASE) - [ ! $DATABASE ] && user.abort + local DATABASE="$(RDS__SELECT_DATABASE)" + [[ "${DATABASE}" ]] || echo.user-abort || return 1 - DB_HOST="$(echo $DATABASE | jq -r '.host')" - [ ! $DB_HOST ] && { echo.error 'unable to find host'; return 2; } + DB_HOST="$(echo "${DATABASE}" | jq -r '.host')" + [[ "${DB_HOST}" ]] || echo.error 'unable to find host' || return 2 - DB_PORT="$(echo $DATABASE | jq -r '.port')" - [ ! $DB_PORT ] && DB_PORT=5432 - [[ $DB_PORT =~ ^null$ ]] && DB_PORT=5432 + DB_PORT="$(echo "${DATABASE}" | jq -r '.port' | grep -v ^null$)" + [[ "${DB_PORT}" ]] || DB_PORT=5432 ########################################## @@ -72,46 +74,46 @@ RDS__GET_DATABASE_CREDENTIALS() { echo "iam\nsecretsmanager\nuser-input" \ | utils.fzf 'select an authentication method' \ ) - [ ! $AUTH_METHOD ] && user.abort + [[ "${AUTH_METHOD}" ]] || echo.user-abort || return 1 - case $AUTH_METHOD in + case "${AUTH_METHOD}" in ( iam ) _RDS_AUTH__iam ;; ( secretsmanager ) _RDS_AUTH__secretsmanager ;; ( user-input ) _RDS_AUTH__userinput ;; esac - [[ $PRINT_PASSWORD -eq 1 ]] && echo.debug "password : $DB_PASS" + [[ "${PRINT_PASSWORD}" -eq 1 ]] && echo.debug "password : ${DB_PASS}" return 0 } _RDS_AUTH__iam() { - DB_PASS=$(\ + DB_PASS="$(\ AWS rds generate-db-auth-token \ - --hostname $DB_HOST \ - --port $DB_PORT \ - --username $DB_USER \ - ) + --hostname "${DB_HOST}" \ + --port "${DB_PORT}" \ + --username "${DB_USER}" \ + )" } _RDS_AUTH__secretsmanager() { local CREDENTIALS=$(_RDS__GET_SECRETSMANAGER_CREDENTIALS) - echo $CREDENTIALS | jq -e '.pass' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.pass' &>/dev/null \ && DB_PASS="$(echo $CREDENTIALS | jq -r '.pass')" - echo $CREDENTIALS | jq -e '.password' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.password' &>/dev/null \ && DB_PASS="$(echo $CREDENTIALS | jq -r '.password')" - echo $CREDENTIALS | jq -e '.user' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.user' &>/dev/null \ && DB_USER=$(echo $CREDENTIALS | jq -r '.user') - echo $CREDENTIALS | jq -e '.username' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.username' &>/dev/null \ && DB_USER=$(echo $CREDENTIALS | jq -r '.username') - echo $CREDENTIALS | jq -e '.name' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.name' &>/dev/null \ && DB_NAME=$(echo $CREDENTIALS | jq -r '.name') - echo $CREDENTIALS | jq -e '.dbname' >/dev/null 2>&1 \ + echo "${CREDENTIALS}" | jq -e '.dbname' &>/dev/null \ && DB_NAME=$(echo $CREDENTIALS | jq -r '.dbname') } @@ -121,9 +123,8 @@ _RDS__GET_SECRETSMANAGER_CREDENTIALS() { | jq -r '.[] | .[] | .Name' \ | utils.fzf 'select a secret' \ ) - [ ! $ID ] && return 1 + [[ "${ID}" ]] || return 1 - AWS secretsmanager get-secret-value --secret-id "$ID" \ + AWS secretsmanager get-secret-value --secret-id "${ID}" \ | jq -r '.SecretString' | jq } - diff --git a/zsh/config.global.zsh b/zsh/config.global.zsh index 645a50b..15523fe 100644 --- a/zsh/config.global.zsh +++ b/zsh/config.global.zsh @@ -2,7 +2,7 @@ scwrypts.config() { [ $1 ] || return 1 case $1 in - ( python.versions ) echo "3.13\n3.12\n3.11\n3.10" ;; + ( python.versions ) echo "3.14\n3.13\n3.12\n3.11\n3.10" ;; ( nodejs.version ) echo "22.0.0" ;; ( * ) return 1 ;; esac diff --git a/zsh/config.zsh b/zsh/config.zsh index d82534c..b70eb08 100644 --- a/zsh/config.zsh +++ b/zsh/config.zsh @@ -50,15 +50,28 @@ readonly SCWRYPTS_DATA_PATH="${XDG_DATA_HOME:-${HOME}/.local/share}/scwrypts" readonly SCWRYPTS_STATE_PATH="${XDG_STATE_HOME:-${HOME}/.local/state}/scwrypts" readonly SCWRYPTS_LOG_PATH="${SCWRYPTS_STATE_PATH}/logs" -[ -d /tmp ] \ - && readonly SCWRYPTS_TEMP_PATH="/tmp/scwrypts/${SCWRYPTS_RUNTIME_ID}" \ - || readonly SCWRYPTS_TEMP_PATH="${XDG_RUNTIME_DIR:-/run/user/${UID}}/scwrypts/${SCWRYPTS_RUNTIME_ID}" \ - ; +if [[ -d /tmp ]] +then + readonly SCWRYPTS_TEMP_PATH="/tmp/scwrypts/${SCWRYPTS_RUNTIME_ID}" + readonly SCWRYPTS_TEMP_PATH_SOCKET="/tmp/scwrypts/socket" +else + readonly SCWRYPTS_TEMP_PATH="${XDG_RUNTIME_DIR:-/run/user/${UID}}/scwrypts/${SCWRYPTS_RUNTIME_ID}" + readonly SCWRYPTS_TEMP_PATH_SOCKET="${XDG_RUNTIME_DIR:-/run/user/${UID}}/scwrypts/socket" +fi -mkdir -p \ +readonly SCWRYPTS_CACHE_PATH__runtime="${SCWRYPTS_TEMP_PATH}/cache" +readonly SCWRYPTS_CACHE_PATH__persistent="${SCWRYPTS_TEMP_PATH_SOCKET}/cache" + +mkdir -p -- \ + "${SCWRYPTS_CONFIG_PATH}" \ "${SCWRYPTS_ENV_PATH}" \ + "${SCWRYPTS_DATA_PATH}" \ + "${SCWRYPTS_STATE_PATH}" \ "${SCWRYPTS_LOG_PATH}" \ "${SCWRYPTS_TEMP_PATH}" \ + "${SCWRYPTS_TEMP_PATH_SOCKET}" \ + "${SCWRYPTS_CACHE_PATH__runtime}" \ + "${SCWRYPTS_CACHE_PATH__persistent}" \ ; DEFAULT_CONFIG="${__SCWRYPTS_ROOT}/zsh/config.user.zsh" @@ -80,9 +93,8 @@ source "${__SCWRYPTS_ROOT}/zsh/config.global.zsh" SCWRYPTS_GROUPS=() -command -v echo.warning &>/dev/null || WARNING() { echo "echo.warning : $@" >&2; } -command -v echo.error &>/dev/null || ERROR() { echo "echo.error : $@" >&2; return 1; } -command -v utils.fail &>/dev/null || FAIL() { echo.error "${@:2}"; exit $1; } +command -v echo.warning &>/dev/null || echo.warning() { echo "echo.warning : $@" >&2; } +command -v echo.error &>/dev/null || echo.error() { echo "echo.error : $@" >&2; return 1; } __SCWRYPTS_GROUP_LOADERS=( "${__SCWRYPTS_ROOT}/scwrypts.scwrypts.zsh" @@ -146,7 +158,7 @@ do || echo.warning "error encountered when loading group '${__SCWRYPTS_GROUP_NAME}'" \ ; - [[ ! ${__SCWRYPTS_GROUP_NAME} =~ ^scwrypts$ ]] && [ ! "$(scwrypts.config.group ${__SCWRYPTS_GROUP_NAME} zshlibrary)" ] && { + [ ! "$(scwrypts.config.group ${__SCWRYPTS_GROUP_NAME} zshlibrary)" ] && { case $(scwrypts.config.group ${__SCWRYPTS_GROUP_NAME} type) in ( zsh ) [ -d "${scwryptsgrouproot}/lib" ] \ @@ -165,7 +177,8 @@ do done [[ ${SCWRYPTS_GROUPS[1]} =~ ^scwrypts$ ]] \ - || utils.fail 69 "encountered error when loading essential group 'scwrypts'; aborting" + || echo.error "encountered error when loading essential group 'scwrypts'; aborting" \ + || exit 69 ##################################################################### ### cleanup ######################################################### diff --git a/zsh/db/postgres/postgres.module.zsh b/zsh/db/postgres/postgres.module.zsh index 8d908b4..72cadfa 100644 --- a/zsh/db/postgres/postgres.module.zsh +++ b/zsh/db/postgres/postgres.module.zsh @@ -94,7 +94,7 @@ PG_RESTORE() { make sure your database is ready to accept the database file! " - yN 'continue?' || user.abort + yN 'continue?' || echo.error.user-abort || return 1 PSQL < "$INPUT_FILE" EXIT_CODE=$? diff --git a/zsh/db/postgres/run-sql b/zsh/db/postgres/run-sql index ea30185..6e19b9b 100755 --- a/zsh/db/postgres/run-sql +++ b/zsh/db/postgres/run-sql @@ -3,39 +3,42 @@ use db/postgres ##################################################################### MAIN() { - echo.warning " \nthis function is in a beta state\n " + echo.warning "---\nthis function is in a beta state\n---" + local _PASS _ARGS=() - POSTGRES__SET_LOGIN_ARGS $@ + POSTGRES__SET_LOGIN_ARGS "${@}" - local INPUT_FILE="$FILENAME" + local input_file="${FILENAME}" - local SQL_DIR="$SCWRYPTS_DATA_PATH/sql" - [ ! -d $SQL_DIR ] && mkdir -p $SQL_DIR + local sql_path="${SCWRYPTS_DATA_PATH}/sql" + mkdir -p -- "${sql_path}" - cd $SQL_DIR + cd -- "${sql_path}" - [[ $(ls "*.sql" 2>&1 | wc -l) -eq 0 ]] && { + [[ "$(find . -type f -name \*.sql 2>/dev/null | wc -l)" -eq 0 ]] && { echo.error "you haven't made any SQL commands yet" - echo.reminder "add '.sql' files here: '$SQL_DIR/'" + echo.reminder "add '.sql' files here: '${sql_path}/'" return 1 } - [ ! $INPUT_FILE ] && INPUT_FILE=$(utils.fzf 'select a sql file to run') - [ ! $INPUT_FILE ] && user.abort + [[ "${input_file}" ]] || input_file="$(utils.fzf.file-select --prompt 'select a sql file to run' "${sql_path}")" + [[ "${input_file}" ]] || echo.error.user-abort || return 2 - [ ! -f "$INPUT_FILE" ] && utils.fail 2 "no such sql file '$SQL_DIR/$INPUT_FILE'" + [[ -f "${input_file}" ]] \ + || echo.error "no such sql file '${input_file}'" -v sql_path \ + || return 2 - echo.status "loading '$INPUT_FILE' preview..." - LESS "$INPUT_FILE" + echo.status "loading '${input_file}' preview..." + utils.less "${input_file}" - echo.status "login : $_USER@$_HOST:$_PORT/$_NAME" - echo.status "command : '$INPUT_FILE'" + echo.status "login : ${_USER}@${_HOST}:${_PORT}/${_NAME}" + echo.status "command : '${input_file}'" - yN 'run this command?' || user.abort + yN 'run this command?' || echo.error.user-abort || return 1 - echo.status "running '$INPUT_FILE'" + echo.status "running '${input_file}'" - PSQL < $INPUT_FILE \ - && echo.success "finished running '$INPUT_FILE'" \ - || utils.fail 3 "something went wrong running '$INPUT_FILE' (see above)" + PSQL < "${input_file}" \ + && echo.success "finished running '${input_file}'" \ + || echo.error "something went wrong running '${input_file}' (see above)" } diff --git a/zsh/docker/cleanup b/zsh/docker/cleanup index 2241427..df5c203 100755 --- a/zsh/docker/cleanup +++ b/zsh/docker/cleanup @@ -18,7 +18,7 @@ MAIN() { pruned resources are PERMANENTLY DELETED " - utils.yN 'continue?' || utils.abort + user.yN 'continue?' || utils.abort echo.success "$( for RESOURCE in ${RESOURCES[@]} diff --git a/zsh/hello-world b/zsh/hello-world index 5b57e0c..494e3cc 100755 --- a/zsh/hello-world +++ b/zsh/hello-world @@ -2,5 +2,5 @@ ##################################################################### MAIN() { - echo.success 'Hello, World!' + echo.success 'Hello, World!' --force-print } diff --git a/zsh/import.driver.zsh b/zsh/import.driver.zsh index 3060ed3..dd95803 100644 --- a/zsh/import.driver.zsh +++ b/zsh/import.driver.zsh @@ -251,8 +251,7 @@ use.compute-scwrypts-library-hash() { LC_ALL=POSIX find "${SCWRYPTS_LIBRARY_ROOT}" -type f -name \*.module.zsh -print0 \ | sort -z \ | xargs -0 sha1sum \ - | sha1sum \ - | awk '{print $1;}' \ + | utils.sha1sum \ ; } diff --git a/zsh/sanity-check b/zsh/sanity-check index 113d851..ed9d76a 100755 --- a/zsh/sanity-check +++ b/zsh/sanity-check @@ -15,39 +15,33 @@ USAGE__description=' ##################################################################### MAIN() { - local OUTPUT_FUNCTION=success - local EXIT_CODE=0 - local MESSAGE='Hello, world!' + local output_function=success + local exit_code=0 + local message='Hello, world!' eval "${ZSHPARSEARGS}" ########################################## - [[ ${OUTPUT_FUNCTION} =~ ^fail$ ]] && utils.fail ${EXIT_CODE} "${MESSAGE}" + command -v "echo.${output_function}" &>/dev/null || { + echo.warning "no such function echo.${output_function} exists; swapping to echo.status" + output_function=status + } - echo.${OUTPUT_FUNCTION} "${MESSAGE}" - return ${EXIT_CODE} + "echo.${output_function}" "${message}" + return ${exit_code} } ##################################################################### MAIN.parse() { - local PARSED=0 + local parsed=0 - case $1 in - ( -e | --exit-code ) - PARSED=2 - EXIT_CODE="$2" - ;; - ( -f | --output-function ) - PARSED=2 - OUTPUT_FUNCTION="$2" - ;; - ( -m | --message ) - PARSED=2 - MESSAGE="$2" - ;; + case "${1}" in + ( -e | --exit-code ) parsed=2; exit_code="${2}" ;; + ( -f | --output-function ) parsed=2; output_function="${2}" ;; + ( -m | --message ) parsed=2; message="${2}" ;; esac - return ${PARSED} + return "${parsed}" } diff --git a/zsh/scwrypts/cache-output.module.zsh b/zsh/scwrypts/cache-output.module.zsh deleted file mode 100644 index 197a49b..0000000 --- a/zsh/scwrypts/cache-output.module.zsh +++ /dev/null @@ -1,143 +0,0 @@ -##################################################################### - -${scwryptsmodule}() { - eval "$(usage.reset)" - local USAGE__description=" - Caches any successful shell operation for the current scwrypts runtime; - useful for operations which require heavy processing, but frequent access. - - Keep in mind the following: - - Operations are hashed exclusively by the --cache-file argument; use of the - same cache file for multiple different commands will result in erratic behavior, - so make sure the filename is unique across the full scwrypts runtime - - - Operations which have a failed shell exit code are not cached; operations - MUST succeed the first time in order to be cached - - - The full stdout of the operation is cached to RAM - " - - local \ - USE_CACHE=true CACHE_ARGS_COUNT=0 CACHE_FILE \ - ARGS=() ARGS_FORCE=allowed \ - ; - - eval "$ZSHPARSEARGS" - - local CACHE_FILE_FULLPATH="$SCWRYPTS_TEMP_PATH/$CACHE_FILE" - - case $USE_CACHE in - ( bypass ) ${ARGS[@]} ;; - ( true | reset ) - local EXIT_CODE=0 - - [[ $USE_CACHE =~ reset ]] && rm -- "$CACHE_FILE_FULLPATH" &>/dev/null - - [ -f "$CACHE_FILE_FULLPATH" ] || { - ${ARGS[@]} > "$CACHE_FILE_FULLPATH" - EXIT_CODE=$? - } - - case $EXIT_CODE in - ( 0 ) cat "$CACHE_FILE_FULLPATH" - ;; - ( * ) cat "$CACHE_FILE_FULLPATH" 2>/dev/null - echo.error "error running '${ARGS[@]}'" - rm -- "$CACHE_FILE_FULLPATH" &>/dev/null - ;; - esac - - return $EXIT_CODE - ;; - esac -} - -##################################################################### - -${scwryptsmodule}.parse() { - # local USE_CACHE CACHE_ARGS_COUNT=0 CACHE_FILE - local PARSED=0 - case $1 in - ( --use-cache ) - PARSED=2 - case $2 in - ( true | reset | bypass ) - ((CACHE_ARGS_COUNT+=1)) - USE_CACHE=$2 - ;; - ( * ) echo.error "cannot set USE_CACHE to '$2'" ;; - esac - ;; - - ( --cache-file ) - PARSED=2 - CACHE_FILE=$2 - ;; - esac - return $PARSED -} - -${scwryptsmodule}.parse.usage() { - USAGE__options+="\n - --cache-file (required) runtime-unique filename for cached data - for cache to be automatically cleared, make sure this is a simple filename (no directories) - - --reset-cache clear the cache before performing the operation - --bypass-cache skip the cache for a clean operation, but don't clear existing or set new cache data - " - - USAGE__args+="\n - remaining arguments are executed as a shell command - " -} - -${scwryptsmodule}.parse.validate() { - case $USE_CACHE in - ( true | reset | bypass ) ;; - ( * ) echo.error "invalid value '$USE_CACHE' for USE_CACHE (are you missing 'local USE_CACHE=true'?)" ;; - esac - - [[ "$CACHE_FILE" ]] && { - mkdir -p -- "$SCWRYPTS_TEMP_PATH/$(dirname -- "$CACHE_FILE")" \ - || echo.error "unable to create base directory '$SCWRYPTS_TEMP_PATH/$(dirname -- "$CACHE_FILE")'" - - true - } || echo.error "missing cache file" - - [[ ${#ARGS[@]} -gt 0 ]] \ - || echo.error "no command provided" - - [[ $CACHE_ARGS_COUNT -le 1 ]] \ - || echo.error "too many '--use-cache' flags used" -} - -##################################################################### - -${scwryptsmodule}.zshparse.args() { - # local CACHE_ARGS=() - local PARSED=0 - case $1 in - ( --use-cache ) - PARSED=2 - case $2 in - ( true | reset | bypass ) - CACHE_ARGS+=(--use-cache $2) - ;; - - ( * ) echo.error "invalid --use-cache value '$2'" ;; - esac - esac - return $PARSED -} - -${scwryptsmodule}.zshparse.args.usage() { - USAGE__options+="\n - --use-cache (default true) one of {'true', 'reset', 'bypass'} - " - - USAGE__description+="\n - (this function uses functions cached per scwrypts runtime) - " -} - -##################################################################### diff --git a/zsh/scwrypts/environment/common.module.zsh b/zsh/scwrypts/environment/common.module.zsh index 541e35c..2ffe695 100644 --- a/zsh/scwrypts/environment/common.module.zsh +++ b/zsh/scwrypts/environment/common.module.zsh @@ -5,59 +5,106 @@ DEPENDENCIES+=(grep jq sed sort yq) ##################################################################### ${scwryptsmodule}.get-env-names() { - [ $REQUIRED_ENVIRONMENT_REGEX ] && { - scwrypts.environment.common.find-env-names \ - | grep "$REQUIRED_ENVIRONMENT_REGEX" - return $? - } - - scwrypts.environment.common.find-env-names + scwrypts.environment.common.find-env-names \ + | grep "${REQUIRED_ENVIRONMENT_REGEX:-.}" } -${scwryptsmodule}.get-env-filename() { # provides the fully qualified path to the group config file - local NAME="$1" - local GROUP="$2" - [ $NAME ] && [ $GROUP ] \ - || echo.error 'cannot determine environment filename without name ($1) and group ($2)' \ +${scwryptsmodule}.get-env-file() { # provides the fully qualified path to the group config file + local environment_name="${1}" group="${2}" + [[ "${environment_name}" ]] && [[ "${group}" ]] \ + || echo.error 'cannot determine environment file without name ($1) and group ($2)' \ || return 1 - echo "$SCWRYPTS_ENV_PATH/$NAME.$GROUP.env.yaml" + echo "${SCWRYPTS_ENV_PATH}/${environment_name}.${group}.env.yaml" } -${scwryptsmodule}.get-parent-env-names() { # deepest parent first; e.g. for 'a.b.c.d', returns (a a.b a.b.c) - local NAME="$1" - [[ $NAME =~ . ]] || return 0 +##################################################################### - local PARENT_ENV_NAMES=() - while [ $NAME ] +${scwryptsmodule}.get-parent-env-names() { # highest-level parent first; e.g. for 'a.b.c.d', returns (a a.b a.b.c) + local environment_name="${1}" + [[ "${environment_name}" ]] || return 0 + + local parent_environment_names=() + while [[ "${environment_name}" ]] do - NAME="$(echo $NAME | sed -n 's/\.[^.]\+$//p')" - [ $NAME ] && PARENT_ENV_NAMES+=($NAME) + environment_name="$(echo "${environment_name}" | sed -n 's/\.[^.]\+$//p')" + [[ "${environment_name}" ]] && parent_environment_names+=("${environment_name}") done - echo ${PARENT_ENV_NAMES[@]} | sed 's/\s\+/\n/g' | sort + printf '%s\n' "${parent_environment_names[@]}" | sort } +##################################################################### + ${scwryptsmodule}.find-env-files() { - find "$SCWRYPTS_ENV_PATH/" -mindepth 1 -maxdepth 1 -type f -name \*.env.yaml 2>/dev/null + find "${SCWRYPTS_ENV_PATH}/" -mindepth 1 -maxdepth 1 -type f -name \*.env.yaml 2>/dev/null } ${scwryptsmodule}.find-env-names() { scwrypts.environment.common.find-env-files \ - | sed "s|^$SCWRYPTS_ENV_PATH/||; s|\\.[^.]\\+\\.env\\.yaml$||" \ + | sed "s|^${SCWRYPTS_ENV_PATH}/||; s|\\.[^.]\\+\\.env\\.yaml$||" \ | sort --reverse --unique \ ; } ${scwryptsmodule}.find-env-files-by-name() { - local NAME="$1" - [ $NAME ] || return 1 + local environment_name="${1}" + [[ "${environment_name}" ]] || return 1 - find "$SCWRYPTS_ENV_PATH/" -mindepth 1 -maxdepth 1 -type f -name $NAME.\*.env.yaml 2>/dev/null + find "${SCWRYPTS_ENV_PATH}/" -mindepth 1 -maxdepth 1 -type f -name ${environment_name}.\*.env.yaml 2>/dev/null } +##################################################################### + ${scwryptsmodule}.combine-template-files() { - utils.yq eval-all '. as $item ireduce ({}; . * $item)' \ - | sed 's/: {}$/:/' \ - | utils.yq 'sort_keys(...)' \ + # combine files & set 'somethingEmpty: {}' to 'somethingEmpty:' + utils.yq.combine-files | sed 's/: {}$/:/' +} + +##################################################################### + +${scwryptsmodule}.get-all-template-files() { + # produces a complete list of template env.yaml across all known scwrypts groups + local files=() + + local group file + for group in "${SCWRYPTS_GROUPS[@]}" + do + file="$(scwrypts.config.group "${group}" root)/.config/env.yaml" + [[ -f "${file}" ]] && files+=("${group}:${file}") + done + + printf '%s\n' "${files[@]}" +} + +${scwryptsmodule}.get-all-template-files.only-filenames() { + scwrypts.environment.common.get-all-template-files | sed 's/^[^:]\+://' +} + +${scwryptsmodule}.get-all-configuration-files() { + # produces a complete list of user environment configurations for the target environment + local PARSERS=(scwrypts.zshparse.environment-name) + eval "$(utils.parse.autosetup)" + ########################################## + + local files=() + + local _environment_name file group group_config_file + for _environment_name in \ + $(scwrypts.environment.common.get-parent-env-names "${environment_name}") \ + ${environment_name} \ + ; + do + for group in "${SCWRYPTS_GROUPS[@]}" + do + group_config_file="$(scwrypts.environment.common.get-env-file "${_environment_name}" "${group}")" + [[ -f "${group_config_file}" ]] && files+=("${group_config_file}") + done + done + + printf '%s\n' "${files[@]}" +} + +${scwryptsmodule}.get-environment-module-files() { + find "$(scwrypts.config.group scwrypts zshlibrary)/scwrypts/environment" -type f } diff --git a/zsh/scwrypts/environment/environment.module.zsh b/zsh/scwrypts/environment/environment.module.zsh index 3a55374..662d45d 100644 --- a/zsh/scwrypts/environment/environment.module.zsh +++ b/zsh/scwrypts/environment/environment.module.zsh @@ -7,26 +7,17 @@ # common lookups used by all environment logic use scwrypts/environment/common - -# allows utils.fzf selection of environments -use scwrypts/environment/select-env - +# primary source of truth for configuration values in the environment +use scwrypts/environment/get-user-json # initialize environments, or skip if already initialized use scwrypts/environment/init +# allows utils.fzf selection of environments +use scwrypts/environment/select-env -# injects metadata and unifies environment template +# creates a merged template file across all known scwrypts groups' env.yaml use scwrypts/environment/get-full-template - -# lookup environment variables by config path -use scwrypts/environment/get-envvar-lookup-map - - -# generates local/non-CI configuration -use scwrypts/environment/user - - # create/edit/delete operations on local configurations use scwrypts/environment/update diff --git a/zsh/scwrypts/environment/get-configuration-values.module.zsh b/zsh/scwrypts/environment/get-configuration-values.module.zsh new file mode 100644 index 0000000..174a2a5 --- /dev/null +++ b/zsh/scwrypts/environment/get-configuration-values.module.zsh @@ -0,0 +1,41 @@ +##################################################################### + +use scwrypts/environment/get-enriched-user-yaml + +##################################################################### + +${scwryptsmodule}() { + local DESCRIPTION=" + returns json which contains shell-compatible lookup values + - moves inherited .PARENTVALUE to value (if value is empty) + - moves inherited .PARENTSELECTION to selection (if selection is empty) + " + + local PARSERS=(scwrypts.zshparse.environment-name) + + eval "$(utils.parse.autosetup)" + + ########################################## + + scwrypts.environment.get-enriched-user-yaml "${@}" \ + | utils.yq '.. + |= select( + ((has ("value") | not) or .value == null or .value | length == 0) and has (".PARENTVALUE") + ).value = .".PARENTVALUE" + ' \ + | utils.yq '.. + |= select( + ((has ("selection") | not) or .selection == null or .selection | length == 0) and has (".PARENTSELECTION") + ).selection = .".PARENTSELECTION" + ' \ + | utils.yq '. + | del(.. | select(has(".PARENTVALUE")).".PARENTVALUE") + | del(.. | select(has(".PARENTSELECTION")).".PARENTSELECTION") + | del(.. | select(has(".GROUP")).".GROUP") + | del(.. | select(has(".DESCRIPTION")).".DESCRIPTION") + | del(.. | select(has("value") and (.value == null or .value | length == 0)).value) + | del(.. | select(has("selection") and (.selection == null or .selection | length == 0)).selection) + | del(.. | select(has("value") and has("selection")).selection) + ' \ + ; +} diff --git a/zsh/scwrypts/environment/get-enriched-user-yaml.module.zsh b/zsh/scwrypts/environment/get-enriched-user-yaml.module.zsh new file mode 100644 index 0000000..1a32f27 --- /dev/null +++ b/zsh/scwrypts/environment/get-enriched-user-yaml.module.zsh @@ -0,0 +1,141 @@ +##################################################################### + +use scwrypts/environment/common +use scwrypts/zshparse + +use zsh-cache + +##################################################################### + +${scwryptsmodule}() { + local DESCRIPTION=" + Generates a metadata-enriched scwrypts environment YAML. + " + local PARSERS=(scwrypts.zshparse.environment-name) + + eval "$(utils.parse.autosetup)" + + ########################################## + + zsh-cache --auto -- "${environment_name}" +} + +${scwryptsmodule}.zsh-cache.get-hash() { + local environment_name="${1:-${SCWRYPTS_ENV}}" + { + scwrypts.environment.common.get-environment-module-files + scwrypts.environment.common.get-all-template-files.only-filenames + scwrypts.environment.common.get-all-configuration-files --environment-name "${environment_name}" + } | utils.sha1sum.filelist +} + +${scwryptsmodule}.zsh-cache() { + local ERRORS=0 WARNINGS=0 + + local environment_name="${SCWRYPTS_ENV}" + local loading_original_env=true + + local _s _p + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( --parent ) loading_original_env=false ;; + ( * ) + case "$((_p+=1))" in + ( 1 ) environment_name="${1}" ;; + ( * ) echo.error "unknown argument '${1}'" ;; + esac + esac + shift "${_s}" || echo.error "missing argument for '${1}'" || shift "${#}" + done + unset _s _p + + utils.check-errors || return "${?}" + + ########################################## + + case "${loading_original_env}" in + ( false ) ;; # only display help for originally requested environment + ( true ) + local show_help="$(normalize.boolean --variable SCWRYPTS_ENVIRONMENT__SHOW_ENV_HELP --default true --mode echo)" + case "${show_help}" in + ( false ) + echo "--- # current scwrypts environment = ${environment_name}" + ;; + + ( true ) + echo " + # + # current scwrypts environment = ${environment_name} + # + # - metadata tags are READONLY; changes to any key which starts with a + # '.' followed by all-caps (e.g. '.DESCRIPTION') will be ignored on + # save + # + # - value precedence is as follows (lower number is higher priority): + # 0. runtime environment variable '__override' value + # 1. the value from the 'value' key + # 2. the value from the '.PARENTVALUE' (if 'value' key is null) + # 3. a user-selected value from the 'selection' list + # 4. a user-selected value from the '.PARENTSELECTION' list + # + # - values are used like environment variables, although most scalar + # values are permitted, they are converted to strings or string arrays + # before use; string 'null' is OK, but null-type means not configured + # + # - for 'value' null-type / empty = not configured + # - for 'selection' null-type / empty / empty-list = not configured + # + --- + " + ;; + esac | sed 's/\(^\s\+\|\s\+$\)//g; /^$/d' + ;; + esac + + { + # the full template ensures all empty keys are available in the edit UI + scwrypts.environment.get-full-template.with-value-keys + + case "${loading_original_env}" in + ( false ) ;; # don't loop through parents outside of original environment + ( true ) + local parent_environment_name + for parent_environment_name in $(scwrypts.environment.common.get-parent-env-names "${environment_name}") + do + echo --- + scwrypts.environment.get-enriched-user-yaml.zsh-cache --parent "${parent_environment_name}" \ + | sed ' + s/^\(\s\+\)\(value\|selection\):/\1.PARENT\U\2:/ + ' \ + | utils.yq '. + | del(.. | select(has(".PARENTVALUE") and .".PARENTVALUE" == null).".PARENTVALUE") + | del(.. | select(has(".PARENTSELECTION") and (.".PARENTSELECTION" | length) == 0).".PARENTSELECTION") + | del(.. | select(has(".PARENTSELECTION") and has(".PARENTVALUE")).".PARENTSELECTION") + ' \ + ; + done + unset parent_environment_name + ;; + esac + + local group group_config_file + for group in ${SCWRYPTS_GROUPS[@]} + do + group_config_file="$(scwrypts.environment.common.get-env-file "${environment_name}" "${group}")" + + touch -- "${group_config_file}" + + [[ "$(head -n1 "${group_config_file}")" =~ ^---$ ]] \ + || echo --- + + cat -- "${group_config_file}" + done + } \ + | scwrypts.environment.common.combine-template-files \ + | utils.yq -P \ + | sed -z 's/\n[a-z]/\n&/g' \ + | sed 's/value: null$/value:/; /\svalue:/G' \ + ; +} diff --git a/zsh/scwrypts/environment/get-envvar-lookup-map.module.zsh b/zsh/scwrypts/environment/get-envvar-lookup-map.module.zsh index 7530f74..fc26f54 100644 --- a/zsh/scwrypts/environment/get-envvar-lookup-map.module.zsh +++ b/zsh/scwrypts/environment/get-envvar-lookup-map.module.zsh @@ -1,13 +1,12 @@ ##################################################################### -use scwrypts/cache-output use scwrypts/environment/get-full-template ##################################################################### ${scwryptsmodule}() { - eval "$(usage.reset)" - local USAGE__description=" + local ARGS=() ARGS_FORCE=true + local DESCRIPTION=" outputs a JSON map which can be used to lookup config-file query paths from environment variable names; GET_FULL_TEMPLATE flags OK @@ -15,20 +14,15 @@ ${scwryptsmodule}() { value : jq-style query path " - local \ - PARSERS=( - scwrypts.cache-output.zshparse.args # passthrough - ) - - eval "$ZSHPARSEARGS" + eval "$(utils.parse.autosetup)" ########################################## - scwrypts.environment.get-full-template $@ \ + scwrypts.environment.get-full-template "${ARGS[@]}" \ | utils.yq -P ' .. | select(. == "*") - | {(.): "." + (path | join(".") + ".value")} + | {(.): "." + (path | join("."))} '\ | sed -n 's/\.\.ENVIRONMENT//p' \ ; diff --git a/zsh/scwrypts/environment/get-full-template.module.zsh b/zsh/scwrypts/environment/get-full-template.module.zsh index 199ac79..c1f7e9b 100644 --- a/zsh/scwrypts/environment/get-full-template.module.zsh +++ b/zsh/scwrypts/environment/get-full-template.module.zsh @@ -1,48 +1,54 @@ ##################################################################### -use scwrypts/cache-output use scwrypts/environment/common +use zsh-cache + ##################################################################### ${scwryptsmodule}() { - eval "$(usage.reset)" - local USAGE__description=" + local DESCRIPTION=" Provies the combined YAML of all available scwrypts group 'template.yaml' files. Template is cached after first generation in a given scwrypts runtime. " - local \ - CACHE_ARGS=() \ - PARSERS=( - scwrypts.cache-output.zshparse.args - ) + eval "$(utils.parse.autosetup)" - eval "$ZSHPARSEARGS" ########################################## - scwrypts.cache-output ${CACHE_ARGS[@]} \ - --cache-file environment.template.yaml \ - -- \ - scwrypts.environment.get-full-template.helper \ + zsh-cache --auto +} + +${scwryptsmodule}.zsh-cache() { + local DEBUG_ARGS=() TRACE_ARGS=() + local template_file group _ + while IFS=: read -r group template_file _ + do + [[ "$(head -n1 "${template_file}")" =~ ^---$ ]] || echo --- + + cat -- "${template_file}" \ + | utils.yq "(.. | select(has(\".ENVIRONMENT\"))) += {\".GROUP\":\"${group}\"}" + done < <(scwrypts.environment.common.get-all-template-files) \ + | scwrypts.environment.common.combine-template-files + + return 0 +} + +${scwryptsmodule}.zsh-cache.get-hash() { + { + scwrypts.environment.common.get-environment-module-files + scwrypts.environment.common.get-all-template-files.only-filenames + } | utils.sha1sum.filelist +} + +${scwryptsmodule}.with-value-keys() { + scwrypts.environment.get-full-template \ + | utils.yq '(.. | select(has(".ENVIRONMENT"))) += { + "selection": [], + "value": null + } + ' \ + | sed 's/ ""$//' \ ; } - -${scwryptsmodule}.helper() { - local GROUP GROUP_ROOT GROUP_TEMPLATE_FILENAME - { - for GROUP in ${SCWRYPTS_GROUPS[@]} - do - GROUP_ROOT="$(scwrypts.config.group ${GROUP} root)" - - GROUP_TEMPLATE_FILENAME="${GROUP_ROOT}/.config/env.yaml" - - [ -f "${GROUP_TEMPLATE_FILENAME}" ] && { - [[ $(head -n1 "${GROUP_TEMPLATE_FILENAME}") =~ ^---$ ]] || echo --- - cat "${GROUP_TEMPLATE_FILENAME}" \ - | utils.yq "(.. | select(has(\".ENVIRONMENT\"))) += {\".GROUP\":\"${GROUP}\"}" - } - done - } | scwrypts.environment.common.combine-template-files -} diff --git a/zsh/scwrypts/environment/get-user-json.module.zsh b/zsh/scwrypts/environment/get-user-json.module.zsh new file mode 100644 index 0000000..f2e0799 --- /dev/null +++ b/zsh/scwrypts/environment/get-user-json.module.zsh @@ -0,0 +1,66 @@ +##################################################################### + +use scwrypts/environment/common +use scwrypts/environment/get-configuration-values +use scwrypts/environment/get-envvar-lookup-map +use scwrypts/environment/get-full-template + +##################################################################### + +${scwryptsmodule}() { + local DESCRIPTION=" + returns the fully populated configuration object for the target + environment; this should be used as the primary source of truth + for scwrypts environment configuration + + values must be retrieved from '.configuration' by their lookup key + + for values with a configured environment variable, the lookup key + can be found in '.lookup.ENVIRONMENT_VARIABLE' + + \`\`\`yaml + someGroup: + myData: + value: abcdefg 1234 + .ENVIRONMENT: MY_DATA_VARIABLE + \`\`\` + + utils.yq .lookup.MY_DATA_VARIABLE > '.someGroup.myData' + utils.yq .configuration.someGroup.myData.value > 'abcdefgh 1234' + " + + local PARSERS=(scwrypts.zshparse.environment-name) + + eval "$(utils.parse.autosetup)" + + ########################################## + + zsh-cache --auto -- --environment-name "${environment_name}" +} + +${scwryptsmodule}.zsh-cache.get-hash() { + local PARSERS=(scwrypts.zshparse.environment-name) + eval "$(utils.parse.autosetup)" + { + scwrypts.environment.common.get-environment-module-files + scwrypts.environment.common.get-all-template-files.only-filenames + scwrypts.environment.common.get-all-configuration-files --environment-name "${environment_name}" + } | utils.sha1sum.filelist +} + +${scwryptsmodule}.zsh-cache() { + local PARSERS=(scwrypts.zshparse.environment-name) + eval "$(utils.parse.autosetup)" + ########################################## + local configuration="$(scwrypts.environment.get-configuration-values --environment-name "${environment_name}")" + local lookup_map="$(scwrypts.environment.get-envvar-lookup-map)" + + echo.trace 'generating user json' -v lookup_map + { + echo "configuration:" + echo "${configuration}" | sed 's/^/ /' + + echo "lookup:" + echo "${lookup_map}" | sed 's/^/ /' + } | utils.yq -oj | jq -c +} diff --git a/zsh/scwrypts/environment/getenv b/zsh/scwrypts/environment/getenv index 6919898..daa9f65 100755 --- a/zsh/scwrypts/environment/getenv +++ b/zsh/scwrypts/environment/getenv @@ -39,7 +39,7 @@ MAIN() { utils.environment.check --print-value ${ARGS[@]} ;; ( all ) - scwrypts.environment.user.get-json + scwrypts.environment.get-user-json ;; esac } diff --git a/zsh/scwrypts/environment/init.module.zsh b/zsh/scwrypts/environment/init.module.zsh index 53e8a45..e1050ac 100644 --- a/zsh/scwrypts/environment/init.module.zsh +++ b/zsh/scwrypts/environment/init.module.zsh @@ -70,7 +70,7 @@ scwrypts.environment.legacy.convert() { echo.status "checking '${ENVIRONMENT_NAME}' configuration files" for GROUP in ${SCWRYPTS_GROUPS[@]} do - GROUP_CONFIG_FILENAME="$(scwrypts.environment.common.get-env-filename "${ENVIRONMENT_NAME}" "${GROUP}")" + GROUP_CONFIG_FILENAME="$(scwrypts.environment.common.get-env-file "${ENVIRONMENT_NAME}" "${GROUP}")" LEGACY_CONFIG_FILENAME="${SCWRYPTS_ENV_PATH}/${GROUP}/${ENVIRONMENT_NAME}" [ ! -f "${GROUP_CONFIG_FILENAME}" ] && [ -f "${LEGACY_CONFIG_FILENAME}" ] && { @@ -120,7 +120,7 @@ scwrypts.environment.legacy.convert.v4-to-v5.config-file() { local ENV_VAR NEW_TEMPLATE_KEY NEW_TEMPLATE="$(scwrypts.environment.get-full-template --reset-cache)" for ENV_VAR in $(echo "${LEGACY_CONFIG_VALUES}" | utils.yq -r 'keys | .[]') do - NEW_TEMPLATE_KEY=$(scwrypts.environment.template.get-envvar-lookup-map | utils.yq -r ".${ENV_VAR}") + NEW_TEMPLATE_KEY="$(scwrypts.environment.template.get-envvar-lookup-map | utils.yq -r ".${ENV_VAR}").value" NEW_TEMPLATE_VALUE="$(echo "${LEGACY_CONFIG_VALUES}" | utils.yq -r ".${ENV_VAR}")" echo "${NEW_TEMPLATE_VALUE}" | grep -q '^[[].*[]]$' \ || NEW_TEMPLATE_VALUE="\"${NEW_TEMPLATE_VALUE}\"" diff --git a/zsh/scwrypts/environment/update b/zsh/scwrypts/environment/update index 1b40089..b951eca 100755 --- a/zsh/scwrypts/environment/update +++ b/zsh/scwrypts/environment/update @@ -73,20 +73,20 @@ MAIN() { ;; ( copy ) local SOURCE_ENVIRONMENT="$(scwrypts.environment.common.get-env-names | utils.fzf 'select an environment to copy')" - [ "$SOURCE_ENVIRONMENT" ] || user.abort + [ "$SOURCE_ENVIRONMENT" ] || echo.error.user-abort || return 1 [ "$ENVIRONMENT_NAME" ] || ENVIRONMENT_NAME=$(echo | utils.fzf.user-input 'new environment name') - [ "$ENVIRONMENT_NAME" ] || user.abort + [ "$ENVIRONMENT_NAME" ] || echo.error.user-abort || return 1 ;; ( delete ) [ "$ENVIRONMENT_NAME" ] || ENVIRONMENT_NAME="$(scwrypts.environment.common.get-env-names | utils.fzf 'select an environment to delete')" - [ "$ENVIRONMENT_NAME" ] || user.abort + [ "$ENVIRONMENT_NAME" ] || echo.error.user-abort || return 1 ;; ( advanced ) [ "$ENVIRONMENT_NAME" ] || ENVIRONMENT_NAME="$SCWRYPTS_ENV" esac - [ -f "$(scwrypts.environment.common.get-env-filename "$ENVIRONMENT_NAME" scwrypts)" ] \ + [ -f "$(scwrypts.environment.common.get-env-file "$ENVIRONMENT_NAME" scwrypts)" ] \ && local ENVIRONMENT_EXISTS=true \ || local ENVIRONMENT_EXISTS=false \ ; @@ -97,7 +97,7 @@ MAIN() { ( true ) ;; ( false ) echo.warning "environment $ENVIRONMENT_NAME does not exist" - Yn "create new environment '$ENVIRONMENT_NAME'?" || user.abort + Yn "create new environment '$ENVIRONMENT_NAME'?" || echo.error.user-abort || return 1 ;; esac ;; @@ -117,7 +117,7 @@ MAIN() { case $ENVIRONMENT_EXISTS in ( true ) echo.reminder 'deleting an environment is PERMANENT\nmake sure your credentials are backed up!' - yN "are you sure you want to delete '$ENVIRONMENT_NAME'?" || user.abort + yN "are you sure you want to delete '$ENVIRONMENT_NAME'?" || echo.error.user-abort || return 1 ;; ( false ) echo.error "environment '$ENVIRONMENT_NAME' does not exist (can't delete it if it's not there)" diff --git a/zsh/scwrypts/environment/update.module.zsh b/zsh/scwrypts/environment/update.module.zsh index 82d8b23..1835e63 100644 --- a/zsh/scwrypts/environment/update.module.zsh +++ b/zsh/scwrypts/environment/update.module.zsh @@ -1,119 +1,128 @@ ##################################################################### use scwrypts/environment/common - -use scwrypts/environment/user use scwrypts/environment/get-full-template use scwrypts/environment/select-env +use zsh-cache/reset + ##################################################################### -[ $SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE ] \ +[ "${SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE}" ] \ || export SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE=basic -scwrypts.environment.update() { - local EDIT_MODE=$SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE - local ENVIRONMENT_NAME="$SCWRYPTS_ENV" - local FROM_EXISTING +${scwryptsmodule}() { + local DESCRIPTION='Update a scwrypts environment configuration' + local PARSERS=(scwrypts.zshparse.environment-name) + eval "$(utils.parse.autosetup)" + ########################################## - local USAGE=" - usage: scwrypts.environment.update [...options...] + local TEMP_CONFIG_FILE="${SCWRYPTS_TEMP_PATH_SOCKET}/environment.temp.yaml" - options: - --mode update execution mode (default: $SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE) - modes: - basic : create or edit environment with all tooltips and metadata - quiet : create or edit a compact environment with minimal metadata - init : create a new environment with no edit prompt - delete : delete target environment - recursive : (advanced) edit all inherited environments, starting from deepest parent - copy : (see --create-from-existing; most likely you don't need to set this flag) - - --environment-name name of the target environment (default is current: $SCWRYPTS_ENV) - --create-from-existing name of the environment to copy - - -h, --help print this dialogue and exit - " - - local _S ERRORS=0 - while [[ $# -gt 0 ]] - do - _S=1 - case $1 in - ( -h | --help ) utils.io.usage; return 0 ;; - - ( --environment-name ) - [ $2 ] && ((_S+=1)) || echo.error "missing environment name" || break - ENVIRONMENT_NAME="$2" - ;; - - ( --create-from-existing ) - [ $2 ] && ((_S+=1)) || echo.error "must provide environment name to copy" || break - EDIT_MODE=copy - FROM_EXISTING="$2" - ;; - - ( --mode ) - [ $2 ] && ((_S+=1)) || echo.error "missing mode" || break - EDIT_MODE="$2" - command -v scwrypts.environment.update.edit.$EDIT_MODE &>/dev/null \ - || echo.error "invalid mode '$EDIT_MODE'" - - ;; - - * ) echo.error "unknown argument '$1'" ;; - esac - shift $_S - done - - case $EDIT_MODE in - ( copy ) - [ $FROM_EXISTING ] || FROM_EXISTING=$(scwrypts.environment.select-env) - [ $FROM_EXISTING ] || echo.error "cannot work in '$EDIT_MODE' without existing target" - - [[ $(scwrypts.environment.common.find-env-files-by-name "$FROM_EXISTING" | wc -l) -gt 0 ]] \ - || echo.error "no such environment '$FROM_EXISTING' exists" - ;; - - ( * ) - [ ! $FROM_EXISTING ] || echo.error "cannot work in '$EDIT_MODE' with --create-from-existing" - ;; - esac - - utils.check-errors || return $? - - local TEMP_CONFIG_FILE="$SCWRYPTS_TEMP_PATH/environment.temp.yaml" - - [ -f "$TEMP_CONFIG_FILE" ] && { + [[ -f "${TEMP_CONFIG_FILE}" ]] && { echo.error "temp config file already exists at '$TEMP_CONFIG_FILE'\nis another environment update in-progress?" - echo.reminder "if you are certain no other environment update is in progress, you can resolve with\n rm -- '$TEMP_CONFIG_FILE'" + echo.reminder " + if you are certain no other environment update is in progress, you can resolve with + rm -- ${(q)TEMP_CONFIG_FILE} + " return 1 } - scwrypts.environment.update.edit.${EDIT_MODE} - local EXIT_CODE=$? + "${edit_mode_helper}" + local exit_code="${?}" - rm "$TEMP_CONFIG_FILE" 2>/dev/null + rm "${TEMP_CONFIG_FILE}" 2>/dev/null - return $EXIT_CODE + return ${exit_code} +} + +##################################################################### + +${scwryptsmodule}.parse.locals() { + local edit_mode="${SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE}" + local edit_mode_helper + local edit_mode_args=() + local from_existing +} + +${scwryptsmodule}.parse() { + local parsed=0 + + case "${1}" in + ( --mode ) + parsed=2 + edit_mode_args+=("${1} ${2}") + edit_mode="${2}" + ;; + + ( --create-from-existing ) + parsed=2 + edit_mode_args+=("${1}") + edit_mode=copy + from_existing="${2}" + ;; + esac + + return "${parsed}" +} + +${scwryptsmodule}.parse.usage() { + USAGE__options+=" + --mode update execution mode (default: ${SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE}) + modes: + basic : create or edit environment with all tooltips and metadata + quiet : create or edit a compact environment with minimal metadata + init : create a new environment with no edit prompt + delete : delete target environment + recursive : (advanced) edit all inherited environments, starting from deepest parent + copy : (see --create-from-existing; most likely you don't need to set this flag) + + --create-from-existing name of the environment to copy + " +} + +${scwryptsmodule}.parse.validate() { + [[ "${#edit_mode_args[@]}" -le 1 ]] \ + || echo.error "incompatible arguments ${(q)edit_mode_args[@]}" \ + || return + + + [[ "${edit_mode}" ]] \ + && edit_mode_helper="scwrypts.environment.update.edit.${edit_mode}" \ + && command -v "${edit_mode_helper}" &>/dev/null \ + || echo.error "invalid edit mode '${edit_mode}'" \ + ; + + case "${edit_mode}" in + ( copy ) + [[ "${from_existing}" ]] || from_existing="$(scwrypts.environment.select-env)" + [[ "${from_existing}" ]] || echo.error "cannot work in '${edit_mode}' without existing target" + ; + esac + + if [[ "${from_existing}" ]] + then + [[ "$(scwrypts.environment.common.find-env-files-by-name "${from_existing}" | wc -l)" -gt 0 ]] \ + || echo.error "no such environment '${from_existing}' exists" + fi } ##################################################################### ${scwryptsmodule}.edit.basic() { - scwrypts.environment.user.get \ - --environment-name $ENVIRONMENT_NAME \ + scwrypts.environment.get-enriched-user-yaml \ + --environment-name $environment_name \ > "$TEMP_CONFIG_FILE" utils.io.edit "$TEMP_CONFIG_FILE" - scwrypts.environment.update.update-user-configs "$(cat "$TEMP_CONFIG_FILE")" "$ENVIRONMENT_NAME" + scwrypts.environment.update.update-user-configs "${environment_name}" } ${scwryptsmodule}.edit.quiet() { - echo "--- # $ENVIRONMENT_NAME" > "$TEMP_CONFIG_FILE" - scwrypts.environment.user.get \ - --environment-name $ENVIRONMENT_NAME \ + echo "--- # $environment_name" > "$TEMP_CONFIG_FILE" + scwrypts.environment.get-enriched-user-yaml \ + --environment-name $environment_name \ | utils.yq '. | del(.. | select(has(".ENVIRONMENT")).".ENVIRONMENT") | del(.. | select(has(".GROUP")).".GROUP") @@ -126,75 +135,75 @@ ${scwryptsmodule}.edit.quiet() { utils.io.edit "$TEMP_CONFIG_FILE" - scwrypts.environment.update.update-user-configs "$(cat "$TEMP_CONFIG_FILE")" "$ENVIRONMENT_NAME" + scwrypts.environment.update.update-user-configs "${environment_name}" } ${scwryptsmodule}.edit.recursive() { - local RECURSIVE_EDIT_MODE="$SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE" - [[ $RECURSIVE_EDIT_MODE =~ ^recursive$ ]] \ - && RECURSIVE_EDIT_MODE=quiet + local recursive_edit_mode="${SCWRYPTS_ENVIRONMENT__PREFERRED_EDIT_MODE}" + [[ "${recursive_edit_mode}" =~ ^recursive$ ]] \ + && recursive_edit_mode=quiet - local PARENT_ENVIRONMENT_NAME - for PARENT_ENVIRONMENT_NAME in \ - $(scwrypts.environment.common.get-parent-env-names "$ENVIRONMENT_NAME") \ - $ENVIRONMENT_NAME + local parent_environment_name + for parent_environment_name in \ + $(scwrypts.environment.common.get-parent-env-names "$environment_name") \ + $environment_name ; do - echo.status "editing environment '$PARENT_ENVIRONMENT_NAME'" + echo.status "editing environment '${parent_environment_name}'" scwrypts.environment.update \ - --environment-name $PARENT_ENVIRONMENT_NAME \ - --mode $RECURSIVE_EDIT_MODE \ + --environment-name "${parent_environment_name}" \ + --mode "${recursive_edit_mode}" \ ; done } ${scwryptsmodule}.edit.init() { - [ -f "$(scwrypts.environment.common.get-env-filename)" ] + [ -f "$(scwrypts.environment.common.get-env-file)" ] - scwrypts.environment.user.get \ - --environment-name $ENVIRONMENT_NAME \ - > "$TEMP_CONFIG_FILE" + scwrypts.environment.get-enriched-user-yaml \ + --environment-name "${environment_name}" \ + > "${TEMP_CONFIG_FILE}" - scwrypts.environment.update.update-user-configs "$(cat "$TEMP_CONFIG_FILE")" + scwrypts.environment.update.update-user-configs "${environment_name}" } ${scwryptsmodule}.edit.copy() { - local GROUP_CONFIG_FILENAME SOURCE_CONFIG_FILENAME - for GROUP in ${SCWRYPTS_GROUPS[@]} + local group_config_filename SOURCE_CONFIG_FILENAME + for group in ${SCWRYPTS_GROUPS[@]} do cp \ - "$(scwrypts.environment.common.get-env-filename "$FROM_EXISTING" "$GROUP")" \ - "$(scwrypts.environment.common.get-env-filename "$ENVIRONMENT_NAME" "$GROUP")" \ + "$(scwrypts.environment.common.get-env-file "${FROM_EXISTING}" "${group}")" \ + "$(scwrypts.environment.common.get-env-file "${environment_name}" "${group}")" \ 2>/dev/null \ ; done - scwrypts.environment.user.get \ - --environment-name $ENVIRONMENT_NAME \ - > "$TEMP_CONFIG_FILE" + scwrypts.environment.get-enriched-user-yaml \ + --environment-name "${environment_name}" \ + > "${TEMP_CONFIG_FILE}" - scwrypts.environment.update.update-user-configs "$(cat "$TEMP_CONFIG_FILE")" + scwrypts.environment.update.update-user-configs "${environment_name}" } ${scwryptsmodule}.edit.delete() { touch "$TEMP_CONFIG_FILE" - local ERRORS=0 GROUP GROUP_CONFIG_FILENAME - for GROUP in ${SCWRYPTS_GROUPS[@]} + local ERRORS=0 group group_config_filename + for group in ${SCWRYPTS_GROUPS[@]} do - local GROUP_CONFIG_FILENAME="$(scwrypts.environment.common.get-env-filename "$ENVIRONMENT_NAME" "$GROUP")" - [ -f "$GROUP_CONFIG_FILENAME" ] || { - echo.status "nothing to cleanup for $ENVIRONMENT_NAME/$GROUP" + local group_config_filename="$(scwrypts.environment.common.get-env-file "${environment_name}" "${group}")" + [ -f "${group_config_filename}" ] || { + echo.status "nothing to cleanup for ${environment_name}/${group}" continue } - rm -- "$GROUP_CONFIG_FILENAME" \ - && echo.success "deleted '$GROUP_CONFIG_FILENAME'" \ - || echo.error "unable to delete '$GROUP_CONFIG_FILENAME'" \ + rm -- "${group_config_filename}" \ + && echo.success "deleted '${group_config_filename}'" \ + || echo.error "unable to delete '${group_config_filename}'" \ ; done - return $ERRORS + return "${ERRORS}" } ##################################################################### @@ -203,55 +212,54 @@ ${scwryptsmodule}.edit.delete() { export __SCWRYPTS_ENVIRONMENT__WORKFLOW_IS_CHANGE_SAFE=false ${scwryptsmodule}.update-user-configs() { - local NEW_CONFIGURATION="$1" - [ $NEW_CONFIGURATION ] || return 1 - - local ENVIRONMENT_NAME="$2" - [ $ENVIRONMENT_NAME ] || return 2 + local environment_name="${1}" + [[ "${environment_name}" ]] || return 2 # reinject all metadata, since the update function is allowed to strip it - NEW_CONFIGURATION="$( - { - scwrypts.environment.user.get-full-template-with-value-keys --environment "$ENVIRONMENT_NAME" - echo --- - echo "$NEW_CONFIGURATION" - } | scwrypts.environment.common.combine-template-files - )" + local new_configuration="$({ + scwrypts.environment.get-full-template.with-value-keys --environment "${environment_name}" + echo --- + cat -- "${TEMP_CONFIG_FILE}" + } | scwrypts.environment.common.combine-template-files)" + [[ "${new_configuration}" ]] || return 1 - local METADATA_DELETE_QUERY="$( - echo "$NEW_CONFIGURATION" \ + + local metadata_delete_query="$( + echo "${new_configuration}" \ | sed -n 's/^\s\+\(\.[-A-Za-z_:]\+\):.*$/ | del(.. | select(has("\1"))."\1")/p' \ | sort --unique \ )" - local GROUP GROUP_CONFIG_FILENAME GROUP_CONFIG - for GROUP in ${SCWRYPTS_GROUPS[@]} + local group group_config_filename group_config + for group in ${SCWRYPTS_GROUPS[@]} do - local GROUP_CONFIG="$(echo "$NEW_CONFIGURATION" \ + local group_config="$(echo "${new_configuration}" \ | utils.yq ". | del(.. | select(has(\".PARENTVALUE\") and has(\"value\") and .\".PARENTVALUE\" == .value)) | del(.. | select(has(\".PARENTSELECTION\") and has(\"selection\") and .\".PARENTSELECTION\" == .selection)) - | del(.. | select(has(\".GROUP\") and .\".GROUP\" != \"$GROUP\")) + | del(.. | select(has(\".GROUP\") and .\".GROUP\" != \"${group}\")) | del(.. | select(has(\"selection\") and (.selection == null or (.selection | length) == 0)).selection) | del(.. | select(has(\"value\") and .value == null).value) - $METADATA_DELETE_QUERY + ${metadata_delete_query} " \ )" - while echo "$GROUP_CONFIG" | grep -q '{}' + while echo "$group_config" | grep -q '{}' do - GROUP_CONFIG="$(echo "$GROUP_CONFIG" | utils.yq 'del(.. | select(tag == "!!map" and length == 0))')" + group_config="$(echo "${group_config}" | utils.yq 'del(.. | select(tag == "!!map" and length == 0))')" done - [ "$GROUP_CONFIG" ] || GROUP_CONFIG='# no configuration set' + [ "${group_config}" ] || group_config='# no configuration set' - echo "--- # $ENVIRONMENT_NAME > $GROUP\n$GROUP_CONFIG" > "$(scwrypts.environment.common.get-env-filename "$ENVIRONMENT_NAME" "$GROUP")" + echo "--- # ${environment_name} > ${group}\n${group_config}" > "$(scwrypts.environment.common.get-env-file "${environment_name}" "${group}")" done - [[ $ENVIRONMENT_NAME =~ ^$SCWRYPTS_ENV$ ]] && [[ $__SCWRYPTS_ENVIRONMENT__WORKFLOW_IS_CHANGE_SAFE =~ false ]] && { + [[ "${environment_name}" == "${SCWRYPTS_ENV}" ]] && [[ "${__SCWRYPTS_ENVIRONMENT__WORKFLOW_IS_CHANGE_SAFE}" =~ false ]] && { echo.warning "current scwrypts environment has changed" export __SCWRYPTS_ENVIRONMENT__USER_ENVIRONMENT= } + zsh-cache.reset.all + return 0 } diff --git a/zsh/scwrypts/environment/user.module.zsh b/zsh/scwrypts/environment/user.module.zsh deleted file mode 100644 index 15693d8..0000000 --- a/zsh/scwrypts/environment/user.module.zsh +++ /dev/null @@ -1,276 +0,0 @@ -##################################################################### - -use scwrypts/environment/common - -use scwrypts/environment/get-full-template -use scwrypts/environment/get-envvar-lookup-map - -use scwrypts/cache-output - -##################################################################### - -${scwryptsmodule}.get() { - eval "$(usage.reset)" - local USAGE__description=" - Generates a metadata-enriched environment YAML for the target environment. - " \ - CACHE_ARGS=() \ - ENVIRONMENT_NAME \ - PARSERS=( - scwrypts.cache-output.zshparse.args - scwrypts.environment.user.zshparse.env-name - ) - - eval "$ZSHPARSEARGS" - - ########################################## - - scwrypts.cache-output ${CACHE_ARGS[@]} \ - --cache-file environment.user.yaml \ - -- \ - scwrypts.environment.user.get.helper "$ENVIRONMENT_NAME" \ - ; -} - - -${scwryptsmodule}.get-shell-values() { - eval "$(usage.reset)" - local USAGE__description=" - used primarily by utils.environment.check in scwrypts environments - - returns yaml which contains shell-compatible lookup values - - moving inherited .PARENTVALUE to value (if value is empty) - - moving inherited .PARENTSELECTION to selection (if selection is empty) - - converting list 'values' to shell-arrays - " \ - CACHE_ARGS=() \ - ENVIRONMENT_NAME \ - PARSERS=( - scwrypts.cache-output.zshparse.args - scwrypts.environment.user.zshparse.env-name - ) - - eval "$ZSHPARSEARGS" - - ########################################## - - scwrypts.cache-output ${CACHE_ARGS[@]} \ - --cache-file environment.shell.yaml \ - -- \ - _SCWRYPTS_ENVIRONMENT__CONVERT_SHELL_VALUES \ - --environment-name "$ENVIRONMENT_NAME" \ - ; -} - -${scwryptsmodule}.get-json() { - eval "$(usage.reset)" - local USAGE__description=" - returns a JSON object containing live environment configurations - for the target environment - - contains both .path.style.lookup path and ENVIRONMENT_VARIABLE lookup keys - " \ - CACHE_ARGS=() \ - ENVIRONMENT_NAME \ - PARSERS=( - scwrypts.cache-output.zshparse.args - scwrypts.environment.user.zshparse.env-name - ) - - eval "$ZSHPARSEARGS" - - ########################################## - - scwrypts.cache-output ${CACHE_ARGS[@]} \ - --cache-file environment.user.json \ - -- \ - scwrypts.environment.user.get-json.helper \ - --environment-name "$ENVIRONMENT_NAME" \ - ; -} - -##################################################################### - -${scwryptsmodule}.zshparse.env-name() { - # local ENVIRONMENT_NAME - local PARSED=0 - case $1 in - ( --environment-name ) - PARSED=2 - ENVIRONMENT_NAME=$2 - - # bypass cache when specifying explicit environment name - [[ ${(t)CACHE_ARGS} =~ array ]] && CACHE_ARGS+=(--use-cache bypass) - ;; - esac - return $PARSED -} - -${scwryptsmodule}.zshparse.env-name.usage() { - USAGE__options+="\n - --environment-name name of a scwrypts environment (default: $SCWRYPTS_ENV) - using this flag will bypass cache - " -} - -${scwryptsmodule}.zshparse.env-name.validate() { - [ "$ENVIRONMENT_NAME" ] || ENVIRONMENT_NAME="$SCWRYPTS_ENV" -} - -##################################################################### - -${scwryptsmodule}.get.helper() { - local ENVIRONMENT_NAME LOADING_ORIGINAL_ENV=true - local _S ERRORS=0 - while [[ $# -gt 0 ]] - do - _S=1 - case $1 in - ( --parent ) LOADING_ORIGINAL_ENV=false - ;; - ( * ) - : \ - && [ ! $ENVIRONMENT_NAME ] \ - || echo.error "unknown argument '$1'" \ - || break - - [ $ENVIRONMENT_NAME ] || { ENVIRONMENT_NAME="$1"; break; } - esac - shift $_S - done - - utils.check-errors || return $? - - local ENVIRONMENT_NAME="$1" - [ $ENVIRONMENT_NAME ] || ENVIRONMENT_NAME=$SCWRYPTS_ENV - - [[ $LOADING_ORIGINAL_ENV =~ true ]] && { - case $SCWRYPTS_ENVIRONMENT__SHOW_ENV_HELP in - ( true ) - echo " - # - # current scwrypts environment = $ENVIRONMENT_NAME - # - # - metadata tags are READONLY; changes to any key which starts with a - # '.' followed by all-caps (e.g. '.DESCRIPTION') will be ignored on - # save - # - # - value precedence is as follows (lower number is higher priority): - # 0. runtime environment variable '__override' value - # 1. the value from the 'value' key - # 2. the value from the '.PARENTVALUE' (if 'value' key is null) - # 3. a user-selected value from the 'selection' list - # 4. a user-selected value from the '.PARENTSELECTION' list - # - # - values are used like environment variables, although most scalar - # values are permitted, they are converted to strings or string arrays - # before use; string 'null' is OK, but null-type means not configured - # - # - for 'value' null-type / empty = not configured - # - for 'selection' null-type / empty / empty-list = not configured - # - --- - " - ;; - ( false ) - echo "--- # current scwrypts environment = $ENVIRONMENT_NAME" - ;; - esac | sed 's/\(^\s\+\|\s\+$\)//g; /^$/d' - } - - local GROUP GROUP_CONFIG_FILENAME - { - scwrypts.environment.user.get-full-template-with-value-keys - [[ $LOADING_ORIGINAL_ENV =~ true ]] && { - for PARENT in $(scwrypts.environment.common.get-parent-env-names "$ENVIRONMENT_NAME") - do - echo --- - scwrypts.environment.user.get.helper --parent "$PARENT" \ - | sed ' - s/^\(\s\+\)\(value\|selection\):/\1.PARENT\U\2:/ - ' \ - | utils.yq '. - | del(.. | select(has(".PARENTVALUE") and .".PARENTVALUE" == null).".PARENTVALUE") - | del(.. | select(has(".PARENTSELECTION") and (.".PARENTSELECTION" | length) == 0).".PARENTSELECTION") - | del(.. | select(has(".PARENTSELECTION") and has(".PARENTVALUE")).".PARENTSELECTION") - ' \ - ; - done - } - for GROUP in ${SCWRYPTS_GROUPS[@]} - do - GROUP_CONFIG_FILENAME="$(scwrypts.environment.common.get-env-filename "$ENVIRONMENT_NAME" "$GROUP")" - - [ -f "${GROUP_CONFIG_FILENAME}" ] || touch "${GROUP_CONFIG_FILENAME}" - - [[ $(head -n1 "$GROUP_CONFIG_FILENAME") =~ ^---$ ]] || echo --- - cat "$GROUP_CONFIG_FILENAME" - done - } \ - | scwrypts.environment.common.combine-template-files \ - | utils.yq -P \ - | sed -z 's/\n[a-z]/\n&/g' \ - | sed 's/value: null$/value:/; /\svalue:/G' \ - ; -} - -${scwryptsmodule}.get-full-template-with-value-keys() { - scwrypts.environment.get-full-template \ - | utils.yq '(.. | select(has(".ENVIRONMENT"))) += { - "selection": [], - "value": null - } - ' \ - | sed 's/ ""$//' -} - -##################################################################### - -_SCWRYPTS_ENVIRONMENT__CONVERT_SHELL_VALUES() { - scwrypts.environment.user.get $@ \ - | utils.yq '.. - |= select( - ((has ("value") | not) or .value == null) and has (".PARENTVALUE") - ).value = .".PARENTVALUE" - ' \ - | utils.yq '.. - |= select( - ((has ("selection") | not) or .selection == null or .selection | length == 0) and has (".PARENTVALUE") - ).value = .".PARENTVALUE" - ' \ - | utils.yq '.. - |= select( - has("value") and .value | type == "!!seq" and .value | length != 0 - ).value = "(" + (.value | join " ") + ")" - ' \ - | utils.yq '. - | del(.. | select(has(".PARENTVALUE")).".PARENTVALUE") - | del(.. | select(has(".ENVIRONMENT")).".ENVIRONMENT") - | del(.. | select(has(".GROUP")).".GROUP") - | del(.. | select(has(".DESCRIPTION")).".DESCRIPTION") - | del(.. | select(has("value") and .value == null).value) - | del(.. | select(has("selection") and (.selection == null or .selection | length == 0)).selection) - | del(.. | select(has("value") and has("selection")).selection) - ' \ - ; -} - -##################################################################### - -${scwryptsmodule}.get-json.helper() { - local SHELL_VALUES="$(_SCWRYPTS_ENVIRONMENT__CONVERT_SHELL_VALUES $@)" - local LOOKUP_MAP="$(scwrypts.environment.get-envvar-lookup-map)" - { - echo "configuration:" - echo "${SHELL_VALUES}" | sed 's/^/ /' - local ENVIRONMENT_VARIABLE - echo "environment:" - for ENVIRONMENT_VARIABLE in $(\ - scwrypts.environment.get-full-template \ - | utils.yq '.. | select(has(".ENVIRONMENT")) | .".ENVIRONMENT"' \ - ) - do - echo " ${ENVIRONMENT_VARIABLE}: $(echo "${SHELL_VALUES}" | utils.yq -r "$(echo "${LOOKUP_MAP}" | utils.yq -r ".${ENVIRONMENT_VARIABLE}")")" - done - } | utils.yq -oj -} diff --git a/zsh/scwrypts/get-realpath.module.zsh b/zsh/scwrypts/get-realpath.module.zsh index 64ebe34..3b9df6d 100644 --- a/zsh/scwrypts/get-realpath.module.zsh +++ b/zsh/scwrypts/get-realpath.module.zsh @@ -1,3 +1,5 @@ +DEPENDENCIES+=(readlink envsubst) + ${scwryptsmodule}() { # # returns the fully-qualified path from a user-specified, relative reference @@ -5,13 +7,20 @@ ${scwryptsmodule}() { # # also uses 'readlink --canonicalize' to read through symlinks to the realpath # + local DEBUG_ARGS=() TRACE_ARGS=(-v path_0_raw -v path_1_evaluated -v path_2_target) - [ "$1" ] || return 1 + local path_0_raw="${1}" + local path_1_evaluated="$(echo "${1}" | envsubst)" + local path_2_target="$( + case "${path_1_evaluated}" in + ( /* | ~/* ) echo "${path_1_evaluated}" ;; + ( * ) echo "${EXECUTION_DIR}/${path_1_evaluated}" ;; + esac + )" - [[ ! $1 =~ ^[/~] ]] \ - && echo $(readlink --canonicalize -- "${EXECUTION_DIR}/$1") \ - || echo "$1" \ - ; + echo.trace - return 0 + [[ "${path_2_target}" ]] || return 1 + + readlink --canonicalize -- "${path_2_target}" } diff --git a/zsh/scwrypts/list-available.module.zsh b/zsh/scwrypts/list-available.module.zsh index 1e97483..d7da43e 100644 --- a/zsh/scwrypts/list-available.module.zsh +++ b/zsh/scwrypts/list-available.module.zsh @@ -12,7 +12,7 @@ ${scwryptsmodule}() { GROUP_TYPE=$(scwrypts.config.group ${GROUP} type) [ ${GROUP_TYPE} ] && MINDEPTH=1 && GROUP_TYPE="${GROUP_TYPE}\\/" || MINDEPTH=2 - command -v SCWRYPTS_GROUP_CONFIGURATION__${GROUP}.list-available >/dev/null 2>&1 \ + command -v SCWRYPTS_GROUP_CONFIGURATION__${GROUP}.list-available &>/dev/null \ && LOOKUP=SCWRYPTS_GROUP_CONFIGURATION__${GROUP}.list-available \ || LOOKUP=scwrypts.list-available.default \ ; diff --git a/zsh/scwrypts/logs/clear b/zsh/scwrypts/logs/clear index a717379..dcaa01a 100755 --- a/zsh/scwrypts/logs/clear +++ b/zsh/scwrypts/logs/clear @@ -12,7 +12,7 @@ MAIN() { echo.status "found ${LOGFILE_COUNT} log files" echo.warning "this will permanently delete everything in\n${SCWRYPTS_LOG_PATH}" - utils.yN 'continue?' || utils.abort + user.yN 'continue?' || utils.abort rm -rf "${SCWRYPTS_LOG_PATH}" \ && echo.success 'all logfiles removed' \ diff --git a/zsh/scwrypts/rebuild-cache b/zsh/scwrypts/rebuild-cache index 901b5d9..9797c1b 100755 --- a/zsh/scwrypts/rebuild-cache +++ b/zsh/scwrypts/rebuild-cache @@ -30,7 +30,7 @@ MAIN() { ( 0 ) ARGS=(${SCWRYPTS_GROUPS[@]}) ;; esac - local GROUP GROUP_ROOT GROUP_TYPE GROUP_MODULES GROUP_MODULE CACHE_DIR + local GROUP GROUP_ROOT GROUP_TYPE GROUP_MODULES GROUP_MODULE CACHE_DIR for GROUP in ${ARGS[@]} do GROUP_ROOT="$(scwrypts.config.group ${GROUP} zshlibrary)" @@ -83,7 +83,7 @@ MAIN.parse() {} MAIN.parse.validate() { [[ ${ZSHIMPORT_USE_CACHE} =~ true ]] \ || echo.error "ZSHIMPORT_USE_CACHE is set to 'false' so I cannot rebuild the cache" - + [ "${ZSHIMPORT_CACHE_DIR}" ] && [ -d "${ZSHIMPORT_CACHE_DIR}" ] \ || echo.error "I cannot find the ZSHIMPORT_CACHE_DIR '${ZSHIMPORT_CACHE_DIR}'" } diff --git a/zsh/scwrypts/zshparse.module.zsh b/zsh/scwrypts/zshparse.module.zsh new file mode 100644 index 0000000..59791ac --- /dev/null +++ b/zsh/scwrypts/zshparse.module.zsh @@ -0,0 +1,20 @@ +${scwryptsmodule}.environment-name.locals() { + local environment_name="${SCWRYPTS_ENV}" +} + +${scwryptsmodule}.environment-name() { + local p=0 + + case "${1}" in + ( --environment-name ) p=2; environment_name="${2}" ;; + esac + + return ${p} +} + +${scwryptsmodule}.environment-name.usage() { + USAGE__options+="\n + --environment-name name of a scwrypts environment; when unspecified, uses current + (current: ${SCWRYPTS_ENV}) + " +} diff --git a/zsh/unittest/execute-test-file.module.zsh b/zsh/unittest/execute-test-file.module.zsh index df7c22d..197324a 100644 --- a/zsh/unittest/execute-test-file.module.zsh +++ b/zsh/unittest/execute-test-file.module.zsh @@ -6,7 +6,7 @@ use unittest/operations ${scwryptsmodule}() { local ERRORS=0 TEST_ERRORS=0 - [ "$SCWRYPTS_LOG_LEVEL" ] || SCWRYPTS_LOG_LEVEL=4 + [[ "$SCWRYPTS_LOG_LEVEL" ]] || local SCWRYPTS_LOG_LEVEL=4 local UNITTESTS=($(echo "${(k)functions}" | sed 's/ /\n/g' | grep '^test\.' | sort)) [[ ${#UNITTESTS[@]} -gt 0 ]] \ diff --git a/zsh/utils/apps/10-zsh.zsh b/zsh/utils/apps/10-zsh.zsh new file mode 100644 index 0000000..8d5b753 --- /dev/null +++ b/zsh/utils/apps/10-zsh.zsh @@ -0,0 +1,17 @@ +zsh.is-in-array() { + # works like the terse "${a[(re)v]}" syntax; + # checks the array returns either + # '' (blank) when no match is found + # ${value} when an exact match is found + local array_variable="${1}" + local value="${2}" + + [[ "${(Pt)array_variable}" =~ array ]] \ + || echo.error "${array_variable} is not an array" \ + || return 1 + + local zsh_output="${${(P@)array_variable}[(re)${value}]}" + + echo "${zsh_output}" + [[ "${zsh_output}" ]] +} diff --git a/zsh/utils/apps/70-wrapper.zsh b/zsh/utils/apps/70-wrapper.zsh deleted file mode 100644 index 8b13789..0000000 --- a/zsh/utils/apps/70-wrapper.zsh +++ /dev/null @@ -1 +0,0 @@ - diff --git a/zsh/utils/apps/99-fzf.zsh b/zsh/utils/apps/99-fzf.zsh index b558281..fda4a56 100644 --- a/zsh/utils/apps/99-fzf.zsh +++ b/zsh/utils/apps/99-fzf.zsh @@ -1,43 +1,191 @@ utils.fzf() { - [ ${CI} ] && utils.fail 1 'currently in CI, but utils.fzf requires user input' + case "$(normalize.boolean CI)" in + ( true ) + echo.error "currently in CI, but utils.fzf requires user input" + return 1 + ;; + esac + ########################################## - local FZF_ARGS=( - -i - --ansi - --bind=ctrl-c:cancel - --height=50% - --layout=reverse - ) + local PARSERS=() DESCRIPTION=' + provides a consistent interface to fzf by unifying display settings + and assigning sensible defaults + returns 0 only when a non-empty selection is made by the user + ' - local SELECTION=$(fzf ${FZF_ARGS[@]} --prompt "$1 : " ${@:2} 2>/dev/tty) - echo.prompt "$1" + eval "$(utils.parse.autosetup)" - [ ${BE_QUIET} ] || { - [[ ${SCWRYPTS_LOG_LEVEL} -ge 1 ]] && echo ${SELECTION} >&2 - } - echo ${SELECTION} - [ ${SELECTION} ] + local DEBUG_ARGS=(-v user_prompt -v quiet -v selection) TRACE_ARGS=(-v fzf_args) + + [[ "${BE_QUIET}" ]] && echo.warning.deprecation v5.3 'use of BE_QUIET= will be removed' + + ########################################## + + local selection="$(fzf "${fzf_args[@]}" 2>/dev/tty)" + + echo.trace + + case "${quiet}" in + ( true ) ;; + ( false ) echo.prompt "${user_prompt}" --response "${(q)selection}" ;; + esac + + echo "${selection}" + [[ "${selection}" ]] } -utils.fzf.user-input() { # allow user to type custom answers; reconfirm if ambiguous with select - local FZF_OUTPUT=$(BE_QUIET=1 utils.fzf $@ --print-query | sed '/^$/d' | sort -u) - [[ ${SCWRYPTS_LOG_LEVEL} -ge 1 ]] && echo ${FZF_OUTPUT} | head -n1 >&2 - [ ! ${FZF_OUTPUT} ] && return 1 +utils.fzf.parse.locals() { + local fzf_args=(-i --ansi --bind=ctrl-c:cancel --height=50% --layout=reverse) + local user_prompt + local user_prompt_delimiter=' : ' + local quiet="${BE_QUIET:-false}" +} - [[ $(echo "${FZF_OUTPUT}" | wc -l) -eq 1 ]] \ - && { echo "${FZF_OUTPUT}"; return 0; } +utils.fzf.parse() { + local parsed=0 - local FZF_OUTPUT=$( - echo "${FZF_OUTPUT}" \ + case "${1}" in + ( --prompt ) parsed=2 + # add the user prompt back in as args to fzf + [[ "${user_prompt}" ]] && fzf_args+=("${user_prompt}") + + user_prompt="${2}" + ;; + + ( --quiet ) parsed=1 + quiet=true + ;; + + ( * ) parsed=1 + case "$((POSITIONAL_ARGS+=1))" in + ( 1 ) [[ "${user_prompt}" ]] && fzf_args+=("${1}") || user_prompt="${1}" ;; + ( * ) fzf_args+=("${1}") ;; + esac + ;; + esac + + return "${parsed}" +} + +utils.fzf.parse.validate() { + [[ "${user_prompt}" ]] || user_prompt='make a selection' + fzf_args+=(--prompt "${user_prompt}${user_prompt_delimiter}") +} + +##################################################################### + +utils.fzf.user-input() { # allow user to type custom answers; reconfirm if ambiguous with select + local fzf_output="$(utils.fzf "${@}" --quiet --print-query | grep . | sort -u)" + [[ "${fzf_output}" ]] || return 1 + + # only 1 line = non-ambiguous + [[ $(echo "${fzf_output}" | wc -l) -eq 1 ]] \ + && echo "${fzf_output}" \ + && return 0 + + local fzf_output="$( + echo "${fzf_output}" \ | sed "1s/\$/^$(utils.colors.print light-gray '<- what you typed')/" \ | sed "2s/\$/^$(utils.colors.print light-gray '<- what you selected')/" \ | column -ts '^' \ - | BE_QUIET=1 utils.fzf "$@ (clarify)" \ - ) + | utils.fzf --quiet "${@}" '(clarify)' \ + | sed 's/\s\+<- what you .*$//' \ + )" - [[ ${SCWRYPTS_LOG_LEVEL} -ge 1 ]] && echo ${FZF_OUTPUT} >&2 - FZF_OUTPUT=$(echo ${FZF_OUTPUT} | sed 's/\s\+<- what you .*$//') - echo ${FZF_OUTPUT} - [ ${FZF_OUTPUT} ] + echo "${fzf_output}" + [[ "${fzf_output}" ]] +} + +##################################################################### + +utils.fzf.file-select() { + local PARSERS=() DESCRIPTION=' + given a directory, provides a user-friendly file selection interface + ' + + eval "$(utils.parse.autosetup)" + + ########################################## + + local fzf_output="$( + cd -- "${directory}" + find . "${find_args[@]}" \ + | sed 's|^\./||' \ + | utils.fzf "${fzf_args[@]}" \ + )" + + [[ "${fzf_output}" ]] && fzf_output="${directory}/${fzf_output}" + + echo "${fzf_output}" + [[ "${fzf_output}" ]] +} + +utils.fzf.file-select.parse.locals() { + local ARGS=() + local directory + local user_prompt + local fzf_args=() + local find_args=() + local filetypes=() + local mindepth=1 + local maxdepth=1 +} + +utils.fzf.file-select.parse() { + local parsed=0 + + case "${1}" in + ( -t | --filetype ) parsed=2; filetypes+=("${2}") ;; + ( -p | --prompt ) parsed=2; user_prompt="${2}" ;; + + ( --mindepth ) parsed=2; mindepth="${2}" ;; + ( --maxdepth ) parsed=2; maxdepth="${2}" ;; + esac + + return "${parsed}" +} + +utils.fzf.file-select.parse.usage() { + USAGE__options+=' + -t, --filetype file extension to include; multiple can be specified + -p, --prompt fzf prompt to the user (default: "select a file from ${directory}") + + --mindepth minimum find depth; unset with 0 (default: 1) + --maxdepth maximum find depth; unset with 0 (default: 1) + ' + + USAGE__args+=' + $1 the directory to target + $2..$N remaining arguments are forwarded to utils.fzf + ' +} + +utils.fzf.file-select.parse.validate() { + directory="$(readlink --canonicalize -- "${ARGS[1]}")" + [[ "${directory}" ]] && [[ -d "${directory}" ]] \ + || echo.error "missing or invalid directory '${ARGS[1]}'" -v directory + + [[ "${user_prompt}" ]] || user_prompt="select a file from ${directory}" + fzf_args=(--prompt "${user_prompt}" "${ARGS[@]:1}") + + [[ "${mindepth}" -le "${maxdepth}" ]] \ + || echo.error "invalid min/max depth setting: mindepth=${(q)mindepth} maxdepth=${(q)maxdepth}" + + local depth_flag + for depth_flag in mindepth maxdepth + do + case "${(P)depth_flag}" in + ( 0 ) ;; + ( * ) find_args+=("-${depth_flag}" "${(P)depth_flag}") + esac + done + + find_args+=(-type f) + + local filetype + for filetype in "${filetypes[@]}" + do + find_args+=(-o -name "\*.${filetype}" -o -name "\*.${filetype:u}") + done } diff --git a/zsh/utils/apps/99-notify-send.zsh b/zsh/utils/apps/99-notify-send.zsh index 1ea9693..d14ca0b 100644 --- a/zsh/utils/apps/99-notify-send.zsh +++ b/zsh/utils/apps/99-notify-send.zsh @@ -1,13 +1,13 @@ utils.notify-send() { - utils.dependencies.check notify-send &>/dev/null || { - echo.warning "missing 'notify-send'; cannot send desktop notifications" + utils.dependencies.check notify-send --optional &>/dev/null || { + echo.warning "cannot send desktop notifications" utils.notify-send() { true; } return 0 } [ "${SCWRYPT_GROUP}" ] && [ "${SCWRYPT_NAME}" ] \ - && local TITLE="scwrypts/${SCWRYPT_GROUP} ${SCWRYPT_NAME}" \ - || local TITLE="zsh" + && local title="scwrypts/${SCWRYPT_GROUP} ${SCWRYPT_NAME}" \ + || local title="zsh" - notify-send "${TITLE}" $@ + notify-send "${title}" "${@}" } diff --git a/zsh/utils/apps/99-sha1sum.zsh b/zsh/utils/apps/99-sha1sum.zsh new file mode 100644 index 0000000..cbb0997 --- /dev/null +++ b/zsh/utils/apps/99-sha1sum.zsh @@ -0,0 +1,9 @@ +utils.sha1sum() { + # only output the sha1 hash (normally output includes filename too) + sha1sum | awk '{print $1}' +} + +utils.sha1sum.filelist() { + # provides a single sha1 hash for a list of input files + sort | xargs sha1sum | utils.sha1sum +} diff --git a/zsh/utils/apps/99-yq.zsh b/zsh/utils/apps/99-yq.zsh index cbb4d99..d9ee7fa 100644 --- a/zsh/utils/apps/99-yq.zsh +++ b/zsh/utils/apps/99-yq.zsh @@ -1,8 +1,15 @@ -utils.yq() { - yq --version | grep -q mikefarah || { - yq $@ # this is a different version from the preferred but throwing this in for compatibility - return $? - } +yq --version | grep -q mikefarah && { + utils.yq() { yq eval '... comments=""' | yq "${@}"; } +} || { + echo.warning ' + The utils.yq helper expects mikefarah/yq, but you appear to have kislyuk/yq or another flavor. + Compatibility may vary. + ' - yq eval '... comments=""' | yq $@ + utils.yq() { yq "${@}"; } } + +utils.yq.combine-files() { utils.yq \ + | utils.yq eval-all '. as $item ireduce ({}; . * $item)' \ + | utils.yq 'sort_keys(..)' \ +; } diff --git a/zsh/utils/dependencies.zsh b/zsh/utils/dependencies.zsh index e8fa0db..039c646 100644 --- a/zsh/utils/dependencies.zsh +++ b/zsh/utils/dependencies.zsh @@ -1,64 +1,130 @@ utils.dependencies.check-all() { - local DEP ERRORS=0 - local SCWRYPTS_LOG_LEVEL=1 - [ ! ${E} ] && E=echo.error + local ERRORS=0 + [[ "${SCWRYPTS_LOG_LEVEL}" ]] || local SCWRYPTS_LOG_LEVEL=1 - DEPENDENCIES=($(echo ${DEPENDENCIES} | sed 's/ \+/\n/g' | sort -u)) + DEPENDENCIES+=(sed awk grep find readlink) + DEPENDENCIES=($(echo ${DEPENDENCIES[@]} | sed 's/ \+/\n/g' | sort -u)) - for DEP in ${DEPENDENCIES[@]}; do utils.dependencies.check ${DEP} || ((ERRORS+=1)); done - utils.dependencies.check-coreutils || ((ERRORS+=$?)) + local dependency + for dependency in ${DEPENDENCIES[@]} + do + utils.dependencies.check "${dependency}" || ((ERRORS+=1)) + done return ${ERRORS} } utils.dependencies.check() { - local DEPENDENCY="$1" - [ ! ${DEPENDENCY} ] && return 1 - command -v ${DEPENDENCY} >/dev/null 2>&1 || { - [[ ${OPTIONAL} -eq 1 ]] \ - && echo.warning "application '$1' preferred but not available on PATH $(utils.dependencies.credits $1)" \ - || echo.error "application '$1' required but not available on PATH $(utils.dependencies.credits $1)" \ - ; + local ERRORS=0 + + local dependency + local dependency_default_args=() + local dependency_required=true + + local _s _p + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( --optional ) dependency_required=false ;; + ( * ) + case $((_p+=1)) in + ( 1 ) dependency="${1}" ;; + ( * ) dependency_default_args+=("${1}") ;; + esac + esac + shift ${_s} || echo.error "missing required argument for '${1}'" || shift "${#}" + done + unset _s _p + + [[ "${dependency}" ]] || echo.error 'no dependency specified' + + [[ "${ERRORS}" -eq 0 ]] || return "${ERRORS}" + + ########################################## + + local executable_path="$(command -v "${dependency}" 2>/dev/null)" + [[ "${executable_path}" ]] || { + case "${dependency_required}" in + ( true ) echo.error "application '${dependency}' required but not available on PATH" ;; + ( false ) echo.warning "application '${dependency}' preferred but not available on PATH" ;; + esac + + local credits="$(utils.dependencies.credits "${dependency}")" + [[ "${credits}" ]] && echo.reminder "${credits}" + return 1 } - - command -v utils.wrapper.${DEPENDENCY} &>/dev/null && { - utils.wrapper.${DEPENDENCY} \ - || echo.error "failed to define required context wrapper for '$1'" \ - || return 1 \ - ; + [[ "${dependency}" =~ "^${executable_path}$" ]] && { + # echo "built-in '${dependency}' cannot be wrapped for safety" >&2 + return 0 } - [[ ${DEPENDENCY} =~ ^yq$ ]] && { - yq --version | grep -q mikefarah \ - || echo.warning 'detected kislyuk/yq but mikefarah/yq is preferred (compatibility may vary)' - } + local automatic_trace=true + local extra_eval_statements='' - return 0 -} + case "${dependency}" in + ( yq ) + yq --version | grep -q mikefarah \ + || echo.warning 'detected kislyuk/yq but mikefarah/yq is required' + ;; -utils.dependencies.check-coreutils() { - local COREUTILS=(awk find grep sed readlink) - local MISSING_DEPENDENCY_COUNT=0 - local NON_GNU_DEPENDENCY_COUNT=0 + ( awk | sed | grep | find | readlink ) + automatic_trace=false + "${executable_path}" --version 2>&1 | grep 'GNU' | grep -qv 'BSD' || { + executable_path="$(command -v g${dependency})" + [[ "${executable_path}" ]] \ + || echo.error "unable to locate GNU ${dependency}; if you're on MacOS you may need to install the appropriate gnu-${dependency} package" + } + ;; - local UTIL - for UTIL in $(echo ${COREUTILS}) - do - utils.dependencies.check ${UTIL} || { ((MISSING_DEPENDENCY_COUNT+=1)); continue; } + ( gawk | gsed | ggrep | gfind | greadlink ) + echo.error "do not use MacOS / BSD shadows for utilities which require GNU (use ${dependency/g/} instead)" + ;; - ${UTIL} --version 2>&1 | grep 'GNU' | grep -qv 'BSD' || { - echo.warning "non-GNU version of ${UTIL} detected" - ((NON_GNU_DEPENDENCY_COUNT+=1)) - } - done + ( aws | eksctl ) + extra_eval_statements+=" + echo.warning 'use of ${dependency} directly is highly discouraged; utilize appropriate module instead' + " + case "${dependency}" in + ( aws ) extra_eval_statements+="echo.reminder 'use cloud/aws/cli : cloud.aws.cli'" ;; + ( * ) extra_eval_statements+="echo.reminder 'use cloud/aws/${dependency} : cloud.aws.${dependency}'" ;; + esac + ;; + esac || return "${ERRORS}" - [[ ${NON_GNU_DEPENDENCY_COUNT} -gt 0 ]] && { - echo.warning 'scripts rely on GNU coreutils; compatibility may vary' - utils.os.is-macos && echo.reminder 'GNU coreutils can be installed and linked through Homebrew' - } + eval "${dependency}() { + local DEBUG_ARGS=() TRACE_ARGS=() ERRORS=0 WARNINGS=0 + local default_args=(${(q)dependency_default_args[@]}) + local print_trace=${automatic_trace} + local args=() - return ${MISSING_DEPENDENCY_COUNT} + while [[ \${#} -gt 0 ]] + do + case \${1} in + ( --omit-default-args ) + default_args=() + ;; + + ( --no-scwrypts-trace ) + print_trace=false + ;; + + ( --force-scwrypts-trace ) + print_trace=true + ;; + + ( * ) args+=(\"\${1}\") ;; + esac + shift 1 + done + + [[ \"\${print_trace}\" =~ true ]] && echo.trace \"${(q-)executable_path} \${(q-)default_args[@]} \${(q-)args[@]}\" + + ${extra_eval_statements} + + '${executable_path}' \"\${default_args[@]}\" \"\${args[@]}\" + }" } utils.dependencies.credits() { diff --git a/zsh/utils/environment.zsh b/zsh/utils/environment.zsh index 696d3af..a86bc5e 100644 --- a/zsh/utils/environment.zsh +++ b/zsh/utils/environment.zsh @@ -1,19 +1,32 @@ utils.environment.check-all() { # checks all environment variables in REQUIRED_ENV=() - local SCWRYPTS_LOG_LEVEL=4 - local VAR ERRORS=0 + [[ "${SCWRYPTS_LOG_LEVEL}" ]] || local SCWRYPTS_LOG_LEVEL=4 + local ERRORS=0 WARNINGS=0 - REQUIRED_ENV=($(echo ${REQUIRED_ENV} | sed 's/\s\+/\n/g' | sort --unique)) + REQUIRED_ENV=($(printf '%s\n' "${REQUIRED_ENV[@]}" | sort --unique)) - for VAR in ${REQUIRED_ENV[@]}; do utils.environment.check ${VAR} || ((ERRORS+=1)); done + local configuration_variable_name + for configuration_variable_name in ${REQUIRED_ENV[@]} + do + utils.environment.check ${configuration_variable_name} || ((ERRORS+=1)) + done return ${ERRORS} } utils.environment.check() { - local NAME OPTIONAL DEFAULT_VALUE LOOKUP_PATH - local NAME_IS=environment-variable - local PRINT_VALUE=false + local ERRORS=0 WARNINGS=0 + local DEBUG_ARGS=() TRACE_ARGS=() + + local configuration_variable_name optional default_value user_json_query + DEBUG_ARGS+=(-v configuration_variable_name -v optional -v default_value) + TRACE_ARGS+=(-v user_json_query) + + local lookup_type + DEBUG_ARGS+=(-v lookup_type) + + local print_value_to_stdout=false + TRACE_ARGS+=(-v print_value_to_stdout) local USAGE=" usage: utils.environment.check [...options...] @@ -34,217 +47,273 @@ utils.environment.check() { configuration env var (even when lookup is a config lookup path) " - local _S ERRORS=0 POSITIONAL_ARGS=0 - while [[ $# -gt 0 ]] + local _s _p=0 + while [[ ${#} -gt 0 ]] do - _S=1 - case $1 in + _s=1 + case ${1} in ( -h | --help ) utils.io.usage return 0 ;; ( --optional ) - OPTIONAL=true + optional=true ;; ( --print-value ) - PRINT_VALUE=true + print_value_to_stdout=true ;; ( --default ) - [ $2 ] && ((_S+=1)) \ + [[ ${2} ]] && ((_S+=1)) \ || echo.error "missing env var default value" \ || break - [ ! "${DEFAULT_VALUE}" ] \ + [[ ! "${default_value}" ]] \ || echo.error "only one default value is supported" \ || break - [[ ${OPTIONAL} =~ true ]] \ + [[ ${optional} == true ]] \ && echo.warning "--optional and --default flags are redundant; remove '--optional' flag" - DEFAULT_VALUE="$2" - OPTIONAL=true + default_value="${2}" + optional=true ;; ( * ) - ((POSITIONAL_ARGS+=1)) - case ${POSITIONAL_ARGS} in - ( 1 ) NAME="$1" + case "$((_p+=1))" in + ( 1 ) configuration_variable_name="${1}" ;; - ( 2 ) DEFAULT_VALUE="$1" + ( 2 ) default_value="${1}" echo.warning "use of positional argument for default value is DEPRECATED\nplease use --default flag" ;; - ( * ) echo.error "unknown argument '$1'" + ( * ) echo.error "unknown argument '${1}'" ;; esac ;; esac - shift ${_S} + shift "${_s}" done + unset _s _p - [ "${NAME}" ] \ - || echo.error "must provide environment variable name" + [[ "${configuration_variable_name}" ]] \ + || echo.error "must provide configuration variable name" - [ "${OPTIONAL}" ] || OPTIONAL=false + [[ "${optional}" ]] || optional=false - echo "${NAME}" | grep -q '^\.' \ - && NAME_IS=lookup-path - - case ${NAME_IS} in - ( lookup-path ) - [ ${__SCWRYPT} ] || echo.error "lookup paths cannot be used outside of scwrypts (${NAME})" - [ ${__SCWRYPT} ] && { - use scwrypts/environment \ - || echo.error "unable to load lookup path" - - LOOKUP_PATH="${NAME}" - NAME=$(\ - scwrypts.environment.get-full-template \ - | utils.yq -r "${LOOKUP_PATH}.\".ENVIRONMENT\"" \ - | grep -v ^null$\ - ) - - [ ${NAME} ] || echo.error "no .ENVIRONMENT key is configured for '${LOOKUP_PATH}'" - } + case "${configuration_variable_name}" in + ( .* ) + lookup_type=query-path + [[ "${__SCWRYPT}" ]] \ + || echo.error "lookup paths cannot be used outside of scwrypts (${configuration_variable_name})" ;; - ( environment-variable ) + + ( * ) + lookup_type=environment-variable ;; esac - utils.check-errors --no-usage || return $? + utils.check-errors --no-usage || return "${?}" ########################################## - # only check env vars once - local ALREADY_CHECKED="$(eval echo '$'${NAME}'__checked')" - [ "${ALREADY_CHECKED}" ] && return ${ALREADY_CHECKED} + # only check env vars once; this is a headache, but serves a substantial performance purpose + local already_checked_status_suffix=scwrypts_checked_status + local already_checked_status_variable + TRACE_ARGS+=(-v already_checked_status_variable) - [ ${__SCWRYPT} ] || local CI=true # outside of scwrypts, environment must load like CI runtime - [ ${CI} ] && { - local VALUE="$(eval echo '$'${NAME})" - [ "${VALUE}" ] && { - export ${NAME}__checked=0 - [[ ${PRINT_VALUE} =~ true ]] && echo "${VALUE}" - return 0 - } + local already_checked_status + DEBUG_ARGS+=(-v already_checked_status) + case "${lookup_type}" in + ( query-path ) ;; # can't check early return status just yet + ( * ) + already_checked_status_variable="${configuration_variable_name}__${already_checked_status_suffix}" + already_checked_status="${(P)already_checked_status_variable}" + [[ "${already_checked_status}" ]] && { + # if the previous check failed, we can escape early + [[ "${already_checked_status}" -ne 0 ]] && echo.trace && return "${already_checked_status}" - case ${OPTIONAL} in - ( true ) - [ "${DEFAULT_VALUE}" ] \ - && export ${NAME}=${DEFAULT_VALUE} \ - || echo.warning "environment variable '${NAME}' is not set" \ - ; + # if we don't need to print, we can escape early + [[ "${print_value_to_stdout}" == false ]] && echo.trace && return "${already_checked_status}" + } + ;; + esac - export ${NAME}__checked=0 - return 0 - ;; - ( false ) - local ERROR_MESSAGE="missing required environment variable '${NAME}'" - [ "${LOOKUP_PATH}" ] && ERROR_MESSAGE+=" (config path '${LOOKUP_PATH}')" + # outside of scwrypts, environment must load like CI runtime + [[ "${__SCWRYPT}" ]] || local CI=true + DEBUG_ARGS+=(-v CI) - echo.error "${ERROR_MESSAGE}" - export ${NAME}__checked=1 - return 1 + local configuration_value configuration_values=() check_status=1 + TRACE_ARGS+=(-v configuration_value -v configuration_values) + case "$(normalize.boolean --variable CI --default false --mode echo)" in + ( true ) # in CI, all environment configuration MUST come from environment + case "${lookup_type}" in + ( query-path ) + use scwrypts/environment/get-user-json 2>/dev/null + + user_json_query=".configuration${configuration_variable_name}" + configuration_variable_name="$(scwrypts.environment.get-user-json 2>/dev/null | utils.yq -r "${user_json_query}.\".ENVIRONMENT\"" | grep -v ^null$)" + + [[ "${configuration_variable_name}" ]] \ + || configuration_variable_name="SCWRYPTS_CONFIG_$(echo "${user_json_query}" | sed 's/[-. ]/_/g' | tr '[:upper:]' '[:lower:]')" + + [[ "${configuration_variable_name}" ]] \ + || echo.error "no .ENVIRONMENT key is configured for ${user_json_query}, and I failed to autogenerate one" \ + || return 1 + ;; + esac + utils.environment.check.environment "${configuration_variable_name}" + ;; + + ( false ) + use scwrypts/environment/get-user-json \ + || echo.error "unable to load required environment library" \ + || return 1 + + case "${lookup_type}" in + ( environment-variable ) + user_json_query=".configuration$(scwrypts.environment.get-user-json | utils.yq -r ".lookup.${configuration_variable_name}" | grep -v ^null$ | sed 's/\.value$//')" + [[ "${user_json_query}" != '.configuration' ]] \ + || echo.error "unable to determine lookup value from variable '${configuration_variable_name}'" \ + || return 1 + ;; + + ( query-path ) + user_json_query=".configuration${configuration_variable_name}" + configuration_variable_name="$(scwrypts.environment.get-user-json | utils.yq -r "${user_json_query}.\".ENVIRONMENT\"" | grep -v ^null$)" + + [[ "${configuration_variable_name}" ]] \ + || configuration_variable_name="SCWRYPTS_CONFIG_$(echo "${user_json_query}" | sed 's/[-. ]/_/g' | tr '[:upper:]' '[:lower:]')" + + [[ "${configuration_variable_name}" ]] \ + || echo.error "no .ENVIRONMENT key is configured for ${user_json_query}, and I failed to autogenerate one" \ + || return 1 + + # now that we know the name of the variable, we can check early return status + already_checked_status_variable="${configuration_variable_name}__${already_checked_status_suffix}" + already_checked_status="${(P)already_checked_status_variable}" + [[ "${already_checked_status}" ]] && { + # if the previous check failed, we can escape early + [[ "${already_checked_status}" -ne 0 ]] && return "${already_checked_status}" + + # if we don't need to print, we can escape early + [[ "${print_value_to_stdout}" == false ]] && return "${already_checked_status}" + } + ;; + esac + + # ensure environment safety; prevent bleed in from user's runtime + [[ "${already_checked_status}" ]] || unset -- "${configuration_variable_name}" + + local configuration_json="$(scwrypts.environment.get-user-json | utils.yq "${user_json_query}")" + local configuration_value_type + DEBUG_ARGS+=(-v configuration_value_type) + + local get_method + for get_method in \ + environment \ + runtime-override \ + value \ + selection \ + ; + do + utils.environment.check.${get_method} "${configuration_variable_name}" "${configuration_json}" + [[ "${configuration_value}" ]] && break + [[ "${#configuration_values[@]}" -gt 0 ]] && break + done + ;; + esac + + [[ "${configuration_value}" ]] && check_status=0 && configuration_value_type=scalar + [[ "${#configuration_values[@]}" -gt 0 ]] && check_status=0 && configuration_value_type=array + + [[ "${check_status}" -eq 0 ]] || { + case "${optional}" in + ( false ) check_status=1 ;; + ( true ) check_status=0 + [[ "${default_value}" ]] \ + && configuration_value="${default_value}" \ + || echo.warning "environment variable '${configuration_variable_name}' is not set" ;; esac } - [ ! ${LOOKUP_PATH} ] && { - LOOKUP_PATH="$(scwrypts.environment.get-envvar-lookup-map | utils.yq -r ".${NAME}" | sed 's/\.value$//')" - } + export ${already_checked_status_variable}="${check_status}" - # ensure environment safety; prevent bleed in from user's runtime - unset ${NAME} - local VALUE - for GET_VALUE_METHOD in \ - runtimeoverride \ - value \ - selection \ - select \ - ; - do - VALUE=$(utils.environment.check.${GET_VALUE_METHOD} "${NAME}" "${LOOKUP_PATH}" 2>/dev/null) - [ "${VALUE}" ] && eval "export ${NAME}=${VALUE}" && break - done - - [ "${VALUE}" ] && { - export ${NAME}__checked=0 - [[ ${PRINT_VALUE} =~ true ]] && echo "${VALUE}" - return 0 - } - - case ${OPTIONAL} in - ( true ) - [ "${DEFAULT_VALUE}" ] \ - && export ${NAME}=${DEFAULT_VALUE} \ - || echo.warning "environment variable '${NAME}' is not set" \ - ; - - export ${NAME}__checked=0 - [[ ${PRINT_VALUE} =~ true ]] && echo "${DEFAULT_VALUE}" - return 0 + case "${configuration_value_type}" in + ( array ) + [[ "${print_value_to_stdout}" == true ]] && printf '%s\n' "${configuration_values[@]}" + eval "export ${configuration_variable_name}=(${(q)configuration_values[@]})" ;; - ( false ) - local ERROR_MESSAGE="missing required environment variable '${NAME}'" - [ "${LOOKUP_PATH}" ] && ERROR_MESSAGE+=" (config path '${LOOKUP_PATH}')" - echo.error "${ERROR_MESSAGE}" - export ${NAME}__checked=1 - return 1 + ( * ) + [[ "${print_value_to_stdout}" == true ]] && echo "${configuration_value}" + export ${configuration_variable_name}="${configuration_value}" ;; esac + + echo.trace + return "${check_status}" +} + +utils.environment.check.environment() { # pulls directly from environment : used in CI and after first check + local configuration_variable_name="${1}" + #local configuration_json="${2}" + + # used when we need to print the variable after it was already checked once + case "${(tP)configuration_variable_name}" in + ( '' ) ;; # no value set + + ( *array* ) configuration_values=("${(P@)configuration_variable_name}") ;; + ( * ) configuration_value="${(P)configuration_variable_name}" ;; + esac } -utils.environment.check.runtimeoverride() { - local NAME="$1" - local LOOKUP_PATH="$2" - eval echo '$'${NAME}'__override' +utils.environment.check.runtime-override() { # support for ENV_VAR__override= : scalar only + local configuration_variable_name="${1}" + #local configuration_json="${2}" + + local environment_override_configuration_variable_name="${configuration_variable_name}__override" + + configuration_value="${(P)environment_override_configuration_variable_name}" } -utils.environment.check.value() { - local NAME="$1" - local LOOKUP_PATH="$2" +utils.environment.check.value() { # support for '.value' : splits between array and scalar + local configuration_variable_name="${1}" + local configuration_json="${2}" - scwrypts.environment.user.get-shell-values \ - | utils.yq -r "${LOOKUP_PATH}.value" \ - | grep -v '^null$' \ - ; + case "$(echo "${configuration_json}" | utils.yq -r '.value | type')" in + ( !!null ) ;; # no value set + ( !!map ) ;; # unsupported type(s) + ( !!seq ) + local _v + while IFS= read -r _v + do + configuration_values+=("${_v}") + done < <(echo "${configuration_json}" | utils.yq -r '.value[]') + ;; + + ( * ) configuration_value="$(echo "${configuration_json}" | utils.yq -r '.value')" ;; + esac } -utils.environment.check.selection() { - local NAME="$1" - local LOOKUP_PATH="$2" +utils.environment.check.selection() { # support for '.selection' : configured list prompts user to select a scalar value + local configuration_variable_name="${1}" + local configuration_json="${2}" - local SELECTION_VALUES=($( - scwrypts.environment.user.get-shell-values \ - | utils.yq -r "${LOOKUP_PATH}.selection[]" \ - | grep -v '^null$' \ - )) + local selection_value selection_values=() + while IFS= read -r selection_value + do + selection_values+=("${selection_value}") + done < <(echo "${configuration_json}" | utils.yq -r ".selection[]" | grep -v '^null$') - [[ ${#SELECTION_VALUES[@]} -gt 0 ]] || return + [[ "${#selection_values[@]}" -gt 0 ]] || return - echo "${SELECTION_VALUES}" \ - | sed 's/\s\+/\n/g' \ - | utils.fzf "select a value for '${NAME}'" \ - ; -} - -utils.environment.check.select() { # support for ENV_VAR__select=() - local NAME="$1" - local LOOKUP_PATH="$2" - - local SELECTION_VALUES=($(eval echo '$'${NAME}'__select')) - - [[ ${#SELECTION_VALUES[@]} -gt 0 ]] || return - - echo.warning "support for ENV_VAR__select syntax is deprecated;\nplease use the .selection[] array in the yaml configuration for user-selectable options" - - echo "${SELECTION_VALUES}" \ - | sed 's/\s\+/\n/g' \ - | utils.fzf "select a value for '${NAME}'" \ - ; + configuration_value="$( + printf '%s\n' "${selection_values[@]}" \ + | utils.fzf "select a value for '${configuration_variable_name}'" \ + )" } diff --git a/zsh/utils/io/00-colors.zsh b/zsh/utils/io/00-colors.zsh index 8e7071c..ac7ffd3 100644 --- a/zsh/utils/io/00-colors.zsh +++ b/zsh/utils/io/00-colors.zsh @@ -22,15 +22,10 @@ utils.colors.remove() { sed 's/\x1b\[[0-9;]*m//g'; } ##################################################################### utils.colors.print() { # print color + message + color-reset sequence - local COLOR="$1" - local MESSAGE="${@:2}" + local color="${1}" + local message="${@:2}" - command -v utils.colors.${COLOR} &>/dev/null || { - printf "${MESSAGE}" - return - } - - printf "$(utils.colors.${COLOR})${MESSAGE}$(utils.colors.reset)" + printf "%s" "$(utils.colors.${color} 2>/dev/null)${message}$(utils.colors.reset)" } ##################################################################### diff --git a/zsh/utils/io/10-print.zsh b/zsh/utils/io/10-print.zsh index da4dca3..a69ef81 100644 --- a/zsh/utils/io/10-print.zsh +++ b/zsh/utils/io/10-print.zsh @@ -2,103 +2,124 @@ DEPENDENCIES+=(jo jq printf) utils.io.print() { local \ - MESSAGE \ - PREFIX COLOR \ - MINIMUM_LOG_LEVEL IGNORE_MINIMUM_LOG_LEVEL=false \ - PRINT_TO_STDERR=true \ - PRINT_TO_STDOUT=false \ - LAST_LINE_END='\n' \ + message \ + prefix prefix_delimiter=' : ' color \ + minimum_log_level ignore_minimum_log_level=false \ + print_to_stderr=true \ + print_to_stdout=false \ + last_line_end=$'\n' \ + variables=() \ ; - [ ${SCWRYPTS_LOG_LEVEL} ] || local SCWRYPTS_LOG_LEVEL=4 - [ ${SCWRYPTS_OUTPUT_FORMAT} ] || local SCWRYPTS_OUTPUT_FORMAT=pretty + # since this function can be used outside of scwrypts, we bump log + # level to maximum to avoid confusion of "why isn't my print + # statment working" + [[ "${SCWRYPTS_LOG_LEVEL:=${LOG_LEVEL}}" ]] \ + || local SCWRYPTS_LOG_LEVEL=4 - local _S - while [[ $# -gt 0 ]] + [[ "${SCWRYPTS_OUTPUT_FORMAT}" ]] \ + || local SCWRYPTS_OUTPUT_FORMAT=pretty + + local _s + while [[ "${#}" -gt 0 ]] do - _S=1 - case $1 in - ( --prefix ) - _S=2 - PREFIX="$2" - ;; + _s=1 + case "${1}" in + ( --prefix ) _s=2; prefix="${2}" ;; + ( --prefix-delimiter ) _s=2; prefix_delimiter="${2}" ;; + ( --color ) _s=2; color="${2}" ;; + ( --minimum-log-level ) _s=2; minimum_log_level="${2}" ;; + ( --format ) _s=2; local SCWRYPTS_OUTPUT_FORMAT="${2}" ;; - ( --color ) - _S=2 - COLOR="$2" - ;; - - ( --minimum-log-level ) - _S=2 - MINIMUM_LOG_LEVEL=$2 + ( -v ) _s=2 # appends 'name=value' to the end of the message + variables+=("${2}") ;; ( --force-print ) - IGNORE_MINIMUM_LOG_LEVEL=true + ignore_minimum_log_level=true ;; ( --stdout ) - PRINT_TO_STDOUT=true - PRINT_TO_STDERR=false + print_to_stdout=true + print_to_stderr=false ;; - ( --no-line-end ) - LAST_LINE_END='' - ;; - - ( --format ) - _S=2 - local SCWRYPTS_OUTPUT_FORMAT=$2 + ( --no-line-end ) # only applicable to some formats (see below) + last_line_end='' ;; ( * ) - [ "${MESSAGE}" ] && MESSAGE+=" $1" || MESSAGE="$1" + [[ "${message}" ]] && message+=" ${1}" || message="${1}" ;; esac - [[ ${_S} -le $# ]] && shift ${_S} || { echo "echo.error : missing argument for '$1'" >&2; return 1; } + shift "${_s}" || { echo "ERROR : missing argument for '${1}'" >&2; return 1; } done : \ - && [ "${MINIMUM_LOG_LEVEL}" ] \ - && [[ "${IGNORE_MINIMUM_LOG_LEVEL}" =~ false ]] \ - && [[ "${SCWRYPTS_LOG_LEVEL}" -lt "${MINIMUM_LOG_LEVEL}" ]] \ + && [[ "${minimum_log_level}" ]] \ + && [[ "${ignore_minimum_log_level}" =~ false ]] \ + && [[ "${SCWRYPTS_LOG_LEVEL}" -lt "${minimum_log_level}" ]] \ && return 0 + local variable + for variable in "${variables[@]}" + do + message+=$'\n'" - ${variable}=${(Pq)variable}" + done + + case ${SCWRYPTS_OUTPUT_FORMAT:l} in + ( raw ) message+="${last_line_end}" ;; - MESSAGE="$(echo "${MESSAGE}" | sed 's/^ \+//; s/%/%%/g')" - case ${SCWRYPTS_OUTPUT_FORMAT} in - ( raw ) MESSAGE+="${LAST_LINE_END}" ;; ( pretty ) - MESSAGE="${COLOR}$({ - while IFS='' read line + message="${color}$({ + while IFS='' read -r line do - [[ ${PREFIX} =~ ^[[:space:]]\+$ ]] && printf '\n' - - printf "${PREFIX} : $(echo "${line}" | sed 's/^ \+//; s/ \+$//')" - - PREFIX=' ' - done <<< $(echo "${MESSAGE}" | sed 's/%/%%/g') - })${LAST_LINE_END}$(utils.colors.reset)" + [[ ${prefix} =~ ^[[:space:]]\+$ ]] && printf '%s' $'\n' + printf "%s" "${prefix}${prefix_delimiter}$(echo "${line}" | sed 's/^ \+//; s/ \+$//')" + prefix="$(printf '%*s' ${#prefix} '')" + done <<< "${message}" + })${last_line_end}$(utils.colors.reset)" ;; + ( json ) - MESSAGE="$(jo \ + message="$(jo \ timestamp=$(date +%s) \ runtime=${SCWRYPTS_RUNTIME_ID} \ - status="$(echo "${PREFIX}" | sed 's/ .*//')" \ - message="$(echo -n "${MESSAGE}" | sed 's/^\t\+//' | jq -Rs)" \ - 2>/dev/null || echo "{\"error\":\"your message was too long so I encoded it manually\",\"messageB64\":\"$(echo "${MESSAGE}" | base64 | tr -d '\n')\"}" - )\n" + status="$(echo "${prefix}" | sed 's/ .*//')" \ + message="$(echo -n "${message}" | sed 's/^\t\+//')" \ + 2>/dev/null || echo "{\"error\":\"your message was too long so I encoded it\",\"messageB64\":\"$(echo "${message}" | base64 | tr -d '\n')\"}" + )"$'\n' ;; + + ( logfmt ) + local ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + local level + case "${prefix}" in + ( ERROR* ) level=error ;; + ( WARNING* ) level=warn ;; + ( DEBUG* ) level=debug ;; + ( TRACE* ) level=trace ;; + ( * ) level=info ;; + esac + + local msg="${message}" + msg="${msg//\\/\\\\}" + msg="${msg//\"/\\\"}" + msg="${msg//$'\n'/\\n}" + + message="ts=${ts} level=${level} runtime=${SCWRYPTS_RUNTIME_ID} msg=\"${msg}\""$'\n' + ;; + ( * ) - echo "echo.error : unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" >&2 + echo "ERROR : unsupported format '${SCWRYPTS_OUTPUT_FORMAT}'" >&2 return 1 ;; esac - - [[ ${PRINT_TO_STDERR} =~ true ]] && printf -- "${MESSAGE}" >&2 - [[ ${PRINT_TO_STDOUT} =~ true ]] && printf -- "${MESSAGE}" + [[ "${print_to_stderr}" =~ true ]] && printf "%s" "${message}" >&2 + [[ "${print_to_stdout}" =~ true ]] && printf "%s" "${message}" return 0 } diff --git a/zsh/utils/io/20-echo.zsh b/zsh/utils/io/20-echo.zsh index 7717227..187c6fd 100644 --- a/zsh/utils/io/20-echo.zsh +++ b/zsh/utils/io/20-echo.zsh @@ -1,57 +1,221 @@ -echo.success.color() { utils.colors.green; } -echo.success() { # command completed successfully - utils.io.print $@ --minimum-log-level 1 --prefix "SUCCESS ✔" --color $(echo.success.color) - return 0 -} +export _SCWRYPTS_DEFAULT_LOG_LEVEL=3 +typeset -gA SCWRYPTS_LOG_LEVELS + +### level 0 : only print forced messages ############################ +SCWRYPTS_LOG_LEVELS[0]=fatal + +### level 1 : only print errors ##################################### +SCWRYPTS_LOG_LEVELS[1]=error echo.error.color() { utils.colors.red; } echo.error() { # command encountered an error - utils.io.print $@ --minimum-log-level 1 --prefix "ERROR ✖" --color $(echo.error.color) + utils.io.print "${@}" --minimum-log-level 1 --prefix "ERROR ✖" --color "$(echo.error.color)" ((ERRORS+=1)) return ${ERRORS} } -echo.reminder.color() { utils.colors.bright-magenta; } -echo.reminder() { # sysadmin reminder or important notice to users - utils.io.print $@ --minimum-log-level 1 --prefix "REMINDER " --color $(echo.reminder.color) +echo.error.user-abort() { # consistent verbiage for user-aborted workflows + echo.error 'user abort' "${@}" +} + +### level 2 : print errors & warnings ############################### +SCWRYPTS_LOG_LEVELS[2]=warning + +echo.warning.color() { utils.colors.yellow; } +echo.warning() { # warning-level messages; not errors + utils.io.print "${@}" --minimum-log-level 2 --prefix "WARNING " --color "$(echo.warning.color)" + ((WARNINGS+=1)) + return 0 +} + +echo.warning.deprecation() { # emit a deprecation warning + local removed_in_version="${1}" + local message="${2:-this function will be removed}" + + # "scwrypts/vX.X" is the default (specifies only vX.X), but custom version allowed + [[ "${removed_in_version}" =~ ^v[0-9.]+$ ]] \ + && removed_in_version="scwrypts/${removed_in_version}" + + echo.warning "DEPRECATED : ${funcstack[2]} : ${message} in ${removed_in_version}" +} + +### level 3 : print status messages & user information ############## +SCWRYPTS_LOG_LEVELS[3]=status + +echo.success.color() { utils.colors.green; } +echo.success() { # command completed successfully + utils.io.print "${@}" --minimum-log-level 3 --prefix "SUCCESS ✔" --color "$(echo.success.color)" return 0 } echo.status.color() { utils.colors.blue; } echo.status() { # general status updates (prefer this to generic 'echo') - utils.io.print $@ --minimum-log-level 2 --prefix "STATUS " --color $(echo.status.color) + utils.io.print "${@}" --minimum-log-level 3 --prefix "STATUS " --color "$(echo.status.color)" return 0 } -echo.warning.color() { utils.colors.yellow; } -echo.warning() { # warning-level messages; not errors - utils.io.print $@ --minimum-log-level 3 --prefix "WARNING " --color $(echo.warning.color) +echo.reminder.color() { utils.colors.bright-magenta; } +echo.reminder() { # sysadmin reminder or important notice to users + utils.io.print "${@}" --minimum-log-level 3 --prefix "REMINDER " --color "$(echo.reminder.color)" return 0 } +### level 4 : include debug messages ################################ +SCWRYPTS_LOG_LEVELS[4]=debug + echo.debug.color() { utils.colors.white; } echo.debug() { # helpful during development or (sparingly) to help others' development - # early exit since debug injects state information - [ ${SCWRYPTS_LOG_LEVEL} ] && [[ ${SCWRYPTS_LOG_LEVEL} -lt 4 ]] && [[ ! $@ =~ --force-print ]] && return 0 + set -- "${DEBUG_ARGS[@]}" "${@}" + case "${@[(re)--force-print]}" in + ( --force-print ) ;; # if --force-print is present; always print + ( * ) # early exit since trace injects state information which is slow + [[ "${SCWRYPTS_LOG_LEVEL:=${LOG_LEVEL}}" ]] || return 0 + [[ "${SCWRYPTS_LOG_LEVEL}" -lt 4 ]] && return 0 + ;; + esac + + local args=() extra_print_lines=() variable_name message_count=0 + local _s + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( -v ) _s=2 + variable_name="${2}" + extra_print_lines+="\n> DEBUG::var${variable_name}${(t)${(P)variable_name}}${(P)variable_name}$(echo "${(P)variable_name}" | grep -q . || echo "$(echo.warning.color)$(echo.debug.color)")" + ;; + + ( --eval ) _s=2 + args+=("$(eval "${2}" 2>&1 | grep . || echo "failed to run ${2}")") + ((message_count+=1)) + ;; + + ( -* ) args+=("${1}") ;; + ( '' ) ;; # a '' is inserted from empty TRACE_ARGS + ( * ) args+=("${1}") ; ((message_count+=1)) ;; + esac + shift "${_s}" || shift "${#}" + done + unset _s + + [[ "${message_count}" -eq 0 ]] && args+=("~}$(echo.debug.color)>") + + utils.io.print "${args[@]}" --minimum-log-level 4 --prefix "DEBUG ℹ" --color "$(echo.debug.color)" \ + $'\n'"$( + [[ "${extra_print_lines[@]}" ]] && { + echo "> DEBUG::var$(utils.colors.bright-yellow)variabletypevalue$(echo.debug.color)" + echo "> DEBUG::var---------$(echo.debug.color)" + echo "${extra_print_lines[@]}" | sort -u + echo "> DEBUG::var---------$(echo.debug.color)" + } | column -ts '' + echo "> DEBUG::called-by : ${funcstack[2]}" + echo "> DEBUG::funcstack : $(echo "${funcstack[@]:1}" | sed 's/ (anon) (eval) (anon)$/ scwrypts/')" + echo "> DEBUG::timestamp : $(date +%s)" + )" + + return 0 +} + +### level 5 : include full trace (may be sensitive) ################# +SCWRYPTS_LOG_LEVELS[5]=trace + +echo.trace.color() { utils.colors.reset; } +echo.trace() { + (( ${funcstack[(Ie)echo.trace]} > 1 )) && return 0 # escape early if echo.trace is recursively nesting + set -- "${TRACE_ARGS[@]}" "${DEBUG_ARGS[@]}" "${@}" + case "${@[(re)--force-print]}" in + ( --force-print ) ;; # if --force-print is present; always print + ( * ) # early exit since trace injects state information which is slow + [[ "${SCWRYPTS_LOG_LEVEL:=${LOG_LEVEL}}" ]] || return 0 + [[ "${SCWRYPTS_LOG_LEVEL}" -lt 5 ]] && return 0 + ;; + esac + + local args=() extra_print_lines=() variable_name message_count=0 + local _s + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( -v ) _s=2 + variable_name="${2}" + extra_print_lines+="\n> TRACE::var${variable_name}${(t)${(P)variable_name}}${(P)variable_name}$(echo "${(P)variable_name}" | grep -q . || echo "$(echo.warning.color)$(echo.trace.color)")" + ;; + + ( --eval ) _s=2 + args+=("$(eval "${2}" 2>&1 | grep . || echo "failed to run ${2}")") + ((message_count+=1)) + ;; + + ( -* ) args+=("${1}") ;; + ( '' ) ;; # a '' is inserted from empty TRACE_ARGS / DEBUG_ARGS + ( * ) args+=("${1}") ; ((message_count+=1)) ;; + esac + shift "${_s}" 2>/dev/null || shift "${#}" + done + unset _s + + [[ "${message_count}" -eq 0 ]] && args+=("~}>") + + utils.io.print "${args[@]}" --minimum-log-level 5 --prefix "TRACE " --color "$(echo.trace.color)" \ + $'\n'"$( + [[ "${extra_print_lines[@]}" ]] && { + echo "> TRACE::var$(utils.colors.bright-yellow)variabletypevalue$(echo.trace.color)" + echo "> TRACE::var---------$(echo.trace.color)" + echo "${extra_print_lines[@]}" | sort -u + echo "> TRACE::var---------$(echo.trace.color)" + } | column -ts '' + echo "> TRACE::called-by : ${funcstack[2]}" + echo "> TRACE::funcstack : ${funcstack[@]}" + echo "> TRACE::timestamp : $(date +%s)" + )" + - utils.io.print $@ --minimum-log-level 4 --prefix "DEBUG ℹ" --color $(echo.debug.color) \ - "\n> DEBUG::funcstack : $(echo "${funcstack[@]:1}" | sed 's/ (anon) (eval) (anon)$/ scwrypts/')" \ - "\n> DEBUG::timestamp : $(date +%s)" \ - ; return 0 } ##################################################################### echo.prompt() { - [ ! "${SCWRYPTS_LOG_LEVEL}" ] && local SCWRYPTS_LOG_LEVEL=4 + local prompt_print_args=() + local response response_set=false only_response=false + local response_print_args=() - [[ "${SCWRYPTS_LOG_LEVEL}" -eq 0 ]] && { - utils.io.print --format raw $@ ": " --no-line-end - return 0 + local _s + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( --response ) _s=2; response_set=true; response="${2}" ;; + ( --only-response ) only_response=true ;; + + ( * ) prompt_print_args+=("${1}") ;; + esac + shift "${_s}" || shift "${#}" + done + unset _s + + [[ "${response_set}" == true ]] \ + && response_print_args+=(':' "${response}") \ + || response_print_args+=(': ' --no-line-end) \ + ; + + [[ "${only_response}" == true ]] && { + utils.io.print "${response}" --prefix-delimiter '' + return "${?}" } - utils.io.print $@ --prefix "PROMPT " --color $(utils.colors.cyan) - utils.io.print '' --prefix "USER ⌨" --color $(utils.colors.bright-cyan) --no-line-end - return 0 + case "${SCWRYPTS_LOG_LEVEL:-${LOG_LEVEL:-${_SCWRYPTS_DEFAULT_LOG_LEVEL}}}" in + ( 0 ) + case "${SCWRYPTS_OUTPUT_FORMAT:-pretty}" in + ( pretty ) utils.io.print "${prompt_print_args[@]}" "${response_print_args[@]}" --format raw ;; + ( * ) utils.io.print "${prompt_print_args[@]}" "${response_print_args[@]}" ;; + esac + ;; + + ( * ) + utils.io.print "${prompt_print_args[@]}" --prefix "PROMPT " --color $(utils.colors.cyan) + utils.io.print "${response_print_args[2]}" --prefix "USER ⌨" --color $(utils.colors.bright-cyan) + ;; + esac } diff --git a/zsh/utils/io/30-user.zsh b/zsh/utils/io/30-user.zsh index cd8875b..1451a08 100644 --- a/zsh/utils/io/30-user.zsh +++ b/zsh/utils/io/30-user.zsh @@ -9,8 +9,8 @@ utils.io.input() { # read a single line of user input # yes/no prompts && = yes (exit code 0) # || = no (exit code 1) -utils.Yn() { [[ ! $(utils.io.read-yn $@ '[Yn]') =~ [nN] ]]; } # default 'yes' -utils.yN() { [[ $(utils.io.read-yn $@ '[yN]') =~ [yY] ]]; } # default 'no' +user.Yn() { [[ ! $(utils.io.read-yn $@ '[Yn]') =~ [nN] ]]; } # default 'yes' +user.yN() { [[ $(utils.io.read-yn $@ '[yN]') =~ [yY] ]]; } # default 'no' utils.io.edit() { # edit a file in user's preferred editor [ ${CI} ] && { @@ -25,7 +25,7 @@ utils.io.edit() { # edit a file in user's preferred editor utils.io.getsudo() { # ensure a user has sudo permissions echo.prompt 'checking sudo password' --stdout | head -n1 >&2 - sudo echo hi >/dev/null 2>&1 /dev/null &2 + echo.prompt --only-response --response "${yn}" - echo ${yn} + echo "${yn}" } diff --git a/zsh/utils/io/70-normalize-boolean.zsh b/zsh/utils/io/70-normalize-boolean.zsh new file mode 100644 index 0000000..d496326 --- /dev/null +++ b/zsh/utils/io/70-normalize-boolean.zsh @@ -0,0 +1,94 @@ +normalize.boolean() { + local DESCRIPTION=' + normalizes common boolean values to exact values "true" and "false" + + note: mutates the target variable based on operation mode (see below) + ' + local PARSERS=() + eval "$(utils.parse.autosetup)" + ########################################## + + local variable_value="${(P)variable}" + + case "${variable_value}" in + ( false | FALSE | False | no | NO | No | n | N | disable* | DISABLE* | Disable* | 0 ) + variable_value=false + ;; + ( true | TRUE | True | yes | YES | Yes | y | Y | enable* | ENABLE* | Enable* | 1 ) + variable_value=true + ;; + ( * ) + case "${default}" in + ( '' ) + echo.error "invalid boolean value ${variable}='${variable_value}'" + return 1 + ;; + ( * ) + [[ "${variable_value}" == '' ]] \ + || echo.warning "unknown boolean value ${variable}='${variable_value}' (using default '${default}')" + + variable_value="${default}" + ;; + esac + ;; + esac + + case "${mode}" in + ( export ) export ${variable}="${variable_value}" ;; + ( set ) ${variable}="${variable_value}" ;; + ( echo ) ;; + esac + echo "${variable_value}" +} + +normalize.boolean.parse.locals() { + local ARGS=() + local variable + local default + local mode=export +} + +normalize.boolean.parse() { + local parsed=0 + + case "${1}" in + ( --variable ) parsed=2; variable="${2}" ;; + ( --default ) parsed=2; default="${2}" ;; + ( --mode ) parsed=2; mode="${2}" ;; + esac + + return "${parsed}" +} + +normalize.boolean.parse.usage() { + USAGE__options+=' + --variable (required) the environment variable to check + --default whether a default value is enabled (default: no default value allowed) + + --mode operation mode; one of the following: + export : "export" the variable on success (default) + set : execute "VARIABLE_NAME=" and allow parent context to determine variable scope + echo : only echo normalized value (do not mutate the variable) + ' + + USAGE__args+=' + $1 variable name: when used instead of the above, uses --mode=echo --default=false + ' +} + +normalize.boolean.parse.validate() { + case "${variable}" in + ( '' ) + variable="${ARGS[1]}" + [[ "${default}" ]] || default=false + [[ "${mode}" ]] || mode=echo + ;; + esac + + [[ "${variable}" ]] || echo.error "missing --variable-name" + + case "${mode}" in + ( export | set | echo ) ;; + ( * ) echo.error "invalid mode '${mode}'" ;; + esac +} diff --git a/zsh/utils/io/99-flow-control.zsh b/zsh/utils/io/99-flow-control.zsh index fd8ca47..de98984 100644 --- a/zsh/utils/io/99-flow-control.zsh +++ b/zsh/utils/io/99-flow-control.zsh @@ -1,14 +1,21 @@ ##################################################################### utils.fail() { # displays a crash error and exit + echo.warning "utils.fail emits an 'exit' code which may unexpectedly terminate the entire scwrypts process" + echo.warning "deprecation warning: ${0} will be removed in scwrypts v5.3+" echo.error --force-print ${@:2} exit $1 } utils.abort() { # for consistency; use after user aborts REQUIRED input + echo.warning "deprecation warning: this will be removed in scwrypts v5.3+" utils.fail 69 'user abort' } +echo.user-abort() { + echo.error 'user abort' "${@}" +} + ##################################################################### utils.check-errors() { # returns an error and reports usage if 'echo.error' was ever called diff --git a/zsh/utils/parse.autosetup.zsh b/zsh/utils/parse.autosetup.zsh index c662fee..f1b33bf 100644 --- a/zsh/utils/parse.autosetup.zsh +++ b/zsh/utils/parse.autosetup.zsh @@ -68,16 +68,16 @@ utils.parse.autosetup() { do command -v ${P}.locals &>/dev/null || continue which ${P}.locals | sed '1d; $d;' - - [[ ${DEBUG} =~ true ]] \ - && echo.debug --force-print "utils.autosetup : parser ${P} locals\n$(which ${P}.locals | sed 's/^/ /; 1d; $d;')" - done | sed 's^\s*local \([A-Z0-9a-z_]*\)\(=\(.*\)\)*$[ "$\1" ] || local \1=\3' + done | sed 's|$| >/dev/null|' usage.reset echo "local USAGE__description=$(printf "%q " "${DESCRIPTION}")" echo "local USAGE__usage=${funcstack[2]}" echo "local ERRORS=0" + echo "local WARNINGS=0" + echo "local DEBUG_ARGS=()" + echo "local TRACE_ARGS=()" echo "local POSITIONAL_ARGS=0" echo ' diff --git a/zsh/utils/parse.zsh b/zsh/utils/parse.zsh index 7bd9791..c2330a7 100644 --- a/zsh/utils/parse.zsh +++ b/zsh/utils/parse.zsh @@ -76,7 +76,7 @@ utils.parse() { for PARSER in ${VALID_PARSERS[@]} do - command -v ${PARSER}.usage &>/dev/null && ${PARSER}.usage + command -v ${PARSER}.usage &>/dev/null && ${PARSER}.usage &>/dev/null done local EARLY_ESCAPE_CODE _S ERRORS=0 POSITIONAL_ARGS=0 diff --git a/zsh/zsh-cache/command-output.module.zsh b/zsh/zsh-cache/command-output.module.zsh new file mode 100644 index 0000000..91b1931 --- /dev/null +++ b/zsh/zsh-cache/command-output.module.zsh @@ -0,0 +1,252 @@ +##################################################################### + +zsh-cache() { + local DESCRIPTION=' + Caches the output of any successful shell command for the current + scwrypts runtime. + + Keep in mind the following: + - the --cache-file arg is trusted to be fully unique; running + more than one command through the same file will resulted in + unexpected behavior + + - requires computation of a sha1 hash (two in persistent mode), + and a write to file on disk/ram; this should only be used for + commands which are expensive to compute + ' + + eval "$(utils.parse.autosetup)" + ########################################## + + local DEBUG_ARGS=( + -v ARGS + -v cache_file -v cache_file_exists + -v persistent_cache_get_hash -v persistent_cache_get_hash_args + -v persistent_cache_hash -v persistent_cache_file + ) + local TRACE_ARGS=(${(q)DEBUG_ARGS[@]}) + + local cache_file_exists \ + && [[ -f "${cache_file}" ]] \ + && cache_file_exists=true \ + || cache_file_exists=false \ + ; + + local cache_status + case "${cache_file_exists}" in + ( true ) cache_status=hit ;; + ( false ) : \ + && [[ "${persistent_cache_enabled}" == true ]] \ + && [[ -f "${persistent_cache_file}" ]] \ + && persistent_cache_hash="$("${persistent_cache_get_hash}" "${persistent_cache_get_hash_args[@]}")" \ + && [[ "$(head -n1 -- "${persistent_cache_file}")" == "${persistent_cache_hash}" ]] \ + && cache_status=persistent-hit \ + || cache_status=miss \ + ;; + esac + + if [[ "${persistent_cache_enabled}" == true ]] && [[ "${cache_status}" == persistent-hit ]] + then + [[ "${persistent_cache_hash}" ]] || { + echo.warning "persistent cache is enabled, but hash compute failed! persistent cache disabled for this call" + echo.debug 'hash compute failed' + persistent_cache_enabled=false + } + fi + + case "${cache_status}" in + ( hit ) + echo.trace "$(utils.colors.bright-green)runtime cache hit$(echo.trace.color)" + ;; + + ( persistent-hit ) + echo.trace "$(utils.colors.green)persistent cache hit; copying to runtime cache$(echo.trace.color)" + sed 1d "${persistent_cache_file}" > "${cache_file}" + ;; + + ( miss ) + echo.trace "$(utils.colors.yellow)cache miss; running command$(echo.trace.color)" + + local target_cache_file \ + && [[ "${persistent_cache_enabled}" == true ]] \ + && target_cache_file="${persistent_cache_file}" \ + || target_cache_file="${cache_file}" \ + ; + + : > "${target_cache_file}" + + [[ "${persistent_cache_enabled}" == true ]] \ + && echo "${persistent_cache_hash}" >> "${target_cache_file}" + + ${ARGS[@]} | grep '.' >> "${target_cache_file}" + local exit_code="${pipestatus[1]}" + + case "${exit_code}" in + ( 0 ) + [[ "${persistent_cache_enabled}" == true ]] \ + && sed 1d "${persistent_cache_file}" > "${cache_file}" + ;; + + ( * ) + echo.error "command '${ARGS[@]}' failed (or failed to produce output) with exit code ${exit_code}" + echo.debug "command failed:\n$(cat -- "${target_cache_file}")" + rm -- "${target_cache_file}" + return "${exit_code}" + ;; + esac + esac + + cat -- "${cache_file}" +} + +zsh-cache.parse.locals() { + local ARGS=() + local cache_file + local CACHE_MODE_ARGS_COUNT=0 + local persistent_cache_file + local persistent_cache_enabled=false + local persistent_cache_get_hash + local persistent_cache_get_hash_args=() + local persistent_cache_hash + local auto=false +} + +zsh-cache.parse.usage() { + USAGE__options+=' + --cache-file runtime-unique filename key for cached data (default: sha1sum of ARGS) + --persistent enables persistence by providing a zsh function to compute the hash + -p passthrough argument to the get-hash function + --persistent-file overwrite the default persistent filename (default: matches --cache-file) + + --auto looks for caller.zsh-cache and, optionally, a caller.zsh-cache.get-hash + - -p and args still accepted + - when no -p is passed, get-hash will use the same args passed to the zsh-cache function + ' + + USAGE__args+=' + $1 the command to be cached + $2..$N the arguments for the command to be cached + ' +} + +zsh-cache.parse() { + local parsed=0 + + case "${1}" in + ( --cache-file ) parsed=2 + cache_file="${2}" + ;; + + ( --persistent-file ) parsed=2 + persistent_cache_file="${2}" + ;; + + ( --persistent ) parsed=2 + persistent_cache_enabled=true + persistent_cache_get_hash="${2}" + ;; + + ( -p ) parsed=2 + persistent_cache_get_hash_args+=("${2}") + ;; + + ( --auto ) parsed=1; auto=true ;; + esac + + return "${parsed}" +} + +zsh-cache.parse.validate() { + [[ "${auto}" == true ]] && { + local caller="${funcstack[5]}" + + command -v "${caller}.zsh-cache" &>/dev/null \ + || echo.error "--auto mode specified, but couldn't find ${caller}.zsh-cache function" + + [[ "${#ARGS[@]}" -gt 0 ]] \ + && ARGS=("${caller}.zsh-cache" "${ARGS[@]}") \ + || ARGS=("${caller}.zsh-cache") \ + ; + + command -v "${caller}.zsh-cache.get-hash" &>/dev/null && { + persistent_cache_enabled=true + persistent_cache_get_hash="${caller}.zsh-cache.get-hash" + + [[ "${#persistent_cache_get_hash_args[@]}" -eq 0 ]] \ + && persistent_cache_get_hash_args=("${ARGS[@]:1}") + } + } + + [[ "${#ARGS[@]}" -gt 0 ]] \ + || echo.error "did not specify a command to cache" \ + || return + + [[ "${cache_file}" ]] || cache_file="zsh-cache.$(echo "${ARGS[@]}" | utils.sha1sum)" + [[ "${persistent_cache_file}" ]] || persistent_cache_file="${cache_file}" + + : \ + && [[ "${cache_file}" ]] \ + && cache_file="${SCWRYPTS_CACHE_PATH__runtime}/${cache_file}" \ + && mkdir -p -- "$(dirname -- "${cache_file}")" \ + || echo.error "must provide valid cache file; cache_file='${cache_file}'" \ + ; + + [[ "${CACHE_MODE_ARGS_COUNT}" -le 1 ]] \ + || echo.error "too many mode arguments specified" + + case "${persistent_cache_enabled}" in + ( false ) ;; + ( true ) + : \ + && [[ "${persistent_cache_file}" ]] \ + && persistent_cache_file="${SCWRYPTS_CACHE_PATH__persistent}/${persistent_cache_file}" \ + && mkdir -p -- "$(dirname -- "${persistent_cache_file}")" \ + || echo.error "must provide valid persistent cache file; persistent_cache_file='${persistent_cache_file}'" \ + ; + + command -v "${persistent_cache_get_hash}" &>/dev/null \ + || echo.error "invalid hash function '${persistent_cache_get_hash}'" \ + || return + ;; + esac +} + +##################################################################### + +case "$(normalize.boolean --mode export --variable SCWRYPTS__ZSH_CACHE_ENABLED --default true)" in + ( true ) DEPENDENCIES+=(sha1sum) ;; + ( false ) + echo.trace "zsh-cache disabled; mocking module ${scwryptsmodule}" # see ./zsh-cache.module.zsh + + zsh-cache() { + local DESCRIPTION=" + cache bypassed with SCWRYPTS__ZSH_CACHE_ENABLED=${SCWRYPTS__ZSH_CACHE_ENABLED} + + instead; this will always execute the forwarded command + " + + eval "$(zsh.parse.autosetup)" + + ########################################## + + ${ARGS[@]} + } + + zsh-cache.parse.validate() { + case "${auto}" in + ( false ) ;; # do nothing; ARGS already setup correctly + ( true ) + local caller="${funcstack[5]}" + + command -v "${caller}.zsh-cache" &>/dev/null \ + || echo.error "--auto mode specified, but couldn't find ${caller}.zsh-cache function" + + [[ "${#ARGS[@]}" -gt 0 ]] \ + && ARGS=("${caller}.zsh-cache" "${ARGS[@]}") \ + || ARGS=("${caller}.zsh-cache") \ + ; + ;; + esac + } + ;; +esac diff --git a/zsh/zsh-cache/reset b/zsh/zsh-cache/reset new file mode 100755 index 0000000..cd69e1d --- /dev/null +++ b/zsh/zsh-cache/reset @@ -0,0 +1,63 @@ +#!/usr/bin/env zsh +##################################################################### + +USAGE__description=' + resets the persistent state cache +' + +USAGE__args=' + --dry-run preview the cache files which would be dropped +' + +##################################################################### + +MAIN() { + eval "$(utils.parse.autosetup)" + + case "${mode}" in + ( default ) + zsh-cache.reset.persistent + ;; + + ( dry-run ) + [[ -d "${SCWRYPTS_CACHE_PATH__persistent}" ]] || { + echo.reminder "[dry-run] cache folder does not exist: ${SCWRYPTS_CACHE_PATH__persistent}" + return 0 + } + + echo.reminder "[dry-run] would clear folder ${SCWRYPTS_CACHE_PATH__persistent} + contents: + $( + cd -- "${SCWRYPTS_CACHE_PATH__persistent}" + find . -type f | sed 's|^\./|- |' | grep . || echo "$(utils.colors.red)$(echo.reminder.color)" + )" + ;; + esac +} + +##################################################################### + +MAIN.parse.locals() { + local mode=default + local mode_args=() +} + +MAIN.parse() { + local p=0 + + case "${1}" in + ( --dry-run ) p=1; mode="${1/--}"; mode_args+=("${1}") ;; + esac + + return "${p}" +} + +MAIN.parse.validate() { + case "${mode}" in + ( default | dry-run ) ;; + ( * ) echo.error "invalid operation mode '${mode}'" + esac + + [[ "${#mode_args[@]}" -le 1 ]] \ + || echo.error "incompatible arguments '${mode_args[@]}'" +} diff --git a/zsh/zsh-cache/reset.module.zsh b/zsh/zsh-cache/reset.module.zsh new file mode 100644 index 0000000..035a51d --- /dev/null +++ b/zsh/zsh-cache/reset.module.zsh @@ -0,0 +1,72 @@ +##################################################################### + +${scwryptsmodule}.all() { + local ERRORS=0 + + local cache_type + for cache_type in persistent runtime + do + zsh-cache.reset.${cache_type} || ((ERRORS+=1)) + done + + return "${ERRORS}" +} + +${scwryptsmodule}.persistent() { zsh-cache.reset._ persistent; } +${scwryptsmodule}.runtime() { zsh-cache.reset._ runtime; } + +${scwryptsmodule}._() { + local ERRORS=0 + local TRACE_ARGS=(-v cache_variable -v cache_path) + + local cache_variable cache_path cache_name="${1}" + case "${cache_name}" in + ( persistent | runtime ) + cache_variable="SCWRYPTS_CACHE_PATH__${cache_name}" + cache_path="${(P)cache_variable}" + + [[ "${cache_path}" ]] \ + || echo.error "could not discover cache path; is ${cache_variable} set?" \ + || return 1 + ;; + ( * ) + echo.error "invalid cache '${cache_name}'" + return 1 + ;; + esac + + if [[ -d "${cache_path}" ]] + then + echo.trace "deleting cache directory" + rm -rf -- "${cache_path}" \ + || echo.error "failed to delete cache directory for ${cache_name}" + fi + + echo.trace "recreating cache directory" + mkdir -p -- "${cache_path}" \ + || echo.error "failed to recreate cache path for ${cache_name}" + + [[ "${ERRORS}" -eq 0 ]] \ + && echo.success "successfully cleared ${cache_name} cache" + + return "${ERRORS}" +} + +##################################################################### +case "$(normalize.boolean --mode export --variable SCWRYPTS__ZSH_CACHE_ENABLED --default true)" in + ( true ) ;; + ( false ) + echo.trace "zsh-cache disabled; mocking module ${scwryptsmodule}" # see ./zsh-cache.module.zsh + + for _PROVIDED_FUNCTION in all persistent runtime + do + ${scwryptsmodule}.${_PROVIDED_FUNCTION}() { + local WARNINGS=0 + echo.warning "cache is disabled; skipping reset request" + return 0 + } + done + + unset _PROVIDED_FUNCTION + ;; +esac diff --git a/zsh/zsh-cache/zsh-cache.module.zsh b/zsh/zsh-cache/zsh-cache.module.zsh new file mode 100644 index 0000000..8621c4c --- /dev/null +++ b/zsh/zsh-cache/zsh-cache.module.zsh @@ -0,0 +1,13 @@ +# +# zsh-cache : cache tools for zsh commands +# +# this module can be disabled with 'export SCWRYPTS__ZSH_CACHE_ENABLED=false' +# all library functions implement pass-through mocks so code does not break when disabled +# +normalize.boolean --mode export --variable SCWRYPTS__ZSH_CACHE_ENABLED --default true >/dev/null + +# primary cache command 'zsh-cache()' which caches output for the current runtime +use zsh-cache/command-output + +# invoke to manually reset cache; provides { .all | .persistent | .runtime } +use zsh-cache/reset