blob: 4b5f206e52f5c7d93fb152c6230181b56d701a93 [file] [log] [blame]
#!/bin/bash
#
# validation.sh
#
# Copyright 2019 Luigi Santivetti <luigi.santivetti@gmail.com>
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @1 : holder variable name - lhs
# @desc : if @1 matches against $rex_legal_holder_variable, then convert
# : it back to the original module name
function get_modname_from_variable
{
is_valid_holder_lhs "${*}" || return $s_err
# Remove leading underscore, strip longest underscore match from end,
# finally lowercase it
local mod=$1; mod=${mod:1}; mod=${mod%%_*}; mod=${mod,,}
# Verify there is one
[ -d "$module_d/$mod" ] || {
lets -l -e "'${mod:-undefined-module}' not found"
return $s_err
}
echo $mod # Success
}
# @1 : absolute path to a module's sh file
# @desc : derive module name from holder.sh path
function get_modname_from_file
{
local modname
[ -f "$1" ] && {
modname="$(dirname "$1")"; modname=${modname##*/}; echo $modname
} || echo ""
}
# @1 : holder variable name
function is_valid_holder_lhs
{
[ -n "$1" ] || return $s_err
# Is this variable a holder legal one
echo $1 | grep -qE -- "$rex_legal_holder_variable"
[ ${PIPESTATUS[1]} -eq 0 ]
}
# @1 : module name
function is_valid_dependency
{
local -r modname="$1"
if is_in_modules $modname; then
return $s_ok
elif is_in_optarg $modname; then
lets -l -w "'$modname' was requested, but it's turned off"
else
local m_list="${MODULES[*]},$modname"; m_list="${m_list// /,}"
lets -l -d "adding dependency: '$module' on '$modname'"
is_opt "check" && module_enable "$1" && return $s_ok
# we cannot source a new module within this subshell, they need to be
# included from the beginning.
is_opt "watch" "doins" "upins" "upall" "doall" && \
lets -l -e "$(get_opt) cannot complete, run --check ${MODULES[@]}"
fi
return $s_err
}
# @1 : module name
function is_in_done
{
local -r modname="$1"
__is_in_array "$modname" "${__done[@]}"
}
# @1 : module name
function push_done_deps
{
local -r modname="$1"
is_in_done "$modname" || __done+=( "$modname" )
}
# @1 : module name
function is_in_deps
{
local -r modname="$1"
__is_in_array "$modname" "${__deps[@]}"
}
# @1 : module name
function pop_pending_deps
{
local -r modname="$1"
if ! is_in_deps "$modname"; then
lets -l -e "dependency is missing: $modname"
return $s_err
fi
unset '__deps[-1]'
}
# @1 : module name
function push_pending_deps
{
local -r modname="$1"
if is_in_deps "$modname"; then
lets -l -e "circular dependecy on $modname"
return $s_err
fi
__deps+=( "$modname" )
}
# @1 : module name
function update_dependecies
{
local -r modname="$1"
pop_pending_deps $modname && push_done_deps $modname && \
return $s_ok || return $s_err
return $s_ok # __deps can be 0
}
# @1 : maybe unbound error from stderr
function __is_valid_stderr_unbound
{
local -r __error="unbound"
[ "$1" = "$__error" ] && return $s_ok
return $s_err
}
# @1 : maybe word from stderr
function __is_valid_stderr_words
{
[ "$(wc -w <<< "$*")" -eq 1 ] && return $s_ok
lets -l -e "'$*' unknown words"
return $s_err
}
# @1 : error message delimiter
function __is_valid_stderr_delimeter
{
local -r regex=":$"
(echo "$1" | grep -qE -- "$regex") && return $s_ok
lets -l -e "'$1' unknown delimiter"
return $s_err
}
# @1 : tail offset for unbound variable name in stderr message
function is_valid_stderr_unbound
{
local -ir offset=$1; shift
local -ar stderr=( ${@} )
local -ir unbofs="$offset - 1"
# Don't get crazy about validation, this is clearly fragile and it
# will just break on a different shell.
[ ${#stderr[@]} -ge $offset ] || {
lets -l -e "$offset is oob ${#stderr[@]}, ${stderr[*]}"
return $s_err
}
__is_valid_stderr_unbound ${stderr[-$unbofs]} || {
lets -l -e "stderr reported:"
lets -l -e "${stderr[@]}";
return $s_err
}
__is_valid_stderr_words ${stderr[-$offset]} && \
__is_valid_stderr_delimeter ${stderr[-$offset]} || return $s_err
}
# @1 : module name
function get_module_complete
{
local -r modname="$1"; shift
if [ ${#__deps[@]} -gt 0 ]; then
update_dependecies $modname && return $s_ok || return $s_err
fi
if [ ${#__deps[@]} -eq 0 ]; then
if is_opt "watch" "doins" "upins" "upall" "doall"; then
local regex="$rex_legal_dynamic_typedef"
regex+="[[:space:]]$rex_legal_holder_variable"
regex+="$rex_legal_holder_assignment_op\""
local closing_lhs
closing_lhs="$dynamic_typedef _${modname^^}_${script_now}_COUNT_"
local -i closing_rhs
closing_rhs="$(printf "%s\n" "${__envv[@]}" | grep -oE -- "$regex" | wc -l)"
[ ${#__envv[@]} -eq $closing_rhs ] || {
lets -l -e "variable count mismatch: ${#__envv[@]} vs $closing_rhs"
return $s_err
}
# Flag that $file_rhs is fully bound, accessing this as an array at -1
# always gives the module name of the environment.
echo "$closing_lhs=\"$closing_rhs\";"
elif is_opt "check"; then
echo "local -a deps=( ${__done[@]} )"
fi
# clean up
unset '__deps' && ! declare -p -- '__deps' &>/dev/null || return $s_err
unset '__done' && ! declare -p -- '__done' &>/dev/null || return $s_err
unset '__envv' && ! declare -p -- '__envv' &>/dev/null || return $s_err
unset '__ubnd' && ! declare -p -- '__ubnd' &>/dev/null || return $s_err
fi
}
# @ : list of lines read back from stdout
function set_query_stdout_return_code
{
# Uncoditionally print this on the stdout
printf "%s\n" "${@}"
if ([ -z "${*}" ] && [ -z "${__ubnd[$modname]}" ]); then
# Module fully bound, nothing to do
return $s_ok
elif ([ -n "${*}" ] && [ -n "${__ubnd[$modname]}" ]); then
# Unbound values fetched, so far so good
return $s_maybe_bound
elif ([ -n "${*}" ] && [ -z "${__ubnd[$modname]}" ]); then
# Unexpected stdout for this module
return $s_unk_stdout
elif ([ -z "${*}" ] && [ -n "${__ubnd[$modname]}" ]); then
# No values fetched for unbound variables
return $s_null
else
# It should not get here - bug
return $s_err2
fi
}
# @ : list of lines read back from stderr
function set_query_stderr_return_code
{
local -ar __lines=( "${@}" ); local -ir offset=3
local __l; local -a __la
for __l in "${__lines[@]}"; do
# Turn a line into an array of words
__la=( ${__l} )
# Bail if not unbound variable stderr (smt we don't care about)
is_valid_stderr_unbound $offset "${__la[@]}" || return $s_err
# Leave variable name on the stdout, will try to bind it
echo "${__la[-$offset]%:}"
done
return $s_unbound
}
# @ : list of files to test source with
function __test_source_helper
{
local __f; local set_flags=""
is_set "errexit" || set_flags="+e"
is_set "nounset" || set_flags="$set_flags +u"
is_set "posix" || set_flags="$set_flags +o posix"
set -ue
for __f in ${@}; do
case "$__f" in
"$module_d/"*"/$holder_sh" ) set -o posix ;;
"$module_d/"*"/$scheme_sh" ) set -o posix ;;
esac
# Don't kill the subshell, collect the stderr/stdout first
(source "$__f" || true)
# Source to kill if an error occurs. In subshell because we
# don't care about the actual contents of $__f here.
(source $__f &>/dev/null)
done
[ -n "$set_flags" ] && eval "set $set_flags" || true
}
# @ : list of files to test source with
function test_source
{
local line __l
(__test_source_helper "${@}" 2>&1) | while read -r __l; do
echo -e "$__l\n"
done
return "${PIPESTATUS[0]}"
}
# @1 : flags for sourcing modules
# @2 : list of modules whose files need sourcing
function __source_files_helper
{
local -r include_scheme="$(getflag "-s" "$1")";
local -r multi_source="$(getflag "-m" "$1")"
local -ar modules="${@:2}"
local -a __files=( ); local -i __e
local __mod __f
if [ "true" = "$multi_source" ]; then
# Order in sourcing modules matters
for __mod in ${modules[@]}; do
# Order in sourcing files matters
__files+=( "$module_d/$__mod/$module_sh" )
__files+=( "$module_d/$__mod/$holder_sh" )
__files+=( "$module_d/$__mod/$scheme_sh" )
for __f in ${__files[@]}; do
test_source $__f; __e=$?
[ $__e -eq 0 ] || return $__e
source "$__f" &>/dev/null
done
done
else
local __var; __mod="${modules[0]}"
# First, source external variable needed for this module, if any
local -a ext_var=( "$(get_filter_out_deps $__mod "${__envv[@]}")" )
[ -n "${ext_var[*]}" ] && source <(echo "${ext_var[@]}")
# Second, does it need module.sh?
[ "$__mod" != "$module" ] && __files+=( "$module_d/$__mod/$module_sh" )
# Third, always holder.sh
__files+=( "$module_d/$__mod/$holder_sh" )
# Fourth, does it need scheme.sh?
[ "true" = "$include_scheme" ] && __files+=( "$module_d/$__mod/$scheme_sh" )
for __f in ${__files[@]}; do
test_source $__f; __e=$?
[ $__e -eq 0 ] || return $__e
source "$__f"
done
# Print on stdout the value of unbound variables for this module
for __var in ${__ubnd["$__mod"]}; do
set -ue
# Equivalent to ${!__var}, but will exit reporting unbound
# variable name in ${__var}.
eval "printf \"%q\\\n\" \"\$${__var}\""
done
fi
}
# @1 : flags for sourcing modules
# @2 : list of modules whose files need sourcing
function source_files
{
local -r flags="$1"; local -ar modules="${@:2}"
local line __l
(__source_files_helper "$flags" "${modules[@]}" 2>&1) | while read -r __l; do
echo -e "$__l\n"
done
return "${PIPESTATUS[0]}"
}
# @1 : flags for sourcing modules
# @2 : list of modules whose files need sourcing
function get_stdout_source
{
local -i __e; local -a __var; local old_ifs="$IFS"
local -r flags="$1"; local -ar modules="${@:2}"
IFS=$'\n' __var=( $(source_files "$flags" "${modules[@]}") )
__e=$?; IFS="$old_ifs"
case "$__e" in
# no error and no stderr, however there could be something
# else on the stdout.
$s_ok ) printf "%s\n" "${__var[@]}"; return $s_ok ;;
# some error, maybe unbound, stderr
* ) printf "%s\n" "${__var[@]}"; return $s_err ;;
esac
}
# @1 : flags for sourcing modules
# @2 : list of modules whose files need sourcing
function get_query_subshell
{
local -r old_ifs="$IFS"; local -r flags="$1"; local -ar modules="${@:2}"
local -i __e; local -a stdout
IFS=$'\n' stdout=( $(get_stdout_source "$flags" "${modules[@]}") )
__e=$?; IFS="$old_ifs"
case "$__e" in
$s_err ) set_query_stderr_return_code "${stdout[@]}" ;;
$s_ok ) set_query_stdout_return_code "${stdout[@]}" ;;
esac
}
# @1 : module name
# @ : list of legal holder variable assignments
function get_filter_out_deps
{
local regex="$dynamic_typedef"
regex+=" _${1^^}"
regex+="$rex_legal_holder_assignment_lhs"
printf "%s\n" "${@:2}" | grep -vE -- "$regex"
}
# @ : list of variables
function get_filter_mods
{
local -r regex="^${rex_legal_holder_token}_"
local -a mods
mods="$(printf "%s\n" "${@}" | grep -oE -- "$regex" | sort -u; \
return ${PIPESTATUS[1]})"
if [ $? -ne $s_ok ]; then
lets -l -e "invalid modules: unknown: ${@}"
return $s_unk_stderr
fi
mods="${mods[@]//_/}"; mods="${mods[@],,}"
# TODO: validate found modules against available modules
# if ! __is_in_array $(get_all_modules); then
# lets -l -e "invalid modules: not in MODULES"
# return $s_unk_stderr
# fi
printf "%s\n" "${mods[@]}"; true
}
# TODO: integrate this function into the validation framework
# @1 : module name
function is_valid_pending_ubnd
{
local var
for var in ${__ubnd["$1"]}; do
! declare -p -- "$var" &>/dev/null || {
lets -l -e "$1: $var defined, corrupted environment"
return $s_err
}
done
}
# @ : list of unbound variables
function push_pending_ubnd
{
local mod; local -a mods
mods=( $(get_filter_mods "${@}") )
[ $? -eq $s_ok ] || return $?
for mod in ${mods[@]}; do
local -a ubnd=( $(printf "%s\n" "${@}" | grep -E -- "^_${mod^^}_") )
__ubnd["$mod"]="$(__merge_array "${ubnd[@]}" "${__ubnd[$mod]}")"
[ $? -eq $s_ok ] || return $?
done
}
# @1 : flags for sourcing modules
# @2 : list of modules whose files need sourcing
function get_query_state
{
local flags="$1"; local -ar modules="${@:2}"
local -i query; local -a umod=( )
umod=( "$(get_query_subshell "$flags" ${modules[@]})" ); query=$?
# Verbose output, comment this in for more debug, off by default
#lets -l -d "$modname: [query=$query]: ${umod:-NO UMOD}"
# Leave this on the stdout
printf "%s\n" "${umod[@]}"
case "$query" in
$s_maybe_bound ) return $s_maybe_bound ;;
$s_unk_stdout ) return $s_unk_stdout ;;
$s_unbound ) return $s_maybe_unbound ;;
$s_err2 ) return $s_err ;; # bug
$s_err ) return $s_unk_stderr ;;
$s_ok ) return $s_ok ;;
esac
}
# @1 : module name
# @ : list of holder variable names
function __maybe_unbound_helper
{
local -r modname="$1"; shift
local -ar ulhs=( "${@}" )
local -a umods
local depmod
local -i __e
umods=( $(get_filter_mods ${ulhs[@]}) )
[ $? -eq $s_ok ] || return $?
if __is_in_array $modname ${umods[@]}; then
# This module has is own unbound variables
lets -l -e "$modname: unbound variables: ${ulhs[@]}"
return $s_err
fi
push_pending_ubnd "${ulhs[@]}"; __e=$?
if [ $__e -ne $s_ok ]; then
lets -l -e "$modname: failed to add unbound variables"
return $__e
fi
for depmod in ${umods[@]}; do
if ! is_valid_dependency $depmod; then
lets -l -d "$depmod: invalid dependency"
return $s_err
fi
if ! push_pending_deps $depmod; then
lets -l -w "$deplhs might be unbound"
return $s_err
fi
# Valid dependency, go for another ride
get_file_extarnal_references $depmod
done
}
# @1 : module name
# @2 : list of values for each variable in __ubnd, IFS being '\n'
function __maybe_bound_helper
{
local -r modname="$1"; shift
local -ar urhs=( ${@} )
local -ar ulhs=( ${__ubnd[$modname]} )
local assignment=""
local -i idx
if [ ${#ulhs[@]} -ne ${#urhs[@]} ]; then
# Mismatch in number of lhs and rhs
lets -l -e "$modname: LHS ${#ulhs[@]} vs RHS ${#urhs[@]}"
lets -l -e "LHS: ${ulhs[@]}"
lets -l -e "RHS: ${urhs[@]}"
return $s_err
fi
for idx in ${!ulhs[@]}; do
local lhs="${ulhs[$idx]}"
local rhs="$(echo "${urhs[$idx]}")"
if [ -z "$rhs" ] || [ "$rhs" = "''" ]; then
lets -l -e "$lhs value is null"
return $s_null
fi
assignment="$dynamic_typedef $lhs=\"$rhs\";"
if is_opt "watch" "doins" "upins" "upall" "doall"; then
# Only leave it on the stdout for those options
echo "$assignment"
fi
__envv+=( "$assignment" )
done
# clear __ubnd
unset __ubnd["$modname"]
}
# @- : (optional) flag for including scheme.sh in the query
# @1 : lhs variable name or null for querying any unbound variable
# @2 : module name
# @return : stdout, a list of variable assignments. Specifically, only those
# : whose rhs-value is an external reference.
function get_file_extarnal_references
{
local sf; case "$1" in -s) sf="-s"; shift ;; *) ;; esac
# This will be unset on exit
declare -p -- '__deps' &>/dev/null || declare -ga __deps=( )
declare -p -- '__done' &>/dev/null || declare -ga __done=( )
declare -p -- '__envv' &>/dev/null || declare -ga __envv=( )
declare -p -- '__ubnd' &>/dev/null || declare -gA __ubnd=( )
local -a ret=( ); local -i state
local -r modname="$1"
ret="$(get_query_state "$sf" "$modname")"; state="$?"
# Verbose output, comment this in for more debug, off by default
#lets -l -d "$modname: [state=$state]: ${ret[@]:-NO RET}"
# Update state with helpers work
case "$state" in
$s_maybe_unbound )
__maybe_unbound_helper $modname "${ret[@]}"; state=$?
# Return only if *not* $s_ok
[ $state -eq $s_ok ] || return $state ;;
$s_maybe_bound )
__maybe_bound_helper $modname "${ret[@]}"; state=$?
# Return only if *not* $s_ok
[ $state -eq $s_ok ] || return $state
# If $modname was done already, then early return
! is_in_done $modname || {
pop_pending_deps $modname; state=$?
# Always return
return $state;
} ;;
$s_ok ) get_module_complete $modname; state=$?
# Return only if $s_ok
[ $state -eq $s_ok ] && return $s_ok ;;
esac
# Verbose output, comment this in for more debug, off by default
#lets -l -d "$modname: [state=$state]: UPD: ${ret[@]:-NO RET}"
case "$state" in
# Errors
$s_unk_stdout ) lets -l -e "unknown stdout" ; return $s_err ;;
$s_unk_stderr ) lets -l -e "unknown stderr" ; return $s_err ;;
$s_inv_lhs ) lets -l -e "invalid variable: $ret" ; return $s_err ;;
$s_unbound ) lets -l -e "unbound variable: $ret" ; return $s_err ;;
$s_null ) lets -l -e "$lhs value is null" ; return $s_err ;;
$s_err ) lets -l -e "query failed" ; return $s_err ;;
esac
[ $state -eq $s_ok ] && get_file_extarnal_references $sf $modname
}
# @1 : holder variable rhs assignment, missing leading double quote sign
function is_valid_eval_rhs
{
# Eval black magic
#
# RHS didn't pass regex validation, this can be for two reasons, a good one
# and a bad one. For a good reason, the line is of the form:
#
# LHS="RHS"; LHS2="RHS2"
# LHS="RHS"; for i in "${a[@]}"
# LHS="RHS" && LHS2="RHS2"
#
# these are only some examples of genuine failure, holder.sh needs fixing.
# For the a bad reason, the line is of the form:
#
# LHS="RHS$(command "")"
#
# An example of this is command substitution in the assignment RHS, this is
# a false positive that must be granted and pass validation.
#
# Regex RHS validation only allows one pair of unescaped double quotes per
# RHS, they are supposed to enclose the whole RHS from start to end, and any
# other double quote appearing within this region should be escaped.
#
# In the latter case above, regex validation detects text after the second
# encountered, unescaped, double quote. For instance from an input such as
# `"RHS$(command "arg")"`, it will match `arg")"` as illegal.
#
# Naturally bash doesn't demand an argument passed to a command substitution
# to be quoted, but here holder.sh files run with the errexit flag, thus one
# command might fail when provided with a null argument. On the other hand,
# such command could handle arguments of length 0 but not null, such as `""`,
# hence quoting every argument in holder files becomes necessary.
#
# This whole validation system is artificial, arbitrarily made by the author
# to make assumptions over the state of a template. It also aims to give the
# developer enough flexibility in writing holder files, for, not preventing
# him or her to make use of constructs like command substitution in holder.sh
# variables.
#
# To fix this, extend RHS validation when it fails and process further the
# supposedly illegal line. is_valid_eval_rhs() takes the RHS text that has
# matched as illegal against $rex_illegal_holder_assignment_rhs and it tries
# to establish if this is a false positive by using the built in `set -x`.
#
# When it gets to this point errexit already passed, so it is fair to assume
# this is a bash legal line.
(
local -i __stack_ops
local __stack_lvl
local __xline
while read -r __xline; do
if echo $__xline | grep -q -- "__stack_${script_now}"; then
__stack_lvl="$(echo $__xline | cut -d' ' -f 1)"
__stack_lvl="${__stack_lvl%%__stack_$script_now*}"
else
[ -z "${__xline%%$__stack_lvl *}" ] && ((__stack_ops++))
echo $__xline | grep -q -- "__lhs_${script_now}" && {
local __lhs_lvl="${__xline%% __lhs_$script_now*}"
[ "$__stack_lvl" = "$__lhs_lvl" ] || return $s_err2
}
fi
done < <(
eval "set -x; __stack_${script_now}=0; __lhs_${script_now}=\"$(echo $*)" 2>&1
); wait $!
[ $__stack_ops -eq 1 ] 2>/dev/null || return $s_err
) && return $s_ok
case "$?" in
$s_err ) lets -l -e "too many commands detected per line" ;;
$s_err2 ) lets -l -e "cannot determine commands per line" ;;
esac; return $s_err
}
# @1 : module name
# @2 : holder.sh line
# @desc : This function can only work with bash script that is known to be
# : valid, as in, it makes assumptions upon valid bash syntex. Make sure
# : the line given in $2 belongs to a valid bash script.
function is_compliant_holder_line
{
local modname=$1
local line=$2
case "$line" in
# Allow conditional statement
\if* | \else* | \elif* | \fi* ) return $s_ok ;;
# Allow blank lines
'\n' | '' ) return $s_ok ;;
# Allow comments
'#'* ) return $s_ok ;;
esac
# From this point, $line can only be an assignment
# Assignment validation, part 1: lhs
#
# $rex_legal_holder_assignment_lhs is a module generic pattern, so make it
# specific to this particular module. Also, $modname must be upper case.
local rex_legal_lhs="^_${modname^^}$rex_legal_holder_assignment_lhs"
local legal_lhs="$(echo "$line" | grep -oE -- $rex_legal_lhs)"
if [ -n "$legal_lhs" ]; then
local rhs=${line##$legal_lhs}
# Assignment validation, part 2: rhs
#
# At this point, according to $rex_illegal_holder_assignment_rhs,
# nothing is allowed on this line other than the assignment rhs.
local rex_illegal_rhs="$rex_illegal_holder_assignment_rhs"
local illegal_rhs="$(echo "$rhs" | grep -oE -- $rex_illegal_rhs)"
[ -z "$illegal_rhs" ] && return $s_ok
is_valid_eval_rhs "$rhs" && {
[ "${illegal_rhs: -1}" != '"' ] && \
lets -l -w "suspicious: $illegal_rhs"
return $s_ok # no error
}
lets -l -e "illegal RHS: $rhs"
else
lets -l -e "illegal LHS: $line"
fi
# Here is error only
return $s_err
}
# @1 : module name
# @2 : absolute path to holder file
function is_compliant_holder
{
local modname=$1
local -i nline
local line
# Is it of the desired form?
while read -r line; do
((nline++))
if ! is_compliant_holder_line $modname "$(echo "$line")"; then
lets -l -e "${2##$module_d/}: line $nline is not compliant"
return $s_err
fi
done < $2
}
# @1 : module name
function is_compliant_module
{
local -r modname=$1
local -r modfile="$module_d/$modname/$module_sh"
local -a valid_modname=""
(
read -a valid_modname < <(
# $module should never be set at this point
! declare -p -- "module" &>/dev/null || return $s_err2
local -i err; local stdout
stdout="$(set -eu; source $modfile)"; err=$?
case "$err" in
$s_ok )
[ -n "$stdout" ] && {
echo "$stdout"; return $s_err3
} || { set -eu; source "$modfile" || return $err4; } ;;
* ) return $err4 ;;
esac
[ "$module" = "$modname" ] || return $s_err5
[ -n "$module" ] && [ -d "$module_d/$module" ] || return $s_err6
declare -p -- "mod_module_d" >/dev/null || return $s_err7
declare -p -- "depmod" &>/dev/null || \
{ set +eu; lets -l -w "$modname doesn't have depmod"; set -eu; }
is_in_modules $module || return $s_err8
set +eu; echo "${depmod[@]} $module" && return $s_ok
); wait $!
case "$?" in
$s_err2 ) lets -l -e "corrupted environment" ;;
$s_err3 ) lets -l -e "$modname/$module_sh spams the stdout"
lets -l -e "this was found on the stdout: ${valid_modname[*]}" ;;
$s_err4 ) lets -l -e "failed to source $modfile" ;;
$s_err5 ) lets -l -e "$modfile doesn't define ${modname:-undefined}" ;;
$s_err6 ) lets -l -e "${modname:-undefined} doesn't exist" ;;
$s_err7 ) lets -l -e "is '$modname' sourcing common.sh" ;;
$s_err8 ) lets -l -w "disabled: '$modname'"; return $s_disabled ;;
$s_ok ) lets -l -i "enabled: '$modname'"
printf "%s\n" "${valid_modname[@]}"; return $s_ok ;;
esac; return $s_err
); return $?
}
# @1 : absolute path to bash script
function run_bash_errexit
{
{ /bin/bash -o errexit $1 ${@:2} 2>/dev/null; } || {
lets -l -e "${1##$module_d/}: doesn't even source, errexit"
return $s_err
}
}
# @1 : absolute path to holder.sh
# @desc : For efficiency and for semantic reasons, this function needs
# : to do multiple things at once, otherwise it would and should
# : have been broken down. Do:
# :
# : 1. validate holder.sh for errexit
# : 2. validate module.sh for errexit and nounset
# : 3. validate holder.sh syntax line by line
# :
# : Only if this function succeeds, any attempt to bind unbound
# : variables in holders.sh is really meaningful.
function __is_valid_module_static
{
local modname="$(get_modname_from_file $1)"
local stdout; local -i ret; local -a deps
# Can holder run?
stdout="$(run_bash_errexit $1)"
case "$?" in
$s_ok ) [ -n "$stdout" ] && {
lets -l -w "${1##$module_d/} spams the stdout"
} ;;
* ) return $s_err ;;
esac
deps=( $(is_compliant_module $modname) ); ret=$?
is_compliant_holder $modname $1 || {
# If this is disabled maybe that's fine even if broken
[ $ret -eq $s_disabled ] && {
lets -l -w "$modname broken, can break the dependencies"
return $s_off_broken
} || return $s_err
}
[ $ret -eq $s_disabled ] && return $s_off_usable || {
printf "%s\n" "${deps[@]}"
return $ret
}
}
# @ : list of absolute path to holder.sh files
# @return : error if at least one fails
function is_valid_module
{
local -a deps
local modname
lets -l -d "checking ${#} module(s): ${@}"
for modname in ${@}; do
local holder="$module_d/$modname/$holder_sh"
deps=$(__is_valid_module_static $holder)
case "$?" in
$s_off_broken ) [ ${#} -eq 1 ] && return $s_off_broken || continue ;;
$s_off_usable ) [ ${#} -eq 1 ] && return $s_off_usable || continue ;;
$s_ok ) MODULES+=( $modname )
MODULES=( $(__merge_array "${MODULES[@]}" "${deps[@]}") ) ;;
$s_err ) return $s_err ;;
esac
done
}
# @ : list of legal holder assignments
function is_valid_env
{
local -r modname=$1
# Returned count of variables set in the environment
#
# get_file_extarnal_references() returns the number of variables used in a
# scheme.sh and holder.sh as last variable of the list. Such count does not
# include itself (this is appended to the list afterwards).
local -i count="$2"
[ -n "${modname}" ] && shift 2 || {
lets -l -e "dynamic env not valid, missing modname"
return $s_err
}
# keep it simple, we know that's what it is called
local -r var="_${modname^^}_${script_now}_COUNT_"
local -ar module_env=( "${@}" )
(
# First, if the subshell is corrupted, bail out
unset $var && ! declare -p -- $var 2>/dev/null || return $s_err3
# Second, if get_valid_modulenv_dynamic() count doesn't match with
# the lenght of assignment list passed in, bail out.
[ $count -eq ${#module_env[@]} ] || return $s_err4
# Third, if the count done by get_file_extarnal_references() doesn't
# match $count - 1, bail out.
eval "${module_env[-1]}"
((count--)); [ ${!var} -eq $count ] || return $s_err5
) || {
case "$?" in
$s_err3 ) lets -l -e "corrupted environment" ;;
$s_err4 ) lets -l -e "list count mismatch: $count vs ${#module_env[@]}" ;;
$s_err5 ) lets -l -e "$modname: invalid: ${#module_env[@]} vs ${!var}" ;;
esac; return $s_err
}
lets -l -i "'$modname' bound with $(($count-1)) external references"; return $s_ok
}
# @ : list of module names
# @return : stdout on success, a valid associative array declaration where every
# : key is the module name and its value is a list of valid holder
# : variable assignments. Note, this function take a list of modules, but
# : as per current design it is always called provinding only one module.
function get_module_dependencies
{
local -A modulenv; local modname; local -a env=( )
local table="$1"; shift
lets -l -i "resolving ${#} module(s): ${@}"
for modname in ${@}; do
lets -l -d "working on '$modname'"
local -i psfd; local -i pspid
# Open a read fd, start processing a scheme file
exec {psfd}< <(
get_file_extarnal_references -s $modname
); pspid=$!
if is_opt "watch" "upins" "upall" "doins" "doall"; then
local assignment; local -i count=0;
while read -u $psfd -r assignment; do
# A little validation of what is coming back from stdout, lhs only
local regex="$rex_legal_dynamic_assignment_lhs.*"
echo "$assignment" | grep -qE -- "$regex" || {
lets -l -e "invalid assignment: $assignment"
# Close fd, kill producer, wait and return an error
exec {psfd}<&-; kill $pspid; wait; return $s_err
}
env+=( "$assignment" ); ((count++))
done
wait $pspid && is_valid_env $modname $count "${env[@]}" || {
lets -l -e "couldn't resolve $modname"
return $s_err
}
elif is_opt "check"; then
wait $pspid; read -u $psfd -a env
[ $? -eq 0 ] || return $s_err
fi
# Collect readings
modulenv[$modname]+="${env[*]}"
exec {psfd}<&-
done
# Once processed all modules
[ -n "$table" ] && {
# return all valid environments and then make it readonly
local def="$(declare -p -- modulenv)"
(echo "${def/declare -A modulenv/declare -rA $table}")
return $s_ok # success
}
lets -l -e "cannot export module environment: no table"
return $s_err # misuse
}