tools -> scripts

This commit is contained in:
2025-11-14 10:44:57 +08:00
parent 0493f18b18
commit c7449f4acb
70 changed files with 369 additions and 2 deletions

View File

@@ -0,0 +1,111 @@
# Argument parser for bash scripts v1.6
## Usage
```shell
# 1. add these lines after shebang:
__RAW_ARGS__=("$@")
source args.sh
# 2. read arguments as flags:
arg a 1 flag_a
echo "Flag -a has value '$flag_a'"
echo "Flag -a has value '$(arg a 1)'"
arg b 1 flag_b
echo "Flag -b has value '$flag_b'"
echo "Flag -b has value '$(arg b 1)'"
arg c 1 flag_c
echo "Flag -c has value '$flag_c'"
echo "Flag -c has value '$(arg c 1)'"
arg d 1 flag_d
echo "Flag -d has value '$flag_d'"
echo "Flag -d has value '$(arg d 1)'"
argl flag1 1 flag_1
echo "Flag --flag1 has value '$flag_1'"
echo "Flag --flag1 has value '$(argl flag1 1)'"
argl flag2 1 flag_2
echo "Flag --flag2 has value '$flag_2'"
echo "Flag --flag2 has value '$(argl flag2 1)'"
# 3. and/or read arguments' values:
arg a 0 arg_a
echo "Arg -a has value '$arg_a'"
echo "Arg -a has value '$(arg a 0)'"
arg b 0 arg_b
echo "Arg -b has value '$arg_b'"
echo "Arg -b has value '$(arg b 0)'"
argl arg1 0 arg_1
echo "Arg --arg1 has value '$arg_1'"
echo "Arg --arg1 has value '$(argl arg1 0)'"
argl arg2 0 arg_2
echo "Arg --arg2 has value '$arg_2'"
echo "Arg --arg2 has value '$(argl arg2 0)'"
```
## How it works
1. Short arguments can be specified contiguously or separately
and their order does not matter, but before each of them
(or the first of them) one leading dash must be specified.
> Valid combinations: `-a -b -c`, `-cba`, `-b -azc "value of z"`
2. Short arguments can have values and if are - value must go
next to argument itself.
> Valid combinations: `-ab avalue`, `-ba avalue`, `-a avalue -b`
3. Long arguments cannot be combined like short ones and each
of them must be specified separately with two leading dashes.
> Valid combinations: `--foo --bar`, `--bar --foo`
4. Long arguments can have a value which must be specified after `=`.
> Valid combinations: `--foo value --bar`, `--bar --foo=value`
5. If arg value may contain space then value must be "double-quoted".
6. You can use arg() or argl() to check presence of any arg, no matter
if it has value or not.
More info:
* 🇷🇺 [axenov.dev/bash-args](https://axenov.dev/bash-args/)
* 🇺🇸 [axenov.dev/en/bash-processing-arguments-in-a-script-when-called-from-the-shell/](https://axenov.dev/en/bash-processing-arguments-in-a-script-when-called-from-the-shell)
Tested in Ubuntu 20.04.2 LTS in:
```
bash 5.0.17(1)-release (x86_64-pc-linux-gnu) and later
zsh 5.8 (x86_64-ubuntu-linux-gnu) and later
```
## Version history
```
v1.0 - initial
v1.1 - arg(): improved skipping uninteresting args
- arg(): check next arg to be valid value
v1.2 - removed all 'return' statements
- arg(): error message corrected
- new examples
v1.3 - argl(): improved flag check
- some text corrections
v1.4 - new function argn()
- some text corrections
v1.5 - arg(), grep_match(): fixed searching for -e argument
- grep_match(): redirect output into /dev/null
v1.6 - removed useless argn()
- arg() and argl() refactored and now support values with whitespaces
```

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Argument parser for bash scripts
#
# Author: Anthony Axenov (Антон Аксенов)
# Version: 1.6
# License: MIT
# Description: https://git.axenov.dev/anthony/shell/src/branch/master/helpers/arg-parser
#purpose Little helper to check if string matches PCRE
#argument $1 - some string
#argument $2 - regex
#exitcode 0 - string valid
#exitcode 1 - string is not valid
grep_match() {
printf "%s" "$1" | grep -qE "$2" >/dev/null
}
#purpose Find short argument or its value
#argument $1 - (string) argument (without leading dashes; only first letter will be processed)
#argument $2 - (number) is it flag? 1 if is, otherwise 0 or nothing
#argument $3 - (string) variable to return value into
# (if not specified then it will be echo'ed in stdout)
#returns (string) 1 (if $2 == 1), value (if correct and if $2 != 1) or nothing
#usage To get value into var: arg v 0 myvar or myvalue=$(arg 'v')
#usage To find flag into var: arg f 1 myvar or flag=$(arg 'f')
#usage To echo value: arg v
#usage To echo 1 if flag exists: arg f
arg() {
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="${1:0:1}" # first character of argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
local value= # initialize empty value to check if we found one later
local arg_found=0 # marker of found argument
for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg_search=${__RAW_ARGS__[idx]} # get current argument
# skip $arg_search if it starts with '--' or letter
grep_match "$arg_search" "^(\w|--)" && continue
# clear $arg_search from special and duplicate characters, e.g. 'fas-)dfs' will become 'fasd'
local arg_chars="$(printf "%s" "$arg_search" \
| tr -s "[$arg_search]" 2>/dev/null \
| tr -d "[:punct:][:blank:]" 2>/dev/null)"
# if $arg_name is not one of $arg_chars the skip it
grep_match "-$arg_name" "^-[$arg_chars]$" || continue
arg_found=1
# then return '1'|'0' back into $value if we need flag or next arg value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
break
done
[ "$is_flag" = 1 ] && [ -z "$value" ] && value=0;
# if value we found is empty or looks like another argument then exit with error message
if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
echo "ERROR: Argument '-$arg_name' must have correct value!" >&2 && exit 1
fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
}
#purpose Find long argument or its value
#argument $1 - argument (without leading dashes)
#argument $2 - (number) is it flag? 1 if is, otherwise 0 or nothing
#argument $3 - (string) variable to return value into
# (if not specified then it will be echo'ed in stdout)
#returns (string) 1 (if $2 == 1), value (if correct and if $2 != 1) or nothing
#usage To get value into var: arg v 0 myvar or myvalue=$(arg 'v')
#usage To find flag into var: arg f 1 myvar or flag=$(arg 'f')
#usage To echo value: arg v
#usage To echo 1 if flag exists: arg f
argl() {
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="$1" # argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
local value= # initialize empty value to check if we found one later
local arg_found=0 # marker of found argument
for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg_search="${__RAW_ARGS__[idx]}" # get current argument
if [ "$arg_search" = "--$arg_name" ]; then # if current arg begins with two dashes
# then return '1' back into $value if we need flag or next arg value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
break # stop the loop
elif grep_match "$arg_search" "^--$arg_name=.+$"; then # check if $arg like '--foo=bar'
# then return '1' back into $value if we need flag or part from '=' to arg's end as value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${arg_search#*=}"
break # stop the loop
fi
done
[ "$is_flag" = 1 ] && [ -z "$value" ] && value=0;
# if value we found is empty or looks like another argument then exit with error message
if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
echo "ERROR: Argument '--$arg_name' must have correct value!" >&2 && exit 1;
fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
}
################################
# This is simple examples which you can play around with.
# 1. uncomment code below
# 2. call thi sscript to see what happens:
# /args.sh -abcd --flag1 --flag2 -e evalue -f fvalue --arg1=value1 --arg2 value2
# __RAW_ARGS__=("$@")
# arg a 1 flag_a
# echo "Flag -a has value '$flag_a'"
# echo "Flag -a has value '$(arg a 1)'"
# arg b 1 flag_b
# echo "Flag -b has value '$flag_b'"
# echo "Flag -b has value '$(arg b 1)'"
# arg c 1 flag_c
# echo "Flag -c has value '$flag_c'"
# echo "Flag -c has value '$(arg c 1)'"
# arg d 1 flag_d
# echo "Flag -d has value '$flag_d'"
# echo "Flag -d has value '$(arg d 1)'"
# argl flag1 1 flag_1
# echo "Flag --flag1 has value '$flag_1'"
# echo "Flag --flag1 has value '$(argl flag1 1)'"
# argl flag2 1 flag_2
# echo "Flag --flag2 has value '$flag_2'"
# echo "Flag --flag2 has value '$(argl flag2 1)'"
# arg e 0 arg_e
# echo "Arg -e has value '$arg_e'"
# echo "Arg -e has value '$(arg e 0)'"
# arg f 0 arg_f
# echo "Arg -f has value '$arg_f'"
# echo "Arg -f has value '$(arg f 0)'"
# argl arg1 0 arg_1
# echo "Arg --arg1 has value '$arg_1'"
# echo "Arg --arg1 has value '$(argl arg1 0)'"
# argl arg2 0 arg_2
# echo "Arg --arg2 has value '$arg_2'"
# echo "Arg --arg2 has value '$(argl arg2 0)'"

127
scripts/helpers/basic.sh Normal file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
source $( dirname $(readlink -e -- "${BASH_SOURCE}"))/io.sh || exit 255
########################################################
# Little handy helpers for scripting
########################################################
is_bash() {
[[ "$(basename "$SHELL")" != "bash" ]]
}
is_sourced() {
[[ "${BASH_SOURCE[0]}" != "$0" ]]
}
is_root() {
[[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]
}
get_os() {
case "$(uname -s)" in
Linux*) echo Linux ;;
Darwin*) echo Macos ;;
CYGWIN*) echo Cygwin ;;
MINGW*) echo MinGw ;;
MSYS_NT*) echo Git ;;
*) return 1 ;;
esac
}
get_os_id() {
[ -f /etc/os-release ] && source /etc/os-release
echo "$ID"
}
# convert relative path $1 to full one
abspath() {
echo $(realpath -q "${1/#\~/$HOME}")
}
# check if path $1 is writable
is_writable() {
[ -w "$(abspath $1)" ]
}
# check if path $1 is a directory
is_dir() {
[ -d "$(abspath $1)" ]
}
# check if path $1 is a file
is_file() {
[ -f "$(abspath $1)" ]
}
# check if an argument is a shell function
is_function() {
declare -F "$1" > /dev/null
}
# check if string $1 matches regex $2
regex_match() {
printf "%s" "$1" | grep -qP "$2"
}
# check if array $2 contains string $1
in_array() {
local find=$1
shift
for e in "$@"; do
[[ "$e" == "$find" ]] && return 0
done
return 1
}
# join all elements of array $2 with delimiter $1
implode() {
local d=${1-}
local f=${2-}
if shift 2; then
printf %s "$f" "${@/#/$d}"
fi
}
# open url $1 in system web-browser
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
}
# unpack .tar.gz file $1 into path $2
unpack_targz() {
require tar
tar -xzvf "$1" -C "$2"
}
# make soft symbolic link of path $1 to path $2
symlink() {
ln -sf "$1" "$2"
}
# download file $1 into path $2 using wget
download() {
require wget
wget "$1" -O "$2"
}
# download file $1 into path $2 using curl
cdownload() {
require curl
curl -fsSL "$1" -o "$2"
}
is_int() {
[[ "$1" =~ ^[0-9]+$ ]]
}
is_number() {
[[ "$1" =~ ^[0-9]+([.,][0-9]+)?$ ]]
}
trim() {
echo "$1" | xargs
}

