mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Add new versions of helpers. Based on bash-modules.
This commit is contained in:
parent
c1b3c3f785
commit
8b9a02539e
20 changed files with 2746 additions and 0 deletions
169
helpers/helpers.v2.d/apps
Normal file
169
helpers/helpers.v2.d/apps
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
ynh::apps::list() {
|
||||||
|
yunohost app list --output-as json --quiet | jq -r '.apps[].id'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Install others YunoHost apps
|
||||||
|
#
|
||||||
|
# usage: ynh_install_apps --apps="appfoo?domain=domain.foo&path=/foo appbar?domain=domain.bar&path=/bar&admin=USER&language=fr&is_public=1&pass?word=pass&port=666"
|
||||||
|
# | arg: -a, --apps= - apps to install
|
||||||
|
#
|
||||||
|
# Requires YunoHost version *.*.* or higher.
|
||||||
|
ynh::apps::install() {
|
||||||
|
local -a apps=()
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--apps)apps;Array,R" \
|
||||||
|
-- "${@+$@}"
|
||||||
|
|
||||||
|
local -a installed_apps
|
||||||
|
mapfile -t installed_apps < <(ynh::apps::list)
|
||||||
|
|
||||||
|
for app_and_args in "${apps[@]}"; do
|
||||||
|
local app_name app_args
|
||||||
|
app_name="$(cut -d "?" -f1 <<< "$app_and_args")"
|
||||||
|
app_args="$(cut -d "?" -f2- -s <<< "$app_and_args")"
|
||||||
|
if string::empty "$app_name"; then
|
||||||
|
log::panic "You didn't provided a YunoHost app to install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
yunohost tools update apps
|
||||||
|
|
||||||
|
if list::contains "$app_name" "${installed_apps[@]}"; then
|
||||||
|
# App already installed, upgrade it
|
||||||
|
yunohost app upgrade "$app_name"
|
||||||
|
else
|
||||||
|
# Install the app with its arguments
|
||||||
|
yunohost app install "$app_name" "$app_args"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ynh::setting::set --key "apps_dependencies" --value "$(array::join ", " "${apps[@]}")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove other YunoHost apps
|
||||||
|
#
|
||||||
|
# Other YunoHost apps will be removed only if no other apps need them.
|
||||||
|
#
|
||||||
|
# usage: ynh_remove_apps
|
||||||
|
#
|
||||||
|
# Requires YunoHost version *.*.* or higher.
|
||||||
|
ynh::apps::remove_deps() {
|
||||||
|
local -a to_remove
|
||||||
|
string::split_to to_remove ", " "$(ynh::setting::get --key "apps_dependencies")"
|
||||||
|
ynh::setting::delete --key "apps_dependencies"
|
||||||
|
|
||||||
|
# Make the array of reverse dependencies
|
||||||
|
local -A reverse_deps
|
||||||
|
for app in $(ynh::apps::list); do
|
||||||
|
local -a app_deps
|
||||||
|
string::split_to app_deps ", " "$(ynh::setting::get --app="$app" --key=apps_dependencies)"
|
||||||
|
for dep in "${app_deps[@]}"; do
|
||||||
|
reverse_deps[$dep]="${reverse_deps[$dep]}, $app"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Now remove all deps that have no other reverse dependencies
|
||||||
|
for app in "${to_remove[@]}"; do
|
||||||
|
if string::empty "${reverse_deps[$app]}"; then
|
||||||
|
log::info "Removing $app..."
|
||||||
|
yunohost app remove "$app" --purge
|
||||||
|
else
|
||||||
|
log::info "$app was not removed because it's still required by ${reverse_deps[$app]}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spawn a Bash shell with the app environment loaded
|
||||||
|
#
|
||||||
|
# usage: ynh_spawn_app_shell --app="app"
|
||||||
|
# | arg: -a, --app= - the app ID
|
||||||
|
#
|
||||||
|
# examples:
|
||||||
|
# ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"'
|
||||||
|
# ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher.
|
||||||
|
# The spawned shell will have environment variables loaded and environment files sourced
|
||||||
|
# from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting).
|
||||||
|
# If the app relies on a specific PHP version, then `php` will be aliased that version. The PHP command will also be appended with the `phpflags` settings.
|
||||||
|
ynh::apps::shell_into() {
|
||||||
|
local app
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--app)apps;String,R" \
|
||||||
|
-- "${@+$@}"
|
||||||
|
|
||||||
|
|
||||||
|
# Force Bash to be used to run this helper
|
||||||
|
if [[ ! "$0" =~ \/?bash$ ]]; then
|
||||||
|
log::panic "Please use Bash as shell."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure the app is installed
|
||||||
|
local installed_apps
|
||||||
|
mapfile -t installed_apps < <(ynh::apps::list)
|
||||||
|
if ! array::contains "$app" installed_apps; then
|
||||||
|
log::panic "$app is not in the apps list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure the app has its own user
|
||||||
|
if ! id -u "$app" &>/dev/null; then
|
||||||
|
log::panic "There is no '$app' system user"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure the app has an install_dir setting
|
||||||
|
local install_dir
|
||||||
|
install_dir="$(ynh::setting::get --app "$app" --key install_dir)"
|
||||||
|
if string::empty "$install_dir"; then
|
||||||
|
log::panic "$app has no install_dir setting (does it use packaging format >=2?)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load the app's service name, or default to $app
|
||||||
|
local service
|
||||||
|
service="$(ynh::setting::get --app "$app" --key service)"
|
||||||
|
if string::empty "$service"; then
|
||||||
|
service="$app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load the service variables
|
||||||
|
local -a env_vars
|
||||||
|
string::split_to env_vars " " "$(systemctl show "$service.service" -p "Environment" --value)"
|
||||||
|
|
||||||
|
local -a env_files
|
||||||
|
string::split_to env_files " " "$(systemctl show "$service.service" -p "EnvironmentFiles" --value)"
|
||||||
|
|
||||||
|
local env_dir
|
||||||
|
env_dir="$(systemctl show "$service.service" -p "WorkingDirectory" --value)"
|
||||||
|
|
||||||
|
# Force `php` to its intended version
|
||||||
|
# We use `eval`+`export` since `alias` is not propagated to subshells, even with `export`
|
||||||
|
local phpversion phpflags
|
||||||
|
phpversion="$(ynh::setting::get --app "$app" --key phpversion)"
|
||||||
|
phpflags="$(ynh::setting::get --app "$app" --key phpflags)"
|
||||||
|
if ! string::empty "$phpversion"; then
|
||||||
|
phpstring="php() { php${phpversion} ${phpflags} \"\$@\"; }"
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
export HOME="$install_dir"
|
||||||
|
for var in "${env_vars[@]}"; do
|
||||||
|
export "${var?}"
|
||||||
|
done
|
||||||
|
for file in "${env_files[@]}"; do
|
||||||
|
set -a
|
||||||
|
source "$file"
|
||||||
|
set +a
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! string::empty "$phpstring"; then
|
||||||
|
eval "${phpstring:-}"
|
||||||
|
export -f php
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! string::empty "$env_dir"; then
|
||||||
|
cd "$env_dir"
|
||||||
|
fi
|
||||||
|
su -s /bin/bash "$app"
|
||||||
|
)
|
||||||
|
}
|
301
helpers/helpers.v2.d/bash-modules/arguments.sh
Normal file
301
helpers/helpers.v2.d/bash-modules/arguments.sh
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `arguments` - contains function to parse arguments and assign option values to variables.
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `arguments::parse [-S|--FULL)VARIABLE;FLAGS[,COND]...]... -- [ARGUMENTS]...`
|
||||||
|
#>
|
||||||
|
#> Where:
|
||||||
|
#>
|
||||||
|
#> * `-S` - short option name.
|
||||||
|
#>
|
||||||
|
#> * `--FULL` - long option name.
|
||||||
|
#>
|
||||||
|
#> * `VARIABLE` - name of shell variable to assign value to.
|
||||||
|
#>
|
||||||
|
#> * `FLAGS` - one of (case sensitive):
|
||||||
|
#> * `Y | Yes` - set variable value to "yes";
|
||||||
|
#> * `No` - set variable value to "no";
|
||||||
|
#> * `I | Inc | Incremental` - incremental (no value) - increment variable value by one;
|
||||||
|
#> * `S | Str | String` - string value;
|
||||||
|
#> * `N | Num | Number` - numeric value;
|
||||||
|
#> * `A | Arr | Array` - array of string values (multiple options);
|
||||||
|
#> * `C | Com | Command` - option name will be assigned to the variable.
|
||||||
|
#>
|
||||||
|
#> * `COND` - post conditions:
|
||||||
|
#> * `R | Req | Required` - option value must be not empty after end of parsing.
|
||||||
|
#> Set initial value to empty value to require this option;
|
||||||
|
#> * any code - executed after option parsing to check post conditions, e.g. "(( FOO > 3 )), (( FOO > BAR ))".
|
||||||
|
#>
|
||||||
|
#> * -- - the separator between option descriptions and script commandline arguments.
|
||||||
|
#>
|
||||||
|
#> * `ARGUMENTS` - command line arguments to parse.
|
||||||
|
#>
|
||||||
|
#> **LIMITATION:** grouping of one-letter options is NOT supported. Argument `-abc` will be parsed as
|
||||||
|
#> option `-abc`, not as `-a -b -c`.
|
||||||
|
#>
|
||||||
|
#> **NOTE:** bash4 requires to use `"${@:+$@}"` to expand empty list of arguments in strict mode (`-u`).
|
||||||
|
#>
|
||||||
|
#> By default, function supports `-h|--help`, `--man` and `--debug` options.
|
||||||
|
#> Options `--help` and `--man` are calling `arguments::help()` function with `2` or `1` as
|
||||||
|
#> argument. Override that function if you want to provide your own help.
|
||||||
|
#>
|
||||||
|
#> Unlike many other parsers, this function stops option parsing at first
|
||||||
|
#> non-option argument.
|
||||||
|
#>
|
||||||
|
#> Use `--` in commandline arguments to strictly separate options and arguments.
|
||||||
|
#>
|
||||||
|
#> After option parsing, unparsed command line arguments are stored in
|
||||||
|
#> `ARGUMENTS` array.
|
||||||
|
#>
|
||||||
|
#> **Example:**
|
||||||
|
#>
|
||||||
|
#> ```bash
|
||||||
|
#> # Boolean variable ("yes" or "no")
|
||||||
|
#> FOO="no"
|
||||||
|
#> # String variable
|
||||||
|
#> BAR=""
|
||||||
|
#> # Indexed array
|
||||||
|
#> declare -a BAZ=( )
|
||||||
|
#> # Integer variable
|
||||||
|
#> declare -i TIMES=0
|
||||||
|
#>
|
||||||
|
#> arguments::parse \\
|
||||||
|
#> "-f|--foo)FOO;Yes" \\
|
||||||
|
#> "-b|--bar)BAR;String,Required" \\
|
||||||
|
#> "-B|--baz)BAZ;Array" \\
|
||||||
|
#> "-i|--inc)TIMES;Incremental,((TIMES<3))" \\
|
||||||
|
#> -- \\
|
||||||
|
#> "${@:+$@}"
|
||||||
|
#>
|
||||||
|
#> # Print name and value of variables
|
||||||
|
#> dbg FOO BAR BAZ TIMES ARGUMENTS
|
||||||
|
#> ```
|
||||||
|
arguments::parse() {
|
||||||
|
|
||||||
|
# Global array to hold command line arguments
|
||||||
|
ARGUMENTS=( )
|
||||||
|
|
||||||
|
# Local variables
|
||||||
|
local OPTION_DESCRIPTIONS PARSER
|
||||||
|
declare -a OPTION_DESCRIPTIONS
|
||||||
|
# Initialize array, because declare -a is not enough anymore for -u opt
|
||||||
|
OPTION_DESCRIPTIONS=( )
|
||||||
|
|
||||||
|
# Split arguments list at "--"
|
||||||
|
while [ $# -gt 0 ]
|
||||||
|
do
|
||||||
|
[ "$1" != "--" ] || {
|
||||||
|
shift
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
OPTION_DESCRIPTIONS[${#OPTION_DESCRIPTIONS[@]}]="$1" # Append argument to end of array
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate option parser and execute it
|
||||||
|
PARSER="$(arguments::generate_parser "${OPTION_DESCRIPTIONS[@]:+${OPTION_DESCRIPTIONS[@]}}")" || return 1
|
||||||
|
eval "$PARSER" || return 1
|
||||||
|
arguments::parse_options "${@:+$@}" || return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `arguments::generate_parser OPTIONS_DESCRIPTIONS` - generate parser for options.
|
||||||
|
#> Will create function `arguments::parse_options()`, which can be used to parse arguments.
|
||||||
|
#> Use `declare -fp arguments::parse_options` to show generated source.
|
||||||
|
arguments::generate_parser() {
|
||||||
|
|
||||||
|
local OPTION_DESCRIPTION OPTION_CASE OPTION_FLAGS OPTION_TYPE OPTION_OPTIONS OPTIONS_PARSER="" OPTION_POSTCONDITIONS=""
|
||||||
|
|
||||||
|
# Parse option description and generate code to parse that option from script arguments
|
||||||
|
while [ $# -gt 0 ]
|
||||||
|
do
|
||||||
|
# Parse option description
|
||||||
|
OPTION_DESCRIPTION="$1" ; shift
|
||||||
|
|
||||||
|
# Check option syntax
|
||||||
|
case "$OPTION_DESCRIPTION" in
|
||||||
|
*')'*';'*) ;; # OK
|
||||||
|
*)
|
||||||
|
log::error "Incorrect syntax of option: \"$OPTION_DESCRIPTION\". Option syntax: -S|--FULL)VARIABLE;TYPE[,CHECK]... . Example: '-f|--foo)FOO;String,Required'."
|
||||||
|
__log__DEBUG=yes; log::stacktrace
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OPTION_CASE="${OPTION_DESCRIPTION%%)*}" # Strip everything after first ')': --foo)BAR -> --foo
|
||||||
|
OPTION_VARIABLE="${OPTION_DESCRIPTION#*)}" # Strip everything before first ')': --foo)BAR -> BAR
|
||||||
|
OPTION_FLAGS="${OPTION_VARIABLE#*;}" # Strip everything before first ';': BAR;Baz -> Baz
|
||||||
|
OPTION_VARIABLE="${OPTION_VARIABLE%%;*}" # String everything after first ';': BAR;Baz -> BAR
|
||||||
|
|
||||||
|
IFS=',' read -a OPTION_OPTIONS <<<"$OPTION_FLAGS" # Convert string into array: 'a,b,c' -> [ a b c ]
|
||||||
|
OPTION_TYPE="${OPTION_OPTIONS[0]:-}" ; unset OPTION_OPTIONS[0] ; # First element of array is option type
|
||||||
|
|
||||||
|
# Generate the parser for option
|
||||||
|
case "$OPTION_TYPE" in
|
||||||
|
|
||||||
|
Y|Yes) # Set variable to "yes", no arguments
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
$OPTION_VARIABLE=\"yes\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
No) # Set variable to "no", no arguments
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
$OPTION_VARIABLE=\"no\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
C|Com|Command) # Set variable to name of the option, no arguments
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
$OPTION_VARIABLE=\"\$1\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
|
||||||
|
I|Incr|Incremental) # Incremental - any use of this option will increment variable by 1
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
let $OPTION_VARIABLE++ || :
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
S|Str|String) # Regular option with string value
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
$OPTION_VARIABLE=\"\${2:?ERROR: String value is required for \\\"$OPTION_CASE\\\" option. See --help for details.}\"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
${OPTION_CASE//|/=*|}=*)
|
||||||
|
$OPTION_VARIABLE=\"\${1#*=}\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
N|Num|Number) # Same as string
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
$OPTION_VARIABLE=\"\${2:?ERROR: Numeric value is required for \\\"$OPTION_CASE\\\" option. See --help for details.}\"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
${OPTION_CASE//|/=*|}=*)
|
||||||
|
$OPTION_VARIABLE=\"\${1#*=}\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
A|Arr|Array) # Array of strings
|
||||||
|
OPTIONS_PARSER="$OPTIONS_PARSER
|
||||||
|
$OPTION_CASE)
|
||||||
|
${OPTION_VARIABLE}[\${#${OPTION_VARIABLE}[@]}]=\"\${2:?Value is required for \\\"$OPTION_CASE\\\". See --help for details.}\"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
${OPTION_CASE//|/=*|}=*)
|
||||||
|
${OPTION_VARIABLE}[\${#${OPTION_VARIABLE}[@]}]=\"\${1#*=}\"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown option type: \"$OPTION_TYPE\"." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Parse option options, e.g "Required". Any other text is treated as condition, e.g. (( VAR > 10 && VAR < 20 ))
|
||||||
|
local OPTION_OPTION
|
||||||
|
for OPTION_OPTION in "${OPTION_OPTIONS[@]:+${OPTION_OPTIONS[@]}}"
|
||||||
|
do
|
||||||
|
case "$OPTION_OPTION" in
|
||||||
|
R|Req|Required)
|
||||||
|
OPTION_POSTCONDITIONS="$OPTION_POSTCONDITIONS
|
||||||
|
[ -n \"\$${OPTION_VARIABLE}\" ] || { echo \"ERROR: Option \\\"$OPTION_CASE\\\" is required. See --help for details.\" >&2; return 1; }
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
*) # Any other code after option type i
|
||||||
|
OPTION_POSTCONDITIONS="$OPTION_POSTCONDITIONS
|
||||||
|
$OPTION_OPTION || { echo \"ERROR: Condition for \\\"$OPTION_CASE\\\" option is failed. See --help for details.\" >&2; return 1; }
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
done
|
||||||
|
echo "
|
||||||
|
arguments::parse_options() {
|
||||||
|
# Global array to hold command line arguments
|
||||||
|
ARGUMENTS=( )
|
||||||
|
|
||||||
|
while [ \$# -gt 0 ]
|
||||||
|
do
|
||||||
|
case \"\$1\" in
|
||||||
|
# User options.
|
||||||
|
$OPTIONS_PARSER
|
||||||
|
|
||||||
|
# Built-in options.
|
||||||
|
-h|--help)
|
||||||
|
arguments::help 2
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--man)
|
||||||
|
arguments::help 1
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
log::enable_debug_mode
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift; break; # Do not parse rest of the command line arguments
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo \"ERROR: Unknown option: \\\"\$1\\\".\" >&2
|
||||||
|
arguments::help 3
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
break; # Do not parse rest of the command line
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ \$# -eq 0 ] || ARGUMENTS=( \"\$@\" ) # Store rest of the command line arguments into the ARGUMENTS array
|
||||||
|
$OPTION_POSTCONDITIONS
|
||||||
|
}
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `arguments::help LEVEL` - display embeded documentation.
|
||||||
|
#>> LEVEL - level of documentation:
|
||||||
|
#>> * 3 - summary (`#>>>` comments),
|
||||||
|
#>> * 2 - summary and usage ( + `#>>` comments),
|
||||||
|
#>> * 1 - full documentation (+ `#>` comments).
|
||||||
|
arguments::help() {
|
||||||
|
local LEVEL="${1:-3}"
|
||||||
|
case "$LEVEL" in
|
||||||
|
2|3) import::show_documentation "$LEVEL" "$IMPORT__BIN_FILE" ;;
|
||||||
|
1) import::show_documentation "$LEVEL" "$IMPORT__BIN_FILE" | less ;;
|
||||||
|
esac
|
||||||
|
}
|
113
helpers/helpers.v2.d/bash-modules/array.sh
Normal file
113
helpers/helpers.v2.d/bash-modules/array.sh
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> list - various functions to manipulate lists (passed as argument list).
|
||||||
|
#>>> array - various functions to manipulate arrays (passed as array names).
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
list::contain() {
|
||||||
|
local searching="$1"; shift
|
||||||
|
if [[ -z "$searching" ]] || [[ "$#" = 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for element in "$@"; do
|
||||||
|
if [[ "$searching" == "$element" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
array::contains() {
|
||||||
|
local searching="$1"; shift
|
||||||
|
declare -n __array_name="$1" ; shift
|
||||||
|
list::contain "$searching" "${__array_name[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
list::join() {
|
||||||
|
local sep="$1"; shift
|
||||||
|
while (( "$#" > 1 )); do
|
||||||
|
printf '%s%s' "$1" "$sep"
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
if [[ "$#" = 1 ]]; then
|
||||||
|
printf '%s' "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
array::join() {
|
||||||
|
local sep="$1"; shift
|
||||||
|
declare -n __array_name="$1"; shift
|
||||||
|
list::join "$sep" "${__array_name[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
list::min() {
|
||||||
|
local __min
|
||||||
|
if [[ "$#" = 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for value in "$@"; do
|
||||||
|
__min=$(echo "if($value<$__min) $value else $__min" | bc)
|
||||||
|
done
|
||||||
|
echo "$__min"
|
||||||
|
}
|
||||||
|
|
||||||
|
array::min() {
|
||||||
|
local sep="$1"; shift
|
||||||
|
declare -n __array_name="$1"; shift
|
||||||
|
list::min "$sep" "${__array_name[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
list::max() {
|
||||||
|
local __max
|
||||||
|
if [[ "$#" = 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for value in "$@"; do
|
||||||
|
__max=$(echo "if($value<$__max) $value else $__max" | bc)
|
||||||
|
done
|
||||||
|
echo "$__max"
|
||||||
|
}
|
||||||
|
|
||||||
|
array::max() {
|
||||||
|
local sep="$1"; shift
|
||||||
|
declare -n __array_name="$1"; shift
|
||||||
|
list::max "$sep" "${__array_name[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
array::sort() {
|
||||||
|
declare -n __array_name="$1" ; shift
|
||||||
|
local __sort_args=("$@")
|
||||||
|
|
||||||
|
printf '%s\n' "${__array_name[@]}" | sort "${__sort_args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
list::sort() {
|
||||||
|
local __array=("$@")
|
||||||
|
array::sort __array
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
list::uniq() {
|
||||||
|
printf "%s\n" "$@" | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
array::uniq() {
|
||||||
|
local sep="$1"; shift
|
||||||
|
declare -n __array_name="$1"; shift
|
||||||
|
list::uniq "$sep" "${__array_name[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
array::empty() {
|
||||||
|
declare -n __array_name="$1"; shift
|
||||||
|
(( ${#__array_name[@]} == 0 ))
|
||||||
|
}
|
33
helpers/helpers.v2.d/bash-modules/cd_to_bindir.sh
Normal file
33
helpers/helpers.v2.d/bash-modules/cd_to_bindir.sh
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
#>> # NAME
|
||||||
|
#>>> `cd_to_bindir` - change working directory to the directory where main script file is located.
|
||||||
|
#>>
|
||||||
|
#>> # DESCRIPTION
|
||||||
|
#>>
|
||||||
|
#>> Just import this cwdir module to change current working directory to a directory,
|
||||||
|
#>> where main script file is located.
|
||||||
|
|
||||||
|
# Get file name of the main source file
|
||||||
|
__CD_TO_BINDIR__BIN_FILE="${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}"
|
||||||
|
|
||||||
|
# If file name doesn't contains "/", then use `which` to find path to file name.
|
||||||
|
[[ "$__CD_TO_BINDIR__BIN_FILE" == */* ]] || __CD_TO_BINDIR__BIN_FILE=$( which "$__CD_TO_BINDIR__BIN_FILE" )
|
||||||
|
|
||||||
|
# Strip everything after last "/" to get directoru: "/foo/bar/baz" -> "/foo/bar", "./foo" -> "./".
|
||||||
|
# Then cd to the directory and get it path.
|
||||||
|
__CD_TO_BINDIR_DIRECTORY=$( cd "${__CD_TO_BINDIR__BIN_FILE%/*}/" ; pwd )
|
||||||
|
|
||||||
|
unset __CD_TO_BINDIR__BIN_FILE
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> # FUNCTIONS
|
||||||
|
#>>
|
||||||
|
#>> * `ch_bin_dir` - Change working directory to directory where script is located, which is usually called "bin dir".
|
||||||
|
cd_to_bindir() {
|
||||||
|
cd "$__CD_TO_BINDIR_DIRECTORY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call this function at import.
|
||||||
|
cd_to_bindir
|
45
helpers/helpers.v2.d/bash-modules/date.sh
Normal file
45
helpers/helpers.v2.d/bash-modules/date.sh
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `date` - date-time functions.
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `date::timestamp VARIABLE` - return current time in seconds since UNIX epoch
|
||||||
|
date::timestamp() {
|
||||||
|
printf -v "$1" "%(%s)T" "-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `date::current_datetime VARIABLE FORMAT` - return current date time in given format.
|
||||||
|
#> See `man 3 strftime` for details.
|
||||||
|
date::current_datetime() {
|
||||||
|
printf -v "$1" "%($2)T" "-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `date::print_current_datetime FORMAT` - print current date time in given format.
|
||||||
|
#> See `man 3 strftime` for details.
|
||||||
|
date::print_current_datetime() {
|
||||||
|
printf "%($1)T" "-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `date::datetime VARIABLE FORMAT TIMESTAMP` - return current date time in given format.
|
||||||
|
#> See `man 3 strftime` for details.
|
||||||
|
date::datetime() {
|
||||||
|
printf -v "$1" "%($2)T" "$3"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `date::print_elapsed_time` - print value of SECONDS variable in human readable form: "Elapsed time: 0 days 00:00:00."
|
||||||
|
#> It's useful to know time of execution of a long script, so here is function for that.
|
||||||
|
#> Assign 0 to SECONDS variable to reset counter.
|
||||||
|
date::print_elapsed_time() {
|
||||||
|
printf "Elapsed time: %d days %02d:%02d:%02d.\n" $((SECONDS/(24*60*60))) $(((SECONDS/(60*60))%24)) $(((SECONDS/60)%60)) $((SECONDS%60))
|
||||||
|
}
|
325
helpers/helpers.v2.d/bash-modules/import.sh
Executable file
325
helpers/helpers.v2.d/bash-modules/import.sh
Executable file
|
@ -0,0 +1,325 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Copyright (c) 2009-2013 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
#
|
||||||
|
# This file is part of bash-modules (http://trac.assembla.com/bash-modules/).
|
||||||
|
#
|
||||||
|
# bash-modules is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# bash-modules is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with bash-modules If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
#> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `import.sh` - import bash modules into scripts or into interactive shell
|
||||||
|
#>
|
||||||
|
#> ## SYNOPSIS
|
||||||
|
#>>
|
||||||
|
#>> ### In a scipt:
|
||||||
|
#>>
|
||||||
|
#>> * `. import.sh MODULE[...]` - import module(s) into script or shell
|
||||||
|
#>> * `source import.sh MODULE[...]` - same as above, but with `source` instead of `.`
|
||||||
|
#>>
|
||||||
|
#>>
|
||||||
|
#>> ### At command line:
|
||||||
|
#>>
|
||||||
|
#>> * `import.sh --help|-h` - print this help text
|
||||||
|
#>> * `import.sh --man` - show manual
|
||||||
|
#>> * `import.sh --list` - list modules with their path
|
||||||
|
#>> * `import.sh --summary|-s [MODULE...]` - list module(s) with summary
|
||||||
|
#>> * `import.sh --usage|-u MODULE[...]` - print module help text
|
||||||
|
#>> * `import.sh --doc|-d MODULE[...]` - print module documentation
|
||||||
|
#>>
|
||||||
|
#> ## DESCRIPTION
|
||||||
|
#>
|
||||||
|
#> Imports given module(s) into current shell.
|
||||||
|
#>
|
||||||
|
#> Use:
|
||||||
|
#>
|
||||||
|
#> * `import.sh --list` - to print list of available modules.
|
||||||
|
#> * `import.sh --summary` - to print list of available modules with short description.
|
||||||
|
#> * `import.sh --usage MODULE[...]` - to print longer description of given module(s).
|
||||||
|
|
||||||
|
[ "${__IMPORT__DEFINED:-}" == "yes" ] || {
|
||||||
|
__IMPORT__DEFINED="yes"
|
||||||
|
|
||||||
|
[ "$BASH_VERSINFO" -ge 4 ] || {
|
||||||
|
echo "[import.sh] ERROR: This script works only with Bash, version 4 or greater. Upgrade is necessary." >&2
|
||||||
|
exit 80
|
||||||
|
}
|
||||||
|
|
||||||
|
# If BASH_MODULES_PATH variable contains a ':' separator, then split it into array
|
||||||
|
if [[ "${BASH_MODULES_PATH:-}" == *':'* ]]; then
|
||||||
|
__split_by_delimiter() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local IFS="$2"
|
||||||
|
local __string__VALUE="${3:-}"
|
||||||
|
read -a "$__string__VAR" <<<"${__string__VALUE:-}"
|
||||||
|
}
|
||||||
|
__split_by_delimiter __BASH_MODULES_PATH_ARRAY ':' "${BASH_MODULES_PATH:+$BASH_MODULES_PATH}"
|
||||||
|
unset -f __split_by_delimiter
|
||||||
|
else
|
||||||
|
__BASH_MODULES_PATH_ARRAY=( "${BASH_MODULES_PATH:+$BASH_MODULES_PATH}" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## CONFIGURATION
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `BASH_MODULES_PATH` - (variable with single path entry, at present time).
|
||||||
|
#> `BASH_MODULES_PATH` can contain multiple directories separated by ":".
|
||||||
|
#>
|
||||||
|
#> * `__IMPORT__BASE_PATH` - array with list of your own directories with modules,
|
||||||
|
#> which will be prepended to module search path. You can set `__IMPORT__BASE_PATH` array in
|
||||||
|
#> script at begining, in `/etc/bash-modules/config.sh`, or in `~/.config/bash-modules/config.sh` file.
|
||||||
|
__IMPORT__BASE_PATH=( "${__BASH_MODULES_PATH_ARRAY[@]:+${__BASH_MODULES_PATH_ARRAY[@]}}" "${__IMPORT__BASE_PATH[@]:+${__IMPORT__BASE_PATH[@]}}" "/usr/share/bash-modules" )
|
||||||
|
unset __BASH_MODULES_PATH_ARRAY
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `/etc/bash-modules/config.sh` - system wide configuration file.
|
||||||
|
#> WARNING: Code in this script will affect all scripts.
|
||||||
|
#>
|
||||||
|
#> ### Example configration file
|
||||||
|
#>
|
||||||
|
#> Put following snippet into `~/.config/bash-modules/config.sh` file:
|
||||||
|
#>
|
||||||
|
#>```bash
|
||||||
|
#>
|
||||||
|
#> # Enable stack trace printing for warnings and errors,
|
||||||
|
#> # like with --debug option:
|
||||||
|
#> __log__STACKTRACE=="yes"
|
||||||
|
#>
|
||||||
|
#> # Add additional directory to module search path:
|
||||||
|
#> BASH_MODULES_PATH="/home/user/my-bash-modules"
|
||||||
|
#>
|
||||||
|
#>```
|
||||||
|
[ ! -s /etc/bash-modules/config.sh ] || source /etc/bash-modules/config.sh || {
|
||||||
|
echo "[import.sh] WARN: Cannot import \"/etc/bash-modules/config.sh\" or an error in this file." >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `~/.config/bash-modules/config.sh` - user configuration file.
|
||||||
|
#> **WARNING:** Code in this script will affect all user scripts.
|
||||||
|
[ ! -s "$HOME/.config/bash-modules/config.sh" ] || source "$HOME/.config/bash-modules/config.sh" || {
|
||||||
|
echo "[import.sh] WARN: Cannot import \"$HOME/.config/bash-modules/config.sh\" or an error in this file." >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## VARIABLES
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `IMPORT__BIN_FILE` - script main file name, e.g. `/usr/bin/my-script`, as in `$0` variable in main file.
|
||||||
|
__IMPORT_INDEX="${#BASH_SOURCE[*]}"
|
||||||
|
IMPORT__BIN_FILE="${BASH_SOURCE[__IMPORT_INDEX-1]}"
|
||||||
|
unset __IMPORT_INDEX
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `import::import_module MODULE` - import single module only.
|
||||||
|
import::import_module() {
|
||||||
|
local __MODULE="${1:?Argument is required: module name, without path and without .sh extension, e.g. "log".}"
|
||||||
|
|
||||||
|
local __PATH
|
||||||
|
for __PATH in "${__IMPORT__BASE_PATH[@]}"
|
||||||
|
do
|
||||||
|
[ -f "$__PATH/$__MODULE.sh" ] || continue
|
||||||
|
|
||||||
|
# Don't import module twice, to avoid loops.
|
||||||
|
# Replace some special symbols in the module name by "_".
|
||||||
|
local -n __IMPORT_MODULE_DEFINED="__${__MODULE//[\[\]\{\}\/!@#$%^&*()=+~\`\\,?|\'\"-]/_}__DEFINED" # Variable reference
|
||||||
|
[ "${__IMPORT_MODULE_DEFINED:-}" != "yes" ] || return 0 # Already imported
|
||||||
|
__IMPORT_MODULE_DEFINED="yes"
|
||||||
|
unset -n __IMPORT_MODULE_DEFINED # Unset reference
|
||||||
|
|
||||||
|
# Import module
|
||||||
|
source "$__PATH/$__MODULE.sh" || return 1
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[import.sh:import_module] ERROR: Cannot locate module: \"$__MODULE\". Search path: ${__IMPORT__BASE_PATH[*]}" >&2
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `import::import_modules MODULE[...]` - import module(s).
|
||||||
|
import::import_modules() {
|
||||||
|
local __MODULE __ERROR_CODE=0
|
||||||
|
for __MODULE in "$@"
|
||||||
|
do
|
||||||
|
import::import_module "$__MODULE" || __ERROR_CODE=$?
|
||||||
|
done
|
||||||
|
return $__ERROR_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `import::list_modules FUNC [MODULE]...` - print various information about module(s).
|
||||||
|
#> `FUNC` is a function to call on each module. Function will be called with two arguments:
|
||||||
|
#> path to module and module name.
|
||||||
|
#> Rest of arguments are module names. No arguments means all modules.
|
||||||
|
import::list_modules() {
|
||||||
|
local __FUNC="${1:?ERROR: Argument is required: function to call with module name.}"
|
||||||
|
shift
|
||||||
|
|
||||||
|
declare -a __MODULES
|
||||||
|
local __PATH __MODULE __MODULES
|
||||||
|
|
||||||
|
# Collect modules
|
||||||
|
if [ $# -eq 0 ]
|
||||||
|
then
|
||||||
|
# If no arguments are given,
|
||||||
|
# then add all modules in all directories
|
||||||
|
for __PATH in "${__IMPORT__BASE_PATH[@]}"
|
||||||
|
do
|
||||||
|
for __MODULE in "$__PATH"/*.sh
|
||||||
|
do
|
||||||
|
[ -f "$__MODULE" ] || continue
|
||||||
|
__MODULES[${#__MODULES[@]}]="$__MODULE"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Argument can be directory or module path or module name.
|
||||||
|
local __ARG
|
||||||
|
for __ARG in "$@"
|
||||||
|
do
|
||||||
|
if [ -d "$__ARG" ]
|
||||||
|
then
|
||||||
|
# Directory. Add all modules in directory
|
||||||
|
for __MODULE in "$__ARG"/*.sh
|
||||||
|
do
|
||||||
|
[ -f "$__MODULE" ] || continue
|
||||||
|
__MODULES[${#__MODULES[@]}]="$__MODULE"
|
||||||
|
done
|
||||||
|
elif [ -f "$__ARG" ]
|
||||||
|
then
|
||||||
|
# Direct path. Add single module.
|
||||||
|
__MODULES[${#__MODULES[@]}]="$__ARG"
|
||||||
|
else
|
||||||
|
# Module name. Find single module in path.
|
||||||
|
for __PATH in "${__IMPORT__BASE_PATH[@]}"
|
||||||
|
do
|
||||||
|
[ -f "$__PATH/$__ARG.sh" ] || continue
|
||||||
|
__MODULES[${#__MODULES[@]}]="$__PATH/$__ARG.sh"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call function on each module
|
||||||
|
local __MODULE_PATH
|
||||||
|
for __MODULE_PATH in "${__MODULES[@]}"
|
||||||
|
do
|
||||||
|
[ -f "$__MODULE_PATH" ] || continue
|
||||||
|
__MODULE="${__MODULE_PATH##*/}" # Strip directory
|
||||||
|
__MODULE="${__MODULE%.sh}" # Strip extension
|
||||||
|
|
||||||
|
# Call requested function on each module
|
||||||
|
$__FUNC "$__MODULE_PATH" "$__MODULE" || { echo "WARN: Error in function \"$__FUNC '$__MODULE_PATH'\"." >&2 ; }
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `import::show_documentation LEVEL PARSER FILE` - print module built-in documentation.
|
||||||
|
#> This function scans given file for lines with "#>" prefix (or given prefix) and prints them to stdout with prefix stripped.
|
||||||
|
#> * `LEVEL` - documentation level (one line summary, usage, full manual):
|
||||||
|
#> - 1 - for manual (`#>` and `#>>` and `#>>>`),
|
||||||
|
#> - 2 - for usage (`#>>` and `#>>>`),
|
||||||
|
#> - 3 - for one line summary (`#>>>`),
|
||||||
|
#> - or arbitrary prefix, e.g. `##`.
|
||||||
|
#> * `FILE` - path to file with built-in documentation.
|
||||||
|
import::show_documentation() {
|
||||||
|
local LEVEL="${1:?ERROR: Argument is required: level of documentation: 1 for all documentation, 2 for usage, 3 for one line summary.}"
|
||||||
|
local FILE="${2:?ERRROR: Argument is required: file to parse documentation from.}"
|
||||||
|
|
||||||
|
[ -e "$FILE" ] || {
|
||||||
|
echo "ERROR: File \"$FILE\" is not exits." >&2
|
||||||
|
}
|
||||||
|
[ -f "$FILE" ] || {
|
||||||
|
echo "ERROR: Path \"$FILE\" is not a file." >&2
|
||||||
|
}
|
||||||
|
[ -r "$FILE" ] || {
|
||||||
|
echo "ERROR: Cannot read file \"$FILE\"." >&2
|
||||||
|
}
|
||||||
|
[ -s "$FILE" ] || {
|
||||||
|
echo "ERROR: File \"$FILE\" is empty." >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
local PREFIX=""
|
||||||
|
case "$LEVEL" in
|
||||||
|
1) PREFIX="#>" ;;
|
||||||
|
2) PREFIX="#>>" ;;
|
||||||
|
3) PREFIX="#>>>" ;;
|
||||||
|
*)
|
||||||
|
PREFIX="$LEVEL"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local line
|
||||||
|
while read line
|
||||||
|
do
|
||||||
|
if [[ "$line" =~ ^\s*"$PREFIX"\>*\ ?(.*)$ ]]
|
||||||
|
then
|
||||||
|
echo "${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
done < "$FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# If this is top level code and program name is .../import.sh
|
||||||
|
if [ "${FUNCNAME:+x}" == "" -a "${0##*/}" == "import.sh" ]
|
||||||
|
then
|
||||||
|
show_module_info() {
|
||||||
|
local module_path="$1"
|
||||||
|
local module_name="$2"
|
||||||
|
|
||||||
|
printf "%-24s\t%s\n" "$module_name" "$module_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# import.sh called as standalone program
|
||||||
|
if [ "$#" -eq 0 ]
|
||||||
|
then
|
||||||
|
import::show_documentation 2 "$IMPORT__BIN_FILE"
|
||||||
|
else
|
||||||
|
case "$1" in
|
||||||
|
--list|-l)
|
||||||
|
shift 1
|
||||||
|
import::list_modules "show_module_info" "${@:+$@}"
|
||||||
|
;;
|
||||||
|
--summary|-s)
|
||||||
|
shift 1
|
||||||
|
import::list_modules "import::show_documentation 3" "${@:+$@}"
|
||||||
|
;;
|
||||||
|
--usage|-u)
|
||||||
|
shift 1
|
||||||
|
import::list_modules "import::show_documentation 2" "${@:+$@}"
|
||||||
|
;;
|
||||||
|
--documentation|--doc|-d)
|
||||||
|
shift 1
|
||||||
|
import::list_modules "import::show_documentation 1" "${@:+$@}" | less
|
||||||
|
;;
|
||||||
|
--man)
|
||||||
|
shift 1
|
||||||
|
import::show_documentation 1 "$IMPORT__BIN_FILE" | less
|
||||||
|
;;
|
||||||
|
--help|-h|*)
|
||||||
|
shift 1
|
||||||
|
import::show_documentation 2 "$IMPORT__BIN_FILE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# Import given modules when parameters are supplied.
|
||||||
|
[ "$#" -eq 0 ] || import::import_modules "$@"
|
||||||
|
fi
|
253
helpers/helpers.v2.d/bash-modules/log.sh
Normal file
253
helpers/helpers.v2.d/bash-modules/log.sh
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `log` - various functions related to logging.
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## VARIABLES
|
||||||
|
|
||||||
|
#export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}}: '.
|
||||||
|
|
||||||
|
#> * `__log__APP` - name of main file without path.
|
||||||
|
__log__APP="${IMPORT__BIN_FILE##*/}" # Strip everything before last "/"
|
||||||
|
|
||||||
|
#> * `__log__DEBUG` - set to yes to enable printing of debug messages and stacktraces.
|
||||||
|
#> * `__log__STACKTRACE` - set to yes to enable printing of stacktraces.
|
||||||
|
|
||||||
|
#> * `__log__TIMESTAMPED` - set to yes to enable timestamped logs
|
||||||
|
#> * `__log_timestamp_format` - format of timestamp. Default value: "%F %T" (full date and time).
|
||||||
|
__log_timestamp_format="%F %T"
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `MESSAGE | log::prefix PREFIX` - display string with PREFIX prefixed to every line
|
||||||
|
#>>
|
||||||
|
log::prefix() {
|
||||||
|
__log__PREFIX="$1"
|
||||||
|
while read -r line; do
|
||||||
|
# shellcheck disable=SC2001
|
||||||
|
echo "$line" | sed 's|^|'"$__log__PREFIX"'|'
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `MESSAGE | log::_generic_log COLOR LEVEL -` - display message with color, date, app name
|
||||||
|
#>> * or log::_generic_log COLOR LEVEL MESSAGE`
|
||||||
|
#>>
|
||||||
|
log::_generic_log() {
|
||||||
|
local color="$1" ; shift
|
||||||
|
local level="$1" ; shift
|
||||||
|
local log_prefix="" log_date="" color_stop=""
|
||||||
|
if [[ -n "$color" ]]; then
|
||||||
|
color_stop=$'\033[39m'
|
||||||
|
fi
|
||||||
|
if [[ "$#" == 1 ]] && [[ "$1" == "-" ]]; then
|
||||||
|
while read -r line ; do
|
||||||
|
if [ "${__log__TIMESTAMPED:-}" == "yes" ]; then
|
||||||
|
# space at the end
|
||||||
|
log_date="$(date::print_current_datetime "$__log_timestamp_format") "
|
||||||
|
fi
|
||||||
|
log_prefix="${log_date}[$__log__APP] ${color}$level${color_stop}: "
|
||||||
|
echo "$line" | log::prefix "$log_prefix"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
if [ "${__log__TIMESTAMPED:-}" == "yes" ]; then
|
||||||
|
# space at the end
|
||||||
|
log_date="$(date::print_current_datetime "$__log_timestamp_format") "
|
||||||
|
fi
|
||||||
|
log_prefix="${log_date}[$__log__APP] ${color}$level${color_stop}: "
|
||||||
|
echo "$@" | log::prefix "$log_prefix"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `stacktrace [INDEX]` - display functions and source line numbers starting
|
||||||
|
#>> from given index in stack trace, when debugging or back tracking is enabled.
|
||||||
|
log::stacktrace() {
|
||||||
|
if [ "${__log__DEBUG:-}" != "yes" ] && [ "${__log__STACKTRACE:-}" != "yes" ]; then
|
||||||
|
local BEGIN="${1:-1}" # Display line numbers starting from given index, e.g. to skip "log::stacktrace" and "error" functions.
|
||||||
|
local I
|
||||||
|
for(( I=BEGIN; I<${#FUNCNAME[@]}; I++ ))
|
||||||
|
do
|
||||||
|
echo $'\t\t'"at ${FUNCNAME[$I]}(${BASH_SOURCE[$I]}:${BASH_LINENO[$I-1]})" >&2
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::debug LEVEL MESSAGE...` - print debug-like LEVEL: MESSAGE to STDOUT.
|
||||||
|
log::debug::custom() {
|
||||||
|
local LEVEL="${1:-DEBUG}" ; shift
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
# STDOUT is tty
|
||||||
|
local __log_DEBUG_BEGIN=$'\033[96m'
|
||||||
|
fi
|
||||||
|
log::_generic_log "${__log_DEBUG_BEGIN:-}" "$LEVEL" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `debug MESAGE...` - print debug message.
|
||||||
|
log::debug() {
|
||||||
|
if [ "${__log__DEBUG:-}" == "yes" ]; then
|
||||||
|
log::debug::custom DEBUG "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::info LEVEL MESSAGE...` - print info-like LEVEL: MESSAGE to STDOUT.
|
||||||
|
log::info::custom() {
|
||||||
|
local LEVEL="${1:-INFO}" ; shift
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
# STDOUT is tty
|
||||||
|
local __log_INFO_BEGIN=$'\033[92m'
|
||||||
|
fi
|
||||||
|
log::_generic_log "${__log_INFO_BEGIN:-}" "$LEVEL" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `info MESAGE...` - print info message.
|
||||||
|
log::info() {
|
||||||
|
log::info::custom INFO "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::warn LEVEL MESSAGE...` - print warning-like LEVEL: MESSAGE to STDERR.
|
||||||
|
log::warn::custom() {
|
||||||
|
local LEVEL="${1:-WARN}" ; shift
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
# STDERR is tty
|
||||||
|
local __log_WARN_BEGIN=$'\033[93m'
|
||||||
|
fi
|
||||||
|
log::_generic_log "${__log_WARN_BEGIN:-}" "$LEVEL" >&2 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `warn MESAGE...` - print warning message and stacktrace (if enabled).
|
||||||
|
log::warn() {
|
||||||
|
log::warn::custom WARN "$@"
|
||||||
|
log::stacktrace 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::error LEVEL MESSAGE...` - print error-like LEVEL: MESSAGE to STDERR.
|
||||||
|
log::error::custom() {
|
||||||
|
local LEVEL="$1" ; shift
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
# STDERR is tty
|
||||||
|
local __log_ERROR_BEGIN=$'\033[91m'
|
||||||
|
fi
|
||||||
|
log::_generic_log "${__log_ERROR_BEGIN:-}" "$LEVEL" >&2 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `error MESAGE...` - print error message and stacktrace (if enabled).
|
||||||
|
log::error() {
|
||||||
|
log::error::custom ERROR "$@"
|
||||||
|
log::stacktrace 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::fatal LEVEL MESSAGE...` - print a fatal-like LEVEL: MESSAGE to STDERR.
|
||||||
|
log::fatal::custom() {
|
||||||
|
local LEVEL="$1" ; shift
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
# STDERR is tty
|
||||||
|
local __log_FATAL_BEGIN=$'\033[95m'
|
||||||
|
fi
|
||||||
|
log::_generic_log "${__log_FATAL_BEGIN:-}" "$LEVEL" >&2 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::fatal LEVEL MESSAGE...` - print a fatal-like LEVEL: MESSAGE to STDERR.
|
||||||
|
log::fatal() {
|
||||||
|
log::fatal::custom FATAL "$@"
|
||||||
|
log::stacktrace 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `panic MESAGE...` - print error message and stacktrace, then exit with error code 1.
|
||||||
|
log::panic() {
|
||||||
|
log::fatal::custom "PANIC" "$@"
|
||||||
|
log::enable_stacktrace
|
||||||
|
log::stacktrace 2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unimplemented MESSAGE...` - print error message and stacktrace, then exit with error code 42.
|
||||||
|
log::unimplemented() {
|
||||||
|
log::fatal::custom "UNIMPLEMENTED" "$@"
|
||||||
|
log::enable_stacktrace
|
||||||
|
log::stacktrace 2
|
||||||
|
exit 42
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `todo MESAGE...` - print todo message and stacktrace.
|
||||||
|
log::todo() {
|
||||||
|
log::warn::custom "TODO" "$@"
|
||||||
|
local __log__STACKTRACE="yes"
|
||||||
|
log::stacktrace 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `dbg VARIABLE...` - print name of variable and it content to stderr
|
||||||
|
log::dbg() {
|
||||||
|
log::debug "$(declare -p "$@" | sed 's|declare -. ||')"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::enable_debug_mode` - enable debug messages and stack traces.
|
||||||
|
log::enable_debug_mode() {
|
||||||
|
__log__DEBUG="yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::disable_debug_mode` - disable debug messages and stack traces.
|
||||||
|
log::disable_debug_mode() {
|
||||||
|
__log__DEBUG="no"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::enable_stacktrace` - enable stack traces.
|
||||||
|
log::enable_stacktrace() {
|
||||||
|
__log__STACKTRACE="yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::disable_stacktrace` - disable stack traces.
|
||||||
|
log::disable_stacktrace() {
|
||||||
|
__log__STACKTRACE="no"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::enable_timestamps` - enable timestamps.
|
||||||
|
log::enable_timestamps() {
|
||||||
|
__log__TIMESTAMPED="yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::disable_timestamps` - disable timestamps.
|
||||||
|
log::disable_timestamps() {
|
||||||
|
__log__TIMESTAMPED="no"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `log::set_timestamp_format FORMAT` - Set format for date. Default value is "%F %T".
|
||||||
|
log::set_timestamp_format() {
|
||||||
|
__log_timestamp_format="$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## NOTES
|
||||||
|
#>>
|
||||||
|
#>> - If STDOUT is connected to tty, then
|
||||||
|
#>> * info and info-like messages will be printed with message level higlighted in green,
|
||||||
|
#>> * warn and warn-like messages will be printed with message level higlighted in yellow,
|
||||||
|
#>> * error and error-like messages will be printed with message level higlighted in red.
|
44
helpers/helpers.v2.d/bash-modules/log_run.sh
Normal file
44
helpers/helpers.v2.d/bash-modules/log_run.sh
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
# Import log module
|
||||||
|
import::import_module log
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `log_run` - various functions related to logging commands.
|
||||||
|
|
||||||
|
run::stderr_to_stdout() {
|
||||||
|
"$@" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
run::debug() {
|
||||||
|
"$@" | log::debug -
|
||||||
|
}
|
||||||
|
|
||||||
|
run::info() {
|
||||||
|
"$@" | log::info -
|
||||||
|
}
|
||||||
|
|
||||||
|
run::warn() {
|
||||||
|
"$@" | log::warn -
|
||||||
|
}
|
||||||
|
|
||||||
|
run::error() {
|
||||||
|
"$@" | log::error -
|
||||||
|
}
|
||||||
|
|
||||||
|
run::fatal() {
|
||||||
|
"$@" | log::fatal -
|
||||||
|
}
|
||||||
|
|
||||||
|
run::quiet() {
|
||||||
|
local result returncode
|
||||||
|
result=$(run::stderr_to_stdout "$@" || echo "__log_run__returncode=$?")
|
||||||
|
returncode=$(echo "$result" | sed -n 's|^__log_run__returncode=\(.*\)$|\1|p')
|
||||||
|
if [[ -n "$returncode" ]]; then
|
||||||
|
log::error "$@"
|
||||||
|
return "$returncode"
|
||||||
|
fi
|
||||||
|
}
|
89
helpers/helpers.v2.d/bash-modules/meta.sh
Normal file
89
helpers/helpers.v2.d/bash-modules/meta.sh
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `meta` - functions for working with bash functions.
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `meta::copy_function FUNCTION_NAME NEW_FUNCTION_PREFIX` - copy function to new function with prefix in name.
|
||||||
|
#> Create copy of function with new prefix.
|
||||||
|
#> Old function can be redefined or `unset -f`.
|
||||||
|
meta::copy_function() {
|
||||||
|
local FUNCTION_NAME="$1"
|
||||||
|
local PREFIX="$2"
|
||||||
|
|
||||||
|
eval "$PREFIX$(declare -fp $FUNCTION_NAME)"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `meta::wrap BEFORE AFTER FUNCTION_NAME[...]` - wrap function.
|
||||||
|
#> Create wrapper for a function(s). Execute given commands before and after
|
||||||
|
#> each function. Original function is available as meta::orig_FUNCTION_NAME.
|
||||||
|
meta::wrap() {
|
||||||
|
local BEFORE="$1"
|
||||||
|
local AFTER="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local FUNCTION_NAME
|
||||||
|
for FUNCTION_NAME in "$@"
|
||||||
|
do
|
||||||
|
# Rename original function
|
||||||
|
meta::copy_function "$FUNCTION_NAME" "meta::orig_" || return 1
|
||||||
|
|
||||||
|
# Redefine function
|
||||||
|
eval "
|
||||||
|
function $FUNCTION_NAME() {
|
||||||
|
$BEFORE
|
||||||
|
|
||||||
|
local __meta__EXIT_CODE=0
|
||||||
|
meta::orig_$FUNCTION_NAME \"\$@\" || __meta__EXIT_CODE=\$?
|
||||||
|
|
||||||
|
$AFTER
|
||||||
|
|
||||||
|
return \$__meta__EXIT_CODE
|
||||||
|
}
|
||||||
|
"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `meta::functions_with_prefix PREFIX` - print list of functions with given prefix.
|
||||||
|
meta::functions_with_prefix() {
|
||||||
|
compgen -A function "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `meta::is_function FUNC_NAME` Checks is given name corresponds to a function.
|
||||||
|
meta::is_function() {
|
||||||
|
declare -F "$1" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `meta::dispatch PREFIX COMMAND [ARGUMENTS...]` - execute function `PREFIX__COMMAND [ARGUMENTS]`
|
||||||
|
#>
|
||||||
|
#> For example, it can be used to execute functions (commands) by name, e.g.
|
||||||
|
#> `main() { meta::dispatch command__ "$@" ; }`, when called as `man hw world` will execute
|
||||||
|
#> `command_hw "$world"`. When command handler is not found, dispatcher will try
|
||||||
|
#> to call `PREFIX__DEFAULT` function instead, or return error code when defaulf handler is not found.
|
||||||
|
meta::dispatch() {
|
||||||
|
local prefix="${1:?Prefix is required.}"
|
||||||
|
local command="${2:?Command is required.}"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local fn="${prefix}${command}"
|
||||||
|
|
||||||
|
# Is handler function exists?
|
||||||
|
meta::is_function "$fn" || {
|
||||||
|
# Is default handler function exists?
|
||||||
|
meta::is_function "${prefix}__DEFAULT" || { echo "ERROR: Function \"$fn\" is not found." >&2; return 1; }
|
||||||
|
fn="${prefix}__DEFAULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
"$fn" "${@:+$@}" || return $?
|
||||||
|
}
|
14
helpers/helpers.v2.d/bash-modules/renice.sh
Normal file
14
helpers/helpers.v2.d/bash-modules/renice.sh
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `renice` - reduce priority of current shell to make it low priority task (`renice 19` to self).
|
||||||
|
#>>
|
||||||
|
#>> ## USAGE
|
||||||
|
#>>
|
||||||
|
#>> `. import.sh renice`
|
||||||
|
|
||||||
|
# Run this script as low priority task
|
||||||
|
renice 19 -p $$ >/dev/null
|
54
helpers/helpers.v2.d/bash-modules/settings.sh
Normal file
54
helpers/helpers.v2.d/bash-modules/settings.sh
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
import::import_modules log arguments
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `settings` - import settings from configuration files and configuration directories.
|
||||||
|
#>> Also known as "configuration directory" pattern.
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>> * `settings::import [-e|--ext EXTENSION] FILE|DIRECTORY...` - Import settings
|
||||||
|
#> (source them into current program as shell script) when
|
||||||
|
#> file or directory exists. For directories, all files with given extension
|
||||||
|
#> (`".sh"` by default) are imported, without recursion.
|
||||||
|
#>
|
||||||
|
#> **WARNING:** this method is powerful, but unsafe, because user can put any shell
|
||||||
|
#> command into the configuration file, which will be executed by script.
|
||||||
|
#>
|
||||||
|
#> **TODO:** implement file parsing instead of sourcing.
|
||||||
|
settings::import() {
|
||||||
|
local __settings_EXTENSION="sh"
|
||||||
|
arguments::parse '-e|--ext)__settings_EXTENSION;String,Required' -- "$@" || panic "Cannot parse arguments."
|
||||||
|
|
||||||
|
local __settings_ENTRY
|
||||||
|
for __settings_ENTRY in "${@:+$@}"
|
||||||
|
do
|
||||||
|
if [ -f "$__settings_ENTRY" -a -r "$__settings_ENTRY" -a -s "$__settings_ENTRY" ]
|
||||||
|
then
|
||||||
|
# Just source configuration file into this script.
|
||||||
|
source "$__settings_ENTRY" || {
|
||||||
|
error "Cannot import settings from \"$__settings_ENTRY\" file: non-zero exit code returned: $?." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
elif [ -d "$__settings_ENTRY" -a -x "$__settings_ENTRY" ]
|
||||||
|
then
|
||||||
|
# Just source each configuration file in the directory into this script.
|
||||||
|
local __settings_FILE
|
||||||
|
for __settings_FILE in "$__settings_ENTRY"/*."$__settings_EXTENSION"
|
||||||
|
do
|
||||||
|
if [ -f "$__settings_FILE" -a -r "$__settings_FILE" -a -s "$__settings_FILE" ]
|
||||||
|
then
|
||||||
|
source "$__settings_FILE" || {
|
||||||
|
error "Cannot import settings from \"$__settings_FILE\" file: non-zero exit code returned: $?." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
51
helpers/helpers.v2.d/bash-modules/strict.sh
Normal file
51
helpers/helpers.v2.d/bash-modules/strict.sh
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
import::import_module array
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `strict` - unofficial strict mode for bash
|
||||||
|
#>>
|
||||||
|
#>> Just import this module, to enabe strict mode: `set -euEo pipefail`.
|
||||||
|
#>
|
||||||
|
#> ## NOTE
|
||||||
|
#>
|
||||||
|
#> * Option `-e` is not working when command is part of a compound command,
|
||||||
|
#> or in subshell. See bash manual for details. For example, `-e` may not working
|
||||||
|
#> in a `for` cycle.
|
||||||
|
|
||||||
|
set -euEo pipefail
|
||||||
|
|
||||||
|
declare -Ag __cleanup_CODES
|
||||||
|
|
||||||
|
cleanup::run() {
|
||||||
|
for key in "${!__cleanup_CODES[@]}"; do
|
||||||
|
echo "Cleaning up $key..."
|
||||||
|
cleanup::pop "$key"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup::add() {
|
||||||
|
key="$1" ; shift
|
||||||
|
value="${1:-}"
|
||||||
|
__cleanup_CODES+=([$key]="$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup::remove() {
|
||||||
|
local key="$1"
|
||||||
|
unset "__cleanup_CODES[$key]"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup::pop() {
|
||||||
|
local key="$1"
|
||||||
|
if array::contains "$key" __cleanup_CODES; then
|
||||||
|
code="${__cleanup_CODES[$key]}"
|
||||||
|
cleanup::remove "$key"
|
||||||
|
eval "$code"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'log::panic "Uncaught error."' ERR
|
||||||
|
trap 'cleanup::run' EXIT
|
274
helpers/helpers.v2.d/bash-modules/string.sh
Normal file
274
helpers/helpers.v2.d/bash-modules/string.sh
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> string - various functions to manipulate strings.
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::trim_spaces VARIABLE VALUE`
|
||||||
|
#> Trim white space characters around value and assign result to variable.
|
||||||
|
string::trim() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__VALUE="${2:-}"
|
||||||
|
|
||||||
|
# remove leading whitespace characters
|
||||||
|
__string__VALUE="${__string__VALUE#"${__string__VALUE%%[![:space:]]*}"}"
|
||||||
|
# remove trailing whitespace characters
|
||||||
|
__string__VALUE="${__string__VALUE%"${__string__VALUE##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
__string__VAR="$__string__VALUE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::trim_start VARIABLE VALUE`
|
||||||
|
#> Trim white space characters at begining of the value and assign result to the variable.
|
||||||
|
string::trim_start() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__VALUE="${2:-}"
|
||||||
|
|
||||||
|
# remove leading whitespace characters
|
||||||
|
__string__VALUE="${__string__VALUE#"${__string__VALUE%%[![:space:]]*}"}" #"
|
||||||
|
|
||||||
|
__string__VAR="$__string__VALUE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::trim_end VARIABLE VALUE`
|
||||||
|
#> Trim white space characters at the end of the value and assign result to the variable.
|
||||||
|
string::trim_end() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__VALUE="${2:-}"
|
||||||
|
|
||||||
|
# remove trailing whitespace characters
|
||||||
|
__string__VALUE="${__string__VALUE%"${__string__VALUE##*[![:space:]]}"}" #"
|
||||||
|
|
||||||
|
__string__VAR="$__string__VALUE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::insert VARIABLE POSITION VALUE`
|
||||||
|
#> Insert `VALUE` into `VARIABLE` at given `POSITION`.
|
||||||
|
#> Example:
|
||||||
|
#>
|
||||||
|
#> ```bash
|
||||||
|
#> v="abba"
|
||||||
|
#> string::insert v 2 "cc"
|
||||||
|
#> # now v=="abccba"
|
||||||
|
#> ```
|
||||||
|
string::insert() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__POSITION="$2"
|
||||||
|
local __string__VALUE="${3:-}"
|
||||||
|
|
||||||
|
__string__VALUE="${__string__VAR::$__string__POSITION}${__string__VALUE}${__string__VAR:$__string__POSITION}"
|
||||||
|
|
||||||
|
__string__VAR="$__string__VALUE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::split ARRAY DELIMITERS VALUE`
|
||||||
|
#> Split value by delimiter(s) and assign result to array. Use
|
||||||
|
#> backslash to escape delimiter in string.
|
||||||
|
string::split_to() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local IFS="$2"
|
||||||
|
local __string__VALUE="${3:-}"
|
||||||
|
|
||||||
|
# We can use "for" loop and strip elements item by item, but we are
|
||||||
|
# unable to assign result to named array, so we must use "read -a" and "<<<" here.
|
||||||
|
|
||||||
|
# TODO: use regexp and loop instead.
|
||||||
|
read -a "$__string__VAR" <<<"${__string__VALUE:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::split DELIMITERS VALUE`
|
||||||
|
#> Split value by delimiter(s) and echo the result. Use
|
||||||
|
#> backslash to escape delimiter in string.
|
||||||
|
string::split() {
|
||||||
|
local -a __string__ECHO
|
||||||
|
string::split_to "__string__ECHO" "$@"
|
||||||
|
echo "${__string__ECHO[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::basename VARIABLE FILE [EXT]`
|
||||||
|
#> Strip path and optional extension from full file name and store
|
||||||
|
#> file name in variable.
|
||||||
|
string::basename() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__FILE="${2:-}"
|
||||||
|
local __string__EXT="${3:-}"
|
||||||
|
|
||||||
|
__string__FILE="${__string__FILE##*/}" # Strip everything before last "/"
|
||||||
|
__string__FILE="${__string__FILE%$__string__EXT}" # Strip .sh extension
|
||||||
|
|
||||||
|
__string__VAR="$__string__FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::dirname VARIABLE FILE`
|
||||||
|
#> Strip file name from path and store directory name in variable.
|
||||||
|
string::dirname() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__FILE="${2:-}"
|
||||||
|
|
||||||
|
local __string__DIR=""
|
||||||
|
case "$__string__FILE" in
|
||||||
|
*/*)
|
||||||
|
__string__DIR="${__string__FILE%/*}" # Strip everything after last "/'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
__string__DIR="."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
__string__VAR="$__string__DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::random_string VARIABLE LENGTH`
|
||||||
|
#> Generate random string of given length using [a-zA-Z0-9]
|
||||||
|
#> characters and store it into variable.
|
||||||
|
string::random_string() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__LENGTH="${2:-8}"
|
||||||
|
|
||||||
|
local __string__ALPHABET="0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
|
||||||
|
local __string__ALPHABET_LENGTH=${#__string__ALPHABET}
|
||||||
|
|
||||||
|
local __string__I __string__RESULT=""
|
||||||
|
for((__string__I=0; __string__I<__string__LENGTH; __string__I++))
|
||||||
|
do
|
||||||
|
__string__RESULT="$__string__RESULT${__string__ALPHABET:RANDOM%__string__ALPHABET_LENGTH:1}"
|
||||||
|
done
|
||||||
|
|
||||||
|
__string__VAR="$__string__RESULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::chr VARIABLE CHAR_CODE`
|
||||||
|
#> Convert decimal character code to its ASCII representation.
|
||||||
|
string::chr() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local __string__CODE="$2"
|
||||||
|
|
||||||
|
local __string__OCTAL_CODE
|
||||||
|
printf -v __string__OCTAL_CODE '%03o' "$__string__CODE"
|
||||||
|
printf -v "$__string__VAR" "\\$__string__OCTAL_CODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::ord VARIABLE CHAR`
|
||||||
|
#> Converts ASCII character to its decimal value.
|
||||||
|
string::ord() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local __string__CHAR="$2"
|
||||||
|
|
||||||
|
printf -v "$__string__VAR" '%d' "'$__string__CHAR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alternative version of function:
|
||||||
|
# string::quote_to_bash_format() {
|
||||||
|
# local -n __string__VAR="$1"
|
||||||
|
# local __string__STRING="$2"
|
||||||
|
#
|
||||||
|
# local __string__QUOTE="'\\''"
|
||||||
|
# local __string__QUOTE="'\"'\"'"
|
||||||
|
# __string__VAR="'${__string__STRING//\'/$__string__QUOTE}'"
|
||||||
|
# }
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::quote_to_bash_format VARIABLE STRING`
|
||||||
|
#> Quote the argument in a way that can be reused as shell input.
|
||||||
|
string::quote_to_bash_format() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local __string__STRING="$2"
|
||||||
|
|
||||||
|
printf -v "$__string__VAR" "%q" "$__string__STRING"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::unescape_backslash_sequences VARIABLE STRING`
|
||||||
|
#> Expand backslash escape sequences.
|
||||||
|
string::unescape_backslash_sequences() {
|
||||||
|
local __string__VAR="$1"
|
||||||
|
local __string__STRING="$2"
|
||||||
|
|
||||||
|
printf -v "$__string__VAR" "%b" "$__string__STRING"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::to_identifier VARIABLE STRING`
|
||||||
|
#> Replace all non-alphanumeric characters in string by underscore character.
|
||||||
|
string::to_identifier() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__STRING="$2"
|
||||||
|
|
||||||
|
# We need a-zA-Z letters only.
|
||||||
|
# 'z' can be in middle of alphabet on some locales.
|
||||||
|
__string__VAR="${__string__STRING//[^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]/_}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::find_string_with_prefix VAR PREFIX [STRINGS...]`
|
||||||
|
#> Find first string with given prefix and assign it to VAR.
|
||||||
|
string::find_string_with_prefix() {
|
||||||
|
local -n __string__VAR="$1"
|
||||||
|
local __string__PREFIX="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local __string__I
|
||||||
|
for __string__I in "$@"
|
||||||
|
do
|
||||||
|
[[ "${__string__I}" != "${__string__PREFIX}"* ]] || {
|
||||||
|
__string__VAR="${__string__I}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::empty STRING`
|
||||||
|
#> Returns zero exit code (true), when string is empty
|
||||||
|
string::empty() {
|
||||||
|
[[ -z "${1:-}" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::contains STRING SUBSTRING`
|
||||||
|
#> Returns zero exit code (true), when string contains substring
|
||||||
|
string::contains() {
|
||||||
|
case "$1" in
|
||||||
|
*"$2"*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::starts_with STRING SUBSTRING`
|
||||||
|
#> Returns zero exit code (true), when string starts with substring
|
||||||
|
string::starts_with() {
|
||||||
|
case "$1" in
|
||||||
|
"$2"*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `string::ends_with STRING SUBSTRING`
|
||||||
|
#> Returns zero exit code (true), when string ends with substring
|
||||||
|
string::ends_with() {
|
||||||
|
case "$1" in
|
||||||
|
*"$2") return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
52
helpers/helpers.v2.d/bash-modules/timestamped_log.sh
Normal file
52
helpers/helpers.v2.d/bash-modules/timestamped_log.sh
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
# Import log module and then override some functions
|
||||||
|
import::import_module log date meta
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `timestamped_log` - print timestamped logs. Drop-in replacement for `log` module.
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## VARIABLES
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> * `__timestamped_log_format` - format of timestamp. Default value: "%F %T" (full date and time).
|
||||||
|
__timestamped_log_format="%F %T "
|
||||||
|
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `timestamped_log::set_format FORMAT` - Set format for date. Default value is "%F %T".
|
||||||
|
timestamped_log::set_format() {
|
||||||
|
__timestamped_log_format="$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## Wrapped functions:
|
||||||
|
#>>
|
||||||
|
#>> `log::info`, `info`, `debug` - print timestamp to stdout and then log message.
|
||||||
|
meta::wrap \
|
||||||
|
'date::print_current_datetime "$__timestamped_log_format"' \
|
||||||
|
'' \
|
||||||
|
log::info::custom \
|
||||||
|
log::debug
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> `log::error`, `log::warn`, `error`, `warn` - print timestamp to stderr and then log message.
|
||||||
|
meta::wrap \
|
||||||
|
'date::print_current_datetime "$__timestamped_log_format" >&2' \
|
||||||
|
'' \
|
||||||
|
log::warn::custom \
|
||||||
|
log::error::custom \
|
||||||
|
log::fatal::custom
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## NOTES
|
||||||
|
#>>
|
||||||
|
#>> See `log` module usage for details about log functions. Original functions
|
||||||
|
#>> are available with prefix `"timestamped_log::orig_"`.
|
259
helpers/helpers.v2.d/bash-modules/unit.sh
Normal file
259
helpers/helpers.v2.d/bash-modules/unit.sh
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
##!/bin/bash
|
||||||
|
# Copyright (c) 2009-2021 Volodymyr M. Lisivka <vlisivka@gmail.com>, All Rights Reserved
|
||||||
|
# License: LGPL2+
|
||||||
|
|
||||||
|
#>> ## NAME
|
||||||
|
#>>
|
||||||
|
#>>> `unit` - functions for unit testing.
|
||||||
|
|
||||||
|
import::import_module log arguments
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> ## FUNCTIONS
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_yes VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"yes"`.
|
||||||
|
unit::assert_yes() {
|
||||||
|
local VALUE="${1:-}"
|
||||||
|
local MESSAGE="${2:-Value is not \"yes\".}"
|
||||||
|
|
||||||
|
[ "${VALUE:-}" == "yes" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_no VALUE [MESSAGE]` - Show error message, when `VALUE` is not equal to `"no"`.
|
||||||
|
unit::assert_no() {
|
||||||
|
local VALUE="$1"
|
||||||
|
local MESSAGE="${2:-Value is not \"no\".}"
|
||||||
|
|
||||||
|
[ "$VALUE" == "no" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_not_empty VALUE [MESSAGE]` - Show error message, when `VALUE` is empty.
|
||||||
|
unit::assert_not_empty() {
|
||||||
|
local VALUE="${1:-}"
|
||||||
|
local MESSAGE="${2:-Value is empty.}"
|
||||||
|
|
||||||
|
[ -n "${VALUE:-}" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_equal ACTUAL EXPECTED [MESSAGE]` - Show error message, when values are not equal.
|
||||||
|
unit::assert_equal() {
|
||||||
|
local ACTUAL="${1:-}"
|
||||||
|
local EXPECTED="${2:-}"
|
||||||
|
local MESSAGE="${3:-Values are not equal.}"
|
||||||
|
|
||||||
|
[ "${ACTUAL:-}" == "${EXPECTED:-}" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE Actual value: \"${ACTUAL:-}\", expected value: \"${EXPECTED:-}\"."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_arrays_are_equal MESSAGE VALUE1... -- VALUE2...` - Show error message when arrays are not equal in size or content.
|
||||||
|
unit::assert_arrays_are_equal() {
|
||||||
|
local MESSAGE="${1:-Arrays are not equal.}" ; shift
|
||||||
|
local ARGS=( $@ )
|
||||||
|
|
||||||
|
local I LEN1=''
|
||||||
|
for((I=0;I<${#ARGS[@]};I++))
|
||||||
|
do
|
||||||
|
[ "${ARGS[I]}" != "--" ] || {
|
||||||
|
LEN1="$I"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "${LEN1:-}" ] || {
|
||||||
|
error "Array separator is not found. Put \"--\" between two arrays."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local LEN2=$(($# - LEN1 - 1))
|
||||||
|
local MIN=$(( (LEN1<LEN2) ? LEN1 : LEN2 ))
|
||||||
|
|
||||||
|
for((I=0; I < MIN; I++)) {
|
||||||
|
local ACTUAL="${ARGS[I]:-}"
|
||||||
|
local EXPECTED="${ARGS[I + LEN1 + 1]:-}"
|
||||||
|
|
||||||
|
[ "${ACTUAL:-}" == "${EXPECTED:-}" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE Actual size of array: $LEN1, expected size of array: $LEN2, position in array: $I, actual value: \"${ACTUAL:-}\", expected value: \"${EXPECTED:-}\"."$'\n'"$@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$LEN1" -eq "$LEN2" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE Arrays are not equal in size. Actual size: $LEN1, expected size: $LEN2."$'\n'"$@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert_not_equal ACTUAL_VALUE UNEXPECTED_VALUE [MESSAGE]` - Show error message, when values ARE equal.
|
||||||
|
unit::assert_not_equal() {
|
||||||
|
local ACTUAL_VALUE="${1:-}"
|
||||||
|
local UNEXPECTED_VALUE="${2:-}"
|
||||||
|
local MESSAGE="${3:-values are equal but must not.}"
|
||||||
|
|
||||||
|
[ "${ACTUAL_VALUE:-}" != "${UNEXPECTED_VALUE:-}" ] || {
|
||||||
|
log::error::custom "ASSERT FAILED" "$MESSAGE Actual value: \"${ACTUAL_VALUE:-}\", unexpected value: \"$UNEXPECTED_VALUE\"."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::assert MESSAGE TEST[...]` - Evaluate test and show error message when it returns non-zero exit code.
|
||||||
|
unit::assert() {
|
||||||
|
local MESSAGE="${1:-}"; shift
|
||||||
|
|
||||||
|
eval "$@" || {
|
||||||
|
log::error::custom "ASSERT FAILED" "${MESSAGE:-}: $@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::fail [MESSAGE]` - Show error message.
|
||||||
|
unit::fail() {
|
||||||
|
local MESSAGE="${1:-This point in test case must not be reached.}"; shift
|
||||||
|
log::error::custom "FAIL" "$MESSAGE $@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::run_test_cases [OPTIONS] [--] [ARGUMENTS]` - Execute all functions with
|
||||||
|
#>> test* prefix in name in alphabetic order
|
||||||
|
#>
|
||||||
|
#> * OPTIONS:
|
||||||
|
#> * `-t | --test TEST_CASE` - execute single test case,
|
||||||
|
#> * `-q | --quiet` - do not print informational messages and dots,
|
||||||
|
#> * `--debug` - enable stack traces.
|
||||||
|
#> * ARGUMENTS - All arguments, which are passed to run_test_cases, are passed then
|
||||||
|
#> to `unit::set_up`, `unit::tear_down` and test cases using `ARGUMENTS` array, so you
|
||||||
|
#> can parametrize your test cases. You can call `run_test_cases` more than
|
||||||
|
#> once with different arguments. Use `"--"` to strictly separate arguments
|
||||||
|
#> from options.
|
||||||
|
#>
|
||||||
|
#> After execution of `run_test_cases`, following variables will have value:
|
||||||
|
#>
|
||||||
|
#> * `NUMBER_OF_TEST_CASES` - total number of test cases executed,
|
||||||
|
#> * `NUMBER_OF_FAILED_TEST_CASES` - number of failed test cases,
|
||||||
|
#> * `FAILED_TEST_CASES` - names of functions of failed tests cases.
|
||||||
|
#>
|
||||||
|
#>
|
||||||
|
#> If you want to ignore some test case, just prefix them with
|
||||||
|
#> underscore, so `unit::run_test_cases` will not see them.
|
||||||
|
#>
|
||||||
|
#> If you want to run few subsets of test cases in one file, define each
|
||||||
|
#> subset in it own subshell and execute `unit::run_test_cases` in each subshell.
|
||||||
|
#>
|
||||||
|
#> Each test case is executed in it own subshell, so you can call `exit`
|
||||||
|
#> in the test case or assign variables without any effect on subsequent test
|
||||||
|
#> cases.
|
||||||
|
unit::run_test_cases() {
|
||||||
|
|
||||||
|
NUMBER_OF_TEST_CASES=0
|
||||||
|
NUMBER_OF_FAILED_TEST_CASES=0
|
||||||
|
FAILED_TEST_CASES=( )
|
||||||
|
|
||||||
|
local __QUIET=no __TEST_CASES=( )
|
||||||
|
|
||||||
|
arguments::parse \
|
||||||
|
"-t|test)__TEST_CASES;Array" \
|
||||||
|
"-q|--quiet)__QUIET;Yes" \
|
||||||
|
-- "$@" || panic "Cannot parse arguments. Arguments: $*"
|
||||||
|
|
||||||
|
# If no test cases are given via options
|
||||||
|
[ "${#__TEST_CASES[@]}" -gt 0 ] || {
|
||||||
|
# Then generate list of test cases using compgen
|
||||||
|
# As alternative, declare -F | cut -d ' ' -f 3 | grep '^test' can be used
|
||||||
|
__TEST_CASES=( $(compgen -A function test) ) || panic "No test cases are found. Create a function with test_ prefix in the name."
|
||||||
|
}
|
||||||
|
|
||||||
|
local __TEST __EXIT_CODE=0
|
||||||
|
|
||||||
|
( set -ueEo pipefail ; FIRST_TEAR_DOWN=yes ; unit::tear_down "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" ) || {
|
||||||
|
__EXIT_CODE=$?
|
||||||
|
log::error::custom "FAIL" "tear_down before first test case is failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
for __TEST in "${__TEST_CASES[@]:+${__TEST_CASES[@]}}"
|
||||||
|
do
|
||||||
|
let NUMBER_OF_TEST_CASES++ || :
|
||||||
|
[ "$__QUIET" == "yes" ] || echo -n "."
|
||||||
|
|
||||||
|
(
|
||||||
|
__EXIT_CODE=0
|
||||||
|
|
||||||
|
unit::set_up "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || {
|
||||||
|
__EXIT_CODE=$?
|
||||||
|
unit::fail "unit::set_up failed before test case #$NUMBER_OF_TEST_CASES ($__TEST)."
|
||||||
|
}
|
||||||
|
|
||||||
|
( "$__TEST" "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" ) || {
|
||||||
|
__EXIT_CODE=$?
|
||||||
|
unit::fail "Test case #$NUMBER_OF_TEST_CASES ($__TEST) failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::tear_down "${ARGUMENTS[@]:+${ARGUMENTS[@]}}" || {
|
||||||
|
__EXIT_CODE=$?
|
||||||
|
unit::fail "unit::tear_down failed after test case #$NUMBER_OF_TEST_CASES ($__TEST)."
|
||||||
|
}
|
||||||
|
exit $__EXIT_CODE # Exit from subshell
|
||||||
|
) || {
|
||||||
|
__EXIT_CODE=$?
|
||||||
|
let NUMBER_OF_FAILED_TEST_CASES++ || :
|
||||||
|
FAILED_TEST_CASES[${#FAILED_TEST_CASES[@]}]="$__TEST"
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$__QUIET" == "yes" ] || echo
|
||||||
|
if [ "$__EXIT_CODE" -eq 0 ]
|
||||||
|
then
|
||||||
|
[ "$__QUIET" == "yes" ] || log::info "OK" "Test cases total: $NUMBER_OF_TEST_CASES, failed: $NUMBER_OF_FAILED_TEST_CASES${FAILED_TEST_CASES[@]:+, failed methods: ${FAILED_TEST_CASES[@]}}."
|
||||||
|
else
|
||||||
|
log::error::custom "FAIL" "Test cases total: $NUMBER_OF_TEST_CASES, failed: $NUMBER_OF_FAILED_TEST_CASES${FAILED_TEST_CASES[@]:+, failed methods: ${FAILED_TEST_CASES[@]}}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $__EXIT_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> `unit::run_test_cases` will also call `unit::set_up` and `unit::tear_down`
|
||||||
|
#> functions before and after each test case. By default, they do nothing.
|
||||||
|
#> Override them to do something useful.
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::set_up` - can set variables which are available for following
|
||||||
|
#>> test case and `tear_down`. It also can alter `ARGUMENTS` array. Test case
|
||||||
|
#>> and tear_down are executed in their own subshell, so they cannot change
|
||||||
|
#>> outer variables.
|
||||||
|
unit::set_up() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#>>
|
||||||
|
#>> * `unit::tear_down` is called first, before first set_up of first test case, to
|
||||||
|
#>> cleanup after possible failed run of previous test case. When it
|
||||||
|
#>> called for first time, `FIRST_TEAR_DOWN` variable with value `"yes"` is
|
||||||
|
#>> available.
|
||||||
|
unit::tear_down() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#>
|
||||||
|
#> ## NOTES
|
||||||
|
#>
|
||||||
|
#> All assert functions are executing `exit` instead of returning error code.
|
137
helpers/helpers.v2.d/fail2ban
Normal file
137
helpers/helpers.v2.d/fail2ban
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Create a dedicated fail2ban config (jail and filter conf files)
|
||||||
|
#
|
||||||
|
# usage 1: ynh_add_fail2ban_config --logpath=log_file --failregex=filter [--max_retry=max_retry] [--ports=ports]
|
||||||
|
# | arg: -l, --logpath= - Log file to be checked by fail2ban
|
||||||
|
# | arg: -r, --failregex= - Failregex to be looked for by fail2ban
|
||||||
|
# | arg: -m, --max_retry= - Maximum number of retries allowed before banning IP address - default: 3
|
||||||
|
# | arg: -p, --ports= - Ports blocked for a banned IP address - default: http,https
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# usage 2: ynh_add_fail2ban_config --use_template
|
||||||
|
# | arg: -t, --use_template - Use this helper in template mode
|
||||||
|
#
|
||||||
|
# This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf`
|
||||||
|
# See the documentation of `ynh_add_config` for a description of the template
|
||||||
|
# format and how placeholders are replaced with actual variables.
|
||||||
|
#
|
||||||
|
# Generally your template will look like that by example (for synapse):
|
||||||
|
# ```
|
||||||
|
# f2b_jail.conf:
|
||||||
|
# [__APP__]
|
||||||
|
# enabled = true
|
||||||
|
# port = http,https
|
||||||
|
# filter = __APP__
|
||||||
|
# logpath = /var/log/__APP__/logfile.log
|
||||||
|
# maxretry = 3
|
||||||
|
# ```
|
||||||
|
# ```
|
||||||
|
# f2b_filter.conf:
|
||||||
|
# [INCLUDES]
|
||||||
|
# before = common.conf
|
||||||
|
# [Definition]
|
||||||
|
#
|
||||||
|
# # Part of regex definition (just used to make more easy to make the global regex)
|
||||||
|
# __synapse_start_line = .? \- synapse\..+ \-
|
||||||
|
#
|
||||||
|
# # Regex definition.
|
||||||
|
# failregex = ^%(__synapse_start_line)s INFO \- POST\-(\d+)\- <HOST> \- \d+ \- Received request\: POST /_matrix/client/r0/login\??<SKIPLINES>%(__synapse_start_line)s INFO \- POST\-\1\- Got login request with identifier: \{u'type': u'm.id.user', u'user'\: u'(.+?)'\}, medium\: None, address: None, user\: u'\5'<SKIPLINES>%(__synapse_start_line)s WARNING \- \- (Attempted to login as @\5\:.+ but they do not exist|Failed password login for user @\5\:.+)$
|
||||||
|
#
|
||||||
|
# ignoreregex =
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Note about the "failregex" option:
|
||||||
|
#
|
||||||
|
# regex to match the password failure messages in the logfile. The host must be
|
||||||
|
# matched by a group named "`host`". The tag "`<HOST>`" can be used for standard
|
||||||
|
# IP/hostname matching and is only an alias for `(?:::f{4,6}:)?(?P<host>[\w\-.^_]+)`
|
||||||
|
#
|
||||||
|
# You can find some more explainations about how to make a regex here :
|
||||||
|
# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters
|
||||||
|
#
|
||||||
|
# Note that the logfile need to exist before to call this helper !!
|
||||||
|
#
|
||||||
|
# To validate your regex you can test with this command:
|
||||||
|
# ```
|
||||||
|
# fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 4.1.0 or higher.
|
||||||
|
|
||||||
|
__FAIL2BAN_FILTER_TEMPLATE="[INCLUDES]
|
||||||
|
before = common.conf
|
||||||
|
[Definition]
|
||||||
|
failregex = __FAILREGEX__
|
||||||
|
ignoreregex =
|
||||||
|
"
|
||||||
|
|
||||||
|
__FAIL2BAN_JAIL_TEMPLATE="[__APP__]
|
||||||
|
enabled = true
|
||||||
|
port = __PORTS__
|
||||||
|
filter = __APP__
|
||||||
|
logpath = __LOGPATH__
|
||||||
|
maxretry = __MAX_RETRY__
|
||||||
|
"
|
||||||
|
|
||||||
|
|
||||||
|
ynh::fail2ban::add() {
|
||||||
|
local logpath
|
||||||
|
local failregex
|
||||||
|
local max_retry=3
|
||||||
|
local ports="http,https"
|
||||||
|
local jail_template filter_template
|
||||||
|
arguments::parse \
|
||||||
|
"-l|--logpath)logpath;String" \
|
||||||
|
"-r|--failregex)failregex;String" \
|
||||||
|
"-m|--max_retries)max_retries;String" \
|
||||||
|
"-p|--ports)ports;String" \
|
||||||
|
"-t|--jail_template)jail_template;String" \
|
||||||
|
"-t|--filter_template)filter_template;String" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
if string::empty "$filter_template"; then
|
||||||
|
filter_template="fail2ban_filter.conf"
|
||||||
|
# Mandatory arguments
|
||||||
|
if string::empty "failregex"; then
|
||||||
|
log::panic "No failregex was passed to ${FUNCNAME[0]}"
|
||||||
|
fi
|
||||||
|
echo "$__FAIL2BAN_FILTER_TEMPLATE" > "$YNH_APP_BASEDIR/conf/$filter_template"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if string::empty "$jail_template"; then
|
||||||
|
jail_template="fail2ban_jail.conf"
|
||||||
|
# Mandatory arguments
|
||||||
|
if string::empty "logpath"; then
|
||||||
|
log::panic "No logpath was passed to ${FUNCNAME[0]}"
|
||||||
|
fi
|
||||||
|
echo "$__FAIL2BAN_JAIL_TEMPLATE" > "$YNH_APP_BASEDIR/conf/$jail_template"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ynh::config::add --template="$filter_template" --destination="/etc/fail2ban/filter.d/$app.conf"
|
||||||
|
ynh::config::add --template="$jail_template" --destination="/etc/fail2ban/jail.d/$app.conf"
|
||||||
|
|
||||||
|
ynh::system::action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd
|
||||||
|
|
||||||
|
local fail2ban_error
|
||||||
|
fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")"
|
||||||
|
if ! string::empty "$fail2ban_error"; then
|
||||||
|
log::err "Fail2ban failed to load the jail for ${app}:"
|
||||||
|
log::warn "${fail2ban_error#*WARNING}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the dedicated fail2ban config (jail and filter conf files)
|
||||||
|
#
|
||||||
|
# usage: ynh_remove_fail2ban_config
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 3.5.0 or higher.
|
||||||
|
ynh::fail2ban::remove() {
|
||||||
|
ynh::fs::remove --file="/etc/fail2ban/filter.d/$app.conf"
|
||||||
|
ynh::fs::remove --file="/etc/fail2ban/jail.d/$app.conf"
|
||||||
|
ynh::systemd::action --service_name=fail2ban --action=reload
|
||||||
|
}
|
20
helpers/helpers.v2.d/import_bash-modules
Normal file
20
helpers/helpers.v2.d/import_bash-modules
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
_modules=(
|
||||||
|
arguments
|
||||||
|
array
|
||||||
|
# cd_to_bindir
|
||||||
|
date
|
||||||
|
log
|
||||||
|
log_run
|
||||||
|
meta
|
||||||
|
# renice
|
||||||
|
settings
|
||||||
|
strict
|
||||||
|
string
|
||||||
|
# timestamped_log
|
||||||
|
# unit
|
||||||
|
)
|
||||||
|
|
||||||
|
BASH_MODULES_PATH="$YNH_APP_HELPERS_DIR/bash-modules"
|
||||||
|
source "$BASH_MODULES_PATH/import.sh" "${_modules[@]}"
|
151
helpers/helpers.v2.d/setting
Normal file
151
helpers/helpers.v2.d/setting
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Small "hard-coded" interface to avoid calling "yunohost app" directly each
|
||||||
|
# time dealing with a setting is needed (which may be so slow on ARM boards)
|
||||||
|
#
|
||||||
|
# [internal]
|
||||||
|
#
|
||||||
|
ynh::_setting() {
|
||||||
|
python3 -c "$__YNH_SETTING_PYTHON_ACCESS" "$1" "$2" "$3" "${4:-}"
|
||||||
|
}
|
||||||
|
__YNH_SETTING_PYTHON_ACCESS=$(cat << EOF
|
||||||
|
import os, sys, yaml
|
||||||
|
_, app, action, key, value = sys.argv
|
||||||
|
action = action.lower()
|
||||||
|
|
||||||
|
setting_file = "/etc/yunohost/apps/%s/settings.yml" % app
|
||||||
|
assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file
|
||||||
|
with open(setting_file) as f:
|
||||||
|
settings = yaml.safe_load(f)
|
||||||
|
if action == "get":
|
||||||
|
if key in settings:
|
||||||
|
print(settings[key])
|
||||||
|
else:
|
||||||
|
if action == "delete":
|
||||||
|
if key in settings:
|
||||||
|
del settings[key]
|
||||||
|
elif action == "set":
|
||||||
|
if key in ['redirected_urls', 'redirected_regex']:
|
||||||
|
value = yaml.safe_load(value)
|
||||||
|
settings[key] = value
|
||||||
|
else:
|
||||||
|
raise ValueError("action should either be get, set or delete")
|
||||||
|
with open(setting_file, "w") as f:
|
||||||
|
yaml.safe_dump(settings, f, default_flow_style=False)
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get an application setting
|
||||||
|
#
|
||||||
|
# usage: ynh::setting::get [--app=app] --key=key
|
||||||
|
# | arg: -a, --app= - the application id
|
||||||
|
# | arg: -k, --key= - the setting to get
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh::setting::get() {
|
||||||
|
local app="$YNH_APP_ID"
|
||||||
|
local key
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--app)app;String" \
|
||||||
|
"-k|--key)key;String,R" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
if string::starts_with "$key" unprotected_ \
|
||||||
|
|| string::starts_with "$key" protected_ \
|
||||||
|
|| string::starts_with "$key" skipped_; then
|
||||||
|
yunohost app setting "$app" "$key"
|
||||||
|
else
|
||||||
|
ynh::_setting get "$app" "$key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set an application setting
|
||||||
|
#
|
||||||
|
# usage: ynh_app_setting_set --app=app --key=key --value=value
|
||||||
|
# | arg: -a, --app= - the application id
|
||||||
|
# | arg: -k, --key= - the setting name to set
|
||||||
|
# | arg: -v, --value= - the setting value to set
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh::setting::set() {
|
||||||
|
local app="$YNH_APP_ID"
|
||||||
|
local key value
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--app)app;String" \
|
||||||
|
"-k|--key)key;String,R" \
|
||||||
|
"-v|--value)value;String,R" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
if string::starts_with "$key" unprotected_ \
|
||||||
|
|| string::starts_with "$key" protected_ \
|
||||||
|
|| string::starts_with "$key" skipped_; then
|
||||||
|
yunohost app setting "$app" "$key" -v "$value"
|
||||||
|
else
|
||||||
|
ynh::_setting set "$app" "$key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get an application setting
|
||||||
|
#
|
||||||
|
# usage: ynh::setting::get [--app=app] --key=key
|
||||||
|
# | arg: -a, --app= - the application id
|
||||||
|
# | arg: -k, --key= - the setting to get
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh::setting::delete() {
|
||||||
|
local app="$YNH_APP_ID"
|
||||||
|
local key
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--app)app;String" \
|
||||||
|
"-k|--key)key;String,R" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
if string::starts_with "$key" unprotected_ \
|
||||||
|
|| string::starts_with "$key" protected_ \
|
||||||
|
|| string::starts_with "$key" skipped_; then
|
||||||
|
yunohost app setting "$app" "$key" -d
|
||||||
|
else
|
||||||
|
ynh::_setting delete "$app" "$key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check availability of a web path
|
||||||
|
#
|
||||||
|
# usage: ynh_webpath_available --domain=domain --path_url=path
|
||||||
|
# | arg: -d, --domain= - the domain/host of the url
|
||||||
|
# | arg: -p, --path_url= - the web path to check the availability of
|
||||||
|
#
|
||||||
|
# example: ynh_webpath_available --domain=some.domain.tld --path_url=/coffee
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.6.4 or higher.
|
||||||
|
ynh::webpath::is_available() {
|
||||||
|
local domain path
|
||||||
|
arguments::parse \
|
||||||
|
"-d|--domain)domain;String,R" \
|
||||||
|
"-p|--path)path;String,R" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
yunohost domain url-available "$domain" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register/book a web path for an app
|
||||||
|
#
|
||||||
|
# usage: ynh_webpath_register --app=app --domain=domain --path_url=path
|
||||||
|
# | arg: -a, --app= - the app for which the domain should be registered
|
||||||
|
# | arg: -d, --domain= - the domain/host of the web path
|
||||||
|
# | arg: -p, --path_url= - the web path to be registered
|
||||||
|
#
|
||||||
|
# example: ynh_webpath_register --app=wordpress --domain=some.domain.tld --path_url=/coffee
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.6.4 or higher.
|
||||||
|
ynh::webpath::register() {
|
||||||
|
local app="$YNH_APP_ID"
|
||||||
|
local domain path
|
||||||
|
arguments::parse \
|
||||||
|
"-a|--app)app;String" \
|
||||||
|
"-d|--domain)domain;String,R" \
|
||||||
|
"-p|--path)path;String,R" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
yunohost domain register-url "$app" "$domain" "$path"
|
||||||
|
}
|
174
helpers/helpers.v2.d/systemd
Normal file
174
helpers/helpers.v2.d/systemd
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Create a dedicated systemd config
|
||||||
|
#
|
||||||
|
# usage: ynh_add_systemd_config [--service=service] [--template=template]
|
||||||
|
# | arg: -s, --service= - Service name (optionnal, `$app` by default)
|
||||||
|
# | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning `../conf/systemd.service` will be used as template)
|
||||||
|
#
|
||||||
|
# This will use the template `../conf/<templatename>.service`.
|
||||||
|
#
|
||||||
|
# See the documentation of `ynh_add_config` for a description of the template
|
||||||
|
# format and how placeholders are replaced with actual variables.
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 4.1.0 or higher.
|
||||||
|
|
||||||
|
ynh::systemd::add_service() {
|
||||||
|
local service="$app"
|
||||||
|
local template=systemd.service
|
||||||
|
arguments::parse \
|
||||||
|
"-s|--service)service;String" \
|
||||||
|
"-t|--template)template;String" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
ynh::config::add --template="$template" --destination="/etc/systemd/system/$service.service"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$service" --quiet
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the dedicated systemd config
|
||||||
|
#
|
||||||
|
# usage: ynh_remove_systemd_config [--service=service]
|
||||||
|
# | arg: -s, --service= - Service name (optionnal, $app by default)
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.7.2 or higher.
|
||||||
|
ynh::systemd::remove_service() {
|
||||||
|
local service="$app"
|
||||||
|
arguments::parse \
|
||||||
|
"-s|--service)service;String" \
|
||||||
|
-- "$@"
|
||||||
|
|
||||||
|
local service_file="/etc/systemd/system/$service.service"
|
||||||
|
if [ -e "$service_file" ]; then
|
||||||
|
ynh_systemd_action --service="$service" --action=stop
|
||||||
|
systemctl disable "$service" --quiet
|
||||||
|
ynh::fs::remove --file="$service_file"
|
||||||
|
systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started
|
||||||
|
#
|
||||||
|
# usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ]
|
||||||
|
# | arg: -n, --service_name= - Name of the service to start. Default : `$app`
|
||||||
|
# | arg: -a, --action= - Action to perform with systemctl. Default: start
|
||||||
|
# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started.
|
||||||
|
# | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log`
|
||||||
|
# | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds.
|
||||||
|
# | arg: -e, --length= - Length of the error log displayed for debugging : Default : 20
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 3.5.0 or higher.
|
||||||
|
ynh::systemd::action() {
|
||||||
|
local service="$app"
|
||||||
|
local action
|
||||||
|
local line_match
|
||||||
|
local log_path="/var/log/$app/$app.log"
|
||||||
|
local timeout=300
|
||||||
|
local length=20
|
||||||
|
arguments::parse \
|
||||||
|
"-s|--service)service;String" \
|
||||||
|
"-a|--action)action;String,R" \
|
||||||
|
"-m|--line_match)line_match;String" \
|
||||||
|
"-p|--log_path)log_path;String" \
|
||||||
|
"-t|--timeout)timeout;String" \
|
||||||
|
"-l|--length)length;String" \
|
||||||
|
|
||||||
|
|
||||||
|
# Manage case of service already stopped
|
||||||
|
if [ "$action" == "stop" ] && ! systemctl is-active --quiet "$service"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start to read the log
|
||||||
|
if ! string::empty "$line_match"; then
|
||||||
|
local templog
|
||||||
|
templog="$(mktemp)"
|
||||||
|
cleanup::add "Systemd action log file" "rm -f \"$templog\""
|
||||||
|
# Following the starting of the app in its log
|
||||||
|
if [ "$log_path" == "systemd" ]; then
|
||||||
|
# Read the systemd journal
|
||||||
|
journalctl --unit="$service" --follow --since=-0 --quiet >"$templog" &
|
||||||
|
local pid_tail=$!
|
||||||
|
else
|
||||||
|
# Read the specified log file
|
||||||
|
tail --follow=name --retry --lines=0 "$log_path" >"$templog" 2>&1 &
|
||||||
|
local pid_tail=$!
|
||||||
|
fi
|
||||||
|
cleanup::add "Systemd action log process" "kill -SIGTERM $pid_tail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use reload-or-restart instead of reload. So it wouldn't fail if the service isn't running.
|
||||||
|
if [ "$action" == "reload" ]; then
|
||||||
|
action="reload-or-restart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local time_start
|
||||||
|
time_start="$(date --utc --rfc-3339=seconds | cut -d+ -f1) UTC"
|
||||||
|
|
||||||
|
# If the service fails to perform the action
|
||||||
|
if ! systemctl "$action" "$service"; then
|
||||||
|
# Show syslog for this service
|
||||||
|
run::error journalctl --quiet --no-hostname --no-pager --lines="$length" --unit="$service"
|
||||||
|
# If a log is specified for this service, show also the content of this log
|
||||||
|
if ! string::empty "$log_path"; then
|
||||||
|
run::error tail --lines=$length "$log_path"
|
||||||
|
fi
|
||||||
|
ynh::systemd::_action_cleanup
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! string::empty "$line_match"; then
|
||||||
|
local start_time max_time long_time is_long timed_out
|
||||||
|
start_time=$(date +%s)
|
||||||
|
long_time=$(( start_time + 30 ))
|
||||||
|
max_time=$(( start_time + timeout ))
|
||||||
|
set +x
|
||||||
|
while true; do
|
||||||
|
# Read the log until the sentence is found, that means the app finished to start. Or run until the timeout
|
||||||
|
if [ "$log_path" == "systemd" ]; then
|
||||||
|
# For systemd services, we in fact dont rely on the templog, which for some reason is not reliable, but instead re-read journalctl every iteration, starting at the timestamp where we triggered the action
|
||||||
|
if journalctl --unit="$service" --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$line_match"; then
|
||||||
|
log::info "The service $service has correctly executed the action $action."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if grep --extended-regexp --quiet "$line_match" "$templog"; then
|
||||||
|
log::info "The service $service has correctly executed the action $action."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if string::empty "$is_long" && (( $(date +%s) >= long_time )); then
|
||||||
|
is_long=true
|
||||||
|
log::warn "(this may take some time)"
|
||||||
|
fi
|
||||||
|
if (( $(date +%s) < max_time )); then
|
||||||
|
timed_out=true
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
set -x
|
||||||
|
|
||||||
|
if [[ "$timed_out" == "true" ]]; then
|
||||||
|
log::warn "The service $service didn't fully executed the action ${action} before the timeout.\n"
|
||||||
|
log::warn "Please find here an extract of the end of the log of the service $service:"
|
||||||
|
run::warn journalctl --quiet --no-hostname --no-pager --lines="$length" --unit="$service"
|
||||||
|
if [ -e "$log_path" ]; then
|
||||||
|
log::warn --message="\-\-\-"
|
||||||
|
run::warn tail --lines=$length "$log_path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
ynh::systemd::_action_cleanup
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean temporary process and file used by ynh_check_starting
|
||||||
|
#
|
||||||
|
# [internal]
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 3.5.0 or higher.
|
||||||
|
|
||||||
|
ynh::systemd::_action_cleanup() {
|
||||||
|
cleanup::pop "Systemd action log process"
|
||||||
|
cleanup::pop "Systemd action log file"
|
||||||
|
}
|
188
helpers/helpers.v2.d/user
Normal file
188
helpers/helpers.v2.d/user
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if a YunoHost user exists
|
||||||
|
#
|
||||||
|
# usage: ynh_user_exists --username=username
|
||||||
|
# | arg: -u, --username= - the username to check
|
||||||
|
# | ret: 0 if the user exists, 1 otherwise.
|
||||||
|
#
|
||||||
|
# example: ynh_user_exists 'toto' || echo "User does not exist"
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh_user_exists() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=u
|
||||||
|
local -A args_array=([u]=username=)
|
||||||
|
local username
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
|
||||||
|
yunohost user list --output-as json --quiet | jq -e ".users.\"${username}\"" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrieve a YunoHost user information
|
||||||
|
#
|
||||||
|
# usage: ynh_user_get_info --username=username --key=key
|
||||||
|
# | arg: -u, --username= - the username to retrieve info from
|
||||||
|
# | arg: -k, --key= - the key to retrieve
|
||||||
|
# | ret: the value associate to that key
|
||||||
|
#
|
||||||
|
# example: mail=$(ynh_user_get_info --username="toto" --key=mail)
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh_user_get_info() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=uk
|
||||||
|
local -A args_array=([u]=username= [k]=key=)
|
||||||
|
local username
|
||||||
|
local key
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
|
||||||
|
yunohost user info "$username" --output-as json --quiet | jq -r ".$key"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the list of YunoHost users
|
||||||
|
#
|
||||||
|
# usage: ynh_user_list
|
||||||
|
# | ret: one username per line as strings
|
||||||
|
#
|
||||||
|
# example: for u in $(ynh_user_list); do ... ; done
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.4.0 or higher.
|
||||||
|
ynh_user_list() {
|
||||||
|
yunohost user list --output-as json --quiet | jq -r ".users | keys[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a user exists on the system
|
||||||
|
#
|
||||||
|
# usage: ynh_system_user_exists --username=username
|
||||||
|
# | arg: -u, --username= - the username to check
|
||||||
|
# | ret: 0 if the user exists, 1 otherwise.
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.2.4 or higher.
|
||||||
|
ynh_system_user_exists() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=u
|
||||||
|
local -A args_array=([u]=username=)
|
||||||
|
local username
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
|
||||||
|
getent passwd "$username" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a group exists on the system
|
||||||
|
#
|
||||||
|
# usage: ynh_system_group_exists --group=group
|
||||||
|
# | arg: -g, --group= - the group to check
|
||||||
|
# | ret: 0 if the group exists, 1 otherwise.
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 3.5.0.2 or higher.
|
||||||
|
ynh_system_group_exists() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=g
|
||||||
|
local -A args_array=([g]=group=)
|
||||||
|
local group
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
|
||||||
|
getent group "$group" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a system user
|
||||||
|
#
|
||||||
|
# usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell] [--groups="group1 group2"]
|
||||||
|
# | arg: -u, --username= - Name of the system user that will be create
|
||||||
|
# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home
|
||||||
|
# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
|
||||||
|
# | arg: -g, --groups - Add the user to system groups. Typically meant to add the user to the ssh.app / sftp.app group (e.g. for borgserver, my_webapp)
|
||||||
|
#
|
||||||
|
# Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) :
|
||||||
|
# ```
|
||||||
|
# ynh_system_user_create --username=nextcloud
|
||||||
|
# ```
|
||||||
|
# Create a discourse user using /var/www/discourse as home directory and the default login shell :
|
||||||
|
# ```
|
||||||
|
# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.6.4 or higher.
|
||||||
|
ynh_system_user_create() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=uhs
|
||||||
|
local -A args_array=([u]=username= [h]=home_dir= [s]=use_shell [g]=groups=)
|
||||||
|
local username
|
||||||
|
local home_dir
|
||||||
|
local use_shell
|
||||||
|
local groups
|
||||||
|
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
use_shell="${use_shell:-0}"
|
||||||
|
home_dir="${home_dir:-}"
|
||||||
|
groups="${groups:-}"
|
||||||
|
|
||||||
|
if ! ynh_system_user_exists "$username"; then # Check if the user exists on the system
|
||||||
|
# If the user doesn't exist
|
||||||
|
if [ -n "$home_dir" ]; then # If a home dir is mentioned
|
||||||
|
local user_home_dir="--home-dir $home_dir"
|
||||||
|
else
|
||||||
|
local user_home_dir="--no-create-home"
|
||||||
|
fi
|
||||||
|
if [ $use_shell -eq 1 ]; then # If we want a shell for the user
|
||||||
|
local shell="" # Use default shell
|
||||||
|
else
|
||||||
|
local shell="--shell /usr/sbin/nologin"
|
||||||
|
fi
|
||||||
|
useradd $user_home_dir --system --user-group $username $shell || ynh_die --message="Unable to create $username system account"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local group
|
||||||
|
for group in $groups; do
|
||||||
|
usermod -a -G "$group" "$username"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete a system user
|
||||||
|
#
|
||||||
|
# usage: ynh_system_user_delete --username=user_name
|
||||||
|
# | arg: -u, --username= - Name of the system user that will be create
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 2.6.4 or higher.
|
||||||
|
ynh_system_user_delete() {
|
||||||
|
# Declare an array to define the options of this helper.
|
||||||
|
local legacy_args=u
|
||||||
|
local -A args_array=([u]=username=)
|
||||||
|
local username
|
||||||
|
# Manage arguments with getopts
|
||||||
|
ynh_handle_getopts_args "$@"
|
||||||
|
|
||||||
|
# Check if the user exists on the system
|
||||||
|
if ynh_system_user_exists "$username"; then
|
||||||
|
deluser $username
|
||||||
|
else
|
||||||
|
ynh_print_warn --message="The user $username was not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the group exists on the system
|
||||||
|
if ynh_system_group_exists "$username"; then
|
||||||
|
delgroup $username
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute a command as another user
|
||||||
|
#
|
||||||
|
# usage: ynh_exec_as $USER COMMAND [ARG ...]
|
||||||
|
#
|
||||||
|
# Requires YunoHost version 4.1.7 or higher.
|
||||||
|
ynh_exec_as() {
|
||||||
|
local user=$1
|
||||||
|
shift 1
|
||||||
|
|
||||||
|
if [[ $user = $(whoami) ]]; then
|
||||||
|
eval "$@"
|
||||||
|
else
|
||||||
|
sudo -u "$user" "$@"
|
||||||
|
fi
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue