624 lines
16 KiB
Bash
624 lines
16 KiB
Bash
#########################################################################
|
|
# #
|
|
# 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 >/dev/null 2>&1 & disown
|
|
elif which gnome-open > /dev/null; then
|
|
gnome-open "$1" </dev/null >/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}$(dt)${IDEBUG} ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$1 " >&2
|
|
else
|
|
print "${DIM}${BOLD}${RESET}${DIM}$(dt)${IDEBUG} $1 " >&2
|
|
fi
|
|
}
|
|
|
|
var_dump() {
|
|
debug "$1 = ${!1}"
|
|
}
|
|
|
|
verbose() {
|
|
print "${BOLD}$(dt)${IVRB}${RESET}${FYELLOW} $1 "
|
|
}
|
|
|
|
info() {
|
|
print "${BOLD}$(dt)${FWHITE}${BLBLUE}${IINFO}${RESET}${FWHITE} $1 "
|
|
}
|
|
|
|
note() {
|
|
print "${BOLD}$(dt)${DIM}${FWHITE}${INOTE}${RESET} $1 "
|
|
}
|
|
|
|
success() {
|
|
print "${BOLD}$(dt)${BGREEN}${FWHITE}${ISUCCESS}${BRESET}$FGREEN $1 "
|
|
}
|
|
|
|
warn() {
|
|
print "${BOLD}$(dt)${BYELLOW}${FBLACK}${IWARN}${BRESET}${FYELLOW} Warning:${RESET} $1 "
|
|
}
|
|
|
|
error() {
|
|
print "${BOLD}$(dt)${BLRED}${FWHITE}${IERROR} Error: ${BRESET}${FLRED} $1 " >&2
|
|
}
|
|
|
|
fatal() {
|
|
print "${BOLD}$(dt)${BRED}${FWHITE}${IFATAL} FATAL: $1 " >&2
|
|
print_stacktrace
|
|
}
|
|
|
|
die() {
|
|
error "${1:-halted}"
|
|
exit ${2:-255}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
# var='test var_dump'
|
|
# var_dump var
|
|
# debug 'test debug'
|
|
# verbose 'test verbose'
|
|
# info 'test info'
|
|
# note 'test note'
|
|
# success 'test success'
|
|
# warn 'test warn'
|
|
# error 'test error'
|
|
# fatal 'test fatal'
|
|
# die 'test die'
|
|
|
|
########################################################
|
|
# 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 |