| #!/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 "${*// /:}" |
| } |