| #!/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 |
| } |