Introduce tod - Template Open Deploy
diff --git a/util/io.sh b/util/io.sh
new file mode 100644
index 0000000..5641c00
--- /dev/null
+++ b/util/io.sh
@@ -0,0 +1,507 @@
+#!/bin/bash
+#
+# io.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      : version file containg git sha
+# @return : stdout, instance version: '${git_sha}@${script_now}'
+function __get_script_ver_file
+{
+	[ "$1" = "-q" ] && local q=1 && shift
+
+	git diff-index --quiet HEAD 2>/dev/null || {
+		 [ -n "$q" ] || lets -l -w "git unstaged changes"
+	}
+
+	[ -f "$instance_d/$__version" ] && {
+		local ver="$(< "$instance_d/$__version")"
+		local now="${ver##*@}"; local sha="${ver%%@*}"
+		[ ${#now} -eq ${#sha} ] && [ ${#now} -eq ${#script_now} ] || {
+			lets -l -e "invalid version"; return $s_err
+		}
+		echo "$ver" && return $s_ok
+	}
+	lets -l -w "invalid version file" && return $s_err
+}
+
+function __get_script_ver_git
+{
+	[ "$1" = "-q" ] && local q=1 && shift
+
+	git diff-index --quiet HEAD 2>/dev/null || {
+		 [ -n "$q" ] || lets -l -w "git unstaged changes"
+	}
+
+	local -i len="${#script_now}"
+	local sha="$(git log --pretty=format:'%H' -n 1)"
+	[ -n "$sha" ] || { lets -l -e "git log failed"; return $s_err; }
+
+	echo "${sha:0:$len}@$script_now"
+}
+
+function get_script_ver
+{
+	case "$1" in
+		--file | -f ) shift; __get_script_ver_file "${@}" ;;
+		--git  | -g ) shift; __get_script_ver_git "${@}" ;;
+	esac
+}
+
+# @1      : absolute path to prospective file
+# @return : file, instance version: '${git_sha}@${script_now}'
+function set_script_ver
+{
+	local -r vfile="$2"
+
+	is_unix_path $vfile && touch $vfile || {
+			lets -l -e "invalid version file"; return 1
+		}
+
+	echo "$1" > "$vfile"
+}
+
+# @1       : text to be sent to stdout via cat
+# @2       : (optional) absolute path to a prospective file in DOCKER_ROOT
+# @return  : success if @2 isn't given or if @2 is valid
+# @desc    : do not write to file if it doesn't live inside $instance_d
+function catvar
+{
+	if [ -z "$2" ]; then
+		cat <<EOF
+${1}
+EOF
+	elif is_in_tree_instance_d $2; then
+		echo > $2; cat > $2 <<EOF
+${1}
+EOF
+	else
+		return $s_err
+	fi
+}
+
+# @1      : text to append
+# @2      : absolute path to a valid file
+# @return : success if @2 is valid
+function appvar
+{
+	[ -f "$2" ] && catvar "$1" >> "$2" || lets -l -e "invalid: $2"
+}
+
+# @1      : (optional) absolute path to a valid file
+# @desc   : echo a formatted header, including the file name if passed in
+function get_header
+{
+	if is_in_tree_instance_d "$1"; then
+		local header="\
+# $head_tag $(basename $1)
+
+$license"
+	else
+		local header="\
+$license"
+	fi
+	echo "$header"
+}
+
+# @desc   : echo a formatted footer
+function get_footer
+{
+	local __ver="$(get_script_ver --file -q)"
+	__ver="${__ver%@*}"
+
+	local footer=\
+"# $foot_tag $__ver@$script_now"
+	echo "$footer"
+}
+
+function __create_instance_d
+{
+	is_valid_new_dir $2 || return $s_err4
+	mkdir -p $2         || return $s_err5
+
+	# do not remove directory - it should never be so
+	rm -f $1            || return $s_err6
+	ln -sf $2 $1        || return $s_err7
+}
+
+# @1      : absolute path to $instance_d
+# @desc   : validate dkrc_out_d acutally is what we expect it to be
+function __is_valid_instance_d_link
+{
+	local ver_id="$(readlink -f $1)"
+	local version_f="$1/$__version"
+	ver_id="${ver_id##*-}"
+	local run_time="${ver_id#*@}"
+
+	test -n "$ver_id"                  || return $s_err3
+	is_unix_path "$1"                  || return $s_err4
+	test -L "$1"                       || return $s_err5
+	test -f "$version_f"               || return $s_err6
+	[ "$ver_id" = "$(< $version_f)"  ] || return $s_err7
+
+	# This is the case calling doins with multiple modules
+	[ "$run_time" = "$script_now"  ] && return $s_err8 || \
+			return $s_ok
+}
+
+# @1      : absolute path to $instance_d
+# @desc   : validate dkrc_out_d acutally is what we expect it to be
+function is_valid_instance_d_link
+{
+	__is_valid_instance_d_link $1
+	case "$?" in
+		$s_err3 ) lets -l -e "instance version id"   ;;
+		$s_err4 ) lets -l -e "instance path"         ;;
+		$s_err5 ) lets -l -e "instance link"         ;;
+		$s_err6 ) lets -l -e "instance version file" ;;
+		$s_err7 ) lets -l -e "signature mismatch"    ;;
+		$s_err8 ) return $s_err2 ;;
+		$s_ok   ) return $s_ok ;;
+	esac; return $s_err
+}
+
+function __is_valid_input
+{
+	test -L "$1" && {
+		is_valid_instance_d_link "$1"
+		case "$?" in
+			$s_err2 ) return $s_err2 ;; # different module, same run
+			$s_err  ) return $s_err  ;; # genuine error
+			$s_ok   )
+				local old_instance="$(basename "$(readlink -f $1)")"
+
+				lets -l -i "'$(basename $1)' points to '$old_instance'"
+				lets -l -i "update this link to '$2'"
+				lets -a "would you like to continue?" && \
+					return $s_ok || return $s_err ;;
+		esac
+	}
+
+	! test -e "$1" || return $s_err3
+}
+
+# @desc   : create a new instance_d, either by updating an existing
+#         : symlink or creating everything from scratch.
+function create_instance_d
+{
+	local ver="$(get_script_ver --git)"
+	local new_instance="$(basename $1)-$ver"
+	local new_instance_d="$(dirname $1)/$new_instance"
+
+	__is_valid_input "$1" "$new_instance"
+	case "$?" in
+		$s_err  ) lets -l -i "goodbye"; return $s_err ;;
+		$s_err2 ) return $s_ok ;; # no error, no further actions
+		$s_err3 ) lets -l -e "$1 busy"; return $s_err ;;
+	esac
+
+	__create_instance_d "$1" "$new_instance_d"
+	case "$?" in
+		$s_err4 ) lets -l -e "instance path"   ; return $s_err ;;
+		$s_err5 ) lets -l -e "instance mkdir"  ; return $s_err ;;
+		$s_err6 ) lets -l -e "instance unlink" ; return $s_err ;;
+		$s_err7 ) lets -l -e "instance link"   ; return $s_err ;;
+	esac
+
+	set_script_ver $ver $instance_d/$__version || return $s_err
+}
+
+# @       : a list of absolute paths to prospective folders
+function create_directory
+{
+	local d
+
+	for d in ${@}; do
+		is_unix_path $d || { lets -l -d "invalid path: $d"; return $s_err; }
+		[ -d "$d" ] || { mkdir -p $d && lets -l -d "mkdir $d"; }
+	done
+}
+
+# @1      : (optional) option --dry
+# @2      : file text content
+# @3      : absolute path to a valid target file
+# @return : Success if @3 is valid
+# @desc   : Redirect a bash variable to stdout or file
+function __write_to_file
+{
+	[ "$1" = "--dry" ] && shift || local file_path=$2
+	local file_text="${!1}"
+
+	case "$1" in
+		*"_bang_"* )
+			local shebang="$(catvar "$file_text" | head --lines 1)"
+			file_text="$(catvar "$file_text" | tail --lines +2)"
+			local file_content="\
+${shebang}
+
+$(get_header $2)
+
+${file_text}
+
+$(get_footer)"
+					;;
+		*          )
+			local file_content="\
+$(get_header $2)
+
+${file_text}
+
+$(get_footer)"
+					;;
+	esac
+
+	catvar "$file_content" $file_path
+}
+
+# @1      : diff flags
+# @2      : file text content
+# @3      : absolute path to an existing file
+# @desc   : This function can diff an existing file against some new content
+function __diff_file
+{
+	local flags=$1; shift
+
+	if [ -f "$2" ]; then
+		# NOTE: "$(echo "$1")" makes sure $1 isn't broken down into multiple
+		# strings according to the bash IFS expasion.
+		# FIXME: check __write_to_file
+		diff $flags $2 <(__write_to_file --dry "$(echo "$1")" $2)
+	else
+		lets -l -d "invalid file: $2"
+		return $s_err2
+	fi
+}
+
+# @1      : diff flags
+# @2      : file text content
+# @3      : absolute path to an existing file
+# @desc   : This function can diff an existing file against some new content
+function is_file_diff
+{
+	__diff_file "$1" "$2" "$3" &>/dev/null
+}
+
+# @1      : file text content
+# @2      : absolute path to a valid file
+function __splash_file
+{
+	local flags="--suppress-common-lines"
+	flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
+	flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
+	flags+=" --report-identical-files"
+	flags+=" --color"
+#	flags+=" --ignore-blank-lines"
+
+	__diff_file "$flags" "$1" "$2" | lets -l -x "diff"
+	case "${PIPESTATUS[0]}" in
+		$s_ok   ) lets -l -i --phew "MATCH: $(basename "$2")" ;;
+		$s_err  ) lets -l -w "DIFFER: $(basename "$2")" ;;
+		$s_err2 ) lets -l -w "NOT-FOUND: $(basename "$2")" ;;
+	esac
+
+	return $s_ok
+}
+
+# @1      : file text content
+# @2      : absolute path to a valid file
+# @desc   : Update the content of an already generated file. This is useful when
+#         : the user doesn't want/need to recreate a new rootfs from scratch.
+function __update_file
+{
+	local file_name="$(basename $2)"
+	local flags="--suppress-common-lines"
+	flags+=" --ignore-matching-lines=$rex_legal_file_header_tag"
+	flags+=" --ignore-matching-lines=$rex_legal_file_footer_tag"
+	#flags+=" --report-identical-files"
+	#flags+=" --ignore-blank-lines"
+
+	is_file_diff "$flags" "$1" $2
+	case "$?" in
+		$s_ok   ) lets -l -i "no need to update: $file_name"; return $s_ok ;;
+		$s_err2 ) lets -l -w "add one on update: $file_name" ;;
+	esac
+
+	local old_foot_tags="$(grep -E "$rex_legal_file_footer_tag" -- $2 2>/dev/null)"
+
+	if ! __write_to_file "$1" $2; then
+		lets -l -e "failed to update $file_name"; return $s_err
+	fi
+
+	lets -l -i "changed on update: $file_name"
+
+	if [ -n "$old_foot_tags" ]; then
+		if ! appvar "$old_foot_tags" $2; then
+			lets -l -e "failed to append $old_foot_tags"
+		fi
+	fi
+}
+
+function __create_file
+{
+	[ "$1" = "--dry" ] && local dry=$1 && shift || local dry=""
+
+	if [ -f "$2" ]; then
+		lets -l -w "overriding: $(basename $2)"
+	fi
+
+	__write_to_file $dry "$1" "$2"
+}
+
+function __file_do_helper
+{
+	local __file="$2"
+	local __cmd="rm"
+	local __line="$__cmd $__file $script_now $module $1"
+
+	case "$1" in
+		--update )
+			[ -f "$3" ] && {
+				local flags="--follow-symlinks -i -E"
+
+				# Has $2 been actually updated?
+				grep -qE "$rex_legal_file_footer_tag$script_now" $2 && {
+
+					# $3 is not marked for deletion
+					grep -qE "^#$__cmd $__file .*" $3 && {
+
+						# Update line to report script_now, module and option
+						sed $flags "s%^#$__cmd $__file .*$%#$__line%g" -- $3
+						return $s_ok
+					}
+
+					# $3 is marked for deletion
+					grep -qE "^$__cmd $__file .*" $3 && {
+
+						# Update line to report script_now, module and option.
+						# Also, comment it out, as $3 is not to be removed.
+						sed $flags "s%^$__cmd $__file .*$%#$__line%g" -- $3
+						return $s_ok
+					}
+
+					[ "$(grep -E "$rex_legal_file_footer_tag" $2 | wc -l)" -eq 1 ] && {
+
+						# Update instance by adding a file. Equivalent to --create, but
+						# do not mark $2 for removal.
+						[ -f "$3" ] && {
+							! grep -q "$__cmd $2" $3 && echo "#$__line" >> $3
+							return $s_ok
+						}
+
+						lets -l -w "update without $3. Advise to wipe it all"
+						echo "#$__line" > $3 && return $s_ok
+					}
+
+					lets -l -e "$(basename $3) not tracking $(basename $2)"
+					return $s_err
+				}
+
+				# Is $2 marked for deletion? If so, un-mark it
+				grep -qE "^$__cmd $__file .*" $3 && {
+					sed $flags "s%^$__cmd $__file%#$__cmd $__file%g" -- $3
+					return $s_ok
+				}
+			} ;;
+		--create )
+			[ -f "$3" ] && {
+				! grep -q "$__cmd $2" $3 && echo "$__line" >> $3
+				return $s_ok
+			}; echo "$__line" > $3 ;;
+	esac
+}
+
+# @1      : file text content
+# @2      : absolute path to a valid file
+# @desc   : Update the content of an already generated file. This is useful
+#         : when the user doesn't want/need to recreate a new rootfs from
+#         : scratch.
+function __file_do
+{
+	# Option
+	local option=$1; shift
+	local __un_f=$1; shift
+
+	case "$option" in
+		--update ) local -r func="__update_file" ;;
+		--create ) local -r func="__create_file" ;;
+		--splash ) local -r func="__splash_file" ;;
+		*) lets -l -e "invalid option $option"; return $s_err ;;
+	esac
+
+	local -i len="${#} / 2"
+	local -i i
+
+	local -a path=( "${@:1:$len}" ); shift $len
+	local -a cont=( "${@}" )
+
+	if [ ${#path[@]} -ne ${#cont[@]} ]; then
+		lets -l -e "mismatch: $len, ${#path[@]} vs ${#cont[@]}"
+		return $s_err
+	fi
+
+	# Remove comment before processing all files, for this module only
+	if [ -f "$__un_f" ]; then
+		cp "$__un_f" "$__un_f.bkp" # be safe
+		sed --follow-symlinks -i -E "s%^#(.+ $module .+)%\1%g" -- $__un_f
+	fi
+
+	for i in ${!path[@]}; do
+		if ! $func ${cont[$i]} ${path[$i]}; then
+			lets -l -d "failed to ${func%_file} ${path[$i]}"
+			cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"
+			return $s_err
+		fi
+
+		# FIXME: handle file permissions in a proper way
+		case "${cont[$i]}" in *"_bang_"* ) chmod +x ${path[$i]} ;; esac
+
+		__file_do_helper $option ${path[$i]} $__un_f || return $s_err
+		lets -l -d "${func%_file} $(basename ${path[$i]})"
+	done
+
+	case "$option" in
+		--update )
+			[ ! -f "$__un_f" ] && {
+				cp "$__un_f.bkp" "$__un_f" && rm -f "$__un_f.bkp"; return $s_err
+			}
+
+			local -a __rmline
+			while read -a __rmline; do
+				local __cmd="${__rmline[0]}"; local __f="${__rmline[1]}"
+				case "${__rmline[*]}" in
+					'' | '\n' | '#'* )
+						lets -l -d "skip: $__cmd: $(basename "$__f")"
+						continue ;;
+					* ) [ ! -f "$__f" ] && {
+							lets -l -w "$__cmd *not* installed: $(basename "$__f")"
+						} || { lets -l -i "$__cmd installed: $(basename "$__f")"; }
+
+						eval "$__cmd $__f" && {
+							# Got to remove this line and it will be lost forever
+							sed --follow-symlinks -i -E "\:$__cmd $__f.*:d" -- "$__un_f"
+						} || { lets -l -e "failed to remove $__f"; } ;;
+				esac
+			done <<< $(grep -E "[[:space:]]$module[[:space:]]" -- $__un_f) ;;
+	esac
+
+	rm -f "$__un_f.bkp"
+	return $s_ok
+}