Last active
February 3, 2023 20:10
-
-
Save delameter/b54f82194a0dd13e7acf5112f358d723 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# ----------------------------------------------------------------------------- | |
# es7s/core (shared code) | |
# (C) 2021-2022 A. Shavykin <0.delameter@gmail.com> | |
# ----------------------------------------------------------------------------- | |
# shellcheck disable=SC2059 | |
# shellcheck disable=SC2120 | |
# ----------------------------------------------------------------------------- | |
if [[ -n "${ES7S_DEBUG:-}" ]] ; then | |
printf "$(basename "${BASH_SOURCE[-1]^^}") %s\n" \ | |
"es7s/commons load request from ${BASH_SOURCE[-1]@Q}" \ | |
" Self-path: ${BASH_SOURCE[0]@Q}" \ | |
"Phys. path: $(stat "${BASH_SOURCE[0]}" -c %N)" \ | |
" Size: $(stat -L "${BASH_SOURCE[0]}" -c %s)" \ | |
"Modif.time: $(stat -L "${BASH_SOURCE[0]}" -c %y)" \ | |
" SHA-1: $(cat "${BASH_SOURCE[0]}" | sha1sum)" | |
fi | |
[[ $(type -t __es7s_com) =~ ^fu && -z $ES7S_RELOADING ]] && return | |
# ----------------------------------------------------------------------------- | |
# FLOW CONTROL | |
_exit() { printn ; exit "${1:-126}" ; } | |
_die() { error "$*" ; _exit 1 ; } | |
_diew() { error "${*:2}" ; _exit "${1:?}" ; } | |
_help_if() { [[ $1 =~ ^--?h(elp)? ]] && cat || return 1 ; } | |
# ----------------------------------------------------------------------------- | |
# MATH, LOGIC, DATETIME | |
declare -x SECONDS_IN_MINUTE=$((60)) | |
declare -x SECONDS_IN_DAY=$((SECONDS_IN_MINUTE * 60 * 24)) | |
declare -x SECONDS_IN_MONTH=$((SECONDS_IN_DAY * 30)) | |
declare -x SECONDS_IN_YEAR=$((SECONDS_IN_MONTH * 12)) | |
min() { | |
# args: <num1> <num2> [decimals=0] | |
local num1=$(( ${1:?Num1 required} )) | |
local num2=$(( ${2:?Num2 required} )) | |
local fmt="%.${3:-0}f" | |
if [[ $(( num1 )) -lt $(( num2 )) ]] | |
then printf $fmt $num1 | |
else printf $fmt $num2 ; fi | |
} | |
max() { | |
# args: <num1> <num2> [decimals=0] | |
local num1=$(( ${1:?Num1 required} )) | |
local num2=$(( ${2:?Num2 required} )) | |
local fmt="%.${3:-0}f" | |
if [[ $(( num1 )) -gt $(( num2 )) ]] | |
then printf $fmt $num1 | |
else printf $fmt $num2 ; fi | |
} | |
round() { | |
# args: <expression> [accuracy=0] | |
# accuracy is number of fraction digits | |
local expr="${1?Expr required}" acc="${2:-0}" | |
LC_NUMERIC="en_US.UTF-8" printf "%.${acc}f" "$(bc <<< "$expr")" | |
} | |
oct2bin() { | |
# args: <oct> [len=9] | |
# first arg is oct value needed to transform | |
# second is total result length | |
# example: translation of access attribute to bit mask: "oct2bin 0644 9" -> 110100100 | |
printf "%0${2:-9}d" "$(printf "obase=2\n%d\n" ${1:?Value required} | bc)" | |
} | |
timedelta() { | |
local usage="usage: timedelta timestamp1 [timestamp2=now]" | |
local ts1=${1:?Required} | |
local ts2=${2:-$(date +%s)} | |
local delta=$(($ts2 - $ts1)) | |
local sign= | |
if [[ $delta -lt 0 ]] ; then | |
delta=$(( -1 * $delta )) | |
sign="-" | |
fi | |
if [[ $delta -le $SECONDS_IN_MINUTE ]]; then | |
printf "<1min" | |
elif [[ $delta -le $SECONDS_IN_DAY ]]; then | |
TZ=UTC0 printf '%s%(%-Hh %Mmin)T' "$sign" "$delta" | |
elif [[ $delta -le $SECONDS_IN_MONTH ]] ; then | |
printf "%s%d day(s)+" "$sign" "$((delta / SECONDS_IN_DAY))" | |
elif [[ $delta -le $SECONDS_IN_YEAR ]] ; then | |
printf "%s%d month(s)+" "$sign" "$((delta / SECONDS_IN_MONTH))" | |
else | |
printf "%s%d year(s)+" "$sign" "$((delta / SECONDS_IN_YEAR))" | |
fi | |
echo | |
} | |
function _is_int { [[ ${1-} =~ ^-?[0-9]+$ ]] && return 0 ; return 1 ; } | |
function _not { [[ -n ${1-} ]] || return 1 ; } # exit with code > 0 for non-empty arg1 | |
function _nots { [[ -n ${1-} ]] || printf "${2:-true}" ; } # print arg2 (or "true") if arg1 is non-empty | |
function _pp { [[ ${1-} -ne 1 ]] && local end=s ; printf "%d%s%s%s" "${1:-0}" "${2:+ }" "${2:-}" "${2:+${end:-}}" ; } # print plurals, ex.: _pp 3 file -> "3 files" | |
function _ps { | |
# print size of <filepath> in human format or in bytes if <1kb | |
# args: filepath | |
local ex="$(stat --printf=%s ${1:?})" | |
if [[ ex -ge 1024 ]] ; then | |
ls "$1" -sh --si | cut -f1 -d' ' | tr A-Z a-z | _ttn | |
else | |
printf %s $ex | |
fi | |
} | |
# ----------------------------------------------------------------------------- | |
# COLOR | |
# MODE 4 | |
declare -x I_BOLD=1 I_UNDERLx2=21 | |
declare -x I_DIM=2 I_NO_DIM_BOLD=22 # turns off both | |
declare -x I_ITALIC=3 I_NO_ITALIC=23 | |
declare -x I_UNDERL=4 I_NO_UNDERL=24 | |
declare -x I_BLINK_SW=5 I_NO_BLINK=25 | |
declare -x I_BLINK_FT=6 | |
declare -x I_INV=7 I_NO_INV=27 | |
declare -x I_HIDDEN=8 I_NO_HIDDEN=28 | |
declare -x I_CROSSL=9 I_NO_CROSSL=29 | |
declare -x I_OVERL=53 I_NO_OVERL=55 | |
declare -x I_BLACK=30 IB_BLACK=40 I_GRAY=90 | |
declare -x I_RED=31 IB_RED=41 IH_RED=91 | |
declare -x I_GREEN=32 IB_GREEN=42 IH_GREEN=92 | |
declare -x I_YELLOW=33 IB_YELLOW=43 IH_YELLOW=93 | |
declare -x I_BLUE=34 IB_BLUE=44 IH_BLUE=94 | |
declare -x I_MAGNETA=35 IB_MAGNETA=45 IH_MAGNETA=95 | |
declare -x I_CYAN=36 IB_CYAN=46 IH_CYAN=96 | |
declare -x I_WHITE=37 IB_WHITE=47 IH_WHITE=97 | |
declare -x I_EXTMODE=38 IB_EXTMODE=48 | |
declare -x I_DEFAULT=39 IB_DEFAULT=49 | |
# MODE 8 | |
declare -x I8_LIGHT_SEA_GREEN=37 | |
declare -x I8_WHITE=231 I8_GRAY=243 | |
function _jb { local d=${1-""} ; shift ; local f=${1-""} ; shift ; printf %s "$f" "${@/#/$d}" ; } # join [arg2...] by arg1, e.g. "_jb , 1 2" -> 1,2 | |
function _cs { printf '\e[%bm' "$(_jb \; "${@}")" ; } # integer codes to escaped codes / 4bit color | |
function _pcs { printf '\[\e[%bm\]' "$(_jb \; "${@}")" ; } # integer codes to prompt-escaped codes / 4bit color | |
function _cs8 { _cs $I_EXTMODE 5 ${1:-196} ; } # integer codes to escaped codes / 8bit color (text) | |
function _cs8b { _cs $IB_EXTMODE 5 ${1:-196} ; } # integer codes to escaped codes / 8bit color (bg) | |
function _csr { _cs $I_EXTMODE 2 ${1:-255} ${2:-128} ${3:-64} ; } # integer codes to RGB/fg SGR | |
function _csrb { _cs $IB_EXTMODE 2 ${1:-255} ${2:-128} ${3:-64} ; } # integer codes to RGB/bg SGR | |
declare -x _esq=$'\033\[[0-9;]*m' # SGR escape sequence regex | |
declare -x _tab=$'\t' _t=$'\t' _n=$'\n' _f=$(_cs) # filter reset | |
#declare -x _imk=$'\ufeff' # unicode invisible marker # though it IS visible when using a fallback font that provides | |
# graphics to all characters -- Unifont or (i assume) Last Resort | |
declare -x _b=$(_cs $I_BOLD); function _b { printf %s "$_b$*$_f" ; } | |
declare -x _d=$(_cs $I_DIM); function _d { printf %s "$_d$*$_f" ; } | |
declare -x _u=$(_cs $I_UNDERL); function _u { printf %s "$_u$*$_f" ; } | |
declare -x _U=$(_cs $I_NO_UNDERL); | |
declare -x _i=$(_cs $I_INV); function _i { printf %s "$_i$*$_f" ; } | |
declare -x _ni=$(_cs $I_NO_INV); function _ni { printf %s "$_ni$*$_f" ; } | |
declare -x _ov=$(_cs $I_OVERL); function _ov { printf %s "$_ov$*$_f" ; } | |
declare -x _r=$(_cs $I_RED); function _r { printf %s "$_r$*$_f" ; } | |
declare -x _gn=$(_cs $I_GREEN); function _gn { printf %s "$_gn$*$_f" ; } | |
declare -x _y=$(_cs $I_YELLOW); function _y { printf %s "$_y$*$_f" ; } | |
declare -x _be=$(_cs $I_BLUE); function _be { printf %s "$_be$*$_f" ; } | |
declare -x _m=$(_cs $I_MAGNETA);function _m { printf %s "$_m$*$_f" ; } | |
declare -x _c=$(_cs $I_CYAN); function _c { printf %s "$_c$*$_f" ; } | |
declare -x _gy=$(_cs $I_GRAY); function _gy { printf %s "$_gy$*$_f" ; } | |
declare -x _hr=$(_cs $IH_RED); function _hr { printf %s "$_hr$*$_f" ; } | |
declare -x _hgn=$(_cs $IH_GREEN); function _hgn { printf %s "$_hgn$*$_f" ; } | |
declare -x _hy=$(_cs $IH_YELLOW); function _hy { printf %s "$_hy$*$_f" ; } | |
declare -x _hbe=$(_cs $IH_BLUE); function _hbe { printf %s "$_hbe$*$_f" ; } | |
declare -x _hm=$(_cs $IH_MAGNETA);function _hm { printf %s "$_hm$*$_f" ; } | |
declare -x _hc=$(_cs $IH_CYAN); function _hc { printf %s "$_hc$*$_f" ; } | |
# underline words only, not spaces in between | |
function _uu { sed -Ee "s/\S+/$_u&$_U/g" <<<"$@" ; } | |
# underline words only, not spaces in between and not separators | |
function _uuu { sed -Ee "s/[^ :-]+/$_u&$_U/g" <<<"$@" ; } | |
# ----------------------------------------------------------------------------- | |
# FORMATTING / SEPARATORS AND LINE BREAKS | |
declare -x RE_NOPAD="<~" | |
function _ww { | |
local width=$COLUMNS | |
[[ -z $width ]] && width=$(tput cols) | |
[[ -z $width ]] && width=$(stty size | cut -f2 -d" " ) | |
printf %s "${width:-80}" | |
} | |
function _w { printf %s "$(( $(_ww) - 2 ))" ; } | |
function _s { printf "%${1:-1}s" | sed "s/ /${2:- }/g" ; } # prints spaces/specified chars ; args: [len] [char] | |
function _sep { printfn "%s%s%s" "${1:-""}" "$(_s "${2:-40}" "-")" "${1:+$_f}" ; } # prints separator; args: [color] [len] | |
function _spl { _s $(( 2 * ${1:-1} )) ; } # get padding by level | |
function _sbp() { # separate-by-places, args: sep place num, example: (_sbp "," 3 1000000) -> "1,000,000" | |
local d="${1:-" "}" p=$((${2:-3})) s="${*:3}" | |
for (( i=0; i<${#s}; i++ )) ; do | |
[[ $i != 0 ]] && [[ $(((${#s} - i) % p)) -eq 0 ]] && printf %s "$d" | |
printf %s "${s:$i:1}" | |
done | |
} | |
declare -x _sp=$(_spl 1) # default padding | |
function pad { local s="${1+"$(_spl ${1:-1})"}" ; sed --unbuffered "s/^/${s:-$_sp}/g" ; } | |
function printfn { [[ "$#" -le 1 ]] && printf "%s\n" "${1-}" || printf "${1-}\n" "${@:2}" ; } # @TODO deprecate | |
function printn { printf "%${1:-1}s" | sed "s/./\n/g" ; } | |
function printb { [[ -n "${1-}" ]] && printf true || printf false ; } # print as boolean | |
function prepend_nl { printn "${1:-1}" ; cat ; } # print newline(s) with stdin (before it) | |
function append_nl { cat ; printn "${1:-1}" ; } # print newline(s) with stdin (after it) | |
function head_skip { tail -n +$((${1:-1}+1)) ; } # skip <arg1> lines from start, default is 1 | |
# +---- control char separators ---------+ | |
declare -x _CCS0=$'\x1c' # FILE SEPARATOR | | |
declare -x _CCS1=$'\x1d' # GROUP SEPARATOR | | |
declare -x _CCS2=$'\x1e' # RECORD SEPARATOR | | |
declare -x _CCS3=$'\x1f' # UNIT SEPARATOR | | |
# +---- first 3 are recognised ----------+ | |
# by pythons splitlines() | |
# ----------------------------------------------------------------------------- | |
# FORMATTING / TEXT COLOR & ATTRIBUTES # ᴏᴷ success ᴡᴬ warn ᴇᴿ error ɪⁱ notice ʏᶰ Continue? (y/n) ⁽ᴏᴷᵏᴼᴋᵒₖˢⱽⅴᵥ⁾ success ⁽ᵂᴡᴿʀᵣrᴬᴀᵃ⁾ warn | |
# (ᴼₖ) success (ᵂₐ) warn (ᴱᵣ) error (ᴵᵢ) notice (ʸₙ) Continue? (y/n) ⁽ˣᴱᴇᵉₑᴿʀᵣr⁾ error ⁽ɪᴵᵢⁱᵎ⁾ notice ⁽ᵞʏʸᴺɴᶰⁿₙˀˁˤ⁾ Continue? | |
declare -x RAW_MODE # ᴼᴋ ᴏᴷ success ᵂᴀ ᴡᴬ warn ᴱʀ ᴇᴿ error ᴵᵢ ɪⁱ notice ʏₙ ʏᴺ Continue? (y/n): A B C | |
declare -x _F_SUCCESS=$(_cs $I_GREEN) _IC_SUCCESS="${_hgn}✓$(_cs 39)" #_IC_SUCCESS="$_hgn$_b✓" ✔ | |
declare -x _F_WARNING=$(_cs $I_YELLOW) _IC_WARNING="${_hy}!$(_cs 39)" #_IC_WARNING="$_hy$_b" ⁈ ⁉ | |
declare -x _F_ERROR=$(_cs $IH_RED) _IC_ERROR="${_r}✕$(_cs 39)" #_IC_ERROR="$_hr$_b✗"✘ ✕ ✖ | |
declare -x _F_NOTICE=$(_cs $I_CYAN) _IC_NOTICE="${_c}⋅$(_cs 39)" #"${_c}ⓘ·•∗*‣◦$(_cs 39)" #"${_be}$(_b i)${_be}" | |
declare -x _F_DISABLED=$(_cs8 239) _IC_DISABLED="╌" | |
declare -x _F_VERBOSE=$(_cs $I_BLUE) | |
declare -x _F_PROMPT=$(_cs $IH_MAGNETA) #; _IC_INPUT="" #"${_hm}${_b}${_hc}?${_f}${_hm}${_f}" | |
declare -x _F_PROMPT_PARAM=$(_cs $IH_CYAN) #; _IC_SELECT="" #${_IC_INPUT/\?/?} | |
declare -x _F_PROMPT_DESC=$(_cs $IH_MAGNETA) | |
declare -x _F_DEBUG=$(_cs8 60) # ; $(_cs8 $I8_LIGHT_SEA_GREEN) # _IC_DEBUG=$'\u25ce' | |
function apply { | |
# args: format [str] | |
# reads stdin if <str> is empty | |
local input format=${1:-$_f} | |
shift 1 | |
printf %s "$format" | |
if [[ -z "${*}" ]] ; then | |
cat | _ttn | |
else | |
printf %s "$*" | |
fi | |
printf %s "$_f" | |
} | |
function header_separator { | |
# args: [color] [prefix] [alignfn=alignc] | |
local title="$(cat)" color="${1-$_i}" alignfn="${3:-alignc}" | |
title="$(squeeze $(( $(_w) - ${#2} )) <<< "$title")" | |
if [[ -z "$RAW_MODE" ]] ; then | |
${alignfn} $(_ww) "$title" | sed -E "s/\s{${#2}}/$2/" | apply "$color" | append_nl | |
else | |
spaces=$(( $(_ww) - $(_ccp <<< "$title") )) | |
spaces_left=$(( spaces / 2 )) | |
_s $((spaces_left - 1)) - | |
printf " %s " "$title" | |
_s $((spaces - spaces_left - 1)) - | |
printn | |
fi | |
} | |
function header { | |
# args: [label]... | |
# env: HEADER_LVL HEADER_LABEL | |
local lvl=${HEADER_LVL:-1} | |
local label="${HEADER_LABEL-"${*-"HEADER LVL $lvl"}"}" | |
if ! header"$lvl" "${label[@]}" 2>/dev/null ; then | |
error "Invalid header level: $lvl" | |
RAW_MODE=true \ | |
header1 "${label[@]}" | |
fi | |
} | |
function header1 { | |
# args: [label...] | |
[[ -n "$RAW_MODE" ]] && { printfn "%s" "${*^^}" ; return ; } | |
printfn "$_b%s$_f" "${*^^}" | |
} | |
function header2 { | |
# args: [label...] | |
# makes part before first encountered '/' primary label | |
[[ -n "$RAW_MODE" ]] && { printfn "$_sp%s" "${*^^}" ; return ; } | |
# find first '/' occurrence or string terminator and insert styles terminator: | |
printfn "$_sp$_b%s$_f" "${*^^}" | sed -E -e "s/\s*(\/|$)\s*/$_f&/" | |
} | |
function header3 { printfn "$(_spl 2)%s" "${*}" ; } # args: [label...] | |
function table { column -e -t -s "$_tab" ; } # reads stdin | |
function tablec { table | sed -Ee 's/( +) /\1/g' ; } # condensed. reads stdin | |
# == raw-mode-aware output == | |
function verbose { debug_enabled || return 0 ; apply $_F_VERBOSE <<< "$*" ; echo ; } | |
#function notice { echo "$*" ; } | |
function disabled { _fmt_message <<< "$*" "$_F_DISABLED" "$_IC_DISABLED" "-" ; } | |
function notice { _fmt_message <<< "$*" "$_F_NOTICE" "$_IC_NOTICE" "." 1 ; } | |
function success { _fmt_message <<< "$*" "$_F_SUCCESS" "$_IC_SUCCESS" "+" 1 ; } | |
function warn { _fmt_message <<< "$*" "$_F_WARNING" "$_IC_WARNING" "!" ; } | |
function error_in { _fmt_message "$_F_ERROR" "$_IC_ERROR" "x" ; } | |
function error { error_in <<< "$*" ; } | |
function prompt_options { | |
# args: [title] | |
# stdin: option lines, no formatting | |
echo "${1:+$1? }Options:" | |
if [[ -n "$RAW_MODE" ]] ; then pad ; else | |
sed -Ee "s/^(\S+)(.+)$/$_F_PROMPT_PARAM\1$_F_PROMPT_DESC\2$_f/" | pad | |
fi | |
} | |
function prompt_yn { | |
# args: [prompt_question] [print_newline] [auto_substitute] | |
local prompt_question="${1:+$1. }Continue?" print_newline=${2:+true} auto_substitute="$3" | |
[[ -n "$auto_substitute" ]] && print_newline=true | |
while true; do | |
printf %s "$prompt_question" | |
if [[ -n "$RAW_MODE" ]] ; then | |
printf %s " y/n: " | |
printf %s "${auto_substitute:+"$auto_substitute <auto>"}" | |
else | |
printf %s " ${_F_PROMPT_DESC}(${_F_PROMPT_PARAM}y${_F_PROMPT_DESC}/${_F_PROMPT_PARAM}$(_u n)${_F_PROMPT_DESC})$_f: " | |
printf %s "${auto_substitute:+"$auto_substitute $_F_PROMPT_PARAM<auto>$_f"}" | |
fi | |
printf %s "${print_newline:+$_n}" | |
if [[ -n "$auto_substitute" ]] ; then | |
yn="$auto_substitute" | |
else | |
read -r -p "" yn | |
fi | |
case $yn in | |
[Yy]*) return 0 ;; | |
[Nn]*) return 1 ;; | |
*) return 2 ;; | |
esac | |
done | |
} | |
function _ic_caution { _fmt_message <<< "${1:-[!]}" "${_F_WARNING}${_b}" ; } # args: [icon] | |
function _fmt_message { | |
# args: [color] [icon] [label] [no-text-color] | |
# stdin: input | |
local prefix="${1:-}" icon="${2-}" label="${3}" no_text_color=${4:+true} | |
debug "" "${FUNCNAME[0]}" <<< "args: ${*@Q}" | |
local input="$(_stdin)" | |
if [[ -z "$input" ]] ; then return ; fi | |
if [[ -n "$RAW_MODE" ]] ; then | |
printfn "(%1.1s) %s" "$label" "$input" | |
elif [[ -n "$ES7S_MESSAGE_PREFIX_STYLE" ]]; then | |
printfn "${prefix}${_b}%s${_f} %s" "$label" "$input" | |
else | |
apply "$prefix" <<< "${icon:+$icon}$_f$prefix${no_text_color:+$_f} $input" | |
printn | |
fi | |
} | |
function _is_direct_output { | |
# returns 0 if direct, 1 if redirection/pipe | |
[[ -t 1 ]] && return 0 || return 1 | |
} | |
function _set_raw_mode_if_not_direct { | |
# disables formatting if pipe or redirect | |
if ! _is_direct_output ; then RAW_MODE=true ; fi | |
} | |
# ----------------------------------------------------------------------------- | |
# FORMATTING / PRECISE CHAR CONTROL | |
squeeze() { | |
# args: [req_len] | |
# stdin: text to fit | |
# - - - | |
# color-aware string shrinker, fits string to specified | |
# length, adds overflow indicator ($ES7S_OVERFLOW) | |
local req_len=${1:-0} __e=$'\033' | |
local input suffix clean_str output control_seqs | |
local overflow_ind="${ES7S_OVERFLOW:-~}" | |
shift 2 | |
[[ $req_len -le 0 ]] && return 0 | |
input="$(cat)" | |
clean_str="$(_dcu <<< "$input")" | |
control_seqs=$(_cesq <<< "$input") | |
if [[ ${#clean_str} -le $req_len ]] ; then | |
[[ $control_seqs -gt 0 ]] && input+="$_f" | |
printf "%s" "$input" | |
return 0 | |
fi | |
# overflow indicator is always visible now | |
req_len=$((req_len - 1)) | |
if [[ $control_seqs -le 0 ]] ; then | |
output="${clean_str:0:$((req_len))}" | |
printf "%s%s" "${output}" "${overflow_ind}" | |
return 0 | |
fi | |
# main idea: iterate through beginnings of control sequences in the string | |
# and move them to the output always as a whole, or don't move at all | |
while [[ $req_len -gt 0 ]] ; do | |
local next_esc_excluded_pos next_esc_included_pos | |
# find position of first (relative to # count how many seq bytes are following | |
# string start) control seq byte: # first one in a row: | |
# .*...2.3\e[1m\e[2m.A.B.C... # .*...2.3\e[1m\e[2m.A.B.C.D.X\e[38... | |
# 0 > > >^ # & > > > > ^ ^? this one doesn't matter (yet) | |
next_esc_excluded_pos=$(( $( sed -Ee "s/$__e.+//" <<< "$input" | _ccp ) )) | |
next_esc_included_pos="$( sed -Ee "s/([^$__e]*)($__e\[[0-9;]+m)[^$__e]+(.+)/\1\2/" <<< "$input" | _cca )" | |
if [[ $req_len -le $next_esc_excluded_pos ]] ; then | |
# if there are no control chars to the left of desired line cut | |
# just copy regular chars, it's safe: | |
output+="${input:0:req_len}" | |
req_len=0 | |
else | |
# + ---- control-seq ----- + | |
# | included | | |
# 0.1.2.3.4.5.6.7..3\e[1m\e[2m.A.B.C.D.X\e[38.. | |
# ^ ^ | |
# \_ control-seq _/ that's the key - we copy all chars (including control sequences), | |
# excluded but _count_ as "copied" only printable characters | |
output+="${input:0:$next_esc_included_pos}" # ------------ # ~ | |
req_len=$((req_len - next_esc_excluded_pos)) #-| OH NO! WHY.. | | |
input="${input:$next_esc_included_pos}" # # ------------ # | |
# | |
# that makes the result string to have required length and also " ~ | |
# prevents control sequences from becoming broken - when one half ..\e[1 . ;32m.. ~ | |
fi # of it cut out, while the other one stays in place, like this: | | |
done # cut > ^ ~ | |
# this algorithm can be improved, though. first version was designed ~ | |
# to iterate chars one by one, but was never developed. second (this) | |
# version uses seds for finding closest control sequences to the | |
# beginning and then handles them. even more optimized algorithm would | |
# be to start searching from cut position, not the 1st char .. NEVERMIND | |
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ 0 | |
# if we want to keep ALL the formatting, the only way | |
# to accomplish this is like the above. but it | |
# can still be improved | |
printf "%s%s" "${output}${_f}" "${overflow_ind}" | |
} | |
# shellcheck disable=SC2086 | |
alignl() { _arg_stdin "${@:2}" | align -1 ${1-0} ; } | |
alignc() { _arg_stdin "${@:2}" | align 0 ${1-0} ; } | |
alignr() { _arg_stdin "${@:2}" | align 1 ${1-0} ; } | |
align() { | |
# args: -1|0|1 req_len [pad|shift] | |
# stdin: text | |
# first is mode: 0 = center, 1 = right, -1 = left | |
# pad counts from edge to center when mode=left|right | |
# and from center when center | |
local input result spaces man_pad left_pad right_pad | |
local mode=${1:-0} | |
local req_len=${2-""} | |
local arg3=${3:-0} | |
shift 3 | |
[[ -z "$req_len" ]] && return 0 | |
input="$(cat)" | |
[[ $req_len -le 0 ]] && { printf %s "$input" ; return 0 ; } | |
local clean_str="$(printfn "%s" "$input" | _dcu)" | |
local clean_len=${#clean_str} | |
result="$input" | |
if [[ $clean_len -lt $req_len ]] ; then | |
spaces=$(( req_len - clean_len )) | |
[[ $spaces -lt 0 ]] && spaces=0 | |
if [[ $mode -eq 0 ]]; then | |
local req_shift=$arg3 | |
left_pad=$(( spaces / 2 )) | |
spaces=$(( spaces - left_pad )) | |
[[ $spaces -lt 0 ]] && spaces=0 | |
[[ $left_pad -gt $spaces ]] && left_pad=$spaces | |
right_pad=$(( spaces )) | |
spaces=0 | |
[[ $right_pad -lt 0 ]] && right_pad=0 | |
local left_ss="$(printf "%${left_pad}s" "")" | |
local right_ss="$(printf "%${right_pad}s" "")" | |
result="$(printf "%s%s%s" "$left_ss" "$result" "$right_ss")" | |
else | |
man_pad=$(( arg3 )) # force_pad | |
[[ $man_pad -gt $spaces ]] && man_pad=$spaces | |
man_ss="$(printf "%${man_pad}s" "")" | |
spaces=$(( spaces - man_pad )) | |
[[ $spaces -lt 0 ]] && spaces=0 | |
auto_ss="$(printf "%${spaces}s" "")" | |
if [[ $mode -eq -1 ]]; then | |
result="$(printf "%s%s%s" "$man_ss" "$result" "$auto_ss")" | |
elif [[ $mode -eq 1 ]]; then | |
result="$(printf "%s%s%s" "$auto_ss" "$result" "$man_ss")" | |
fi | |
fi | |
fi | |
printf "%s" "$result" | |
} | |
trim() { | |
# reads stdin, trims it to <max_len> chars and adds dots if input was longer | |
# args: [max_len=10] | |
sed -Ee "s/(.{${1:-10}}).{2,}/\1$ES7S_OVERFLOW/" | |
} | |
function _stdin { cat - ; } # stdin read wrapper | |
function _arg_stdin { | |
# Reads args from stdin when none is supplied. | |
# Usage: _arg_stdin "$@" | ... | |
[[ -n "$*" ]] && printfn %s "$@" && return | |
_stdin | |
} | |
# ----------------------------------------------------------------------------- | |
# FORMATTING / HARDCORE | |
vesq() { | |
# args: [esq_details] [omit_esq] | |
# visualize escape sequences and whitespaces | |
if [[ "$1" = -h ]] || [[ "$1" = --help ]] ; then | |
printfn %s "Set first argument to a non-empty value to get more information about printed control sequences." "By default recognized escape sequences apply to original text as well as seq markers; to disable it set second argument to a non-empty value." | pad | |
return 0 ; fi | |
local detailed_esq="${1:+"\\2"}" | |
local apply_esq=$(_nots "${2-}" "&") | |
local __e=$'\033' | |
#@TODO допилить поток сука блять заебало | |
# https://en.wikipedia.org/wiki/ANSI_escape_code | |
sed -E -e "s/($__e\[)(0?)(m)/\10\3/g" \ | |
-e "s/($__e\[)([0-9;]*)(m)/${_f}${_i}ɘ${detailed_esq}${apply_esq}${_ni}/g" \ | |
-e "s/($__e\[0m)($__e\[7m)ɘ0*/\1\2Ǝ/g" \ | |
-e "s/ /␣/g" -e "s/\t/${_i}»${_ni}&/g" \ | |
-e "s/$/$_i$_b$_gy"$'\u21b5'"$_f/g" -e "s/\v/$_i$_b$_gy\\\v$_f/g" -e "s/\f/$_i$_b$_gy\\\f$_f/g" \ | |
-e "s/\r/$_i$_b$_y↤$_f/g" -e "s/\x00/${_hr}${_i}${_b}Ø$_f/g" \ | |
-e "s/($__e)([^_@A-Z\x5c\x5b\x5d])/${_r}${_i}æ${detailed_esq}${_f}/g" # \ | |
#| sed -z -e "s/(\e\[)([0-9:;<=>?[\]]*[!\"#$%&\'()*+,\-\.\/]*[A-Za-z_\[\]])/$_hr$_i\\\2$_f/g" | |
# -e "s/(\e)([0-9:;<=>?[\]]*[!\\\"#$%&'()*+,\-\.\/]*[A-Za-z[\]_\`\{\|\}~^])/\2/g" | |
# -e "s/($__e)([0-9:;<=>?[\]]*[ \!\"\#\$\%\&\'\(\)\*\+,\-\.\/]*[A-Za-z\[\\\]\^_\`\{\|\}~]) /$_b${_y}$_i"$'\u241b'"\2${_u}$_f/g" | |
} | |
# == trimming whitespaces, context - line == | |
function _trim { _triml | _trimr ; } | |
function _triml { sed -Ee "1 s/^(\s*)//" ; } | |
function _trimr { sed -Ee "$ s/(\s*)$//" ; } | |
# == trimming whitespaces, context - byte stream/multiline == | |
function _ttn { sed -z '$ s/\n$//' ; } # trim trialing newline | |
function _ttw { sed -zEe 's/[[:space:]]*$//' ; } # trim trialing whitespace | |
# == char counting == | |
function _cca { _ttn | wc -m ; } # count chars - all | |
function _ccp { _dc | _ttn | wc -m ; } # count chars - printable | |
function _cesq { tr -cd '\033' | wc -c ; } # count \e[*m seqs in stdin | |
# == deformatting == | |
function _dc { decolorize ; } | |
function _dcu { decolorize-clean-up ; } | |
# read stdin, remove \e[*m seqs: | |
decolorize() { sed --unbuffered -Ee 's/\x1b\[[0-9\;]*m//g' ; } | |
# read stdin, remove \e[*m seqs and ASCII control chars: | |
decolorize-clean-up() { sed --unbuffered -Ee 's/\x1b\[[0-9\;]*m//g' | tr -d '\000-\011\013-\037'; } | |
# remove \[ "\\\[" and \] "\\\]" sequences from stdin (used to tell shell to skip chars between when counting printable): | |
remove-prompt-escaped-brackets() { sed -zEe 's/\\(\[|\])//g' ; } | |
# translate/remove UNICODE control chars: | |
tr-unicode-controls() { | |
# args: [replacement="."] | |
#sed -Ee "s/((\xc2[\x7f-x9f])[|]?+)/${1-}/g" | |
# ^ breaks watson U+0080-009F displaying @see #225 | |
sed -Ee "s/\xc2\x7f|\xc2\x80|\xc2\x81|\xc2\x82|\xc2\x83|\xc2\x84|\xc2\x85|\xc2\x86|\xc2\x87|\xc2\x88|\xc2\x89|\xc2\x8a|\xc2\x8b|\xc2\x8c|\xc2\x8d|\xc2\x8e|\xc2\x8f|\xc2\x90|\xc2\x91|\xc2\x92|\xc2\x93|\xc2\x94|\xc2\x95|\xc2\x96|\xc2\x97|\xc2\x98|\xc2\x99|\xc2\x9a|\xc2\x9b|\xc2\x9c|\xc2\x9d|\xc2\x9e|\xc2\x9f/${1-''}/g" | |
} | |
# ----------------------------------------------------------------------------- | |
# GIT SHARED HELPERSq | |
function __git_current_branch { | |
git rev-parse --abbrev-ref HEAD 2> /dev/null | |
} | |
# ----------------------------------------------------------------------------- | |
# HIGH ORDER ALCHEMY | |
# forces terminal to ignore all user input except Ctrl+C: | |
hide-user-input() { trap __terminal_cleanup EXIT ; trap __hide_input CONT ; __hide_input ; } | |
function __hide_input { if [ -t 0 ]; then stty -echo -icanon time 0 min 0 ; fi } | |
function __terminal_cleanup { if [ -t 0 ]; then stty sane ; fi } | |
# determine current cursor position: | |
function _curpos_request { printf "\e[6n" ; } | |
function _curpos_response_getx { | |
local CURPOS_TIMEOUT_SECONDS=1 | |
local CURPOS_WAIT_SECONDS=1.1 | |
# timeout is needed to prevent infinite hanging up ; | |
# an alternative is to check if stdout is a terminal: [[ -t 1 ]] | |
# and disable this feature if it is not | |
start_time=$(date +%s.%3N) | |
read -t $CURPOS_WAIT_SECONDS -sdR CURPOS_STREAM | |
elapsed_time=$(round "$(date +%s.%3N) - $start_time") | |
if [[ $elapsed_time -ge $CURPOS_TIMEOUT_SECONDS ]] ; then return 2 ; fi | |
# originally: ${CURPOS_STREAM#*[} | |
CURPOS="$(sed <<< "${CURPOS_STREAM##*[}" -Ee "s/^[0-9]+;([0-9]+)$/\1/" -e "s/([^;]+)(;.+|$)/\1/")" | |
if ! _ttw <<< "$CURPOS" | grep -qEe "^[0-9]+$" ; then return 1 ; fi | |
printf %s "$CURPOS" | |
} | |
set-title() { | |
printf "\e]0;%s\007" "$*"; | |
} | |
set-title-and-run() { | |
set-title "$*" | |
"$@" | |
} | |
# ----------------------------------------------------------------------------- | |
# DEBUGGING (KIND OF) | |
declare -x ES7S_DEBUG_BUF | |
enable-debug() { ES7S_DEBUG=true ; } | |
debug-char-ruler() { | |
# args: [force=] [no_color=] | |
# if first arg is non-empty value, displays ruler even in normal mode | |
local force="${1:+true}" no_color="${2:+true}" | |
debug_enabled || [[ -n $force ]] || return 0 | |
local logo="es7s│" | |
local sep10="╹" section="#''''╵''''" | |
local f_inactive="$(_cs8 $I8_GRAY)" | |
local f_active="$_u$_ov${_F_DEBUG}" | |
local width=$(_ww) shift=1 | |
# shift is needed because first char should be marked as 1, not 0 | |
local i begin end output label | |
local n=$((1 + width / 10)) | |
for (( i=0 ; i<n ; i++ )) ; do | |
printf "%d " $i >> /tmp/ruler | |
[[ $i -eq 1 ]] && { shift=0 ; logo="│$logo" ; } | |
label=$(( i * 10 )) | |
if [[ $((i%20)) -eq 0 ]] ; then begin="$f_active${logo}${f_inactive}" | |
else begin="$f_active${sep10}${label}${f_inactive}$(_s 1)" | |
fi ; if [[ $i -eq 20 ]] ; then begin="$f_active${sep10}$(squeeze 9 <<< "weeeeeeeeee")$f_inactive" | |
elif [[ $i -eq 40 ]] ; then begin="$f_active${ES7S_OVERFLOW}eeees7s${logo::1}$f_inactive" | |
fi | |
end="${section:$(( $(_ccp <<< "$begin") + shift ))}" ; | |
output+="$begin$end" | |
if [[ $( _ccp <<< "$output" ) -ge $width ]] ; then | |
[[ -n $no_color ]] && output="$(_dcu <<< "$output")" | |
squeeze $width <<< "$output" | |
break | |
fi | |
done | |
} | |
var-dump() { | |
# args: [filter_names=] | |
# dump all variables, filter var name by egrep -e "$1" if provided | |
local filter_names="${1-}" | |
local skipped=0 vars | |
vars=$(set -o posix; IFS=$'\n' set) | |
for v in $vars ; do | |
if [[ -z "$filter_names" ]] || grep -qEe "$filter_names" <<< "$v" ; then | |
sed <<< "$v$_f" -E -e "s/^/$_gn/" -e "s/=/$_be&$_f/" | |
else | |
((skipped++)) | |
fi | |
done | |
[[ -n "$filter_names" ]] && notice "${skipped} results filtered" | |
} | |
debug_enabled() { [[ -n "${ES7S_DEBUG-""}" ]] ; } # usage: debug_enabled && <cmd> || return | |
debug() { # can be used to flush previously buffered output | |
# args: [add_newline=true] [src_override=] | |
# stdin: text | |
debug_enabled || return 0 | |
debug_buf "${1:-true}" | |
debug_flush "${2:-}" | |
} | |
function debug_flush { | |
# args: [src_override=] | |
# stdin: text | |
debug_enabled || return 0 | |
local func_name="$(echo "${FUNCNAME[-1]//[^A-Za-z0-9_]/}" | tr -s _)" | |
local source_name="$(basename "${BASH_SOURCE[-1]//.sh/}")" | |
[[ $source_name == "bash-alias" ]] && source_name="(${source_name}) ${func_name}" | |
[[ -n "$1" ]] && source_name="$source_name:$1" | |
if [[ -n "$RAW_MODE" ]] ; then | |
printf %s "${ES7S_DEBUG_BUF}" | sed -Ee "s/^/(DEBUG)[${source_name^^}] /;" | |
else | |
printf %s "${ES7S_DEBUG_BUF}" | | |
sed -E -e "s/^.*/${_F_DEBUG}${_i}${source_name^^}${_ni}${_F_DEBUG} &${_f}/g" | |
fi | |
ES7S_DEBUG_BUF= | |
} | |
function debug_buf { | |
# args: [add_newline=] | |
# stdin: text | |
debug_enabled || return 0 | |
local append_nl_arg="${1:+1}" | |
mapfile -c1 -C "_debug_buf_line_callback" < <( _stdin | append_nl "${append_nl_arg:-0}" | _ttn) | |
} | |
function _debug_buf_line_callback { shift 1 ; ES7S_DEBUG_BUF+="$*" ; } | |
# ----------------------------------------------------------------------------- | |
# DEBUGGING (PROPER) | |
enable-etrace() { trap '__trace_errors' ERR ; set -o errtrace ; } | |
function __trace_errors { | |
local err=$? | |
set +o xtrace | |
local code="${1:-1}" | |
echo "Error in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}. '${BASH_COMMAND}' exited with status $err" | |
# print out the stack trace described by $function_stack | |
if [ ${#FUNCNAME[@]} -gt 2 ] | |
then | |
echo "Call tree:" | |
for ((i=1;i<${#FUNCNAME[@]}-1;i++)) | |
do | |
echo " $i: ${BASH_SOURCE[$i+1]}:${BASH_LINENO[$i]} ${FUNCNAME[$i]}(...)" | |
done | |
fi | |
echo "Exiting with status ${code}" | |
exit "${code}" | |
} | |
################################################################################ | |
# DEPRECATIONS / DO NOT USE | |
function _deprecated { | |
printf %s "$_f$_b$_i${_r}DEPRECATION WARNING $_f$_u${_y}$(_jb " <- " "${FUNCNAME[@]}")" | |
printfn %b " $(_cs $IH_YELLOW $I_BOLD 5)DO NOT USE OR YOU WILL REGRET$_f" | |
printf %s "$*" | |
} | |
#function _g { _deprecated "$*" ; } ; declare _g="$(_g nested call)" | |
#function _gr { _deprecated "$*" ; } ; declare _gr="$(_gr nested call)" | |
#function info { _deprecated "$*" ; } | |
#function sb { _deprecated "$*" ; } | |
declare _imk=$(_deprecated _imk) | |
################################################################################ | |
# DONE | |
function __es7s_com { echo "ES7S commons loaded"; } | |
debug <<< "Loaded es7s/commons" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment