Last active
July 23, 2023 01:20
-
-
Save anthonyaxenov/2b9153cab406656bde04163aec6501d4 to your computer and use it in GitHub Desktop.
[SHELL] My bash helpers
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
######################################################################### | |
# # | |
# 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 | |
######################################################## | |
# https://askubuntu.com/a/30414 | |
is_full_screen() { | |
WINDOW=$(echo $(xwininfo -id $(xdotool getactivewindow) -stats | \ | |
egrep '(Width|Height):' | \ | |
awk '{print $NF}') | \ | |
sed -e 's/ /x/') | |
SCREEN=$(xdpyinfo | grep -m1 dimensions | awk '{print $2}') | |
if [ "$WINDOW" = "$SCREEN" ]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment