blob: 4b5f206e52f5c7d93fb152c6230181b56d701a93 [file] [log] [blame]
Luigi Santivetti0fdd4702020-06-22 19:00:32 +01001#!/bin/bash
2#
3# validation.sh
4#
5# Copyright 2019 Luigi Santivetti <luigi.santivetti@gmail.com>
6
7# Permission is hereby granted, free of charge, to any person obtaining a
8# copy of this software and associated documentation files (the "Software"),
9# to deal in the Software without restriction, including without limitation
10# the rights to use, copy, modify, merge, publish, distribute, sublicense,
11# and/or sell copies of the Software, and to permit persons to whom the
12# Software is furnished to do so, subject to the following conditions:
13
14# The above copyright notice and this permission notice (including the next
15# paragraph) shall be included in all copies or substantial portions of the
16# Software.
17
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21# ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25# @1 : holder variable name - lhs
26# @desc : if @1 matches against $rex_legal_holder_variable, then convert
27# : it back to the original module name
28function get_modname_from_variable
29{
30 is_valid_holder_lhs "${*}" || return $s_err
31
32 # Remove leading underscore, strip longest underscore match from end,
33 # finally lowercase it
34 local mod=$1; mod=${mod:1}; mod=${mod%%_*}; mod=${mod,,}
35
36 # Verify there is one
37 [ -d "$module_d/$mod" ] || {
38 lets -l -e "'${mod:-undefined-module}' not found"
39 return $s_err
40 }
41
42 echo $mod # Success
43}
44
45# @1 : absolute path to a module's sh file
46# @desc : derive module name from holder.sh path
47function get_modname_from_file
48{
49 local modname
50
51 [ -f "$1" ] && {
52 modname="$(dirname "$1")"; modname=${modname##*/}; echo $modname
53 } || echo ""
54}
55
56# @1 : holder variable name
57function is_valid_holder_lhs
58{
59 [ -n "$1" ] || return $s_err
60
61 # Is this variable a holder legal one
62 echo $1 | grep -qE -- "$rex_legal_holder_variable"
63
64 [ ${PIPESTATUS[1]} -eq 0 ]
65}
66
67# @1 : module name
68function is_valid_dependency
69{
70 local -r modname="$1"
71
72 if is_in_modules $modname; then
73 return $s_ok
74 elif is_in_optarg $modname; then
75 lets -l -w "'$modname' was requested, but it's turned off"
76 else
77 local m_list="${MODULES[*]},$modname"; m_list="${m_list// /,}"
78 lets -l -d "adding dependency: '$module' on '$modname'"
79
80 is_opt "check" && module_enable "$1" && return $s_ok
81
82 # we cannot source a new module within this subshell, they need to be
83 # included from the beginning.
84 is_opt "watch" "doins" "upins" "upall" "doall" && \
85 lets -l -e "$(get_opt) cannot complete, run --check ${MODULES[@]}"
86 fi
87
88 return $s_err
89}
90
91# @1 : module name
92function is_in_done
93{
94 local -r modname="$1"
95
96 __is_in_array "$modname" "${__done[@]}"
97}
98
99# @1 : module name
100function push_done_deps
101{
102 local -r modname="$1"
103
104 is_in_done "$modname" || __done+=( "$modname" )
105}
106
107# @1 : module name
108function is_in_deps
109{
110 local -r modname="$1"
111
112 __is_in_array "$modname" "${__deps[@]}"
113}
114
115# @1 : module name
116function pop_pending_deps
117{
118 local -r modname="$1"
119
120 if ! is_in_deps "$modname"; then
121 lets -l -e "dependency is missing: $modname"
122 return $s_err
123 fi
124
125 unset '__deps[-1]'
126}
127
128# @1 : module name
129function push_pending_deps
130{
131 local -r modname="$1"
132
133 if is_in_deps "$modname"; then
134 lets -l -e "circular dependecy on $modname"
135 return $s_err
136 fi
137
138 __deps+=( "$modname" )
139}
140
141# @1 : module name
142function update_dependecies
143{
144 local -r modname="$1"
145
146 pop_pending_deps $modname && push_done_deps $modname && \
147 return $s_ok || return $s_err
148
149 return $s_ok # __deps can be 0
150}
151
152# @1 : maybe unbound error from stderr
153function __is_valid_stderr_unbound
154{
155 local -r __error="unbound"
156
157 [ "$1" = "$__error" ] && return $s_ok
158
159 return $s_err
160}
161
162# @1 : maybe word from stderr
163function __is_valid_stderr_words
164{
165 [ "$(wc -w <<< "$*")" -eq 1 ] && return $s_ok
166
167 lets -l -e "'$*' unknown words"
168 return $s_err
169}
170
171# @1 : error message delimiter
172function __is_valid_stderr_delimeter
173{
174 local -r regex=":$"
175
176 (echo "$1" | grep -qE -- "$regex") && return $s_ok
177
178 lets -l -e "'$1' unknown delimiter"
179 return $s_err
180}
181
182# @1 : tail offset for unbound variable name in stderr message
183function is_valid_stderr_unbound
184{
185 local -ir offset=$1; shift
186 local -ar stderr=( ${@} )
187 local -ir unbofs="$offset - 1"
188
189 # Don't get crazy about validation, this is clearly fragile and it
190 # will just break on a different shell.
191
192 [ ${#stderr[@]} -ge $offset ] || {
193 lets -l -e "$offset is oob ${#stderr[@]}, ${stderr[*]}"
194 return $s_err
195 }
196
197 __is_valid_stderr_unbound ${stderr[-$unbofs]} || {
198 lets -l -e "stderr reported:"
199 lets -l -e "${stderr[@]}";
200 return $s_err
201 }
202
203 __is_valid_stderr_words ${stderr[-$offset]} && \
204 __is_valid_stderr_delimeter ${stderr[-$offset]} || return $s_err
205}
206
207# @1 : module name
208function get_module_complete
209{
210 local -r modname="$1"; shift
211
212 if [ ${#__deps[@]} -gt 0 ]; then
213 update_dependecies $modname && return $s_ok || return $s_err
214 fi
215
216 if [ ${#__deps[@]} -eq 0 ]; then
217 if is_opt "watch" "doins" "upins" "upall" "doall"; then
218 local regex="$rex_legal_dynamic_typedef"
219 regex+="[[:space:]]$rex_legal_holder_variable"
220 regex+="$rex_legal_holder_assignment_op\""
221
222 local closing_lhs
223 closing_lhs="$dynamic_typedef _${modname^^}_${script_now}_COUNT_"
224
225 local -i closing_rhs
226 closing_rhs="$(printf "%s\n" "${__envv[@]}" | grep -oE -- "$regex" | wc -l)"
227
228 [ ${#__envv[@]} -eq $closing_rhs ] || {
229 lets -l -e "variable count mismatch: ${#__envv[@]} vs $closing_rhs"
230 return $s_err
231 }
232
233 # Flag that $file_rhs is fully bound, accessing this as an array at -1
234 # always gives the module name of the environment.
235 echo "$closing_lhs=\"$closing_rhs\";"
236 elif is_opt "check"; then
237 echo "local -a deps=( ${__done[@]} )"
238 fi
239
240 # clean up
241 unset '__deps' && ! declare -p -- '__deps' &>/dev/null || return $s_err
242 unset '__done' && ! declare -p -- '__done' &>/dev/null || return $s_err
243 unset '__envv' && ! declare -p -- '__envv' &>/dev/null || return $s_err
244 unset '__ubnd' && ! declare -p -- '__ubnd' &>/dev/null || return $s_err
245 fi
246}
247
248# @ : list of lines read back from stdout
249function set_query_stdout_return_code
250{
251 # Uncoditionally print this on the stdout
252 printf "%s\n" "${@}"
253
254 if ([ -z "${*}" ] && [ -z "${__ubnd[$modname]}" ]); then
255
256 # Module fully bound, nothing to do
257 return $s_ok
258 elif ([ -n "${*}" ] && [ -n "${__ubnd[$modname]}" ]); then
259
260 # Unbound values fetched, so far so good
261 return $s_maybe_bound
262 elif ([ -n "${*}" ] && [ -z "${__ubnd[$modname]}" ]); then
263
264 # Unexpected stdout for this module
265 return $s_unk_stdout
266 elif ([ -z "${*}" ] && [ -n "${__ubnd[$modname]}" ]); then
267
268 # No values fetched for unbound variables
269 return $s_null
270 else
271
272 # It should not get here - bug
273 return $s_err2
274 fi
275}
276
277# @ : list of lines read back from stderr
278function set_query_stderr_return_code
279{
280 local -ar __lines=( "${@}" ); local -ir offset=3
281 local __l; local -a __la
282
283 for __l in "${__lines[@]}"; do
284
285 # Turn a line into an array of words
286 __la=( ${__l} )
287
288 # Bail if not unbound variable stderr (smt we don't care about)
289 is_valid_stderr_unbound $offset "${__la[@]}" || return $s_err
290
291 # Leave variable name on the stdout, will try to bind it
292 echo "${__la[-$offset]%:}"
293 done
294
295 return $s_unbound
296}
297# @ : list of files to test source with
298function __test_source_helper
299{
300 local __f; local set_flags=""
301
302 is_set "errexit" || set_flags="+e"
303 is_set "nounset" || set_flags="$set_flags +u"
304 is_set "posix" || set_flags="$set_flags +o posix"
305
306 set -ue
307 for __f in ${@}; do
308 case "$__f" in
309 "$module_d/"*"/$holder_sh" ) set -o posix ;;
310 "$module_d/"*"/$scheme_sh" ) set -o posix ;;
311 esac
312
313 # Don't kill the subshell, collect the stderr/stdout first
314 (source "$__f" || true)
315
316 # Source to kill if an error occurs. In subshell because we
317 # don't care about the actual contents of $__f here.
318 (source $__f &>/dev/null)
319 done
320
321 [ -n "$set_flags" ] && eval "set $set_flags" || true
322}
323
324# @ : list of files to test source with
325function test_source
326{
327 local line __l
328
329 (__test_source_helper "${@}" 2>&1) | while read -r __l; do
330 echo -e "$__l\n"
331 done
332
333 return "${PIPESTATUS[0]}"
334}
335
336# @1 : flags for sourcing modules
337# @2 : list of modules whose files need sourcing
338function __source_files_helper
339{
340 local -r include_scheme="$(getflag "-s" "$1")";
341 local -r multi_source="$(getflag "-m" "$1")"
342 local -ar modules="${@:2}"
343 local -a __files=( ); local -i __e
344 local __mod __f
345
346 if [ "true" = "$multi_source" ]; then
347 # Order in sourcing modules matters
348 for __mod in ${modules[@]}; do
349 # Order in sourcing files matters
350 __files+=( "$module_d/$__mod/$module_sh" )
351 __files+=( "$module_d/$__mod/$holder_sh" )
352 __files+=( "$module_d/$__mod/$scheme_sh" )
353
354 for __f in ${__files[@]}; do
355 test_source $__f; __e=$?
356 [ $__e -eq 0 ] || return $__e
357 source "$__f" &>/dev/null
358 done
359 done
360 else
361 local __var; __mod="${modules[0]}"
362
363 # First, source external variable needed for this module, if any
364 local -a ext_var=( "$(get_filter_out_deps $__mod "${__envv[@]}")" )
365
366 [ -n "${ext_var[*]}" ] && source <(echo "${ext_var[@]}")
367
368 # Second, does it need module.sh?
369 [ "$__mod" != "$module" ] && __files+=( "$module_d/$__mod/$module_sh" )
370
371 # Third, always holder.sh
372 __files+=( "$module_d/$__mod/$holder_sh" )
373
374 # Fourth, does it need scheme.sh?
375 [ "true" = "$include_scheme" ] && __files+=( "$module_d/$__mod/$scheme_sh" )
376
377 for __f in ${__files[@]}; do
378 test_source $__f; __e=$?
379 [ $__e -eq 0 ] || return $__e
380 source "$__f"
381 done
382
383 # Print on stdout the value of unbound variables for this module
384 for __var in ${__ubnd["$__mod"]}; do
385 set -ue
386
387 # Equivalent to ${!__var}, but will exit reporting unbound
388 # variable name in ${__var}.
389 eval "printf \"%q\\\n\" \"\$${__var}\""
390 done
391 fi
392}
393
394# @1 : flags for sourcing modules
395# @2 : list of modules whose files need sourcing
396function source_files
397{
398 local -r flags="$1"; local -ar modules="${@:2}"
399 local line __l
400
401 (__source_files_helper "$flags" "${modules[@]}" 2>&1) | while read -r __l; do
402 echo -e "$__l\n"
403 done
404
405 return "${PIPESTATUS[0]}"
406}
407
408# @1 : flags for sourcing modules
409# @2 : list of modules whose files need sourcing
410function get_stdout_source
411{
412 local -i __e; local -a __var; local old_ifs="$IFS"
413 local -r flags="$1"; local -ar modules="${@:2}"
414
415 IFS=$'\n' __var=( $(source_files "$flags" "${modules[@]}") )
416 __e=$?; IFS="$old_ifs"
417
418 case "$__e" in
419
420 # no error and no stderr, however there could be something
421 # else on the stdout.
422 $s_ok ) printf "%s\n" "${__var[@]}"; return $s_ok ;;
423
424 # some error, maybe unbound, stderr
425 * ) printf "%s\n" "${__var[@]}"; return $s_err ;;
426 esac
427}
428
429# @1 : flags for sourcing modules
430# @2 : list of modules whose files need sourcing
431function get_query_subshell
432{
433 local -r old_ifs="$IFS"; local -r flags="$1"; local -ar modules="${@:2}"
434 local -i __e; local -a stdout
435
436 IFS=$'\n' stdout=( $(get_stdout_source "$flags" "${modules[@]}") )
437 __e=$?; IFS="$old_ifs"
438
439 case "$__e" in
440 $s_err ) set_query_stderr_return_code "${stdout[@]}" ;;
441 $s_ok ) set_query_stdout_return_code "${stdout[@]}" ;;
442 esac
443}
444
445# @1 : module name
446# @ : list of legal holder variable assignments
447function get_filter_out_deps
448{
449 local regex="$dynamic_typedef"
450 regex+=" _${1^^}"
451 regex+="$rex_legal_holder_assignment_lhs"
452
453 printf "%s\n" "${@:2}" | grep -vE -- "$regex"
454}
455
456# @ : list of variables
457function get_filter_mods
458{
459 local -r regex="^${rex_legal_holder_token}_"
460 local -a mods
461
462 mods="$(printf "%s\n" "${@}" | grep -oE -- "$regex" | sort -u; \
463 return ${PIPESTATUS[1]})"
464 if [ $? -ne $s_ok ]; then
465 lets -l -e "invalid modules: unknown: ${@}"
466 return $s_unk_stderr
467 fi
468
469 mods="${mods[@]//_/}"; mods="${mods[@],,}"
470
471 # TODO: validate found modules against available modules
472# if ! __is_in_array $(get_all_modules); then
473# lets -l -e "invalid modules: not in MODULES"
474# return $s_unk_stderr
475# fi
476
477 printf "%s\n" "${mods[@]}"; true
478}
479
480# TODO: integrate this function into the validation framework
481# @1 : module name
482function is_valid_pending_ubnd
483{
484 local var
485
486 for var in ${__ubnd["$1"]}; do
487 ! declare -p -- "$var" &>/dev/null || {
488 lets -l -e "$1: $var defined, corrupted environment"
489 return $s_err
490 }
491 done
492}
493
494# @ : list of unbound variables
495function push_pending_ubnd
496{
497 local mod; local -a mods
498
499 mods=( $(get_filter_mods "${@}") )
500 [ $? -eq $s_ok ] || return $?
501
502 for mod in ${mods[@]}; do
503 local -a ubnd=( $(printf "%s\n" "${@}" | grep -E -- "^_${mod^^}_") )
504 __ubnd["$mod"]="$(__merge_array "${ubnd[@]}" "${__ubnd[$mod]}")"
505 [ $? -eq $s_ok ] || return $?
506 done
507}
508
509# @1 : flags for sourcing modules
510# @2 : list of modules whose files need sourcing
511function get_query_state
512{
513 local flags="$1"; local -ar modules="${@:2}"
514 local -i query; local -a umod=( )
515
516 umod=( "$(get_query_subshell "$flags" ${modules[@]})" ); query=$?
517
518 # Verbose output, comment this in for more debug, off by default
519 #lets -l -d "$modname: [query=$query]: ${umod:-NO UMOD}"
520
521 # Leave this on the stdout
522 printf "%s\n" "${umod[@]}"
523
524 case "$query" in
525 $s_maybe_bound ) return $s_maybe_bound ;;
526 $s_unk_stdout ) return $s_unk_stdout ;;
527 $s_unbound ) return $s_maybe_unbound ;;
528 $s_err2 ) return $s_err ;; # bug
529 $s_err ) return $s_unk_stderr ;;
530 $s_ok ) return $s_ok ;;
531 esac
532}
533
534# @1 : module name
535# @ : list of holder variable names
536function __maybe_unbound_helper
537{
538 local -r modname="$1"; shift
539 local -ar ulhs=( "${@}" )
540 local -a umods
541 local depmod
542 local -i __e
543
544 umods=( $(get_filter_mods ${ulhs[@]}) )
545 [ $? -eq $s_ok ] || return $?
546
547 if __is_in_array $modname ${umods[@]}; then
548
549 # This module has is own unbound variables
550 lets -l -e "$modname: unbound variables: ${ulhs[@]}"
551 return $s_err
552 fi
553
554 push_pending_ubnd "${ulhs[@]}"; __e=$?
555 if [ $__e -ne $s_ok ]; then
556 lets -l -e "$modname: failed to add unbound variables"
557 return $__e
558 fi
559
560 for depmod in ${umods[@]}; do
561 if ! is_valid_dependency $depmod; then
562 lets -l -d "$depmod: invalid dependency"
563 return $s_err
564 fi
565
566 if ! push_pending_deps $depmod; then
567 lets -l -w "$deplhs might be unbound"
568 return $s_err
569 fi
570
571 # Valid dependency, go for another ride
572 get_file_extarnal_references $depmod
573 done
574}
575
576# @1 : module name
577# @2 : list of values for each variable in __ubnd, IFS being '\n'
578function __maybe_bound_helper
579{
580 local -r modname="$1"; shift
581 local -ar urhs=( ${@} )
582 local -ar ulhs=( ${__ubnd[$modname]} )
583 local assignment=""
584 local -i idx
585
586 if [ ${#ulhs[@]} -ne ${#urhs[@]} ]; then
587
588 # Mismatch in number of lhs and rhs
589 lets -l -e "$modname: LHS ${#ulhs[@]} vs RHS ${#urhs[@]}"
590 lets -l -e "LHS: ${ulhs[@]}"
591 lets -l -e "RHS: ${urhs[@]}"
592 return $s_err
593 fi
594
595 for idx in ${!ulhs[@]}; do
596 local lhs="${ulhs[$idx]}"
597 local rhs="$(echo "${urhs[$idx]}")"
598
599 if [ -z "$rhs" ] || [ "$rhs" = "''" ]; then
600 lets -l -e "$lhs value is null"
601 return $s_null
602 fi
603
604 assignment="$dynamic_typedef $lhs=\"$rhs\";"
605 if is_opt "watch" "doins" "upins" "upall" "doall"; then
606
607 # Only leave it on the stdout for those options
608 echo "$assignment"
609 fi
610
611 __envv+=( "$assignment" )
612 done
613
614 # clear __ubnd
615 unset __ubnd["$modname"]
616}
617
618# @- : (optional) flag for including scheme.sh in the query
619# @1 : lhs variable name or null for querying any unbound variable
620# @2 : module name
621# @return : stdout, a list of variable assignments. Specifically, only those
622# : whose rhs-value is an external reference.
623function get_file_extarnal_references
624{
625 local sf; case "$1" in -s) sf="-s"; shift ;; *) ;; esac
626
627 # This will be unset on exit
628 declare -p -- '__deps' &>/dev/null || declare -ga __deps=( )
629 declare -p -- '__done' &>/dev/null || declare -ga __done=( )
630 declare -p -- '__envv' &>/dev/null || declare -ga __envv=( )
631 declare -p -- '__ubnd' &>/dev/null || declare -gA __ubnd=( )
632
633 local -a ret=( ); local -i state
634 local -r modname="$1"
635
636 ret="$(get_query_state "$sf" "$modname")"; state="$?"
637
638 # Verbose output, comment this in for more debug, off by default
639 #lets -l -d "$modname: [state=$state]: ${ret[@]:-NO RET}"
640
641 # Update state with helpers work
642 case "$state" in
643 $s_maybe_unbound )
644 __maybe_unbound_helper $modname "${ret[@]}"; state=$?
645
646 # Return only if *not* $s_ok
647 [ $state -eq $s_ok ] || return $state ;;
648
649 $s_maybe_bound )
650 __maybe_bound_helper $modname "${ret[@]}"; state=$?
651
652 # Return only if *not* $s_ok
653 [ $state -eq $s_ok ] || return $state
654
655 # If $modname was done already, then early return
656 ! is_in_done $modname || {
657 pop_pending_deps $modname; state=$?
658
659 # Always return
660 return $state;
661 } ;;
662
663 $s_ok ) get_module_complete $modname; state=$?
664
665 # Return only if $s_ok
666 [ $state -eq $s_ok ] && return $s_ok ;;
667 esac
668
669 # Verbose output, comment this in for more debug, off by default
670 #lets -l -d "$modname: [state=$state]: UPD: ${ret[@]:-NO RET}"
671
672 case "$state" in
673 # Errors
674 $s_unk_stdout ) lets -l -e "unknown stdout" ; return $s_err ;;
675 $s_unk_stderr ) lets -l -e "unknown stderr" ; return $s_err ;;
676 $s_inv_lhs ) lets -l -e "invalid variable: $ret" ; return $s_err ;;
677 $s_unbound ) lets -l -e "unbound variable: $ret" ; return $s_err ;;
678 $s_null ) lets -l -e "$lhs value is null" ; return $s_err ;;
679 $s_err ) lets -l -e "query failed" ; return $s_err ;;
680 esac
681
682 [ $state -eq $s_ok ] && get_file_extarnal_references $sf $modname
683}
684
685# @1 : holder variable rhs assignment, missing leading double quote sign
686function is_valid_eval_rhs
687{
688 # Eval black magic
689 #
690 # RHS didn't pass regex validation, this can be for two reasons, a good one
691 # and a bad one. For a good reason, the line is of the form:
692 #
693 # LHS="RHS"; LHS2="RHS2"
694 # LHS="RHS"; for i in "${a[@]}"
695 # LHS="RHS" && LHS2="RHS2"
696 #
697 # these are only some examples of genuine failure, holder.sh needs fixing.
698 # For the a bad reason, the line is of the form:
699 #
700 # LHS="RHS$(command "")"
701 #
702 # An example of this is command substitution in the assignment RHS, this is
703 # a false positive that must be granted and pass validation.
704 #
705 # Regex RHS validation only allows one pair of unescaped double quotes per
706 # RHS, they are supposed to enclose the whole RHS from start to end, and any
707 # other double quote appearing within this region should be escaped.
708 #
709 # In the latter case above, regex validation detects text after the second
710 # encountered, unescaped, double quote. For instance from an input such as
711 # `"RHS$(command "arg")"`, it will match `arg")"` as illegal.
712 #
713 # Naturally bash doesn't demand an argument passed to a command substitution
714 # to be quoted, but here holder.sh files run with the errexit flag, thus one
715 # command might fail when provided with a null argument. On the other hand,
716 # such command could handle arguments of length 0 but not null, such as `""`,
717 # hence quoting every argument in holder files becomes necessary.
718 #
719 # This whole validation system is artificial, arbitrarily made by the author
720 # to make assumptions over the state of a template. It also aims to give the
721 # developer enough flexibility in writing holder files, for, not preventing
722 # him or her to make use of constructs like command substitution in holder.sh
723 # variables.
724 #
725 # To fix this, extend RHS validation when it fails and process further the
726 # supposedly illegal line. is_valid_eval_rhs() takes the RHS text that has
727 # matched as illegal against $rex_illegal_holder_assignment_rhs and it tries
728 # to establish if this is a false positive by using the built in `set -x`.
729 #
730 # When it gets to this point errexit already passed, so it is fair to assume
731 # this is a bash legal line.
732 (
733 local -i __stack_ops
734 local __stack_lvl
735 local __xline
736
737 while read -r __xline; do
738 if echo $__xline | grep -q -- "__stack_${script_now}"; then
739 __stack_lvl="$(echo $__xline | cut -d' ' -f 1)"
740 __stack_lvl="${__stack_lvl%%__stack_$script_now*}"
741 else
742 [ -z "${__xline%%$__stack_lvl *}" ] && ((__stack_ops++))
743 echo $__xline | grep -q -- "__lhs_${script_now}" && {
744 local __lhs_lvl="${__xline%% __lhs_$script_now*}"
745 [ "$__stack_lvl" = "$__lhs_lvl" ] || return $s_err2
746 }
747 fi
748 done < <(
749 eval "set -x; __stack_${script_now}=0; __lhs_${script_now}=\"$(echo $*)" 2>&1
750 ); wait $!
751
752 [ $__stack_ops -eq 1 ] 2>/dev/null || return $s_err
753 ) && return $s_ok
754
755 case "$?" in
756 $s_err ) lets -l -e "too many commands detected per line" ;;
757 $s_err2 ) lets -l -e "cannot determine commands per line" ;;
758 esac; return $s_err
759}
760
761# @1 : module name
762# @2 : holder.sh line
763# @desc : This function can only work with bash script that is known to be
764# : valid, as in, it makes assumptions upon valid bash syntex. Make sure
765# : the line given in $2 belongs to a valid bash script.
766function is_compliant_holder_line
767{
768 local modname=$1
769 local line=$2
770
771 case "$line" in
772
773 # Allow conditional statement
774 \if* | \else* | \elif* | \fi* ) return $s_ok ;;
775
776 # Allow blank lines
777 '\n' | '' ) return $s_ok ;;
778
779 # Allow comments
780 '#'* ) return $s_ok ;;
781 esac
782
783 # From this point, $line can only be an assignment
784 # Assignment validation, part 1: lhs
785 #
786 # $rex_legal_holder_assignment_lhs is a module generic pattern, so make it
787 # specific to this particular module. Also, $modname must be upper case.
788 local rex_legal_lhs="^_${modname^^}$rex_legal_holder_assignment_lhs"
789 local legal_lhs="$(echo "$line" | grep -oE -- $rex_legal_lhs)"
790 if [ -n "$legal_lhs" ]; then
791 local rhs=${line##$legal_lhs}
792
793 # Assignment validation, part 2: rhs
794 #
795 # At this point, according to $rex_illegal_holder_assignment_rhs,
796 # nothing is allowed on this line other than the assignment rhs.
797 local rex_illegal_rhs="$rex_illegal_holder_assignment_rhs"
798 local illegal_rhs="$(echo "$rhs" | grep -oE -- $rex_illegal_rhs)"
799 [ -z "$illegal_rhs" ] && return $s_ok
800
801 is_valid_eval_rhs "$rhs" && {
802 [ "${illegal_rhs: -1}" != '"' ] && \
803 lets -l -w "suspicious: $illegal_rhs"
804 return $s_ok # no error
805 }
806
807 lets -l -e "illegal RHS: $rhs"
808 else
809 lets -l -e "illegal LHS: $line"
810 fi
811
812 # Here is error only
813 return $s_err
814}
815
816# @1 : module name
817# @2 : absolute path to holder file
818function is_compliant_holder
819{
820 local modname=$1
821 local -i nline
822 local line
823
824 # Is it of the desired form?
825 while read -r line; do
826 ((nline++))
827 if ! is_compliant_holder_line $modname "$(echo "$line")"; then
828 lets -l -e "${2##$module_d/}: line $nline is not compliant"
829 return $s_err
830 fi
831 done < $2
832}
833
834# @1 : module name
835function is_compliant_module
836{
837 local -r modname=$1
838 local -r modfile="$module_d/$modname/$module_sh"
839 local -a valid_modname=""
840
841 (
842 read -a valid_modname < <(
843 # $module should never be set at this point
844 ! declare -p -- "module" &>/dev/null || return $s_err2
845
846 local -i err; local stdout
847 stdout="$(set -eu; source $modfile)"; err=$?
848 case "$err" in
849 $s_ok )
850 [ -n "$stdout" ] && {
851 echo "$stdout"; return $s_err3
852 } || { set -eu; source "$modfile" || return $err4; } ;;
853 * ) return $err4 ;;
854 esac
855
856 [ "$module" = "$modname" ] || return $s_err5
857
858 [ -n "$module" ] && [ -d "$module_d/$module" ] || return $s_err6
859
860 declare -p -- "mod_module_d" >/dev/null || return $s_err7
861 declare -p -- "depmod" &>/dev/null || \
862 { set +eu; lets -l -w "$modname doesn't have depmod"; set -eu; }
863
864 is_in_modules $module || return $s_err8
865
866 set +eu; echo "${depmod[@]} $module" && return $s_ok
867 ); wait $!
868 case "$?" in
869 $s_err2 ) lets -l -e "corrupted environment" ;;
870 $s_err3 ) lets -l -e "$modname/$module_sh spams the stdout"
871 lets -l -e "this was found on the stdout: ${valid_modname[*]}" ;;
872 $s_err4 ) lets -l -e "failed to source $modfile" ;;
873 $s_err5 ) lets -l -e "$modfile doesn't define ${modname:-undefined}" ;;
874 $s_err6 ) lets -l -e "${modname:-undefined} doesn't exist" ;;
875 $s_err7 ) lets -l -e "is '$modname' sourcing common.sh" ;;
876 $s_err8 ) lets -l -w "disabled: '$modname'"; return $s_disabled ;;
877 $s_ok ) lets -l -i "enabled: '$modname'"
878 printf "%s\n" "${valid_modname[@]}"; return $s_ok ;;
879 esac; return $s_err
880 ); return $?
881}
882
883# @1 : absolute path to bash script
884function run_bash_errexit
885{
886 { /bin/bash -o errexit $1 ${@:2} 2>/dev/null; } || {
887 lets -l -e "${1##$module_d/}: doesn't even source, errexit"
888 return $s_err
889 }
890}
891
892# @1 : absolute path to holder.sh
893# @desc : For efficiency and for semantic reasons, this function needs
894# : to do multiple things at once, otherwise it would and should
895# : have been broken down. Do:
896# :
897# : 1. validate holder.sh for errexit
898# : 2. validate module.sh for errexit and nounset
899# : 3. validate holder.sh syntax line by line
900# :
901# : Only if this function succeeds, any attempt to bind unbound
902# : variables in holders.sh is really meaningful.
903function __is_valid_module_static
904{
905 local modname="$(get_modname_from_file $1)"
906 local stdout; local -i ret; local -a deps
907
908 # Can holder run?
909 stdout="$(run_bash_errexit $1)"
910 case "$?" in
911 $s_ok ) [ -n "$stdout" ] && {
912 lets -l -w "${1##$module_d/} spams the stdout"
913 } ;;
914 * ) return $s_err ;;
915 esac
916
917 deps=( $(is_compliant_module $modname) ); ret=$?
918 is_compliant_holder $modname $1 || {
919 # If this is disabled maybe that's fine even if broken
920 [ $ret -eq $s_disabled ] && {
921 lets -l -w "$modname broken, can break the dependencies"
922 return $s_off_broken
923 } || return $s_err
924 }
925
926 [ $ret -eq $s_disabled ] && return $s_off_usable || {
927 printf "%s\n" "${deps[@]}"
928 return $ret
929 }
930}
931
932# @ : list of absolute path to holder.sh files
933# @return : error if at least one fails
934function is_valid_module
935{
936 local -a deps
937 local modname
938
939 lets -l -d "checking ${#} module(s): ${@}"
940 for modname in ${@}; do
941 local holder="$module_d/$modname/$holder_sh"
942 deps=$(__is_valid_module_static $holder)
943 case "$?" in
944 $s_off_broken ) [ ${#} -eq 1 ] && return $s_off_broken || continue ;;
945 $s_off_usable ) [ ${#} -eq 1 ] && return $s_off_usable || continue ;;
946 $s_ok ) MODULES+=( $modname )
947 MODULES=( $(__merge_array "${MODULES[@]}" "${deps[@]}") ) ;;
948 $s_err ) return $s_err ;;
949 esac
950 done
951}
952
953# @ : list of legal holder assignments
954function is_valid_env
955{
956 local -r modname=$1
957
958 # Returned count of variables set in the environment
959 #
960 # get_file_extarnal_references() returns the number of variables used in a
961 # scheme.sh and holder.sh as last variable of the list. Such count does not
962 # include itself (this is appended to the list afterwards).
963 local -i count="$2"
964
965 [ -n "${modname}" ] && shift 2 || {
966 lets -l -e "dynamic env not valid, missing modname"
967 return $s_err
968 }
969
970 # keep it simple, we know that's what it is called
971 local -r var="_${modname^^}_${script_now}_COUNT_"
972 local -ar module_env=( "${@}" )
973 (
974 # First, if the subshell is corrupted, bail out
975 unset $var && ! declare -p -- $var 2>/dev/null || return $s_err3
976
977 # Second, if get_valid_modulenv_dynamic() count doesn't match with
978 # the lenght of assignment list passed in, bail out.
979 [ $count -eq ${#module_env[@]} ] || return $s_err4
980
981 # Third, if the count done by get_file_extarnal_references() doesn't
982 # match $count - 1, bail out.
983 eval "${module_env[-1]}"
984 ((count--)); [ ${!var} -eq $count ] || return $s_err5
985 ) || {
986 case "$?" in
987 $s_err3 ) lets -l -e "corrupted environment" ;;
988 $s_err4 ) lets -l -e "list count mismatch: $count vs ${#module_env[@]}" ;;
989 $s_err5 ) lets -l -e "$modname: invalid: ${#module_env[@]} vs ${!var}" ;;
990 esac; return $s_err
991 }
992
993 lets -l -i "'$modname' bound with $(($count-1)) external references"; return $s_ok
994}
995
996# @ : list of module names
997# @return : stdout on success, a valid associative array declaration where every
998# : key is the module name and its value is a list of valid holder
999# : variable assignments. Note, this function take a list of modules, but
1000# : as per current design it is always called provinding only one module.
1001function get_module_dependencies
1002{
1003 local -A modulenv; local modname; local -a env=( )
1004 local table="$1"; shift
1005
1006 lets -l -i "resolving ${#} module(s): ${@}"
1007 for modname in ${@}; do
1008 lets -l -d "working on '$modname'"
1009 local -i psfd; local -i pspid
1010
1011 # Open a read fd, start processing a scheme file
1012 exec {psfd}< <(
1013 get_file_extarnal_references -s $modname
1014 ); pspid=$!
1015
1016 if is_opt "watch" "upins" "upall" "doins" "doall"; then
1017 local assignment; local -i count=0;
1018
1019 while read -u $psfd -r assignment; do
1020
1021 # A little validation of what is coming back from stdout, lhs only
1022 local regex="$rex_legal_dynamic_assignment_lhs.*"
1023 echo "$assignment" | grep -qE -- "$regex" || {
1024 lets -l -e "invalid assignment: $assignment"
1025
1026 # Close fd, kill producer, wait and return an error
1027 exec {psfd}<&-; kill $pspid; wait; return $s_err
1028 }
1029 env+=( "$assignment" ); ((count++))
1030 done
1031
1032 wait $pspid && is_valid_env $modname $count "${env[@]}" || {
1033 lets -l -e "couldn't resolve $modname"
1034 return $s_err
1035 }
1036
1037 elif is_opt "check"; then
1038 wait $pspid; read -u $psfd -a env
1039 [ $? -eq 0 ] || return $s_err
1040 fi
1041
1042 # Collect readings
1043 modulenv[$modname]+="${env[*]}"
1044 exec {psfd}<&-
1045 done
1046
1047 # Once processed all modules
1048 [ -n "$table" ] && {
1049 # return all valid environments and then make it readonly
1050 local def="$(declare -p -- modulenv)"
1051 (echo "${def/declare -A modulenv/declare -rA $table}")
1052 return $s_ok # success
1053 }
1054
1055 lets -l -e "cannot export module environment: no table"
1056 return $s_err # misuse
1057}