Introduce tod - Template Open Deploy
diff --git a/util/util.sh b/util/util.sh
new file mode 100755
index 0000000..6bd5289
--- /dev/null
+++ b/util/util.sh
@@ -0,0 +1,699 @@
+#!/bin/bash
+#
+# util.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.
+
+# Set log line max width
+if [ -n "$COLUMNS" ]; then
+	declare -ir prn_wmax="$COLUMNS"
+elif [ -n "$(tput cols 2>/dev/null)" ]; then
+	declare -ir prn_wmax="$(tput cols)"
+else
+	declare -ir prn_wmax="135" # my size :)
+fi
+
+# Set width ration for description
+if [ "${LOG_D:-0}" -eq "1" ]; then
+	declare -ir prn_wdesc_ratio="35"
+	# Set log description max width
+	declare -ir prn_wdesc="$prn_wmax / 100 * $prn_wdesc_ratio"
+else
+	declare -ir prn_wdesc="0"
+fi
+
+# Set log tags
+declare -Ar prn_tag=( \
+	["W"]="WARNING" ["E"]="ERROR"  ["D"]="DEBUG" \
+	["I"]="INFO"    ["A"]="ASKING" ["X"]="PIPE" \
+)
+
+# Set log tag max width
+declare -ir prn_wtag="$(for t in ${prn_tag[@]}; do \
+	echo -ne $t | wc -m; done | sort -r | head -1)"
+
+# Set log offset for symbols and whitespaces
+declare -ir prn_wofs="8"
+
+# Set log message max width
+declare -ir prn_wmsg="$prn_wmax - $prn_wdesc - $prn_wtag - $prn_wofs"
+
+# Some color
+declare -rA c=( \
+	[LGREEN]="\033[1;32m" [LGREY]="\033[0;37m" [LCYAN]="\033[96m" \
+	[YELLOW]="\033[1;33m" [LRED]="\033[1;31m" [LBLUE]="\033[1;34m" \
+	[NONE]="\033[0m" \
+)
+
+if which fold &>/dev/null; then
+	function prn_line
+	{
+		echo -en "$1\n" | fold -s -w "$prn_wmsg"
+	}
+else
+	function prn_line
+	{
+		echo -e "$1\n"
+	}
+fi
+
+# @return : stdout, absolute path to the main script, assume it is one level up '..'
+function get_script_dir
+{
+	pushd \
+		"$(dirname "$(readlink -f ${BASH_SOURCE[0]} 2>/dev/null)" 2>/dev/null)" \
+		&>/dev/null && {
+		echo "$(realpath "$(pwd)"/..)"
+		popd &>/dev/null
+	}
+}
+
+# @return : stdout, runtime timestamp at lauching time
+function get_script_now
+{
+	local now="$(date +'%d%m%Y%H%M%S')" && echo "$now"
+}
+
+# @desc   : main routine for handling basic stdout messages
+function prn
+{
+	local nl colr ltag lmsg desc line fdesc ftag fmt_ltag fmt
+
+	fdesc="^"
+	ftag="^"
+
+	# Order here should match the invocation order in log()
+	while [ ${#} -gt 0 ]; do
+		case "$1" in
+			-n) nl="\n"; shift ;;
+			-d) desc="$2"; shift 2 ;;
+			-t) ltag="$2"; shift 2 ;;
+			-c) colr="$2"; shift 2 ;;
+			-x) ftag="$2"; line="$2\n"; shift 2 ;;
+			-l)	line="$(prn_line "$line$2\n")"; shift 2 ;;
+			*) shift ;;
+		esac
+	done
+
+	# Build the format output string
+	fmt_ltag="$colr%*s${c[NONE]}"
+	fmt="%*.*s $fmt_ltag %.*s$nl"
+
+	# Process stdout
+	while read -r lmsg; do
+		printf "$fmt" $prn_wdesc $prn_wdesc $desc \
+			   $prn_wtag ${ltag:0:$prn_wtag} $prn_wmsg "$lmsg"
+
+		# If printing folded lines, remove func and tag
+		desc=$fdesc && ltag=$ftag
+	done < <(cat <<EOF
+$line
+EOF
+			)
+}
+
+# @1      : text to be sent to stdout via prn
+# @return : Success if user enters y, error if n, otherwise it keeps
+#         : asking for input.
+function ask
+{
+	local desc="${FUNCNAME[2]}:${BASH_LINENO[1]}"
+	local ans=""
+
+	[ "$answer" = 'y' ] && return $s_ok
+	[ "$answer" = 'n' ] && return $s_err
+
+	prn -d $desc -t ${prn_tag[A]} -c ${c[LBLUE]} -l "${*} [y/n]" 1>&2
+	while [ "$ans" != 'y' ] && [ "$ans" != 'n' ]; do read -s -n 1 ans; done
+
+	printf " $ans\n" 1>&2; [ "$ans" = "y" ] && return $s_ok || return $s_err
+}
+
+# @       : list of options and arguments (-x, -w, -d, -e, -i and --phew). For
+#         : -d, debug and -i, info, --phew overrides the color to green.
+# @desc   : main routine for logging facilities. Note, all logs are redirected
+#         : to stderr and logging error (-e) returns an error (return 1) 
+function log
+{
+	local -i do_print=0
+	local -a msg=( )
+	local desc=""
+	local ltag=""
+	local colr=""
+	local xlog=""
+
+	[ $LOG_D -eq 1 ] && desc="-d ${FUNCNAME[2]}:${BASH_LINENO[1]}" || desc="-d ''"
+
+	case "${1}" in
+		-x) [ $LOG_X -eq 1 ] && {
+				shift
+				[ -n "$1" ] && xlog="-x $1"
+				shift
+				colr="-c ${c[LCYAN]}"
+				do_print=1
+				msg=( "${*:-$(</dev/stdin)}" )
+				ltag=${prn_tag[X]}
+			} ;;
+		-w) [ $LOG_W -eq 1 ] && {
+				shift
+				colr="-c ${c[YELLOW]}"
+				do_print=1
+				msg=( "${*}" )
+				ltag=${prn_tag[W]}
+			} ;;
+		-d) [ $LOG_D -eq 1 ] && {
+				shift
+				[ "$1" = "--phew" ] && {
+					shift
+					colr="-c ${c[LGREEN]}"
+				} || colr="-c ${c[LGREY]}"
+				do_print=1
+				msg=( "${*}" )
+				ltag=${prn_tag[D]}
+			} ;;
+		-i) [ $LOG_I -eq 1 ] && {
+				shift
+				[ "$1" = "--phew" ] && {
+					shift
+					colr="-c ${c[LGREEN]}"
+				} || colr="-c ${c[NONE]}"
+				do_print=1
+				msg=( "${*}" )
+				ltag=${prn_tag[I]}
+			} ;;
+		-e) # Always print errors
+			shift
+			prn -n $desc -t ${prn_tag[E]} -c ${c[LRED]} -l "${*}" 1>&2
+			return $s_err ;;
+	esac
+
+	[ $do_print -eq 1 ] && {
+		prn -n $desc -t $ltag $colr $xlog -l "${msg[*]}" 1>&2
+	}; return $s_ok
+}
+
+function is_set
+{
+	[ "$(set -o | grep $1 | grep -oE "(on|off)")" = "on" ]
+}
+
+# desc    : util handler
+function lets
+{
+	is_set "xtrace" && local xtrace="set -x" && set +x
+	local -i ret
+
+	case "$1" in
+		-l|--log) shift && log ${@}; ret=$? ;;
+		-a|--ask) shift && ask ${@}; ret=$? ;;
+		-d|--die) shift && die ${@}; ret=$? ;;
+		*) eval ${xtrace}; return $s_err ;;
+	esac
+
+	eval ${xtrace}; return $ret
+}
+
+# @1      : string compliant with unix file system node addressing
+# @desc   : using ${*} to get it to fail if passed in with multiple words
+function is_unix_path
+{
+	[ ! -z "${*}" ] && pathchk -P -- "${*}" &>/dev/null
+}
+
+# @1      : absolute path to file
+# @2      : absolute path to directory
+# @return : success if @2 exists and @1 is a valid unix path and @1 is a node in @2,
+#         : error otherwise.
+# @desc   : useful when @1 is a prospective file, but not a file just yet
+function is_in_tree
+{
+	[ -d "${2}" ] && is_unix_path "${1}" || return 1
+
+	case "${1}" in "${2}"*) return 0 ;; *) return 1 ;; esac
+}
+
+# @1      : absolute path to a prospective file
+# @return : success if the named file in @1 is or will be in the $instance_d tree
+function is_in_tree_instance_d
+{
+	is_in_tree "${1}" "${instance_d}"
+}
+
+# @1      : file mode permission
+# @2      : file owner and group mode permission
+# @3      : (optional) recursive flag -R
+# @       : list of files and/or directories
+# @return : Success if it could chmod and chown a valid file
+function set_mode
+{
+	local mode=$1; shift
+	local own=$1; shift
+	local t
+
+	if [ "$1" = "-R" ]; then local flags=$1; shift; fi
+
+	for t in ${@}; do
+		if [ -f "$t" ] || [ -d "$t" ]; then
+			sudo chmod $flags $mode "$t" && sudo chown $own:$own "$t" || return 1
+		else
+			lets -l -e "invalid: $t"
+		fi
+	done
+}
+
+# @1      : list of packages
+function util_install_dependency
+{
+	local pkg
+
+	for pkg in ${@}; do
+		sudo apt-get install -y --fix-missing $pkg 2>&1 | lets -l -x "apt-get"
+		[ ${PIPESTATUS[0]} -eq 0 ] || {
+			lets -l -e "failed to install $pkg"; return 1
+		}
+	done
+}
+
+# @1      : absolute path
+# @desc   : Validate a prospective directory path
+function is_valid_new_dir
+{
+	is_unix_path "${*}" && [ ! -e "${*}" ]
+}
+
+# @desc   : remove trailing and leading whitespaces
+function get_wtrim
+{
+	echo "${*}"
+}
+
+# @1      : absolute path to a generic file
+# @return : basename without extension
+# @desc   : convert an absolute path to a lhs suitable variable name. This
+#         : means stripping anything after the dot
+function get_stripname
+{
+	local stripped
+
+	stripped="$(basename $1)"; stripped="${stripped%%.*}"
+	echo "$stripped"
+}
+
+# @1      : array variable name
+# @desc   : check if the vriable named in @1 is set and is an array
+function is_array_set
+{
+	declare -p -- "${*}" | grep -q "declare \-a"
+}
+
+# @1      : function name
+# @desc   : check if the vriable named in @1 is set
+function is_function_set
+{
+	declare -F "${*}" &>/dev/null
+}
+
+function is_system_ready
+{
+	case "$BASH_VERSION" in
+		''|[123].*|4.[0123]) echo "ERROR: Bash 4.4 required" >&2; return 1 ;;
+	esac
+}
+
+# @1      : asoblute path to output file
+# @2      : fd variable name reference
+# @3      : char representing file mode operation - r,w,rw
+# @desc   : in preparation to add support for dedicated fds for logs and output
+function fdopen
+{
+	local -n out_fd=$2
+	local -r file=$1
+
+	[ -z "$out_fd" ] || { lets -l -e "invalid out fd"; return 1; }
+
+	case "$3" in
+		rw|wr) [ -n "$file" ] && [ ! -f "$file" ] && \
+				   is_unix_path $file && exec {fd}<> $file ;;
+		w    ) [ ! -f "$file" ] && \
+				   is_unix_path $file && exec {fd}> $file ;;
+		r    ) [ -r "$file" ] && exec {fd}< $file ;;
+	esac
+
+	# Posix bash dynamic fd allocation is from 9 on
+	[ -f "$file" ] && [ $fd -gt 9 ] && out_fd=$fd && return
+
+	lets -l -e "failed to open fd=$1"
+	return 1
+}
+
+# @1      : fd int
+# @2      : char representing file mode operation - r,w,rw
+# @desc   : in preparation to add support for dedicated fds for logs and output
+function fdclose
+{
+	local -ri fd=$1
+
+	[ $fd -gt 9 ] || { lets -l -e "invalid file fd"; return 1; }
+
+	case "$2" in
+		rw|wr) exec {fd}>&- && { lets -l -d "$fd closed"; return; }
+			   exec {fd}<&- && { lets -l -d "$fd closed"; return; } ;;
+		w    ) exec {fd}>&- && { lets -l -d "$fd closed"; return; } ;;
+		r    ) exec {fd}<&- && { lets -l -d "$fd closed"; return; } ;;
+	esac
+
+	lets -l -e "failed to close fd=$2"
+	return 1
+}
+
+function get_hunk_indexes
+{
+	local begin="$rex_legal_manifest_hook_begin"
+	local ended="$rex_legal_manifest_hook_ended"
+	local -a idx
+
+	idx=( $(grep -n -e "$begin" -e "$ended" $manifest_f | cut -f1 -d':') )
+	local -i mod="${#idx[@]} % 2"
+
+	[ $mod -eq 0 ] && echo ${idx[@]} || lets -l -e "invalid $manifest_f"
+}
+
+function get_hunk_pair
+{
+	local -i head; local -i tail; local -a pair
+	local -i from=$1; shift
+
+	pair=( ${@:$from:2} )
+	[ ${#pair[@]} -eq 2 ] || { [ ${#pair[@]} -eq 0 ] && return $s_ok; } || \
+		return $s_err
+
+	tail=${pair[0]}; head=${pair[1]};
+	tail="$head - $tail + 1"
+
+	echo "$head $tail"
+}
+
+function get_hunk
+{
+	cat $manifest_f | head -n $1 | tail -n $2
+}
+
+# @1      : remote address
+# @2      : branch name
+# @3      : mirror name
+function get_new_sha_upstream
+{
+	local remote_sha
+
+	remote_sha="$(git ls-remote $1 $2 | head -1 | cut -f 1)"
+
+	if [ -d "$3" ]; then
+		[ -f "$3/$__version" ] || {
+			lets -l -e "${3:-uknown mirror} not versioned"
+			return $s_err2
+		}
+	else
+		mkdir -p "$3" || { lets -l -e "mkdir failed: $3"; return $s_err2; }
+		echo $remote_sha; return $s_ok
+	fi
+
+	[ "$(< "$3/$__version")" != "$remote_sha" ] && {
+		lets -l -d "$3: $remote_sha"
+		rm -rf $3; echo $remote_sha; return $s_ok
+	} || {
+		lets -l -i "$3 is up to date"; 
+	}
+
+	return $s_err
+}
+
+function __git_archive
+{
+	local mirror="$1"
+	local remote="$4"
+	local staging_d="$(dirname $2)"
+	local flags="--prefix=$mirror/" # need trailing slash
+	flags+=" --output=$2"
+	flags+=" --format=$3"
+	flags+=" --remote=$remote"
+#	flags+=" --verbose"
+	local branch="$5"
+	local remote_sha
+
+	remote_sha="$(get_new_sha_upstream $remote $branch $staging_d/$mirror)"
+	case "$?" in
+		$s_err2 ) return $s_err ;; # no new sha, an error occurred
+		$s_err ) return $s_ok ;; # no new sha, no need to update
+	esac
+
+	git archive $flags $branch 2>&1 | lets -l -x "git"
+
+	[ ${PIPESTATUS[0]} -eq $s_ok ] && {
+		tar -xf $2 -C "${2%/*}" 2>&1 | lets -l -x "tar"
+		[ ${PIPESTATUS[0]} -eq $s_ok ] && rm -rf $2 || {
+				lets -l -w "tar failed: $2. Clean up needed"
+				return $s_err
+			}
+		echo $remote_sha > "${2%/*}/$mirror/$__version"
+	}
+
+	return ${PIPESTATUS[0]}
+}
+
+function __git_clone
+{
+	local mirror_d="$3"
+	local branch="$1"
+	local remote="$2"
+	local flags="--single-branch"
+	flags+=" --depth=1"
+	flags+=" --branch=$branch"
+	flags+=" --verbose"
+	local remote_sha
+
+	remote_sha="$(get_new_sha_upstream $remote $branch $mirror_d)"
+	case "$?" in
+		$s_err2 ) return $s_err ;; # no new sha, an error occurred
+		$s_err ) return $s_ok ;; # no new sha, no need to update
+	esac
+
+	git clone $flags $remote $3 2>&1 | lets -l -x "git"
+	[ ${PIPESTATUS[0]} -eq $s_ok ] || return $s_err
+
+	echo $remote_sha > "$mirror_d/$__version"
+}
+
+function __wget_file
+{
+	local output="-O $1"
+	local remote="$2"
+	local flags="-nv"
+
+	if [ ! -f "$1" ]; then
+		wget $flags $output $remote 2>&1 | lets -l -x "wget"
+		[ ${PIPESTATUS[0]} -eq 0 ] || {
+			lets -l -d "failed wget $(basename $1)"
+			return $s_err
+		}
+	else
+		lets -l -i "$1 up to date"
+	fi;	[ -f "$1" ]	
+}
+
+# @1      : module name
+# @2      : output dir
+function process_manifest
+{
+	local -i from=1 # $@ is 1 indexed
+	local -a hunks
+	local -a pair
+
+	[ -z "$1" ] && { lets -l -e "invalid module name"; return $s_err; }
+
+	hunks=( $(get_hunk_indexes) )
+	case "$?" in $s_err ) return $s_err ;; esac
+
+	pair=( $(get_hunk_pair $from ${hunks[@]}) )
+	case "$?" in $s_err ) return $s_err ;; esac
+
+	while [ -n "$pair" ]; do
+		(
+			# Clean up manifest fields before sourcing
+			modname=""; mirror=""; protocol=""; branch=""; host=""; user="";
+			method=""; format=""; file=""
+
+			set -e
+			source <(get_hunk ${pair[@]}) && set +e || { set +e; return $s_err3; }
+
+			[ "$1" = "$modname" ] || return $s_ok # no error, try again
+
+			case "$method" in
+				git-archive )
+					[ -n "$user" ] && user="$user@"
+					[ -n "$protocol" ] && protocol="$protocol://"
+					[ -n "$format" ] || return $s_err4
+
+					local remote="${protocol:-}${user:-}$host/$mirror"
+					local tarball="$2/$mirror.$format"
+
+					__git_archive \
+						$mirror $tarball $format $remote $branch || return $s_err5 ;;
+				git-clone )
+					[ -n "$user" ] && user="$user@"
+					[ -n "$protocol" ] && protocol="$protocol://"
+
+					local remote="${protocol:-}${user:-}$host/$mirror"
+
+					__git_clone $branch $remote $2/$mirror || return $s_err6 ;;
+				wget-file )
+					[ -n "$file" ] || return $s_err7
+					[ -n "$format" ] && file="$file.$format"
+
+					__wget_file $2/$file $host/$file || return $s_err8 ;;
+				*) return $s_err9 ;;
+			esac
+		) || {
+			case "$?" in
+				$s_err3 ) lets -l -e "$1: failed to source manifest" ;;
+				$s_err4 ) lets -l -e "$1: manifest missing format"   ;;
+				$s_err5 ) lets -l -e "$1: failed git archive"        ;;
+				$s_err6 ) lets -l -e "$1: failed git clone"          ;;
+				$s_err7 ) lets -l -e "$1: manifest missing file"     ;;
+				$s_err8 ) lets -l -e "$1: failed wget file"          ;;
+				$s_err9 ) lets -l -e "$1: invalid manifest method"   ;;
+			esac; return $s_err
+		}
+
+		from="$from + 2"; pair=( $(get_hunk_pair $from ${hunks[@]}) )
+		[ $? -eq 0 ] || return $s_err
+	done; return $s_ok
+}
+
+function get_all_modules
+{
+	ls -Ad $module_d/*/ | xargs -n1 basename
+}
+
+function get_modules_optarg
+{
+	local modname; local -a mods=( ); local -a imods=( )
+
+	if [ -z "${*}" ]; then
+		imods=( $(get_all_modules) )
+	else
+		imods=( "${@}" )
+	fi
+
+	for modname in ${imods[@]}; do
+		[ -n "$modname" ] && [ -d "$module_d/$modname" ] || {
+			lets -l -w "$modname not found"
+			continue
+		}
+
+		__is_in_array "$modname" "${BLMODULES[@]}" && [ $modall -eq 0 ] && {
+			lets -l -d "$modname is blacklisted, set MODALL=1 to force"
+			continue
+		}
+
+		mods+=( $modname )
+	done
+
+	[ -n "${mods[*]}" ] && echo "${mods[@]}"
+}
+
+function is_opt
+{
+	local k
+
+	for k in ${!OPTIONS[@]}; do
+		[ ${OPTIONS[$k]} -eq 1 ] && \
+			case "${*}" in *"$k"* ) return $s_ok ;; esac
+	done
+
+	return $s_err
+}
+
+function get_opt
+{
+	local k
+
+	for k in ${!OPTIONS[@]}; do
+		[ ${OPTIONS[$k]} -eq 1 ] && echo $k
+	done
+}
+
+function __is_in_array
+{
+	printf "%s\n" "${@:2}" | grep -qFx -- "$1"
+}
+
+# @       : list of items to be merged
+# @desc   : very inefficient way for ensuring uniqueness without sorting
+function __merge_array
+{
+	cat -n <(printf "%s\n" "${@}") | sort -t$'\t' -k2,2 -u | \
+		sort -t$'\t' -k1,1 | cut -d$'\t' -f 2
+}
+
+function __append_item
+{
+	# emulating `+=` which adds items to the end of the list
+	local -ar __tmp_a=( "${@:2}" "$1" )
+	__merge_array "${__tmp_a[@]}"
+}
+
+function is_in_options
+{
+	__is_in_array "$1" ${!OPTIONS[@]}
+}
+
+function is_in_modules
+{
+	__is_in_array "$1" ${MODULES[@]}
+}
+
+function are_in_modules
+{
+	local i
+	for i in "${@}"; do __is_in_array "$i" "${MODULES[@]}" || return $s_err; done 
+}
+
+function is_in_optarg
+{
+	__is_in_array "$1" ${OPTARG[@]}
+}
+
+function module_enable
+{
+	MODULES=( $(__append_item "$1" "${MODULES[@]}") )
+}
+
+function getflag
+{
+	local -ar flags=( ${2//:/ } )
+
+	__is_in_array "$1" ${flags[@]} && echo "true" || echo "false"
+}
+
+function setflag
+{
+	echo "${*// /:}"
+}