3
scripts/helpers/debug.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
source $( dirname $(readlink -e -- "${BASH_SOURCE}"))/io.sh || exit 255

79
scripts/helpers/docker.sh Normal file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
########################################################
# Docker wrappers
########################################################
# Вызывает корректную команду docker compose
docker.compose() {
require docker
argl profiles 0 profiles
args=${*/--profiles=[a-zA-Z_,0-9]*/}
if $(docker compose &>/dev/null); then
local cmd="docker compose $args"
elif installed_pkg "docker-compose"; then
local cmd="docker-compose $args"
warn
warn "docker-compose v1 устарел и не поддерживается, его поведение непредсказуемо."
warn "Обнови docker согласно документации: https://docs.docker.com/engine/install/"
warn
else
error "Должен быть установлен docker-compose-plugin!"
die "Установи docker согласно документации: https://docs.docker.com/engine/install/" 2
fi
if [[ "$profiles" ]]; then
export COMPOSE_PROFILES=$profiles
debug "Выполнено: export COMPOSE_PROFILES=$profiles"
fi
debug "Команда: $cmd"
$cmd
}
# Выводит информацию о контейнере
docker.inspect() {
cmd="docker inspect $*"
debug "Команда: $cmd"
$cmd 2>/dev/null
}
# Выполняет команду в контейнере от имени root
docker.exec() {
cmd="docker exec -u root -it $*"
debug "Команда: $cmd"
$cmd
}
# Выводит информацию о контейнере
docker.inspect() {
cmd="docker inspect $*"
debug "Команда: $cmd"
$cmd 2>/dev/null
}
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
}

