blob: 6bd5289e5ef217f583011b3c8bbd9162d74b522b [file] [log] [blame]
Luigi Santivetti0fdd4702020-06-22 19:00:32 +01001#!/bin/bash
2#
3# util.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# Set log line max width
26if [ -n "$COLUMNS" ]; then
27 declare -ir prn_wmax="$COLUMNS"
28elif [ -n "$(tput cols 2>/dev/null)" ]; then
29 declare -ir prn_wmax="$(tput cols)"
30else
31 declare -ir prn_wmax="135" # my size :)
32fi
33
34# Set width ration for description
35if [ "${LOG_D:-0}" -eq "1" ]; then
36 declare -ir prn_wdesc_ratio="35"
37 # Set log description max width
38 declare -ir prn_wdesc="$prn_wmax / 100 * $prn_wdesc_ratio"
39else
40 declare -ir prn_wdesc="0"
41fi
42
43# Set log tags
44declare -Ar prn_tag=( \
45 ["W"]="WARNING" ["E"]="ERROR" ["D"]="DEBUG" \
46 ["I"]="INFO" ["A"]="ASKING" ["X"]="PIPE" \
47)
48
49# Set log tag max width
50declare -ir prn_wtag="$(for t in ${prn_tag[@]}; do \
51 echo -ne $t | wc -m; done | sort -r | head -1)"
52
53# Set log offset for symbols and whitespaces
54declare -ir prn_wofs="8"
55
56# Set log message max width
57declare -ir prn_wmsg="$prn_wmax - $prn_wdesc - $prn_wtag - $prn_wofs"
58
59# Some color
60declare -rA c=( \
61 [LGREEN]="\033[1;32m" [LGREY]="\033[0;37m" [LCYAN]="\033[96m" \
62 [YELLOW]="\033[1;33m" [LRED]="\033[1;31m" [LBLUE]="\033[1;34m" \
63 [NONE]="\033[0m" \
64)
65
66if which fold &>/dev/null; then
67 function prn_line
68 {
69 echo -en "$1\n" | fold -s -w "$prn_wmsg"
70 }
71else
72 function prn_line
73 {
74 echo -e "$1\n"
75 }
76fi
77
78# @return : stdout, absolute path to the main script, assume it is one level up '..'
79function get_script_dir
80{
81 pushd \
82 "$(dirname "$(readlink -f ${BASH_SOURCE[0]} 2>/dev/null)" 2>/dev/null)" \
83 &>/dev/null && {
84 echo "$(realpath "$(pwd)"/..)"
85 popd &>/dev/null
86 }
87}
88
89# @return : stdout, runtime timestamp at lauching time
90function get_script_now
91{
92 local now="$(date +'%d%m%Y%H%M%S')" && echo "$now"
93}
94
95# @desc : main routine for handling basic stdout messages
96function prn
97{
98 local nl colr ltag lmsg desc line fdesc ftag fmt_ltag fmt
99
100 fdesc="^"
101 ftag="^"
102
103 # Order here should match the invocation order in log()
104 while [ ${#} -gt 0 ]; do
105 case "$1" in
106 -n) nl="\n"; shift ;;
107 -d) desc="$2"; shift 2 ;;
108 -t) ltag="$2"; shift 2 ;;
109 -c) colr="$2"; shift 2 ;;
110 -x) ftag="$2"; line="$2\n"; shift 2 ;;
111 -l) line="$(prn_line "$line$2\n")"; shift 2 ;;
112 *) shift ;;
113 esac
114 done
115
116 # Build the format output string
117 fmt_ltag="$colr%*s${c[NONE]}"
118 fmt="%*.*s $fmt_ltag %.*s$nl"
119
120 # Process stdout
121 while read -r lmsg; do
122 printf "$fmt" $prn_wdesc $prn_wdesc $desc \
123 $prn_wtag ${ltag:0:$prn_wtag} $prn_wmsg "$lmsg"
124
125 # If printing folded lines, remove func and tag
126 desc=$fdesc && ltag=$ftag
127 done < <(cat <<EOF
128$line
129EOF
130 )
131}
132
133# @1 : text to be sent to stdout via prn
134# @return : Success if user enters y, error if n, otherwise it keeps
135# : asking for input.
136function ask
137{
138 local desc="${FUNCNAME[2]}:${BASH_LINENO[1]}"
139 local ans=""
140
141 [ "$answer" = 'y' ] && return $s_ok
142 [ "$answer" = 'n' ] && return $s_err
143
144 prn -d $desc -t ${prn_tag[A]} -c ${c[LBLUE]} -l "${*} [y/n]" 1>&2
145 while [ "$ans" != 'y' ] && [ "$ans" != 'n' ]; do read -s -n 1 ans; done
146
147 printf " $ans\n" 1>&2; [ "$ans" = "y" ] && return $s_ok || return $s_err
148}
149
150# @ : list of options and arguments (-x, -w, -d, -e, -i and --phew). For
151# : -d, debug and -i, info, --phew overrides the color to green.
152# @desc : main routine for logging facilities. Note, all logs are redirected
153# : to stderr and logging error (-e) returns an error (return 1)
154function log
155{
156 local -i do_print=0
157 local -a msg=( )
158 local desc=""
159 local ltag=""
160 local colr=""
161 local xlog=""
162
163 [ $LOG_D -eq 1 ] && desc="-d ${FUNCNAME[2]}:${BASH_LINENO[1]}" || desc="-d ''"
164
165 case "${1}" in
166 -x) [ $LOG_X -eq 1 ] && {
167 shift
168 [ -n "$1" ] && xlog="-x $1"
169 shift
170 colr="-c ${c[LCYAN]}"
171 do_print=1
172 msg=( "${*:-$(</dev/stdin)}" )
173 ltag=${prn_tag[X]}
174 } ;;
175 -w) [ $LOG_W -eq 1 ] && {
176 shift
177 colr="-c ${c[YELLOW]}"
178 do_print=1
179 msg=( "${*}" )
180 ltag=${prn_tag[W]}
181 } ;;
182 -d) [ $LOG_D -eq 1 ] && {
183 shift
184 [ "$1" = "--phew" ] && {
185 shift
186 colr="-c ${c[LGREEN]}"
187 } || colr="-c ${c[LGREY]}"
188 do_print=1
189 msg=( "${*}" )
190 ltag=${prn_tag[D]}
191 } ;;
192 -i) [ $LOG_I -eq 1 ] && {
193 shift
194 [ "$1" = "--phew" ] && {
195 shift
196 colr="-c ${c[LGREEN]}"
197 } || colr="-c ${c[NONE]}"
198 do_print=1
199 msg=( "${*}" )
200 ltag=${prn_tag[I]}
201 } ;;
202 -e) # Always print errors
203 shift
204 prn -n $desc -t ${prn_tag[E]} -c ${c[LRED]} -l "${*}" 1>&2
205 return $s_err ;;
206 esac
207
208 [ $do_print -eq 1 ] && {
209 prn -n $desc -t $ltag $colr $xlog -l "${msg[*]}" 1>&2
210 }; return $s_ok
211}
212
213function is_set
214{
215 [ "$(set -o | grep $1 | grep -oE "(on|off)")" = "on" ]
216}
217
218# desc : util handler
219function lets
220{
221 is_set "xtrace" && local xtrace="set -x" && set +x
222 local -i ret
223
224 case "$1" in
225 -l|--log) shift && log ${@}; ret=$? ;;
226 -a|--ask) shift && ask ${@}; ret=$? ;;
227 -d|--die) shift && die ${@}; ret=$? ;;
228 *) eval ${xtrace}; return $s_err ;;
229 esac
230
231 eval ${xtrace}; return $ret
232}
233
234# @1 : string compliant with unix file system node addressing
235# @desc : using ${*} to get it to fail if passed in with multiple words
236function is_unix_path
237{
238 [ ! -z "${*}" ] && pathchk -P -- "${*}" &>/dev/null
239}
240
241# @1 : absolute path to file
242# @2 : absolute path to directory
243# @return : success if @2 exists and @1 is a valid unix path and @1 is a node in @2,
244# : error otherwise.
245# @desc : useful when @1 is a prospective file, but not a file just yet
246function is_in_tree
247{
248 [ -d "${2}" ] && is_unix_path "${1}" || return 1
249
250 case "${1}" in "${2}"*) return 0 ;; *) return 1 ;; esac
251}
252
253# @1 : absolute path to a prospective file
254# @return : success if the named file in @1 is or will be in the $instance_d tree
255function is_in_tree_instance_d
256{
257 is_in_tree "${1}" "${instance_d}"
258}
259
260# @1 : file mode permission
261# @2 : file owner and group mode permission
262# @3 : (optional) recursive flag -R
263# @ : list of files and/or directories
264# @return : Success if it could chmod and chown a valid file
265function set_mode
266{
267 local mode=$1; shift
268 local own=$1; shift
269 local t
270
271 if [ "$1" = "-R" ]; then local flags=$1; shift; fi
272
273 for t in ${@}; do
274 if [ -f "$t" ] || [ -d "$t" ]; then
275 sudo chmod $flags $mode "$t" && sudo chown $own:$own "$t" || return 1
276 else
277 lets -l -e "invalid: $t"
278 fi
279 done
280}
281
282# @1 : list of packages
283function util_install_dependency
284{
285 local pkg
286
287 for pkg in ${@}; do
288 sudo apt-get install -y --fix-missing $pkg 2>&1 | lets -l -x "apt-get"
289 [ ${PIPESTATUS[0]} -eq 0 ] || {
290 lets -l -e "failed to install $pkg"; return 1
291 }
292 done
293}
294
295# @1 : absolute path
296# @desc : Validate a prospective directory path
297function is_valid_new_dir
298{
299 is_unix_path "${*}" && [ ! -e "${*}" ]
300}
301
302# @desc : remove trailing and leading whitespaces
303function get_wtrim
304{
305 echo "${*}"
306}
307
308# @1 : absolute path to a generic file
309# @return : basename without extension
310# @desc : convert an absolute path to a lhs suitable variable name. This
311# : means stripping anything after the dot
312function get_stripname
313{
314 local stripped
315
316 stripped="$(basename $1)"; stripped="${stripped%%.*}"
317 echo "$stripped"
318}
319
320# @1 : array variable name
321# @desc : check if the vriable named in @1 is set and is an array
322function is_array_set
323{
324 declare -p -- "${*}" | grep -q "declare \-a"
325}
326
327# @1 : function name
328# @desc : check if the vriable named in @1 is set
329function is_function_set
330{
331 declare -F "${*}" &>/dev/null
332}
333
334function is_system_ready
335{
336 case "$BASH_VERSION" in
337 ''|[123].*|4.[0123]) echo "ERROR: Bash 4.4 required" >&2; return 1 ;;
338 esac
339}
340
341# @1 : asoblute path to output file
342# @2 : fd variable name reference
343# @3 : char representing file mode operation - r,w,rw
344# @desc : in preparation to add support for dedicated fds for logs and output
345function fdopen
346{
347 local -n out_fd=$2
348 local -r file=$1
349
350 [ -z "$out_fd" ] || { lets -l -e "invalid out fd"; return 1; }
351
352 case "$3" in
353 rw|wr) [ -n "$file" ] && [ ! -f "$file" ] && \
354 is_unix_path $file && exec {fd}<> $file ;;
355 w ) [ ! -f "$file" ] && \
356 is_unix_path $file && exec {fd}> $file ;;
357 r ) [ -r "$file" ] && exec {fd}< $file ;;
358 esac
359
360 # Posix bash dynamic fd allocation is from 9 on
361 [ -f "$file" ] && [ $fd -gt 9 ] && out_fd=$fd && return
362
363 lets -l -e "failed to open fd=$1"
364 return 1
365}
366
367# @1 : fd int
368# @2 : char representing file mode operation - r,w,rw
369# @desc : in preparation to add support for dedicated fds for logs and output
370function fdclose
371{
372 local -ri fd=$1
373
374 [ $fd -gt 9 ] || { lets -l -e "invalid file fd"; return 1; }
375
376 case "$2" in
377 rw|wr) exec {fd}>&- && { lets -l -d "$fd closed"; return; }
378 exec {fd}<&- && { lets -l -d "$fd closed"; return; } ;;
379 w ) exec {fd}>&- && { lets -l -d "$fd closed"; return; } ;;
380 r ) exec {fd}<&- && { lets -l -d "$fd closed"; return; } ;;
381 esac
382
383 lets -l -e "failed to close fd=$2"
384 return 1
385}
386
387function get_hunk_indexes
388{
389 local begin="$rex_legal_manifest_hook_begin"
390 local ended="$rex_legal_manifest_hook_ended"
391 local -a idx
392
393 idx=( $(grep -n -e "$begin" -e "$ended" $manifest_f | cut -f1 -d':') )
394 local -i mod="${#idx[@]} % 2"
395
396 [ $mod -eq 0 ] && echo ${idx[@]} || lets -l -e "invalid $manifest_f"
397}
398
399function get_hunk_pair
400{
401 local -i head; local -i tail; local -a pair
402 local -i from=$1; shift
403
404 pair=( ${@:$from:2} )
405 [ ${#pair[@]} -eq 2 ] || { [ ${#pair[@]} -eq 0 ] && return $s_ok; } || \
406 return $s_err
407
408 tail=${pair[0]}; head=${pair[1]};
409 tail="$head - $tail + 1"
410
411 echo "$head $tail"
412}
413
414function get_hunk
415{
416 cat $manifest_f | head -n $1 | tail -n $2
417}
418
419# @1 : remote address
420# @2 : branch name
421# @3 : mirror name
422function get_new_sha_upstream
423{
424 local remote_sha
425
426 remote_sha="$(git ls-remote $1 $2 | head -1 | cut -f 1)"
427
428 if [ -d "$3" ]; then
429 [ -f "$3/$__version" ] || {
430 lets -l -e "${3:-uknown mirror} not versioned"
431 return $s_err2
432 }
433 else
434 mkdir -p "$3" || { lets -l -e "mkdir failed: $3"; return $s_err2; }
435 echo $remote_sha; return $s_ok
436 fi
437
438 [ "$(< "$3/$__version")" != "$remote_sha" ] && {
439 lets -l -d "$3: $remote_sha"
440 rm -rf $3; echo $remote_sha; return $s_ok
441 } || {
442 lets -l -i "$3 is up to date";
443 }
444
445 return $s_err
446}
447
448function __git_archive
449{
450 local mirror="$1"
451 local remote="$4"
452 local staging_d="$(dirname $2)"
453 local flags="--prefix=$mirror/" # need trailing slash
454 flags+=" --output=$2"
455 flags+=" --format=$3"
456 flags+=" --remote=$remote"
457# flags+=" --verbose"
458 local branch="$5"
459 local remote_sha
460
461 remote_sha="$(get_new_sha_upstream $remote $branch $staging_d/$mirror)"
462 case "$?" in
463 $s_err2 ) return $s_err ;; # no new sha, an error occurred
464 $s_err ) return $s_ok ;; # no new sha, no need to update
465 esac
466
467 git archive $flags $branch 2>&1 | lets -l -x "git"
468
469 [ ${PIPESTATUS[0]} -eq $s_ok ] && {
470 tar -xf $2 -C "${2%/*}" 2>&1 | lets -l -x "tar"
471 [ ${PIPESTATUS[0]} -eq $s_ok ] && rm -rf $2 || {
472 lets -l -w "tar failed: $2. Clean up needed"
473 return $s_err
474 }
475 echo $remote_sha > "${2%/*}/$mirror/$__version"
476 }
477
478 return ${PIPESTATUS[0]}
479}
480
481function __git_clone
482{
483 local mirror_d="$3"
484 local branch="$1"
485 local remote="$2"
486 local flags="--single-branch"
487 flags+=" --depth=1"
488 flags+=" --branch=$branch"
489 flags+=" --verbose"
490 local remote_sha
491
492 remote_sha="$(get_new_sha_upstream $remote $branch $mirror_d)"
493 case "$?" in
494 $s_err2 ) return $s_err ;; # no new sha, an error occurred
495 $s_err ) return $s_ok ;; # no new sha, no need to update
496 esac
497
498 git clone $flags $remote $3 2>&1 | lets -l -x "git"
499 [ ${PIPESTATUS[0]} -eq $s_ok ] || return $s_err
500
501 echo $remote_sha > "$mirror_d/$__version"
502}
503
504function __wget_file
505{
506 local output="-O $1"
507 local remote="$2"
508 local flags="-nv"
509
510 if [ ! -f "$1" ]; then
511 wget $flags $output $remote 2>&1 | lets -l -x "wget"
512 [ ${PIPESTATUS[0]} -eq 0 ] || {
513 lets -l -d "failed wget $(basename $1)"
514 return $s_err
515 }
516 else
517 lets -l -i "$1 up to date"
518 fi; [ -f "$1" ]
519}
520
521# @1 : module name
522# @2 : output dir
523function process_manifest
524{
525 local -i from=1 # $@ is 1 indexed
526 local -a hunks
527 local -a pair
528
529 [ -z "$1" ] && { lets -l -e "invalid module name"; return $s_err; }
530
531 hunks=( $(get_hunk_indexes) )
532 case "$?" in $s_err ) return $s_err ;; esac
533
534 pair=( $(get_hunk_pair $from ${hunks[@]}) )
535 case "$?" in $s_err ) return $s_err ;; esac
536
537 while [ -n "$pair" ]; do
538 (
539 # Clean up manifest fields before sourcing
540 modname=""; mirror=""; protocol=""; branch=""; host=""; user="";
541 method=""; format=""; file=""
542
543 set -e
544 source <(get_hunk ${pair[@]}) && set +e || { set +e; return $s_err3; }
545
546 [ "$1" = "$modname" ] || return $s_ok # no error, try again
547
548 case "$method" in
549 git-archive )
550 [ -n "$user" ] && user="$user@"
551 [ -n "$protocol" ] && protocol="$protocol://"
552 [ -n "$format" ] || return $s_err4
553
554 local remote="${protocol:-}${user:-}$host/$mirror"
555 local tarball="$2/$mirror.$format"
556
557 __git_archive \
558 $mirror $tarball $format $remote $branch || return $s_err5 ;;
559 git-clone )
560 [ -n "$user" ] && user="$user@"
561 [ -n "$protocol" ] && protocol="$protocol://"
562
563 local remote="${protocol:-}${user:-}$host/$mirror"
564
565 __git_clone $branch $remote $2/$mirror || return $s_err6 ;;
566 wget-file )
567 [ -n "$file" ] || return $s_err7
568 [ -n "$format" ] && file="$file.$format"
569
570 __wget_file $2/$file $host/$file || return $s_err8 ;;
571 *) return $s_err9 ;;
572 esac
573 ) || {
574 case "$?" in
575 $s_err3 ) lets -l -e "$1: failed to source manifest" ;;
576 $s_err4 ) lets -l -e "$1: manifest missing format" ;;
577 $s_err5 ) lets -l -e "$1: failed git archive" ;;
578 $s_err6 ) lets -l -e "$1: failed git clone" ;;
579 $s_err7 ) lets -l -e "$1: manifest missing file" ;;
580 $s_err8 ) lets -l -e "$1: failed wget file" ;;
581 $s_err9 ) lets -l -e "$1: invalid manifest method" ;;
582 esac; return $s_err
583 }
584
585 from="$from + 2"; pair=( $(get_hunk_pair $from ${hunks[@]}) )
586 [ $? -eq 0 ] || return $s_err
587 done; return $s_ok
588}
589
590function get_all_modules
591{
592 ls -Ad $module_d/*/ | xargs -n1 basename
593}
594
595function get_modules_optarg
596{
597 local modname; local -a mods=( ); local -a imods=( )
598
599 if [ -z "${*}" ]; then
600 imods=( $(get_all_modules) )
601 else
602 imods=( "${@}" )
603 fi
604
605 for modname in ${imods[@]}; do
606 [ -n "$modname" ] && [ -d "$module_d/$modname" ] || {
607 lets -l -w "$modname not found"
608 continue
609 }
610
611 __is_in_array "$modname" "${BLMODULES[@]}" && [ $modall -eq 0 ] && {
612 lets -l -d "$modname is blacklisted, set MODALL=1 to force"
613 continue
614 }
615
616 mods+=( $modname )
617 done
618
619 [ -n "${mods[*]}" ] && echo "${mods[@]}"
620}
621
622function is_opt
623{
624 local k
625
626 for k in ${!OPTIONS[@]}; do
627 [ ${OPTIONS[$k]} -eq 1 ] && \
628 case "${*}" in *"$k"* ) return $s_ok ;; esac
629 done
630
631 return $s_err
632}
633
634function get_opt
635{
636 local k
637
638 for k in ${!OPTIONS[@]}; do
639 [ ${OPTIONS[$k]} -eq 1 ] && echo $k
640 done
641}
642
643function __is_in_array
644{
645 printf "%s\n" "${@:2}" | grep -qFx -- "$1"
646}
647
648# @ : list of items to be merged
649# @desc : very inefficient way for ensuring uniqueness without sorting
650function __merge_array
651{
652 cat -n <(printf "%s\n" "${@}") | sort -t$'\t' -k2,2 -u | \
653 sort -t$'\t' -k1,1 | cut -d$'\t' -f 2
654}
655
656function __append_item
657{
658 # emulating `+=` which adds items to the end of the list
659 local -ar __tmp_a=( "${@:2}" "$1" )
660 __merge_array "${__tmp_a[@]}"
661}
662
663function is_in_options
664{
665 __is_in_array "$1" ${!OPTIONS[@]}
666}
667
668function is_in_modules
669{
670 __is_in_array "$1" ${MODULES[@]}
671}
672
673function are_in_modules
674{
675 local i
676 for i in "${@}"; do __is_in_array "$i" "${MODULES[@]}" || return $s_err; done
677}
678
679function is_in_optarg
680{
681 __is_in_array "$1" ${OPTARG[@]}
682}
683
684function module_enable
685{
686 MODULES=( $(__append_item "$1" "${MODULES[@]}") )
687}
688
689function getflag
690{
691 local -ar flags=( ${2//:/ } )
692
693 __is_in_array "$1" ${flags[@]} && echo "true" || echo "false"
694}
695
696function setflag
697{
698 echo "${*// /:}"
699}