Compare commits

..

2 Commits

Author SHA1 Message Date
wrynegade 3b8f7628db go 2026-08-14 12:37:10 -06:00
wrynegade e7484103b9 v5.1.0
=====================================================================

A follow-up to V5 focused on hardening the environment system and
expanding the ZSH logging/caching toolkit. No migration required from
v5.0.x, though note the log-level reorganization below may quiet some
messages at lower verbosities.

--- New Features -------------------------

- persistent cross-runtime cache for ZSH-type scwrypts; extends the
  v5.0 runtime speedup cache to survive between runs with automatic
  hash-based invalidation (toggle via SCWRYPTS__ZSH_CACHE_ENABLED)

- new 'trace' log level (5) and echo.trace, plus '-v VARNAME' state
  injection for echo.debug / echo.trace to surface variable values
  inline

- additional scwrypts output formats: 'raw' and 'logfmt'
  (alongside pretty and json)

- normalize.boolean utility for consistent truthy/falsey handling
  across ZSH-type scwrypts

- utils.fzf.file-select for directory-scoped file selection prompts

- automatic dependency wrappers providing default arguments, GNU
  coreutil resolution, and scwrypts trace on wrapped commands

--- Changes ------------------------------

- log levels reorganized; success / status / reminder messages now
  emit at level 3, so lower verbosities will show fewer of these

- environment library restructured around get-user-json as the single
  source of truth (replaces the previous user module and cache-output)

- utility renames for consistency: user.Yn / user.yN (from utils.*)

- utils.fail and utils.abort deprecated in favor of echo.error and
  echo.error.user-abort

--- Bug Fixes ----------------------------

- CI runs now resolve config lookup-paths (.dotted.path) to their
  environment variable before reading, fixing false "not set" reports
  and invalid-export errors for lookup-path checks in CI

- hardened environment type detection so array-valued configs reprint
  correctly after their first check

- corrected exit-code capture in the cache layer (now reflects the
  cached command, not the downstream output filter)

- scwrypt exit code now survives the logging pipeline

- assorted script fixes (efs unmount file listing, postgres run-sql
  file discovery)
