######################################################################### # # # Bunch of helpers for bash scripting # # # # This file is compilation from some of my projects. # # I'm not sure they're all in perfiect condition but I use them # # time to time in my scripts. # # # ######################################################################### ###################################### # Little handy helpers for scripting ###################################### log() { [ ! -d "/home/logs" ] && mkdir -p "/home/logs" echo -e "[`date '+%H:%M:%S'`] $*" | tee -a "/home/logs/`date '+%Y%m%d'`.log" } installed() { command -v "$1" >/dev/null 2>&1 } installed_pkg() { dpkg --list | grep -qw "ii $1" } apt_install() { sudo apt install -y --autoremove "$*" } require() { sw=() for package in "$@"; do if ! installed "$package" && ! installed_pkg "$package"; then sw+=("$package") fi done if [ ${#sw[@]} -gt 0 ]; then echo "These packages will be installed in your system:\n${sw[*]}" apt_install "${sw[*]}" [ $? -gt 0 ] && { echo "installation cancelled" exit 201 } fi } require_pkg() { sw=() for package in "$@"; do if ! installed "$package" && ! installed_pkg "$package"; then sw+=("$package") fi done if [ ${#sw[@]} -gt 0 ]; then echo "These packages must be installed in your system:\n${sw[*]}" exit 200 fi } require_dir() { is_dir "$1" || die "Directory '$1' does not exist!" 1 } title() { [ "$1" ] && title="$1" || title="$(grep -m 1 -oP "(?<=^##makedesc:\s).*$" ${BASH_SOURCE[1]})" info info "===============================================" info "$title" info "===============================================" info } unpak_targz() { require tar tar -xzf "$1" -C "$2" } symlink() { ln -sf "$1" "$2" } download() { require wget wget "$1" -O "$2" } clone() { require git git clone $* } clone_quick() { require git git clone $* --depth=1 --single-branch } abspath() { echo $(realpath -q "${1/#\~/$HOME}") } is_writable() { [ -w "$(abspath $1)" ] } is_dir() { [ -d "$(abspath $1)" ] } is_file() { [ -f "$(abspath $1)" ] } is_function() { declare -F "$1" > /dev/null } regex_match() { printf "%s" "$1" | grep -qP "$2" } in_array() { local find=$1 shift for e in "$@"; do [[ "$e" == "$find" ]] && return 0 done return 1 } implode() { local d=${1-} local f=${2-} if shift 2; then printf %s "$f" "${@/#/$d}" fi } open_url() { if which xdg-open > /dev/null; then xdg-open "$1" /dev/null 2>&1 & disown elif which gnome-open > /dev/null; then gnome-open "$1" /dev/null 2>&1 & disown fi } ######################################################## # Desktop notifications ######################################################## notify () { require "notify-send" [ -n "$1" ] && local title="$1" || local title="My notification" local text="$2" local level="$3" local icon="$4" case $level in "critical") local timeout=0 ;; "low") local timeout=5000 ;; *) local timeout=10000 ;; esac notify-send "$title" "$text" -a "MyScript" -u "$level" -i "$icon" -t $timeout } notify_error() { notify "Error" "$1" "critical" "dialog-error" } notify_warning() { notify "Warning" "$1" "normal" "dialog-warning" } notify_info() { notify "" "$1" "low" "dialog-information" } ###################################### # Input & output ###################################### IINFO="( i )" INOTE="( * )" IWARN="( # )" IERROR="( ! )" IFATAL="( @ )" ISUCCESS="( ! )" IASK="( ? )" IDEBUG="(DBG)" IVRB="( + )" BOLD="\e[1m" DIM="\e[2m" NOTBOLD="\e[22m" # sometimes \e[21m NOTDIM="\e[22m" NORMAL="\e[20m" RESET="\e[0m" FRESET="\e[39m" FBLACK="\e[30m" FWHITE="\e[97m" FRED="\e[31m" FGREEN="\e[32m" FYELLOW="\e[33m" FBLUE="\e[34m" FLRED="\e[91m" FLGREEN="\e[92m" FLYELLOW="\e[93m" FLBLUE="\e[94m" BRESET="\e[49m" BBLACK="\e[40m" BWHITE="\e[107m" BRED="\e[41m" BGREEN="\e[42m" BYELLOW="\e[43m" BBLUE="\e[44m" BLRED="\e[101m" BLGREEN="\e[102m" BLYELLOW="\e[103m" BLBLUE="\e[104m" dt() { echo "[$(date +'%H:%M:%S')] " } ask() { IFS= read -rp "$(print ${BOLD}${BBLUE}${FWHITE}${IASK}${BRESET}\ ${BOLD}$1 ): " $2 } print() { echo -e "$*${RESET}" } debug() { if [ "$2" ]; then print "${DIM}${BOLD}${RESET}${DIM} ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$1 " else print "${DIM}${BOLD}${RESET}${DIM}$1 " fi } var_dump() { debug "$1 = ${!1}" 0 } verbose() { print "${BOLD}${IVRB}${RESET}${FYELLOW} $1 " } info() { print "${BOLD}${FWHITE}${BLBLUE}${IINFO}${RESET}${FWHITE} $1 " } note() { print "${BOLD}${DIM}${FWHITE}${INOTE}${RESET} $1 " } success() { print "${BOLD}${BGREEN}${FWHITE}${ISUCCESS}${BRESET}$FGREEN $1 " } warn() { print "${BOLD}${BYELLOW}${FBLACK}${IWARN}${BRESET}${FYELLOW} Warning:${RESET} $1 " } error() { print "${BOLD}${BLRED}${FWHITE}${IERROR} Error: ${BRESET}${FLRED} $1 " >&2 } fatal() { print "${BOLD}${BRED}${FWHITE}${IFATAL} FATAL: $1 " >&2 print_stacktrace } die() { error "${1:-halted}" exit ${2:-100} } print_stacktrace() { STACK="" local i local stack_size=${#FUNCNAME[@]} debug "Callstack:" # for (( i=$stack_size-1; i>=1; i-- )); do for (( i=1; i<$stack_size; i++ )); do local func="${FUNCNAME[$i]}" [ x$func = x ] && func=MAIN local linen="${BASH_LINENO[$(( i - 1 ))]}" local src="${BASH_SOURCE[$i]}" [ x"$src" = x ] && src=non_file_source debug " at $func $src:$linen" done } ######################################################## # Tests ######################################################## # $1 - command to exec assert_exec() { [ "$1" ] || exit 1 local prefix="$(dt)${BOLD}${FWHITE}[TEST EXEC]" if $($1 1>/dev/null 2>&1); then local text="${BGREEN} PASSED" else local text="${BLRED} FAILED" fi print "${prefix} ${text} ${BRESET} ($?):${RESET} $1" } # usage: # func1() { # return 0 # } # func2() { # return 1 # } # assert_exec "func1" # PASSED # assert_exec "func2" # FAILED # assert_exec "whoami" # PASSED # $1 - command to exec # $2 - expected output assert_output() { [ "$1" ] || exit 1 [ "$2" ] && local expected="$2" || local expected='' local prefix="$(dt)${BOLD}${FWHITE}[TEST OUTP]" local output=$($1 2>&1) local code=$? if [[ "$output" == *"$expected"* ]]; then local text="${BGREEN} PASSED" else local text="${BLRED} FAILED" fi print "${prefix} ${text} ${BRESET} (${code}|${expected}):${RESET} $1" # print "\tOutput > $output" } # usage: # func1() { # echo "some string" # } # func2() { # echo "another string" # } # expect_output "func1" "string" # PASSED # expect_output "func2" "some" # FAILED # expect_output "func2" "string" # PASSED # $1 - command to exec # $2 - expected exit-code assert_code() { [ "$1" ] || exit 1 [ "$2" ] && local expected=$2 || local expected=0 local prefix="$(dt)${BOLD}${FWHITE}[TEST CODE]" $($1 1>/dev/null 2>&1) local code=$? if [[ $code -eq $expected ]]; then local text="${BGREEN} PASSED" else local text="${BLRED} FAILED" fi print "${prefix} ${text} ${BRESET} (${code}|${expected}):${RESET} $1" } # usage: # func1() { # # exit 0 # return 0 # } # func2() { # # exit 1 # return 1 # } # expect_code "func1" 0 # PASSED # expect_code "func1" 1 # FAILED # expect_code "func2" 0 # FAILED # expect_code "func2" 1 # PASSED ######################################################## # Misc ######################################################## curltime() { curl -w @- -o /dev/null -s "$@" <<'EOF' time_namelookup: %{time_namelookup} sec\n time_connect: %{time_connect} sec\n time_appconnect: %{time_appconnect} sec\n time_pretransfer: %{time_pretransfer} sec\n time_redirect: %{time_redirect} sec\n time_starttransfer: %{time_starttransfer} sec\n ---------------\n time_total: %{time_total} sec\n EOF } ytm() { youtube-dl \ --extract-audio \ --audio-format flac \ --audio-quality 0 \ --format bestaudio \ --write-info-json \ --output "${HOME}/Downloads/ytm/%(playlist_title)s/%(channel)s - %(title)s.%(ext)s" \ $* } docker.ip() { # not finished if [ "$1" ]; then if [ "$1" = "-a" ]; then docker ps -aq \ | xargs -n 1 docker inspect --format '{{.Name}}{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}' \ | sed -e 's#^/##' \ | column -t elif [ "$1" = "-c" ]; then docker-compose ps -q \ | xargs -n 1 docker inspect --format '{{.Name}}{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}' \ | sed -e 's#^/##' \ | column -t else docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1" docker port "$1" fi else docker ps -q \ | xargs -n 1 docker inspect --format '{{.Name}}{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}' \ | sed -e 's#^/##' \ | column -t fi } ######################################################## # Working with git ######################################################## git.is_repo() { [ "$1" ] || die "Path is not specified" 101 require_dir "$1/" check_dir "$1/.git" } git.require_repo() { git.is_repo "$1" || die "'$1' is not git repository!" 10 } git.cfg() { [ "$1" ] || die "Key is not specified" 101 if [[ "$2" ]]; then git config --global --replace-all "$1" "$2" else echo $(git config --global --get-all "$1") fi } git.set_user() { [ "$1" ] || die "git.set_user: Repo is not specified" 100 git.cfg "$1" "user.name" "$2" git.cfg "$1" "user.email" "$3" success "User set to '$name <$email>' in ${FWHITE}$1" } git.fetch() { if [ "$1" ]; then if git.remote_branch_exists "origin/$1"; then git fetch origin "refs/heads/$1:refs/remotes/origin/$1" --progress --prune --quiet 2>&1 || die "Could not fetch $1 from origin" 12 else warn "Tried to fetch branch 'origin/$1' but it does not exist." fi else git fetch origin --progress --prune --quiet 2>&1 || exit 12 fi } git.reset() { git reset --hard HEAD git clean -fd } git.clone() { git clone $* 2>&1 } git.co() { git checkout $* 2>&1 } git.is_it_current_branch() { [ "$1" ] || die "git.is_it_current_branch: Branch is not specified" 19 [[ "$(git.current_branch)" = "$1" ]] } git.pull() { [ "$1" ] && BRANCH=$1 || BRANCH=$(git.current_branch) # note "Updating branch $BRANCH..." git pull origin "refs/heads/$BRANCH:refs/remotes/origin/$BRANCH" --prune --force --quiet 2>&1 || exit 13 git pull origin --tags --force --quiet 2>&1 || exit 13 # [ "$1" ] || die "git.pull: Branch is not specified" 19 # if [ "$1" ]; then # note "Updating branch $1..." # git pull origin "refs/heads/$1:refs/remotes/origin/$1" --prune --force --quiet 2>&1 || exit 13 # else # note "Updating current branch..." # git pull # fi } git.current_branch() { git branch --show-current || exit 18 } git.local_branch_exists() { [ -n "$(git for-each-ref --format='%(refname:short)' refs/heads/$1)" ] } git.update_refs() { info "Updating local refs..." git remote update origin --prune 1>/dev/null 2>&1 || exit 18 } git.delete_remote_branch() { [ "$1" ] || die "git.remote_branch_exists: Branch is not specified" 19 if git.remote_branch_exists "origin/$1"; then git push origin :"$1" # || die "Could not delete the remote $1 in $ORIGIN" return 0 else warn "Trying to delete the remote branch $1, but it does not exists in origin" return 1 fi } git.is_clean_worktree() { git rev-parse --verify HEAD >/dev/null || exit 18 git update-index -q --ignore-submodules --refresh git diff-files --quiet --ignore-submodules || return 1 git diff-index --quiet --ignore-submodules --cached HEAD -- || return 2 return 0 } git.is_branch_merged_into() { [ "$1" ] || die "git.remote_branch_exists: Branch1 is not specified" 19 [ "$2" ] || die "git.remote_branch_exists: Branch2 is not specified" 19 git.update_refs local merge_hash=$(git merge-base "$1"^{} "$2"^{}) local base_hash=$(git rev-parse "$1"^{}) [ "$merge_hash" = "$base_hash" ] } git.remote_branch_exists() { [ "$1" ] || die "git.remote_branch_exists: Branch is not specified" 19 git.update_refs [ -n "$(git for-each-ref --format='%(refname:short)' refs/remotes/$1)" ] } git.new_branch() { [ "$1" ] || die "git.new_branch: Branch is not specified" 19 if [ "$2" ] && ! git.local_branch_exists "$2" && git.remote_branch_exists "origin/$2"; then git.co -b "$1" origin/"$2" else git.co -b "$1" "$2" fi } git.require_clean_worktree() { if ! git.is_clean_worktree; then warn "Your working tree is dirty! Look at this:" git status -bs _T="What should you do now?\n" _T="${_T}\t${BOLD}${FWHITE}0.${RESET} try to continue as is\t- errors may occur!\n" _T="${_T}\t${BOLD}${FWHITE}1.${RESET} hard reset\t\t\t- clear current changes and new files\n" _T="${_T}\t${BOLD}${FWHITE}2.${RESET} stash changes (default)\t- save all changes in safe to apply them later via 'git stash pop'\n" _T="${_T}\t${BOLD}${FWHITE}3.${RESET} cancel\n" ask "${_T}${BOLD}${FWHITE}Your choice [0-3]" reset_answer case $reset_answer in 1 ) warn "Clearing your work..." && git.reset ;; 3 ) exit ;; * ) git stash -a -u -m "WIP before switch to $branch_task" ;; esac fi } ######################################################## # Also ######################################################## # https://gist.github.com/anthonyaxenov/d53c4385b7d1466e0affeb56388b1005 # https://gist.github.com/anthonyaxenov/89c99e09ddb195985707e2b24a57257d # ...and other my gists with [SHELL] prefix ######################################################## # Sources and articles used ######################################################## # https://github.com/nvie/gitflow/blob/develop/gitflow-common (BSD License) # https://github.com/petervanderdoes/gitflow-avh/blob/develop/gitflow-common (FreeBSD License) # https://github.com/vaniacer/bash_color/blob/master/color # https://misc.flogisoft.com/bash/tip_colors_and_formatting # https://www-users.york.ac.uk/~mijp1/teaching/2nd_year_Comp_Lab/guides/grep_awk_sed.pdf # https://www.galago-project.org/specs/notification/ # https://laurvas.ru/bash-trap/ # https://stackoverflow.com/a/52674277 # https://rtfm.co.ua/bash-funkciya-getopts-ispolzuem-opcii-v-skriptax/ # https://gist.github.com/jacknlliu/7c51e0ee8b51881dc8fb2183c481992e # https://gist.github.com/anthonyaxenov/d53c4385b7d1466e0affeb56388b1005 # https://github.com/nvie/gitflow/blob/develop/gitflow-common # https://github.com/petervanderdoes/gitflow-avh/blob/develop/gitflow-common # https://gitlab.com/kyb/autorsync/-/blob/master/ # https://lug.fh-swf.de/vim/vim-bash/StyleGuideShell.en.pdf # https://www.thegeekstuff.com/2010/06/bash-array-tutorial/ # https://www.distributednetworks.com/linux-network-admin/module4/ephemeral-reserved-portNumbers.php