#!/bin/zsh
# vim: ft=zsh noet ts=4 sts=4 sw=4
autoload -Uz colors; colors
setopt extended_glob warn_create_global no_unset
zmodload zsh/zutil
typeset -gA vars vars_prev
typeset -ga do_command actions_performed
typeset -g confz_indent check_only verbose fail_reason quiet
: ${check_only:=0}
: ${verbose:=0}
: ${quiet:=0}
# helper that prints out error message and exits
die() {
print -r - "$fg_bold[red]*$reset_color $@" >&2
exit 1
}
confz_print_usage() {
echo >&2 "usage: $0:h [-f <fpath>] <requirement> [<args>] [; <requirement> ...]"
exit 2
}
# option parsing
typeset -ga confz_opt_f
zparseopts -D - f+:=confz_opt_f || die "Unable to parse options"
() {
local opt val
for opt val in "$confz_opt_f[@]"; do
[[ $opt == "-"* ]] || die "Something went wrong"
fpath=( $val "$fpath[@]" )
done
}
# trace-printing helpers
confz_check_start() {
(($quiet)) || print -r - "${confz_indent}checking $fg_bold[default]$1$reset_color ${(@q)argv[2,-1]} ${reset_color}[" >&2
confz_indent+=" "
}
confz_check_ok() {
confz_indent=${confz_indent% }
(($quiet)) || print -r - "$confz_indent] $fg[green]$1$reset_color OK" >&2
}
confz_check_fail() {
confz_indent=${confz_indent% }
if (($check_only)); then
print -P "$confz_indent] $fg[red]$1$reset_color FAIL" >&2
else
(($quiet)) || print -P "$confz_indent] $fg[yellow]$1$reset_color NEED" >&2
fi
}
confz_do() {
(($quiet)) || print -r - "$confz_indent$fg[yellow]*$fg[default] ${(q)@}" >&2
actions_performed+=( "$*" )
"$@" || die "command failed with error $?: ${(q)@}"
}
# trace-printing helper for setting $vars values
setvar() {
if (($verbose)); then
print -r - "$confz_indent$fg[cyan]$1$fg[default]=${(qqq)2}" >&2
fi
vars[$1]=$2
}
# set $vars parameter if it's empty (ie. set default value)
defvar() {
if ! (($+vars[$1])); then
setvar "$1" "$2"
fi
}
# check if variables are nonempty
checkvars() {
local var
local -a empty
for var in "$@"; do
if ! (($+vars[$var])); then
empty+=( $var )
fi
done
(( $#empty )) && die "required parameters are empty: ${(@q)empty}"
}
# check if given arguments (as name value pairs) match current $vars
matchvars() {
local name value
for name value in "$@"; do
if (($+vars[$name])); then
[[ $value == $vars[$name] ]] || return 1
fi
done
return 0
}
# unify given arguments (as name value pairs) with current $vars
unify() {
local name value
for name value in "$@"; do
if (($+vars[$name])); then
[[ $value == $vars[$name] ]] \
|| die "Unified variable $name has value ${(qqq)vars[$name]} not matching ${(qqq)value}"
else
vars[$name]=$value
fi
done
}
# autoload all relevant functions and run confz_*_init
confz_load() {
local func
local -a match mbegin mend confz_functions func_files
func_files=( $^fpath/confz_*(N) )
for func in $func_files ; do
if ! [[ -r "$func" ]]; then
print -r >&2 - "$func is not readable"
continue
fi
autoload -Uz $func:t
confz_functions+=$func:t
done
for func in ${(o)confz_functions}; do
if [[ $func == *_init ]]; then
$func || die "Init function failed: $func"
fi
done
}
# check & run & check a dependency
require() {
# usage: require <name> [<variables>] [-- <arguments>]
# where:
# <name> is a dependency name as defined by function confz_<name>_check
# <variables> are variable assignments in the form of:
# foo=bar -- sets ${vars[foo]} in calee to value "bar"
# :foo -- passes ${vars[foo]} from caller to calee
# ?foo=bar -- sets ${vars[foo]} in calee to value of ${vars[bar]}
# iff it is defined in the caller
# ?foo -- passes ${vars[foo]} from caller to calee iff it is defined
# in the caller
# %foo -- passes ${vars[foo]} from callee to caller
# %foo=bar -- passes ${vars[foo]} from callee
# to variable ${vars[bar]} of caller
# <arguments> are arguments passed to the dependency function
local name outer inner indent_prev check_only_prev check_ret
local prev_name new_name
local -a do_command_prev
local -A vars_local lift
name=$1
shift
# store and clear $vars
vars_prev=( "${(@kv)vars}" )
vars_local=( "${(@kv)vars}" )
vars=( )
# print check start
confz_check_start $name "$@"
# parse variable assignments
while (( $# )); do
case $1 in
(:*) ((${+vars_prev[${1#:}]})) || \
die "variable ${(qqq)1#:} not set, passed as :argument"
setvar ${1#:} "${vars_prev[${1#:}]}";;
(\?*=*)
prev_name=${${1#\?}#*=}
new_name=${${1#\?}%%=*}
(($+vars_prev[$prev_name])) && \
setvar $new_name "$vars_prev[$prev_name]";;
(\?*) ((${+vars_prev[${1#\?}]})) && \
setvar ${1#\?} "${vars_prev[${1#\?}]}";;
(%*=*) lift[${${1#%}%%=*}]=${${1#%}#*=};;
(%*) lift[${1#%}]=${1#%};;
(*=*) setvar "${1%%=*}" "${1#*=}";;
(--) shift; break;;
(*) die "$name: unrecognised argument: ${(qqq)1}";;
esac
shift
done
# store old $do_command
do_command_prev=( "${do_command[@]}" )
do_command=( confz_${name}_do )
# clear fail_reason
fail_reason=''
if ! (($+functions[confz_${name}_check])); then
die "$name: Function confz_${name}_check not defined."
fi
# perform check - run - check
if confz_${name}_check "$@"; then
confz_check_ok $name
else
check_ret=$?
confz_check_fail $name
if (($check_only)); then
# if (($verbose)); then
# print "re-running check with xtrace"
# set +x
# confz_${name}_check
# set -x
# fi
die "$name: check failed: ${fail_reason:-error $check_ret}"
else
if (($verbose)); then
print -r - "$confz_indent$fg[cyan]*$fg[default] reason: ${fail_reason:-error $check_ret}"
(($+functions[confz_${name}_do])) && \
typeset -f -t confz_${name}_do
fi
confz_do "${do_command[@]}" "$@"
# print check start
confz_check_start $name "$@" again...
# clear fail_reason
fail_reason=''
# perform check once again
check_only_prev=$check_only
check_only=1
confz_${name}_check "$@" || \
die "$name: check failed: ${fail_reason:-error $?}"
confz_check_ok $name
check_only=$check_only_prev
fi
fi
# clear fail_reason
fail_reason=''
# restore $do_command
do_command=( "${do_command_prev[@]}" )
# restore old $vars and put calee's $vars into $vars_prev
vars_prev=( "${(@kv)vars}" )
vars=( "${(@kv)vars_local}" )
# lift %variables from calee to caller
for inner outer in ${(kv)lift}; do
((${+vars_prev[$inner]})) || \
die "variable ${(qqq)inner} not set, was requested by caller as ${(qqq)outer}"
setvar $outer ${vars_prev[$inner]}
done
}
# "dependency" that represents toplevel and gets it's deps out of argv
confz_main_check() {
local arg
local -a args
local -a printout
for arg in "$@"; do
if [[ $arg == ';' ]]; then
require $args
for arg in $printout; do
printf "%s=%s\n" $arg ${(qqq)vars[$arg]}
done
args=()
printout=()
else
if [[ $arg == %* ]]; then
printout+=( ${${arg#%}##*=} )
fi
args+=( $arg )
fi
done
(( $#args )) && require $args
for arg in $printout; do
printf "%s=%s\n" $arg ${(qqq)vars[$arg]}
done
(($check_only)) && return 0
fail_reason="performed $#actions_performed actions, recheck"
return $#actions_performed
}
# noop for the toplevel, functionality is in the dependencies
confz_main_do() {
true
}
# run the toplevel
confz_load
require main -- "$@"