178
scripts/helpers/git.sh Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
_dir=$( dirname $(readlink -e -- "${BASH_SOURCE}"))
source "$_dir/io.sh" || exit 255
source "$_dir/basic.sh" || exit 255
source "$_dir/packages.sh" || exit 255
########################################################
# Shorthands for git
########################################################
git.clone_quick() {
require git
git clone --depth=1 --single-branch "$@"
}
git.is_repo() {
require git
[ "$1" ] || die "Path is not specified" 101
require_dir "$1/"
check_dir "$1/.git"
}
git.require_repo() {
require git
git.is_repo "$1" || die "'$1' is not git repository!" 10
}
git.cfg() {
require git
[ "$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() {
require git
[ "$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() {
require git
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() {
require git
git reset --hard HEAD
git clean -fd
}
git.clone() {
require git
git clone "$*" 2>&1
}
git.co() {
require git
git checkout "$*" 2>&1
}
git.is_it_current_branch() {
require git
[ "$1" ] || die "git.is_it_current_branch: Branch is not specified" 19
[[ "$(git.current_branch)" = "$1" ]]
}
git.pull() {
require git
[ "$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() {
require git
git branch --show-current || exit 18
}
git.local_branch_exists() {
require git
[ -n "$(git for-each-ref --format='%(refname:short)' refs/heads/$1)" ]
}
git.update_refs() {
require git
info "Updating local refs..."
git remote update origin --prune 1>/dev/null 2>&1 || exit 18
}
git.delete_remote_branch() {
require git
[ "$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() {
require git
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() {
require git
[ "$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() {
require git
[ "$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() {
require git
[ "$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() {
require git
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
}

28
scripts/helpers/help.sh Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
#TODO source basic.sh
#TODO source args-parser/args.sh
########################################################
# Help functions
########################################################
process_help_arg() {
command="${FUNCNAME[1]}"
need_help=$(arg help 1)
[[ "$need_help" -eq 0 ]] && need_help=$(argl help 1)
[[ "$need_help" -eq 1 ]] && help "$command"
}
help() {
is_function "help.$1" && help."$1" && exit
echo "Main help message"
}
help.example() {
echo "Example help message"
}
example() {
process_help_arg
echo "Example command"
}

393
scripts/helpers/io.sh Normal file
View File

@@ -0,0 +1,393 @@
#!/usr/bin/env bash
########################################################
# Simple and fancy input & output
########################################################
which tput > /dev/null 2>&1 && [[ $(tput -T$TERM colors) -gt 8 ]] && CAN_USE_COLORS=1 || CAN_USE_COLORS=0
USE_COLORS=${USE_COLORS:-$CAN_USE_COLORS}
# Icons (message prefixes)
[[ "$USE_COLORS" == 1 ]] && IINFO="( i )" || IINFO=''
[[ "$USE_COLORS" == 1 ]] && INOTE="( * )" || INOTE=''
[[ "$USE_COLORS" == 1 ]] && IWARN="( # )" || IWARN=''
[[ "$USE_COLORS" == 1 ]] && IERROR="( ! )" || IERROR=''
[[ "$USE_COLORS" == 1 ]] && IFATAL="( @ )" || IFATAL=''
[[ "$USE_COLORS" == 1 ]] && ISUCCESS="( ! )" || ISUCCESS=''
[[ "$USE_COLORS" == 1 ]] && IASK="( ? )" || IASK=''
[[ "$USE_COLORS" == 1 ]] && IDEBUG="(DBG)" || IDEBUG=''
[[ "$USE_COLORS" == 1 ]] && IVRB="( + )" || IVRB=''
# Text attributes
[[ "$USE_COLORS" == 1 ]] && FRESET="$(tput sgr0)" || FRESET='' # Normal
[[ "$USE_COLORS" == 1 ]] && FBOLD="$(tput bold)" || FBOLD='' # Bold
[[ "$USE_COLORS" == 1 ]] && FDIM="$(tput dim)" || FDIM='' # Dimmed
[[ "$USE_COLORS" == 1 ]] && FLINE="$(tput smul)" || FLINE='' # Underlined
[[ "$USE_COLORS" == 1 ]] && FENDLINE="$(tput rmul)" || FENDLINE='' # End of underlined
[[ "$USE_COLORS" == 1 ]] && FBLINK="$(tput blink)" || FBLINK='' # Blink
[[ "$USE_COLORS" == 1 ]] && FREV="$(tput rev)" || FREV='' # Reversed
# Text colors - normal
[[ "$USE_COLORS" == 1 ]] && FBLACK="$(tput setaf 0)" || FBLACK='' # Black
[[ "$USE_COLORS" == 1 ]] && FRED="$(tput setaf 1)" || FRED='' # Red
[[ "$USE_COLORS" == 1 ]] && FGREEN="$(tput setaf 2)" || FGREEN='' # Green
[[ "$USE_COLORS" == 1 ]] && FYELLOW="$(tput setaf 3)" || FYELLOW='' # Yellow
[[ "$USE_COLORS" == 1 ]] && FBLUE="$(tput setaf 4)" || FBLUE='' # Blue
[[ "$USE_COLORS" == 1 ]] && FPURPLE="$(tput setaf 5)" || FPURPLE='' # Purple
[[ "$USE_COLORS" == 1 ]] && FCYAN="$(tput setaf 6)" || FCYAN='' # Cyan
[[ "$USE_COLORS" == 1 ]] && FWHITE="$(tput setaf 7)" || FWHITE='' # White
# Text colors - bright
[[ "$USE_COLORS" == 1 ]] && FLBLACK="$(tput setaf 8)" || FLBLACK='' # Black
[[ "$USE_COLORS" == 1 ]] && FLRED="$(tput setaf 9)" || FLRED='' # Red
[[ "$USE_COLORS" == 1 ]] && FLGREEN="$(tput setaf 10)" || FLGREEN='' # Green
[[ "$USE_COLORS" == 1 ]] && FLYELLOW="$(tput setaf 11)" || FLYELLOW='' # Yellow
[[ "$USE_COLORS" == 1 ]] && FLBLUE="$(tput setaf 12)" || FLBLUE='' # Blue
[[ "$USE_COLORS" == 1 ]] && FLPURPLE="$(tput setaf 13)" || FLPURPLE='' # Purple
[[ "$USE_COLORS" == 1 ]] && FLCYAN="$(tput setaf 14)" || FLCYAN='' # Cyan
[[ "$USE_COLORS" == 1 ]] && FLWHITE="$(tput setaf 15)" || FLWHITE='' # White
# Background colors - normal
[[ "$USE_COLORS" == 1 ]] && FBBLACK="$(tput setab 0)" || FBBLACK='' # Black
[[ "$USE_COLORS" == 1 ]] && FBRED="$(tput setab 1)" || FBRED='' # Red
[[ "$USE_COLORS" == 1 ]] && FBGREEN="$(tput setab 2)" || FBGREEN='' # Green
[[ "$USE_COLORS" == 1 ]] && FBYELLOW="$(tput setab 3)" || FBYELLOW='' # Yellow
[[ "$USE_COLORS" == 1 ]] && FBBLUE="$(tput setab 4)" || FBBLUE='' # Blue
[[ "$USE_COLORS" == 1 ]] && FBPURPLE="$(tput setab 5)" || FBPURPLE='' # Purple
[[ "$USE_COLORS" == 1 ]] && FBCYAN="$(tput setab 6)" || FBCYAN='' # Cyan
[[ "$USE_COLORS" == 1 ]] && FBWHITE="$(tput setab 7)" || FBWHITE='' # White
# Background colors - bright
[[ "$USE_COLORS" == 1 ]] && FBLBLACK="$(tput setab 8)" || FBLBLACK='' # Black
[[ "$USE_COLORS" == 1 ]] && FBLRED="$(tput setab 9)" || FBLRED='' # Red
[[ "$USE_COLORS" == 1 ]] && FBLGREEN="$(tput setab 10)" || FBLGREEN='' # Green
[[ "$USE_COLORS" == 1 ]] && FBLYELLOW="$(tput setab 11)" || FBLYELLOW='' # Yellow
[[ "$USE_COLORS" == 1 ]] && FBLBLUE="$(tput setab 12)" || FBLBLUE='' # Blue
[[ "$USE_COLORS" == 1 ]] && FBLPURPLE="$(tput setab 13)" || FBLPURPLE='' # Purple
[[ "$USE_COLORS" == 1 ]] && FBLCYAN="$(tput setab 14)" || FBLCYAN='' # Cyan
[[ "$USE_COLORS" == 1 ]] && FBLWHITE="$(tput setab 15)" || FBLWHITE='' # White
now() {
echo "[$(date +'%H:%M:%S')] "
}
ask() {
IFS= read -rp "$(print ${FBOLD}${FBBLUE}${FWHITE}${IASK}${FRESET}\ ${FBOLD}$1 ): " $2
}
print() {
# if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi
echo -e "$*${FRESET}"
}
link() {
echo -e "\e]8;;$2\a$1\e]8;;\a"
}
debug() {
if [ "$2" ]; then
print "${FDIM}${FBOLD}${FRESET}${FDIM}$(now)${IDEBUG} ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$1 " >&2
else
print "${FDIM}${FBOLD}${FRESET}${FDIM}$(now)${IDEBUG} $1 " >&2
fi
}
var_dump() {
debug "$1 = ${!1}"
}
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
}
verbose() {
print "${FBOLD}$(now)${IVRB}${FRESET}${FYELLOW} $1 "
}
info() {
print "${FBOLD}$(now)${FWHITE}${FBLBLUE}${IINFO}${FRESET}${FWHITE} $1 "
}
note() {
print "${FBOLD}$(now)${FDIM}${FWHITE}${INOTE}${FRESET} $1 "
}
success() {
print "${FBOLD}$(now)${FBGREEN}${FWHITE}${ISUCCESS}${FRESET}$FGREEN $1 "
}
warn() {
print "${FBOLD}$(now)${FBYELLOW}${FBLACK}${IWARN}${FRESET}${FYELLOW} Warning:${FRESET} $1 "
}
error() {
print "${FBOLD}$(now)${FBLRED}${FWHITE}${IERROR} Error: ${FRESET}${FLRED} $1 " >&2
}
fatal() {
print "${FBOLD}$(now)${FBRED}${FWHITE}${IFATAL} FATAL: $1 " >&2
print_stacktrace
}
die() {
error "${1:-halted}"
exit ${2:-255}
}
# 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'
# experiments ==============================================================================
# spinner() {
# local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
# local spin_i=0
# local interval=0.1
# printf "\e[?25l"
# local color="${FGREEN}"
# while true; do
# printf "\r ${color}%s${CL}" "${frames[spin_i]}"
# spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
# sleep "$interval"
# done
# }
# echo "lorem ipsum dolor sit amet"
# spinner &
# SPINNER_PID=$!
# ===========
# https://unix.stackexchange.com/a/269085
# https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
# https://linuxcommand.org/lc3_adv_tput.php
# https://gist.github.com/nowmilano/4055d6df5b6e4ea87c5a72dc2d604193
# https://gist.github.com/nick3499/402a6d7dccd474f2bdb679f4311b1238
# https://gist.github.com/connorjan/2b02126868157c2b69f9aa0a052cdc86
# tput setaf 0
# echo "BLACK FOREGROUND"
# tput setaf 1
# echo "RED FOREGROUND"
# tput setaf 2
# echo "GREEN FOREGROUND"
# tput setaf 3
# echo "YELLOW FOREGROUND"
# tput setaf 4
# echo "BLUE FOREGROUND"
# tput setaf 5
# echo "MAGENTA FOREGROUND"
# tput setaf 6
# echo "CYAN FOREGROUND"
# tput setaf 7
# echo "WHITE FOREGROUND"
# tput reset
# ===========
# ===========
# tohex(){
# dec=$(($1%256)) ### input must be a number in range 0-255.
# if [ "$dec" -lt "16" ]; then
# bas=$(( dec%16 ))
# mul=128
# [ "$bas" -eq "7" ] && mul=192
# [ "$bas" -eq "8" ] && bas=7
# [ "$bas" -gt "8" ] && mul=255
# a="$(( (bas&1) *mul ))"
# b="$(( ((bas&2)>>1)*mul ))"
# c="$(( ((bas&4)>>2)*mul ))"
# printf 'dec= %3s basic= #%02x%02x%02x\n' "$dec" "$a" "$b" "$c"
# elif [ "$dec" -gt 15 ] && [ "$dec" -lt 232 ]; then
# b=$(( (dec-16)%6 )); b=$(( b==0?0: b*40 + 55 ))
# g=$(( (dec-16)/6%6)); g=$(( g==0?0: g*40 + 55 ))
# r=$(( (dec-16)/36 )); r=$(( r==0?0: r*40 + 55 ))
# printf 'dec= %3s color= #%02x%02x%02x\n' "$dec" "$r" "$g" "$b"
# else
# gray=$(( (dec-232)*10+8 ))
# printf 'dec= %3s gray= #%02x%02x%02x\n' "$dec" "$gray" "$gray" "$gray"
# fi
# }
# for i in $(seq 0 255); do
# tohex ${i}
# done
# ===========
# fromhex(){
# hex=${1#"#"}
# r=$(printf '0x%0.2s' "$hex")
# g=$(printf '0x%0.2s' ${hex#??})
# b=$(printf '0x%0.2s' ${hex#????})
# printf '%03d' "$(( (r<75?0:(r-35)/40)*6*6 +
# (g<75?0:(g-35)/40)*6 +
# (b<75?0:(b-35)/40) + 16 ))"
# }
# fromhex 00fc7b
# ===========
# mode2header(){
# #### For 16 Million colors use \e[0;38;2;R;G;Bm each RGB is {0..255}
# printf '\e[mR\n' # reset the colors.
# printf '\n\e[m%59s\n' "Some samples of colors for r;g;b. Each one may be 000..255"
# printf '\e[m%59s\n' "for the ansi option: \e[0;38;2;r;g;bm or \e[0;48;2;r;g;bm :"
# }
# mode2colors(){
# # foreground or background (only 3 or 4 are accepted)
# local fb="$1"
# [[ $fb != 3 ]] && fb=4
# local samples=(0 63 127 191 255)
# for r in "${samples[@]}"; do
# for g in "${samples[@]}"; do
# for b in "${samples[@]}"; do
# printf '\e[0;%s8;2;%s;%s;%sm%03d;%03d;%03d ' "$fb" "$r" "$g" "$b" "$r" "$g" "$b"
# done; printf '\e[m\n'
# done; printf '\e[m'
# done; printf '\e[mReset\n'
# }
# mode2header
# mode2colors 3
# mode2colors 4
# ===========
# printf '\e[48;5;%dm ' {0..255}; printf '\e[0m \n'
# for r in {200..255..5}; do
# fb=4
# g=1
# b=1
# printf '\e[0;%s8;2;%s;%s;%sm ' "$fb" "$r" "$g" "$b"
# done
# echo
# ===========
# color(){
# for c; do
# printf '\e[48;5;%dm%03d' $c $c
# done
# printf '\e[0m \n'
# }
# IFS=$' \t\n'
# color {0..15}
# for ((i=0;i<6;i++)); do
# color $(seq $((i*36+16)) $((i*36+51)))
# done
# color {232..255}
# ===========
# for ((i=0; i<256; i++)) ;do
# echo -n ' '
# tput setab $i
# tput setaf $(( ( (i>231&&i<244 ) || ( (i<17)&& (i%8<2)) ||
# (i>16&&i<232)&& ((i-16)%6 <(i<100?3:2) ) && ((i-16)%36<15) )?7:16))
# printf " C %03d " $i
# tput op
# (( ((i<16||i>231) && ((i+1)%8==0)) || ((i>16&&i<232)&& ((i-15)%6==0)) )) &&
# printf "\n" ''
# done
# ===========
# echo "tput character test"
# echo "==================="
# echo
# tput bold; echo "This text has the bold attribute."; tput sgr0
# tput smul; echo "This text is underlined (smul)."; tput rmul
# # Most terminal emulators do not support blinking text (though xterm
# # does) because blinking text is considered to be in bad taste ;-)
# tput blink; echo "This text is blinking (blink)."; tput sgr0
# tput rev; echo "This text has the reverse attribute"; tput sgr0
# # Standout mode is reverse on many terminals, bold on others.
# tput smso; echo "This text is in standout mode (smso)."; tput rmso
# tput sgr0
# echo
# experiments ==============================================================================
# function delay_spinner(){
# ##
# ## Usage:
# ##
# ## $ long-running-command &
# ## $ delay_spinner " Please wait msg..."
# ##
# ## Spinner exists when long-running-command completes
# ##
# local PROGRESSTXT
# if [ ! "$1" ]; then
# PROGRESSTXT=" Please wait..."
# else
# PROGRESSTXT="$1"
# fi
# # visual progress marker function
# # http://stackoverflow.com/users/2869509/wizurd
# # vars
# local pid=$!
# local delay=0.1
# local spinstr='|/-\'
# echo -e "\n\n"
# while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
# local temp=${spinstr#?}
# printf "\r$PROGRESSTXT[%c] " "$spinstr"
# local spinstr=$temp${spinstr%"$temp"}
# sleep $delay
# printf "\b\b\b\b\b\b"
# done
# printf -- '\n\n'
# #
# # <-- end function ec2cli_spinner -->
# #
# }
# sleep 10 && echo 'test' &
# delay_spinner "Please wait msg..."

13
scripts/helpers/log.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
########################################################
# Logging functions
########################################################
# write some message $1 in log file and stdout with timestamp
log_path="/home/$USER/logs"
log() {
[ ! -d "$log_path" ] && log_path="./log"
[ ! -d "$log_path" ] && mkdir -p "$log_path"
echo -e "[$(date '+%H:%M:%S')] $*" | tee -a "$log_path/$(date '+%Y%m%d').log"
}

29
scripts/helpers/misc.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
########################################################
# Misc
########################################################
# https://askubuntu.com/a/30414
is_full_screen() {
local WINDOW=$(xwininfo -id "$(xdotool getactivewindow)" -stats \
| grep -E '(Width|Height):' \
| awk '{print $NF}' \
| sed -e 's/ /x/')
local SCREEN=$(xdpyinfo | grep -m1 dimensions | awk '{print $2}')
if [ "$WINDOW" = "$SCREEN" ]; then
return 0
fi
return 1
}
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" \
"$@"
}

51
scripts/helpers/net.sh Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
########################################################
# Networking functions
########################################################
get_current_ip() {
local CURRENT_IP
[ -f /etc/os-release ] && source /etc/os-release
case "$ID" in
debian|ubuntu) CURRENT_IP=$(hostname -I | awk '{print $1}') ;;
alpine) CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) ;;
*) CURRENT_IP="Unknown" ;;
esac
echo "$CURRENT_IP"
}
get_external_ip() {
local ip="$(curl -s https://api.myip.com | jq .ip)"
echo "$ip" | tr -d '"'
}
is_valid_ipv4() {
local ip="$1"
local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
if [[ $ip =~ $regex ]]; then
IFS='.' read -r -a parts <<< "$ip"
for part in "${parts[@]}"; do
if ! [[ $part =~ ^[0-9]+$ ]] || ((part < 0 || part > 255)); then
return 1
fi
done
return 0
fi
return 1
}
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
}

63
scripts/helpers/notif.sh Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
########################################################
# Notifications
########################################################
TITLE="$0"
NTFY_CHANNEL="example"
# отправляет простую нотификацию
ntfy_info() {
require ntfy
ntfy send \
--title "$TITLE" \
--message "$1" \
--priority 1 \
"$NTFY_CHANNEL"
}
# отправляет нотификацию с предупреждением
ntfy_warn() {
require ntfy
ntfy send \
--title "$TITLE" \
--tags "warning" \
--message "$1" \
--priority 5 \
"$NTFY_CHANNEL"
}
notify () {
if ! installed "notify-send"; then
warning "Notifications toggled on, but 'notify-send' is not installed!"
return 1
fi
[ -n "$1" ] && local title="$1"
local text="$2"
local level="$3"
local icon="$4"
case "$level" in
critical) local timeout=0 ;;
low) local timeout=5000 ;;
*) local timeout=10000 ;;
esac
debug "$title / $text / $level / $icon / $timeout"
notify-send "$title" "$text" -a "$0" -u "$level" -i "$icon" -t $timeout
}
# TODO: docblock
notify_error() {
notify "Error" "$1" "critical" "dialog-error"
}
# TODO: docblock
notify_warning() {
notify "Warning" "$1" "normal" "dialog-warning"
}
# TODO: docblock
notify_info() {
notify "" "$1" "low" "dialog-information"
}

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
source $( dirname $(readlink -e -- "${BASH_SOURCE}"))/packages.sh || exit 255
########################################################
# 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"
}

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
source $( dirname $(readlink -e -- "${BASH_SOURCE}"))/io.sh || exit 255
########################################################
# Functions to control system packages
########################################################
installed() {
command -v "$1" >/dev/null 2>&1
}
installed_pkg() {
dpkg --list | grep -qw "ii $1"
}
apt_ppa_add() {
sudo add-apt-repository -y $*
}
apt_ppa_remove() {
sudo add-apt-repository -ry $*
}
apt_update() {
sudo apt update $*
}
apt_install() {
sudo apt install -y $*
}
apt_remove() {
sudo apt purge -y $*
}
dpkg_install() {
sudo dpkg -i $*
}
dpkg_remove() {
sudo dpkg -r $*
}
dpkg_arch() {
dpkg --print-architecture
}
require() {
sw=()
for package in "$@"; do
if ! installed "$package" && ! installed_pkg "$package"; then
sw+=("$package")
fi
done
if [ ${#sw[@]} -gt 0 ]; then
info "These packages will be installed in your system:\n${sw[*]}"
apt_install ${sw[*]}
[ $? -gt 0 ] && die "installation cancelled" 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
die "These packages must be installed in your system:\n${sw[*]}" 200
fi
}

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
source $( dirname $(readlink -e -- "${BASH_SOURCE}"))/io.sh || exit 255
########################################################
# Testing functions
########################################################
# $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

21
scripts/helpers/traps.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
#TODO source basic.sh
#TODO source args-parser/args.sh
########################################################
# Trap usage examples
########################################################
# for sig in SIGHUP SIGINT SIGQUIT SIGABRT SIGKILL SIGTERM SIGTSTP; do
# # shellcheck disable=SC2064
# trap "set +x && echo && echo && echo '*** Прервано сигналом $sig, остановка ***' && exit" $sig
# done
for sig in SIGHUP SIGINT SIGQUIT SIGABRT SIGKILL SIGTERM SIGTSTP; do
trap "myfunc" $sig
done
myfunc() {
echo "trapped!"
exit
}