Files
scwrypts/scwrypts
T
wrynegade 814fc01b93 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:39:56 -06:00

577 lines
19 KiB
Bash
Executable File

#!/usr/bin/env zsh
export EXECUTION_DIR=$(pwd)
export SCWRYPTS_RUNTIME_ID=$(uuidgen)
source "$(dirname -- $(readlink -f -- "$0"))/zsh/import.driver.zsh" || return 42
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)" "${@}"; }
local ERRORS=0
local USAGE="
usage: scwrypts [...options...] [...patterns...] -- [...script options...]
options:
selection
-m, --name <scwrypt-name> only run the script if there is an exact match
(requires type and group)
-g, --group <group-name> only use scripts from the indicated group
-t, --type <type-name> only use scripts of the indicated type
runtime
-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-5> set incremental scwrypts log level to one of the following:
0 : only command output and critical failures; skips logfile
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,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
--root print out scwrypts.config.group.scwrypts.root and exit
--update update scwrypts library to latest version
--version print out scwrypts version and exit
patterns:
- 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)
"
#####################################################################
### cli argument parsing and global configuration ###################
#####################################################################
local target_scwrypts_env="${SCWRYPTS_ENV}"
local search_patterns=()
local search_group search_type search_name
[[ "${SCWRYPTS_LOG_LEVEL}" ]] || local SCWRYPTS_LOG_LEVEL=3
local _s
while [[ $# -gt 0 ]]
do
_s=1
case $1 in
( -[a-z][a-z]* )
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 ###################
( -h | --help )
utils.io.usage
return 0
;;
( -l | --list )
scwrypts.list-available
return 0
;;
( --list-envs )
scwrypts.environment.common.get-env-names
return 0
;;
( --list-groups )
echo "${SCWRYPTS_GROUPS[@]}" | sed 's/\s\+/\n/g' | sort -u
return 0
;;
( --version )
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
;;
( --root )
scwrypts.config.group scwrypts root
return 0
;;
( --config )
echo "source '$(scwrypts.config.group scwrypts root)/zsh/import.driver.zsh'"
echo "utils.check-environment --no-fail --no-usage"
echo "unset __SCWRYPT"
return 0
;;
( --update )
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 )
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="${?}"
git.scwrypts diff --exit-code origin/main -- . &>/dev/null
local diff_status="${?}"
[[ "${sync_status}" -eq 0 ]] && [[ "${diff_status}" -eq 0 ]] && {
echo.success 'already up-to-date with origin/main'
} || {
git.scwrypts rebase --autostash origin/main \
&& echo.success 'up-to-date with origin/main' \
&& git.scwrypts log -n1 \
|| {
git.scwrypts rebase --abort
echo.error 'unable to update scwrypts; please try manual upgrade'
echo.reminder "installation in '$(scwrypts.config.group scwrypts root)'"
}
}
;;
* )
echo.reminder --force-print "
This is a managed installation of scwrypts. Please update through your
system package manager.
"
;;
esac
return 0
;;
### scwrypts filters #####################
( -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 #####################
( -y | --yes ) export __SCWRYPTS_YES=1 ;;
( -n ) SCWRYPTS_LOG_LEVEL=0 ;;
( -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 ) _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 ) _s=2
[[ "${target_scwrypts_env}" ]] \
&& echo.debug "overwriting session environment to ${2}" -v target_scwrypts_env
target_scwrypts_env="${2}"
;;
##########################################
( -- ) shift 1; break ;; # pass arguments after '--' to the scwrypt
( --* ) echo.error "unrecognized argument '${1}'" ;;
( * ) search_patterns+=(${1}) ;;
esac
shift "${_s}" 2>/dev/null || echo.error "missing argument for '${1}'" || shift "${#}"
done
[[ "${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'
}
utils.check-errors --fail
#####################################################################
### scwrypts selection / filtering ##################################
#####################################################################
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}" | 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 '^' \
)"
[[ "${search_group}" ]] && scwrypts_available="$(
echo "${scwrypts_available}" \
| grep -- "${search_group}"'[^/ ]*$' \
| sed 's/ \+$/'$(utils.colors.reset)'/; s/ \+/^/g' \
| column -ts '^' \
)"
[[ "${#search_patterns[@]}" -gt 0 ]] && {
local search_pattern
for search_pattern in "${search_patterns[@]}"
do
scwrypts_available="$(echo "${scwrypts_available}" | grep -- "${search_pattern}")"
done
unset search_pattern
}
}
[[ $(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 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
##########################################
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 \
&& [[ ! "${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)"
[[ "${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
}
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 \
;
case "${sync_with_main_required}" in
( false ) ;;
( true )
echo.status "on '${SCWRYPTS_ENV}'; checking diff against origin/main"
[[ ! "${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 scwrypts_run_string="$(scwrypts.get-runstring "${SCWRYPT_NAME}" "${SCWRYPT_TYPE}" "${SCWRYPT_GROUP}")"
[[ "${scwrypts_run_string}" ]] || return 42
#####################################################################
### logging and pretty header/footer setup ##########################
#####################################################################
local scwrypts_execution_mode=normal
[[ "${SCWRYPT_NAME}" =~ interactive ]] && scwrypts_execution_mode=interactive
local LOGFILE \
&& [[ "${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' \
;
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 ---"
;;
( pretty )
header=$(
echo "
=====================================================================
scwrypt : ${SCWRYPT_GROUP} ${SCWRYPT_TYPE} ${SCWRYPT_NAME}
run at : $(date)
config : ${SCWRYPTS_ENV}
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 ---------------------------------------------------')"
;;
( json )
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}" -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} ---")"
;;
( 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 #################################################
#####################################################################
{
local print_exit_code=false
[[ "${SCWRYPTS_LOG_LEVEL}" -ge 3 ]] && print_exit_code=true
scwrypts.runner.header-and-footer header >&2
case "${scwrypts_execution_mode}" in
( normal ) eval "${scwrypts_run_string} ${(q)@}" ;;
( interactive ) eval "${scwrypts_run_string} ${(q)@}" </dev/tty &>/dev/tty ;;
esac
local exit_code="${?}"
echo "${exit_code}" > "${EXIT_CODE_FILE}"
scwrypts.runner.header-and-footer footer >&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
;;
( * )
[[ "${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)
} "${@}"
EXIT_CODE="${?}"
[[ -f "${EXIT_CODE_FILE}" ]] && EXIT_CODE="$(cat -- "${EXIT_CODE_FILE}")"
[[ "${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}"