2026-08-14 12:28:08 -06:00
57 changed files with 2395 additions and 1426 deletions
+3 -2
View File
@@ -261,11 +261,12 @@ jobs:
coreutils \
findutils \
fzf \
perl \
sed \
gawk \
git \
grep \
jo \
perl \
sed \
jq \
util-linux \
uuidgen \
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+325 -274
View File
@@ -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 <env-name> 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 <format> specify output format; one of: pretty,json (default: pretty)
-o, --output <format> 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 &>/dev/tty
;;
esac
)
case "${scwrypts_execution_mode}" in
( normal ) eval "${scwrypts_run_string} ${(q)@}" ;;
( interactive ) eval "${scwrypts_run_string} ${(q)@}" </dev/tty &>/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}"
+1 -1
View File
@@ -84,7 +84,7 @@ readonly ${scwryptsgroup}__color=$(utils.colors.green)
#
readonly ${scwryptsgroup}__zshlibrary=
#readonly ${scwryptsgroup}__zshlibrary=
#
# ${scwryptsgroup}__zshlibrary (optional) (default = *see below*)
#
+3 -3
View File
@@ -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
+6 -6
View File
@@ -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}'"
}
+35 -34
View File
@@ -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
}
+1 -1
View File
@@ -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
+23 -10
View File
@@ -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 #########################################################
+1 -1
View File
@@ -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=$?
+23 -20
View File
@@ -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)"
}
+1 -1
View File
@@ -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[@]}
+1 -1
View File
@@ -2,5 +2,5 @@
#####################################################################
MAIN() {
echo.success 'Hello, World!'
echo.success 'Hello, World!' --force-print
}
+1 -2
View File
@@ -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 \
;
}
+15 -21
View File
@@ -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}"
}
-143
View File
@@ -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)
"
}
#####################################################################
+76 -29
View File
@@ -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
}
@@ -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
@@ -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)
' \
;
}
@@ -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' \
;
}
@@ -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' \
;
@@ -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
}
@@ -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
}
+1 -1
View File
@@ -39,7 +39,7 @@ MAIN() {
utils.environment.check --print-value ${ARGS[@]}
;;
( all )
scwrypts.environment.user.get-json
scwrypts.environment.get-user-json
;;
esac
}
+2 -2
View File
@@ -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}\""
+6 -6
View File
@@ -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)"
+151 -143
View File
@@ -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 <string> 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 <string> name of the target environment (default is current: $SCWRYPTS_ENV)
--create-from-existing <string> 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 <string> 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 <string> 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
}
-276
View File
@@ -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 <string> 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
}
+15 -6
View File
@@ -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}"
}
+1 -1
View File
@@ -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 \
;
+1 -1
View File
@@ -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' \
+2 -2
View File
@@ -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}'"
}
+20
View File
@@ -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 <string> name of a scwrypts environment; when unspecified, uses current
(current: ${SCWRYPTS_ENV})
"
}
+1 -1
View File
@@ -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 ]] \
+17
View File
@@ -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}" ]]
}
-1
View File
@@ -1 +0,0 @@
+177 -29
View File
@@ -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 <ext> file extension to include; multiple can be specified
-p, --prompt <string> fzf prompt to the user (default: "select a file from ${directory}")
--mindepth <number> minimum find depth; unset with 0 (default: 1)
--maxdepth <number> 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
}
+5 -5
View File
@@ -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}" "${@}"
}
+9
View File
@@ -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
}
+13 -6
View File
@@ -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(..)' \
; }
+109 -43
View File
@@ -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() {
+222 -153
View File
@@ -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 <environment variable> [...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 <value> 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}'" \
)"
}
+3 -8
View File
@@ -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)"
}
#####################################################################
+81 -60
View File
@@ -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
}
+190 -26
View File
@@ -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)<variable empty>$(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+=("<debug called by $(utils.colors.cyan)${funcstack[2]:-<unknown>~}$(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)<variable empty>$(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+=("<trace called by ${funcstack[2]:-<unknown>~}>")
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
}
+47 -40
View File
@@ -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/tty \
sudo echo hi &>/dev/null </dev/tty \
&& echo.success '...authenticated!' \
|| echo.error 'failed :c' \
|| return 1
@@ -35,51 +35,56 @@ utils.io.getsudo() { # ensure a user has sudo permissions
#####################################################################
utils.io.read() {
[ ${CI} ] && [ -t 0 ] \
&& utils.fail 42 'currently in CI, but attempting interactive read; aborting'
local stdin_is_a_terminal \
&& [[ -t 0 ]] \
&& stdin_is_a_terminal=true \
|| stdin_is_a_terminal=false \
;
local FORCE_USER_INPUT=false
[[ "$(normalize.boolean CI)" == true ]] && [[ "${stdin_is_a_terminal}" == true ]] && {
echo.error 'currently in CI, but attempting interactive read; aborting'
return 42
}
local force_user_input=false
local ARGS=()
while [[ $# -gt 0 ]]
while [[ "${#}" -gt 0 ]]
do
case $1 in
--force-user-input ) FORCE_USER_INPUT=true ;;
-k )
ARGS+=($1)
;;
* ) ARGS+=($1) ;;
case "${1}" in
( --force-user-input ) force_user_input=true ;;
( * ) ARGS+=("${1}") ;;
esac
shift 1
done
while read -k -t 0; do :; done; # flush user stdin
case ${FORCE_USER_INPUT} in
true )
read ${PREARGS[@]} ${ARGS[@]} $@ </dev/tty
case "${force_user_input}" in
( true )
read "${PREARGS[@]}" "${ARGS[@]}" "${@}" </dev/tty
;;
false )
[ -t 0 ] || ARGS=(-u 0 ${ARGS[@]})
read ${ARGS[@]} $@
( false )
[[ "${stdin_is_a_terminal}" == false ]] && ARGS=(-u 0 ${ARGS[@]})
read "${ARGS[@]}" "${@}"
;;
esac
}
utils.io.read-yn() { # yes/no read is suprisingly tricky
local FORCE_USER_INPUT=false
local force_user_input=false
local USERPROMPT=()
local READ_ARGS=()
local read_args=()
[ "${SCWRYPTS_LOG_LEVEL}" ] || local SCWRYPTS_LOG_LEVEL=4
[[ "${SCWRYPTS_LOG_LEVEL}" ]] || local SCWRYPTS_LOG_LEVEL=4
while [[ $# -gt 0 ]]
while [[ "${#}" -gt 0 ]]
do
case $1 in
--force-user-input )
case "${1}" in
( --force-user-input )
# overrides 'scwrypts -y' and stdin pipe but not CI
FORCE_USER_INPUT=true
READ_ARGS+=($1)
force_user_input=true
read_args+=($1)
;;
* ) USERPROMPT+=($1) ;;
esac
@@ -88,27 +93,29 @@ utils.io.read-yn() { # yes/no read is suprisingly tricky
##########################################
local SKIP_USER_INPUT=false
local skip_user_input=false
case "$(normalize.boolean CI)" in
( true ) # always skip input in CI
skip_user_input=true
;;
[ ${CI} ] \
&& SKIP_USER_INPUT=true
[ ${__SCWRYPTS_YES} ] && [[ ${__SCWRYPTS_YES} -eq 1 ]] && [[ ${FORCE_USER_INPUT} =~ false ]] \
&& SKIP_USER_INPUT=true
( false )
[[ "${force_user_input}" == false ]] && [[ "$(normalize.boolean __SCWRYPTS_YES)" == true ]] \
&& skip_user_input=true
;;
esac
##########################################
local yn
echo.prompt "${USERPROMPT[@]}"
case ${SKIP_USER_INPUT} in
true ) yn=y ;;
false )
utils.io.read ${READ_ARGS[@]} -s -k yn
;;
case "${skip_user_input}" in
( true ) yn=y ;;
( false ) utils.io.read "${read_args[@]}" -s -k yn ;;
esac
[[ ${SCWRYPTS_LOG_LEVEL} -ge 1 ]] && echo ${yn} >&2
echo.prompt --only-response --response "${yn}"
echo ${yn}
echo "${yn}"
}
+94
View File
@@ -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 <string> (required) the environment variable to check
--default <value> whether a default value is enabled (default: no default value allowed)
--mode <string> operation mode; one of the following:
export : "export" the variable on success (default)
set : execute "VARIABLE_NAME=<value>" 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
}
+7
View File
@@ -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
+4 -4
View File
@@ -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 '
+1 -1
View File
@@ -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
+252
View File
@@ -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 <string> runtime-unique filename key for cached data (default: sha1sum of ARGS)
--persistent <hash-func> enables persistence by providing a zsh function to compute the hash
-p <argument> passthrough argument to the get-hash function
--persistent-file <string> 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
+63
View File
@@ -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)<folder is empty>$(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[@]}'"
}
+72
View File
@@ -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
+13
View File
@@ -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