blob: 6bd5289e5ef217f583011b3c8bbd9162d74b522b [file] [log] [blame]
#!/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 "${*// /:}"
}