diff --git a/.framework-config b/.framework-config index a27032a9..d2f5d861 100755 --- a/.framework-config +++ b/.framework-config @@ -16,3 +16,5 @@ FRAMEWORK_SRC_DIRS=( ) export REPOSITORY_URL="https://github.com/fchastanet/bash-tools" + +export BASH_FRAMEWORK_DISPLAY_LEVEL="3" diff --git a/.gitignore b/.gitignore index 0807fb4b..9bb824d1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ /bin/test /bin/findShebangFiles /bin/buildPushDockerImages +/bin/buildPushDockerImage /bin/frameworkLint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea638e8f..8f0c0f15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,8 @@ repos: - id: check-json exclude: | (?x)^( - conf\/.vscode\/settings.json + conf\/.vscode\/settings.json| + .vscode\/launch.json )$ - repo: https://github.com/jumanjihouse/pre-commit-hooks @@ -54,7 +55,7 @@ repos: exclude: /testsData/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.6 + rev: v3.0.3 hooks: - id: prettier diff --git a/.vscode/extensions.json b/.vscode/extensions.json index aa3e215b..3858c590 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,7 @@ "exiasr.hadolint", "shd101wyy.markdown-preview-enhanced", "monosans.djlint", - "foxundermoon.shell-format" + "foxundermoon.shell-format", + "rogalmic.bash-debug" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..bbdc225b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "bashdb", + "request": "launch", + "name": "Bash-Debug (generic)", + "program": "${file}" + }, + { + "type": "bashdb", + "request": "launch", + "name": "Bash-Debug (mysql2puml)", + "program": "bin/mysql2puml", + "args": ["src/_binaries/DbImport/testsData/dump.sql"] + } + ] +} diff --git a/TODO.md b/TODO.md index 5cdbd971..69925afd 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,9 @@ # Todo +- report megalinter changes yml + github from framework to this repo +- get rid of install command + - each command should call a similar function before running + - could include installRequirements too - display supported matrix (bash version, linux version) - dbImportStream ability to import from dbAuthFile internally or from db parameters @@ -13,7 +17,5 @@ - src/build/install.sh use backupDir - - -- I don't understand where the code is executed if not - using lite version - add code coverage - upload code coverage to deepsource using github action diff --git a/bin/cli b/bin/cli index 42bbcead..c572642a 100755 --- a/bin/cli +++ b/bin/cli @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Docker/cli.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Docker/cli.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/cli +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,124 +86,133 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + if [[ "${needEcho}" = "1" ]]; then + echo fi - return "${status}" } -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' - fi - done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") -} +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." -# Public: check if command specified exists or return 1 +# @description check if command specified exists or return 1 # with error and message if not # -# **Arguments**: -# * $1 commandName on which existence must be checked -# * $2 helpIfNotExists a help command to display if the command does not exist +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist # -# **Exit**: code 1 if the command specified does not exist +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided Assert::commandExists() { local commandName="$1" local helpIfNotExists="$2" @@ -213,45 +227,31 @@ Assert::commandExists() { return 0 } -# Public: exits with message if current user is root -# -# **Exit**: code 1 if current user is root -Assert::expectNonRootUser() { - if [[ "$(id -u)" = "0" ]]; then - Log::fatal "The script must not be run as root" - fi -} - -# Public: determine if the script is executed under windows +# @description determine if the script is executed under windows (using wsl) # cspell:disable -#
-# uname GitBash windows (with wsl) => MINGW64_NT-10.0 ZOXFL-6619QN2 2.10.0(0.325/5/3) 2018-06-13 23:34 x86_64 Msys
-# uname GitBash windows (wo wsl)   => MINGW64_NT-10.0 frsa02-j5cbkc2 2.9.0(0.318/5/3) 2018-01-12 23:37 x86_64 Msys
-# uname wsl => Linux ZOXFL-6619QN2 4.4.0-17134-Microsoft #112-Microsoft Thu Jun 07 22:57:00 PST 2018 x86_64 x86_64 x86_64 GNU/Linux
-# 
+# @example text +# uname GitBash windows (with wsl) => MINGW64_NT-10.0 ZOXFL-6619QN2 2.10.0(0.325/5/3) 2018-06-13 23:34 x86_64 Msys +# uname GitBash windows (wo wsl) => MINGW64_NT-10.0 frsa02-j5cbkc2 2.9.0(0.318/5/3) 2018-01-12 23:37 x86_64 Msys +# uname wsl => Linux ZOXFL-6619QN2 4.4.0-17134-Microsoft #112-Microsoft Thu Jun 07 22:57:00 PST 2018 x86_64 x86_64 x86_64 GNU/Linux # cspell:enable # -# @return 1 on error +# @exitcode 1 on error Assert::windows() { - if [[ "$(uname -o)" = "Msys" ]]; then - return 0 - else - return 1 - fi + [[ "$(uname -o)" = "Msys" ]] } -# Public: get absolute conf file from specified conf folder deduced using these rules +# @description get absolute conf file from specified conf folder deduced using these rules # * from absolute file (ignores and ) # * relative to where script is executed (ignores and ) # * from home/.bash-tools/ # * from framework conf/ # -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 conf file to use without extension -# * $3 the extension (sh by default) +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String the extension (.sh by default) # -# Returns absolute conf filename +# @stdout absolute conf filename +# @exitcode 1 if file is not found in any location Conf::getAbsoluteFile() { local confFolder="$1" local conf="$2" @@ -301,22 +301,22 @@ Conf::getAbsoluteFile() { return 1 } -# Public: list the conf files list available in bash-tools/conf/ folder +# @description list the conf files list available in bash-tools/conf/ folder # and those overridden in ${HOME}/.bash-tools/ folder -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 the extension (sh by default) -# * $3 the indentation (' - ' by default) can be any string compatible with sed not containing any / # -# **Output**: list of files without extension/directory -# eg: +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 extension:String the extension (.sh by default) +# @arg $3 indentStr:String the indentation (' - ' by default) can be any string compatible with sed not containing any / +# +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root Conf::getMergedList() { local confFolder="$1" - local extension="${2:-sh}" - local indentStr="${3:- - }" + local extension="${2-sh}" + local indentStr="${3- - }" local DEFAULT_CONF_DIR="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}" local HOME_CONF_DIR="${HOME}/.bash-tools/${confFolder}" @@ -331,17 +331,16 @@ Conf::getMergedList() { ) | sort | uniq } -# Public: get absolute file from name deduced using these rules +# @description get absolute file from name deduced using these rules # * using absolute/relative file (ignores and # * from home/.bash-tools// file # * from framework conf/ file # -# **Arguments**: -# * $1 confFolder to use below bash-tools conf folder -# * $2 conf file to use without extension -# * $3 file extension to use (default: sh) +# @arg $1 confFolder:String directory to use (traditionally below bash-tools conf folder) +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String file extension to use (default: .sh) # -# Returns 1 if file not found or error during file loading +# @exitcode 1 if file not found or error during file loading Conf::load() { local confFolder="$1" local conf="$2" @@ -369,273 +368,332 @@ Conf::load() { source "${confFile}" } -# Internal: check if dsn file has all the mandatory variables set +# @description check if dsn file has all the mandatory variables set # Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # -# **Arguments**: -# * $1 - dsn absolute filename -# -# Returns 0 on valid file, 1 otherwise with log output +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file Database::checkDsnFile() { - local DSN_FILENAME="$1" - if [[ ! -f "${DSN_FILENAME}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} not found" + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" return 1 fi ( unset HOSTNAME PORT PASSWORD USER # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${DSN_FILENAME}" + source "${dsnFileName}" if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : HOSTNAME not provided" + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" return 1 fi if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : HOSTNAME value not provided" + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" fi if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" fi if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT not provided" + Log::displayError "dsn file ${dsnFileName} : PORT not provided" return 1 fi if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT invalid" + Log::displayError "dsn file ${dsnFileName} : PORT invalid" return 1 fi if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : USER not provided" + Log::displayError "dsn file ${dsnFileName} : USER not provided" return 1 fi if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PASSWORD not provided" + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" return 1 fi ) } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Public: list files of dir with given extension and display it as a list one by line +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} + +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi +} + +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description list files of dir with given extension and display it as a list one by line # -# @param {String} dir $1 the directory to list -# @param {String} prefix $2 the profile file prefix (default: "") -# @param {String} ext $3 the extension -# @param {String} findOptions $4 find options, eg: -type d -# @paramDefault {String} findOptions $4 '-type f' -# @param {String} indentStr $5 the indentation can be any string compatible with sed not containing any / -# @paramDefault {String} indentStr $5 ' - ' -# @output list of files without extension/directory -# eg: +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root -# @return 1 if directory does not exists +# @exitcode 1 if directory does not exists Conf::list() { local dir="$1" local prefix="${2:-}" @@ -658,115 +716,237 @@ Conf::list() { ) } -File::concatenatePath() { - local basePath="${1}" - local subPath=${2} - local fullPath="${basePath:+${basePath}/}${subPath}" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done - realpath -m "${fullPath}" 2>/dev/null + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done +} + +# @description concatenate 2 paths and ensure the path is correct using realpath -m +# @arg $1 basePath:String +# @arg $2 subPath:String +# @require Linux::requireRealpathCommand +File::concatenatePath() { + local basePath="$1" + local subPath="$2" + local fullPath="${basePath:+${basePath}/}${subPath}" + + realpath -m "${fullPath}" 2>/dev/null } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -785,222 +965,898 @@ Log::rotate() { fi } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} - dir="$(dirname "${file}")" +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_738fd7f6601040de82a4a46d9d2efa6f() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -Log::load +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Linux::requireRealpathCommand +Log::requireLoad +Compiler::Facade::requireCommandBinDir + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + cliCommand help + exit 0 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 +} -Assert::expectNonRootUser +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -SCRIPT_NAME=${0##*/} -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/cliProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/cliProfiles" +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -showHelp() { - local containers - containers=$(docker ps --format '{{.Names}}' | sed -E 's/[^-]+-(.*)/\1/' | paste -sd "," -) - local profilesList="" - Conf::load "cliProfiles" "default" +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} - profilesList="$(Conf::getMergedList "cliProfiles" ".sh" || true)" +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} - cat <] [user] [command] +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} - : container should be one of these values (provided by 'docker ps'): - ${containers} - if not provided, it will load the container specified in default configuration (${finalContainerArg}) +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} -${__HELP_TITLE}examples:${__HELP_NORMAL} +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +cliCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountContainerArg + ((options_parse_argParsedCountContainerArg = 0)) || true + local -i options_parse_argParsedCountUserArg + ((options_parse_argParsedCountUserArg = 0)) || true + local -i options_parse_argParsedCountCommandArg + ((options_parse_argParsedCountCommandArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/14 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 2/14 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 3/14 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 4/14 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 5/14 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 6/14 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 7/14 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 8/14 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 9/14 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 10/14 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 11/14 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 12/14 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 13/14 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 14/14 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + unknownOption "${options_parse_arg}" + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/3 + # Argument containerArg min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountContainerArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument container - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountContainerArg)) + containerArg="${options_parse_arg}" + # Argument 2/3 + # Argument userArg min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountUserArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument user - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountUserArg)) + userArg="${options_parse_arg}" + # Argument 3/3 + # Argument commandArg min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 2 && options_parse_parsedArgIndex < 3)); then + if ((options_parse_argParsedCountCommandArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument commandArg - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountCommandArg)) + commandArg="${options_parse_arg}" + else + unknownOption "${options_parse_arg}" + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "easy connection to docker container")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " [${__HELP_OPTION_COLOR}container${__HELP_NORMAL} {single}]" + echo -n " " + Array::wrap ' ' 76 4 "$(containerArgHelpCallback)" + echo -e " [${__HELP_OPTION_COLOR}user${__HELP_NORMAL} {single}]" + echo -n " " + Array::wrap ' ' 76 4 "$(userArgHelpCallback)" + echo -e " [${__HELP_OPTION_COLOR}commandArg${__HELP_NORMAL} {single}]" + echo -n " " + Array::wrap ' ' 76 4 "$(commandArgHelpCallback)" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}AVAILABLE PROFILES (from ${PROFILES_DIR})${__HELP_NORMAL} +This list can be overridden in ${HOME_PROFILES_DIR} + +${profilesList} + +${__HELP_TITLE}AVAILABLE CONTAINERS:${__HELP_NORMAL} +${containers} + +${__HELP_TITLE}EXAMPLES:${__HELP_EXAMPLE} to connect to mysql container in bash mode with user mysql ${SCRIPT_NAME} mysql mysql "//bin/bash" to connect to web container with user root ${SCRIPT_NAME} web root +${__HELP_NORMAL} + +${__HELP_TITLE}CREATE NEW PROFILE:${__HELP_NORMAL} +You can create new profiles in ${HOME_PROFILES_DIR}. +This script will be called with the +arguments ${__HELP_OPTION_COLOR}userArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}containerArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}commandArg${__HELP_NORMAL} +The script has to compute the following +variables ${__HELP_OPTION_COLOR}finalUserArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}finalContainerArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}finalCommandArg${__HELP_NORMAL}""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Docker/cli.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} -you can override these mappings by providing your own profile in ${CLI_PROFILE_HOME} +# default values +declare containerArg="default" +declare finalUserArg="root" +declare finalCommandArg=("//bin/sh") +declare copyrightBeginYear="2020" -This script will be executed with the variables userArg containerArg commandArg set as specified in command line -and should provide value for the following variables finalUserArg finalContainerArg finalCommandArg +# constants +PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/cliProfiles" +HOME_PROFILES_DIR="${HOME}/.bash-tools/cliProfiles" -${__HELP_TITLE}List of available profiles (from ${PROFILES_DIR} and can be overridden in ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} +containerArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "container should be the name of a profile from profile list," + echo "check containers list below." $'\n' + echo "If not provided, it will load the container specified in default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" $'\n' + echo "Default container: ${__HELP_OPTION_COLOR}${finalContainerArg}${__HELP_NORMAL}" +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +userArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "user to connect on this container" $'\n' + echo "Default user: ${__HELP_OPTION_COLOR}${finalUserArg}${__HELP_NORMAL}" $'\n' + echo " loaded from profile selected as first arg" $'\n' + echo " or deduced from default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" + echo "if first arg is not a profile" +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Docker/cli.sh +commandArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "The command to execute" $'\n' + echo "Default command: ${__HELP_OPTION_COLOR}${finalCommandArg[*]}${__HELP_NORMAL}" $'\n' + echo " loaded from profile selected as first arg" $'\n' + echo " or deduced from default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" + echo "if first arg is not a profile" +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +optionHelpCallback() { + local containers + # shellcheck disable=SC2046 + containers="$(Array::wrap ", " 80 0 $(docker ps --format '{{.Names}}'))" + local profilesList="" + Conf::load "cliProfiles" "default" -Copyright (c) 2022 François Chastanet -EOF -} + profilesList="$(Conf::getMergedList "cliProfiles" ".sh" " - " || true)" -# Internal function that can be used in conf profiles to load the dsn file -loadDsn() { - local dsn="$1" - local dsnFile - dsnFile="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" - Database::checkDsnFile "${dsnFile}" - # shellcheck source=/conf/dsn/default.local.env - # shellcheck disable=SC1091 - source "${dsnFile}" + cliCommand help | envsubst + exit 0 } -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help -o h -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" +# shellcheck disable=SC2317 # if function is overridden +unknownOption() { + commandArg+=("$1") } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done +cliCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -declare containerArg="$1" -declare userArg -declare -a commandArg -if shift; then - userArg="$1" -fi -if shift; then - commandArg=("$@") -fi +run() { -# check dependencies -Assert::commandExists docker "check https://docs.docker.com/engine/install/ubuntu/" - -# load default conf file -Conf::load "cliProfiles" "default" -# try to load config file associated to container if provided -if [[ -n "${containerArg}" ]]; then - Conf::load "cliProfiles" "${containerArg}" || { - # conf file not existing fallback to provided args or to default ones if not provided - finalContainerArg="${containerArg}" - finalUserArg=${userArg:-${finalUserArg}} - finalCommandArg=${commandArg:-${finalCommandArg}} + # Internal function that can be used in conf profiles to load the dsn file + loadDsn() { + local dsn="$1" + local dsnFile + dsnFile="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" + Database::checkDsnFile "${dsnFile}" + # shellcheck source=/conf/dsn/default.local.env + # shellcheck disable=SC1091 + source "${dsnFile}" } -fi + export -f loadDsn -declare -a cmd=() -if Assert::windows; then - # open tty for git bash - cmd+=(winpty) -fi -INTERACTIVE_MODE="-i" -if ! read -r -t 0; then - # command is not piped or TTY not available - INTERACTIVE_MODE+="t" + # check dependencies + Assert::commandExists docker "check https://docs.docker.com/engine/install/ubuntu/" + + # load default conf file + Conf::load "cliProfiles" "default" + + # try to load config file associated to container if provided + if [[ -n "${containerArg}" ]]; then + Conf::load "cliProfiles" "${containerArg}" || { + # conf file not existing fallback to provided args or to default ones if not provided + finalContainerArg="${containerArg}" + finalUserArg=${userArg:-${finalUserArg}} + finalCommandArg=("${commandArg[@]:-${finalCommandArg[@]}}") + } + fi + + declare -a cmd=() + if Assert::windows; then + # open tty for git bash + cmd+=(winpty) + fi + INTERACTIVE_MODE="-i" + if ! read -r -t 0; then + # command is not piped or TTY not available + INTERACTIVE_MODE+="t" + fi + + cmd+=(docker) + cmd+=(exec) + cmd+=("${INTERACTIVE_MODE}") + # ensure column/lines will be updated upon terminal resize + cmd+=(-e) + cmd+=("COLUMNS=$(tput cols)") + cmd+=(-e) + cmd+=("LINES=$(tput lines)") + + cmd+=("--user=${finalUserArg}") + cmd+=("${finalContainerArg}") + cmd+=("${finalCommandArg[@]}") + if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "0" ]]; then + (echo >&2 MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}") + fi + MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -cmd+=(docker) -cmd+=(exec) -cmd+=("${INTERACTIVE_MODE}") -# ensure column/lines will be updated upon terminal resize -cmd+=(-e) -cmd+=("COLUMNS=$(tput cols)") -cmd+=(-e) -cmd+=("LINES=$(tput lines)") - -cmd+=("--user=${finalUserArg}") -cmd+=("${finalContainerArg}") -cmd+=("${finalCommandArg[@]}") -(echo >&2 MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}") -MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}" +} + +facade_main_738fd7f6601040de82a4a46d9d2efa6f "$@" diff --git a/bin/dbImport b/bin/dbImport index 20b2a559..d679462d 100755 --- a/bin/dbImport +++ b/bin/dbImport @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImport.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImport.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImport +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") # shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,124 +86,149 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + if [[ "${needEcho}" = "1" ]]; then + echo fi - return "${status}" } -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' - fi - done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") -} - -# Public: check if command specified exists or return 1 +# @description check if command specified exists or return 1 # with error and message if not # -# **Arguments**: -# * $1 commandName on which existence must be checked -# * $2 helpIfNotExists a help command to display if the command does not exist +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist # -# **Exit**: code 1 if the command specified does not exist +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided Assert::commandExists() { local commandName="$1" local helpIfNotExists="$2" @@ -213,27 +243,18 @@ Assert::commandExists() { return 0 } -# Public: exits with message if current user is root -# -# **Exit**: code 1 if current user is root -Assert::expectNonRootUser() { - if [[ "$(id -u)" = "0" ]]; then - Log::fatal "The script must not be run as root" - fi -} - -# Public: get absolute conf file from specified conf folder deduced using these rules +# @description get absolute conf file from specified conf folder deduced using these rules # * from absolute file (ignores and ) # * relative to where script is executed (ignores and ) # * from home/.bash-tools/ # * from framework conf/ # -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 conf file to use without extension -# * $3 the extension (sh by default) +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String the extension (.sh by default) # -# Returns absolute conf filename +# @stdout absolute conf filename +# @exitcode 1 if file is not found in any location Conf::getAbsoluteFile() { local confFolder="$1" local conf="$2" @@ -283,22 +304,22 @@ Conf::getAbsoluteFile() { return 1 } -# Public: list the conf files list available in bash-tools/conf/ folder +# @description list the conf files list available in bash-tools/conf/ folder # and those overridden in ${HOME}/.bash-tools/ folder -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 the extension (sh by default) -# * $3 the indentation (' - ' by default) can be any string compatible with sed not containing any / # -# **Output**: list of files without extension/directory -# eg: +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 extension:String the extension (.sh by default) +# @arg $3 indentStr:String the indentation (' - ' by default) can be any string compatible with sed not containing any / +# +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root Conf::getMergedList() { local confFolder="$1" - local extension="${2:-sh}" - local indentStr="${3:- - }" + local extension="${2-sh}" + local indentStr="${3- - }" local DEFAULT_CONF_DIR="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}" local HOME_CONF_DIR="${HOME}/.bash-tools/${confFolder}" @@ -313,16 +334,14 @@ Conf::getMergedList() { ) | sort | uniq } -# Public: dump db limited to optional table list +# @description dump db limited to optional table list # -# **Arguments**: -# * $1 (passed by reference) database instance to use -# * $2 the db to dump -# * _$3(optional)_ string containing table list -# (can be empty string in order to specify additional options) -# * _$4(optional)_ ... additional dump options -# -# **Returns**: mysqldump command status code +# @arg $1 instanceDump:&Map (passed by reference) database instance to use +# @arg $2 db:String the db to dump +# @arg $3 optionalTableList:String (optional) string containing tables list (can be empty string in order to specify additional options) +# @arg $4 dumpAdditionalOptions:String[] (optional)_ ... additional dump options +# @stderr display db sql debug +# @exitcode * mysqldump command status code Database::dump() { # shellcheck disable=SC2178 local -n instanceDump=$1 @@ -354,18 +373,18 @@ Database::dump() { Log::displayDebug "execute command: '${mysqlCommand[*]}'" "${mysqlCommand[@]}" - return $? } -# Public: check if given database exists +# @description check if given database exists # -# **Arguments**: -# * $1 (passed by reference) database instance to use -# * $2 database name +# @arg $1 instanceIfDbExists:&Map (passed by reference) database instance to use +# @arg $2 dbName:String database name +# @exitcode 1 if db doesn't exist +# @stderr debug command Database::ifDbExists() { local -n instanceIfDbExists=$1 - local dbName result - dbName="$2" + local dbName="$2" + local result local -a mysqlCommand=() mysqlCommand+=(mysqlshow) @@ -378,20 +397,17 @@ Database::ifDbExists() { [[ "${result}" = "${dbName}" ]] } -# Public: create a new db instance +# @description create a new db instance +# Returns immediately if the instance is already initialized # -# **Arguments**: -# * $1 - (passed by reference) database instance to create -# * $2 - dsn profile - load the dsn.env profile -# absolute file is deduced using rules defined in Conf::getAbsoluteFile +# @arg $1 instanceNewInstance:&Map (passed by reference) database instance to use +# @arg $2 dsn:String dsn profile - load the dsn.env profile deduced using rules defined in Conf::getAbsoluteFile # -# **Example:** -# ```shell -# declare -Agx dbInstance -# Database::newInstance dbInstance "default.local" -# ``` +# @example +# declare -Agx dbInstance +# Database::newInstance dbInstance "default.local" # -# Returns immediately if the instance is already initialized +# @exitcode 1 if dns file not able to loaded Database::newInstance() { local -n instanceNewInstance=$1 local dsn="$2" @@ -439,18 +455,19 @@ Database::newInstance() { instanceNewInstance['INITIALIZED']=1 } -# Public: mysql query on a given db -# -# **Arguments**: -# * $1 (passed by reference) database instance to use -# * $2 sql query to execute. -# if not provided or empty, the command can be piped (eg: cat file.sql | Database::query ...) -# * _$3 (optional)_ the db name +# @description mysql query on a given db +# @warning could use QUERY_OPTIONS variable from dsn if defined +# @example +# cat file.sql | Database::query ... +# @arg $1 instanceQuery:&Map (passed by reference) database instance to use +# @arg $2 sqlQuery:String (optional) sql query or sql file to execute. if not provided or empty, the command can be piped +# @arg $3 dbName:String (optional) the db name # -# **Returns**: mysql command status code +# @exitcode mysql command status code Database::query() { local -n instanceQuery=$1 local -a mysqlCommand=() + local -a queryOptions mysqlCommand+=(mysql) mysqlCommand+=("--defaults-extra-file=${instanceQuery['AUTH_FILE']}") @@ -464,11 +481,9 @@ Database::query() { mysqlCommand+=("$3") fi # add optional sql query - if [[ -n "${2+x}" && -n "$2" ]]; then - if [[ ! -f "$2" ]]; then - mysqlCommand+=("-e") - mysqlCommand+=("$2") - fi + if [[ -n "${2+x}" && -n "$2" && ! -f "$2" ]]; then + mysqlCommand+=("-e") + mysqlCommand+=("$2") fi Log::displayDebug "$(printf "execute command: '%s'" "${mysqlCommand[*]}")" @@ -479,265 +494,305 @@ Database::query() { fi } -# Public: set the general options to use on mysql command to query the database +# @description set the general options to use on mysql command to query the database # Differs than setOptions in the way that these options could change each time # -# **Arguments**: -# * $1 - (passed by reference) database instance to use -# * $2 - options list +# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use +# @arg $2 optionsList:String query options list Database::setQueryOptions() { local -n instanceSetQueryOptions=$1 # shellcheck disable=SC2034 instanceSetQueryOptions['QUERY_OPTIONS']="$2" } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 -} - -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -# Public: delete files older than n days -# -# **Arguments**: -# * $1 path -# * $2 modification time -# eg: +1 match files that have been accessed at least two days ago (rounding effect) +# @description delete files older than n days in given path +# @warning use this function with caution as it will delete all files in given path without any prompt +# @arg $1 path:String the directory in which files will be deleted or the file to delete +# @arg $2 mtime:String expiration time in days (eg: 1 means 1 day) (default value: 1). Eg: +1 match files that have been accessed at least two days ago (rounding effect) +# @arg $3 maxdepth:int Descend at most levels (a non-negative integer) levels of directories below the starting-points. (default value: 1) +# @exitcode 1 if path not provided or empty +# @exitcode * find command failure code +# @stderr find output on error or diagnostics logs # @see man find atime -# -# **Exit**: code 1 if the command failed File::garbageCollect() { local path="$1" local mtime="$2" local maxdepth="${3:-1}" - Log::displayInfo "Garbage collect files older than ${mtime} days in directory ${path}" - find "${path}" -maxdepth "${maxdepth}" -type f -mtime "${mtime}" -print -delete + if [[ -z "${path}" ]]; then + return 1 + fi + + if [[ ! -e "${path}" ]]; then + # path already removed + return 0 + fi + + Log::displayInfo "Garbage collect files older than ${mtime} days in path ${path} with max depth ${maxdepth}" + find "${path}" -depth -maxdepth "${maxdepth}" -type f -mtime "${mtime}" -print -delete } -# Public: create a temp file using default TMPDIR variable -# initialized in src/_includes/_header.tpl -# -# **Arguments**: -# @param $1 {String} template (optional) +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "$1.XXXXXXXXXXXX" + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} + +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Check that command version is greater than expected minimal version +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} + +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi +} + +# @description Check that command version is greater than expected minimal version # display warning if command version greater than expected minimal version # display error if command version less than expected minimal version and exit 1 -# @param {String} $1 command path -# @param {String} $2 command line parameters to launch to get command version -# @param {String} $3 expected minimal command version -# @param {String} $4 optional help message to display if command does not exist -# @return 1 if command version less than expected minimal version, 0 otherwise -# @return 2 if command does not exist +# @arg $1 commandName:String command path +# @arg $2 argVersion:String command line parameters to launch to get command version +# @arg $3 minimalVersion:String expected minimal command version +# @arg $4 parseVersionCallback:Function +# @arg $5 help:String optional help message to display if command does not exist +# @exitcode 0 if command version greater or equal to expected minimal version +# @exitcode 1 if command version less than expected minimal version +# @exitcode 2 if command does not exist Version::checkMinimal() { local commandName="$1" local argVersion="$2" local minimalVersion="$3" local parseVersionCallback=${4:-Version::parse} - local help="${4:-}" + local help="${5:-}" Assert::commandExists "${commandName}" "${help}" || return 2 @@ -759,21 +814,54 @@ Version::checkMinimal() { } -# Public: list files of dir with given extension and display it as a list one by line +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description list files of dir with given extension and display it as a list one by line # -# @param {String} dir $1 the directory to list -# @param {String} prefix $2 the profile file prefix (default: "") -# @param {String} ext $3 the extension -# @param {String} findOptions $4 find options, eg: -type d -# @paramDefault {String} findOptions $4 '-type f' -# @param {String} indentStr $5 the indentation can be any string compatible with sed not containing any / -# @paramDefault {String} indentStr $5 ' - ' -# @output list of files without extension/directory -# eg: +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root -# @return 1 if directory does not exists +# @exitcode 1 if directory does not exists Conf::list() { local dir="$1" local prefix="${2:-}" @@ -796,162 +884,288 @@ Conf::list() { ) } -# Internal: check if dsn file has all the mandatory variables set -# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" # -# **Arguments**: -# * $1 - dsn absolute filename +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 +} + +# @description check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # -# Returns 0 on valid file, 1 otherwise with log output +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file Database::checkDsnFile() { - local DSN_FILENAME="$1" - if [[ ! -f "${DSN_FILENAME}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} not found" + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" return 1 fi ( unset HOSTNAME PORT PASSWORD USER # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${DSN_FILENAME}" + source "${dsnFileName}" if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : HOSTNAME not provided" + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" return 1 fi if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : HOSTNAME value not provided" + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" fi if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" fi if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT not provided" + Log::displayError "dsn file ${dsnFileName} : PORT not provided" return 1 fi if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT invalid" + Log::displayError "dsn file ${dsnFileName} : PORT invalid" return 1 fi if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : USER not provided" + Log::displayError "dsn file ${dsnFileName} : USER not provided" return 1 fi if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PASSWORD not provided" + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" return 1 fi ) } -File::concatenatePath() { - local basePath="${1}" - local subPath=${2} - local fullPath="${basePath:+${basePath}/}${subPath}" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi - realpath -m "${fullPath}" 2>/dev/null + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description concatenate 2 paths and ensure the path is correct using realpath -m +# @arg $1 basePath:String +# @arg $2 subPath:String +# @require Linux::requireRealpathCommand +File::concatenatePath() { + local basePath="$1" + local subPath="$2" + local fullPath="${basePath:+${basePath}/}${subPath}" + + realpath -m "${fullPath}" 2>/dev/null } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -970,12 +1184,12 @@ Log::rotate() { fi } -# @param $1 version 1 -# @param $2 version 2 -# @return -# 0 if equal -# 1 if version1 > version2 -# 2 else +# @description compare 2 version numbers +# @arg $1 version1:String version 1 +# @arg $2 version2:String version 2 +# @exitcode 0 if equal +# @exitcode 1 if version1 > version2 +# @exitcode 2 else Version::compare() { if [[ "$1" = "$2" ]]; then return 0 @@ -1002,356 +1216,1064 @@ Version::compare() { return 0 } -# filter to keep only version number from a string -# @stdin the string to parse +# @description filter to keep only version number from a string +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# shellcheck disable=SC2120 Version::parse() { - sed -En 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' | head -n1 + sed -En 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' "$@" | head -n1 } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message -# -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date - - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 - fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 - fi - fi +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir - - dir="$(dirname "${file}")" +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 - Assert::validPath "${file}" && [[ -w "${dir}" ]] + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" } -# Public: check if argument is a valid linux path +# @description search a file in parent directories # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" + fi + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 + fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# FUNCTIONS +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} -Log::load +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# FUNCTIONS -Assert::expectNonRootUser +facade_main_fc50b62ffd6b46bd903195f347ce1017() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -# default values -PROFILE="default" -TABLES="" -DOWNLOAD_DUMP=0 -FROM_AWS=0 -SKIP_SCHEMA=0 -REMOTE_DB="" -TARGET_DB="" -COLLATION_NAME="" -CHARACTER_SET="" -FROM_DSN="" -DEFAULT_FROM_DSN="default.remote" -TARGET_DSN="default.local" -TIMEFORMAT='time spent : %3R' -# jscpd:ignore-start -DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR%/} -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" - -showHelp() { - local profilesList="" - local dsnList="" - dsnList="$(Conf::getMergedList "dsn" "env")" - profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Linux::requireRealpathCommand +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} - cat < [] -${__HELP_TITLE}Usage:${__HELP_NORMAL} ${SCRIPT_NAME} -a|--from-aws [] - [-a|--from-aws] - [-s|--skip-schema] [-p|--profile profileName] - [-o|--collation-name utf8_general_ci] [-c|--character-set utf8] - [-t|--target-dsn dsn] [-f|--from-dsn dsn] - [--tables tableName1,tableName2] - - If option -a is provided - remoteDBName will represent the name of the s3 file - Only .gz or tar.gz file are supported - the name of the source/remote database - the name of the target database, use fromDbName(without extension) if not provided - -s|--skip-schema avoid to import the schema - -o|--collation-name change the collation name used during database creation - (default value: collation name used by remote db) - -c|--character-set change the character set used during database creation - (default value: character set used by remote db or dump file if aws) - -p|--profile profileName the name of the profile to use in order to include or exclude tables - (if not specified ${HOME_PROFILES_DIR}/default.sh is used if exists otherwise ${PROFILES_DIR}/default.sh) - -t|--target-dsn dsn dsn to use for target database (Default: ${TARGET_DSN}) - -f|--from-dsn dsn dsn to use for source database (Default: ${DEFAULT_FROM_DSN}) - this option is incompatible with -a|--from-aws option - -a|--from-aws db dump will be downloaded from s3 instead of using remote db, - remoteDBName will represent the name of the file - profile will be calculated against the dump itself - this option is incompatible with -f|--from-dsn option - --tables table1,table2 import only table specified in the list - if aws mode, ignore profile option - - Aws s3 location : ${S3_BASE_URL} - -${__HELP_TITLE}List of available profiles (default profiles dir ${PROFILES_DIR} can be overridden in home profiles ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} -${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} -${dsnList} +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + dbImportCommand help + exit 0 +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImport.sh +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -Copyright (c) 2022 François Chastanet -EOF +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -# jscpd:ignore-end -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,tables:,target-dsn:,from-dsn:,from-aws,skip-schema,profile:,collation-name:,character-set: -o aht:f:sp:c:o: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - -a | --from-aws) - FROM_AWS="1" - # structure is included in s3 file - SKIP_SCHEMA="1" +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" ;; - --tables) - shift || true - TABLES="$1" + ERROR) + echo "${__LEVEL_ERROR}" ;; - -t | --target-dsn) - shift || true - TARGET_DSN="$1" + WARNING) + echo "${__LEVEL_WARNING}" ;; - -f | --from-dsn) - shift || true - FROM_DSN="${1:-${DEFAULT_FROM_DSN}}" + INFO) + echo "${__LEVEL_INFO}" ;; - -s | --skip-schema) - SKIP_SCHEMA="1" + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" ;; - -p | --profile) - shift || true - PROFILE="$1" + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" ;; - -o | --collation-name) - shift || true - COLLATION_NAME="$1" + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" ;; - -c | --character-set) - shift || true - CHARACTER_SET="$1" + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" ;; - --) - shift || true - break + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" ;; *) - showHelp - Log::fatal "invalid argument $1" - ;; + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 esac - shift || true -done - -# check dependencies -Assert::commandExists mysql "sudo apt-get install -y mysql-client" -Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" -Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" -Assert::commandExists pv "sudo apt-get install -y pv" -Assert::commandExists gawk "sudo apt-get install -y gawk" -Assert::commandExists awk "sudo apt-get install -y gawk" -Version::checkMinimal "gawk" "--version" "5.0.1" - -# additional arguments -shift $((OPTIND - 1)) || true -while true; do - if [[ -z "$1" ]]; then - # last argument - break - fi - if [[ -z "${REMOTE_DB}" ]]; then - REMOTE_DB="$1" +} + +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" else - TARGET_DB="$1" + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" fi - shift || true -done -if [[ -z "${REMOTE_DB}" ]]; then - Log::fatal "you must provide remoteDbName" -fi + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} -if [[ -z "${TARGET_DB}" ]]; then - # remove eventual file extension - TARGET_DB="${REMOTE_DB%%.*}" -fi +# default values +declare optionProfile="default" +declare optionTables="" +declare profileCommand="" -# check s3 parameter -if [[ "${FROM_AWS}" = "1" ]]; then - Assert::commandExists aws \ - "missing aws, please check https://docs.aws.amazon.com/fr_fr/cli/latest/userguide/install-cliv2.html" || exit 1 +profileOptionHelpCallback() { + echo "the name of the profile to use in order to include or exclude tables" + echo "(if not specified ${HOME_PROFILES_DIR}/default.sh is used if exists otherwise ${PROFILES_DIR}/default.sh)" +} - if [[ -n "${FROM_DSN}" ]]; then - Log::fatal "you cannot use from-dsn and from-aws at the same time" +optionTablesCallback() { + if [[ ! ${optionTables} =~ ^[A-Za-z0-9_]+(,[A-Za-z0-9_]+)*$ ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Table list is not valid : ${optionTables}" fi +} - if [[ -z "${S3_BASE_URL}" ]]; then - Log::fatal "missing S3_BASE_URL, please provide a value in .env file" +profileOptionCallback() { + local -a profilesList + readarray -t profilesList < <(Conf::getMergedList "dbImportProfiles" "sh" "" || true) + if ! Array::contains "$2" "${profilesList[@]}"; then + Log::displayError "${SCRIPT_NAME} - invalid profile '$2' provided" + return 1 + fi +} +initProfileCommandCallback() { + if [[ "${optionProfile}" != "default" && -n "${optionTables}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - you cannot use table and profile options at the same time" fi -elif [[ -z "${FROM_DSN}" ]]; then - # default value for FROM_DSN if from-aws not set - FROM_DSN="${DEFAULT_FROM_DSN}" -fi -# load the profile -if [[ -z "${PROFILE}" ]]; then - showHelp - Log::fatal "you should specify a profile" -fi + # Profile selection + local profileMsgInfo + # shellcheck disable=SC2154 + if [[ "${optionProfile}" = 'default' && -n "${optionTables}" ]]; then + profileCommand=$(Framework::createTempFile "profileCmd.XXXXXXXXXXXX") + profileMsgInfo="only ${optionTables} will be imported" + ( + echo '#!/usr/bin/env bash' + if [[ -n "${optionTables}" ]]; then + echo "${optionTables}" | sed -E 's/([A-Za-z0-9_]+),?/echo "\1"\n/g' + else + # tables option not specified, we will import all tables of the profile + echo 'cat' + fi + ) >"${profileCommand}" + else + profileCommand="$(Conf::getAbsoluteFile "dbImportProfiles" "${optionProfile}" "sh")" || exit 1 + profileMsgInfo="Using profile ${profileCommand}" + fi + chmod +x "${profileCommand}" + Log::displayInfo "${profileMsgInfo}" +} -[[ "${PROFILE}" != "default" && -n "${TABLES}" ]] && - Log::fatal "you cannot use table and profile options at the same time" +declare optionTargetDsn="default.local" # old TARGET_DSN +declare optionCharacterSet="" # old CHARACTER_SET +declare defaultTargetCharacterSet="utf8" -# Profile selection -PROFILE_COMMAND="$(Conf::getAbsoluteFile "dbImportProfiles" "${PROFILE}" "sh")" || exit 1 -PROFILE_MSG_INFO="Using profile ${PROFILE_COMMAND}" -if [[ -n "${TABLES}" ]]; then - [[ ${TABLES} =~ ^[A-Za-z0-9_]+(,[A-Za-z0-9_]+)*$ ]] || { - Log::fatal "Table list is not valid : ${TABLES}" - } -fi +initializeDefaultTargetMysqlOptions() { + local -n dbFromInstanceTargetMysql=$1 + local fromDbName="$2" -if [[ "${PROFILE}" = 'default' && -n "${TABLES}" ]]; then - PROFILE_COMMAND=$(Framework::createTempFile "profileCmd.XXXXXXXXXXXX") - chmod +x "${PROFILE_COMMAND}" - PROFILE_MSG_INFO="only ${TABLES} will be imported" - ( - echo '#!/usr/bin/env bash' - if [[ -n "${TABLES}" ]]; then - echo "${TABLES}" | sed -E 's/([A-Za-z0-9_]+),?/echo "\1"\n/g' - else - # tables option not specified, we will import all tables of the profile - echo 'cat' + # get remote db collation name + if [[ -n ${optionCollationName+x} && -z "${optionCollationName}" ]]; then + optionCollationName=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_collation_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi + + # get remote db character set + if [[ -z "${optionCharacterSet}" ]]; then + optionCharacterSet=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi +} + +declare optionCollationName="" # old COLLATION_NAME +declare defaultTargetCollationName="utf8_general_ci" + +dbImportCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + optionSkipSchema="0" + local -i options_parse_optionParsedCountOptionSkipSchema + ((options_parse_optionParsedCountOptionSkipSchema = 0)) || true + local -i options_parse_optionParsedCountOptionFromDsn + ((options_parse_optionParsedCountOptionFromDsn = 0)) || true + local -i options_parse_optionParsedCountOptionFromAws + ((options_parse_optionParsedCountOptionFromAws = 0)) || true + local -i options_parse_optionParsedCountOptionProfile + ((options_parse_optionParsedCountOptionProfile = 0)) || true + local -i options_parse_optionParsedCountOptionTables + ((options_parse_optionParsedCountOptionTables = 0)) || true + local -i options_parse_optionParsedCountOptionTargetDsn + ((options_parse_optionParsedCountOptionTargetDsn = 0)) || true + local -i options_parse_optionParsedCountOptionCharacterSet + ((options_parse_optionParsedCountOptionCharacterSet = 0)) || true + local -i options_parse_optionParsedCountOptionCollationName + ((options_parse_optionParsedCountOptionCollationName = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountFromDbName + ((options_parse_argParsedCountFromDbName = 0)) || true + local -i options_parse_argParsedCountTargetDbName + ((options_parse_argParsedCountTargetDbName = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/22 + # Option optionSkipSchema --skip-schema|-s variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --skip-schema | -s) + optionSkipSchema="1" + if ((options_parse_optionParsedCountOptionSkipSchema >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionSkipSchema)) + ;; + # Option 2/22 + # Option optionFromDsn --from-dsn|-f variableType String min 0 max 1 authorizedValues '' regexp '' + --from-dsn | -f) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionFromDsn >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionFromDsn)) + optionFromDsn="$1" + ;; + # Option 3/22 + # Option optionFromAws --from-aws|-a variableType String min 0 max 1 authorizedValues '' regexp '' + --from-aws | -a) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionFromAws >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionFromAws)) + optionFromAws="$1" + ;; + # Option 4/22 + # Option optionProfile --profile|-p variableType String min 0 max 1 authorizedValues '' regexp '' + --profile | -p) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionProfile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionProfile)) + optionProfile="$1" + profileOptionCallback "${options_parse_arg}" "${optionProfile}" + ;; + # Option 5/22 + # Option optionTables --tables variableType String min 0 max 1 authorizedValues '' regexp '' + --tables) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTables >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTables)) + optionTables="$1" + optionTablesCallback "${options_parse_arg}" "${optionTables}" + ;; + # Option 6/22 + # Option optionTargetDsn --target-dsn|-t variableType String min 0 max 1 authorizedValues '' regexp '' + --target-dsn | -t) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTargetDsn >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTargetDsn)) + optionTargetDsn="$1" + ;; + # Option 7/22 + # Option optionCharacterSet --character-set|-c variableType String min 0 max 1 authorizedValues '' regexp '' + --character-set | -c) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionCharacterSet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionCharacterSet)) + optionCharacterSet="$1" + ;; + # Option 8/22 + # Option optionCollationName --collation-name|-o variableType String min 0 max 1 authorizedValues '' regexp '' + --collation-name | -o) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionCollationName >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionCollationName)) + optionCollationName="$1" + ;; + # Option 9/22 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 10/22 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 11/22 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 12/22 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 13/22 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 14/22 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 15/22 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 16/22 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 17/22 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 18/22 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 19/22 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 20/22 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 21/22 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 22/22 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/2 + # Argument fromDbName min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountFromDbName >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument fromDbName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountFromDbName)) + fromDbName="${options_parse_arg}" + # Argument 2/2 + # Argument targetDbName min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountTargetDbName >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument targetDbName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountTargetDbName)) + targetDbName="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountFromDbName < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'fromDbName' should be provided at least 1 time(s)" + return 1 fi - ) >"${PROFILE_COMMAND}" -fi -Log::displayInfo "${PROFILE_MSG_INFO}" + commandOptionParseFinished + initProfileCommandCallback + dbImportCommandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Import source db into target db using eventual table filter")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--skip-schema|-s]" "[--from-dsn|-f ]" "[--from-aws|-a ]" "[--profile|-p ]" "[--tables ]" "[--target-dsn|-t ]" "[--character-set|-c ]" "[--collation-name|-o ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}fromDbName${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ source/remote\ database + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " [${__HELP_OPTION_COLOR}targetDbName${__HELP_NORMAL} {single}]" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ target\ database\,\ use\ fromDbName\(without\ extension\)\ if\ not\ provided + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}FROM OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--skip-schema${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-s${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< avoid\ to\ import\ the\ schema + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--from-dsn${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-f ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< dsn\ to\ use\ for\ source\ database\ \(Default:\ default.remote\)\ this\ option\ is\ incompatible\ with\ -a\|--from-aws\ option + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--from-aws${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-a ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< db\ dump\ will\ be\ downloaded\ from\ s3\ instead\ of\ using\ remote\ db.\ The\ value\ \\ is\ the\ name\ of\ the\ file\ without\ s3\ location\ \(Only\ .gz\ or\ tar.gz\ file\ are\ supported\).\ This\ option\ is\ incompatible\ with\ -f\|--from-dsn\ option + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}PROFILE OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--profile${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-p ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " $(Array::wrap ' ' 76 4 $(profileOptionHelpCallback))" + printf " %b\n" "${__HELP_OPTION_COLOR}--tables ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< import\ only\ table\ specified\ in\ the\ list.\ \ If\ aws\ mode\,\ ignore\ profile\ option + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}TARGET OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--target-dsn${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-t ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< dsn\ to\ use\ for\ target\ database\ \(Default:\ default.local\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--character-set${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-c ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< change\ the\ character\ set\ used\ during\ database\ creation\ \(default\ value:\ utf8\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--collation-name${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-o ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< change\ the\ collation\ name\ used\ during\ database\ creation\ \(default\ value:\ utf8_general_ci\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} + +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList} -[[ -z "${DB_IMPORT_DUMP_DIR}" ]] && - Log::fatal "you have to specify a value for DB_IMPORT_DUMP_DIR env variable" +${__HELP_TITLE}Aws s3 location:${__HELP_NORMAL} +${S3_BASE_URL} + +${__HELP_TITLE}Example 1: from one database to another one${__HELP_NORMAL} +${__HELP_EXAMPLE}TODO${__HELP_NORMAL} + +${__HELP_TITLE}Example 2: import from S3${__HELP_NORMAL} +${__HELP_EXAMPLE}TODO${__HELP_NORMAL}""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImport.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} -if [[ ! -d "${DB_IMPORT_DUMP_DIR}" ]]; then - mkdir -p "${DB_IMPORT_DUMP_DIR}" || - Log::fatal "impossible to create directory ${DB_IMPORT_DUMP_DIR} specified by DB_IMPORT_DUMP_DIR env variable" -fi +# default values +declare optionFromAws="" +declare optionSkipSchema="0" +declare targetDbName="" +declare fromDbName="" +declare optionFromDsn="" + +# other configuration +declare copyrightBeginYear="2020" +declare TIMEFORMAT='time spent : %3R' +declare DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR%/} +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" +declare DOWNLOAD_DUMP=0 + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" -# create db instances -declare -Agx dbFromInstance dbTargetDatabase + dbImportCommand help | envsubst + exit 0 +} -Database::newInstance dbTargetDatabase "${TARGET_DSN}" -Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" -Log::displayInfo "Using target dsn ${dbTargetDatabase['DSN_FILE']}" -if [[ "${FROM_AWS}" = "0" ]]; then - Database::newInstance dbFromInstance "${FROM_DSN}" - Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" - Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" -fi +dbImportCommandCallback() { + if [[ -z "${targetDbName}" ]]; then + targetDbName="${fromDbName}" + fi -if [[ "${FROM_AWS}" = "1" ]]; then - REMOTE_DB_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}" -else - REMOTE_DB_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}_${PROFILE}.sql.gz" - REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}_${PROFILE}_structure.sql.gz" -fi + if [[ -n "${optionFromAws}" ]]; then + Assert::commandExists aws \ + "Command ${SCRIPT_NAME} - missing aws, please check https://docs.aws.amazon.com/fr_fr/cli/latest/userguide/install-cliv2.html" || exit 1 -# check if local dump exists -if [[ ! -f "${REMOTE_DB_DUMP_TEMP_FILE}" ]]; then - Log::displayInfo "local dump does not exist" - DOWNLOAD_DUMP=1 -fi -if [[ "${FROM_AWS}" = "0" && ! -f "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" ]]; then - Log::displayInfo "local structure dump does not exist" - DOWNLOAD_DUMP=1 -fi -if [[ "${DOWNLOAD_DUMP}" = "0" ]]; then - Log::displayInfo "local dump ${REMOTE_DB_DUMP_TEMP_FILE} already exists, avoid download" -fi + if [[ -n "${optionFromDsn}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - you cannot use from-dsn and from-aws at the same time" + fi + + if [[ -z "${S3_BASE_URL}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - missing S3_BASE_URL, please provide a value in .env file" + fi + elif [[ -z "${optionFromDsn}" ]]; then + # default value for FROM_DSN if from-aws not set + optionFromDsn="default.remote" + fi + + if [[ -z "${DB_IMPORT_DUMP_DIR}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} -you have to specify a value for DB_IMPORT_DUMP_DIR env variable" + fi + + if [[ ! -d "${DB_IMPORT_DUMP_DIR}" ]]; then + mkdir -p "${DB_IMPORT_DUMP_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} -impossible to create directory ${DB_IMPORT_DUMP_DIR} specified by DB_IMPORT_DUMP_DIR env variable" + fi +} # dump header/footer read -r -d '\0' DUMP_HEADER <<-EOM @@ -1367,127 +2289,197 @@ read -r -d '\0' DUMP_FOOTER <<-EOM2 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;\0 EOM2 -Log::displayInfo "tables list will calculated using profile ${PROFILE} => ${PROFILE_COMMAND}" -chmod +x "${PROFILE_COMMAND}" -SECONDS=0 -if [[ "${DOWNLOAD_DUMP}" = "1" ]]; then - Log::displayInfo "Download dump" - - if [[ "${FROM_AWS}" = "1" ]]; then - # download dump from s3 - S3_URL="${S3_BASE_URL%/}/${REMOTE_DB}" - aws s3 ls --human-readable "${S3_URL}" || { - Log::fatal "unable to get information on S3 object : ${S3_URL}" - } - Log::displayInfo "Download dump from ${S3_URL} ..." - TMPDIR="${TMDIR:-/tmp}" aws s3 cp "${S3_URL}" "${REMOTE_DB_DUMP_TEMP_FILE}" || { - Log::fatal "unable to download dump from S3 : ${S3_URL}" - } +declare DUMP_SIZE_QUERY +DUMP_SIZE_QUERY="$( + cat <<'EOF' +SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS size +FROM information_schema.TABLES WHERE table_schema='${fromDbName}' +AND table_name IN(${listTablesDumpSize}, 'dummy') +GROUP BY table_schema +EOF +)" + +dbImportCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" + Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" + Assert::commandExists pv "sudo apt-get install -y pv" + Assert::commandExists gawk "sudo apt-get install -y gawk" + Assert::commandExists awk "sudo apt-get install -y gawk" + Version::checkMinimal "gawk" "--version" "5.0.1" + + # create db instances + declare -Agx dbFromInstance dbTargetDatabase + + # shellcheck disable=SC2154 + Database::newInstance dbTargetDatabase "${optionTargetDsn}" + Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using target dsn ${dbTargetDatabase['DSN_FILE']}" + if [[ -z "${optionFromAws}" ]]; then + # shellcheck disable=SC2154 + Database::newInstance dbFromInstance "${optionFromDsn}" + Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + fi + + local remoteDbDumpTempFile + local remoteDbStructureDumpTempFile + if [[ -n "${optionFromAws}" ]]; then + remoteDbDumpTempFile="${DB_IMPORT_DUMP_DIR}/${optionFromAws}" else - # check if remote db exists - Database::ifDbExists dbFromInstance "${REMOTE_DB}" || { - Log::fatal "Remote Database ${REMOTE_DB} does not exist" - } - - # get remote db collation name - if [[ -z "${COLLATION_NAME}" ]]; then - COLLATION_NAME=$(Database::query dbFromInstance \ - "SELECT default_collation_name FROM information_schema.SCHEMATA WHERE schema_name = \"${REMOTE_DB}\";" "information_schema") - fi + # shellcheck disable=SC2154 + remoteDbDumpTempFile="${DB_IMPORT_DUMP_DIR}/${fromDbName}_${optionProfile}.sql.gz" + remoteDbStructureDumpTempFile="${DB_IMPORT_DUMP_DIR}/${fromDbName}_${optionProfile}_structure.sql.gz" + fi - # get remote db character set - if [[ -z "${CHARACTER_SET}" ]]; then - CHARACTER_SET=$(Database::query dbFromInstance \ - "SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = \"${REMOTE_DB}\";" "information_schema") - fi + # check if local dump exists + local downloadDump=0 + if [[ ! -f "${remoteDbDumpTempFile}" ]]; then + Log::displayInfo "local dump does not exist" + downloadDump=1 + fi + if [[ -z "${optionFromAws}" && ! -f "${remoteDbStructureDumpTempFile}" ]]; then + Log::displayInfo "local structure dump does not exist" + downloadDump=1 + fi + if [[ "${downloadDump}" = "0" ]]; then + Log::displayInfo "local dump ${remoteDbDumpTempFile} already exists, avoid download" + fi - DUMP_HEADER=$(printf "%s\nSET names '%s';\n" "${DUMP_HEADER}" "${CHARACTER_SET}") - - # calculate remote db dump size - LIST_TABLES="$(Database::query dbFromInstance "show tables" "${REMOTE_DB}" | ${PROFILE_COMMAND} | sort)" - LIST_TABLES_DUMP_SIZE="$(echo "${LIST_TABLES}" | awk -v d="," -v q="'" '{s=(NR==1?s:s d)q $0 q}END{print s }')" - LIST_TABLES_DUMP=$(echo "${LIST_TABLES}" | awk -v d=" " -v q="" '{s=(NR==1?s:s d)q $0 q}END{print s }') - Log::displayInfo "Calculate dump size for tables ${LIST_TABLES_DUMP}" - DUMP_SIZE_QUERY="SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS size FROM information_schema.TABLES WHERE table_schema=\"${REMOTE_DB}\"" - DUMP_SIZE_QUERY+=" AND table_name IN(${LIST_TABLES_DUMP_SIZE}, 'dummy') " - DUMP_SIZE_QUERY+=" GROUP BY table_schema" - REMOTE_DB_DUMP_SIZE=$(echo "${DUMP_SIZE_QUERY}" | Database::query dbFromInstance) - if [[ -z "${REMOTE_DB_DUMP_SIZE}" ]]; then - # could occur with the none profile - REMOTE_DB_DUMP_SIZE="0" - fi + Log::displayInfo "tables list will calculated using profile ${optionProfile} => ${profileCommand}" + SECONDS=0 + if [[ "${downloadDump}" = "1" ]]; then + Log::displayInfo "Download dump" - # dump db - Log::displayInfo "Dump the database ${REMOTE_DB} (Size:${REMOTE_DB_DUMP_SIZE}MB) ..." - DUMP_SIZE_PV_ESTIMATION=$(awk "BEGIN {printf \"%.0f\",${REMOTE_DB_DUMP_SIZE}/1.5}") - time ( - echo "${DUMP_HEADER}" - Database::dump dbFromInstance "${REMOTE_DB}" "${LIST_TABLES_DUMP}" \ - --no-create-info --skip-add-drop-table --single-transaction=TRUE | - pv --progress --size "${DUMP_SIZE_PV_ESTIMATION}m" - echo "${DUMP_FOOTER}" - ) | gzip >"${REMOTE_DB_DUMP_TEMP_FILE}" - - Log::displayInfo "Dump structure of the database ${REMOTE_DB} ..." - time ( - echo "${DUMP_HEADER}" - #shellcheck disable=SC2016 - Database::dump dbFromInstance "${REMOTE_DB}" "" \ - --no-data --skip-add-drop-table --single-transaction=TRUE | - sed 's/^CREATE TABLE `/CREATE TABLE IF NOT EXISTS `/g' - echo "${DUMP_FOOTER}" - ) | gzip >"${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" - fi - Log::displayInfo "Dump done." -fi + if [[ -n "${optionFromAws}" ]]; then + # download dump from s3 + local s3Url="${S3_BASE_URL%/}/${optionFromAws}" + aws s3 ls --human-readable "${s3Url}" || { + Log::fatal "Command ${SCRIPT_NAME} - unable to get information on S3 object : ${s3Url}" + } + Log::displayInfo "Download dump from ${s3Url} ..." + TMPDIR="${TMDIR:-/tmp}" aws s3 cp "${s3Url}" "${remoteDbDumpTempFile}" || { + Log::fatal "Command ${SCRIPT_NAME} - unable to download dump from S3 : ${s3Url}" + } + else + # check if remote db exists + Database::ifDbExists dbFromInstance "${fromDbName}" || { + Log::fatal "Command ${SCRIPT_NAME} - Remote Database ${fromDbName} does not exist" + } -# mark dumps as modified now to avoid them to be garbage collected -touch -c -m "${REMOTE_DB_DUMP_TEMP_FILE}" || true -touch -c -m "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" || true - -# TODO Collation and character set should be retrieved from dump files if possible -COLLATION_NAME="${COLLATION_NAME:-utf8_general_ci}" -CHARACTER_SET="${CHARACTER_SET:-utf8}" - -Log::displayInfo "create target database ${TARGET_DB} if needed" -#shellcheck disable=SC2016 -Database::query dbTargetDatabase \ - "$(printf 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET "%s" COLLATE "%s"' "${TARGET_DB}" "${CHARACTER_SET}" "${COLLATION_NAME}")" - -if [[ "${FROM_AWS}" = "1" ]]; then - "${CURRENT_DIR}/dbImportStream" \ - "${REMOTE_DB_DUMP_TEMP_FILE}" \ - "${TARGET_DB}" \ - "${PROFILE_COMMAND}" \ - "${dbTargetDatabase['AUTH_FILE']}" \ - "${CHARACTER_SET}" \ - "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" -else - Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" - Log::displayInfo "Importing remote db '${REMOTE_DB}' to local db '${TARGET_DB}'" - if [[ "${SKIP_SCHEMA}" = "1" ]]; then - Log::displayInfo "avoid to create db structure" - else - Log::displayInfo "create db structure from ${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" - time ( - pv "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" | zcat | - Database::query dbTargetDatabase "" "${TARGET_DB}" - ) + initializeDefaultTargetMysqlOptions dbFromInstance "${fromDbName}" + + local dumpHeader + dumpHeader=$(printf "%s\nSET names '%s';\n" "${DUMP_HEADER}" "${optionCharacterSet}") + + # calculate remote db dump size + local listTables + local listTablesDumpSize + local listTablesDump + listTables="$(Database::query dbFromInstance "show tables" "${fromDbName}" | ${profileCommand} | sort)" + # shellcheck disable=SC2034 # used by DUMP_SIZE_QUERY + listTablesDumpSize="$(echo "${listTables}" | awk -v d="," -v q="'" '{s=(NR==1?s:s d)q $0 q}END{print s }')" + listTablesDump=$(echo "${listTables}" | awk -v d=" " -v q="" '{s=(NR==1?s:s d)q $0 q}END{print s }') + + Log::displayInfo "Calculate dump size for tables ${listTablesDump}" + local remoteDbDumpSize + remoteDbDumpSize=$(echo "${DUMP_SIZE_QUERY}" | envsubst | Database::query dbFromInstance) + if [[ -z "${remoteDbDumpSize}" ]]; then + # could occur with the none profile + remoteDbDumpSize="0" + fi + + # dump db + Log::displayInfo "Dump the database ${fromDbName} (Size:${remoteDbDumpSize}MB) ..." + local dumpSizePvEstimation + dumpSizePvEstimation=$(awk "BEGIN {printf \"%.0f\",${remoteDbDumpSize}/1.5}") + time ( + echo "${dumpHeader}" + Database::dump dbFromInstance "${fromDbName}" "${listTablesDump}" \ + --no-create-info --skip-add-drop-table --single-transaction=TRUE | + pv --progress --size "${dumpSizePvEstimation}m" + echo "${DUMP_FOOTER}" + ) | gzip >"${remoteDbDumpTempFile}" + + Log::displayInfo "Dump structure of the database ${fromDbName} ..." + time ( + echo "${dumpHeader}" + #shellcheck disable=SC2016 + Database::dump dbFromInstance "${fromDbName}" "" \ + --no-data --skip-add-drop-table --single-transaction=TRUE | + sed 's/^CREATE TABLE `/CREATE TABLE IF NOT EXISTS `/g' + echo "${DUMP_FOOTER}" + ) | gzip >"${remoteDbStructureDumpTempFile}" + fi + Log::displayInfo "Dump done." fi - Log::displayInfo "import remote to local from file ${REMOTE_DB_DUMP_TEMP_FILE}" + # mark dumps as modified now to avoid them to be garbage collected + touch -c -m "${remoteDbDumpTempFile}" || true + touch -c -m "${remoteDbStructureDumpTempFile}" || true + + # TODO Collation and character set should be retrieved from dump files if possible + # shellcheck disable=SC2154 + local targetCollationName="${optionCollationName:-${defaultTargetCollationName}}" + # shellcheck disable=SC2154 + local taregtCharacterSet="${optionCharacterSet:-${defaultTargetCharacterSet}}" + + # shellcheck disable=SC2154 + Log::displayInfo "create target database ${targetDbName} if needed" + #shellcheck disable=SC2016 + Database::query dbTargetDatabase \ + "$(printf 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET "%s" COLLATE "%s"' "${targetDbName}" "${taregtCharacterSet}" "${targetCollationName}")" + + if [[ -z "${optionFromAws}" ]]; then + Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" + Log::displayInfo "Importing remote db '${fromDbName}' to local db '${targetDbName}'" + # shellcheck disable=SC2154 + if [[ "${optionSkipSchema}" = "1" ]]; then + Log::displayInfo "avoid to create db structure" + else + Log::displayInfo "create db structure from ${remoteDbStructureDumpTempFile}" + time ( + pv "${remoteDbStructureDumpTempFile}" | zcat | + Database::query dbTargetDatabase "" "${targetDbName}" + ) + fi + fi + Log::displayInfo "import remote to local from file ${remoteDbDumpTempFile}" + local -a dbImportStreamOptions=( + --profile "${optionProfile}" \ + --target-dsn "${optionTargetDsn}" \ + --character-set "${taregtCharacterSet}" \ + ) + if [[ -n "${optionTables:-}" ]]; then + dbImportStreamOptions+=( + --tables "${optionTables}" \ + ) + fi time ( "${CURRENT_DIR}/dbImportStream" \ - "${REMOTE_DB_DUMP_TEMP_FILE}" \ - "${TARGET_DB}" \ - "${PROFILE_COMMAND}" \ - "${dbTargetDatabase['AUTH_FILE']}" \ - "${CHARACTER_SET}" \ - "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" + "${dbImportStreamOptions[@]}" \ + "${remoteDbDumpTempFile}" \ + "${targetDbName}" + ) + + # garbage collect db import dumps + File::garbageCollect "${DB_IMPORT_DUMP_DIR}" "${DB_IMPORT_GARBAGE_COLLECT_DAYS:-+30}" || true + + Log::displayInfo "Import database duration : $(date -u -d "@${SECONDS}" +"%T")" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -# garbage collect db import dumps -File::garbageCollect "${DB_IMPORT_DUMP_DIR}" "${DB_IMPORT_GARBAGE_COLLECT_DAYS:-+30}" || true +} -Log::displayInfo "Import database duration : $(date -u -d "@${SECONDS}" +"%T")" +facade_main_fc50b62ffd6b46bd903195f347ce1017 "$@" diff --git a/bin/dbImportProfile b/bin/dbImportProfile index e78b6b18..7b4d6b88 100755 --- a/bin/dbImportProfile +++ b/bin/dbImportProfile @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportProfile.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportProfile.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImportProfile +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage # shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,124 +86,133 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} - fi - return "${status}" -} - -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi fi + ((firstLine = 0)) || true done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") + if [[ "${needEcho}" = "1" ]]; then + echo + fi } -# Public: check if command specified exists or return 1 +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description check if command specified exists or return 1 # with error and message if not # -# **Arguments**: -# * $1 commandName on which existence must be checked -# * $2 helpIfNotExists a help command to display if the command does not exist +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist # -# **Exit**: code 1 if the command specified does not exist +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided Assert::commandExists() { local commandName="$1" local helpIfNotExists="$2" @@ -213,31 +227,22 @@ Assert::commandExists() { return 0 } -# Public: exits with message if current user is root -# -# **Exit**: code 1 if current user is root -Assert::expectNonRootUser() { - if [[ "$(id -u)" = "0" ]]; then - Log::fatal "The script must not be run as root" - fi -} - -# Public: list the conf files list available in bash-tools/conf/ folder +# @description list the conf files list available in bash-tools/conf/ folder # and those overridden in ${HOME}/.bash-tools/ folder -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 the extension (sh by default) -# * $3 the indentation (' - ' by default) can be any string compatible with sed not containing any / # -# **Output**: list of files without extension/directory -# eg: +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 extension:String the extension (.sh by default) +# @arg $3 indentStr:String the indentation (' - ' by default) can be any string compatible with sed not containing any / +# +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root Conf::getMergedList() { local confFolder="$1" - local extension="${2:-sh}" - local indentStr="${3:- - }" + local extension="${2-sh}" + local indentStr="${3- - }" local DEFAULT_CONF_DIR="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}" local HOME_CONF_DIR="${HOME}/.bash-tools/${confFolder}" @@ -252,15 +257,16 @@ Conf::getMergedList() { ) | sort | uniq } -# Public: check if given database exists +# @description check if given database exists # -# **Arguments**: -# * $1 (passed by reference) database instance to use -# * $2 database name +# @arg $1 instanceIfDbExists:&Map (passed by reference) database instance to use +# @arg $2 dbName:String database name +# @exitcode 1 if db doesn't exist +# @stderr debug command Database::ifDbExists() { local -n instanceIfDbExists=$1 - local dbName result - dbName="$2" + local dbName="$2" + local result local -a mysqlCommand=() mysqlCommand+=(mysqlshow) @@ -273,20 +279,17 @@ Database::ifDbExists() { [[ "${result}" = "${dbName}" ]] } -# Public: create a new db instance +# @description create a new db instance +# Returns immediately if the instance is already initialized # -# **Arguments**: -# * $1 - (passed by reference) database instance to create -# * $2 - dsn profile - load the dsn.env profile -# absolute file is deduced using rules defined in Conf::getAbsoluteFile +# @arg $1 instanceNewInstance:&Map (passed by reference) database instance to use +# @arg $2 dsn:String dsn profile - load the dsn.env profile deduced using rules defined in Conf::getAbsoluteFile # -# **Example:** -# ```shell -# declare -Agx dbInstance -# Database::newInstance dbInstance "default.local" -# ``` +# @example +# declare -Agx dbInstance +# Database::newInstance dbInstance "default.local" # -# Returns immediately if the instance is already initialized +# @exitcode 1 if dns file not able to loaded Database::newInstance() { local -n instanceNewInstance=$1 local dsn="$2" @@ -334,18 +337,19 @@ Database::newInstance() { instanceNewInstance['INITIALIZED']=1 } -# Public: mysql query on a given db -# -# **Arguments**: -# * $1 (passed by reference) database instance to use -# * $2 sql query to execute. -# if not provided or empty, the command can be piped (eg: cat file.sql | Database::query ...) -# * _$3 (optional)_ the db name +# @description mysql query on a given db +# @warning could use QUERY_OPTIONS variable from dsn if defined +# @example +# cat file.sql | Database::query ... +# @arg $1 instanceQuery:&Map (passed by reference) database instance to use +# @arg $2 sqlQuery:String (optional) sql query or sql file to execute. if not provided or empty, the command can be piped +# @arg $3 dbName:String (optional) the db name # -# **Returns**: mysql command status code +# @exitcode mysql command status code Database::query() { local -n instanceQuery=$1 local -a mysqlCommand=() + local -a queryOptions mysqlCommand+=(mysql) mysqlCommand+=("--defaults-extra-file=${instanceQuery['AUTH_FILE']}") @@ -359,11 +363,9 @@ Database::query() { mysqlCommand+=("$3") fi # add optional sql query - if [[ -n "${2+x}" && -n "$2" ]]; then - if [[ ! -f "$2" ]]; then - mysqlCommand+=("-e") - mysqlCommand+=("$2") - fi + if [[ -n "${2+x}" && -n "$2" && ! -f "$2" ]]; then + mysqlCommand+=("-e") + mysqlCommand+=("$2") fi Log::displayDebug "$(printf "execute command: '%s'" "${mysqlCommand[*]}")" @@ -374,235 +376,300 @@ Database::query() { fi } -# Public: set the general options to use on mysql command to query the database +# @description set the general options to use on mysql command to query the database # Differs than setOptions in the way that these options could change each time # -# **Arguments**: -# * $1 - (passed by reference) database instance to use -# * $2 - options list +# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use +# @arg $2 optionsList:String query options list Database::setQueryOptions() { local -n instanceSetQueryOptions=$1 # shellcheck disable=SC2034 instanceSetQueryOptions['QUERY_OPTIONS']="$2" } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Public: get absolute conf file from specified conf folder deduced using these rules +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} + +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi +} + +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description get absolute conf file from specified conf folder deduced using these rules # * from absolute file (ignores and ) # * relative to where script is executed (ignores and ) # * from home/.bash-tools/ # * from framework conf/ # -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 conf file to use without extension -# * $3 the extension (sh by default) +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String the extension (.sh by default) # -# Returns absolute conf filename +# @stdout absolute conf filename +# @exitcode 1 if file is not found in any location Conf::getAbsoluteFile() { local confFolder="$1" local conf="$2" @@ -652,21 +719,19 @@ Conf::getAbsoluteFile() { return 1 } -# Public: list files of dir with given extension and display it as a list one by line +# @description list files of dir with given extension and display it as a list one by line # -# @param {String} dir $1 the directory to list -# @param {String} prefix $2 the profile file prefix (default: "") -# @param {String} ext $3 the extension -# @param {String} findOptions $4 find options, eg: -type d -# @paramDefault {String} findOptions $4 '-type f' -# @param {String} indentStr $5 the indentation can be any string compatible with sed not containing any / -# @paramDefault {String} indentStr $5 ' - ' -# @output list of files without extension/directory -# eg: +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root -# @return 1 if directory does not exists +# @exitcode 1 if directory does not exists Conf::list() { local dir="$1" local prefix="${2:-}" @@ -689,154 +754,267 @@ Conf::list() { ) } -# Internal: check if dsn file has all the mandatory variables set -# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" # -# **Arguments**: -# * $1 - dsn absolute filename +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 +} + +# @description check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # -# Returns 0 on valid file, 1 otherwise with log output +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file Database::checkDsnFile() { - local DSN_FILENAME="$1" - if [[ ! -f "${DSN_FILENAME}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} not found" + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" return 1 fi ( unset HOSTNAME PORT PASSWORD USER # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${DSN_FILENAME}" + source "${dsnFileName}" if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : HOSTNAME not provided" + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" return 1 fi if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : HOSTNAME value not provided" + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" fi if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${DSN_FILENAME} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" fi if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT not provided" + Log::displayError "dsn file ${dsnFileName} : PORT not provided" return 1 fi if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PORT invalid" + Log::displayError "dsn file ${dsnFileName} : PORT invalid" return 1 fi if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : USER not provided" + Log::displayError "dsn file ${dsnFileName} : USER not provided" return 1 fi if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${DSN_FILENAME} : PASSWORD not provided" + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" return 1 fi ) } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" -} +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -855,258 +1033,949 @@ Log::rotate() { fi } +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description concatenate 2 paths and ensure the path is correct using realpath -m +# @arg $1 basePath:String +# @arg $2 subPath:String +# @require Linux::requireRealpathCommand File::concatenatePath() { - local basePath="${1}" - local subPath=${2} + local basePath="$1" + local subPath="$2" local fullPath="${basePath:+${basePath}/}${subPath}" realpath -m "${fullPath}" 2>/dev/null } -# Internal: common log message +# @description search a file in parent directories # -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message -# -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir - - dir="$(dirname "${file}")" - - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" - - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" } -# FUNCTIONS - -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" - -Log::load +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} -Assert::expectNonRootUser +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} -# default values -SCRIPT_NAME=${0##*/} -PROFILE="" -FROM_DB="" -DEFAULT_FROM_DSN="default.remote" -FROM_DSN="${DEFAULT_FROM_DSN}" -RATIO=70 -# jscpd:ignore-start -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" - -showHelp() { - local profilesList="" - local dsnList="" - dsnList="$(Conf::getMergedList "dsn" "env")" - profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath +} - cat < - [-p|--profile profileName] - [-f|--from-dsn dsn] +facade_main_dc9a7ae43eb7453ca997db3a8dcfeed0() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" - the name of the source/remote database - -p|--profile profileName the name of the profile to write in ${HOME_PROFILES_DIR} directory - if not provided, the file name pattern will be 'auto__.sh' - -f|--from-dsn dsn dsn to use for source database (Default: ${DEFAULT_FROM_DSN}) - -r|--ratio ratio define the ratio to use (0 to 100% - default 70) - 0 means profile will filter out all the tables - 100 means profile will keep all the tables - eg: 70 means that table size (table+index) > 70%*max table size will be excluded +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Linux::requireRealpathCommand +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} -${__HELP_TITLE}List of available profiles (default profiles dir ${PROFILES_DIR} can be overridden in home profiles ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} -${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} -${dsnList} +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + dbImportProfileCommand help + exit 0 +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportProfile.sh +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -Copyright (c) 2022 François Chastanet -EOF +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -# jscpd:ignore-end -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,profile:,from-dsn:,ratio: -o hf:p:r: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" ;; - -f | --from-dsn) - shift || true - FROM_DSN="${1:-${DEFAULT_FROM_DSN}}" + ERROR) + echo "${__LEVEL_ERROR}" ;; - -p | --profile) - shift || true - PROFILE="$1" + WARNING) + echo "${__LEVEL_WARNING}" ;; - -r | --ratio) - shift || true - RATIO="$1" + INFO) + echo "${__LEVEL_INFO}" ;; - --) - shift || true - break + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" ;; *) - showHelp - Log::fatal "invalid argument $1" + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 esac +} + +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +dbImportProfileCommand() { + local options_parse_cmd="$1" shift || true -done - -# check dependencies -Assert::commandExists mysql "sudo apt-get install -y mysql-client" -Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" -# additional arguments -shift $((OPTIND - 1)) || true -FROM_DB="$1" -shift || true -if (($# > 0)); then - Log::fatal "too much arguments provided" -fi -if [[ -z "${FROM_DB}" ]]; then - Log::fatal "you must provide fromDbName" -fi + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionProfile + ((options_parse_optionParsedCountOptionProfile = 0)) || true + local -i options_parse_optionParsedCountOptionFromDsn + ((options_parse_optionParsedCountOptionFromDsn = 0)) || true + local -i options_parse_optionParsedCountOptionRatio + ((options_parse_optionParsedCountOptionRatio = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountFromDbName + ((options_parse_argParsedCountFromDbName = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/17 + # Option optionProfile --profile|-p variableType String min 0 max 1 authorizedValues '' regexp '' + --profile | -p) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionProfile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionProfile)) + optionProfile="$1" + ;; + # Option 2/17 + # Option optionFromDsn --from-dsn|-f variableType String min 0 max 1 authorizedValues '' regexp '' + --from-dsn | -f) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionFromDsn >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionFromDsn)) + optionFromDsn="$1" + ;; + # Option 3/17 + # Option optionRatio --ratio|-r variableType String min 0 max 1 authorizedValues '' regexp '' + --ratio | -r) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionRatio >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionRatio)) + optionRatio="$1" + ;; + # Option 4/17 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 5/17 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 6/17 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 7/17 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 8/17 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 9/17 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 10/17 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 11/17 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 12/17 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 13/17 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 14/17 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 15/17 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 16/17 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 17/17 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/1 + # Argument fromDbName min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountFromDbName >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument fromDbName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountFromDbName)) + fromDbName="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountFromDbName < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'fromDbName' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + dbImportProfileCommandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate optimized profiles to be used by dbImport")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--profile|-p ]" "[--from-dsn|-f ]" "[--ratio|-r ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}fromDbName${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ source/remote\ database + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--profile${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-p ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ profile\ to\ write\ in\ profiles\ directory.\ \ If\ not\ provided\,\ the\ file\ name\ pattern\ will\ be\ \'auto_\_\.sh\' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--from-dsn${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-f ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< dsn\ to\ use\ for\ source\ database\ \(Default:\ default.remote\)\ if\ not\ provided\,\ the\ file\ name\ pattern\ will\ be\ \'auto_\_\.sh\' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--ratio${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-r ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< define\ the\ ratio\ to\ use\ \(0\ to\ 100%\ -\ default\ 70\).\ \ 0\ means\ profile\ will\ filter\ out\ all\ the\ tables.\ \ 100\ means\ profile\ will\ keep\ all\ the\ tables.\ \ Eg:\ 70\ means\ that\ tables\ with\ size\(table+index\)\ that\ are\ greater\ that\ 70%\ of\ the\ max\ table\ size\ will\ be\ excluded. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} -if [[ -z "${PROFILE}" ]]; then - PROFILE="auto_${FROM_DSN}_${FROM_DB}.sh" -fi +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList}""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportProfile.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} -if ! [[ "${RATIO}" =~ ^-?[0-9]+$ ]]; then - Log::fatal "Ratio value should be a number" -fi +# default values +declare optionProfile="" +declare fromDbName="" # old FROM_DB +declare optionFromDsn="default.remote" # old FROM_DSN +declare optionRatio=70 # old RATIO -if ((RATIO < 0 || RATIO > 100)); then - Log::fatal "Ratio value should be between 0 and 100" -fi +# other configuration +declare copyrightBeginYear="2020" +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" -# create db instance -declare -Agx dbFromInstance + dbImportProfileCommand help | envsubst + exit 0 +} + +dbImportProfileCommandCallback() { + if [[ -z "${fromDbName}" ]]; then + Log::fatal "you must provide fromDbName" + fi -Database::newInstance dbFromInstance "${FROM_DSN}" -Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" -Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + if [[ -z "${optionProfile}" ]]; then + optionProfile="auto_${optionFromDsn}_${fromDbName}.sh" + fi -# check if from db exists -Database::ifDbExists dbFromInstance "${FROM_DB}" || { - Log::fatal "From Database ${FROM_DB} does not exist !" + if ! [[ "${optionRatio}" =~ ^-?[0-9]+$ ]]; then + Log::fatal "Ratio value should be a number" + fi + + if ((optionRatio < 0 || optionRatio > 100)); then + Log::fatal "Ratio value should be between 0 and 100" + fi } + +# shellcheck disable=SC2154 read -r -d '' QUERY <"${HOME_PROFILES_DIR}/${PROFILE}" - -Log::displayInfo "File saved in '${HOME_PROFILES_DIR}/${PROFILE}'" + +dbImportProfileCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" + + # create db instance + declare -Agx dbFromInstance + + # shellcheck disable=SC2154 + Database::newInstance dbFromInstance "${optionFromDsn}" + Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + + # check if from db exists + # shellcheck disable=SC2154 + Database::ifDbExists dbFromInstance "${fromDbName}" || { + Log::fatal "From Database ${fromDbName} does not exist !" + } + local tableList + tableList="$(Database::query dbFromInstance "${QUERY//@DB@/${fromDbName}}" "information_schema")" + # first table is the biggest one + local maxTableSize + maxTableSize="$(echo "${tableList}" | head -1 | awk -F ' ' '{print $2}')" + ( + echo "#!/usr/bin/env bash" + echo + echo "# cat represents the whole list of tables" + echo "cat |" + local -i excludedTablesCount + ((excludedTablesCount = 0)) || true + local tableSize + local tableName + while IFS="" read -r line || [[ -n "${line}" ]]; do + tableSize="$(echo "${line}" | awk -F ' ' '{print $2}')" + tableName="$(echo "${line}" | awk -F ' ' '{print $1}')" + # shellcheck disable=SC2154 + if ((tableSize < maxTableSize * optionRatio / 100)); then + echo -n '#' + else + excludedTablesCount=$((excludedTablesCount + 1)) + fi + echo " grep -v '^${tableName}$' | # table size ${tableSize}MB" + done < <(echo "${tableList}") + echo "cat" + tablesCount="$(echo "${tableList}" | wc -l)" + Log::displayInfo "Profile generated - ${excludedTablesCount}/${tablesCount} tables bigger than ${optionRatio}% of max table size (${maxTableSize}MB) automatically excluded" + ) >"${HOME_PROFILES_DIR}/${optionProfile}" + + Log::displayInfo "File saved in '${HOME_PROFILES_DIR}/${optionProfile}'" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi + +} + +facade_main_dc9a7ae43eb7453ca997db3a8dcfeed0 "$@" diff --git a/bin/dbImportStream b/bin/dbImportStream index c304a6cb..94434249 100755 --- a/bin/dbImportStream +++ b/bin/dbImportStream @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportStream.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportStream.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImportStream +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage # shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,458 +86,996 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" /dev/null) && shopt -s inherit_errexit + local arg -# a log is generated when a command fails -set -o errtrace + # convert multi-line arg to several args + local -a allArgs=() + for arg in "$@"; do + local line + local IFS=$'\n' + arg="$(echo -e "${arg}")" + while read -r line; do + if [[ -z "${line}" ]]; then + allArgs+=($'\n') + else + allArgs+=("${line}") + fi + done <<<"${arg}" + done + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break + fi + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX +# @description check if command specified exists or return 1 +# with error and message if not +# +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist +# +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" + + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" + fi + return 1 + } + return 0 +} -export TERM=xterm-256color +# @description get absolute conf file from specified conf folder deduced using these rules +# * from absolute file (ignores and ) +# * relative to where script is executed (ignores and ) +# * from home/.bash-tools/ +# * from framework conf/ +# +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String the extension (.sh by default) +# +# @stdout absolute conf filename +# @exitcode 1 if file is not found in any location +Conf::getAbsoluteFile() { + local confFolder="$1" + local conf="$2" + local extension="${3-.sh}" + if [[ -n "${extension}" && "${extension:0:1}" != "." ]]; then + extension=".${extension}" + fi -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true + testAbs() { + local result + result="$(realpath -e "$1" 2>/dev/null)" + # shellcheck disable=SC2181 + if [[ "$?" = "0" && -f "${result}" ]]; then + echo "${result}" + return 0 + fi + return 1 + } -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR + # conf is absolute file (including extension) + testAbs "${confFolder}${extension}" && return 0 + # conf is absolute file + testAbs "${confFolder}" && return 0 + # conf is absolute file (including extension) + testAbs "${conf}${extension}" && return 0 + # conf is absolute file + testAbs "${conf}" && return 0 + + # relative to where script is executed (including extension) + if [[ -n "${CURRENT_DIR+xxx}" ]]; then + testAbs "$(File::concatenatePath "${CURRENT_DIR}" "${confFolder}")/${conf}${extension}" && return 0 + fi + # from home/.bash-tools/ + testAbs "$(File::concatenatePath "${HOME}/.bash-tools" "${confFolder}")/${conf}${extension}" && return 0 -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi + if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then + # from framework conf/ (including extension) + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 -# shellcheck disable=SC2034 + # from framework conf/ + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 + fi -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi + # file not found + Log::displayError "conf file '${conf}' not found" -Args::defaultHelp() { - local helpArg=$1 - shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi + return 1 } -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 - shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break +# @description list the conf files list available in bash-tools/conf/ folder +# and those overridden in ${HOME}/.bash-tools/ folder +# +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 extension:String the extension (.sh by default) +# @arg $3 indentStr:String the indentation (' - ' by default) can be any string compatible with sed not containing any / +# +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +Conf::getMergedList() { + local confFolder="$1" + local extension="${2-sh}" + local indentStr="${3- - }" + + local DEFAULT_CONF_DIR="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}" + local HOME_CONF_DIR="${HOME}/.bash-tools/${confFolder}" + + ( + if [[ -d "${DEFAULT_CONF_DIR}" ]]; then + Conf::list "${DEFAULT_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} - fi - return "${status}" + if [[ -d "${HOME_CONF_DIR}" ]]; then + Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" + fi + ) | sort | uniq } -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done +# @description create a new db instance +# Returns immediately if the instance is already initialized +# +# @arg $1 instanceNewInstance:&Map (passed by reference) database instance to use +# @arg $2 dsn:String dsn profile - load the dsn.env profile deduced using rules defined in Conf::getAbsoluteFile +# +# @example +# declare -Agx dbInstance +# Database::newInstance dbInstance "default.local" +# +# @exitcode 1 if dns file not able to loaded +Database::newInstance() { + local -n instanceNewInstance=$1 + local dsn="$2" + local DSN_FILE + + if [[ -v instanceNewInstance['INITIALIZED'] && "${instanceNewInstance['INITIALIZED']:-0}" == "1" ]]; then + return + fi - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' - fi - done + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") -} + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${instanceNewInstance['DSN_FILE']}" -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" + instanceNewInstance['USER']="${USER}" + instanceNewInstance['PASSWORD']="${PASSWORD}" + instanceNewInstance['HOSTNAME']="${HOSTNAME}" + instanceNewInstance['PORT']="${PORT}" + # generate authFile for easy authentication + instanceNewInstance['AUTH_FILE']=$(mktemp -p "${TMPDIR:-/tmp}" -t "mysql.XXXXXXXXXXXX") ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") + echo "[client]" + echo "user = ${USER}" + echo "password = ${PASSWORD}" + echo "host = ${HOSTNAME}" + echo "port = ${PORT}" + ) >"${instanceNewInstance['AUTH_FILE']}" + + # some of those values can be overridden using the dsn file + # SKIP_COLUMN_NAMES enabled by default + instanceNewInstance['SKIP_COLUMN_NAMES']="${SKIP_COLUMN_NAMES:-1}" + instanceNewInstance['SSL_OPTIONS']="${MYSQL_SSL_OPTIONS:---ssl-mode=DISABLED}" + instanceNewInstance['QUERY_OPTIONS']="${MYSQL_QUERY_OPTIONS:---batch --raw --default-character-set=utf8}" + instanceNewInstance['DUMP_OPTIONS']="${MYSQL_DUMP_OPTIONS:---default-character-set=utf8 --compress --hex-blob --routines --triggers --single-transaction --set-gtid-purged=OFF --column-statistics=0 ${instanceNewInstance['SSL_OPTIONS']}}" + instanceNewInstance['DB_IMPORT_OPTIONS']="${DB_IMPORT_OPTIONS:---connect-timeout=5 --batch --raw --default-character-set=utf8}" + + instanceNewInstance['INITIALIZED']=1 +} - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done +# @description mysql query on a given db +# @warning could use QUERY_OPTIONS variable from dsn if defined +# @example +# cat file.sql | Database::query ... +# @arg $1 instanceQuery:&Map (passed by reference) database instance to use +# @arg $2 sqlQuery:String (optional) sql query or sql file to execute. if not provided or empty, the command can be piped +# @arg $3 dbName:String (optional) the db name +# +# @exitcode mysql command status code +Database::query() { + local -n instanceQuery=$1 + local -a mysqlCommand=() + local -a queryOptions + + mysqlCommand+=(mysql) + mysqlCommand+=("--defaults-extra-file=${instanceQuery['AUTH_FILE']}") + IFS=' ' read -r -a queryOptions <<<"${instanceQuery['QUERY_OPTIONS']}" + mysqlCommand+=("${queryOptions[@]}") + if [[ "${instanceQuery['SKIP_COLUMN_NAMES']}" = "1" ]]; then + mysqlCommand+=("-s" "--skip-column-names") + fi + # add optional db name + if [[ -n "${3+x}" ]]; then + mysqlCommand+=("$3") + fi + # add optional sql query + if [[ -n "${2+x}" && -n "$2" && ! -f "$2" ]]; then + mysqlCommand+=("-e") + mysqlCommand+=("$2") + fi + Log::displayDebug "$(printf "execute command: '%s'" "${mysqlCommand[*]}")" - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi + if [[ -f "$2" ]]; then + "${mysqlCommand[@]}" <"$2" + else + "${mysqlCommand[@]}" + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 +# @description set the general options to use on mysql command to query the database +# Differs than setOptions in the way that these options could change each time +# +# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use +# @arg $2 optionsList:String query options list +Database::setQueryOptions() { + local -n instanceSetQueryOptions=$1 + # shellcheck disable=SC2034 + instanceSetQueryOptions['QUERY_OPTIONS']="$2" +} - # ensure all sourced variables will be exported - set -o allexport +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } +} - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + fi -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" + if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then + Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" + fi fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } +} + +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} + +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } +} + +# @description Check that command version is greater than expected minimal version +# display warning if command version greater than expected minimal version +# display error if command version less than expected minimal version and exit 1 +# @arg $1 commandName:String command path +# @arg $2 argVersion:String command line parameters to launch to get command version +# @arg $3 minimalVersion:String expected minimal command version +# @arg $4 parseVersionCallback:Function +# @arg $5 help:String optional help message to display if command does not exist +# @exitcode 0 if command version greater or equal to expected minimal version +# @exitcode 1 if command version less than expected minimal version +# @exitcode 2 if command does not exist +Version::checkMinimal() { + local commandName="$1" + local argVersion="$2" + local minimalVersion="$3" + local parseVersionCallback=${4:-Version::parse} + local help="${5:-}" + + Assert::commandExists "${commandName}" "${help}" || return 2 + + local version + version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback})" + + Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + + Version::compare "${version}" "${minimalVersion}" || { + local result=$? + if [[ "${result}" = "1" ]]; then + Log::displayWarning "${commandName} version is ${version} greater than ${minimalVersion}, OK let's continue" + elif [[ "${result}" = "2" ]]; then + Log::displayError "${commandName} minimal version is ${minimalVersion}, your version is ${version}" + return 1 + fi + return 0 + } + +} + +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } + [[ -t 1 || -t 2 ]] +} + +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" - fi - fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" - if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then - Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 +} + +# @description check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file +Database::checkDsnFile() { + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" + return 1 fi + + ( + unset HOSTNAME PORT PASSWORD USER + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${dsnFileName}" + if [[ -z ${HOSTNAME+x} ]]; then + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" + return 1 + fi + if [[ -z "${HOSTNAME}" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" + fi + if [[ "${HOSTNAME}" = "localhost" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + fi + if [[ -z "${PORT+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT not provided" + return 1 + fi + if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT invalid" + return 1 + fi + if [[ -z "${USER+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : USER not provided" + return 1 + fi + if [[ -z "${PASSWORD+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" + return 1 + fi + ) } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" done } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description concatenate 2 paths and ensure the path is correct using realpath -m +# @arg $1 basePath:String +# @arg $2 subPath:String +# @require Linux::requireRealpathCommand +File::concatenatePath() { + local basePath="$1" + local subPath="$2" + local fullPath="${basePath:+${basePath}/}${subPath}" + + realpath -m "${fullPath}" 2>/dev/null } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -551,121 +1094,979 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description compare 2 version numbers +# @arg $1 version1:String version 1 +# @arg $2 version2:String version 2 +# @exitcode 0 if equal +# @exitcode 1 if version1 > version2 +# @exitcode 2 else +Version::compare() { + if [[ "$1" = "$2" ]]; then + return 0 + fi + local IFS=. + # shellcheck disable=2206 + local i ver1=($1) ver2=($2) + # fill empty fields in ver1 with zeros + for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i = 0; i < ${#ver1[@]}; i++)); do + if [[ -z "${ver2[i]+unset}" ]] || [[ -z ${ver2[i]} ]]; then + # fill empty fields in ver2 with zeros + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 + fi + done + return 0 } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description filter to keep only version number from a string +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# shellcheck disable=SC2120 +Version::parse() { + sed -En 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' "$@" | head -n1 +} + +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 + fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 +} + +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" fi } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} - dir="$(dirname "${file}")" +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath +} + +# FUNCTIONS + +facade_main_d2bf5f5cbe1a441fbd98a9d712dd6116() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" - Assert::validPath "${file}" && [[ -w "${dir}" ]] +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Linux::requireRealpathCommand +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir +# shellcheck disable=SC2154 + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + dbImportStreamCommand help + exit 0 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 } -# FUNCTIONS +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -Log::load +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -HELP="$( - cat < [characterSet] [dbImportOptions] -characterSet: default value utf8 -dbImportOptions: default value empty +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportStream.sh +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} -Copyright (c) 2022 François Chastanet -EOF -)" -Args::defaultHelp "${HELP}" "$@" +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} -DUMP_FILE="$1" -DB_NAME="$2" -PROFILE_COMMAND="${3}" -MYSQL_AUTH_FILE="${4}" -CHARACTER_SET="${5:-utf8}" -DB_IMPORT_OPTIONS="${6:-}" +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} -if [[ -z "${PROFILE_COMMAND}" ]]; then - Log::fatal "You should provide a profile command" -fi +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +# default values +declare optionProfile="default" +declare optionTables="" +declare profileCommand="" + +profileOptionHelpCallback() { + echo "the name of the profile to use in order to include or exclude tables" + echo "(if not specified ${HOME_PROFILES_DIR}/default.sh is used if exists otherwise ${PROFILES_DIR}/default.sh)" +} + +optionTablesCallback() { + if [[ ! ${optionTables} =~ ^[A-Za-z0-9_]+(,[A-Za-z0-9_]+)*$ ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Table list is not valid : ${optionTables}" + fi +} + +profileOptionCallback() { + local -a profilesList + readarray -t profilesList < <(Conf::getMergedList "dbImportProfiles" "sh" "" || true) + if ! Array::contains "$2" "${profilesList[@]}"; then + Log::displayError "${SCRIPT_NAME} - invalid profile '$2' provided" + return 1 + fi +} +initProfileCommandCallback() { + if [[ "${optionProfile}" != "default" && -n "${optionTables}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - you cannot use table and profile options at the same time" + fi + + # Profile selection + local profileMsgInfo + # shellcheck disable=SC2154 + if [[ "${optionProfile}" = 'default' && -n "${optionTables}" ]]; then + profileCommand=$(Framework::createTempFile "profileCmd.XXXXXXXXXXXX") + profileMsgInfo="only ${optionTables} will be imported" + ( + echo '#!/usr/bin/env bash' + if [[ -n "${optionTables}" ]]; then + echo "${optionTables}" | sed -E 's/([A-Za-z0-9_]+),?/echo "\1"\n/g' + else + # tables option not specified, we will import all tables of the profile + echo 'cat' + fi + ) >"${profileCommand}" + else + profileCommand="$(Conf::getAbsoluteFile "dbImportProfiles" "${optionProfile}" "sh")" || exit 1 + profileMsgInfo="Using profile ${profileCommand}" + fi + chmod +x "${profileCommand}" + Log::displayInfo "${profileMsgInfo}" +} + +declare optionTargetDsn="default.local" # old TARGET_DSN +declare optionCharacterSet="" # old CHARACTER_SET +declare defaultTargetCharacterSet="utf8" + +initializeDefaultTargetMysqlOptions() { + local -n dbFromInstanceTargetMysql=$1 + local fromDbName="$2" + + # get remote db collation name + if [[ -n ${optionCollationName+x} && -z "${optionCollationName}" ]]; then + optionCollationName=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_collation_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi + + # get remote db character set + if [[ -z "${optionCharacterSet}" ]]; then + optionCharacterSet=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi +} + +dbImportStreamCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionProfile + ((options_parse_optionParsedCountOptionProfile = 0)) || true + local -i options_parse_optionParsedCountOptionTables + ((options_parse_optionParsedCountOptionTables = 0)) || true + local -i options_parse_optionParsedCountOptionTargetDsn + ((options_parse_optionParsedCountOptionTargetDsn = 0)) || true + local -i options_parse_optionParsedCountOptionCharacterSet + ((options_parse_optionParsedCountOptionCharacterSet = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountArgDumpFile + ((options_parse_argParsedCountArgDumpFile = 0)) || true + local -i options_parse_argParsedCountArgTargetDbName + ((options_parse_argParsedCountArgTargetDbName = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/18 + # Option optionProfile --profile|-p variableType String min 0 max 1 authorizedValues '' regexp '' + --profile | -p) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionProfile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionProfile)) + optionProfile="$1" + profileOptionCallback "${options_parse_arg}" "${optionProfile}" + ;; + # Option 2/18 + # Option optionTables --tables variableType String min 0 max 1 authorizedValues '' regexp '' + --tables) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTables >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTables)) + optionTables="$1" + optionTablesCallback "${options_parse_arg}" "${optionTables}" + ;; + # Option 3/18 + # Option optionTargetDsn --target-dsn|-t variableType String min 0 max 1 authorizedValues '' regexp '' + --target-dsn | -t) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTargetDsn >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTargetDsn)) + optionTargetDsn="$1" + ;; + # Option 4/18 + # Option optionCharacterSet --character-set|-c variableType String min 0 max 1 authorizedValues '' regexp '' + --character-set | -c) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionCharacterSet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionCharacterSet)) + optionCharacterSet="$1" + ;; + # Option 5/18 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 6/18 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 7/18 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 8/18 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 9/18 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 10/18 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 11/18 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 12/18 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 13/18 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 14/18 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 15/18 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 16/18 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 17/18 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 18/18 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/2 + # Argument argDumpFile min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountArgDumpFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument argDumpFile - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountArgDumpFile)) + argDumpFile="${options_parse_arg}" + # Argument 2/2 + # Argument argTargetDbName min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountArgTargetDbName >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument argTargetDbName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountArgTargetDbName)) + argTargetDbName="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountArgDumpFile < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'argDumpFile' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountArgTargetDbName < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'argTargetDbName' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + initProfileCommandCallback + dbImportStreamCommandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "stream tar.gz file or gz file through mysql")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--profile|-p ]" "[--tables ]" "[--target-dsn|-t ]" "[--character-set|-c ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}argDumpFile${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ of\ the\ file\ that\ will\ be\ streamed\ through\ mysql + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}argTargetDbName${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ mysql\ target\ database + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}PROFILE OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--profile${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-p ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " $(Array::wrap ' ' 76 4 $(profileOptionHelpCallback))" + printf " %b\n" "${__HELP_OPTION_COLOR}--tables ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< import\ only\ table\ specified\ in\ the\ list.\ \ If\ aws\ mode\,\ ignore\ profile\ option + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}TARGET OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--target-dsn${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-t ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< dsn\ to\ use\ for\ target\ database\ \(Default:\ default.local\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--character-set${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-c ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< change\ the\ character\ set\ used\ during\ database\ creation\ \(default\ value:\ utf8\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} + +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList}""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/DbImport/dbImportStream.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} + +# default values +declare optionProfile="" +declare argTargetDbName="" + +# other configuration +declare copyrightBeginYear="2020" +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" + + dbImportStreamCommand help | envsubst + exit 0 +} + +dbImportStreamCommandCallback() { + if [[ -z "${argTargetDbName}" ]]; then + Log::fatal "you must provide argTargetDbName" + fi + if [[ ! -f "${argDumpFile}" ]]; then + Log::fatal "invalid argDumpFile provided - file does not exist" + fi +} awkScript="$( cat <<'EOF' @@ -710,19 +2111,60 @@ BEGIN{ } EOF )" -# shellcheck disable=2086 -( - if [[ "${DUMP_FILE}" == *tar.gz ]]; then - tar xOfz "${DUMP_FILE}" - elif [[ "${DUMP_FILE}" == *.gz ]]; then - zcat "${DUMP_FILE}" - fi - # zcat will continue to write to stdout whereas awk has finished if table has been found - # we detect this case because zcat will return code 141 because pipe closed - status=$? - if [[ "${status}" -eq "141" ]]; then true; else exit "${status}"; fi -) | awk \ - -v PROFILE_COMMAND="${PROFILE_COMMAND}" \ - -v CHARACTER_SET="${CHARACTER_SET}" \ - --source "${awkScript}" \ - - | mysql --defaults-extra-file="${MYSQL_AUTH_FILE}" ${DB_IMPORT_OPTIONS} "${DB_NAME}" || exit $? + +dbImportStreamCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists gawk "sudo apt-get install -y gawk" + Assert::commandExists awk "sudo apt-get install -y gawk" + Version::checkMinimal "gawk" "--version" "5.0.1" + + # create db instances + declare -Agx dbTargetInstance + + # shellcheck disable=SC2154 + Database::newInstance dbTargetInstance "${optionTargetDsn}" + Database::setQueryOptions dbTargetInstance "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using target dsn ${dbTargetInstance['DSN_FILE']}" + + initializeDefaultTargetMysqlOptions dbTargetInstance "${argTargetDbName}" + + # TODO character set should be retrieved from dump files if possible + # shellcheck disable=SC2154 + declare remoteCharacterSet="${optionCharacterSet:-${defaultRemoteCharacterSet}}" + + # shellcheck disable=2086 + ( + if [[ "${argDumpFile}" =~ \.tar.gz$ ]]; then + tar xOfz "${argDumpFile}" + elif [[ "${argDumpFile}" =~ \.gz$ ]]; then + zcat "${argDumpFile}" + fi + # zcat will continue to write to stdout whereas awk has finished if table has been found + # we detect this case because zcat will return code 141 because pipe closed + status=$? + if [[ "${status}" -eq "141" ]]; then true; else exit "${status}"; fi + ) | + awk \ + -v PROFILE_COMMAND="${profileCommand}" \ + -v CHARACTER_SET="${remoteCharacterSet}" \ + --source "${awkScript}" \ + - | mysql \ + "--defaults-extra-file=${dbTargetInstance['AUTH_FILE']}" \ + ${dbTargetInstance['DB_IMPORT_OPTIONS']} \ + "${argTargetDbName}" || exit $? +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi + +} + +facade_main_d2bf5f5cbe1a441fbd98a9d712dd6116 "$@" diff --git a/bin/doc b/bin/doc index 8687d393..8cfa181d 100755 --- a/bin/doc +++ b/bin/doc @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/doc.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/doc.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/doc +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,332 +86,290 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi -} - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 + local -i maxLineLength=$1 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using info color (blue) but warning level -# @param {String} $1 message +# @description Display message using info color (blue) but warning level +# @arg $1 message:String the message to display Log::displayStatus() { local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logStatus "$1" "${type}" } -# shellcheck disable=SC2317 +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +Log::fatal() { + echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 1 +} -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# fix markdown TOC generated by Markdown all in one vscode extension -# https://regex101.com/r/DJJf2I/1 +# @description fix markdown TOC generated by Markdown all in one vscode extension +# to make TOC compatible with docsify +# @arg $1 file:String file to fix +# @exitcode 1 if awk fails +# @see https://regex101.com/r/DJJf2I/1 ShellDoc::fixMarkdownToc() { local file="$1" local fixMarkdownToc @@ -432,17 +395,18 @@ EOF awk -i inplace "${fixMarkdownToc}" "${file}" } -# generate markdown file from template by replacing -# @@@command_help@@@ by the help of the command +# @description generates markdown file from template by +# replacing @@@command_help@@@ by the help of the command # eg: @@@test_help@@@ will be replaced by the output # of the command `test --help` in the directory provided -# @param {String} $1 templateFile the file to use as template -# @param {String} $2 targetFile the target file -# @param {String} $3 fromDir the directory from which commands will be searched -# @param {int} $4 tokenNotFoundCount passed by reference, will return -# the number of tokens @@@command_help@@@ not found in the template file -# @param {String} excludeFilesPattern $5 grep exclude pattern -# eg: '^(bash-tpl)$' +# +# @arg $1 templateFile:String the file to use as template +# @arg $2 targetFile:String the target file +# @arg $3 fromDir:String the directory from which commands will be searched +# @arg $4 tokenNotFoundCount:&int (passed by reference) number of tokens @@@command_help@@@ not found in the template file +# @arg $5 excludeFilesPattern:String grep exclude pattern (eg: '^(bash-tpl)$') (default value: "") +# @stderr diagnostics logs +# @stdout the generated markdown with help of the matching command ShellDoc::generateMdFileFromTemplate() { local templateFile="$1" local targetFile="$2" @@ -482,34 +446,252 @@ ShellDoc::generateMdFileFromTemplate() { Log::displayInfo "${nbTokensGenerated} commands' help replaced in $(echo "scale=3; ${endTime} - ${startTime}" | bc)seconds" } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi +} + +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done +} + +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport +} + +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done +} + +# @description replace token by input(stdin) in given targetFile +# @warning special ansi codes will be removed from stdin +# @arg $1 token:String the token to replace by stdin +# @arg $2 targetFile:String the file in which token will be replaced by stdin +# @exitcode 1 if error +# @stdin the file content that will be injected in targetFile File::replaceTokenByInput() { local token="$1" local targetFile="$2" @@ -518,7 +700,7 @@ File::replaceTokenByInput() { local tokenFile tokenFile="$(Framework::createTempFile "replaceTokenByInput")" - cat - | Filters::escapeColorCodes >"${tokenFile}" + cat - | Filters::removeAnsiCodes >"${tokenFile}" sed -E -i \ -e "/${token}/r ${tokenFile}" \ @@ -527,93 +709,105 @@ File::replaceTokenByInput() { ) } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" -} - -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using skip color (yellow) -# @param {String} $1 message +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi Log::logSkipped "$1" } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" -} - -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logStatus() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-STATUS}" "$1" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -632,179 +826,824 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -Filters::escapeColorCodes() { - cat - | sed -E $'s/\e\\[[0-9;:]*[a-zA-Z]//g' +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" } -# Public: create a temp file using default TMPDIR variable -# initialized in src/_includes/_header.tpl +# @description search a file in parent directories # -# **Arguments**: -# @param $1 {String} template (optional) +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true + + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" + fi + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 + fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 +} + +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "$1.XXXXXXXXXXXX" + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} + +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + +# @description check if an element is contained in an array # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 +# FUNCTIONS + +facade_main_8fd47ebedea14305a1452a7b8820b174() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir + +# @require Compiler::Facade::requireCommandBinDir + +DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 - fi + echo "Copyright (c) ${years} François Chastanet" +} + +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + docCommand help + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 fi } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} - dir="$(dirname "${file}")" +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac } -# FUNCTIONS +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -Log::load +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" -DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" -showHelp() { -cat </,//d' \ - -e 's#https://fchastanet.github.io/bash-tools/#/#' \ - -e 's#^> \*\*_TIP:_\*\* (.*)$#> [!TIP|label:\1]#' \ - "${DOC_DIR}/README.md" - -ShellDoc::fixMarkdownToc "${DOC_DIR}/README.md" -ShellDoc::fixMarkdownToc "${DOC_DIR}/Commands.md" - -if ((TOKEN_NOT_FOUND_COUNT > 0)); then - exit 1 +# shellcheck disable=SC2317 # if function is overridden +updateOptionSkipDockerBuildCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=("$1") +} + +docCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + optionSkipDockerBuild="0" + local -i options_parse_optionParsedCountOptionSkipDockerBuild + ((options_parse_optionParsedCountOptionSkipDockerBuild = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/15 + # Option optionSkipDockerBuild --skip-docker-build variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --skip-docker-build) + optionSkipDockerBuild="1" + if ((options_parse_optionParsedCountOptionSkipDockerBuild >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionSkipDockerBuild)) + updateOptionSkipDockerBuildCallback "${options_parse_arg}" + ;; + # Option 2/15 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 3/15 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 4/15 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 5/15 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 6/15 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 7/15 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 8/15 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 9/15 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 10/15 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 11/15 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 12/15 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 13/15 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 14/15 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 15/15 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided" + return 1 + fi + ;; + esac + shift || true + done + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate markdown documentation")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--skip-docker-build]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--skip-docker-build${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< skip\ docker\ image\ build\ if\ option\ provided + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/doc.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" +declare -a RUN_CONTAINER_ARGV_FILTERED=() +updateOptionSkipDockerBuildCallback() { + if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + BASH_FRAMEWORK_ARGV_FILTERED+=("$1") + RUN_CONTAINER_ARGV_FILTERED+=("$1") + fi +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(--verbose) + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(-vv) + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(-vvv) + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} + +docCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +run() { + if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + DOCKER_RUN_OPTIONS=$"-e ORIGINAL_DOC_DIR=${DOC_DIR}" \ + "${COMMAND_BIN_DIR}/runBuildContainer" "/bash/bin/doc" "${RUN_CONTAINER_ARGV_FILTERED[@]}" + return $? + fi + + #----------------------------- + # configure docker environment + #----------------------------- + mkdir -p "${HOME}/.bash-tools" + + ( + cd "${BASH_TOOLS_ROOT_DIR}" || exit 1 + cp -R conf/. "${HOME}/.bash-tools" + sed -i \ + -e "s@^S3_BASE_URL=.*@S3_BASE_URL=s3://example.com/exports/@g" \ + "${HOME}/.bash-tools/.env" + # fake docker command + touch /tmp/docker + chmod 755 /tmp/docker + ) + export PATH=/tmp:${PATH} + + #----------------------------- + # doc generation + #----------------------------- + + Log::displayInfo 'generate Commands.md' + ((TOKEN_NOT_FOUND_COUNT = 0)) || true + ShellDoc::generateMdFileFromTemplate \ + "${BASH_TOOLS_ROOT_DIR}/Commands.tmpl.md" \ + "${DOC_DIR}/Commands.md" \ + "${FRAMEWORK_BIN_DIR}" \ + TOKEN_NOT_FOUND_COUNT \ + '(bash-tpl|plantuml|definitionLint|compile)$' + + # inject plantuml diagram source code into command + sed -E -i \ + -e "/@@@mysql2puml_plantuml_diagram@@@/r ${BASH_TOOLS_ROOT_DIR}/src/_binaries/Converters/testsData/mysql2puml.puml" \ + -e "/@@@mysql2puml_plantuml_diagram@@@/d" \ + "${DOC_DIR}/Commands.md" + + mkdir -p "${DOC_DIR}/src/_binaries/Converters/testsData" || true + cp "${BASH_TOOLS_ROOT_DIR}/src/_binaries/Converters/testsData/mysql2puml-model.png" "${DOC_DIR}/src/_binaries/Converters/testsData" + + # copy other files + cp "${BASH_TOOLS_ROOT_DIR}/README.md" "${DOC_DIR}/README.md" + sed -i -E \ + -e '//,//d' \ + -e 's#https://fchastanet.github.io/bash-tools/#/#' \ + -e 's#^> \*\*_TIP:_\*\* (.*)$#> [!TIP|label:\1]#' \ + "${DOC_DIR}/README.md" + + ShellDoc::fixMarkdownToc "${DOC_DIR}/README.md" + ShellDoc::fixMarkdownToc "${DOC_DIR}/Commands.md" + + if ((TOKEN_NOT_FOUND_COUNT > 0)); then + return 1 + fi + + Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" +} + +facade_main_8fd47ebedea14305a1452a7b8820b174 "$@" diff --git a/bin/gitIsAncestorOf b/bin/gitIsAncestorOf index 4f059242..26dbcd41 100755 --- a/bin/gitIsAncestorOf +++ b/bin/gitIsAncestorOf @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsAncestorOf.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsAncestorOf.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitIsAncestorOf +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage # shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,458 +86,606 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi -} - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 + local -i maxLineLength=$1 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" - - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac - done +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -551,116 +704,789 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - dir="$(dirname "${file}")" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi } -# Public: check if argument is a valid linux path +# @description check if an element is contained in an array # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_8f16db2b392c4f27975664b203757d91() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -Log::load +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + gitIsAncestorOfCommand help + exit 0 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} -HELP="$( - cat < -show an error if commit is not an ancestor of branch +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsAncestorOf.sh +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -Copyright (c) 2022 François Chastanet -EOF -)" -Args::defaultHelp "${HELP}" "$@" +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -if [[ "$#" != "2" ]]; then - Log::fatal "${SCRIPT_NAME}: invalid arguments" -fi +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} -claimedBranch="$1" -commit="$2" +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} -merge_base="$(git merge-base "${commit}" "${claimedBranch}")" -if [[ -z "${merge_base}" || "${merge_base}" != "$(git rev-parse --verify "${commit}")" ]]; then - Log::fatal "${commit} is not an ancestor of ${claimedBranch}" +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +gitIsAncestorOfCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountClaimedBranchArg + ((options_parse_argParsedCountClaimedBranchArg = 0)) || true + local -i options_parse_argParsedCountCommitArg + ((options_parse_argParsedCountCommitArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/14 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 2/14 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 3/14 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 4/14 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 5/14 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 6/14 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 7/14 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 8/14 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 9/14 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 10/14 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 11/14 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 12/14 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 13/14 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 14/14 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/2 + # Argument claimedBranchArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountClaimedBranchArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument claimedBranch - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountClaimedBranchArg)) + claimedBranchArg="${options_parse_arg}" + # Argument 2/2 + # Argument commitArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountCommitArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument commit - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountCommitArg)) + commitArg="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountClaimedBranchArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'claimedBranch' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountCommitArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'commit' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "check if commit is inside a given branch")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}claimedBranch${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ branch\ in\ which\ the\ commit\ will\ be\ searched + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}commit${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ commit\ oid\ to\ check + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}EXIT CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}1${__HELP_NORMAL}: if commit does not exists +${__HELP_OPTION_COLOR}2${__HELP_NORMAL}: if commit is not included in given branch""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsAncestorOf.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" +declare claimedBranchArg="" +declare commitArg="" + +gitIsAncestorOfCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + if ! git cat-file -t "${commitArg}" &>/dev/null; then + Log::displayError "Commit ${commitArg} does not exists at all" + exit 1 + fi + + # shellcheck disable=SC2154 + merge_base="$(git merge-base "${commitArg}" "${claimedBranchArg}")" + if [[ -z "${merge_base}" || "${merge_base}" != "$(git rev-parse --verify "${commitArg}")" ]]; then + Log::displayError "Commit ${commitArg} is not an ancestor of branch ${claimedBranchArg}" + exit 2 + fi +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi + +} + +facade_main_8f16db2b392c4f27975664b203757d91 "$@" diff --git a/bin/gitIsBranch b/bin/gitIsBranch index 8d906f3d..0c6f4201 100755 --- a/bin/gitIsBranch +++ b/bin/gitIsBranch @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsBranch.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsBranch.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitIsBranch +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,458 +86,606 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi -} - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 + local -i maxLineLength=$1 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" - - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac - done +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -551,114 +704,757 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} - dir="$(dirname "${file}")" +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" } -# Public: check if argument is a valid linux path +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} + +# @description check if an element is contained in an array # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_5c164a34fb6c464a90667d094e1f3bef() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} + +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + gitIsBranchCommand help + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} + +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} + +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -Log::load +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -HELP="$( - cat < -show an error if branchName is not a known branch +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsBranch.sh +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} -Copyright (c) 2022 François Chastanet -EOF -)" -Args::defaultHelp "${HELP}" "$@" +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} -if [[ "$#" != "1" ]]; then - Log::fatal "$0: invalid arguments" +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +gitIsBranchCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountBranchNameArg + ((options_parse_argParsedCountBranchNameArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/14 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 2/14 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 3/14 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 4/14 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 5/14 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 6/14 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 7/14 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 8/14 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 9/14 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 10/14 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 11/14 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 12/14 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 13/14 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 14/14 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/1 + # Argument branchNameArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountBranchNameArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument branchName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountBranchNameArg)) + branchNameArg="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountBranchNameArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'branchName' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "show an error if branchName is not a known branch")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}branchName${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ branch\ name\ to\ check + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitIsBranch.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" +declare branchNameArg="" + +gitIsBranchCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + # check various branch hierarchies, adjust as needed + # shellcheck disable=SC2154 + git show-ref --verify refs/heads/"${branchNameArg}" || + git show-ref --verify refs/remotes/"${branchNameArg}" || + Log::fatal "not a branch name: ${branchNameArg}" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -branch="$1" -# check various branch hierarchies, adjust as needed -git show-ref --verify refs/heads/"${branch}" || - git show-ref --verify refs/remotes/"${branch}" || - Log::fatal "not a branch name: ${branch}" +} + +facade_main_5c164a34fb6c464a90667d094e1f3bef "$@" diff --git a/bin/gitRenameBranch b/bin/gitRenameBranch index 73cc74d3..647480a9 100755 --- a/bin/gitRenameBranch +++ b/bin/gitRenameBranch @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitRenameBranch.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitRenameBranch.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitRenameBranch +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true +# store command arguments for later usage # shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,326 +86,297 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Ask user to enter y or n, retry until answer is correct -# @param {String} $1 message to display before asking -# @output displays message
[msg arg $1] (y or n)?
-# @output if characters entered different than [yYnN] displays "Invalid answer" and continue to ask -# @return 0 if yes, 1 else +# @description Ask user to enter y or n, retry until answer is correct +# @arg $1 message:String message to display before asking +# @stdout displays message
[msg arg $1] (y or n)?
+# @stdout if characters entered different than [yYnN] displays "Invalid answer" and continue to ask +# @exitcode 0 if yes +# @exitcode 1 else UI::askYesNo() { while true; do read -p "$1 (y or n)? " -n 1 -r @@ -417,107 +393,321 @@ UI::askYesNo() { done } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -536,205 +726,914 @@ Log::rotate() { fi } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - dir="$(dirname "${file}")" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi } -# Public: check if argument is a valid linux path +# @description check if an element is contained in an array # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_a7456d3b93ad4a0cb0c417608cfc15d7() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir +# shellcheck disable=SC2154 + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} + +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + gitRenameBranchCommand help + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} -Log::load +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -#default values -PUSH="0" -DELETE="0" -INTERACTIVE="1" - -# Usage info -showHelp() { - cat < [] [--push|-p] [--delete|-d] [--assume-yes|-yes|-y] - --help,-h prints this help and exits - -y, --yes, --assume-yes do not ask for confirmation (use with caution) - Automatic yes to prompts; assume "y" as answer to all prompts - and run non-interactively. - --push,-p push new branch - --delete,-d delete old remote branch - the new branch name to give to current branch - (optional) the name of the old branch if not current one - -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) - -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitRenameBranch.sh - -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License - -Copyright (c) 2022 François Chastanet -EOF -} - -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,push,delete,yes,assume-yes -o hpdy -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" -} - -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" ;; - --push | -p) - PUSH="1" + ERROR) + echo "${__LEVEL_ERROR}" ;; - --delete | -d) - DELETE="1" + WARNING) + echo "${__LEVEL_WARNING}" ;; - --assume-yes | -yes | -y) - INTERACTIVE="0" + INFO) + echo "${__LEVEL_INFO}" ;; - --) - shift || true - break + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" ;; *) - showHelp - Log::fatal "invalid argument $1" + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 esac +} + +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +gitRenameBranchCommand() { + local options_parse_cmd="$1" shift || true -done -shift $((OPTIND - 1)) || true - -newName="$1" -shift || true -oldName="${1:-}" -shift || true -if [[ $# -gt 0 ]]; then - Log::fatal "too much arguments provided" -fi -if ! git rev-parse --git-dir >/dev/null 2>&1; then - Log::fatal "not a git repository (or any of the parent directories)" -fi + if [[ "${options_parse_cmd}" = "parse" ]]; then + optionAssumeYes="0" + local -i options_parse_optionParsedCountOptionAssumeYes + ((options_parse_optionParsedCountOptionAssumeYes = 0)) || true + optionPush="0" + local -i options_parse_optionParsedCountOptionPush + ((options_parse_optionParsedCountOptionPush = 0)) || true + optionDelete="0" + local -i options_parse_optionParsedCountOptionDelete + ((options_parse_optionParsedCountOptionDelete = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountNewBranchNameArg + ((options_parse_argParsedCountNewBranchNameArg = 0)) || true + local -i options_parse_argParsedCountOldBranchNameArg + ((options_parse_argParsedCountOldBranchNameArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/17 + # Option optionAssumeYes --assume-yes|--yes|-y variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --assume-yes | --yes | -y) + optionAssumeYes="1" + if ((options_parse_optionParsedCountOptionAssumeYes >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionAssumeYes)) + ;; + # Option 2/17 + # Option optionPush --push|-p variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --push | -p) + optionPush="1" + if ((options_parse_optionParsedCountOptionPush >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionPush)) + ;; + # Option 3/17 + # Option optionDelete --delete|-d variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --delete | -d) + optionDelete="1" + if ((options_parse_optionParsedCountOptionDelete >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDelete)) + ;; + # Option 4/17 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 5/17 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 6/17 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 7/17 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 8/17 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 9/17 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 10/17 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 11/17 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 12/17 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 13/17 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 14/17 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 15/17 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 16/17 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 17/17 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/2 + # Argument newBranchNameArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountNewBranchNameArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument newBranchName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountNewBranchNameArg)) + newBranchNameArg="${options_parse_arg}" + # Argument 2/2 + # Argument oldBranchNameArg min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountOldBranchNameArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument oldBranchName - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountOldBranchNameArg)) + oldBranchNameArg="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountNewBranchNameArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'newBranchName' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + commandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "rename git local branch, push new branch and delete old branch")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--assume-yes|--yes|-y]" "[--push|-p]" "[--delete|-d]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}newBranchName${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ branch\ name\ to\ check + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " [${__HELP_OPTION_COLOR}oldBranchName${__HELP_NORMAL} {single}]" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ name\ of\ the\ old\ branch\ if\ not\ current\ one + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--assume-yes${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}--yes${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-y${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " $(Array::wrap ' ' 76 4 $(assumeYesHelpCallback))" + printf " %b\n" "${__HELP_OPTION_COLOR}--push${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-p${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< push\ the\ new\ branch + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--delete${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-d${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< delete\ the\ old\ remote\ branch + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +${__HELP_TITLE}EXIT CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}1${__HELP_NORMAL} : if current directory is not a git repository + or if invalid or missing arguments +${__HELP_OPTION_COLOR}2${__HELP_NORMAL} : if impossible to compute current branch name +${__HELP_OPTION_COLOR}3${__HELP_NORMAL} : master/main branch not supported by this command, + please do it manually +${__HELP_OPTION_COLOR}5${__HELP_NORMAL} : New and old branch names are the same +${__HELP_OPTION_COLOR}6${__HELP_NORMAL} : You can use this tool in non interactive mode only + if --assume-yes option is provided +${__HELP_OPTION_COLOR}7${__HELP_NORMAL} : if failed to rename local branch +${__HELP_OPTION_COLOR}8${__HELP_NORMAL} : if remote branch deletion failed +${__HELP_OPTION_COLOR}9${__HELP_NORMAL} : if failed to push the new branch""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/gitRenameBranch.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" -if [[ -z "${oldName}" ]]; then - oldName="$(git branch --show-current)" - [[ -z "${oldName}" ]] && Log::fatal "Impossible to calculate current branch name" -fi -[[ "${oldName}" =~ ^(master|main)$ ]] && - Log::fatal "master/main branch not supported by this command, please do it manually" -[[ "${newName}" =~ ^(master|main)$ ]] && - Log::fatal "master/main branch not supported by this command, please do it manually" -[[ -z "${newName}" ]] && Log::fatal "new branch name not provided" -[[ "${oldName}" = "${newName}" ]] && Log::fatal "Branch name has not changed" - -Log::displayInfo "Renaming branch locally from ${oldName} to ${newName}" -declare -a CMD=() -CMD=(git branch -m "${oldName}" "${newName}") -Log::displayDebug "Running '${CMD[*]}'" -"${CMD[@]}" - -if [[ "${DELETE}" = "1" ]]; then - deleteBranch() { - Log::displayInfo "Removing eventual old remote branch ${oldName}" - CMD=(git push origin ":${oldName}") - Log::displayDebug "Running '${CMD[*]}'" - "${CMD[@]}" || true - } - if [[ "${INTERACTIVE}" = "0" ]] || UI::askYesNo "remove eventual old remote branch ${oldName}"; then - deleteBranch +#default values +declare optionPush="0" +declare optionDelete="0" +declare optionAssumeYes="0" +declare newBranchNameArg="" +declare oldBranchNameArg="" + +assumeYesHelpCallback() { + echo "do not ask for confirmation (use with caution)" $'\n' + echo ' Automatic yes to prompts; assume "y" as answer to all prompts' $'\n' + echo ' and run non-interactively.' +} + +commandCallback() { + if ! Assert::tty && [[ "${optionAssumeYes}" != "1" ]]; then + Log::displayError "You can use this tool in non interactive mode only if --assume-yes option is provided" + exit 6 fi -fi -if [[ "${PUSH}" = "1" ]]; then - push() { - Log::displayInfo "Pushing new branch name ${newName}" - CMD=(git push --set-upstream origin "${newName}") - Log::displayDebug "Running '${CMD[*]}'" - "${CMD[@]}" || true - } - if [[ "${INTERACTIVE}" = "0" ]] || UI::askYesNo "Push new branch name ${newName}"; then - push +} + +gitRenameBranchCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + local -a cmd=() + if ! git rev-parse --git-dir >/dev/null 2>&1; then + Log::displayError "not a git repository (or any of the parent directories)" + exit 1 + fi + + if [[ -z "${oldBranchNameArg}" ]]; then + oldBranchNameArg="$(git branch --show-current)" + if [[ -z "${oldBranchNameArg}" ]]; then + Log::displayError "Impossible to compute current branch name" + exit 2 + fi + fi + + if [[ "${oldBranchNameArg}" =~ ^(master|main)$ || "${newBranchNameArg}" =~ ^(master|main)$ ]]; then + Log::displayError "master/main branch not supported by this command, please do it manually" + exit 3 + fi + + if [[ -z "${newBranchNameArg}" ]]; then + Log::displayError "new branch name not provided" + exit 4 + fi + + if [[ "${oldBranchNameArg}" = "${newBranchNameArg}" ]]; then + Log::displayError "New and old branch names are the same" + exit 5 + fi + + Log::displayInfo "Renaming branch locally from ${oldBranchNameArg} to ${newBranchNameArg}" + declare -a cmd=() + cmd=(git branch -m "${oldBranchNameArg}" "${newBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to rename local branch ${oldBranchNameArg} to ${newBranchNameArg}" + exit 7 + fi + + if [[ "${optionDelete}" = "1" ]]; then + if [[ "${optionAssumeYes}" = "1" ]] || UI::askYesNo "Remove eventual old remote branch ${oldBranchNameArg}"; then + Log::displayInfo "Removing eventual old remote branch ${oldBranchNameArg}" + cmd=(git push origin ":${oldBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to delete remote branch ${oldBranchNameArg}" + exit 8 + fi + fi + fi + + if [[ "${optionPush}" = "1" ]]; then + if [[ "${optionAssumeYes}" = "1" ]] || UI::askYesNo "Push new branch name ${newBranchNameArg}"; then + Log::displayInfo "Pushing new branch name ${newBranchNameArg}" + cmd=(git push --set-upstream origin "${newBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to push the new branch ${newBranchNameArg}" + exit 9 + fi + fi fi +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi + +} + +facade_main_a7456d3b93ad4a0cb0c417608cfc15d7 "$@" diff --git a/bin/installRequirements b/bin/installRequirements index a5551614..4e907e71 100755 --- a/bin/installRequirements +++ b/bin/installRequirements @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/installRequirements.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/installRequirements.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/installRequirements +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") # shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,234 +86,171 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi -} - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 + local -i maxLineLength=$1 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# clone the repository if not done yet, else pull it if no change in it -# @param {String} $1 directory in which repository is installed or will be cloned -# @param {String} $2 repository url -# @param {function} $3 callback on successful clone -# @param {function} $4 callback on successful pull -# @param {$@} gitCloneOptions -# @return 0 on successful pulling/cloning, 1 on failure +# @description clone the repository if not done yet, else pull it if no change in it +# @arg $1 dir:String directory in which repository is installed or will be cloned +# @arg $2 repo:String repository url +# @arg $3 cloneCallback:Function callback on successful clone +# @arg $4 pullCallback:Function callback on successful pull +# @env GIT_CLONE_OPTIONS:String additional options to pass to git clone command +# @exitcode 0 on successful pulling/cloning, 1 on failure Git::cloneOrPullIfNoChanges() { local dir="$1" shift || true @@ -320,275 +262,496 @@ Git::cloneOrPullIfNoChanges() { shift || true if [[ -d "${dir}/.git" ]]; then - Git::pullIfNoChanges "${dir}" && ( + if Git::pullIfNoChanges "${dir}"; then # shellcheck disable=SC2086 if [[ "$(type -t ${pullCallback})" = "function" ]]; then ${pullCallback} "${dir}" fi - ) + fi else Log::displayInfo "cloning ${repo} ..." mkdir -p "$(dirname "${dir}")" - git clone "${GIT_CLONE_OPTIONS[@]}" --progress "$@" "${repo}" "${dir}" && ( + # shellcheck disable=SC2086,SC2248 + if git clone ${GIT_CLONE_OPTIONS} --progress "$@" "${repo}" "${dir}"; then # shellcheck disable=SC2086 if [[ "$(type -t ${cloneCallback})" = "function" ]]; then ${cloneCallback} "${dir}" fi - ) + else + Log::displayError "Cloning '${repo}' on '${dir}' failed" + return 1 + fi fi } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" - - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac - done +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# pull the repository if no change in it -# @return 0 on successful pulling, 1 on failure or no pull needed -Git::pullIfNoChanges() { - local dir="$1" - if [[ -d "${dir}/.git" ]]; then - ( - cd "${dir}" - git update-index --refresh &>/dev/null || true - if git diff-index --quiet HEAD --; then - Log::displayInfo "Pull git repository '${dir}' as no changes detected" - git pull --progress - return 0 - else - Log::displayWarning "Pulling git repository '${dir}' avoided as changes detected" - fi - ) && return 0 +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' fi - return 1 } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description pull git directory only if no change has been detected +# @arg $1 dir:String the git directory to pull +# @exitcode 0 on successful pulling +# @exitcode 1 on any other failure +# @exitcode 2 changes detected, pull avoided +# @exitcode 3 not a git directory +# @exitcode 4 not able to update index +# @stderr diagnostics information is displayed +# @require Git::requireGitCommand +Git::pullIfNoChanges() { + local dir="$1" + if [[ ! -d "${dir}/.git" ]]; then + return 3 + fi + ( + cd "${dir}" || exit 3 + git update-index --refresh &>/dev/null || exit 4 + if ! git diff-index --quiet HEAD --; then + Log::displayWarning "Pulling git repository '${dir}' avoided as changes detected" + exit 2 + fi + Log::displayInfo "Pull git repository '${dir}' as no changes detected" + git pull --progress + ) } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -607,128 +770,776 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 +} + +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 fi + Log::logWarning "$1" } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} - dir="$(dirname "${file}")" +# @description ensure command git is available +# @exitcode 1 if git command not available +# @stderr diagnostics information is displayed +Git::requireGitCommand() { + Assert::commandExists git +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } -# Public: check if argument is a valid linux path +# @description check if command specified exists or return 1 +# with error and message if not +# +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" + + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" + fi + return 1 + } + return 0 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_ae2d7ee85d7a47bdbda1838afc5f3c2c() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Git::requireGitCommand +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} + +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + installRequirementsCommand help + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} + +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} + +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} -Log::load +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +installRequirementsCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/14 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 2/14 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 3/14 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 4/14 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 5/14 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 6/14 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 7/14 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 8/14 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 9/14 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 10/14 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 11/14 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 12/14 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 13/14 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 14/14 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided" + return 1 + fi + ;; + esac + shift || true + done + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "installs requirements")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +INSTALLS REQUIREMENTS: +- fchastanet/bash-tools-framework +- and fchastanet/bash-tools-framework useful binaries: + bin/awkLint, bin/buildBinFiles, bin/frameworkLint, bin/findShebangFiles, bin/megalinter, bin/runBuildContainer, bin/shellcheckLint, bin/test, bin/buildPushDockerImage""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/installRequirements.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare -a externalBinaries=([0]="bin/awkLint" [1]="bin/buildBinFiles" [2]="bin/frameworkLint" [3]="bin/findShebangFiles" [4]="bin/megalinter" [5]="bin/runBuildContainer" [6]="bin/shellcheckLint" [7]="bin/test" [8]="bin/buildPushDockerImage") +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" + +installRequirementsCommand parse "${BASH_FRAMEWORK_ARGV[@]}" if [[ "$(id -u)" = "0" ]]; then Log::fatal "this script should be executed as normal user" fi -HELP="$( - cat </dev/null +else + run +fi + +} + +facade_main_ae2d7ee85d7a47bdbda1838afc5f3c2c "$@" diff --git a/bin/mysql2puml b/bin/mysql2puml index 351e4b80..9b20b421 100755 --- a/bin/mysql2puml +++ b/bin/mysql2puml @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Converters/mysql2puml.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Converters/mysql2puml.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/mysql2puml +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,128 +86,153 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } -trap interruptManagement INT -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} - fi - return "${status}" -} - -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi fi + ((firstLine = 0)) || true done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") + if [[ "${needEcho}" = "1" ]]; then + echo + fi } -# Public: get absolute conf file from specified conf folder deduced using these rules +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description get absolute conf file from specified conf folder deduced using these rules # * from absolute file (ignores and ) # * relative to where script is executed (ignores and ) # * from home/.bash-tools/ # * from framework conf/ # -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 conf file to use without extension -# * $3 the extension (sh by default) +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 conf:String file to use without extension +# @arg $3 extension:String the extension (.sh by default) # -# Returns absolute conf filename +# @stdout absolute conf filename +# @exitcode 1 if file is not found in any location Conf::getAbsoluteFile() { local confFolder="$1" local conf="$2" @@ -252,22 +282,22 @@ Conf::getAbsoluteFile() { return 1 } -# Public: list the conf files list available in bash-tools/conf/ folder +# @description list the conf files list available in bash-tools/conf/ folder # and those overridden in ${HOME}/.bash-tools/ folder -# **Arguments**: -# * $1 confFolder the directory name (not the path) to list -# * $2 the extension (sh by default) -# * $3 the indentation (' - ' by default) can be any string compatible with sed not containing any / # -# **Output**: list of files without extension/directory -# eg: +# @arg $1 confFolder:String the directory name (not the path) to list +# @arg $2 extension:String the extension (.sh by default) +# @arg $3 indentStr:String the indentation (' - ' by default) can be any string compatible with sed not containing any / +# +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root Conf::getMergedList() { local confFolder="$1" - local extension="${2:-sh}" - local indentStr="${3:- - }" + local extension="${2-sh}" + local indentStr="${3- - }" local DEFAULT_CONF_DIR="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}" local HOME_CONF_DIR="${HOME}/.bash-tools/${confFolder}" @@ -282,234 +312,305 @@ Conf::getMergedList() { ) | sort | uniq } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 -} - -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } +# @description remove all empty lines +# - at the beginning of the file before non empty line +# - at the end of the file after last non empty line +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# shellcheck disable=SC2120 # @see https://unix.stackexchange.com/a/653883 Filters::trimEmptyLines() { awk ' NF {print saved $0; saved = ""; started = 1; next} started {saved = saved $0 ORS} - ' + ' "$@" +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" } -# Public: log level off +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" +} + +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Public: list files of dir with given extension and display it as a list one by line +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} + +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi +} + +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description list files of dir with given extension and display it as a list one by line # -# @param {String} dir $1 the directory to list -# @param {String} prefix $2 the profile file prefix (default: "") -# @param {String} ext $3 the extension -# @param {String} findOptions $4 find options, eg: -type d -# @paramDefault {String} findOptions $4 '-type f' -# @param {String} indentStr $5 the indentation can be any string compatible with sed not containing any / -# @paramDefault {String} indentStr $5 ' - ' -# @output list of files without extension/directory -# eg: +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text # - default.local # - default.remote # - localhost-root -# @return 1 if directory does not exists +# @exitcode 1 if directory does not exists Conf::list() { local dir="$1" local prefix="${2:-}" @@ -532,115 +633,228 @@ Conf::list() { ) } -File::concatenatePath() { - local basePath="${1}" - local subPath=${2} - local fullPath="${basePath:+${basePath}/}${subPath}" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done - realpath -m "${fullPath}" 2>/dev/null + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description concatenate 2 paths and ensure the path is correct using realpath -m +# @arg $1 basePath:String +# @arg $2 subPath:String +# @require Linux::requireRealpathCommand +File::concatenatePath() { + local basePath="$1" + local subPath="$2" + local fullPath="${basePath:+${basePath}/}${subPath}" + + realpath -m "${fullPath}" 2>/dev/null } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} - -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -659,189 +873,807 @@ Log::rotate() { fi } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} - dir="$(dirname "${file}")" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} + +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath } -# Public: check if argument is a valid linux path +# @description check if command specified exists or return 1 +# with error and message if not # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist +# +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" + + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" + fi + return 1 + } + return 0 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_0c81f03399944490b21f8ac30d7e073b() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Linux::requireRealpathCommand +Log::requireLoad +Compiler::Facade::requireCommandBinDir + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} -Log::load +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + mysql2pumlCommand help + exit 0 +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -#default values -SCRIPT_VERSION="0.1" -SKIN="default" +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} -# Usage info -showHelp() { - local skinList="" - skinList="$(Conf::getMergedList "mysql2pumlSkins" ".puml")" +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} - cat </dev/null) || { - showHelp - Log::fatal "invalid options specified" +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --version) - showVersion - exit 0 - ;; - --skin | -s) - shift - SKIN="$1" - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac +mysql2pumlCommand() { + local options_parse_cmd="$1" shift || true -done -shift $((OPTIND - 1)) || true - -sqlFile="${1:-}" -shift || true -if (($# > 0)); then - showHelp - Log::fatal "too much arguments provided" -fi -absSkinFile="$(Conf::getAbsoluteFile "mysql2pumlSkins" "${SKIN}" "puml")" || - Log::fatal "the skin ${SKIN} does not exist" + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionSkin + ((options_parse_optionParsedCountOptionSkin = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountInputSqlFile + ((options_parse_argParsedCountInputSqlFile = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/15 + # Option optionSkin --skin variableType String min 0 max 1 authorizedValues '' regexp '' + --skin) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionSkin >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionSkin)) + optionSkin="$1" + optionSkinCallback "${options_parse_arg}" "${optionSkin}" + ;; + # Option 2/15 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 3/15 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 4/15 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 5/15 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 6/15 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 7/15 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 8/15 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 9/15 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 10/15 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 11/15 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 12/15 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 13/15 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 14/15 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 15/15 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/1 + # Argument inputSqlFile min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountInputSqlFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument inputSqlFile - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountInputSqlFile)) + inputSqlFile="${options_parse_arg}" + inputSqlFileCallback "${inputSqlFile}" -- "${@:2}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "convert mysql dump sql schema to plantuml format")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--skin ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " [${__HELP_OPTION_COLOR}inputSqlFile${__HELP_NORMAL} {single}]" + local -a helpArray + IFS=' ' read -r -a helpArray <<< sql\ filepath\ to\ parse\ \(read\ from\ stdin\ if\ not\ provided\) + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--skin ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< header\ configuration\ of\ the\ plant\ uml\ file\ \(default:\ default\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +Examples +mysql2puml dump.dql -if [[ -n "${sqlFile}" ]]; then - if [[ ! -f "${sqlFile}" ]]; then - Log::fatal "file ${sqlFile} does not exist" +mysqldump --skip-add-drop-table --skip-add-locks --skip-disable-keys --skip-set-charset --user=root --password=root --no-data skills | mysql2puml + +List of available skins: +@@@SKINS_LIST@@@""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Converters/mysql2puml.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 fi - exec 3<"${sqlFile}" -elif [[ ! -t 0 ]]; then - exec 3<&0 -else - Log::fatal "No sql file provided..." -fi +} +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" +declare optionSkin="default" + +optionHelpCallback() { + local skinListHelpFile + skinListHelpFile="$(Framework::createTempFile "shellcheckHelp")" + Conf::getMergedList "mysql2pumlSkins" ".puml" " - " >"${skinListHelpFile}" + + mysql2pumlCommand help | + sed -E \ + -e "/@@@SKINS_LIST@@@/r ${skinListHelpFile}" \ + -e "/@@@SKINS_LIST@@@/d" + exit 0 +} + +optionSkinCallback() { + declare -a skinList + readarray -t skinList < <(Conf::getMergedList "mysql2pumlSkins" ".puml" "") + if ! Array::contains "$2" "${skinList[@]}"; then + Log::displayError "${SCRIPT_NAME} - invalid skin '$2' provided" + return 1 + fi +} + +inputSqlFileCallback() { + # shellcheck disable=SC2154 + if [[ ! -f "${inputSqlFile}" ]]; then + Log::displayError "${SCRIPT_NAME} - File '${inputSqlFile}' does not exists" + return 1 + fi +} + +mysql2pumlCommand parse "${BASH_FRAMEWORK_ARGV[@]}" +declare awkScript awkScript="$( cat <<'EOF' # ========================================================================= @@ -1063,4 +1895,27 @@ END { # ========================================================================= EOF )" -awk --source "${awkScript}" "${absSkinFile}" - <&3 | Filters::trimEmptyLines + +run() { + # shellcheck disable=SC2154 + absSkinFile="$(Conf::getAbsoluteFile "mysql2pumlSkins" "${optionSkin}" "puml")" || + Log::fatal "the skin ${optionSkin} does not exist" + + if [[ -n "${inputSqlFile}" ]]; then + exec 3<"${inputSqlFile}" + elif [[ ! -t 0 ]]; then + exec 3<&0 + fi + + awk --source "${awkScript}" "${absSkinFile}" - <&3 | Filters::trimEmptyLines +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi + +} + +facade_main_0c81f03399944490b21f8ac30d7e073b "$@" diff --git a/bin/upgradeGithubRelease b/bin/upgradeGithubRelease index 191fa364..e36e4341 100755 --- a/bin/upgradeGithubRelease +++ b/bin/upgradeGithubRelease @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/upgradeGithubRelease.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/upgradeGithubRelease.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/upgradeGithubRelease +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,247 +86,223 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} - fi - return "${status}" -} - -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi fi + ((firstLine = 0)) || true done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") + if [[ "${needEcho}" = "1" ]]; then + echo + fi } +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + # Checks if file can be created in folder +# + +# @description Checks if file can be created in folder # The file does not need to exist +# @arg $1 file:String +# @exitcode 1 if file is not a valid path +# @exitcode 2 if file parent's dir is not writable +# @exitcode 3 if existing file is not writable +# @see Assert::validPath Assert::fileWritable() { local file="$1" local dir - dir="$(dirname "${file}")" + Assert::validPath "${file}" || return 1 + if [[ -f "${file}" ]]; then + [[ -w "${file}" ]] || return 3 + else + dir="$(dirname "${file}")" + [[ -w "${dir}" ]] || return 2 + fi - Assert::validPath "${file}" && [[ -w "${dir}" ]] } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid +# @description check if argument is a valid linux path # invalid path are those with: # - invalid characters -# - component beginning by a - (because option) +# - component beginning by a - (because could be considered as a command's option) # - not beginning with a slash # - relative +# +# @arg $1 path:string path that needs to be checked +# @exitcode 1 if path is invalid +# @see https://regex101.com/r/afLrmM/2 +# @see Assert::validPosixPath if you need more restrictive check Assert::validPath() { local path="$1" - # https://regex101.com/r/afLrmM/2 [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } +# @description intermediate callback that is used by Github::upgradeRelease +# or Github::installRelease +# if installCallback is not set, it allows to: +# - copy the downloaded file to the right target file +# - and set the execution bit +# else +# installCallback is called with newSoftware, targetFile, version arguments +# fi +# @warning do not use this function as callback for Github::upgradeRelease or Github::installRelease, as it would result to an infinite loop +# @arg $1 newSoftware:String the downloaded software file +# @arg $2 targetFile:String where we want to copy the file +# @arg $3 version:String the version that has been downloaded +# @arg $4 installCallback:Function (optional) the callback to call with 3 first arguments +# @exitcode * on failure +# @see Github::upgradeRelease +# @see Github::installRelease +# @internal Github::defaultInstall() { local newSoftware="$1" local targetFile="$2" @@ -333,16 +314,16 @@ Github::defaultInstall() { ${installCallback} "${newSoftware}" "${targetFile}" "${version}" else mv "${newSoftware}" "${targetFile}" + chmod +x "${targetFile}" + hash -r + rm -f "${newSoftware}" || true fi - chmod +x "${targetFile}" - hash -r - rm -f "${newSoftware}" || true } -# download specified release software version from github -# @param {String} releaseUrl $1 eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 -# @return 1 on failure -# @output the path to the downloaded release +# @description download specified release software version from github +# @arg $1 releaseUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 +# @exitcode 1 on failure +# @stdout the path to the downloaded release Github::downloadReleaseVersion() { local releaseUrl="$1" local newSoftwarePath @@ -357,10 +338,10 @@ Github::downloadReleaseVersion() { echo "${newSoftwarePath}" } -# Retrieve the latest version number for given github url -# @param {String} $1 github url from which repository will be extracted -# @error log messages about retry -# @output the version number retrieved +# @description Retrieve the latest version number for given github url +# @arg $1 releaseUrl:String github url from which repository will be extracted +# @stderr log messages about retry +# @stdout the version number retrieved Github::getLatestVersionFromUrl() { local releaseUrl="$1" local repo @@ -377,172 +358,244 @@ Github::getLatestVersionFromUrl() { echo "${latestVersion}" } -# Public: log level off +# @description check if specified release software version exists in github +# @arg $1 releaseUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 +# @exitcode 1 on failure +# @exitcode 0 if release version exists +Github::isReleaseVersionExist() { + local releaseUrl="$1" + + curl \ + -L \ + -o /dev/null \ + --silent \ + --head \ + --fail \ + "${releaseUrl}" +} + +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using error color (red) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi Log::logError "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using info color (blue) but warning level -# @param {String} $1 message +# @description Display message using info color (blue) but warning level +# @arg $1 message:String the message to display Log::displayStatus() { local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logStatus "$1" "${type}" } -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Check that command version is greater than expected minimal version -# display warning if command version greater than expected minimal version -# display error if command version less than expected minimal version and exit 1 -# @param {String} $1 command path -# @param {String} $2 command line parameters to launch to get command version -# @param {String} $3 expected minimal command version -# @param {String} $4 optional help message to display if command does not exist -# @return 1 if command version less than expected minimal version, 0 otherwise -# @return 2 if command does not exist -Version::checkMinimal() { - local commandName="$1" - local argVersion="$2" - local minimalVersion="$3" - local parseVersionCallback=${4:-Version::parse} - local help="${4:-}" - - Assert::commandExists "${commandName}" "${help}" || return 2 - - local version - version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback})" - - Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" - - Version::compare "${version}" "${minimalVersion}" || { - local result=$? - if [[ "${result}" = "1" ]]; then - Log::displayWarning "${commandName} version is ${version} greater than ${minimalVersion}, OK let's continue" - elif [[ "${result}" = "2" ]]; then - Log::displayError "${commandName} minimal version is ${minimalVersion}, your version is ${version}" - return 1 - fi - return 0 - } +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# @param $1 version 1 -# @param $2 version 2 -# @return -# 0 if equal -# 1 if version1 > version2 -# 2 else +# @description compare 2 version numbers +# @arg $1 version1:String version 1 +# @arg $2 version2:String version 2 +# @exitcode 0 if equal +# @exitcode 1 if version1 > version2 +# @exitcode 2 else Version::compare() { if [[ "$1" = "$2" ]]; then return 0 @@ -569,38 +622,187 @@ Version::compare() { return 0 } -# filter to keep only version number from a string -# @stdin the string to parse +# @description filter to keep only version number from a string +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# shellcheck disable=SC2120 Version::parse() { - sed -En 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' | head -n1 + sed -En 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' "$@" | head -n1 } -# Public: check if command specified exists or return 1 -# with error and message if not -# -# **Arguments**: -# * $1 commandName on which existence must be checked -# * $2 helpIfNotExists a help command to display if the command does not exist +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" +} + +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" # -# **Exit**: code 1 if the command specified does not exist -Assert::commandExists() { - local commandName="$1" - local helpIfNotExists="$2" +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done - "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { - Log::displayError "${commandName} is not installed, please install it" - if [[ -n "${helpIfNotExists}" ]]; then - Log::displayInfo "${helpIfNotExists}" + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 +} + +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 fi - return 1 - } - return 0 + echo "${file}" + done } -# github repository eg: kubernetes-sigs/kind -# @param {String} githubUrl eg: https://github.com/kubernetes-sigs/kind/releases/download/@latestVersion@/kind-linux-amd64 -# @return 1 if no matching repo found in provided url, 0 otherwise -# @output the repo in the form owner/repo +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport +} + +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done +} + +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable +} + +# @description github repository eg: kubernetes-sigs/kind +# @arg $1 githubUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/@latestVersion@/kind-linux-amd64 +# @exitcode 1 if no matching repo found in provided url, 0 otherwise +# @stdout the repo in the form owner/repo Github::extractRepoFromGithubUrl() { local githubUrl="$1" local result @@ -611,11 +813,11 @@ Github::extractRepoFromGithubUrl() { echo "${result}" } -# Retrieve the latest version number of a github release using Github API using retry -# @param {String} $1 repository in the format fchastanet/bash-tools -# that would match https://github.com/fchastanet/bash-tools -# @param {String} $2 reference to a variable that will contain the result of the command -# @output log messages about retry +# @description Retrieve the latest version number of a github release using Github API using retry +# repo arg with fchastanet/bash-tools value would match https://github.com/fchastanet/bash-tools +# @arg $1 repo:String repository in the format fchastanet/bash-tools +# @arg $2 resultRef:&String reference to a variable that will contain the result of the command +# @stdout log messages about retry Github::getLatestRelease() { local repo="$1" # we need to pass the result through a reference instead of output directly @@ -626,6 +828,7 @@ Github::getLatestRelease() { resultFile="$(mktemp -p "${TMPDIR:-/tmp}" -t githubLatestRelease.XXXX)" # Get latest release from GitHub api if Retry::default curl \ + -L \ -o "${resultFile}" \ --fail \ --silent \ @@ -639,85 +842,91 @@ Github::getLatestRelease() { rm -f "${resultFile}" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" -} - -# Display message using skip color (yellow) -# @param {String} $1 message +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi Log::logSkipped "$1" } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logStatus() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-STATUS}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -736,54 +945,107 @@ Log::rotate() { fi } -# Retry a command 5 times with a delay of 15 seconds between each attempt -# @param $@ the command to run -# @return 0 on success, 1 if max retries count reached +# @description Retry a command 5 times with a delay of 15 seconds between each attempt +# @arg $@ command:String[] the command to run +# @exitcode 0 on success +# @exitcode 1 if max retries count reached Retry::default() { Retry::parameterized 5 15 "" "$@" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 +} + +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} + +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" fi } -# Retry a command several times depending on parameters -# @param {int} $1 max retries -# @param {int} $2 delay between attempt -# @param {String} $3 message to display to describe the attempt -# @param ... $@ rest of parameters, the command to run -# @return 0 on success -# @return 1 if max retries count reached -# @return 2 if maxRetries invalid value +# @description Retry a command several times depending on parameters +# @arg $1 maxRetries:int $1 max retries +# @arg $2 delay:int between attempt +# @arg $3 message:String to display to describe the attempt +# @arg $@ rest of parameters, the command to run +# @exitcode 0 on success +# @exitcode 1 if max retries count reached +# @exitcode 2 if maxRetries invalid value Retry::parameterized() { local maxRetries=$1 shift || true @@ -813,224 +1075,970 @@ Retry::parameterized() { return 0 } -# extract version number from github api +# @description extract version number from github api +# @noargs # @stdin json result of github API +# @exitcode 1 if jq or Version::parse fails +# @stdout the version parsed +# @require Linux::requireJqCommand Version::githubApiExtractVersion() { jq -r ".tag_name" | Version::parse } -# FUNCTIONS +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +# @description ensure command jq is available +# @exitcode 1 if jq command not available +# @stderr diagnostics information is displayed +Linux::requireJqCommand() { + Assert::commandExists jq +} -Log::load +# @description check if command specified exists or return 1 +# with error and message if not +# +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist +# +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" -Env::pathPrepend "${COMMAND_BIN_DIR}" + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" + fi + return 1 + } + return 0 +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# FUNCTIONS -#default values -TARGET_FILE="" -VERSION_ARG="--version" -MIN_VERSION="" -CURRENT_VERSION="" -EXACT_VERSION="" -GITHUB_URL_PATTERN="" +facade_main_24dcc22b6cc8473babfc796abe45df9b() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -# Usage info -showHelp() { - cat < - [--version-arg ] - [--minimal-version|-m ] - [--current-version|-c ] - [--exact-version|-e ] +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + upgradeGithubReleaseCommand help + exit 0 +} - --help,-h prints this help and exits +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 +} - --version-arg : The argument that will be provided to the currently installed binary - to check the version of the software. This parameter is needed if --minimal-version - argument is used and is different than default value (--version). +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} - --current-version|-c : sometimes the command to retrieve the version is complicated - some command needs you to parse json or other commands provides multiple sub command versions. - In this case you can provide the version you currently have, see examples below. +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} - --minimal-version|-m : if provided, if currently installed binary is below - this minimalVersion, a new version of the binary will be installed. If this argument is not - provided, the latest binary is unconditionally downloaded from github. +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} - --current-version|-c and --version-arg are mutually exclusive, you cannot use both argument at the - same time. +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} - --exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at - the same time. +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} - the binary downloaded will e written to this file path. Ensure the path is writable. - the url pattern to use to download the binary, see examples below. - @version@ is template variable that will be replaced by the latest version tag found on - github. +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -${__HELP_TITLE}Github template urls examples:${__HELP_NORMAL} +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -Simple ones(Sometimes @version@ template variable has to be specified twice): -"https://github.com/hadolint/hadolint/releases/download/v@version@/hadolint-Linux-x86_64" -"https://github.com/koalaman/shellcheck/releases/download/v@version@/shellcheck-v@version@.linux.x86_64.tar.xz" -"https://github.com/sharkdp/fd/releases/download/v@version@/fd_@version@_amd64.deb" -"https://github.com/sharkdp/bat/releases/download/v@version@/bat_@version@_amd64.deb" -'https://github.com/kubernetes-sigs/kind/releases/download/v@version@/kind-linux-amd64' -"https://github.com/kubernetes/minikube/releases/download/v@version@/minikube-linux-amd64" -"https://github.com/plantuml/plantuml/releases/download/v@version@/plantuml-@version@.jar" -"https://github.com/Versent/saml2aws/releases/download/v@version@/saml2aws_@version@_linux_amd64.tar.gz" +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -If you want to add condition on architecture(linux, windows, x86, 64/32 bits): -"https://github.com/docker/compose/releases/download/v@version@/docker-compose-\$(uname -s | tr '[:upper:]' '[:lower:]')-\$(uname -m)" -"https://github.com/docker/docker-credential-helpers/releases/download/v@version@/docker-credential-wincred-v@version@.windows-\$(dpkg --print-architecture).exe" -"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)" +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -${__HELP_TITLE}Command examples:${__HELP_NORMAL} -upgradeGithubRelease +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/upgradeGithubRelease.sh +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} -Copyright (c) 2022 François Chastanet -EOF +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi } -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt \ - -l help,version-arg:,minimal-version:,current-version:,exact-version: \ - -o hm:c:e: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --version-arg) - shift || true - VERSION_ARG="$1" - ;; - --minimal-version | -m) - shift || true - MIN_VERSION="$1" - ;; - --current-version | -c) - shift || true - CURRENT_VERSION="$1" - ;; - --exact-version | -e) - shift || true - EXACT_VERSION="$1" - ;; - --) - shift || true - break - ;; - *) - Log::fatal "invalid argument $1" - ;; - esac +upgradeGithubReleaseCommand() { + local options_parse_cmd="$1" shift || true -done -shift $((OPTIND - 1)) || true -if [[ -n "${EXACT_VERSION}" && -n "${MIN_VERSION}" ]]; then - Log::fatal "--exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at the same time." -fi -if (($# != 2)); then - Log::fatal "Exactly 2 fixed arguments are required" -fi -TARGET_FILE="$1" -GITHUB_URL_PATTERN="$2" + if [[ "${options_parse_cmd}" = "parse" ]]; then + optionVersionArg=--version + local -i options_parse_optionParsedCountOptionVersionArg + ((options_parse_optionParsedCountOptionVersionArg = 0)) || true + local -i options_parse_optionParsedCountOptionCurrentVersion + ((options_parse_optionParsedCountOptionCurrentVersion = 0)) || true + local -i options_parse_optionParsedCountOptionExactVersion + ((options_parse_optionParsedCountOptionExactVersion = 0)) || true + local -i options_parse_optionParsedCountOptionMinimalVersion + ((options_parse_optionParsedCountOptionMinimalVersion = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountTargetFileArg + ((options_parse_argParsedCountTargetFileArg = 0)) || true + local -i options_parse_argParsedCountGithubUrlPatternArg + ((options_parse_argParsedCountGithubUrlPatternArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/18 + # Option optionVersionArg --version-arg variableType String min 0 max 1 authorizedValues '' regexp '' + --version-arg) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionVersionArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersionArg)) + optionVersionArg="$1" + ;; + # Option 2/18 + # Option optionCurrentVersion --current-version|-c variableType String min 0 max 1 authorizedValues '' regexp '' + --current-version | -c) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionCurrentVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionCurrentVersion)) + optionCurrentVersion="$1" + ;; + # Option 3/18 + # Option optionExactVersion --exact-version|-e variableType String min 0 max 1 authorizedValues '' regexp '' + --exact-version | -e) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionExactVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionExactVersion)) + optionExactVersion="$1" + ;; + # Option 4/18 + # Option optionMinimalVersion --minimal-version|-m variableType String min 0 max 1 authorizedValues '' regexp '' + --minimal-version | -m) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionMinimalVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionMinimalVersion)) + optionMinimalVersion="$1" + ;; + # Option 5/18 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 6/18 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 7/18 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 8/18 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 9/18 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 10/18 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 11/18 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 12/18 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 13/18 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 14/18 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 15/18 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 16/18 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 17/18 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 18/18 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/2 + # Argument targetFileArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountTargetFileArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument targetFile - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountTargetFileArg)) + targetFileArg="${options_parse_arg}" + targetFileArgCallback "${targetFileArg}" -- "${@:2}" + # Argument 2/2 + # Argument githubUrlPatternArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountGithubUrlPatternArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument githubUrlPattern - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountGithubUrlPatternArg)) + githubUrlPatternArg="${options_parse_arg}" + githubUrlPatternArgCallback "${githubUrlPatternArg}" -- "${@:2}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountTargetFileArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'targetFile' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountGithubUrlPatternArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'githubUrlPattern' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + upgradeGithubReleaseCommandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "retrieve latest binary release from github and install it")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--version-arg ]" "[--current-version|-c ]" "[--exact-version|-e ]" "[--minimal-version|-m ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}targetFile${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< the\ binary\ downloaded\ will\ e\ written\ to\ this\ file\ path.\ Ensure\ the\ path\ is\ writable. + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}githubUrlPattern${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'the url pattern to use to download the binary, see examples below. \n @version@ is template variable that will be replaced by the latest \n version tag found on github.' + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}VERSION MANAGEMENT:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--version-arg ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'The argument that will be provided to the currently installed binary to check the version of the software. \n This parameter is needed if \E[1;30m--minimal-version\E[0m argument is used and is different than default value (\E[1;30m--version\E[0m).' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo ' Default value: --version' + printf " %b\n" "${__HELP_OPTION_COLOR}--current-version${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-c ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'Sometimes the command to retrieve the version is complicated. \n Some command needs you to parse json or other commands provides multiple sub command versions. In this case you can provide the version you currently have, see examples below.' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--exact-version${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-e ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'if provided and currently installed binary is not this \E[1;30mexactVersion\E[0m, \n This exact version of the binary will be installed.' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--minimal-version${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-m ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'if provided and currently installed binary is below this \E[1;30mminimalVersion\E[0m, \n a new version of the binary will be installed. \n If this argument is not provided, the latest binary is unconditionally downloaded from github.' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +OPTIONS EXCEPTIONS: + +--current-version|-c and --version-arg are mutually exclusive, +you cannot use both argument at the same time. + +--exact-version|-e and --minimal-version|-m are mutually exclusive, +you cannot use both argument at the same time. + +GITHUB TEMPLATE URLS EXAMPLES: -if [[ ! "${GITHUB_URL_PATTERN}" =~ ^https://github.com/ ]]; then - Log::fatal "Invalid githubUrlPattern ${GITHUB_URL_PATTERN} provided, it should begin with https://github.com/" -fi +Simple ones(Sometimes @version@ template variable has to be specified twice): +'https://github.com/hadolint/hadolint/releases/download/v@version@/hadolint-Linux-x86_64' +'https://github.com/koalaman/shellcheck/releases/download/v@version@/shellcheck-v@version@.linux.x86_64.tar.xz' +'https://github.com/sharkdp/fd/releases/download/v@version@/fd_@version@_amd64.deb' +'https://github.com/sharkdp/bat/releases/download/v@version@/bat_@version@_amd64.deb' +'https://github.com/kubernetes-sigs/kind/releases/download/v@version@/kind-linux-amd64' +'https://github.com/kubernetes/minikube/releases/download/v@version@/minikube-linux-amd64' +'https://github.com/plantuml/plantuml/releases/download/v@version@/plantuml-@version@.jar' +'https://github.com/Versent/saml2aws/releases/download/v@version@/saml2aws_@version@_linux_amd64.tar.gz' + +If you want to add a condition on architecture(linux, windows, x86, 64/32 bits): +\"https://github.com/docker/compose/releases/download/v@version@/docker-compose-\$(uname -s | tr '[:upper:]' '[:lower:]')-\$(uname -m)\" +\"https://github.com/docker/docker-credential-helpers/releases/download/v@version@/docker-credential-wincred-v@version@.windows-\$(dpkg --print-architecture).exe\" +\"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)\" + +COMMAND EXAMPLES: +Download docker-compose latest version +upgradeGithubRelease /usr/local/bin/docker-compose \"https://github.com/docker/compose/releases/download/v@version@/docker-compose-\$(uname -s | tr '[:upper:]' '[:lower:]')-\$(uname -m)\" + +Download oq specific version +upgradeGithubRelease /usr/local/bin/oq --exact-version 1.3.4 \"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)\" + +Download oq specific version correctly retrieving the oq version and not the jq one +upgradeGithubRelease /usr/local/bin/oq --exact-version 1.3.4 --version-arg '-V | grep oq:' \"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)\"""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Git/upgradeGithubRelease.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" +# default values +declare targetFileArg="" +declare githubUrlPatternArg="" +declare optionVersionArg="--version" +declare optionCurrentVersion="" +declare optionMinimalVersion="" +declare optionExactVersion="" + +upgradeGithubReleaseCommandCallback() { + if [[ -n "${optionExactVersion}" && -n "${optionMinimalVersion}" ]]; then + Log::fatal "--exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at the same time." + fi +} -if [[ "${TARGET_FILE:0:1}" != "/" ]]; then - TARGET_FILE="$(pwd)/${TARGET_FILE}" -fi -if ! Assert::validPath "${TARGET_FILE}"; then - Log::fatal "File ${TARGET_FILE} is not a valid path" -fi -if ! Assert::fileWritable "${TARGET_FILE}"; then - Log::fatal "File ${TARGET_FILE} is not writable" -fi +githubUrlPatternArgCallback() { + if [[ ! "${githubUrlPatternArg}" =~ ^https://github.com/ ]]; then + Log::fatal "Invalid githubUrlPattern ${githubUrlPatternArg} provided, it should begin with https://github.com/" + fi +} -# if minVersion arg provided, we have to compute current bin version -TRY_DOWNLOAD_NEW_VERSION=1 -if [[ -f "${TARGET_FILE}" ]]; then - if [[ -n "${MIN_VERSION}" ]]; then - if [[ -z "${CURRENT_VERSION}" && -n "${VERSION_ARG}" ]]; then - if Version::checkMinimal "${TARGET_FILE}" "${VERSION_ARG}" "${MIN_VERSION}"; then - TRY_DOWNLOAD_NEW_VERSION=0 +targetFileArgCallback() { + if [[ "${targetFileArg:0:1}" != "/" ]]; then + targetFileArg="$(pwd)/${targetFileArg}" + fi + if ! Assert::validPath "${targetFileArg}"; then + Log::fatal "File ${targetFileArg} is not a valid path" + fi + if ! Assert::fileWritable "${targetFileArg}"; then + Log::fatal "File ${targetFileArg} is not writable" + fi +} + +upgradeGithubReleaseCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +run() { + computeCurrentCommandVersion() { + if [[ -n "${optionCurrentVersion}" ]]; then + echo "${optionCurrentVersion}" + return 0 + fi + if [[ -n "${optionVersionArg}" ]]; then + # need eval here to correctly interpret --version-arg '-V | grep oq:' + eval "'${targetFileArg}' ${optionVersionArg} 2>&1" | Version::parse || return 3 + fi + } + + # if minVersion arg provided, we have to compute current bin version + local tryDownloadNewVersion=1 + if [[ -f "${targetFileArg}" ]]; then + local commandVersion + commandVersion="$(computeCurrentCommandVersion)" + + if [[ -n "${optionExactVersion}" ]]; then + if Version::compare "${commandVersion}" "${optionExactVersion}"; then + tryDownloadNewVersion=0 + Log::displayStatus "${targetFileArg} version is the exact required version ${optionExactVersion}" + else + Log::displayWarning "${targetFileArg} version ${commandVersion} is different than required version ${optionExactVersion}" fi - elif [[ -n "${CURRENT_VERSION}" ]]; then - versionCompare=0 - Version::compare "${CURRENT_VERSION}" "${MIN_VERSION}" || versionCompare=$? - # do not try to down version if current version is greater or equal to min version - if [[ "${versionCompare}" = "1" ]]; then - # current version > min version - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayWarning "${TARGET_FILE} version is ${CURRENT_VERSION} greater than ${MIN_VERSION}" - elif [[ "${versionCompare}" = "2" ]]; then - # current version < min version - Log::displayError "${TARGET_FILE} minimal version is ${MIN_VERSION}, your version is ${CURRENT_VERSION}" + else + if [[ -n "${optionMinimalVersion}" ]]; then + if ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionMinimalVersion}/g")"; then + Log::displayError "Minimal version ${optionMinimalVersion} doesn't exist on github" + return 5 + fi + local versionCompare=0 + Version::compare "${commandVersion}" "${optionMinimalVersion}" || versionCompare=$? + # do not try to down version if current version is greater or equal to min version + if [[ "${versionCompare}" = "1" ]]; then + local msg="${targetFileArg} version ${commandVersion} is greater than minimal version ${optionMinimalVersion}" + # current version > min version + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + versionCompare=0 + Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? + if [[ "${versionCompare}" = "2" ]]; then + # current version < remote version + Log::displayWarning "${msg} but new version ${optionExactVersion} is available on github" + else + Log::displayInfo "${msg}" + fi + return 0 + elif [[ "${versionCompare}" = "2" ]]; then + # current version < min version + Log::displayWarning "${targetFileArg} version ${commandVersion} is lesser than minimal version ${optionMinimalVersion}" + else + tryDownloadNewVersion=2 # need to check if a newer version exists + Log::displayStatus "${targetFileArg} version is the required minimal version ${optionMinimalVersion}" + fi else - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayStatus "${TARGET_FILE} version is the required minimal version ${MIN_VERSION}" + tryDownloadNewVersion="2" + fi + + # check if a newer version is available + if [[ "${tryDownloadNewVersion}" = "2" ]]; then + Log::displayInfo "compute last remote version" + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + versionCompare=0 + Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? + if [[ "${versionCompare}" = "1" ]]; then + # current version > remote version, shouldn't happen + tryDownloadNewVersion=0 + Log::displayWarning "${targetFileArg} version ${commandVersion} is greater than remote version ${optionExactVersion}" + elif [[ "${versionCompare}" = "2" ]]; then + # current version < remote version + tryDownloadNewVersion=1 + Log::displayWarning "${targetFileArg} version ${optionCurrentVersion} is lesser than remote version ${optionExactVersion}" + else + tryDownloadNewVersion=0 + Log::displayStatus "${targetFileArg} version is the same as remote version ${optionExactVersion}" + fi fi fi - elif [[ -n "${EXACT_VERSION}" ]]; then - if [[ -z "${CURRENT_VERSION}" && -n "${VERSION_ARG}" ]]; then - CURRENT_VERSION="$("${TARGET_FILE}" "${VERSION_ARG}" 2>&1 | Version::parse)" - fi - if Version::compare "${CURRENT_VERSION}" "${EXACT_VERSION}"; then - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayStatus "${TARGET_FILE} version is the exact required version ${EXACT_VERSION}" - else - Log::displayWarning "${TARGET_FILE} version ${CURRENT_VERSION} is different than required version ${EXACT_VERSION}" + fi + + if [[ "${tryDownloadNewVersion}" = "0" ]]; then + return 0 + fi + + # check if target file is writable + Assert::fileWritable "${targetFileArg}" + + if [[ -z "${optionExactVersion}" ]]; then + Log::displayInfo "compute last remote version" + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + if [[ -z "${optionExactVersion}" ]]; then + Log::displayError "${targetFileArg} latest version not found on github" + return 5 fi + elif ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")"; then + Log::displayError "${targetFileArg} version ${optionExactVersion} doesn't exist on github" + return 4 fi -fi -if [[ "${TRY_DOWNLOAD_NEW_VERSION}" = "0" ]]; then - exit 0 -fi + local githubUrl + githubUrl="$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")" + Log::displayInfo "Using url ${githubUrl}" + + newSoftware=$(Github::downloadReleaseVersion "${githubUrl}") + Github::defaultInstall "${newSoftware}" "${targetFileArg}" + Log::displayStatus "Version ${optionExactVersion} installed in ${targetFileArg}" +} -if [[ -z "${EXACT_VERSION}" ]]; then - EXACT_VERSION="$(Github::getLatestVersionFromUrl "${GITHUB_URL_PATTERN}")" +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -GITHUB_URL="$(echo "${GITHUB_URL_PATTERN}" | sed -E "s/@version@/${EXACT_VERSION}/g")" -Log::displayInfo "Using url ${GITHUB_URL}" -newSoftware=$(Github::downloadReleaseVersion "${GITHUB_URL}") -Github::defaultInstall "${newSoftware}" "${TARGET_FILE}" -Log::displayStatus "Version ${EXACT_VERSION} installed in ${TARGET_FILE}" +} + +facade_main_24dcc22b6cc8473babfc796abe45df9b "$@" diff --git a/bin/waitForIt b/bin/waitForIt index 148ced54..91c8f990 100755 --- a/bin/waitForIt +++ b/bin/waitForIt @@ -1,61 +1,63 @@ #!/usr/bin/env bash - -# Use this script to test if a given TCP host/port are available -# https://github.com/vishnubob/wait-for-it - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForIt.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForIt.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/waitForIt +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases + +# shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true +# store command arguments for later usage # shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -84,422 +86,635 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 } -trap interruptManagement INT -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + if [[ "${needEcho}" = "1" ]]; then + echo fi - return "${status}" } -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() - - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' +# @description check if command specified exists or return 1 +# with error and message if not +# +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist +# +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" + + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" fi - done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") + return 1 + } + return 0 } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using error color (red) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi Log::logError "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi +} + +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -518,259 +733,1043 @@ Log::rotate() { fi } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" +} + +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} - dir="$(dirname "${file}")" +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_d396e2bc1f6e43a7b79e8a25ad41ac25() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -Log::load +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir + +# @require Compiler::Facade::requireCommandBinDir +# shellcheck disable=SC2154 +# shellcheck disable=SC2317 -Env::pathPrepend "${COMMAND_BIN_DIR}" +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} -showHelp() { - cat < 0)); then - Log::displayInfo "${SCRIPT_NAME}: waiting ${TIMEOUT} seconds for ${HOST}:${PORT}" - else - Log::displayInfo "${SCRIPT_NAME}: waiting for ${HOST}:${PORT} without a timeout" - fi - local start_ts=${SECONDS} - while true; do - result=0 - if [[ "${ISBUSY}" = "1" ]]; then - (nc -z "${HOST}" "${PORT}") >/dev/null 2>&1 || result=$? || true - else - (echo >"/dev/tcp/${HOST}/${PORT}") >/dev/null 2>&1 || result=$? || true - fi - if [[ "${result}" = "0" ]]; then - local end_ts=${SECONDS} - Log::displayInfo "${SCRIPT_NAME}: ${HOST}:${PORT} is available after $((end_ts - start_ts)) seconds" - break - fi - sleep 1 - done - return "${result}" -} - -waitForWrapper() { - local result - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - local -a ARGS=(--child "--host=${HOST}" "--port=${PORT}" "--timeout=${TIMEOUT}") - if [[ "${QUIET}" = "1" ]]; then - ARGS+=(--quiet) - fi - timeout "${BUSYTIMEFLAG}" "${TIMEOUT}" "$0" "${ARGS[@]}" & - - local pid=$! - # shellcheck disable=2064 - trap "kill -INT -${pid}" INT - wait "${pid}" - result=$? - if [[ "${result}" != "0" ]]; then - Log::displayError "${SCRIPT_NAME}: timeout occurred after waiting ${TIMEOUT} seconds for ${HOST}:${PORT}" - fi - return "${result}" -} - -# process arguments -while [[ $# -gt 0 ]]; do - case "$1" in - *:*) - # shellcheck disable=2206 - hostPort=(${1//:/ }) - HOST=${hostPort[0]} - PORT=${hostPort[1]} - shift 1 || true - ;; - --child) - CHILD=1 - shift 1 || true +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} + +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" ;; - -q | --quiet) - QUIET=1 - shift 1 || true + ERROR) + echo "${__LEVEL_ERROR}" ;; - -s | --strict) - STRICT=1 - shift 1 || true + WARNING) + echo "${__LEVEL_WARNING}" ;; - -h) - HOST="$2" - if [[ "${HOST}" = "" ]]; then break; fi - shift 2 || true + INFO) + echo "${__LEVEL_INFO}" ;; - --host=*) - HOST="${1#*=}" - shift 1 || true + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" ;; - -p) - PORT="$2" - if [[ "${PORT}" = "" ]]; then break; fi - shift 2 || true + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} + +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" ;; - --port=*) - PORT="${1#*=}" - shift 1 || true + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" ;; - -t) - TIMEOUT="$2" - if [[ "${TIMEOUT}" = "" ]]; then break; fi - shift 2 || true + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" ;; - --timeout=*) - TIMEOUT="${1#*=}" - shift 1 || true - ;; - --) - shift || true - CLI=("$@") - break - ;; - --help) - showHelp - exit 0 + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" ;; *) - showHelp - Log::fatal "Unknown argument: $1" - ;; + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 esac -done +} -if [[ "${HOST}" = "" || "${PORT}" = "" ]]; then - showHelp - Log::fatal "Error: you need to provide a host and port to test." -fi +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -TIMEOUT=${TIMEOUT:-15} -STRICT=${STRICT:-0} -CHILD=${CHILD:-0} -QUIET=${QUIET:-0} - -# check to see if timeout is from busybox? -# check to see if timeout is from busybox? -TIMEOUT_PATH=$(dirname "$(command -v timeout)") -if [[ ${TIMEOUT_PATH} =~ "busybox" ]]; then - ISBUSY=1 - BUSYTIMEFLAG="-t" -else - ISBUSY=0 - BUSYTIMEFLAG="" -fi +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -if [[ ${CHILD} -gt 0 ]]; then - waitFor - RESULT=$? - exit "${RESULT}" -else - if [[ ${TIMEOUT} -gt 0 ]]; then - waitForWrapper - RESULT=$? +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} + +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" else - waitFor - RESULT=$? + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" fi -fi -if [[ -n "${CLI+x}" && "${CLI[*]}" != "" ]]; then - if [[ "${RESULT}" != "0" && "${STRICT}" = "1" ]]; then - Log::displayError "${SCRIPT_NAME}: strict mode, refusing to execute sub-process" - exit "${RESULT}" + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +waitForItCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + local -i options_parse_optionParsedCountOptionHostOrIp + ((options_parse_optionParsedCountOptionHostOrIp = 0)) || true + local -i options_parse_optionParsedCountOptionPort + ((options_parse_optionParsedCountOptionPort = 0)) || true + local -i options_parse_optionParsedCountOptionAlgo + ((options_parse_optionParsedCountOptionAlgo = 0)) || true + optionStrict="0" + local -i options_parse_optionParsedCountOptionStrict + ((options_parse_optionParsedCountOptionStrict = 0)) || true + optionTimeout=15 + local -i options_parse_optionParsedCountOptionTimeout + ((options_parse_optionParsedCountOptionTimeout = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountCommandArgs + ((options_parse_argParsedCountCommandArgs = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/19 + # Option optionHostOrIp --host|-i variableType String min 1 max 1 authorizedValues '' regexp '' + --host | -i) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionHostOrIp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHostOrIp)) + optionHostOrIp="$1" + ;; + # Option 2/19 + # Option optionPort --port|-p variableType String min 1 max 1 authorizedValues '' regexp '' + --port | -p) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionPort >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionPort)) + optionPort="$1" + optionPortCallback "${options_parse_arg}" "${optionPort}" + ;; + # Option 3/19 + # Option optionAlgo --algorithm|--algo variableType String min 0 max 1 authorizedValues '' regexp '' + --algorithm | --algo) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionAlgo >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionAlgo)) + optionAlgo="$1" + optionAlgoCallback "${options_parse_arg}" "${optionAlgo}" + ;; + # Option 4/19 + # Option optionStrict --exec-command-on-success-only|--strict|-s variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --exec-command-on-success-only | --strict | -s) + optionStrict="1" + if ((options_parse_optionParsedCountOptionStrict >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionStrict)) + ;; + # Option 5/19 + # Option optionTimeout --timeout|-t variableType String min 0 max 1 authorizedValues '' regexp '' + --timeout | -t) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTimeout >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTimeout)) + optionTimeout="$1" + optionTimeoutCallback "${options_parse_arg}" "${optionTimeout}" + ;; + # Option 6/19 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 7/19 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 8/19 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 9/19 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 10/19 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 11/19 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 12/19 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 13/19 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 14/19 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 15/19 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 16/19 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 17/19 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 18/19 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 19/19 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + unknownOption "${options_parse_arg}" + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/1 + # Argument commandArgs min 0 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountCommandArgs >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument commandArgs - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountCommandArgs)) + commandArgs="${options_parse_arg}" + else + unknownOption "${options_parse_arg}" + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_optionParsedCountOptionHostOrIp < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option '--host' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_optionParsedCountOptionPort < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option '--port' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + commandCallback + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for host:port to be available")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "--host|-i " "--port|-p " "[--algorithm|--algo ]" "[--exec-command-on-success-only|--strict|-s]" "[--timeout|-t ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " [${__HELP_OPTION_COLOR}commandArgs${__HELP_NORMAL} {single}]" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Execute\ command\ with\ args\ after\ the\ test\ finishes\ or\ exit\ with\ status\ code\ if\ no\ command\ provided. + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--host${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-i ${__HELP_NORMAL} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Host\ or\ IP\ under\ test. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--port${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-p ${__HELP_NORMAL} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< TCP\ port\ under\ test. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--algorithm${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}--algo ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< $'Algorithm to use Check algorithms list below. \n (default: automatic selection based on commands availability and timeout option value).' + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--exec-command-on-success-only${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}--strict${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-s${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Only\ execute\ sub-command\ if\ the\ test\ succeeds. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--timeout${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-t ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Timeout\ in\ seconds\,\ zero\ for\ no\ timeout. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo ' Default value: 15' + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +EXIT STATUS CODES: +\e[1;34m0: the host/port is available +\e[1;34m1: indicates host/port is not available or argument error +\e[1;34m2: timeout reached + +AVAILABLE ALGORITHMS: +\e[1;34mtimeoutV1WithNc: previous version of timeout command with --timeout option, base command nc +\e[1;34mtimeoutV2WithNc: newer version of timeout command using timeout as argument, base command nc +\e[1;34mwhileLoopWithNc: timeout command simulated using while loop, base command nc +\e[1;34mtimeoutV1WithTcp: previous version of timeout command with --timeout option +\e[1;34mtimeoutV2WithTcp: newer version of timeout command using timeout as argument +\e[1;34mwhileLoopWithTcp: timeout command simulated using while loop, base command tcp""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForIt.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} + +# shellcheck disable=SC2317 # if function is overridden +unknownOption() { + commandArgs+=("$1") +} + +optionPortCallback() { + if [[ ! "${optionPort}" =~ ^[0-9]+$ ]] || (( optionPort == 0 )); then + Log::fatal "${SCRIPT_NAME} - invalid port option - must be greater than to 0" + fi +} + +optionTimeoutCallback() { + if [[ ! "${optionTimeout}" =~ ^[0-9]+$ ]]; then + Log::fatal "${SCRIPT_NAME} - invalid timeout option - must be greater or equal to 0" + fi +} + +optionAlgoCallback() { + if ! Array::contains "${optionAlgo}" "${availableAlgos[@]}"; then + Log::fatal "${SCRIPT_NAME} - invalid algorithm '${optionAlgo}'" + fi +} + +commandCallback() { + if [[ "${optionHostOrIp}" = "" || "${optionPort}" = "" ]]; then + Log::fatal "${SCRIPT_NAME} - you need to provide a host and port to test." + fi +} + +# default values +declare -a commandArgs=() +declare copyrightBeginYear="2020" +declare optionTimeout="15" +declare optionAlgo="" +declare -a availableAlgos=(timeoutV1WithNc +timeoutV2WithNc +whileLoopWithNc +timeoutV1WithTcp +timeoutV2WithTcp +whileLoopWithTcp) + +# Use this script to test if a given TCP host/port are available +# https://github.com/vishnubob/wait-for-it +waitForItCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +run() { + usingTcp() { + # couldn't find another way to mock this part + if [[ -n "${WAIT_FOR_IT_MOCKED_TCP:-}" ]]; then + "${WAIT_FOR_IT_MOCKED_TCP}" "/dev/tcp/${optionHostOrIp}/${optionPort}" 2>&1 + else + echo >"/dev/tcp/${optionHostOrIp}/${optionPort}" 2>&1 + fi + } + + usingNc() { + nc -z "${optionHostOrIp}" "${optionPort}" -w 1 2>&1 + } + + whileLoop() { + local commandToUse="$1" + local reportTimeout="${2:-0}" + if ! Array::contains "${commandToUse}" "usingTcp" "usingNc"; then + Log::fatal "${SCRIPT_NAME} - can't call command ${commandToUse} in child mode" + fi + + local -i start_ts=${SECONDS} + while true; do + if "${commandToUse}"; then + Log::displayInfo "${SCRIPT_NAME} - ${optionHostOrIp}:${optionPort} is available after $((SECONDS - start_ts)) seconds" + break + fi + if (( optionTimeout!=0 && SECONDS - start_ts >= optionTimeout)); then + if [[ "${reportTimeout}" = "1" ]]; then + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${optionHostOrIp}:${optionPort}" + fi + return 2 + fi + sleep 1 + done + return 0 + } + + timeoutCommand() { + local timeoutVersion="$1" + local commandToUse="$2" + local result + local -i start_ts=${SECONDS} + + if ! Array::contains "${commandToUse}" "usingTcp" "usingNc"; then + Log::fatal "${SCRIPT_NAME} - can't call command ${commandToUse} in timeout mode" + fi + + # compute timeout command + local -a timeoutCmd=(timeout) + if [[ "${timeoutVersion}" = "v1" ]]; then + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + timeoutCmd+=("-t") + fi + timeoutCmd+=( + "${optionTimeout}" + "$0" + "${ORIGINAL_BASH_FRAMEWORK_ARGV[@]}" + ) + WAIT_FOR_IT_TIMEOUT_CHILD_ALGO="${commandToUse}" "${timeoutCmd[@]}" & + + local pid=$! + # shellcheck disable=2064 + trap "kill -INT -${pid}" INT + wait "${pid}" + result=$? + if [[ "${result}" != "0" ]]; then + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${optionHostOrIp}:${optionPort}" + fi + return "${result}" + } + + # -------------------------------------- + # ALGORITHMS + timeoutV1WithNc() { + timeoutCommand "v1" "usingNc" + } + timeoutV2WithNc() { + timeoutCommand "v2" "usingNc" + } + whileLoopWithNc() { + whileLoop "usingNc" "1" + } + timeoutV1WithTcp() { + timeoutCommand "v1" "usingTcp" + } + timeoutV2WithTcp() { + timeoutCommand "v2" "usingTcp" + } + whileLoopWithTcp() { + whileLoop "usingTcp" "1" + } + # -------------------------------------- + + algorithmAutomaticSelection() { + if Array::contains "${optionAlgo}" "${availableAlgos[@]}"; then + echo "${optionAlgo}" + return 0 + fi + + local command="WithTcp" + if Assert::commandExists nc &>/dev/null; then + # nc has the -w option allowing for timeout + command="WithNc" + fi + + if (( optionTimeout > 0 )); then + if Assert::commandExists timeout &>/dev/null; then + if timeout --help 2>&1 | grep -q -E -e '--timeout '; then + echo "timeoutV1${command}" + else + echo "timeoutV2${command}" + fi + fi + return 0 + fi + echo "whileLoop${command}" + } + + local result="0" + if [[ -n "${WAIT_FOR_IT_TIMEOUT_CHILD_ALGO:-}" ]]; then + # parent process is executing timeout with current child process + # call algo nc or tcp inside whileLoop + whileLoop "${WAIT_FOR_IT_TIMEOUT_CHILD_ALGO}" "0" || result=$? + else + local algo="${optionAlgo}" + if [[ -z "${algo}" ]]; then + algo=$(algorithmAutomaticSelection) + fi + Log::displayInfo "${SCRIPT_NAME} - using algorithm ${algo}" + if ((optionTimeout > 0)); then + Log::displayInfo "${SCRIPT_NAME} - waiting ${optionTimeout} seconds for ${optionHostOrIp}:${optionPort}" + else + Log::displayInfo "${SCRIPT_NAME} - waiting for ${optionHostOrIp}:${optionPort} without a timeout" + fi + "${algo}" || result=$? + # when timed out, call command if any + if [[ -n "${commandArgs+x}" && "${commandArgs[*]}" != "" ]]; then + if [[ "${result}" != "0" && "${optionStrict}" = "1" ]]; then + Log::displayError "${SCRIPT_NAME} - failed to connect - strict mode - command not executed" + exit "${result}" + fi + exec "${commandArgs[@]}" + fi fi - exec "${CLI[@]}" + + exit "${result}" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null else - exit "${RESULT}" + run fi + +} + +facade_main_d396e2bc1f6e43a7b79e8a25ad41ac25 "$@" diff --git a/bin/waitForMysql b/bin/waitForMysql index a9f76d21..2b99ddbc 100755 --- a/bin/waitForMysql +++ b/bin/waitForMysql @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForMysql.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForMysql.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/waitForMysql +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,444 +86,619 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 + shift || true + local -i maxLineLength=$1 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then break fi - shift || break + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" + fi + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" + else + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" + fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + if [[ "${needEcho}" = "1" ]]; then + echo fi - return "${status}" } -# remove elements from array -# Performance1 : version taken from https://stackoverflow.com/a/59030460 -# Performance2 : for multiple values to remove, prefer using Array::removeIf -Array::remove() { - local -n arrayRemoveArray=$1 - shift || true # $@ contains elements to remove - local -A valuesToRemoveKeys=() +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' +# @description check if command specified exists or return 1 +# with error and message if not +# +# @arg $1 commandName:String on which existence must be checked +# @arg $2 helpIfNotExists:String a help command to display if the command does not exist +# +# @exitcode 1 if the command specified does not exist +# @stderr diagnostic information + help if second argument is provided +Assert::commandExists() { + local commandName="$1" + local helpIfNotExists="$2" + + "${BASH_FRAMEWORK_COMMAND:-command}" -v "${commandName}" >/dev/null 2>/dev/null || { + Log::displayError "${commandName} is not installed, please install it" + if [[ -n "${helpIfNotExists}" ]]; then + Log::displayInfo "${helpIfNotExists}" fi - done - - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") + return 1 + } + return 0 } -# lazy initialization -declare -g BASH_FRAMEWORK_CACHED_ENV_FILE -declare -g BASH_FRAMEWORK_DEFAULT_ENV_FILE - -# load variables in order(from less specific to more specific) from : -# - ${FRAMEWORK_ROOT_DIR}/src/Env/testsData/.env file -# - ${FRAMEWORK_ROOT_DIR}/conf/.env file if exists -# - ~/.env file if exists -# - ~/.bash-tools/.env file if exists -# - BASH_FRAMEWORK_ENV_FILEPATH= -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" return 0 fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") - fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") - fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" - fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - fi - - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# shellcheck disable=SC2317 +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +Log::fatal() { + echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 1 +} -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" - - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac - done +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done } -# Display message using warning color (yellow) -# @param {String} $1 message -Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - Log::logWarning "$1" +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# log message to file -# @param {String} $1 message -Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# log message to file -# @param {String} $1 message -Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" } -# log message to file -# @param {String} $1 message -Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logError() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date + + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -537,118 +717,878 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 fi - fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 + fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} - dir="$(dirname "${file}")" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi } -# Public: check if argument is a valid linux path +# @description check if an element is contained in an array # -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } # FUNCTIONS -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +facade_main_665f5dabe75f418ea1c10f53fac6da5e() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -Log::load +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir + +# @require Compiler::Facade::requireCommandBinDir +# shellcheck disable=SC2154 +# shellcheck disable=SC2317 -Env::pathPrepend "${COMMAND_BIN_DIR}" +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" + fi + echo "Copyright (c) ${years} François Chastanet" +} + +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + waitForMysqlCommand help + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi +} -HELP="$( - cat < +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -${__HELP_TITLE}Author:${__HELP_NORMAL} -[François Chastanet](https://github.com/fchastanet) +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -${__HELP_TITLE}Source file:${__HELP_NORMAL} -https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForMysql.sh +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -${__HELP_TITLE}License:${__HELP_NORMAL} -MIT License +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -Copyright (c) 2022 François Chastanet -EOF -)" -Args::defaultHelp "${HELP}" "$@" +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -declare mysqlHost="$1" -declare mysqlPort="$2" -declare mysqlUser="$3" -declare mysqlPass="$4" +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} + +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -(echo >&2 "Waiting for mysql") -until (echo "select 1" | mysql -h"${mysqlHost}" -P"${mysqlPort}" -u"${mysqlUser}" -p"${mysqlPass}" &>/dev/null); do +# shellcheck disable=SC2317 # if function is overridden +optionQuietCallback() { + BASH_FRAMEWORK_QUIET_MODE=1 +} + +# shellcheck disable=SC2317 # if function is overridden +optionNoColorCallback() { + UI::theme "noColor" +} + +# shellcheck disable=SC2317 # if function is overridden +optionThemeCallback() { + UI::theme "$2" +} + +displayConfig() { + echo "Config" + UI::drawLine "-" + local var + while read -r var; do + printf '%-40s = %s\n' "${var}" "$(declare -p "${var}" | sed -E -e 's/^[^=]+=(.*)/\1/')" + done < <(typeset -p | awk 'match($3, "^(BASH_FRAMEWORK_[^=]+)=", m) { print m[1] }' | sort) + exit 0 +} + +optionBashFrameworkConfigCallback() { + if [[ ! -f "$2" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Bash framework config file '$2' does not exists" + fi +} + +commandOptionParseFinished() { + if [[ -z "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + BASH_FRAMEWORK_ENV_FILES=() + fi + BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") + export BASH_FRAMEWORK_ENV_FILES + Env::requireLoad + Log::requireLoad + + # load .framework-config + if [[ -n "${optionBashFrameworkConfig}" && -f "${optionBashFrameworkConfig}" ]]; then + BASH_FRAMEWORK_CONFIG_FILE="${optionBashFrameworkConfig}" + # shellcheck source=/.framework-config + source "${optionBashFrameworkConfig}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading specific .framework-config file: ${optionBashFrameworkConfig}" + else + # shellcheck disable=SC2034 + BASH_FRAMEWORK_CONFIG_FILE="" + # shellcheck source=/.framework-config + Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE "${FRAMEWORK_ROOT_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} - error while loading .framework-config file" + fi + + if [[ "${optionConfig}" = "1" ]]; then + displayConfig + fi +} + +waitForMysqlCommand() { + local options_parse_cmd="$1" + shift || true + + if [[ "${options_parse_cmd}" = "parse" ]]; then + optionTimeout=15 + local -i options_parse_optionParsedCountOptionTimeout + ((options_parse_optionParsedCountOptionTimeout = 0)) || true + local -i options_parse_optionParsedCountOptionBashFrameworkConfig + ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true + optionConfig="0" + local -i options_parse_optionParsedCountOptionConfig + ((options_parse_optionParsedCountOptionConfig = 0)) || true + optionInfoVerbose="0" + local -i options_parse_optionParsedCountOptionInfoVerbose + ((options_parse_optionParsedCountOptionInfoVerbose = 0)) || true + optionDebugVerbose="0" + local -i options_parse_optionParsedCountOptionDebugVerbose + ((options_parse_optionParsedCountOptionDebugVerbose = 0)) || true + optionTraceVerbose="0" + local -i options_parse_optionParsedCountOptionTraceVerbose + ((options_parse_optionParsedCountOptionTraceVerbose = 0)) || true + optionNoColor="0" + local -i options_parse_optionParsedCountOptionNoColor + ((options_parse_optionParsedCountOptionNoColor = 0)) || true + local -i options_parse_optionParsedCountOptionTheme + ((options_parse_optionParsedCountOptionTheme = 0)) || true + optionHelp="0" + local -i options_parse_optionParsedCountOptionHelp + ((options_parse_optionParsedCountOptionHelp = 0)) || true + optionVersion="0" + local -i options_parse_optionParsedCountOptionVersion + ((options_parse_optionParsedCountOptionVersion = 0)) || true + optionQuiet="0" + local -i options_parse_optionParsedCountOptionQuiet + ((options_parse_optionParsedCountOptionQuiet = 0)) || true + local -i options_parse_optionParsedCountOptionLogLevel + ((options_parse_optionParsedCountOptionLogLevel = 0)) || true + local -i options_parse_optionParsedCountOptionLogFile + ((options_parse_optionParsedCountOptionLogFile = 0)) || true + local -i options_parse_optionParsedCountOptionDisplayLevel + ((options_parse_optionParsedCountOptionDisplayLevel = 0)) || true + local -i options_parse_argParsedCountMysqlHostArg + ((options_parse_argParsedCountMysqlHostArg = 0)) || true + local -i options_parse_argParsedCountMysqlPortArg + ((options_parse_argParsedCountMysqlPortArg = 0)) || true + local -i options_parse_argParsedCountMysqlUserArg + ((options_parse_argParsedCountMysqlUserArg = 0)) || true + local -i options_parse_argParsedCountMysqlPasswordArg + ((options_parse_argParsedCountMysqlPasswordArg = 0)) || true + local -i options_parse_parsedArgIndex=0 + while (($# > 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/15 + # Option optionTimeout --timeout|-t variableType String min 0 max 1 authorizedValues '' regexp '' + --timeout | -t) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionTimeout >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTimeout)) + optionTimeout="$1" + optionTimeoutCallback "${options_parse_arg}" "${optionTimeout}" + ;; + # Option 2/15 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 3/15 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 4/15 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 5/15 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 6/15 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 7/15 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 8/15 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 9/15 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 10/15 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 11/15 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 12/15 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 13/15 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 14/15 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 15/15 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if ((0)); then + # Technical if - never reached + : + # Argument 1/4 + # Argument mysqlHostArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1)); then + if ((options_parse_argParsedCountMysqlHostArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument mysqlHost - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountMysqlHostArg)) + mysqlHostArg="${options_parse_arg}" + # Argument 2/4 + # Argument mysqlPortArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2)); then + if ((options_parse_argParsedCountMysqlPortArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument mysqlPort - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountMysqlPortArg)) + mysqlPortArg="${options_parse_arg}" + mysqlPortArgCallback "${mysqlPortArg}" -- "${@:2}" + # Argument 3/4 + # Argument mysqlUserArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 2 && options_parse_parsedArgIndex < 3)); then + if ((options_parse_argParsedCountMysqlUserArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument mysqlUserArg - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountMysqlUserArg)) + mysqlUserArg="${options_parse_arg}" + # Argument 4/4 + # Argument mysqlPasswordArg min 1 max 1 authorizedValues '' regexp '' + elif ((options_parse_parsedArgIndex >= 3 && options_parse_parsedArgIndex < 4)); then + if ((options_parse_argParsedCountMysqlPasswordArg >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument mysqlPasswordArg - Maximum number of argument occurrences reached(1)" + return 1 + fi + ((++options_parse_argParsedCountMysqlPasswordArg)) + mysqlPasswordArg="${options_parse_arg}" + else + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided: $*" + return 1 + fi + fi + ((++options_parse_parsedArgIndex)) + ;; + esac + shift || true + done + if ((options_parse_argParsedCountMysqlHostArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'mysqlHost' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountMysqlPortArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'mysqlPort' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountMysqlUserArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'mysqlUserArg' should be provided at least 1 time(s)" + return 1 + fi + if ((options_parse_argParsedCountMysqlPasswordArg < 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Argument 'mysqlPasswordArg' should be provided at least 1 time(s)" + return 1 + fi + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for mysql to be ready")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--timeout|-t ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" + echo -e " ${__HELP_OPTION_COLOR}mysqlHost${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Mysql\ host\ name + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}mysqlPort${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Mysql\ port + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}mysqlUserArg${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Mysql\ user\ name + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo -e " ${__HELP_OPTION_COLOR}mysqlPasswordArg${__HELP_NORMAL} {single} (mandatory)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Mysql\ password + echo -n " " + Array::wrap " " 76 4 "${helpArray[@]}" + echo + echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--timeout${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-t ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Timeout\ in\ seconds\,\ zero\ for\ no\ timeout. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo ' Default value: 15' + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo -e """ +EXIT STATUS CODES: +\e[1;34m0: mysql is available +\e[1;34m1: indicates mysql is not available or argument error +\e[1;34m2: timeout reached""" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '2.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/Utils/waitForMysql.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} + +mysqlPortArgCallback() { + if [[ ! "${mysqlPortArg}" =~ ^[0-9]+$ ]] || (( mysqlPortArg == 0 )); then + Log::fatal "${SCRIPT_NAME} - invalid port option - must be greater than to 0" + fi +} + +optionTimeoutCallback() { + if [[ ! "${optionTimeout}" =~ ^[0-9]+$ ]]; then + Log::fatal "${SCRIPT_NAME} - invalid timeout option - must be greater or equal to 0" + fi +} + +# default values +declare copyrightBeginYear="2020" +declare optionTimeout="15" + +waitForMysqlCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +run() { + Assert::commandExists "mysql" + Log::displayInfo "Waiting for mysql" + local -i start_ts=${SECONDS} (printf >&2 ".") - sleep 1 -done + until (echo "select 1" | mysql \ + -h"${mysqlHostArg}" \ + -P"${mysqlPortArg}" \ + -u"${mysqlUserArg}" \ + -p"${mysqlPasswordArg}" &>/dev/null); do + (printf >&2 ".") + if (( optionTimeout!=0 && SECONDS - start_ts >= optionTimeout)); then + (echo >&2 "") + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${mysqlHostArg}:${mysqlPortArg}" + return 2 + fi + sleep 1 + done + + (echo >&2 "") + Log::displayInfo "mysql ready" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi + +} -(echo >&2 -e "\nmysql ready") +facade_main_665f5dabe75f418ea1c10f53fac6da5e "$@" diff --git a/build.sh b/build.sh index d3312d54..58ae3ff9 100755 --- a/build.sh +++ b/build.sh @@ -21,7 +21,7 @@ declare -a params=( --template-dir "${BASH_TOOLS_SRC_DIR}" ) if [[ "${ARGS_VERBOSE}" = "1" ]]; then - params+=("--verbose") + params+=("-vvv") fi ( diff --git a/conf/cliProfiles/mysql.remote.sh b/conf/cliProfiles/mysql.remote.sh index aa25d9fb..47132c8d 100755 --- a/conf/cliProfiles/mysql.remote.sh +++ b/conf/cliProfiles/mysql.remote.sh @@ -10,9 +10,9 @@ finalUserArg="${userArg:-mysql}" # shellcheck disable=SC2034 # shellcheck disable=SC2154 -finalCommandArg=("${commandArg}") +finalCommandArg=("${commandArg[@]}") -if [[ -z "${commandArg}" ]]; then +if [[ -z "${commandArg[*]}" ]]; then loadDsn "default.remote" finalCommandArg=(//bin/bash -c "mysql -h${HOSTNAME} -u${USER} -p${PASSWORD} -P${PORT}") fi diff --git a/conf/cliProfiles/mysql.sh b/conf/cliProfiles/mysql.sh index a06258a1..d71c1d8b 100755 --- a/conf/cliProfiles/mysql.sh +++ b/conf/cliProfiles/mysql.sh @@ -10,7 +10,7 @@ finalUserArg="${userArg:-mysql}" # shellcheck disable=SC2034 # shellcheck disable=SC2154 -finalCommandArg="${commandArg}" +finalCommandArg=("${commandArg[@]}") if [[ -z "${commandArg}" ]]; then loadDsn "default.local" diff --git a/conf/cliProfiles/node.sh b/conf/cliProfiles/node.sh index 714e0782..ce627acc 100755 --- a/conf/cliProfiles/node.sh +++ b/conf/cliProfiles/node.sh @@ -11,4 +11,4 @@ finalUserArg="${userArg:-node}" # we are using // to keep compatibility with "windows git bash" # shellcheck disable=SC2034 # shellcheck disable=SC2154 -finalCommandArg=("${commandArg:-//bin/bash}") +finalCommandArg=("${commandArg[@]:-//bin/bash}") diff --git a/conf/cliProfiles/redis.sh b/conf/cliProfiles/redis.sh index 4346cf0d..90c6481f 100755 --- a/conf/cliProfiles/redis.sh +++ b/conf/cliProfiles/redis.sh @@ -10,4 +10,4 @@ finalUserArg="${userArg:-redis}" # shellcheck disable=SC2034 # shellcheck disable=SC2154 -finalCommandArg=("${commandArg:-redis-cli}") +finalCommandArg=("${commandArg[@]:-redis-cli}") diff --git a/conf/cliProfiles/web.sh b/conf/cliProfiles/web.sh index bd9f1262..0d2ed202 100755 --- a/conf/cliProfiles/web.sh +++ b/conf/cliProfiles/web.sh @@ -11,4 +11,4 @@ finalUserArg="${userArg:-www-data}" # we are using // to keep compatibility with "windows git bash" # shellcheck disable=SC2034 # shellcheck disable=SC2154 -finalCommandArg=("${commandArg:-//bin/bash}") +finalCommandArg=("${commandArg[@]:-//bin/bash}") diff --git a/install b/install index 52180b48..85a739f4 100755 --- a/install +++ b/install @@ -1,58 +1,63 @@ #!/usr/bin/env bash - -##################################### -# GENERATED FILE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/install.sh +############################################################################### +# GENERATED FACADE FROM https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/install.sh # DO NOT EDIT IT -##################################### +# @generated +############################################################################### +# shellcheck disable=SC2288,SC2034 +# BIN_FILE=${FRAMEWORK_ROOT_DIR}/install +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=. +# FACADE # ensure that no user aliases could interfere with # commands used in this script unalias -a || true +shopt -u expand_aliases # shellcheck disable=SC2034 +((failures = 0)) || true + +# Bash will remember & return the highest exit code in a chain of pipes. +# This way you can catch the error inside pipes, e.g. mysqldump | gzip +set -o pipefail +set -o errexit + +# Command Substitution can inherit errexit option since bash v4.4 +shopt -s inherit_errexit || true + +# a log is generated when a command fails +set -o errtrace + +# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard +shopt -s nullglob + +# ensure regexp are interpreted without accentuated characters +export LC_ALL=POSIX + +export TERM=xterm-256color + +# avoid interactive install +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# store command arguments for later usage +# shellcheck disable=SC2034 +declare -a BASH_FRAMEWORK_ARGV=("$@") +# shellcheck disable=SC2034 +declare -a ORIGINAL_BASH_FRAMEWORK_ARGV=("$@") + +# @see https://unix.stackexchange.com/a/386856 +interruptManagement() { + # restore SIGINT handler + trap - INT + # ensure that Ctrl-C is trapped by this script and not by sub process + # report to the parent that we have indeed been interrupted + kill -s INT "$$" +} +trap interruptManagement INT SCRIPT_NAME=${0##*/} REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" -# shellcheck disable=SC2034 CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" -# shellcheck disable=SC2034 -COMMAND_BIN_DIR="${CURRENT_DIR}" - -if [[ -t 1 || -t 2 ]]; then - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='\e[31m' # Red - export __INFO_COLOR='\e[44m' # white on lightBlue - export __SUCCESS_COLOR='\e[32m' # Green - export __WARNING_COLOR='\e[33m' # Yellow - export __TEST_COLOR='\e[100m' # Light magenta - export __TEST_ERROR_COLOR='\e[41m' # white on red - export __SKIPPED_COLOR='\e[33m' # Yellow - export __HELP_COLOR='\e[7;49;33m' # Black on Gold - export __DEBUG_COLOR='\e[37m' # Grey - # Internal: reset color - export __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - export __HELP_NORMAL="$(echo -e "\033[0m")" -else - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - export __ERROR_COLOR='' - export __INFO_COLOR='' - export __SUCCESS_COLOR='' - export __WARNING_COLOR='' - export __SKIPPED_COLOR='' - export __HELP_COLOR='' - export __TEST_COLOR='' - export __TEST_ERROR_COLOR='' - export __DEBUG_COLOR='' - # Internal: reset color - export __RESET_COLOR='' - export __HELP_EXAMPLE='' - export __HELP_TITLE='' - export __HELP_NORMAL='' -fi ################################################ # Temp dir management @@ -81,458 +86,631 @@ cleanOnExit() { } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @see https://unix.stackexchange.com/a/386856 -interruptManagement() { - # restore SIGINT handler - trap - INT - # ensure that Ctrl-C is trapped by this script and not by sub process - # report to the parent that we have indeed been interrupted - kill -s INT "$$" -} -trap interruptManagement INT - -# shellcheck disable=SC2034 -((failures = 0)) || true - -shopt -s expand_aliases - -# Bash will remember & return the highest exit code in a chain of pipes. -# This way you can catch the error inside pipes, e.g. mysqldump | gzip -set -o pipefail -set -o errexit - -# Command Substitution can inherit errexit option since bash v4.4 -(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit - -# a log is generated when a command fails -set -o errtrace - -# use nullglob so that (file*.php) will return an empty array if no file matches the wildcard -shopt -s nullglob - -# ensure regexp are interpreted without accentuated characters -export LC_ALL=POSIX - -export TERM=xterm-256color - -#avoid interactive install -export DEBIAN_FRONTEND=noninteractive -export DEBCONF_NONINTERACTIVE_SEEN=true - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi - -# shellcheck disable=SC2034 - -COMMAND_BIN_DIR="${CURRENT_DIR}" -if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # shellcheck disable=SC2034 - FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" - # shellcheck disable=SC2034 - FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" - # shellcheck disable=SC2034 - FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" -fi - -Args::defaultHelp() { - local helpArg=$1 +# @description concat each element of an array with a separator +# but wrapping text when line length is more than provided argument +# The algorithm will try not to cut the array element if can +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap() { + local glue="${1-}" + local -i glueLength=0 shift || true - if ! Args::defaultHelpNoExit "${helpArg}" "$@"; then - exit 0 - fi -} - -# change display level to level argument -# if --verbose|-v option is parsed in arguments -Args::parseVerbose() { - local verboseDisplayLevel="$1" - declare -gx ARGS_VERBOSE=0 + local -i maxLineLength=$1 shift || true - local status=1 - while true; do - if [[ "$1" = "--verbose" || "$1" = "-v" ]]; then - status=0 - ARGS_VERBOSE=1 - break - fi - shift || break - done - if [[ "${status}" = "0" ]]; then - export BASH_FRAMEWORK_DISPLAY_LEVEL=${verboseDisplayLevel} + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" -Env::load() { - if [[ "${BASH_FRAMEWORK_INITIALIZED:-0}" = "1" ]]; then - return 0 - fi - BASH_FRAMEWORK_CACHED_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "env_vars.XXXXXXX")" - BASH_FRAMEWORK_DEFAULT_ENV_FILE="$(mktemp -p "${TMPDIR:-/tmp}" -t "default_env_file.XXXXXXX")" - # shellcheck source=src/Env/testsData/.env - ( - echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" - echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" - echo "BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log}" - echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" - ) >"${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" - - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" - - # list .env files that need to be loaded - local -a files=() - if [[ -f "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" ]]; then - files+=("${BASH_FRAMEWORK_DEFAULT_ENV_FILE}") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/conf/.env" && -r "${FRAMEWORK_ROOT_DIR}/conf/.env" ]]; then - files+=("${FRAMEWORK_ROOT_DIR}/conf/.env") - fi - if [[ -f "${HOME}/.env" && -r "${HOME}/.env" ]]; then - files+=("${HOME}/.env") + set -- "${allArgs[@]}" + + local -i currentLineLength=0 + local needEcho="0" + local arg="$1" + local argNoAnsi + local -i argNoAnsiLength=0 + + while (($# > 0)); do + argNoAnsi="$(echo "${arg}" | Filters::removeAnsiCodes)" + ((argNoAnsiLength = ${#argNoAnsi})) || true + if (($# < 1 && argNoAnsiLength == 0)); then + break fi - local file - for file in "$@"; do - if [[ -f "${file}" && -r "${file}" ]]; then - files+=("${file}") + if [[ "${arg}" = $'\n' ]]; then + if [[ "${needEcho}" = "1" ]]; then + needEcho="0" fi - done - # import custom .env file - if [[ -n "${BASH_FRAMEWORK_ENV_FILEPATH+xxx}" ]]; then - # load BASH_FRAMEWORK_ENV_FILEPATH - if [[ -f "${BASH_FRAMEWORK_ENV_FILEPATH}" && -r "${BASH_FRAMEWORK_ENV_FILEPATH}" ]]; then - files+=("${BASH_FRAMEWORK_ENV_FILEPATH}") - else - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + echo "" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + shift || return 0 + arg="$1" + elif ((argNoAnsiLength < maxLineLength - currentLineLength - glueLength)); then + # arg can be stored as a whole on current line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) fi - fi - - # add all files added as parameters - files+=("$@") - - # source each file in order - local file - for file in "${files[@]}"; do - # shellcheck source=src/Env/testsData/.env - source "${file}" || { - Log::displayWarning "Cannot load '${file}'" - } - done - - # copy only the variables to the tmp file - local varName overrideVarName - while IFS=$'\n' read -r varName; do - overrideVarName="OVERRIDE_${varName}" - if [[ -z ${!overrideVarName+xxx} ]]; then - echo "${varName}='${!varName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + fi + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + if ((currentLineLength == 0 && firstLine == 0)); then + echo -n "${indentStr}" + ((currentLineLength += indentNextLine)) + fi + # arg can be stored on a whole line + if ((glueLength > 0)); then + echo -e -n "${glue}" + ((currentLineLength += glueLength)) + fi + local -i length + ((length = maxLineLength - currentLineLength)) || true + echo -e "${arg:0:${length}}" + ((currentLineLength = 0)) || true + ((glueLength = 0)) || true + arg="${arg:${length}}" + needEcho="0" else - # variable is overridden - echo "${varName}='${!overrideVarName}'" >>"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + # arg cannot be stored on a whole line, so we add it on next line as a whole + echo + echo -e -n "${indentStr}${arg}" + ((glueLength = ${#glue})) || true + ((currentLineLength = argNoAnsiLength)) + arg="" # allows to go to next arg + needEcho="1" fi + if [[ -z "${arg}" ]]; then + shift || return 0 + arg="$1" + fi + fi + ((firstLine = 0)) || true + done + if [[ "${needEcho}" = "1" ]]; then + echo + fi +} - # using awk deduce all variables that need to be copied in tmp file - # from less specific file to the most - done < <(awk -F= '!a[$1]++' "${files[@]}" | grep -v '^$\|^\s*\#' | cut -d= -f1) - ) || exit 1 - - # ensure all sourced variables will be exported - set -o allexport - - # Finally load the temp file to make the variables available in current script - # shellcheck source=src/Env/testsData/.env - source "${BASH_FRAMEWORK_CACHED_ENV_FILE}" +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." + +# @description ensure env files are loaded +# @noargs +# @exitcode 1 if getOrderedConfFiles fails +# @exitcode 2 if one of env files fails to load +# @stderr diagnostics information is displayed +Env::requireLoad() { + local configFilesStr + configFilesStr="$(Env::getOrderedConfFiles)" || return 1 + + local -a configFiles + readarray -t configFiles <<<"${configFilesStr}" + + # if empty string, there will be one element + if ((${#configFiles[@]} == 0)) || [[ -z "${configFilesStr}" ]]; then + # should not happen, as there is always default file + Log::displaySkipped "no env file to load" + return 0 + fi - set +o allexport - BASH_FRAMEWORK_INITIALIZED=1 + Env::mergeConfFiles "${configFiles[@]}" || { + Log::displayError "while loading config files: ${configFiles[*]}" + return 2 + } } -Env::pathPrepend() { - local arg - for arg in "$@"; do - if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then - PATH="$(realpath "${arg}"):${PATH}" - fi - done +# @description load .framework-config +# @arg $1 loadedConfigFile:&String (passed by reference) the finally loaded configuration file path +# @arg $@ srcDirs:String[] the src directories in which .framework-config file will be searched +# @stdout the config file path loaded if any +# @exitcode 0 if .framework-config file has been found in srcDirs provided +# @exitcode 1 if .framework-config file not found +# @see Conf::loadNearestFile +Framework::loadConfig() { + # shellcheck disable=SC2034 + local -n loadConfig_loadedConfigFile=$1 + shift || true + Conf::loadNearestFile ".framework-config" loadConfig_loadedConfigFile "$@" } -# Public: log level off +# @description Log namespace provides 2 kind of functions +# - Log::display* allows to display given message with +# given display level +# - Log::log* allows to log given message with +# given log level +# Log::display* functions automatically log the message too +# @see Env::requireLoad to load the display and log level from .env file + +# @description log level off export __LEVEL_OFF=0 -# Public: log level error +# @description log level error export __LEVEL_ERROR=1 -# Public: log level warning +# @description log level warning export __LEVEL_WARNING=2 -# Public: log level info +# @description log level info export __LEVEL_INFO=3 -# Public: log level success +# @description log level success export __LEVEL_SUCCESS=3 -# Public: log level debug +# @description log level debug export __LEVEL_DEBUG=4 -export __LEVEL_OFF -export __LEVEL_ERROR -export __LEVEL_WARNING -export __LEVEL_INFO -export __LEVEL_SUCCESS -export __LEVEL_DEBUG - -# Display message using debug color (grey) -# @param {String} $1 message +# @description verbose level off +export __VERBOSE_LEVEL_OFF=0 +# @description verbose level info +export __VERBOSE_LEVEL_INFO=1 +# @description verbose level info +export __VERBOSE_LEVEL_DEBUG=2 +# @description verbose level info +export __VERBOSE_LEVEL_TRACE=3 + +# @description Display message using debug color (grey) +# @arg $1 message:String the message to display Log::displayDebug() { - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + fi Log::logDebug "$1" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message +# @description Display message using error color (red) +# @arg $1 message:String the message to display +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" +} + +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display Log::displayInfo() { local type="${2:-INFO}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + fi Log::logInfo "$1" "${type}" } -# Display message using warning color (yellow) -# @param {String} $1 message +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 + fi + Log::logSkipped "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display Log::displayWarning() { - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + fi Log::logWarning "$1" } -# Display message using error color (red) and exit immediately with error status 1 -# @param {String} $1 message +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display Log::fatal() { echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 1 } -# shellcheck disable=SC2317 - -Log::load() { - # disable display methods following display level - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_DEBUG)); then - Log::displayDebug() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_INFO)); then - Log::displayHelp() { :; } - Log::displayInfo() { :; } - Log::displaySkipped() { :; } - Log::displaySuccess() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_WARNING)); then - Log::displayWarning() { :; } - Log::displayStatus() { :; } - fi - if ((BASH_FRAMEWORK_DISPLAY_LEVEL < __LEVEL_ERROR)); then - Log::displayError() { :; } - fi - # disable log methods following log level - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_DEBUG)); then - Log::logDebug() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_INFO)); then - Log::logHelp() { :; } - Log::logInfo() { :; } - Log::logSkipped() { :; } - Log::logSuccess() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } - fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } +# @description activate or not Log::display* and Log::log* functions +# based on BASH_FRAMEWORK_DISPLAY_LEVEL and BASH_FRAMEWORK_LOG_LEVEL +# environment variables loaded by Env::requireLoad +# try to create log file and rotate it if necessary +# @noargs +# @set BASH_FRAMEWORK_LOG_LEVEL int to OFF level if BASH_FRAMEWORK_LOG_FILE is empty or not writable +# @env BASH_FRAMEWORK_DISPLAY_LEVEL int +# @env BASH_FRAMEWORK_LOG_LEVEL int +# @env BASH_FRAMEWORK_LOG_FILE String +# @env BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION int do log rotation if > 0 +# @exitcode 0 always successful +# @stderr diagnostics information about log file is displayed +# @require Env::requireLoad +# @require UI::requireTheme +Log::requireLoad() { + if [[ -z "${BASH_FRAMEWORK_LOG_FILE:-}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + export BASH_FRAMEWORK_LOG_LEVEL fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} - export BASH_FRAMEWORK_LOG_LEVEL - elif [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - if ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log dir cannot be created $(dirname "${BASH_FRAMEWORK_LOG_FILE}")" - fi - if ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null; then - BASH_FRAMEWORK_LOG_LEVEL=__LEVEL_OFF - Log::displayWarning "Log file '${BASH_FRAMEWORK_LOG_FILE}' cannot be created" + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if + ! mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" 2>/dev/null || + ! touch --no-create "${BASH_FRAMEWORK_LOG_FILE}" 2>/dev/null + then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi + elif [[ ! -w "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - Log::displayInfo "Logging to file ${BASH_FRAMEWORK_LOG_FILE}" + + fi + + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + # will always be created even if not in info level + Log::logMessage "INFO" "Logging to file ${BASH_FRAMEWORK_LOG_FILE} - Log level ${BASH_FRAMEWORK_LOG_LEVEL}" if ((BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION > 0)); then Log::rotate "${BASH_FRAMEWORK_LOG_FILE}" "${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION}" fi fi } -Args::defaultHelpNoExit() { - local helpArg=$1 - shift || true - # shellcheck disable=SC2034 - local args - args="$(getopt -l help -o h -- "$@" 2>/dev/null)" || true - eval set -- "${args}" +# @description draw a line with the character passed in parameter repeated depending on terminal width +# @arg $1 character:String character to use as separator (default value #) +UI::drawLine() { + local character="${1:-#}" + printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +} - while true; do - case $1 in - -h | --help) - if [[ "$(type -t "${helpArg}")" = "function" ]]; then - "${helpArg}" "$@" - else - Args::showHelp "${helpArg}" - fi - return 1 - ;; - --) - break - ;; - *) - # ignore - ;; - esac - done +# @description load colors theme constants +# @warning if tty not opened, noColor theme will be chosen +# @arg $1 theme:String the theme to use (default, noColor) +# @arg $@ args:String[] +# @set __ERROR_COLOR String indicate error status +# @set __INFO_COLOR String indicate info status +# @set __SUCCESS_COLOR String indicate success status +# @set __WARNING_COLOR String indicate warning status +# @set __SKIPPED_COLOR String indicate skipped status +# @set __DEBUG_COLOR String indicate debug status +# @set __HELP_COLOR String indicate help status +# @set __TEST_COLOR String not used +# @set __TEST_ERROR_COLOR String not used +# @set __HELP_TITLE_COLOR String used to display help title in help strings +# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# +# @set __RESET_COLOR String reset default color +# +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" + fi + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + export BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='\e[31m' # Red + export __INFO_COLOR='\e[44m' # white on lightBlue + export __SUCCESS_COLOR='\e[32m' # Green + export __WARNING_COLOR='\e[33m' # Yellow + export __SKIPPED_COLOR='\e[33m' # Yellow + export __DEBUG_COLOR='\e[37m' # Grey + export __HELP_COLOR='\e[7;49;33m' # Black on Gold + export __TEST_COLOR='\e[100m' # Light magenta + export __TEST_ERROR_COLOR='\e[41m' # white on red + export __HELP_TITLE_COLOR="\e[1;37m" # Bold + export __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + export __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + export __HELP_EXAMPLE="$(echo -e "\e[1;30m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + export __HELP_NORMAL="$(echo -e "\033[0m")" + else + export BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + export __ERROR_COLOR='' + export __INFO_COLOR='' + export __SUCCESS_COLOR='' + export __WARNING_COLOR='' + export __SKIPPED_COLOR='' + export __DEBUG_COLOR='' + export __HELP_COLOR='' + export __TEST_COLOR='' + export __TEST_ERROR_COLOR='' + export __HELP_TITLE_COLOR='' + export __HELP_OPTION_COLOR='' + # Internal: reset color + export __RESET_COLOR='' + export __HELP_EXAMPLE='' + export __HELP_TITLE='' + export __HELP_NORMAL='' + fi } -# Display message using error color (red) -# @param {String} $1 message -Log::displayError() { - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 - Log::logError "$1" +# @description ensure COMMAND_BIN_DIR env var is set +# and PATH correctly prepared +# @noargs +# @set COMMAND_BIN_DIR string the directory where to find this command +# @set PATH string add directory where to find this command binary +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# Display message using info color (bg light blue/fg white) -# @param {String} $1 message -Log::displayHelp() { - local type="${2:-HELP}" - echo -e "${__HELP_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logHelp "$1" "${type}" +# @description ensure running user is not root +# @exitcode 1 if current user is root +# @stderr diagnostics information is displayed +Linux::requireExecutedAsUser() { + if [[ "$(id -u)" = "0" ]]; then + Log::fatal "this script should be executed as normal user" + fi } -# Display message using skip color (yellow) -# @param {String} $1 message -Log::displaySkipped() { - echo -e "${__SKIPPED_COLOR}SKIPPED - ${1}${__RESET_COLOR}" >&2 - Log::logSkipped "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @environment NON_INTERACTIVE if 1 consider as not interactive even if environement is interactive +# @environment INTERACTIVE if 1 consider as interactive even if environement is not interactive +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + [[ -t 1 || -t 2 ]] +} + +# @description Load the nearest config file +# in next example will search first .framework-config file in "srcDir1" +# then if not found will go in up directories until / +# then will search in "srcDir2" +# then if not found will go in up directories until / +# source the file if found +# @example +# Conf::loadNearestFile ".framework-config" "srcDir1" "srcDir2" +# +# @arg $1 configFileName:String config file name to search +# @arg $2 loadedFile:String (passed by reference) will return the loaded config file name +# @arg $@ srcDirs:String[] source directories in which the config file will be searched +# @exitcode 0 if file found +# @exitcode 1 if file not found +Conf::loadNearestFile() { + local configFileName="$1" + local -n loadedFile="$2" + shift 2 || true + local -a srcDirs=("$@") + for srcDir in "${srcDirs[@]}"; do + configFile="$(File::upFind "${srcDir}" "${configFileName}" || true)" + if [[ -n "${configFile}" ]]; then + # shellcheck source=/.framework-config + source "${configFile}" || Log::fatal "error while loading config file '${configFile}'" + Log::displayDebug "Config file ${configFile} is loaded" + # shellcheck disable=SC2034 + loadedFile="${configFile}" + return 0 + fi + done + + Log::displayWarning "Config file '${configFileName}' not found in any source directories provided" + return 1 +} + +# @description get list of env files to load +# in order to make them available for Env::requireLoad +# @env BASH_FRAMEWORK_ENV_FILES String[] list of env files that should be loaded +# @exitcode 1 if one of the env file cannot be generated +# @exitcode 2 if one of the env file is not a file or readable +# @stdout the env files asked to be loaded +# @stderr diagnostic information on failure +# @see https://github.com/fchastanet/bash-tools-framework/blob/master/FrameworkDoc.md#config_file_order +Env::getOrderedConfFiles() { + local -a configFiles=() + + if [[ -n "${BASH_FRAMEWORK_ENV_FILES[0]+1}" ]]; then + # BASH_FRAMEWORK_ENV_FILES is an array + configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") + fi + + local defaultEnvFile + defaultEnvFile="$(Env::createDefaultEnvFile)" || return 1 + configFiles+=("${defaultEnvFile}") + + local file + for file in "${configFiles[@]}"; do + if [[ ! -f "${file}" || ! -r "${file}" ]]; then + Log::displayError "One of the config file is not available '${file}'" + return 2 + fi + echo "${file}" + done +} + +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport } -# Display message using info color (blue) but warning level -# @param {String} $1 message -Log::displayStatus() { - local type="${2:-STATUS}" - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - Log::logStatus "$1" "${type}" +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done } -# Display message using success color (bg green/fg white) -# @param {String} $1 message -Log::displaySuccess() { - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - Log::logSuccess "$1" +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logDebug() { - Log::logMessage "${2:-DEBUG}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logError() { - Log::logMessage "${2:-ERROR}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR)); then + Log::logMessage "${2:-ERROR}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logFatal() { Log::logMessage "${2:-FATAL}" "$1" } -# log message to file -# @param {String} $1 message -Log::logHelp() { - Log::logMessage "${2:-HELP}" "$1" -} - -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logInfo() { - Log::logMessage "${2:-INFO}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" + fi } -# log message to file -# @param {String} $1 message -Log::logSkipped() { - Log::logMessage "${2:-SKIPPED}" "$1" -} +# @description Internal: common log message +# @example text +# [date]|[levelMsg]|message +# +# @example text +# 2020-01-19 19:20:21|ERROR |log error +# 2020-01-19 19:20:21|SKIPPED|log skipped +# +# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) +# @arg $2 msg:String the message to display +# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty +# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages +# @stderr diagnostics information is displayed +# @require Env::requireLoad +# @require Log::requireLoad +Log::logMessage() { + local levelMsg="$1" + local msg="$2" + local date -# log message to file -# @param {String} $1 message -Log::logStatus() { - Log::logMessage "${2:-STATUS}" "$1" + if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + date="$(date '+%Y-%m-%d %H:%M:%S')" + touch "${BASH_FRAMEWORK_LOG_FILE}" + printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" + fi } -# log message to file -# @param {String} $1 message -Log::logSuccess() { - Log::logMessage "${2:-SUCCESS}" "$1" +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi } -# log message to file -# @param {String} $1 message +# @description log message to file +# @arg $1 message:String the message to display Log::logWarning() { - Log::logMessage "${2:-WARNING}" "$1" + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi } -# To be called before logging in the log file -# @param {string} file $1 log file name -# @param {int} maxLogFilesCount $2 maximum number of log files +# @description To be called before logging in the log file +# @arg $1 file:string log file name +# @arg $2 maxLogFilesCount:int maximum number of log files Log::rotate() { local file="$1" local maxLogFilesCount="${2:-5}" @@ -551,134 +729,728 @@ Log::rotate() { fi } -Args::showHelp() { - local helpArg="$1" - echo -e "${helpArg}" +# @description load color theme +# @noargs +# @env BASH_FRAMEWORK_THEME String theme to use +# @exitcode 0 always successful +UI::requireTheme() { + UI::theme "${BASH_FRAMEWORK_THEME-default}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 + + ( + echo "BASH_FRAMEWORK_THEME=${BASH_FRAMEWORK_THEME:-default}" + echo "BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0}" + echo "BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-${__LEVEL_WARNING}}" + # shellcheck disable=SC2016 + echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' + echo "BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + ) >"${envFile}" + echo "${envFile}" +} + +# @description search a file in parent directories # -# **Examples**: -#
-# 2020-01-19 19:20:21|ERROR  |log error
-# 2020-01-19 19:20:21|SKIPPED|log skipped
-# 
-Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date +# @arg $1 fromPath:String path +# @arg $2 fileName:String +# @arg $3 untilInclusivePath:String (optional) find for given file until reaching this folder (default value: /) +# @arg $@ untilInclusivePaths:String[] list of untilInclusivePath +# @stdout The filename if found +# @exitcode 1 if the command failed or file not found +File::upFind() { + local fromPath="$1" + shift || true + local fileName="$1" + shift || true + local untilInclusivePath="${1:-/}" + shift || true - if [[ -z "${BASH_FRAMEWORK_LOG_FILE}" ]]; then - return 0 + if [[ -f "${fromPath}" ]]; then + fromPath="$(dirname "${fromPath}")" fi - if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)) || [[ "${levelMsg}" = "FATAL" ]]; then - mkdir -p "$(dirname "${BASH_FRAMEWORK_LOG_FILE}")" || true - if Assert::fileWritable "${BASH_FRAMEWORK_LOG_FILE}"; then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - else - echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 + while true; do + if [[ -f "${fromPath}/${fileName}" ]]; then + echo "${fromPath}/${fileName}" + return 0 + fi + if Array::contains "${fromPath}" "${untilInclusivePath}" "$@" "/"; then + return 1 fi + fromPath="$(readlink -f "${fromPath}"/..)" + done + return 1 +} + +# @description remove comment lines from input or files provided as arguments +# @arg $@ files:String[] (optional) the files to filter +# @env commentLinePrefix String the comment line prefix (default value: #) +# @exitcode 0 if lines filtered or not +# @exitcode 2 if grep fails for any other reasons than not found +# @stdin the file as stdin to filter (alternative to files argument) +# @stdout the filtered lines +# shellcheck disable=SC2120 +Filters::commentLines() { + grep -vxE "[[:blank:]]*(${commentLinePrefix:-#}.*)?" "$@" || test $? = 1 +} + +# @description create a temp file using default TMPDIR variable +# initialized in _includes/_commonHeader.sh +# @env TMPDIR String (default value /tmp) +# @arg $1 templateName:String template name to use(optional) +Framework::createTempFile() { + mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" +} + +# @description check if an element is contained in an array +# +# @arg $1 needle:String +# @arg $@ array:String[] +# @exitcode 0 if found +# @exitcode 1 otherwise +# @example +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} + +# FUNCTIONS + +facade_main_ddde11adc18142a8b5cc63d0041ff1b9() { +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/." && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +# REQUIRES +Env::requireLoad +UI::requireTheme +Log::requireLoad +Compiler::Facade::requireCommandBinDir +Linux::requireExecutedAsUser + +# @require Compiler::Facade::requireCommandBinDir + +declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + +copyrightCallback() { + local years + years="$(date +%Y)" + if [[ -n "${copyrightBeginYear}" && "${copyrightBeginYear}" != "${years}" ]]; then + years="${copyrightBeginYear}-${years}" fi + echo "Copyright (c) ${years} François Chastanet" } -# Checks if file can be created in folder -# The file does not need to exist -Assert::fileWritable() { - local file="$1" - local dir +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListEnvFileCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListLogLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListDisplayLevelCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListNoColorCallback() { + BASH_FRAMEWORK_ARGV_FILTERED+=(--no-color) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListThemeCallback() { :; } +# shellcheck disable=SC2317 # if function is overridden +updateArgListQuietCallback() { :; } + +# shellcheck disable=SC2317 # if function is overridden +optionHelpCallback() { + installCommand help + exit 0 +} - dir="$(dirname "${file}")" +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} - Assert::validPath "${file}" && [[ -w "${dir}" ]] +# shellcheck disable=SC2317 # if function is overridden +optionEnvFileCallback() { + local envFile="$2" + if [[ ! -f "${envFile}" || ! -r "${envFile}" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option --env-file - File '${envFile}' doesn't exist" + exit 1 + fi } -# Public: check if argument is a valid linux path -# -# @param {string} path $1 path that needs to be checked -# @return 1 if path is invalid -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because option) -# - not beginning with a slash -# - relative -Assert::validPath() { - local path="$1" +# shellcheck disable=SC2317 # if function is overridden +optionInfoVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='--verbose' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_INFO} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_INFO} +} - # https://regex101.com/r/afLrmM/2 - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# shellcheck disable=SC2317 # if function is overridden +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -# FUNCTIONS +# shellcheck disable=SC2317 # if function is overridden +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} -Env::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" + ;; + ERROR) + echo "${__LEVEL_ERROR}" + ;; + WARNING) + echo "${__LEVEL_WARNING}" + ;; + INFO) + echo "${__LEVEL_INFO}" + ;; + DEBUG | TRACE) + echo "${__LEVEL_DEBUG}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -Log::load +getVerboseLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__VERBOSE_LEVEL_OFF}" + ;; + ERROR | WARNING | INFO) + echo "${__VERBOSE_LEVEL_INFO}" + ;; + DEBUG) + echo "${__VERBOSE_LEVEL_DEBUG}" + ;; + TRACE) + echo "${__VERBOSE_LEVEL_TRACE}" + ;; + *) + Log::displayError "Command ${SCRIPT_NAME} - Invalid level ${level}" + return 1 + esac +} -Env::pathPrepend "${COMMAND_BIN_DIR}" +# shellcheck disable=SC2317 # if function is overridden +optionDisplayLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_DISPLAY_LEVEL=${logLevel} +} -# prepare bin directory for eventual bin files generated by Embed::embed -mkdir -p "${TMPDIR:-/tmp}/bin" -Env::pathPrepend "${TMPDIR:-/tmp}/bin" +# shellcheck disable=SC2317 # if function is overridden +optionLogLevelCallback() { + local level="$2" + local logLevel verboseLevel + logLevel="$(getLevel "${level}")" + verboseLevel="$(getVerboseLevel "${level}")" + BASH_FRAMEWORK_ARGS_VERBOSE=${verboseLevel} + BASH_FRAMEWORK_LOG_LEVEL=${logLevel} +} -if [[ "$(id -u)" = "0" ]]; then - Log::fatal "this script should be executed as normal user" -fi +# shellcheck disable=SC2317 # if function is overridden +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -HELP="$( - cat < 0)); do + local options_parse_arg="$1" + local argOptDefaultBehavior=0 + case "${options_parse_arg}" in + # Option 1/14 + # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' + --bash-framework-config) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionBashFrameworkConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionBashFrameworkConfig)) + optionBashFrameworkConfig="$1" + optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" + ;; + # Option 2/14 + # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --config) + optionConfig="1" + if ((options_parse_optionParsedCountOptionConfig >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionConfig)) + ;; + # Option 3/14 + # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --verbose | -v) + optionInfoVerbose="1" + if ((options_parse_optionParsedCountOptionInfoVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionInfoVerbose)) + optionInfoVerboseCallback "${options_parse_arg}" + updateArgListInfoVerboseCallback "${options_parse_arg}" + ;; + # Option 4/14 + # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vv) + optionDebugVerbose="1" + if ((options_parse_optionParsedCountOptionDebugVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDebugVerbose)) + optionDebugVerboseCallback "${options_parse_arg}" + updateArgListDebugVerboseCallback "${options_parse_arg}" + ;; + # Option 5/14 + # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' + -vvv) + optionTraceVerbose="1" + if ((options_parse_optionParsedCountOptionTraceVerbose >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTraceVerbose)) + optionTraceVerboseCallback "${options_parse_arg}" + updateArgListTraceVerboseCallback "${options_parse_arg}" + ;; + # Option 6/14 + # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' + --env-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + ((++options_parse_optionParsedCountOptionEnvFiles)) + optionEnvFiles+=("$1") + optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" + ;; + # Option 7/14 + # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --no-color) + optionNoColor="1" + if ((options_parse_optionParsedCountOptionNoColor >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionNoColor)) + optionNoColorCallback "${options_parse_arg}" + updateArgListNoColorCallback "${options_parse_arg}" + ;; + # Option 8/14 + # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' + --theme) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ default|default-force|noColor ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(default|default-force|noColor)" + return 1 + fi + if ((options_parse_optionParsedCountOptionTheme >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionTheme)) + optionTheme="$1" + optionThemeCallback "${options_parse_arg}" "${optionTheme}" + updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" + ;; + # Option 9/14 + # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --help | -h) + optionHelp="1" + if ((options_parse_optionParsedCountOptionHelp >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionHelp)) + optionHelpCallback "${options_parse_arg}" + ;; + # Option 10/14 + # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --version) + optionVersion="1" + if ((options_parse_optionParsedCountOptionVersion >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionVersion)) + optionVersionCallback "${options_parse_arg}" + ;; + # Option 11/14 + # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --quiet | -q) + optionQuiet="1" + if ((options_parse_optionParsedCountOptionQuiet >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionQuiet)) + optionQuietCallback "${options_parse_arg}" + updateArgListQuietCallback "${options_parse_arg}" + ;; + # Option 12/14 + # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --log-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogLevel)) + optionLogLevel="$1" + optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" + ;; + # Option 13/14 + # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' + --log-file) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if ((options_parse_optionParsedCountOptionLogFile >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionLogFile)) + optionLogFile="$1" + optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" + updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" + ;; + # Option 14/14 + # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERROR|WARNING|INFO|DEBUG|TRACE' regexp '' + --display-level) + shift + if (($# == 0)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" + return 1 + fi + if [[ ! "$1" =~ OFF|ERROR|WARNING|INFO|DEBUG|TRACE ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - value '$1' is not part of authorized values(OFF|ERROR|WARNING|INFO|DEBUG|TRACE)" + return 1 + fi + if ((options_parse_optionParsedCountOptionDisplayLevel >= 1)); then + Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" + return 1 + fi + ((++options_parse_optionParsedCountOptionDisplayLevel)) + optionDisplayLevel="$1" + optionDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + updateArgListDisplayLevelCallback "${options_parse_arg}" "${optionDisplayLevel}" + ;; + -*) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Invalid option ${options_parse_arg}" + return 1 + fi + ;; + *) + if [[ "${argOptDefaultBehavior}" = "0" ]]; then + Log::displayError "Command ${SCRIPT_NAME} - Argument - too much arguments provided" + return 1 + fi + ;; + esac + shift || true + done + commandOptionParseFinished + Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" + Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" + elif [[ "${options_parse_cmd}" = "help" ]]; then + echo -e "$(Array::wrap " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Install dependent softwares and configuration needed to use bash-tools +- GNU parallel +- Install default configuration files")" + echo + + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" + echo -e "$(Array::wrap " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ + "${SCRIPT_NAME}" \ + "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + echo + echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" + printf " %b\n" "${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< use\ alternate\ bash\ framework\ configuration. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ configuration + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< info\ level\ verbose\ mode\ \(alias\ of\ --display-level\ INFO\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< debug\ level\ verbose\ mode\ \(alias\ of\ --display-level\ DEBUG\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< trace\ level\ verbose\ mode\ \(alias\ of\ --display-level\ TRACE\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Load\ the\ specified\ env\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Produce\ monochrome\ output.\ alias\ of\ --theme\ noColor. + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< choose\ color\ theme\ \(default\,\ default-force\ or\ noColor\)\ -\ default-force\ means\ colors\ will\ be\ produced\ even\ if\ command\ is\ piped + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Display\ this\ command\ help + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Print\ version\ information\ and\ quit + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< quiet\ mode\,\ doesn\'t\ display\ any\ output + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< Set\ log\ file + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + local -a helpArray + IFS=' ' read -r -a helpArray <<< set\ display\ level\ \(one\ of\ OFF\,\ ERROR\,\ WARNING\,\ INFO\,\ DEBUG\,\ TRACE\ value\) + echo -e " $(Array::wrap " " 76 4 "${helpArray[@]}")" + echo + echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" + echo '1.0' + echo + echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" + echo '[François Chastanet](https://github.com/fchastanet)' + echo + echo -e "${__HELP_TITLE_COLOR}SOURCE FILE:${__RESET_COLOR}" + echo 'https://github.com/fchastanet/bash-tools/tree/master/src/_binaries/build/install.sh' + echo + echo -e "${__HELP_TITLE_COLOR}LICENSE:${__RESET_COLOR}" + echo 'MIT License' + echo + Array::wrap ' ' 76 4 "$(copyrightCallback)" + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi +} +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" + +installCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + if ! command -v parallel &>/dev/null; then + Log::displayInfo "We will install GNU parallel software, please enter you sudo password" + sudo apt update || true + if sudo apt install -y parallel; then + # remove parallel nagware + mkdir -p ~/.parallel + touch ~/.parallel/will-cite + else + Log::displayWarning "Impossible to install GNU parallel, please install it manually" + fi + else + Log::displaySkipped "parallel is already installed" + fi -if ! command -v parallel 2>/dev/null; then - Log::displayInfo "We will install GNU parallel software, please enter you sudo password" - sudo apt update || true - if sudo apt install -y parallel; then - # remove parallel nagware - mkdir -p ~/.parallel - touch ~/.parallel/will-cite + if [[ -d "${HOME}/.bash-tools" ]]; then + Log::displayInfo "Updating configuration" + cp -R --no-clobber "${BASH_TOOLS_ROOT_DIR}/conf/." "${HOME}/.bash-tools" + if [[ "${BASE_DIR}/conf/.env" -nt "${HOME}/.bash-tools/.env" ]]; then + Log::displayWarning "${BASE_DIR}/conf/.env is newer than ${HOME}/.bash-tools/.env, compare the files to check if some updates need to be applied" + else + Log::displaySkipped "${HOME}/.bash-tools/.env is up to date" + fi else - Log::displayWarning "Impossible to install GNU parallel, please install it manually" + Log::displayInfo "Installing configuration in ~/.bash-tools" + mkdir -p ~/.bash-tools + cp -R conf/. ~/.bash-tools fi -fi +} -if [[ -d "${HOME}/.bash-tools" ]]; then - # update - cp -R --no-clobber "${BASH_TOOLS_ROOT_DIR}/conf/." "${HOME}/.bash-tools" - [[ "${BASE_DIR}/conf/.env" -nt "${HOME}/.bash-tools/.env" ]] && { - Log::displayWarning "${BASE_DIR}/conf/.env is newer than ${HOME}/.bash-tools/.env, compare the files to check if some updates need to be applied" - } +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null else - mkdir -p ~/.bash-tools - cp -R conf/. ~/.bash-tools + run fi + +} + +facade_main_ddde11adc18142a8b5cc63d0041ff1b9 "$@" diff --git a/src/_binaries/Converters/mysql2puml.bats b/src/_binaries/Converters/mysql2puml.bats index 9e69500d..9ac6fd65 100755 --- a/src/_binaries/Converters/mysql2puml.bats +++ b/src/_binaries/Converters/mysql2puml.bats @@ -3,9 +3,6 @@ # shellcheck source=src/batsHeaders.sh source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" -# shellcheck source=vendor/bash-tools-framework/src/Env/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Env/load.sh" || exit 1 - setup() { export TMPDIR="${BATS_TEST_TMPDIR}" @@ -22,25 +19,25 @@ function Converters::mysql2puml::display_help { #@test # shellcheck disable=SC2154 run "${binDir}/mysql2puml" --help 2>&1 assert_success - assert_line --index 0 "Description: convert mysql dump sql schema to plantuml format" + assert_line --index 0 "DESCRIPTION: convert mysql dump sql schema to plantuml format" } function Converters::mysql2puml::display_version { #@test run "${binDir}/mysql2puml" --version 2>&1 assert_success - assert_line --index 0 "mysql2puml Version: 0.1" + assert_line --index 0 "mysql2puml version 1.0" } function Converters::mysql2puml::bad_skin_file { #@test run "${binDir}/mysql2puml" --skin badSkin 2>&1 assert_failure - assert_line --index 0 --partial "ERROR - conf file 'badSkin' not found" + assert_line --index 0 --partial "ERROR - mysql2puml - invalid skin 'badSkin' provided" } function Converters::mysql2puml::input_file_not_found { #@test run "${binDir}/mysql2puml" --skin default notFound.sql 2>&1 assert_failure - assert_line --index 0 --partial "FATAL - file notFound.sql does not exist" + assert_line --index 0 --partial "ERROR - mysql2puml - File 'notFound.sql' does not exists" } function Converters::mysql2puml::parse_file { #@test diff --git a/src/_binaries/Converters/mysql2puml.options.tpl b/src/_binaries/Converters/mysql2puml.options.tpl new file mode 100644 index 00000000..df8f5ee3 --- /dev/null +++ b/src/_binaries/Converters/mysql2puml.options.tpl @@ -0,0 +1,81 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="mysql2pumlCommand" +declare optionSkinDefault="default" +declare help="convert mysql dump sql schema to plantuml format" +declare longDescription=""" +${__HELP_TITLE}Examples${__HELP_NORMAL} +mysql2puml dump.dql + +mysqldump --skip-add-drop-table \ + --skip-add-locks \ + --skip-disable-keys \ + --skip-set-charset \ + --user=root \ + --password=root \ + --no-data skills | mysql2puml + +${__HELP_TITLE}List of available skins:${__HELP_NORMAL} +@@@SKINS_LIST@@@""" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + Options::generateOption \ + --variable-type String \ + --help "header configuration of the plant uml file (default: ${optionSkinDefault})" \ + --alt "--skin" \ + --callback "optionSkinCallback" \ + --variable-name "optionSkin" \ + --function-name optionSkinFunction + inputSqlFileCallback() { :; } + Options::generateArg \ + --variable-name "inputSqlFile" \ + --min 0 \ + --max 1 \ + --name "inputSqlFile" \ + --callback inputSqlFileCallback \ + --help "sql filepath to parse (read from stdin if not provided)" \ + --function-name argumentInputSqlFileFunction +) +options+=( + optionSkinFunction + argumentInputSqlFileFunction +) +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" +declare optionSkin="<% ${optionSkinDefault} %>" + +optionHelpCallback() { + local skinListHelpFile + skinListHelpFile="$(Framework::createTempFile "shellcheckHelp")" + Conf::getMergedList "mysql2pumlSkins" ".puml" " - " >"${skinListHelpFile}" + + <% ${commandFunctionName} %> help | + sed -E \ + -e "/@@@SKINS_LIST@@@/r ${skinListHelpFile}" \ + -e "/@@@SKINS_LIST@@@/d" + exit 0 +} + +optionSkinCallback() { + declare -a skinList + readarray -t skinList < <(Conf::getMergedList "mysql2pumlSkins" ".puml" "") + if ! Array::contains "$2" "${skinList[@]}"; then + Log::displayError "${SCRIPT_NAME} - invalid skin '$2' provided" + return 1 + fi +} + +inputSqlFileCallback() { + # shellcheck disable=SC2154 + if [[ ! -f "${inputSqlFile}" ]]; then + Log::displayError "${SCRIPT_NAME} - File '${inputSqlFile}' does not exists" + return 1 + fi +} diff --git a/src/_binaries/Converters/mysql2puml.sh b/src/_binaries/Converters/mysql2puml.sh index ebd23eb4..2d216386 100755 --- a/src/_binaries/Converters/mysql2puml.sh +++ b/src/_binaries/Converters/mysql2puml.sh @@ -1,108 +1,35 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/mysql2puml +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Converters/mysql2puml.options.tpl)" -#default values -SCRIPT_VERSION="0.1" -SKIN="default" +mysql2pumlCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -# Usage info -showHelp() { - local skinList="" - skinList="$(Conf::getMergedList "mysql2pumlSkins" ".puml")" - - cat </dev/null) || { - showHelp - Log::fatal "invalid options specified" -} +)" -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --version) - showVersion - exit 0 - ;; - --skin | -s) - shift - SKIN="$1" - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done -shift $((OPTIND - 1)) || true +run() { + # shellcheck disable=SC2154 + absSkinFile="$(Conf::getAbsoluteFile "mysql2pumlSkins" "${optionSkin}" "puml")" || + Log::fatal "the skin ${optionSkin} does not exist" -sqlFile="${1:-}" -shift || true -if (($# > 0)); then - showHelp - Log::fatal "too much arguments provided" -fi + if [[ -n "${inputSqlFile}" ]]; then + exec 3<"${inputSqlFile}" + elif [[ ! -t 0 ]]; then + exec 3<&0 + fi -absSkinFile="$(Conf::getAbsoluteFile "mysql2pumlSkins" "${SKIN}" "puml")" || - Log::fatal "the skin ${SKIN} does not exist" + awk --source "${awkScript}" "${absSkinFile}" - <&3 | Filters::trimEmptyLines +} -if [[ -n "${sqlFile}" ]]; then - if [[ ! -f "${sqlFile}" ]]; then - Log::fatal "file ${sqlFile} does not exist" - fi - exec 3<"${sqlFile}" -elif [[ ! -t 0 ]]; then - exec 3<&0 +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null else - Log::fatal "No sql file provided..." + run fi - -awkScript="$( - cat <<'EOF' -.INCLUDE "$(dynamicSrcFile _binaries/Converters/mysql2puml.awk)" -EOF -)" -awk --source "${awkScript}" "${absSkinFile}" - <&3 | Filters::trimEmptyLines diff --git a/src/_binaries/DbImport/dbImport.bats b/src/_binaries/DbImport/dbImport.bats index 02626324..1a1f6403 100755 --- a/src/_binaries/DbImport/dbImport.bats +++ b/src/_binaries/DbImport/dbImport.bats @@ -3,13 +3,10 @@ # shellcheck source=src/batsHeaders.sh source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" -# shellcheck source=vendor/bash-tools-framework/src/Env/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Env/load.sh" || exit 1 - setup() { export TMPDIR="${BATS_TEST_TMPDIR}" - export HOME="${BATS_TEST_TMPDIR}/home" + mkdir -p "${HOME}" mkdir -p \ "${HOME}/bin" \ @@ -38,45 +35,64 @@ teardown() { function Database::dbImport::display_help { #@test # shellcheck disable=SC2154 run "${binDir}/dbImport" --help 2>&1 - assert_line --index 0 "Description: Import source db into target db" + assert_success + assert_line --index 0 "DESCRIPTION: Import source db into target db using eventual table filter" } function Database::dbImport::remoteDbName_not_provided { #@test # shellcheck disable=SC2154 run "${binDir}/dbImport" 2>&1 - assert_output --partial "FATAL - you must provide remoteDbName" + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command dbImport - Argument 'fromDbName' should be provided at least 1 time(s)" } function Database::dbImport::from_aws_and_aws_not_installed { #@test - run "${binDir}/dbImport" --from-aws fromDb >"${BATS_TEST_TMPDIR}/output" + run "${binDir}/dbImport" --from-aws fromAws fromDb + assert_failure 1 # if it fails, check you are running it from docker so aws command is not available - assert_output --partial "ERROR - aws is not installed, please install it" + assert_lines_count 3 + assert_line --index 0 --partial "INFO - Using profile /tmp" + assert_line --index 1 --partial "ERROR - aws is not installed, please install it" + assert_line --index 2 --partial "INFO - Command dbImport - missing aws, please check https://docs.aws.amazon.com/fr_fr/cli/latest/userguide/install-cliv2.html" } function Database::dbImport::from_aws_and_from_dsn_are_incompatible { #@test stub aws - run "${binDir}/dbImport" --from-dsn default --from-aws fromDb 2>&1 - assert_output --partial "FATAL - you cannot use from-dsn and from-aws at the same time" + run "${binDir}/dbImport" --from-dsn default --from-aws fromAws fromDb 2>&1 + assert_failure 1 + assert_lines_count 2 + assert_line --index 0 --partial "INFO - Using profile /tmp" + assert_line --index 1 --partial "FATAL - Command dbImport - you cannot use from-dsn and from-aws at the same time" } function Database::dbImport::from_aws_missing_S3_BASE_URL { #@test stub aws sed -i -E 's#^S3_BASE_URL=.*$##g' "${HOME}/.bash-tools/.env" - run "${binDir}/dbImport" --from-aws fromDb 2>&1 - assert_output --partial "FATAL - missing S3_BASE_URL, please provide a value in .env file" + run "${binDir}/dbImport" --from-aws fromAws fromDb 2>&1 + assert_failure 1 + assert_lines_count 2 + assert_line --index 0 --partial "INFO - Using profile /tmp" + assert_line --index 1 --partial "FATAL - Command dbImport - missing S3_BASE_URL, please provide a value in .env file" } function Database::dbImport::a_and_f_are_incompatible { #@test stub aws - run "${binDir}/dbImport" -f default -a fromDb 2>&1 - assert_output --partial "FATAL - you cannot use from-dsn and from-aws at the same time" + run "${binDir}/dbImport" -f default -a fromAws fromDb 2>&1 + assert_failure 1 + assert_lines_count 2 + assert_line --index 0 --partial "INFO - Using profile /tmp" + assert_line --index 1 --partial "FATAL - Command dbImport - you cannot use from-dsn and from-aws at the same time" } function Database::dbImport::missing_aws { #@test # missing argument - run "${binDir}/dbImport" -a fromDb --verbose 2>&1 - assert_output --partial "ERROR - aws is not installed, please install it" - assert_output --partial "INFO - missing aws, please check" + run "${binDir}/dbImport" -a fromAws fromDb --verbose 2>&1 + assert_failure 1 + assert_lines_count 3 + assert_line --index 0 --partial "INFO - Using profile /tmp" + assert_line --index 1 --partial "ERROR - aws is not installed, please install it" + assert_line --index 2 --partial "INFO - Command dbImport - missing aws, please check" } function Database::dbImport::tables_invalid { #@test @@ -85,34 +101,40 @@ function Database::dbImport::tables_invalid { #@test export BASH_FRAMEWORK_ENV_FILEPATH="${BATS_TEST_DIRNAME}/testsData/.env" # missing argument - run "${binDir}/dbImport" -a fromDb --tables 2>&1 - assert_output --partial "FATAL - invalid options specified" + run "${binDir}/dbImport" -a fromAws fromDb --tables 2>&1 + assert_failure 1 + assert_output --partial "ERROR - Command dbImport - Option --tables - a value needs to be specified" # invalid argument - run "${binDir}/dbImport" -a fromDb --tables ddd@ 2>&1 - assert_output --partial "FATAL - Table list is not valid : ddd@" + run "${binDir}/dbImport" -a fromAws fromDb --tables ddd@ 2>&1 + assert_failure 1 + assert_output --partial "FATAL - Command dbImport - Table list is not valid : ddd@" # invalid argument - run "${binDir}/dbImport" -a fromDb --tables ddd, 2>&1 - assert_output --partial "FATAL - Table list is not valid : ddd," + run "${binDir}/dbImport" -a fromAws fromDb --tables ddd, 2>&1 + assert_failure 1 + assert_output --partial "FATAL - Command dbImport - Table list is not valid : ddd," # invalid argument - run "${binDir}/dbImport" -a fromDb --tables ddd,dd, 2>&1 - assert_output --partial "FATAL - Table list is not valid : ddd,dd," + run "${binDir}/dbImport" -a fromAws fromDb --tables ddd,dd, 2>&1 + assert_failure 1 + assert_output --partial "FATAL - Command dbImport - Table list is not valid : ddd,dd," # invalid argument - run "${binDir}/dbImport" -a fromDb --tables ddd- 2>&1 - assert_output --partial "FATAL - Table list is not valid : ddd-" + run "${binDir}/dbImport" -a fromAws fromDb --tables ddd- 2>&1 + assert_failure 1 + assert_output --partial "FATAL - Command dbImport - Table list is not valid : ddd-" } function Database::dbImport::aws_file_not_found { #@test stub aws \ - "s3 ls --human-readable s3://s3server/exports/fromDb : exit 1" + "s3 ls --human-readable s3://s3server/exports/fromAws.tar.gz : exit 1" export BASH_FRAMEWORK_ENV_FILEPATH="${BATS_TEST_DIRNAME}/testsData/.env" - run "${binDir}/dbImport" -a fromDb 2>&1 - assert_output --partial "FATAL - unable to get information on S3 object : s3://s3server/exports/fromDb" + run "${binDir}/dbImport" -a fromAws.tar.gz fromDb 2>&1 + assert_failure 1 + assert_output --partial "FATAL - Command dbImport - unable to get information on S3 object : s3://s3server/exports/fromAws.tar.gz" } function Database::dbImport::dsn_file_not_found { #@test @@ -165,7 +187,7 @@ function Database::dbImport::remote_db_fully_functional_from_mysql { #@test [[ "$(zcat "${HOME}/.bash-tools/dbImportDumps/fromDb_default_structure.sql.gz" | grep '####structure####')" = "####structure####" ]] } -function Database::dbImport::remote_db_dump_already_present { #@test +function Database::dbImport::remote_db_dump_already_present_from_db { #@test # change modification date 32 days in the past touch -d@$(($(date +%s) - 32 * 86400)) "${HOME}/.bash-tools/dbImportDumps/oldDump.sql.gz" # change modification date 1 day in the future @@ -181,7 +203,7 @@ function Database::dbImport::remote_db_dump_already_present { #@test stub mysql \ $'* --batch --raw --default-character-set=utf8 --connect-timeout=5 -s --skip-column-names -e \'CREATE DATABASE IF NOT EXISTS `toDb` CHARACTER SET "utf8" COLLATE "utf8_general_ci"\' : echo "db created"' \ "\* --connect-timeout=5 --batch --raw --default-character-set=utf8 -s --skip-column-names toDb : echo 'import structure dump'" \ - $'* --connect-timeout=5 --batch --raw --default-character-set=utf8 toDb : i=0 ; while read line; do ((i=i+1)); echo "line $i"; done < /dev/stdin' + $'* --connect-timeout=5 --batch --raw --default-character-set=utf8 toDb : i=0 ; while read line; do ((i=i+1)); echo "line $i"; done < /dev/stdin' export BASH_FRAMEWORK_ENV_FILEPATH="${BATS_TEST_DIRNAME}/testsData/.env" @@ -205,7 +227,7 @@ function Database::dbImport::remote_db_fully_functional_from_aws { #@test stub aws \ 's3 ls --human-readable s3://s3server/exports/fromDb.tar.gz : exit 0' \ - "s3 cp s3://s3server/exports/fromDb.tar.gz '${HOME}/.bash-tools/dbImportDumps/fromDb.tar.gz' : exit 0" + "s3 cp s3://s3server/exports/fromDb.tar.gz '${HOME}/.bash-tools/dbImportDumps/fromDb.tar.gz' : touch '${HOME}/.bash-tools/dbImportDumps/fromDb.tar.gz'; exit 0" stub tar \ "xOfz '${HOME}/.bash-tools/dbImportDumps/fromDb.tar.gz' : cat '${BATS_TEST_DIRNAME}/testsData/dump.sql'" diff --git a/src/_binaries/DbImport/dbImport.options.tpl b/src/_binaries/DbImport/dbImport.options.tpl new file mode 100644 index 00000000..9fcfa74e --- /dev/null +++ b/src/_binaries/DbImport/dbImport.options.tpl @@ -0,0 +1,161 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="dbImportCommand" +declare help="Import source db into target db using eventual table filter" +# shellcheck disable=SC2016 +declare longDescription=''' +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} + +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList} + +${__HELP_TITLE}Aws s3 location:${__HELP_NORMAL} +${S3_BASE_URL} + +${__HELP_TITLE}Example 1: from one database to another one${__HELP_NORMAL} +${__HELP_EXAMPLE}TODO${__HELP_NORMAL} + +${__HELP_TITLE}Example 2: import from S3${__HELP_NORMAL} +${__HELP_EXAMPLE}TODO${__HELP_NORMAL}''' +declare defaultFromDsn="default.remote" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.profile.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.mysql.target.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.mysql.collationName.tpl)" + +% +# shellcheck source=/dev/null +source <( + Options::generateGroup \ + --title "FROM OPTIONS:" \ + --function-name groupFromOptionsFunction + + Options::generateOption \ + --help "avoid to import the schema" \ + --group groupFromOptionsFunction \ + --alt "--skip-schema" \ + --alt "-s" \ + --variable-name "optionSkipSchema" \ + --function-name optionSkipSchemaFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo \ + "dsn to use for source database (Default: ${defaultFromDsn})" \ + "this option is incompatible with -a|--from-aws option" \ + )" \ + --variable-type "String" \ + --group groupFromOptionsFunction \ + --alt "--from-dsn" \ + --alt "-f" \ + --variable-name "optionFromDsn" \ + --function-name optionFromDsnFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "awsFile" \ + --help "$(echo \ + "db dump will be downloaded from s3 instead of using remote db." \ + "The value is the name of the file without s3 location" \ + "(Only .gz or tar.gz file are supported)." \ + "This option is incompatible with -f|--from-dsn option" \ + )" \ + --group groupFromOptionsFunction \ + --alt "--from-aws" \ + --alt "-a" \ + --variable-type "String" \ + --variable-name "optionFromAws" \ + --function-name optionFromAwsFunction + + Options::generateArg \ + --help "the name of the source/remote database" \ + --min 1 \ + --max 1 \ + --name "fromDbName" \ + --variable-name "fromDbName" \ + --function-name argumentFromDbNameFunction + + Options::generateArg \ + --help "the name of the target database, use fromDbName(without extension) if not provided" \ + --variable-name "targetDbName" \ + --min 0 \ + --max 1 \ + --name "targetDbName" \ + --function-name argumentTargetDbNameFunction +) +options+=( + optionSkipSchemaFunction + optionFromDsnFunction + optionFromAwsFunction + argumentFromDbNameFunction + argumentTargetDbNameFunction + --callback dbImportCommandCallback +) +Options::generateCommand "${options[@]}" +% + +# default values +declare optionFromAws="" +declare optionSkipSchema="0" +declare targetDbName="" +declare fromDbName="" +declare optionFromDsn="" + +# other configuration +declare copyrightBeginYear="2020" +declare TIMEFORMAT='time spent : %3R' +declare DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR%/} +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" +declare DOWNLOAD_DUMP=0 + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" + + <% ${commandFunctionName} %> help | envsubst + exit 0 +} + +dbImportCommandCallback() { + if [[ -z "${targetDbName}" ]]; then + targetDbName="${fromDbName}" + fi + + if [[ -n "${optionFromAws}" ]]; then + Assert::commandExists aws \ + "Command ${SCRIPT_NAME} - missing aws, please check https://docs.aws.amazon.com/fr_fr/cli/latest/userguide/install-cliv2.html" || exit 1 + + if [[ -n "${optionFromDsn}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - you cannot use from-dsn and from-aws at the same time" + fi + + if [[ -z "${S3_BASE_URL}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - missing S3_BASE_URL, please provide a value in .env file" + fi + elif [[ -z "${optionFromDsn}" ]]; then + # default value for FROM_DSN if from-aws not set + optionFromDsn="<% ${defaultFromDsn} %>" + fi + + if [[ -z "${DB_IMPORT_DUMP_DIR}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} -you have to specify a value for DB_IMPORT_DUMP_DIR env variable" + fi + + if [[ ! -d "${DB_IMPORT_DUMP_DIR}" ]]; then + mkdir -p "${DB_IMPORT_DUMP_DIR}" || + Log::fatal "Command ${SCRIPT_NAME} -impossible to create directory ${DB_IMPORT_DUMP_DIR} specified by DB_IMPORT_DUMP_DIR env variable" + fi +} diff --git a/src/_binaries/DbImport/dbImport.sh b/src/_binaries/DbImport/dbImport.sh index f4a2f63e..2d03dd7b 100755 --- a/src/_binaries/DbImport/dbImport.sh +++ b/src/_binaries/DbImport/dbImport.sh @@ -1,266 +1,9 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImport +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" - -Assert::expectNonRootUser - -# default values -PROFILE="default" -TABLES="" -DOWNLOAD_DUMP=0 -FROM_AWS=0 -SKIP_SCHEMA=0 -REMOTE_DB="" -TARGET_DB="" -COLLATION_NAME="" -CHARACTER_SET="" -FROM_DSN="" -DEFAULT_FROM_DSN="default.remote" -TARGET_DSN="default.local" -TIMEFORMAT='time spent : %3R' -# jscpd:ignore-start -DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR%/} -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" - -showHelp() { - local profilesList="" - local dsnList="" - dsnList="$(Conf::getMergedList "dsn" "env")" - profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" - - cat < [] -${__HELP_TITLE}Usage:${__HELP_NORMAL} ${SCRIPT_NAME} -a|--from-aws [] - [-a|--from-aws] - [-s|--skip-schema] [-p|--profile profileName] - [-o|--collation-name utf8_general_ci] [-c|--character-set utf8] - [-t|--target-dsn dsn] [-f|--from-dsn dsn] - [--tables tableName1,tableName2] - - If option -a is provided - remoteDBName will represent the name of the s3 file - Only .gz or tar.gz file are supported - the name of the source/remote database - the name of the target database, use fromDbName(without extension) if not provided - -s|--skip-schema avoid to import the schema - -o|--collation-name change the collation name used during database creation - (default value: collation name used by remote db) - -c|--character-set change the character set used during database creation - (default value: character set used by remote db or dump file if aws) - -p|--profile profileName the name of the profile to use in order to include or exclude tables - (if not specified ${HOME_PROFILES_DIR}/default.sh is used if exists otherwise ${PROFILES_DIR}/default.sh) - -t|--target-dsn dsn dsn to use for target database (Default: ${TARGET_DSN}) - -f|--from-dsn dsn dsn to use for source database (Default: ${DEFAULT_FROM_DSN}) - this option is incompatible with -a|--from-aws option - -a|--from-aws db dump will be downloaded from s3 instead of using remote db, - remoteDBName will represent the name of the file - profile will be calculated against the dump itself - this option is incompatible with -f|--from-dsn option - --tables table1,table2 import only table specified in the list - if aws mode, ignore profile option - - Aws s3 location : ${S3_BASE_URL} - -${__HELP_TITLE}List of available profiles (default profiles dir ${PROFILES_DIR} can be overridden in home profiles ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} -${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} -${dsnList} - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -} -# jscpd:ignore-end - -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,tables:,target-dsn:,from-dsn:,from-aws,skip-schema,profile:,collation-name:,character-set: -o aht:f:sp:c:o: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" -} - -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - -a | --from-aws) - FROM_AWS="1" - # structure is included in s3 file - SKIP_SCHEMA="1" - ;; - --tables) - shift || true - TABLES="$1" - ;; - -t | --target-dsn) - shift || true - TARGET_DSN="$1" - ;; - -f | --from-dsn) - shift || true - FROM_DSN="${1:-${DEFAULT_FROM_DSN}}" - ;; - -s | --skip-schema) - SKIP_SCHEMA="1" - ;; - -p | --profile) - shift || true - PROFILE="$1" - ;; - -o | --collation-name) - shift || true - COLLATION_NAME="$1" - ;; - -c | --character-set) - shift || true - CHARACTER_SET="$1" - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done - -# check dependencies -Assert::commandExists mysql "sudo apt-get install -y mysql-client" -Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" -Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" -Assert::commandExists pv "sudo apt-get install -y pv" -Assert::commandExists gawk "sudo apt-get install -y gawk" -Assert::commandExists awk "sudo apt-get install -y gawk" -Version::checkMinimal "gawk" "--version" "5.0.1" - -# additional arguments -shift $((OPTIND - 1)) || true -while true; do - if [[ -z "$1" ]]; then - # last argument - break - fi - if [[ -z "${REMOTE_DB}" ]]; then - REMOTE_DB="$1" - else - TARGET_DB="$1" - fi - shift || true -done - -if [[ -z "${REMOTE_DB}" ]]; then - Log::fatal "you must provide remoteDbName" -fi - -if [[ -z "${TARGET_DB}" ]]; then - # remove eventual file extension - TARGET_DB="${REMOTE_DB%%.*}" -fi - -# check s3 parameter -if [[ "${FROM_AWS}" = "1" ]]; then - Assert::commandExists aws \ - "missing aws, please check https://docs.aws.amazon.com/fr_fr/cli/latest/userguide/install-cliv2.html" || exit 1 - - if [[ -n "${FROM_DSN}" ]]; then - Log::fatal "you cannot use from-dsn and from-aws at the same time" - fi - - if [[ -z "${S3_BASE_URL}" ]]; then - Log::fatal "missing S3_BASE_URL, please provide a value in .env file" - fi -elif [[ -z "${FROM_DSN}" ]]; then - # default value for FROM_DSN if from-aws not set - FROM_DSN="${DEFAULT_FROM_DSN}" -fi - -# load the profile -if [[ -z "${PROFILE}" ]]; then - showHelp - Log::fatal "you should specify a profile" -fi - -[[ "${PROFILE}" != "default" && -n "${TABLES}" ]] && - Log::fatal "you cannot use table and profile options at the same time" - -# Profile selection -PROFILE_COMMAND="$(Conf::getAbsoluteFile "dbImportProfiles" "${PROFILE}" "sh")" || exit 1 -PROFILE_MSG_INFO="Using profile ${PROFILE_COMMAND}" -if [[ -n "${TABLES}" ]]; then - [[ ${TABLES} =~ ^[A-Za-z0-9_]+(,[A-Za-z0-9_]+)*$ ]] || { - Log::fatal "Table list is not valid : ${TABLES}" - } -fi - -if [[ "${PROFILE}" = 'default' && -n "${TABLES}" ]]; then - PROFILE_COMMAND=$(Framework::createTempFile "profileCmd.XXXXXXXXXXXX") - chmod +x "${PROFILE_COMMAND}" - PROFILE_MSG_INFO="only ${TABLES} will be imported" - ( - echo '#!/usr/bin/env bash' - if [[ -n "${TABLES}" ]]; then - echo "${TABLES}" | sed -E 's/([A-Za-z0-9_]+),?/echo "\1"\n/g' - else - # tables option not specified, we will import all tables of the profile - echo 'cat' - fi - ) >"${PROFILE_COMMAND}" -fi -Log::displayInfo "${PROFILE_MSG_INFO}" - -[[ -z "${DB_IMPORT_DUMP_DIR}" ]] && - Log::fatal "you have to specify a value for DB_IMPORT_DUMP_DIR env variable" - -if [[ ! -d "${DB_IMPORT_DUMP_DIR}" ]]; then - mkdir -p "${DB_IMPORT_DUMP_DIR}" || - Log::fatal "impossible to create directory ${DB_IMPORT_DUMP_DIR} specified by DB_IMPORT_DUMP_DIR env variable" -fi - -# create db instances -declare -Agx dbFromInstance dbTargetDatabase - -Database::newInstance dbTargetDatabase "${TARGET_DSN}" -Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" -Log::displayInfo "Using target dsn ${dbTargetDatabase['DSN_FILE']}" -if [[ "${FROM_AWS}" = "0" ]]; then - Database::newInstance dbFromInstance "${FROM_DSN}" - Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" - Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" -fi - -if [[ "${FROM_AWS}" = "1" ]]; then - REMOTE_DB_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}" -else - REMOTE_DB_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}_${PROFILE}.sql.gz" - REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE="${DB_IMPORT_DUMP_DIR}/${REMOTE_DB}_${PROFILE}_structure.sql.gz" -fi - -# check if local dump exists -if [[ ! -f "${REMOTE_DB_DUMP_TEMP_FILE}" ]]; then - Log::displayInfo "local dump does not exist" - DOWNLOAD_DUMP=1 -fi -if [[ "${FROM_AWS}" = "0" && ! -f "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" ]]; then - Log::displayInfo "local structure dump does not exist" - DOWNLOAD_DUMP=1 -fi -if [[ "${DOWNLOAD_DUMP}" = "0" ]]; then - Log::displayInfo "local dump ${REMOTE_DB_DUMP_TEMP_FILE} already exists, avoid download" -fi +.INCLUDE "$(dynamicTemplateDir _binaries/DbImport/dbImport.options.tpl)" # dump header/footer read -r -d '\0' DUMP_HEADER <<-EOM @@ -276,127 +19,191 @@ read -r -d '\0' DUMP_FOOTER <<-EOM2 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;\0 EOM2 -Log::displayInfo "tables list will calculated using profile ${PROFILE} => ${PROFILE_COMMAND}" -chmod +x "${PROFILE_COMMAND}" -SECONDS=0 -if [[ "${DOWNLOAD_DUMP}" = "1" ]]; then - Log::displayInfo "Download dump" +declare DUMP_SIZE_QUERY +DUMP_SIZE_QUERY="$( + cat <<'EOF' +.INCLUDE "${TEMPLATE_DIR}/_binaries/DbImport/dumpSizeQuery.sql" +EOF +)" + +dbImportCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" + Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" + Assert::commandExists pv "sudo apt-get install -y pv" + Assert::commandExists gawk "sudo apt-get install -y gawk" + Assert::commandExists awk "sudo apt-get install -y gawk" + Version::checkMinimal "gawk" "--version" "5.0.1" + + # create db instances + declare -Agx dbFromInstance dbTargetDatabase + + # shellcheck disable=SC2154 + Database::newInstance dbTargetDatabase "${optionTargetDsn}" + Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using target dsn ${dbTargetDatabase['DSN_FILE']}" + if [[ -z "${optionFromAws}" ]]; then + # shellcheck disable=SC2154 + Database::newInstance dbFromInstance "${optionFromDsn}" + Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + fi - if [[ "${FROM_AWS}" = "1" ]]; then - # download dump from s3 - S3_URL="${S3_BASE_URL%/}/${REMOTE_DB}" - aws s3 ls --human-readable "${S3_URL}" || { - Log::fatal "unable to get information on S3 object : ${S3_URL}" - } - Log::displayInfo "Download dump from ${S3_URL} ..." - TMPDIR="${TMDIR:-/tmp}" aws s3 cp "${S3_URL}" "${REMOTE_DB_DUMP_TEMP_FILE}" || { - Log::fatal "unable to download dump from S3 : ${S3_URL}" - } + local remoteDbDumpTempFile + local remoteDbStructureDumpTempFile + if [[ -n "${optionFromAws}" ]]; then + remoteDbDumpTempFile="${DB_IMPORT_DUMP_DIR}/${optionFromAws}" else - # check if remote db exists - Database::ifDbExists dbFromInstance "${REMOTE_DB}" || { - Log::fatal "Remote Database ${REMOTE_DB} does not exist" - } + # shellcheck disable=SC2154 + remoteDbDumpTempFile="${DB_IMPORT_DUMP_DIR}/${fromDbName}_${optionProfile}.sql.gz" + remoteDbStructureDumpTempFile="${DB_IMPORT_DUMP_DIR}/${fromDbName}_${optionProfile}_structure.sql.gz" + fi - # get remote db collation name - if [[ -z "${COLLATION_NAME}" ]]; then - COLLATION_NAME=$(Database::query dbFromInstance \ - "SELECT default_collation_name FROM information_schema.SCHEMATA WHERE schema_name = \"${REMOTE_DB}\";" "information_schema") - fi + # check if local dump exists + local downloadDump=0 + if [[ ! -f "${remoteDbDumpTempFile}" ]]; then + Log::displayInfo "local dump does not exist" + downloadDump=1 + fi + if [[ -z "${optionFromAws}" && ! -f "${remoteDbStructureDumpTempFile}" ]]; then + Log::displayInfo "local structure dump does not exist" + downloadDump=1 + fi + if [[ "${downloadDump}" = "0" ]]; then + Log::displayInfo "local dump ${remoteDbDumpTempFile} already exists, avoid download" + fi - # get remote db character set - if [[ -z "${CHARACTER_SET}" ]]; then - CHARACTER_SET=$(Database::query dbFromInstance \ - "SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = \"${REMOTE_DB}\";" "information_schema") + Log::displayInfo "tables list will calculated using profile ${optionProfile} => ${profileCommand}" + SECONDS=0 + if [[ "${downloadDump}" = "1" ]]; then + Log::displayInfo "Download dump" + + if [[ -n "${optionFromAws}" ]]; then + # download dump from s3 + local s3Url="${S3_BASE_URL%/}/${optionFromAws}" + aws s3 ls --human-readable "${s3Url}" || { + Log::fatal "Command ${SCRIPT_NAME} - unable to get information on S3 object : ${s3Url}" + } + Log::displayInfo "Download dump from ${s3Url} ..." + TMPDIR="${TMDIR:-/tmp}" aws s3 cp "${s3Url}" "${remoteDbDumpTempFile}" || { + Log::fatal "Command ${SCRIPT_NAME} - unable to download dump from S3 : ${s3Url}" + } + else + # check if remote db exists + Database::ifDbExists dbFromInstance "${fromDbName}" || { + Log::fatal "Command ${SCRIPT_NAME} - Remote Database ${fromDbName} does not exist" + } + + initializeDefaultTargetMysqlOptions dbFromInstance "${fromDbName}" + + local dumpHeader + dumpHeader=$(printf "%s\nSET names '%s';\n" "${DUMP_HEADER}" "${optionCharacterSet}") + + # calculate remote db dump size + local listTables + local listTablesDumpSize + local listTablesDump + listTables="$(Database::query dbFromInstance "show tables" "${fromDbName}" | ${profileCommand} | sort)" + # shellcheck disable=SC2034 # used by DUMP_SIZE_QUERY + listTablesDumpSize="$(echo "${listTables}" | awk -v d="," -v q="'" '{s=(NR==1?s:s d)q $0 q}END{print s }')" + listTablesDump=$(echo "${listTables}" | awk -v d=" " -v q="" '{s=(NR==1?s:s d)q $0 q}END{print s }') + + Log::displayInfo "Calculate dump size for tables ${listTablesDump}" + local remoteDbDumpSize + remoteDbDumpSize=$(echo "${DUMP_SIZE_QUERY}" | envsubst | Database::query dbFromInstance) + if [[ -z "${remoteDbDumpSize}" ]]; then + # could occur with the none profile + remoteDbDumpSize="0" + fi + + # dump db + Log::displayInfo "Dump the database ${fromDbName} (Size:${remoteDbDumpSize}MB) ..." + local dumpSizePvEstimation + dumpSizePvEstimation=$(awk "BEGIN {printf \"%.0f\",${remoteDbDumpSize}/1.5}") + time ( + echo "${dumpHeader}" + Database::dump dbFromInstance "${fromDbName}" "${listTablesDump}" \ + --no-create-info --skip-add-drop-table --single-transaction=TRUE | + pv --progress --size "${dumpSizePvEstimation}m" + echo "${DUMP_FOOTER}" + ) | gzip >"${remoteDbDumpTempFile}" + + Log::displayInfo "Dump structure of the database ${fromDbName} ..." + time ( + echo "${dumpHeader}" + #shellcheck disable=SC2016 + Database::dump dbFromInstance "${fromDbName}" "" \ + --no-data --skip-add-drop-table --single-transaction=TRUE | + sed 's/^CREATE TABLE `/CREATE TABLE IF NOT EXISTS `/g' + echo "${DUMP_FOOTER}" + ) | gzip >"${remoteDbStructureDumpTempFile}" fi + Log::displayInfo "Dump done." + fi - DUMP_HEADER=$(printf "%s\nSET names '%s';\n" "${DUMP_HEADER}" "${CHARACTER_SET}") - - # calculate remote db dump size - LIST_TABLES="$(Database::query dbFromInstance "show tables" "${REMOTE_DB}" | ${PROFILE_COMMAND} | sort)" - LIST_TABLES_DUMP_SIZE="$(echo "${LIST_TABLES}" | awk -v d="," -v q="'" '{s=(NR==1?s:s d)q $0 q}END{print s }')" - LIST_TABLES_DUMP=$(echo "${LIST_TABLES}" | awk -v d=" " -v q="" '{s=(NR==1?s:s d)q $0 q}END{print s }') - Log::displayInfo "Calculate dump size for tables ${LIST_TABLES_DUMP}" - DUMP_SIZE_QUERY="SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS size FROM information_schema.TABLES WHERE table_schema=\"${REMOTE_DB}\"" - DUMP_SIZE_QUERY+=" AND table_name IN(${LIST_TABLES_DUMP_SIZE}, 'dummy') " - DUMP_SIZE_QUERY+=" GROUP BY table_schema" - REMOTE_DB_DUMP_SIZE=$(echo "${DUMP_SIZE_QUERY}" | Database::query dbFromInstance) - if [[ -z "${REMOTE_DB_DUMP_SIZE}" ]]; then - # could occur with the none profile - REMOTE_DB_DUMP_SIZE="0" + # mark dumps as modified now to avoid them to be garbage collected + touch -c -m "${remoteDbDumpTempFile}" || true + touch -c -m "${remoteDbStructureDumpTempFile}" || true + + # TODO Collation and character set should be retrieved from dump files if possible + # shellcheck disable=SC2154 + local targetCollationName="${optionCollationName:-${defaultTargetCollationName}}" + # shellcheck disable=SC2154 + local taregtCharacterSet="${optionCharacterSet:-${defaultTargetCharacterSet}}" + + # shellcheck disable=SC2154 + Log::displayInfo "create target database ${targetDbName} if needed" + #shellcheck disable=SC2016 + Database::query dbTargetDatabase \ + "$(printf 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET "%s" COLLATE "%s"' "${targetDbName}" "${taregtCharacterSet}" "${targetCollationName}")" + + if [[ -z "${optionFromAws}" ]]; then + Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" + Log::displayInfo "Importing remote db '${fromDbName}' to local db '${targetDbName}'" + # shellcheck disable=SC2154 + if [[ "${optionSkipSchema}" = "1" ]]; then + Log::displayInfo "avoid to create db structure" + else + Log::displayInfo "create db structure from ${remoteDbStructureDumpTempFile}" + time ( + pv "${remoteDbStructureDumpTempFile}" | zcat | + Database::query dbTargetDatabase "" "${targetDbName}" + ) fi - - # dump db - Log::displayInfo "Dump the database ${REMOTE_DB} (Size:${REMOTE_DB_DUMP_SIZE}MB) ..." - DUMP_SIZE_PV_ESTIMATION=$(awk "BEGIN {printf \"%.0f\",${REMOTE_DB_DUMP_SIZE}/1.5}") - time ( - echo "${DUMP_HEADER}" - Database::dump dbFromInstance "${REMOTE_DB}" "${LIST_TABLES_DUMP}" \ - --no-create-info --skip-add-drop-table --single-transaction=TRUE | - pv --progress --size "${DUMP_SIZE_PV_ESTIMATION}m" - echo "${DUMP_FOOTER}" - ) | gzip >"${REMOTE_DB_DUMP_TEMP_FILE}" - - Log::displayInfo "Dump structure of the database ${REMOTE_DB} ..." - time ( - echo "${DUMP_HEADER}" - #shellcheck disable=SC2016 - Database::dump dbFromInstance "${REMOTE_DB}" "" \ - --no-data --skip-add-drop-table --single-transaction=TRUE | - sed 's/^CREATE TABLE `/CREATE TABLE IF NOT EXISTS `/g' - echo "${DUMP_FOOTER}" - ) | gzip >"${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" fi - Log::displayInfo "Dump done." -fi - -# mark dumps as modified now to avoid them to be garbage collected -touch -c -m "${REMOTE_DB_DUMP_TEMP_FILE}" || true -touch -c -m "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" || true - -# TODO Collation and character set should be retrieved from dump files if possible -COLLATION_NAME="${COLLATION_NAME:-utf8_general_ci}" -CHARACTER_SET="${CHARACTER_SET:-utf8}" - -Log::displayInfo "create target database ${TARGET_DB} if needed" -#shellcheck disable=SC2016 -Database::query dbTargetDatabase \ - "$(printf 'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET "%s" COLLATE "%s"' "${TARGET_DB}" "${CHARACTER_SET}" "${COLLATION_NAME}")" - -if [[ "${FROM_AWS}" = "1" ]]; then - "${CURRENT_DIR}/dbImportStream" \ - "${REMOTE_DB_DUMP_TEMP_FILE}" \ - "${TARGET_DB}" \ - "${PROFILE_COMMAND}" \ - "${dbTargetDatabase['AUTH_FILE']}" \ - "${CHARACTER_SET}" \ - "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" -else - Database::setQueryOptions dbTargetDatabase "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" - Log::displayInfo "Importing remote db '${REMOTE_DB}' to local db '${TARGET_DB}'" - if [[ "${SKIP_SCHEMA}" = "1" ]]; then - Log::displayInfo "avoid to create db structure" - else - Log::displayInfo "create db structure from ${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" - time ( - pv "${REMOTE_DB_STRUCTURE_DUMP_TEMP_FILE}" | zcat | - Database::query dbTargetDatabase "" "${TARGET_DB}" + Log::displayInfo "import remote to local from file ${remoteDbDumpTempFile}" + local -a dbImportStreamOptions=( + --profile "${optionProfile}" \ + --target-dsn "${optionTargetDsn}" \ + --character-set "${taregtCharacterSet}" \ + ) + if [[ -n "${optionTables:-}" ]]; then + dbImportStreamOptions+=( + --tables "${optionTables}" \ ) fi - - Log::displayInfo "import remote to local from file ${REMOTE_DB_DUMP_TEMP_FILE}" time ( "${CURRENT_DIR}/dbImportStream" \ - "${REMOTE_DB_DUMP_TEMP_FILE}" \ - "${TARGET_DB}" \ - "${PROFILE_COMMAND}" \ - "${dbTargetDatabase['AUTH_FILE']}" \ - "${CHARACTER_SET}" \ - "${dbTargetDatabase['DB_IMPORT_OPTIONS']}" + "${dbImportStreamOptions[@]}" \ + "${remoteDbDumpTempFile}" \ + "${targetDbName}" + ) -fi -# garbage collect db import dumps -File::garbageCollect "${DB_IMPORT_DUMP_DIR}" "${DB_IMPORT_GARBAGE_COLLECT_DAYS:-+30}" || true + # garbage collect db import dumps + File::garbageCollect "${DB_IMPORT_DUMP_DIR}" "${DB_IMPORT_GARBAGE_COLLECT_DAYS:-+30}" || true -Log::displayInfo "Import database duration : $(date -u -d "@${SECONDS}" +"%T")" + Log::displayInfo "Import database duration : $(date -u -d "@${SECONDS}" +"%T")" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/DbImport/dbImportProfile.bats b/src/_binaries/DbImport/dbImportProfile.bats index fd0f8c2b..aa3af708 100755 --- a/src/_binaries/DbImport/dbImportProfile.bats +++ b/src/_binaries/DbImport/dbImportProfile.bats @@ -32,15 +32,15 @@ teardown() { function Database::dbImportProfile::display_help { #@test # shellcheck disable=SC2154 run "${binDir}/dbImportProfile" --help 2>&1 - assert_line --index 0 "Description: generate optimized profiles to be used by dbImport" + assert_line --index 0 "DESCRIPTION: generate optimized profiles to be used by dbImport" run "${binDir}/dbImportProfile" -h 2>&1 - assert_line --index 0 "Description: generate optimized profiles to be used by dbImport" + assert_line --index 0 "DESCRIPTION: generate optimized profiles to be used by dbImport" } function Database::dbImportProfile::fromDbName_not_provided { #@test # shellcheck disable=SC2154 run "${binDir}/dbImportProfile" 2>&1 - assert_output --partial "FATAL - you must provide fromDbName" + assert_output --partial "ERROR - Command dbImportProfile - Argument 'fromDbName' should be provided at least 1 time(s)" assert_failure } @@ -92,7 +92,7 @@ function Database::dbImportProfile::remote_db_not_found { #@test assert_output --partial "FATAL - From Database dbNotFound does not exist !" } -function Database::dbImportProfile::remote_db_fully_functional { #@test +function Database::dbImportProfile::remote_db_fully_functional_default_ratio { #@test stub mysqlshow \ '* * fromDb : echo "Database: fromDb"' stub mysql \ @@ -103,7 +103,7 @@ function Database::dbImportProfile::remote_db_fully_functional { #@test [[ -f "${HOME}/tableSizeQuery.sql" ]] assert_output --partial "Profile generated - 1/3 tables bigger than 70% of max table size (29MB) automatically excluded" - [[ "$(md5sum "${HOME}/tableSizeQuery.sql" | awk '{ print $1 }')" == "$(md5sum "${BATS_TEST_DIRNAME}/testsData/expectedDbImportProfileTableListQuery.sql" | awk '{ print $1 }')" ]] + diff >&3 "${HOME}/tableSizeQuery.sql" "${BATS_TEST_DIRNAME}/testsData/expectedDbImportProfileTableListQuery.sql" [[ -f "${HOME}/.bash-tools/dbImportProfiles/auto_default.local_fromDb.sh" ]] [[ "$(md5sum "${HOME}/.bash-tools/dbImportProfiles/auto_default.local_fromDb.sh" | awk '{ print $1 }')" == "$(md5sum "${BATS_TEST_DIRNAME}/testsData/auto_default.local_fromDb_70.sh" | awk '{ print $1 }')" ]] } diff --git a/src/_binaries/DbImport/dbImportProfile.options.tpl b/src/_binaries/DbImport/dbImportProfile.options.tpl new file mode 100644 index 00000000..49b6d874 --- /dev/null +++ b/src/_binaries/DbImport/dbImportProfile.options.tpl @@ -0,0 +1,119 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="dbImportProfileCommand" +declare help="generate optimized profiles to be used by dbImport" +# shellcheck disable=SC2016 +declare longDescription=''' +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} + +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList}''' +declare defaultFromDsn="default.remote" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo \ + "the name of the profile to write in profiles directory. " \ + "If not provided, the file name pattern will be 'auto__.sh'" \ + )" \ + --variable-type "String" \ + --alt "--profile" \ + --alt "-p" \ + --variable-name "optionProfile" \ + --function-name optionProfileFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo \ + "dsn to use for source database (Default: ${defaultFromDsn})" \ + "if not provided, the file name pattern will be 'auto__.sh'" \ + )" \ + --variable-type "String" \ + --alt "--from-dsn" \ + --alt "-f" \ + --variable-name "optionFromDsn" \ + --function-name optionFromDsnFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo -e "define the ratio to use (0 to 100% - default 70). " \ + "0 means profile will filter out all the tables. " \ + "100 means profile will keep all the tables. " \ + "Eg: 70 means that tables with size(table+index) that are greater that 70% of the max table size will be excluded." \ + )" \ + --variable-type "String" \ + --alt "--ratio" \ + --alt "-r" \ + --variable-name "optionRatio" \ + --function-name optionRatioFunction + + Options::generateArg \ + --help "the name of the source/remote database" \ + --min 1 \ + --max 1 \ + --name "fromDbName" \ + --variable-name "fromDbName" \ + --function-name argumentFromDbNameFunction +) +options+=( + optionProfileFunction + optionFromDsnFunction + optionRatioFunction + argumentFromDbNameFunction + --callback dbImportProfileCommandCallback +) +Options::generateCommand "${options[@]}" +% + +# default values +declare optionProfile="" +declare fromDbName="" # old FROM_DB +declare optionFromDsn="<% ${defaultFromDsn} %>" # old FROM_DSN +declare optionRatio=70 # old RATIO + +# other configuration +declare copyrightBeginYear="2020" +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" + + <% ${commandFunctionName} %> help | envsubst + exit 0 +} + +dbImportProfileCommandCallback() { + if [[ -z "${fromDbName}" ]]; then + Log::fatal "you must provide fromDbName" + fi + + if [[ -z "${optionProfile}" ]]; then + optionProfile="auto_${optionFromDsn}_${fromDbName}.sh" + fi + + if ! [[ "${optionRatio}" =~ ^-?[0-9]+$ ]]; then + Log::fatal "Ratio value should be a number" + fi + + if ((optionRatio < 0 || optionRatio > 100)); then + Log::fatal "Ratio value should be between 0 and 100" + fi +} diff --git a/src/_binaries/DbImport/dbImportProfile.sh b/src/_binaries/DbImport/dbImportProfile.sh index 220f98bc..75df65a1 100755 --- a/src/_binaries/DbImport/dbImportProfile.sh +++ b/src/_binaries/DbImport/dbImportProfile.sh @@ -1,166 +1,79 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImportProfile +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/DbImport/dbImportProfile.options.tpl)" -Assert::expectNonRootUser - -# default values -SCRIPT_NAME=${0##*/} -PROFILE="" -FROM_DB="" -DEFAULT_FROM_DSN="default.remote" -FROM_DSN="${DEFAULT_FROM_DSN}" -RATIO=70 -# jscpd:ignore-start -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" - -showHelp() { - local profilesList="" - local dsnList="" - dsnList="$(Conf::getMergedList "dsn" "env")" - profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" - - cat < - [-p|--profile profileName] - [-f|--from-dsn dsn] - - the name of the source/remote database - -p|--profile profileName the name of the profile to write in ${HOME_PROFILES_DIR} directory - if not provided, the file name pattern will be 'auto__.sh' - -f|--from-dsn dsn dsn to use for source database (Default: ${DEFAULT_FROM_DSN}) - -r|--ratio ratio define the ratio to use (0 to 100% - default 70) - 0 means profile will filter out all the tables - 100 means profile will keep all the tables - eg: 70 means that table size (table+index) > 70%*max table size will be excluded - -${__HELP_TITLE}List of available profiles (default profiles dir ${PROFILES_DIR} can be overridden in home profiles ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} -${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} -${dsnList} - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -} -# jscpd:ignore-end - -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,profile:,from-dsn:,ratio: -o hf:p:r: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" -} - -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - -f | --from-dsn) - shift || true - FROM_DSN="${1:-${DEFAULT_FROM_DSN}}" - ;; - -p | --profile) - shift || true - PROFILE="$1" - ;; - -r | --ratio) - shift || true - RATIO="$1" - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done - -# check dependencies -Assert::commandExists mysql "sudo apt-get install -y mysql-client" -Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" -# additional arguments -shift $((OPTIND - 1)) || true -FROM_DB="$1" -shift || true -if (($# > 0)); then - Log::fatal "too much arguments provided" -fi - -if [[ -z "${FROM_DB}" ]]; then - Log::fatal "you must provide fromDbName" -fi - -if [[ -z "${PROFILE}" ]]; then - PROFILE="auto_${FROM_DSN}_${FROM_DB}.sh" -fi - -if ! [[ "${RATIO}" =~ ^-?[0-9]+$ ]]; then - Log::fatal "Ratio value should be a number" -fi - -if ((RATIO < 0 || RATIO > 100)); then - Log::fatal "Ratio value should be between 0 and 100" -fi - -# create db instance -declare -Agx dbFromInstance - -Database::newInstance dbFromInstance "${FROM_DSN}" -Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" -Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" - -# check if from db exists -Database::ifDbExists dbFromInstance "${FROM_DB}" || { - Log::fatal "From Database ${FROM_DB} does not exist !" -} +# shellcheck disable=SC2154 read -r -d '' QUERY <"${HOME_PROFILES_DIR}/${PROFILE}" -Log::displayInfo "File saved in '${HOME_PROFILES_DIR}/${PROFILE}'" +dbImportProfileCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" + + # create db instance + declare -Agx dbFromInstance + + # shellcheck disable=SC2154 + Database::newInstance dbFromInstance "${optionFromDsn}" + Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + + # check if from db exists + # shellcheck disable=SC2154 + Database::ifDbExists dbFromInstance "${fromDbName}" || { + Log::fatal "From Database ${fromDbName} does not exist !" + } + local tableList + tableList="$(Database::query dbFromInstance "${QUERY//@DB@/${fromDbName}}" "information_schema")" + # first table is the biggest one + local maxTableSize + maxTableSize="$(echo "${tableList}" | head -1 | awk -F ' ' '{print $2}')" + ( + echo "#!/usr/bin/env bash" + echo + echo "# cat represents the whole list of tables" + echo "cat |" + local -i excludedTablesCount + ((excludedTablesCount = 0)) || true + local tableSize + local tableName + while IFS="" read -r line || [[ -n "${line}" ]]; do + tableSize="$(echo "${line}" | awk -F ' ' '{print $2}')" + tableName="$(echo "${line}" | awk -F ' ' '{print $1}')" + # shellcheck disable=SC2154 + if ((tableSize < maxTableSize * optionRatio / 100)); then + echo -n '#' + else + excludedTablesCount=$((excludedTablesCount + 1)) + fi + echo " grep -v '^${tableName}$' | # table size ${tableSize}MB" + done < <(echo "${tableList}") + echo "cat" + tablesCount="$(echo "${tableList}" | wc -l)" + Log::displayInfo "Profile generated - ${excludedTablesCount}/${tablesCount} tables bigger than ${optionRatio}% of max table size (${maxTableSize}MB) automatically excluded" + ) >"${HOME_PROFILES_DIR}/${optionProfile}" + + Log::displayInfo "File saved in '${HOME_PROFILES_DIR}/${optionProfile}'" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/DbImport/dbImportStream.options.tpl b/src/_binaries/DbImport/dbImportStream.options.tpl new file mode 100644 index 00000000..0ba0d5e5 --- /dev/null +++ b/src/_binaries/DbImport/dbImportStream.options.tpl @@ -0,0 +1,82 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="dbImportStreamCommand" +declare help="stream tar.gz file or gz file through mysql" +# shellcheck disable=SC2016 +declare longDescription=''' +${__HELP_TITLE}Default profiles directory:${__HELP_NORMAL} +${PROFILES_DIR-configuration error} + +${__HELP_TITLE}User profiles directory:${__HELP_NORMAL} +${HOME_PROFILES_DIR-configuration error} +Allows to override profiles defined in "Default profiles directory" + +${__HELP_TITLE}List of available profiles:${__HELP_NORMAL} +${profilesList} + +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList}''' +declare defaultFromDsn="default.remote" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.profile.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.mysql.target.tpl)" + +% +# shellcheck source=/dev/null +source <( + + Options::generateArg \ + --help "the of the file that will be streamed through mysql" \ + --min 1 \ + --max 1 \ + --name "argDumpFile" \ + --variable-name "argDumpFile" \ + --function-name argDumpFileFunction + + Options::generateArg \ + --help "the name of the mysql target database" \ + --min 1 \ + --max 1 \ + --name "argTargetDbName" \ + --variable-name "argTargetDbName" \ + --function-name argTargetDbNameFunction + +) +options+=( + argDumpFileFunction + argTargetDbNameFunction + --callback dbImportStreamCommandCallback +) +Options::generateCommand "${options[@]}" +% + +# default values +declare optionProfile="" +declare argTargetDbName="" + +# other configuration +declare copyrightBeginYear="2020" +declare PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/dbImportProfiles" +declare HOME_PROFILES_DIR="${HOME}/.bash-tools/dbImportProfiles" + + +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" + + <% ${commandFunctionName} %> help | envsubst + exit 0 +} + +dbImportStreamCommandCallback() { + if [[ -z "${argTargetDbName}" ]]; then + Log::fatal "you must provide argTargetDbName" + fi + if [[ ! -f "${argDumpFile}" ]]; then + Log::fatal "invalid argDumpFile provided - file does not exist" + fi +} diff --git a/src/_binaries/DbImport/dbImportStream.sh b/src/_binaries/DbImport/dbImportStream.sh index bc0b3253..30767136 100755 --- a/src/_binaries/DbImport/dbImportStream.sh +++ b/src/_binaries/DbImport/dbImportStream.sh @@ -1,51 +1,66 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/dbImportStream +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE +# shellcheck disable=SC2154 -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" - -HELP="$( - cat < [characterSet] [dbImportOptions] -characterSet: default value utf8 -dbImportOptions: default value empty - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -)" -Args::defaultHelp "${HELP}" "$@" - -DUMP_FILE="$1" -DB_NAME="$2" -PROFILE_COMMAND="${3}" -MYSQL_AUTH_FILE="${4}" -CHARACTER_SET="${5:-utf8}" -DB_IMPORT_OPTIONS="${6:-}" - -if [[ -z "${PROFILE_COMMAND}" ]]; then - Log::fatal "You should provide a profile command" -fi +.INCLUDE "$(dynamicTemplateDir _binaries/DbImport/dbImportStream.options.tpl)" awkScript="$( cat <<'EOF' .INCLUDE "$(dynamicSrcFile "_binaries/DbImport/dbImportStream.awk")" EOF )" -# shellcheck disable=2086 -( - if [[ "${DUMP_FILE}" == *tar.gz ]]; then - tar xOfz "${DUMP_FILE}" - elif [[ "${DUMP_FILE}" == *.gz ]]; then - zcat "${DUMP_FILE}" - fi - # zcat will continue to write to stdout whereas awk has finished if table has been found - # we detect this case because zcat will return code 141 because pipe closed - status=$? - if [[ "${status}" -eq "141" ]]; then true; else exit "${status}"; fi -) | awk \ - -v PROFILE_COMMAND="${PROFILE_COMMAND}" \ - -v CHARACTER_SET="${CHARACTER_SET}" \ - --source "${awkScript}" \ - - | mysql --defaults-extra-file="${MYSQL_AUTH_FILE}" ${DB_IMPORT_OPTIONS} "${DB_NAME}" || exit $? + +dbImportStreamCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +# @require Linux::requireExecutedAsUser +run() { + + # check dependencies + Assert::commandExists mysql "sudo apt-get install -y mysql-client" + Assert::commandExists gawk "sudo apt-get install -y gawk" + Assert::commandExists awk "sudo apt-get install -y gawk" + Version::checkMinimal "gawk" "--version" "5.0.1" + + # create db instances + declare -Agx dbTargetInstance + + # shellcheck disable=SC2154 + Database::newInstance dbTargetInstance "${optionTargetDsn}" + Database::setQueryOptions dbTargetInstance "${dbTargetDatabase[QUERY_OPTIONS]} --connect-timeout=5" + Log::displayInfo "Using target dsn ${dbTargetInstance['DSN_FILE']}" + + initializeDefaultTargetMysqlOptions dbTargetInstance "${argTargetDbName}" + + # TODO character set should be retrieved from dump files if possible + # shellcheck disable=SC2154 + declare remoteCharacterSet="${optionCharacterSet:-${defaultRemoteCharacterSet}}" + + # shellcheck disable=2086 + ( + if [[ "${argDumpFile}" =~ \.tar.gz$ ]]; then + tar xOfz "${argDumpFile}" + elif [[ "${argDumpFile}" =~ \.gz$ ]]; then + zcat "${argDumpFile}" + fi + # zcat will continue to write to stdout whereas awk has finished if table has been found + # we detect this case because zcat will return code 141 because pipe closed + status=$? + if [[ "${status}" -eq "141" ]]; then true; else exit "${status}"; fi + ) | + awk \ + -v PROFILE_COMMAND="${profileCommand}" \ + -v CHARACTER_SET="${remoteCharacterSet}" \ + --source "${awkScript}" \ + - | mysql \ + "--defaults-extra-file=${dbTargetInstance['AUTH_FILE']}" \ + ${dbTargetInstance['DB_IMPORT_OPTIONS']} \ + "${argTargetDbName}" || exit $? +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/DbImport/dumpSizeQuery.sql b/src/_binaries/DbImport/dumpSizeQuery.sql new file mode 100644 index 00000000..abd09352 --- /dev/null +++ b/src/_binaries/DbImport/dumpSizeQuery.sql @@ -0,0 +1,4 @@ +SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS size +FROM information_schema.TABLES WHERE table_schema='${fromDbName}' +AND table_name IN(${listTablesDumpSize}, 'dummy') +GROUP BY table_schema diff --git a/src/_binaries/Docker/cli.bats b/src/_binaries/Docker/cli.bats index 7f8be649..c072d677 100755 --- a/src/_binaries/Docker/cli.bats +++ b/src/_binaries/Docker/cli.bats @@ -30,7 +30,7 @@ function Docker::cli::display_help { #@test # shellcheck disable=SC2154 run "${binDir}/cli" --help 2>&1 assert_success - assert_line --index 0 "Description: easy connection to docker container" + assert_line --index 0 "DESCRIPTION: easy connection to docker container" } function Docker::cli::without_any_parameter_connects_to_default_container { #@test diff --git a/src/_binaries/Docker/cli.options.tpl b/src/_binaries/Docker/cli.options.tpl new file mode 100644 index 00000000..355c9505 --- /dev/null +++ b/src/_binaries/Docker/cli.options.tpl @@ -0,0 +1,127 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="cliCommand" +declare help="easy connection to docker container" +# shellcheck disable=SC2016 +declare longDescription=''' +${__HELP_TITLE}AVAILABLE PROFILES (from ${PROFILES_DIR})${__HELP_NORMAL} +This list can be overridden in ${HOME_PROFILES_DIR} + +${profilesList} + +${__HELP_TITLE}AVAILABLE CONTAINERS:${__HELP_NORMAL} +${containers} + +${__HELP_TITLE}EXAMPLES:${__HELP_EXAMPLE} + to connect to mysql container in bash mode with user mysql + ${SCRIPT_NAME} mysql mysql "//bin/bash" + to connect to web container with user root + ${SCRIPT_NAME} web root +${__HELP_NORMAL} + +${__HELP_TITLE}CREATE NEW PROFILE:${__HELP_NORMAL} +You can create new profiles in ${HOME_PROFILES_DIR}. +This script will be called with the +arguments ${__HELP_OPTION_COLOR}userArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}containerArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}commandArg${__HELP_NORMAL} +The script has to compute the following +variables ${__HELP_OPTION_COLOR}finalUserArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}finalContainerArg${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}finalCommandArg${__HELP_NORMAL} +''' +declare defaultUserArg="root" +declare -a defaultCommandArg=("//bin/sh") +% +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +% +# shellcheck source=/dev/null +source <( + containerArgHelpCallback() { :; } + Options::generateArg \ + --help containerArgHelpCallback \ + --min 0 \ + --max 1 \ + --name "container" \ + --variable-name "containerArg" \ + --function-name containerArgFunction + + userArgHelpCallback() { :; } + Options::generateArg \ + --help userArgHelpCallback \ + --min 0 \ + --max 1 \ + --name "user" \ + --variable-name "userArg" \ + --function-name userArgFunction + + commandArgHelpCallback() { :; } + Options::generateArg \ + --help commandArgHelpCallback \ + --variable-name "commandArg" \ + --min 0 \ + --name "commandArg" \ + --function-name commandArgFunction +) +options+=( + --unknown-option-callback unknownOption + --unknown-argument-callback unknownOption + containerArgFunction + userArgFunction + commandArgFunction +) +Options::generateCommand "${options[@]}" +% + +# default values +declare containerArg="default" +declare finalUserArg="<% ${defaultUserArg} %>" +declare finalCommandArg=("<% ${defaultCommandArg[@]} %>") +declare copyrightBeginYear="2020" + +# constants +PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/cliProfiles" +HOME_PROFILES_DIR="${HOME}/.bash-tools/cliProfiles" + +containerArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "container should be the name of a profile from profile list," + echo "check containers list below." $'\n' + echo "If not provided, it will load the container specified in default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" $'\n' + echo "Default container: ${__HELP_OPTION_COLOR}${finalContainerArg}${__HELP_NORMAL}" +} + +userArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "user to connect on this container" $'\n' + echo "Default user: ${__HELP_OPTION_COLOR}${finalUserArg}${__HELP_NORMAL}" $'\n' + echo " loaded from profile selected as first arg" $'\n' + echo " or deduced from default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" + echo "if first arg is not a profile" +} + +commandArgHelpCallback() { + Conf::load "cliProfiles" "default" + echo "The command to execute" $'\n' + echo "Default command: ${__HELP_OPTION_COLOR}${finalCommandArg[*]}${__HELP_NORMAL}" $'\n' + echo " loaded from profile selected as first arg" $'\n' + echo " or deduced from default configuration." $'\n' + echo "Default configuration: ${__HELP_OPTION_COLOR}${containerArg}${__HELP_NORMAL}" + echo "if first arg is not a profile" +} + +optionHelpCallback() { + local containers + # shellcheck disable=SC2046 + containers="$(Array::wrap ", " 80 0 $(docker ps --format '{{.Names}}'))" + local profilesList="" + Conf::load "cliProfiles" "default" + + profilesList="$(Conf::getMergedList "cliProfiles" ".sh" " - " || true)" + + <% ${commandFunctionName} %> help | envsubst + exit 0 +} + +# shellcheck disable=SC2317 # if function is overridden +unknownOption() { + commandArg+=("$1") +} diff --git a/src/_binaries/Docker/cli.sh b/src/_binaries/Docker/cli.sh index 951d3e09..43b29ff7 100755 --- a/src/_binaries/Docker/cli.sh +++ b/src/_binaries/Docker/cli.sh @@ -1,138 +1,74 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/cli +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Docker/cli.options.tpl)" -Assert::expectNonRootUser +cliCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -SCRIPT_NAME=${0##*/} -PROFILES_DIR="${BASH_TOOLS_ROOT_DIR}/conf/cliProfiles" -HOME_PROFILES_DIR="${HOME}/.bash-tools/cliProfiles" +run() { -showHelp() { - local containers - containers=$(docker ps --format '{{.Names}}' | sed -E 's/[^-]+-(.*)/\1/' | paste -sd "," -) - local profilesList="" - Conf::load "cliProfiles" "default" - - profilesList="$(Conf::getMergedList "cliProfiles" ".sh" || true)" - - cat <] [user] [command] - - : container should be one of these values (provided by 'docker ps'): - ${containers} - if not provided, it will load the container specified in default configuration (${finalContainerArg}) - -${__HELP_TITLE}examples:${__HELP_NORMAL} - to connect to mysql container in bash mode with user mysql - ${SCRIPT_NAME} mysql mysql "//bin/bash" - to connect to web container with user root - ${SCRIPT_NAME} web root -you can override these mappings by providing your own profile in ${CLI_PROFILE_HOME} - -This script will be executed with the variables userArg containerArg commandArg set as specified in command line -and should provide value for the following variables finalUserArg finalContainerArg finalCommandArg - -${__HELP_TITLE}List of available profiles (from ${PROFILES_DIR} and can be overridden in ${HOME_PROFILES_DIR}):${__HELP_NORMAL} -${profilesList} + # Internal function that can be used in conf profiles to load the dsn file + loadDsn() { + local dsn="$1" + local dsnFile + dsnFile="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" + Database::checkDsnFile "${dsnFile}" + # shellcheck source=/conf/dsn/default.local.env + # shellcheck disable=SC1091 + source "${dsnFile}" + } + export -f loadDsn -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -} + # check dependencies + Assert::commandExists docker "check https://docs.docker.com/engine/install/ubuntu/" -# Internal function that can be used in conf profiles to load the dsn file -loadDsn() { - local dsn="$1" - local dsnFile - dsnFile="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" - Database::checkDsnFile "${dsnFile}" - # shellcheck source=/conf/dsn/default.local.env - # shellcheck disable=SC1091 - source "${dsnFile}" -} + # load default conf file + Conf::load "cliProfiles" "default" -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help -o h -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" + # try to load config file associated to container if provided + if [[ -n "${containerArg}" ]]; then + Conf::load "cliProfiles" "${containerArg}" || { + # conf file not existing fallback to provided args or to default ones if not provided + finalContainerArg="${containerArg}" + finalUserArg=${userArg:-${finalUserArg}} + finalCommandArg=("${commandArg[@]:-${finalCommandArg[@]}}") + } + fi + + declare -a cmd=() + if Assert::windows; then + # open tty for git bash + cmd+=(winpty) + fi + INTERACTIVE_MODE="-i" + if ! read -r -t 0; then + # command is not piped or TTY not available + INTERACTIVE_MODE+="t" + fi + + cmd+=(docker) + cmd+=(exec) + cmd+=("${INTERACTIVE_MODE}") + # ensure column/lines will be updated upon terminal resize + cmd+=(-e) + cmd+=("COLUMNS=$(tput cols)") + cmd+=(-e) + cmd+=("LINES=$(tput lines)") + + cmd+=("--user=${finalUserArg}") + cmd+=("${finalContainerArg}") + cmd+=("${finalCommandArg[@]}") + if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "0" ]]; then + (echo >&2 MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}") + fi + MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}" } -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done - -declare containerArg="$1" -declare userArg -declare -a commandArg -if shift; then - userArg="$1" -fi -if shift; then - commandArg=("$@") -fi - -# check dependencies -Assert::commandExists docker "check https://docs.docker.com/engine/install/ubuntu/" - -# load default conf file -Conf::load "cliProfiles" "default" -# try to load config file associated to container if provided -if [[ -n "${containerArg}" ]]; then - Conf::load "cliProfiles" "${containerArg}" || { - # conf file not existing fallback to provided args or to default ones if not provided - finalContainerArg="${containerArg}" - finalUserArg=${userArg:-${finalUserArg}} - finalCommandArg=${commandArg:-${finalCommandArg}} - } +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi - -declare -a cmd=() -if Assert::windows; then - # open tty for git bash - cmd+=(winpty) -fi -INTERACTIVE_MODE="-i" -if ! read -r -t 0; then - # command is not piped or TTY not available - INTERACTIVE_MODE+="t" -fi - -cmd+=(docker) -cmd+=(exec) -cmd+=("${INTERACTIVE_MODE}") -# ensure column/lines will be updated upon terminal resize -cmd+=(-e) -cmd+=("COLUMNS=$(tput cols)") -cmd+=(-e) -cmd+=("LINES=$(tput lines)") - -cmd+=("--user=${finalUserArg}") -cmd+=("${finalContainerArg}") -cmd+=("${finalCommandArg[@]}") -(echo >&2 MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}") -MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${cmd[@]}" diff --git a/src/_binaries/Git/gitIsAncestorOf.options.tpl b/src/_binaries/Git/gitIsAncestorOf.options.tpl new file mode 100644 index 00000000..c859a30f --- /dev/null +++ b/src/_binaries/Git/gitIsAncestorOf.options.tpl @@ -0,0 +1,43 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="gitIsAncestorOfCommand" +declare help="check if commit is inside a given branch" +declare longDescription=''' +${__HELP_TITLE}EXIT CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}1${__HELP_NORMAL}: if commit does not exists +${__HELP_OPTION_COLOR}2${__HELP_NORMAL}: if commit is not included in given branch +''' +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + containerArgHelpCallback() { :; } + Options::generateArg \ + --help "the branch in which the commit will be searched" \ + --min 1 \ + --max 1 \ + --name "claimedBranch" \ + --variable-name "claimedBranchArg" \ + --function-name claimedBranchArgFunction + + userArgHelpCallback() { :; } + Options::generateArg \ + --help "the commit oid to check" \ + --min 1 \ + --max 1 \ + --name "commit" \ + --variable-name "commitArg" \ + --function-name commitArgFunction +) +options+=( + claimedBranchArgFunction + commitArgFunction +) +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +declare claimedBranchArg="" +declare commitArg="" diff --git a/src/_binaries/Git/gitIsAncestorOf.sh b/src/_binaries/Git/gitIsAncestorOf.sh index e059032c..6c13d3c2 100755 --- a/src/_binaries/Git/gitIsAncestorOf.sh +++ b/src/_binaries/Git/gitIsAncestorOf.sh @@ -1,27 +1,30 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitIsAncestorOf +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Git/gitIsAncestorOf.options.tpl)" -HELP="$( - cat < -show an error if commit is not an ancestor of branch +gitIsAncestorOfCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -)" -Args::defaultHelp "${HELP}" "$@" +# @require Linux::requireExecutedAsUser +run() { -if [[ "$#" != "2" ]]; then - Log::fatal "${SCRIPT_NAME}: invalid arguments" -fi + if ! git cat-file -t "${commitArg}" &>/dev/null; then + Log::displayError "Commit ${commitArg} does not exists at all" + exit 1 + fi -claimedBranch="$1" -commit="$2" + # shellcheck disable=SC2154 + merge_base="$(git merge-base "${commitArg}" "${claimedBranchArg}")" + if [[ -z "${merge_base}" || "${merge_base}" != "$(git rev-parse --verify "${commitArg}")" ]]; then + Log::displayError "Commit ${commitArg} is not an ancestor of branch ${claimedBranchArg}" + exit 2 + fi +} -merge_base="$(git merge-base "${commit}" "${claimedBranch}")" -if [[ -z "${merge_base}" || "${merge_base}" != "$(git rev-parse --verify "${commit}")" ]]; then - Log::fatal "${commit} is not an ancestor of ${claimedBranch}" +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi diff --git a/src/_binaries/Git/gitIsBranch.options.tpl b/src/_binaries/Git/gitIsBranch.options.tpl new file mode 100644 index 00000000..d6873ac9 --- /dev/null +++ b/src/_binaries/Git/gitIsBranch.options.tpl @@ -0,0 +1,28 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="gitIsBranchCommand" +declare help="show an error if branchName is not a known branch" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + containerArgHelpCallback() { :; } + Options::generateArg \ + --help "the branch name to check" \ + --min 1 \ + --max 1 \ + --name "branchName" \ + --variable-name "branchNameArg" \ + --function-name branchNameArgFunction + +) +options+=( + branchNameArgFunction +) +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +declare branchNameArg="" diff --git a/src/_binaries/Git/gitIsBranch.sh b/src/_binaries/Git/gitIsBranch.sh index 1acade8d..224b674a 100755 --- a/src/_binaries/Git/gitIsBranch.sh +++ b/src/_binaries/Git/gitIsBranch.sh @@ -1,25 +1,23 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitIsBranch +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Git/gitIsBranch.options.tpl)" -HELP="$( - cat < -show an error if branchName is not a known branch +gitIsBranchCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -)" -Args::defaultHelp "${HELP}" "$@" +# @require Linux::requireExecutedAsUser +run() { + # check various branch hierarchies, adjust as needed + # shellcheck disable=SC2154 + git show-ref --verify refs/heads/"${branchNameArg}" || + git show-ref --verify refs/remotes/"${branchNameArg}" || + Log::fatal "not a branch name: ${branchNameArg}" +} -if [[ "$#" != "1" ]]; then - Log::fatal "$0: invalid arguments" +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi -branch="$1" - -# check various branch hierarchies, adjust as needed -git show-ref --verify refs/heads/"${branch}" || - git show-ref --verify refs/remotes/"${branch}" || - Log::fatal "not a branch name: ${branch}" diff --git a/src/_binaries/Git/gitRenameBranch.bats b/src/_binaries/Git/gitRenameBranch.bats index 7471fd93..f5d47df4 100755 --- a/src/_binaries/Git/gitRenameBranch.bats +++ b/src/_binaries/Git/gitRenameBranch.bats @@ -5,9 +5,6 @@ source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" load "${FRAMEWORK_ROOT_DIR}/src/_standalone/Bats/assert_lines_count.sh" -# shellcheck source=vendor/bash-tools-framework/src/Env/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Env/load.sh" || exit 1 - setup() { export TMPDIR="${BATS_TEST_TMPDIR}" @@ -28,6 +25,7 @@ setup() { git checkout -b oldBranch master export BASH_FRAMEWORK_ENV_FILEPATH="${BATS_TEST_DIRNAME}/testsData/.env" + export INTERACTIVE=1 } teardown() { @@ -36,79 +34,63 @@ teardown() { } function Git::gitRenameBranch::display_help { #@test + export INTERACTIVE=0 run "${binDir}/gitRenameBranch" --help 2>&1 assert_success - assert_line --index 0 "Description: rename git local branch, use options to push new branch and delete old branch" + assert_line --index 0 "DESCRIPTION: rename git local branch, push new branch and delete old branch" } function Git::gitRenameBranch::not_a_git_repository { #@test cd "${BATS_TEST_TMPDIR}/gitRepoFake" || exit 1 run "${binDir}/gitRenameBranch" "test" --verbose 2>&1 - assert_failure + assert_failure 1 + # shellcheck disable=SC2154 - assert_output "FATAL - not a git repository (or any of the parent directories)" + assert_output --partial "ERROR - not a git repository (or any of the parent directories)" } function Git::gitRenameBranch::master_branch_not_supported { #@test git checkout master - run "${binDir}/gitRenameBranch" --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + run "${binDir}/gitRenameBranch" --verbose master 2>&1 + assert_failure 3 + assert_output --partial "ERROR - master/main branch not supported by this command, please do it manually" } function Git::gitRenameBranch::main_branch_not_supported { #@test git checkout main - run "${binDir}/gitRenameBranch" --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + run "${binDir}/gitRenameBranch" --verbose main 2>&1 + assert_failure 3 + assert_output --partial "ERROR - master/main branch not supported by this command, please do it manually" } function Git::gitRenameBranch::master_branch_not_supported_as_argument { #@test run "${binDir}/gitRenameBranch" master --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + assert_failure 3 + assert_output --partial "ERROR - master/main branch not supported by this command, please do it manually" } function Git::gitRenameBranch::main_branch_not_supported_as_argument { #@test run "${binDir}/gitRenameBranch" main --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + assert_failure 3 + assert_output --partial "ERROR - master/main branch not supported by this command, please do it manually" } function Git::gitRenameBranch::new_branch_name_not_provided { #@test run "${binDir}/gitRenameBranch" --verbose 2>&1 - assert_failure - assert_output "FATAL - new branch name not provided" -} - -function Git::gitRenameBranch::branch_not_provided { #@test - run "${binDir}/gitRenameBranch" --verbose 2>&1 - assert_failure - assert_output "FATAL - new branch name not provided" -} - -function Git::gitRenameBranch::branch_master_provided { #@test - run "${binDir}/gitRenameBranch" master --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" -} - -function Git::gitRenameBranch::branch_main_provided { #@test - run "${binDir}/gitRenameBranch" main --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + assert_failure 1 + assert_output --partial "ERROR - Command gitRenameBranch - Argument 'newBranchName' should be provided at least 1 time(s)" } function Git::gitRenameBranch::branch_master_provided_as_oldBranch { #@test run "${binDir}/gitRenameBranch" newBranch master --verbose 2>&1 - assert_failure - assert_output "FATAL - master/main branch not supported by this command, please do it manually" + assert_failure 3 + assert_output --partial "ERROR - master/main branch not supported by this command, please do it manually" } function Git::gitRenameBranch::too_much_parameters { #@test - run "${binDir}/gitRenameBranch" newBranch oldBranch tooMuch --verbose 2>&1 + run "${binDir}/gitRenameBranch" newBranch oldBranch tooMuch 2>&1 assert_failure - assert_output "FATAL - too much arguments provided" + assert_output --partial "ERROR - Command gitRenameBranch - Argument - too much arguments provided: tooMuch" } function Git::gitRenameBranch::rename_local_and_push_branch { #@test @@ -118,11 +100,7 @@ function Git::gitRenameBranch::rename_local_and_push_branch { #@test 'branch -m oldName newBranch : exit 0' \ 'push --set-upstream origin newBranch : exit 0' - testRename5() { - # shellcheck disable=SC2317 - echo -n 'y' | "${binDir}/gitRenameBranch" newBranch --push --verbose 2>&1 - } - run testRename5 + run "${binDir}/gitRenameBranch" newBranch --push --verbose 2>&1 <<< 'y' assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" @@ -138,11 +116,7 @@ function Git::gitRenameBranch::rename_local_push_delete_remote_branch { #@test 'push origin :oldName : exit 0' \ 'push --set-upstream origin newBranch : exit 0' - testRename() { - # shellcheck disable=SC2317 - echo -n 'yy' | "${binDir}/gitRenameBranch" newBranch --push --delete --verbose 2>&1 - } - run testRename + run "${binDir}/gitRenameBranch" newBranch --push --delete --verbose 2>&1 <<< 'yy' assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" assert_line -n 1 --partial "INFO - Removing eventual old remote branch oldName" @@ -156,12 +130,7 @@ function Git::gitRenameBranch::rename_local_and_delete_remote_branch { #@test 'branch -m oldName newBranch : exit 0' \ 'push origin :oldName : exit 0' - testRename4() { - # shellcheck disable=SC2317 - echo -n 'y' | "${binDir}/gitRenameBranch" newBranch oldName --delete --verbose 2>&1 - } - - run testRename4 + run "${binDir}/gitRenameBranch" newBranch oldName --delete --verbose 2>&1 <<< 'y' assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" @@ -176,11 +145,7 @@ function Git::gitRenameBranch::rename_local_and_delete_remote_branch_without_old 'branch -m oldName newBranch : exit 0' \ 'push origin :oldName : exit 0' - testRename6() { - # shellcheck disable=SC2317 - echo -n 'y' | "${binDir}/gitRenameBranch" newBranch --delete --verbose 2>&1 - } - run testRename6 + run "${binDir}/gitRenameBranch" newBranch --delete --verbose 2>&1 <<< 'y' assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" @@ -195,11 +160,7 @@ function Git::gitRenameBranch::rename_local_and_push_branch_assume_yes { #@test 'branch -m oldName newBranch : exit 0' \ 'push --set-upstream origin newBranch : exit 0' - testRename7() { - # shellcheck disable=SC2317 - "${binDir}/gitRenameBranch" newBranch --push --assume-yes --verbose 2>&1 - } - run testRename7 + run "${binDir}/gitRenameBranch" newBranch --push --assume-yes --verbose 2>&1 assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" @@ -215,11 +176,7 @@ function Git::gitRenameBranch::rename_local_push_delete_remote_branch_assume_yes 'push origin :oldName : exit 0' \ 'push --set-upstream origin newBranch : exit 0' - testRename8() { - # shellcheck disable=SC2317 - echo -n 'yy' | "${binDir}/gitRenameBranch" newBranch --push --delete --assume-yes --verbose 2>&1 - } - run testRename8 + run "${binDir}/gitRenameBranch" newBranch --push --delete --assume-yes --verbose 2>&1 <<< 'yy' assert_success assert_line -n 0 --partial "INFO - Renaming branch locally from oldName to newBranch" @@ -234,11 +191,7 @@ function Git::gitRenameBranch::rename_local_and_delete_remote_branch_assume_yes 'branch -m oldName newBranch : echo "git branch -m oldName newBranch"' \ 'push origin :oldName : echo "git push origin :oldName"' - runRename3() { - # shellcheck disable=SC2317 - echo -n 'y' | "${binDir}/gitRenameBranch" newBranch oldName --delete --assume-yes --verbose 2>&1 - } - run runRename3 + run "${binDir}/gitRenameBranch" newBranch oldName --delete --assume-yes --verbose 2>&1 <<< 'y' assert_success assert_lines_count 4 @@ -253,11 +206,7 @@ function Git::gitRenameBranch::rename_local_and_delete_remote_branch_without_old 'branch -m oldName newBranch : echo "git branch -m oldName newBranch"' \ 'push origin :oldName : echo "git push origin :oldName"' - runRename2() { - # shellcheck disable=SC2317 - echo -n 'y' | "${binDir}/gitRenameBranch" newBranch --delete --assume-yes --verbose 2>&1 - } - run runRename2 + run "${binDir}/gitRenameBranch" newBranch --delete --assume-yes --verbose 2>&1 <<< 'y' assert_success assert_lines_count 4 diff --git a/src/_binaries/Git/gitRenameBranch.options.tpl b/src/_binaries/Git/gitRenameBranch.options.tpl new file mode 100644 index 00000000..bb87206d --- /dev/null +++ b/src/_binaries/Git/gitRenameBranch.options.tpl @@ -0,0 +1,95 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="gitRenameBranchCommand" +declare help="rename git local branch, push new branch and delete old branch" +declare longDescription=''' +${__HELP_TITLE}EXIT CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}1${__HELP_NORMAL} : if current directory is not a git repository + or if invalid or missing arguments +${__HELP_OPTION_COLOR}2${__HELP_NORMAL} : if impossible to compute current branch name +${__HELP_OPTION_COLOR}3${__HELP_NORMAL} : master/main branch not supported by this command, + please do it manually +${__HELP_OPTION_COLOR}5${__HELP_NORMAL} : New and old branch names are the same +${__HELP_OPTION_COLOR}6${__HELP_NORMAL} : You can use this tool in non interactive mode only + if --assume-yes option is provided +${__HELP_OPTION_COLOR}7${__HELP_NORMAL} : if failed to rename local branch +${__HELP_OPTION_COLOR}8${__HELP_NORMAL} : if remote branch deletion failed +${__HELP_OPTION_COLOR}9${__HELP_NORMAL} : if failed to push the new branch''' +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + Options::generateArg \ + --help "the branch name to check" \ + --min 1 \ + --max 1 \ + --name "newBranchName" \ + --variable-name "newBranchNameArg" \ + --function-name newBranchNameArgFunction + + Options::generateArg \ + --help "the name of the old branch if not current one" \ + --min 0 \ + --max 1 \ + --name "oldBranchName" \ + --variable-name "oldBranchNameArg" \ + --function-name oldBranchNameArgFunction + + assumeYesHelpCallback() { :; } + # shellcheck disable=SC2116 + Options::generateOption \ + --help assumeYesHelpCallback \ + --alt "--assume-yes" \ + --alt "--yes" \ + --alt "-y" \ + --variable-name "optionAssumeYes" \ + --function-name optionAssumeYesFunction + + Options::generateOption \ + --help "push the new branch" \ + --alt "--push" \ + --alt "-p" \ + --variable-name "optionPush" \ + --function-name optionPushFunction + + Options::generateOption \ + --help "delete the old remote branch" \ + --alt "--delete" \ + --alt "-d" \ + --variable-name "optionDelete" \ + --function-name optionDeleteFunction +) +options+=( + newBranchNameArgFunction + oldBranchNameArgFunction + optionAssumeYesFunction + optionPushFunction + optionDeleteFunction + --callback commandCallback +) +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" + +#default values +declare optionPush="0" +declare optionDelete="0" +declare optionAssumeYes="0" +declare newBranchNameArg="" +declare oldBranchNameArg="" + +assumeYesHelpCallback() { + echo "do not ask for confirmation (use with caution)" $'\n' + echo ' Automatic yes to prompts; assume "y" as answer to all prompts' $'\n' + echo ' and run non-interactively.' +} + +commandCallback() { + if ! Assert::tty && [[ "${optionAssumeYes}" != "1" ]]; then + Log::displayError "You can use this tool in non interactive mode only if --assume-yes option is provided" + exit 6 + fi +} diff --git a/src/_binaries/Git/gitRenameBranch.sh b/src/_binaries/Git/gitRenameBranch.sh index 89859ba6..b08a71fa 100755 --- a/src/_binaries/Git/gitRenameBranch.sh +++ b/src/_binaries/Git/gitRenameBranch.sh @@ -1,121 +1,81 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/gitRenameBranch +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE +# shellcheck disable=SC2154 -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Git/gitRenameBranch.options.tpl)" -#default values -PUSH="0" -DELETE="0" -INTERACTIVE="1" +gitRenameBranchCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -# Usage info -showHelp() { - cat < [] [--push|-p] [--delete|-d] [--assume-yes|-yes|-y] - --help,-h prints this help and exits - -y, --yes, --assume-yes do not ask for confirmation (use with caution) - Automatic yes to prompts; assume "y" as answer to all prompts - and run non-interactively. - --push,-p push new branch - --delete,-d delete old remote branch - the new branch name to give to current branch - (optional) the name of the old branch if not current one - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -} - -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt -l help,push,delete,yes,assume-yes -o hpdy -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" -} +# @require Linux::requireExecutedAsUser +run() { + local -a cmd=() + if ! git rev-parse --git-dir >/dev/null 2>&1; then + Log::displayError "not a git repository (or any of the parent directories)" + exit 1 + fi -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --push | -p) - PUSH="1" - ;; - --delete | -d) - DELETE="1" - ;; - --assume-yes | -yes | -y) - INTERACTIVE="0" - ;; - --) - shift || true - break - ;; - *) - showHelp - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done -shift $((OPTIND - 1)) || true + if [[ -z "${oldBranchNameArg}" ]]; then + oldBranchNameArg="$(git branch --show-current)" + if [[ -z "${oldBranchNameArg}" ]]; then + Log::displayError "Impossible to compute current branch name" + exit 2 + fi + fi -newName="$1" -shift || true -oldName="${1:-}" -shift || true -if [[ $# -gt 0 ]]; then - Log::fatal "too much arguments provided" -fi + if [[ "${oldBranchNameArg}" =~ ^(master|main)$ || "${newBranchNameArg}" =~ ^(master|main)$ ]]; then + Log::displayError "master/main branch not supported by this command, please do it manually" + exit 3 + fi -if ! git rev-parse --git-dir >/dev/null 2>&1; then - Log::fatal "not a git repository (or any of the parent directories)" -fi + if [[ -z "${newBranchNameArg}" ]]; then + Log::displayError "new branch name not provided" + exit 4 + fi -if [[ -z "${oldName}" ]]; then - oldName="$(git branch --show-current)" - [[ -z "${oldName}" ]] && Log::fatal "Impossible to calculate current branch name" -fi -[[ "${oldName}" =~ ^(master|main)$ ]] && - Log::fatal "master/main branch not supported by this command, please do it manually" -[[ "${newName}" =~ ^(master|main)$ ]] && - Log::fatal "master/main branch not supported by this command, please do it manually" -[[ -z "${newName}" ]] && Log::fatal "new branch name not provided" -[[ "${oldName}" = "${newName}" ]] && Log::fatal "Branch name has not changed" + if [[ "${oldBranchNameArg}" = "${newBranchNameArg}" ]]; then + Log::displayError "New and old branch names are the same" + exit 5 + fi -Log::displayInfo "Renaming branch locally from ${oldName} to ${newName}" -declare -a CMD=() -CMD=(git branch -m "${oldName}" "${newName}") -Log::displayDebug "Running '${CMD[*]}'" -"${CMD[@]}" + Log::displayInfo "Renaming branch locally from ${oldBranchNameArg} to ${newBranchNameArg}" + declare -a cmd=() + cmd=(git branch -m "${oldBranchNameArg}" "${newBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to rename local branch ${oldBranchNameArg} to ${newBranchNameArg}" + exit 7 + fi -if [[ "${DELETE}" = "1" ]]; then - deleteBranch() { - Log::displayInfo "Removing eventual old remote branch ${oldName}" - CMD=(git push origin ":${oldName}") - Log::displayDebug "Running '${CMD[*]}'" - "${CMD[@]}" || true - } - if [[ "${INTERACTIVE}" = "0" ]] || UI::askYesNo "remove eventual old remote branch ${oldName}"; then - deleteBranch + if [[ "${optionDelete}" = "1" ]]; then + if [[ "${optionAssumeYes}" = "1" ]] || UI::askYesNo "Remove eventual old remote branch ${oldBranchNameArg}"; then + Log::displayInfo "Removing eventual old remote branch ${oldBranchNameArg}" + cmd=(git push origin ":${oldBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to delete remote branch ${oldBranchNameArg}" + exit 8 + fi + fi fi -fi -if [[ "${PUSH}" = "1" ]]; then - push() { - Log::displayInfo "Pushing new branch name ${newName}" - CMD=(git push --set-upstream origin "${newName}") - Log::displayDebug "Running '${CMD[*]}'" - "${CMD[@]}" || true - } - if [[ "${INTERACTIVE}" = "0" ]] || UI::askYesNo "Push new branch name ${newName}"; then - push + + if [[ "${optionPush}" = "1" ]]; then + if [[ "${optionAssumeYes}" = "1" ]] || UI::askYesNo "Push new branch name ${newBranchNameArg}"; then + Log::displayInfo "Pushing new branch name ${newBranchNameArg}" + cmd=(git push --set-upstream origin "${newBranchNameArg}") + Log::displayDebug "Running '${cmd[*]}'" + if ! "${cmd[@]}"; then + Log::displayError "Failed to push the new branch ${newBranchNameArg}" + exit 9 + fi + fi fi +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run fi + diff --git a/src/_binaries/Git/upgradeGithubRelease.bats b/src/_binaries/Git/upgradeGithubRelease.bats index 1928f68d..ede4ac75 100755 --- a/src/_binaries/Git/upgradeGithubRelease.bats +++ b/src/_binaries/Git/upgradeGithubRelease.bats @@ -5,9 +5,6 @@ source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" load "${FRAMEWORK_ROOT_DIR}/src/_standalone/Bats/assert_lines_count.sh" -# shellcheck source=vendor/bash-tools-framework/src/Env/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Env/load.sh" || exit 1 - setup() { export TMPDIR="${BATS_TEST_TMPDIR}" export HOME="${BATS_TEST_TMPDIR}/home" @@ -26,16 +23,15 @@ function Git::upgradeGithubRelease::display_help { #@test function Git::upgradeGithubRelease::noArg { #@test run "${binDir}/upgradeGithubRelease" 2>&1 - assert_failure + assert_failure 1 assert_lines_count 1 - assert_line --index 0 --partial "FATAL - Exactly 2 fixed arguments are required" + assert_output --partial "ERROR - Command upgradeGithubRelease - Argument 'targetFile' should be provided at least 1 time(s)" } function Git::upgradeGithubRelease::1Arg { #@test run "${binDir}/upgradeGithubRelease" arg1 2>&1 - assert_failure - assert_lines_count 1 - assert_line --index 0 --partial "FATAL - Exactly 2 fixed arguments are required" + assert_failure 1 + assert_output --partial "ERROR - Command upgradeGithubRelease - Argument 'githubUrlPattern' should be provided at least 1 time(s)" } function Git::upgradeGithubRelease::githubArgInvalid { #@test @@ -63,6 +59,7 @@ function Git::upgradeGithubRelease::filePathNotWritable { #@test function Git::upgradeGithubRelease::filePathNotExistsExactVersionShortArg { #@test stub curl \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : exit 0' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -81,6 +78,7 @@ function Git::upgradeGithubRelease::filePathNotExistsExactVersionShortArg { #@te function Git::upgradeGithubRelease::filePathNotExistsExactVersionLongArg { #@test stub curl \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : exit 0' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -97,9 +95,26 @@ function Git::upgradeGithubRelease::filePathNotExistsExactVersionLongArg { #@tes [[ "$(cat "${BATS_TEST_TMPDIR}/targetFile")" = "success" ]] } -function Git::upgradeGithubRelease::filePathNotExistsLatestVersion { #@test +function Git::upgradeGithubRelease::filePathNotExistsLatestVersionNotFound { #@test + stub curl \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{}" > "$3"' + + run "${binDir}/upgradeGithubRelease" \ + "${BATS_TEST_TMPDIR}/targetFile" \ + "https://github.com/hadolint/hadolint/releases/download/v@version@/hadolint-Linux-x86_64" \ + --verbose \ + 2>&1 + assert_failure 5 + assert_lines_count 4 + assert_line --index 0 --partial "INFO - compute last remote version" + assert_line --index 1 --partial "INFO - Attempt 1/5:" + assert_line --index 2 --partial "INFO - Repo hadolint/hadolint latest version found is " + assert_line --index 3 --partial "ERROR - ${BATS_TEST_TMPDIR}/targetFile latest version not found on github" +} + +function Git::upgradeGithubRelease::filePathNotExistsLatestVersionFound { #@test stub curl \ - '-o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.0.0\"}" > "$2"' \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.0.0\"}" > "$3"' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -108,19 +123,21 @@ function Git::upgradeGithubRelease::filePathNotExistsLatestVersion { #@test --verbose \ 2>&1 assert_success - assert_lines_count 5 - assert_line --index 0 --partial "INFO - Attempt 1/5:" - assert_line --index 1 --partial "INFO - Repo hadolint/hadolint latest version found is 1.0.0" - assert_line --index 2 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64" - assert_line --index 3 --partial "INFO - Attempt 1/5:" - assert_line --index 4 --partial "STATUS - Version 1.0.0 installed in ${BATS_TEST_TMPDIR}/targetFile" + assert_lines_count 6 + assert_line --index 0 --partial "INFO - compute last remote version" + assert_line --index 1 --partial "INFO - Attempt 1/5:" + assert_line --index 2 --partial "INFO - Repo hadolint/hadolint latest version found is 1.0.0" + assert_line --index 3 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64" + assert_line --index 4 --partial "INFO - Attempt 1/5:" + assert_line --index 5 --partial "STATUS - Version 1.0.0 installed in ${BATS_TEST_TMPDIR}/targetFile" [[ "$(cat "${BATS_TEST_TMPDIR}/targetFile")" = "success" ]] } function Git::upgradeGithubRelease::filePathExistsMinVersion { #@test cp "${BATS_TEST_DIRNAME}/testsData/upgradeGithubRelease_bin" "${BATS_TEST_TMPDIR}" stub curl \ - '-o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.1.0\"}" > "$2"' \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : exit 0' \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.1.0\"}" > "$3"' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -131,20 +148,22 @@ function Git::upgradeGithubRelease::filePathExistsMinVersion { #@test 2>&1 assert_success - assert_lines_count 6 - assert_line --index 0 --partial "ERROR - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin minimal version is 1.1.0, your version is 1.0.0" - assert_line --index 1 --partial "INFO - Attempt 1/5:" - assert_line --index 2 --partial "INFO - Repo hadolint/hadolint latest version found is 1.1.0" - assert_line --index 3 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64" - assert_line --index 4 --partial "INFO - Attempt 1/5:" - assert_line --index 5 --partial "STATUS - Version 1.1.0 installed in ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" + assert_lines_count 7 + assert_line --index 0 --partial "WARN - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version 1.0.0 is lesser than minimal version 1.1.0" + assert_line --index 1 --partial "INFO - compute last remote version" + assert_line --index 2 --partial "INFO - Attempt 1/5:" + assert_line --index 3 --partial "INFO - Repo hadolint/hadolint latest version found is 1.1.0" + assert_line --index 4 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64" + assert_line --index 5 --partial "INFO - Attempt 1/5:" + assert_line --index 6 --partial "STATUS - Version 1.1.0 installed in ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" [[ "$(cat "${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin")" = "success" ]] } function Git::upgradeGithubRelease::filePathExistsCurrentVersionLessThanMinVersion { #@test cp "${BATS_TEST_DIRNAME}/testsData/upgradeGithubRelease_bin" "${BATS_TEST_TMPDIR}" stub curl \ - '-o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.1.0\"}" > "$2"' \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : exit 0' \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.1.0\"}" > "$3"' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -156,18 +175,22 @@ function Git::upgradeGithubRelease::filePathExistsCurrentVersionLessThanMinVersi 2>&1 assert_success - assert_lines_count 6 - assert_line --index 0 --partial "ERROR - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin minimal version is 1.1.0, your version is 1.0.0" - assert_line --index 1 --partial "INFO - Attempt 1/5:" - assert_line --index 2 --partial "INFO - Repo hadolint/hadolint latest version found is 1.1.0" - assert_line --index 3 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64" - assert_line --index 4 --partial "INFO - Attempt 1/5:" - assert_line --index 5 --partial "STATUS - Version 1.1.0 installed in ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" + assert_lines_count 7 + assert_line --index 0 --partial "WARN - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version 1.0.0 is lesser than minimal version 1.1.0" + assert_line --index 1 --partial "INFO - compute last remote version" + assert_line --index 2 --partial "INFO - Attempt 1/5:" + assert_line --index 3 --partial "INFO - Repo hadolint/hadolint latest version found is 1.1.0" + assert_line --index 4 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64" + assert_line --index 5 --partial "INFO - Attempt 1/5:" + assert_line --index 6 --partial "STATUS - Version 1.1.0 installed in ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" [[ "$(cat "${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin")" = "success" ]] } function Git::upgradeGithubRelease::filePathExistsCurrentVersionEqualsMinVersion { #@test cp "${BATS_TEST_DIRNAME}/testsData/upgradeGithubRelease_bin" "${BATS_TEST_TMPDIR}" + stub curl \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : exit 0' \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.0.0\"}" > "$3"' run "${binDir}/upgradeGithubRelease" \ "${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" \ @@ -178,12 +201,19 @@ function Git::upgradeGithubRelease::filePathExistsCurrentVersionEqualsMinVersion 2>&1 assert_success - assert_lines_count 1 + assert_lines_count 5 assert_line --index 0 --partial "STATUS - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version is the required minimal version 1.0.0" + assert_line --index 1 --partial "INFO - compute last remote version" + assert_line --index 2 --partial "INFO - Attempt 1/5:" + assert_line --index 3 --partial "INFO - Repo hadolint/hadolint latest version found is 1.0.0" + assert_line --index 4 --partial "STATUS - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version is the same as remote version 1.0.0" } function Git::upgradeGithubRelease::filePathExistsCurrentVersionGreaterThanMinVersion { #@test cp "${BATS_TEST_DIRNAME}/testsData/upgradeGithubRelease_bin" "${BATS_TEST_TMPDIR}" + stub curl \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.0.0/hadolint-Linux-x86_64 : exit 0' \ + '-L -o * --fail --silent https://api.github.com/repos/hadolint/hadolint/releases/latest : echo "{\"tag_name\": \"1.0.0\"}" > "$3"' run "${binDir}/upgradeGithubRelease" \ "${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" \ @@ -194,13 +224,16 @@ function Git::upgradeGithubRelease::filePathExistsCurrentVersionGreaterThanMinVe 2>&1 assert_success - assert_lines_count 1 - assert_line --index 0 --partial "WARN - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version is 1.1.0 greater than 1.0.0" + assert_lines_count 3 + assert_line --index 0 --partial "INFO - Attempt 1/5:" + assert_line --index 1 --partial "INFO - Repo hadolint/hadolint latest version found is 1.0.0" + assert_line --index 2 --partial "INFO - ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin version 1.1.0 is greater than minimal version 1.0.0" } function Git::upgradeGithubRelease::filePathExistsExactVersionUpgradeNeeded { #@test cp "${BATS_TEST_DIRNAME}/testsData/upgradeGithubRelease_bin" "${BATS_TEST_TMPDIR}" stub curl \ + '-L -o /dev/null --silent --head --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : exit 0' \ '-L -o * --fail https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64 : echo "success" > "$3"' run "${binDir}/upgradeGithubRelease" \ @@ -216,6 +249,7 @@ function Git::upgradeGithubRelease::filePathExistsExactVersionUpgradeNeeded { #@ assert_line --index 1 --partial "INFO - Using url https://github.com/hadolint/hadolint/releases/download/v1.1.0/hadolint-Linux-x86_64" assert_line --index 2 --partial "INFO - Attempt 1/5:" assert_line --index 3 --partial "STATUS - Version 1.1.0 installed in ${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin" + [[ "$(cat "${BATS_TEST_TMPDIR}/upgradeGithubRelease_bin")" = "success" ]] } diff --git a/src/_binaries/Git/upgradeGithubRelease.options.tpl b/src/_binaries/Git/upgradeGithubRelease.options.tpl new file mode 100644 index 00000000..8406af8c --- /dev/null +++ b/src/_binaries/Git/upgradeGithubRelease.options.tpl @@ -0,0 +1,184 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="upgradeGithubReleaseCommand" +declare help="retrieve latest binary release from github and install it" +declare example1=$'\\"https://github.com/docker/compose/releases/download/v@version@/docker-compose-\$(uname -s | tr \'[:upper:]\' \'[:lower:]\')-\$(uname -m)\\"' +declare example2=$'\\"https://github.com/docker/docker-credential-helpers/releases/download/v@version@/docker-credential-wincred-v@version@.windows-\$(dpkg --print-architecture).exe\\"' +declare example3=$'\\"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)\\"' +declare longDescription=""" +${__HELP_TITLE}OPTIONS EXCEPTIONS:${__HELP_NORMAL} + +${__HELP_EXAMPLE}--current-version${__HELP_NORMAL}|${__HELP_EXAMPLE}-c${__HELP_NORMAL} and ${__HELP_EXAMPLE}--version-arg${__HELP_NORMAL} are mutually exclusive, +you cannot use both argument at the same time. + +${__HELP_EXAMPLE}--exact-version${__HELP_NORMAL}|${__HELP_EXAMPLE}-e${__HELP_NORMAL} and ${__HELP_EXAMPLE}--minimal-version${__HELP_NORMAL}|${__HELP_EXAMPLE}-m${__HELP_NORMAL} are mutually exclusive, +you cannot use both argument at the same time. + +${__HELP_TITLE}GITHUB TEMPLATE URLS EXAMPLES:${__HELP_NORMAL} + +Simple ones(Sometimes @version@ template variable has to be specified twice): +'https://github.com/hadolint/hadolint/releases/download/v@version@/hadolint-Linux-x86_64' +'https://github.com/koalaman/shellcheck/releases/download/v@version@/shellcheck-v@version@.linux.x86_64.tar.xz' +'https://github.com/sharkdp/fd/releases/download/v@version@/fd_@version@_amd64.deb' +'https://github.com/sharkdp/bat/releases/download/v@version@/bat_@version@_amd64.deb' +'https://github.com/kubernetes-sigs/kind/releases/download/v@version@/kind-linux-amd64' +'https://github.com/kubernetes/minikube/releases/download/v@version@/minikube-linux-amd64' +'https://github.com/plantuml/plantuml/releases/download/v@version@/plantuml-@version@.jar' +'https://github.com/Versent/saml2aws/releases/download/v@version@/saml2aws_@version@_linux_amd64.tar.gz' + +If you want to add a condition on architecture(linux, windows, x86, 64/32 bits): +${example1} +${example2} +${example3} + +${__HELP_TITLE}COMMAND EXAMPLES:${__HELP_NORMAL} +Download docker-compose latest version +${__HELP_EXAMPLE}upgradeGithubRelease /usr/local/bin/docker-compose ${example1}${__HELP_NORMAL} + +Download oq specific version +${__HELP_EXAMPLE}upgradeGithubRelease /usr/local/bin/oq --exact-version 1.3.4 ${example3}${__HELP_NORMAL} + +Download oq specific version correctly retrieving the oq version and not the jq one +${__HELP_EXAMPLE}upgradeGithubRelease /usr/local/bin/oq --exact-version 1.3.4 --version-arg '-V | grep oq:' ${example3}${__HELP_NORMAL} +""" +declare defaultVersionArg="--version" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +# shellcheck source=/dev/null +source <( + targetFileArgCallback() { :; } + Options::generateArg \ + --help "the binary downloaded will e written to this file path. Ensure the path is writable." \ + --min 1 \ + --max 1 \ + --name "targetFile" \ + --variable-name "targetFileArg" \ + --callback targetFileArgCallback \ + --function-name targetFileArgFunction + + githubUrlPatternArgCallback() { :; } + # shellcheck disable=SC2116 + Options::generateArg \ + --help "$(echo \ + "the url pattern to use to download the binary, see examples below." $'\n' \ + "@version@ is template variable that will be replaced by the latest" $'\n' \ + "version tag found on github." \ + )" \ + --min 1 \ + --max 1 \ + --name "githubUrlPattern" \ + --variable-name "githubUrlPatternArg" \ + --callback githubUrlPatternArgCallback \ + --function-name githubUrlPatternArgFunction + + Options::generateGroup \ + --title "VERSION MANAGEMENT:" \ + --function-name groupVersionManagementFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "versionArg" \ + --default-value "${defaultVersionArg}" \ + --help "$(echo \ + "The argument that will be provided to the currently installed binary " \ + "to check the version of the software." $'\n' \ + "This parameter is needed if ${__HELP_EXAMPLE}--minimal-version${__HELP_NORMAL} argument is used and is " \ + "different than default value (${__HELP_EXAMPLE}${defaultVersionArg}${__HELP_NORMAL})." \ + )" \ + --group groupVersionManagementFunction \ + --alt "--version-arg" \ + --variable-type "String" \ + --variable-name "optionVersionArg" \ + --function-name optionVersionArgFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "currentVersion" \ + --help "$(echo \ + "Sometimes the command to retrieve the version is complicated. " $'\n' \ + "Some command needs you to parse json or other commands provides " \ + "multiple sub command versions. In this case you can provide the " \ + "version you currently have, see examples below." + )" \ + --group groupVersionManagementFunction \ + --alt "--current-version" \ + --alt "-c" \ + --variable-type "String" \ + --variable-name "optionCurrentVersion" \ + --function-name optionCurrentVersionFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "minimalVersion" \ + --help "$(echo \ + "if provided and currently installed binary is below this ${__HELP_EXAMPLE}minimalVersion${__HELP_NORMAL}," $'\n' \ + "a new version of the binary will be installed." $'\n' \ + "If this argument is not provided, the latest binary is unconditionally downloaded from github." \ + )" \ + --group groupVersionManagementFunction \ + --alt "--minimal-version" \ + --alt "-m" \ + --variable-type "String" \ + --variable-name "optionMinimalVersion" \ + --function-name optionMinimalVersionFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "exactVersion" \ + --help "$(echo \ + "if provided and currently installed binary is not this ${__HELP_EXAMPLE}exactVersion${__HELP_NORMAL}," $'\n' \ + "This exact version of the binary will be installed." \ + )" \ + --group groupVersionManagementFunction \ + --alt "--exact-version" \ + --alt "-e" \ + --variable-type "String" \ + --variable-name "optionExactVersion" \ + --function-name optionExactVersionFunction +) +options+=( + targetFileArgFunction + githubUrlPatternArgFunction + optionVersionArgFunction + optionCurrentVersionFunction + optionExactVersionFunction + optionMinimalVersionFunction + --callback upgradeGithubReleaseCommandCallback +) +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +# default values +declare targetFileArg="" +declare githubUrlPatternArg="" +declare optionVersionArg="<% ${defaultVersionArg} %>" +declare optionCurrentVersion="" +declare optionMinimalVersion="" +declare optionExactVersion="" + +upgradeGithubReleaseCommandCallback() { + if [[ -n "${optionExactVersion}" && -n "${optionMinimalVersion}" ]]; then + Log::fatal "--exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at the same time." + fi +} + +githubUrlPatternArgCallback() { + if [[ ! "${githubUrlPatternArg}" =~ ^https://github.com/ ]]; then + Log::fatal "Invalid githubUrlPattern ${githubUrlPatternArg} provided, it should begin with https://github.com/" + fi +} + +targetFileArgCallback() { + if [[ "${targetFileArg:0:1}" != "/" ]]; then + targetFileArg="$(pwd)/${targetFileArg}" + fi + if ! Assert::validPath "${targetFileArg}"; then + Log::fatal "File ${targetFileArg} is not a valid path" + fi + if ! Assert::fileWritable "${targetFileArg}"; then + Log::fatal "File ${targetFileArg} is not writable" + fi +} diff --git a/src/_binaries/Git/upgradeGithubRelease.sh b/src/_binaries/Git/upgradeGithubRelease.sh index 0d3a4a7c..a3f5ba74 100755 --- a/src/_binaries/Git/upgradeGithubRelease.sh +++ b/src/_binaries/Git/upgradeGithubRelease.sh @@ -1,195 +1,123 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/upgradeGithubRelease +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE +# shellcheck disable=SC2154 -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Git/upgradeGithubRelease.options.tpl)" -#default values -TARGET_FILE="" -VERSION_ARG="--version" -MIN_VERSION="" -CURRENT_VERSION="" -EXACT_VERSION="" -GITHUB_URL_PATTERN="" +upgradeGithubReleaseCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -# Usage info -showHelp() { - cat < - [--version-arg ] - [--minimal-version|-m ] - [--current-version|-c ] - [--exact-version|-e ] - - --help,-h prints this help and exits - - --version-arg : The argument that will be provided to the currently installed binary - to check the version of the software. This parameter is needed if --minimal-version - argument is used and is different than default value (--version). - - --current-version|-c : sometimes the command to retrieve the version is complicated - some command needs you to parse json or other commands provides multiple sub command versions. - In this case you can provide the version you currently have, see examples below. - - --minimal-version|-m : if provided, if currently installed binary is below - this minimalVersion, a new version of the binary will be installed. If this argument is not - provided, the latest binary is unconditionally downloaded from github. - - --current-version|-c and --version-arg are mutually exclusive, you cannot use both argument at the - same time. - - --exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at - the same time. - - the binary downloaded will e written to this file path. Ensure the path is writable. - the url pattern to use to download the binary, see examples below. - @version@ is template variable that will be replaced by the latest version tag found on - github. - -${__HELP_TITLE}Github template urls examples:${__HELP_NORMAL} - -Simple ones(Sometimes @version@ template variable has to be specified twice): -"https://github.com/hadolint/hadolint/releases/download/v@version@/hadolint-Linux-x86_64" -"https://github.com/koalaman/shellcheck/releases/download/v@version@/shellcheck-v@version@.linux.x86_64.tar.xz" -"https://github.com/sharkdp/fd/releases/download/v@version@/fd_@version@_amd64.deb" -"https://github.com/sharkdp/bat/releases/download/v@version@/bat_@version@_amd64.deb" -'https://github.com/kubernetes-sigs/kind/releases/download/v@version@/kind-linux-amd64' -"https://github.com/kubernetes/minikube/releases/download/v@version@/minikube-linux-amd64" -"https://github.com/plantuml/plantuml/releases/download/v@version@/plantuml-@version@.jar" -"https://github.com/Versent/saml2aws/releases/download/v@version@/saml2aws_@version@_linux_amd64.tar.gz" - -If you want to add condition on architecture(linux, windows, x86, 64/32 bits): -"https://github.com/docker/compose/releases/download/v@version@/docker-compose-\$(uname -s | tr '[:upper:]' '[:lower:]')-\$(uname -m)" -"https://github.com/docker/docker-credential-helpers/releases/download/v@version@/docker-credential-wincred-v@version@.windows-\$(dpkg --print-architecture).exe" -"https://github.com/Blacksmoke16/oq/releases/download/v@version@/oq-v@version@-\$(uname -s)-\$(uname -m)" - -${__HELP_TITLE}Command examples:${__HELP_NORMAL} -upgradeGithubRelease - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -} - -# read command parameters -# $@ is all command line parameters passed to the script. -# -o is for short options like -h -# -l is for long options with double dash like --help -# the comma separates different long options -options=$(getopt \ - -l help,version-arg:,minimal-version:,current-version:,exact-version: \ - -o hm:c:e: -- "$@" 2>/dev/null) || { - showHelp - Log::fatal "invalid options specified" -} - -eval set -- "${options}" -while true; do - case $1 in - -h | --help) - showHelp - exit 0 - ;; - --version-arg) - shift || true - VERSION_ARG="$1" - ;; - --minimal-version | -m) - shift || true - MIN_VERSION="$1" - ;; - --current-version | -c) - shift || true - CURRENT_VERSION="$1" - ;; - --exact-version | -e) - shift || true - EXACT_VERSION="$1" - ;; - --) - shift || true - break - ;; - *) - Log::fatal "invalid argument $1" - ;; - esac - shift || true -done -shift $((OPTIND - 1)) || true - -if [[ -n "${EXACT_VERSION}" && -n "${MIN_VERSION}" ]]; then - Log::fatal "--exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at the same time." -fi -if (($# != 2)); then - Log::fatal "Exactly 2 fixed arguments are required" -fi -TARGET_FILE="$1" -GITHUB_URL_PATTERN="$2" - -if [[ ! "${GITHUB_URL_PATTERN}" =~ ^https://github.com/ ]]; then - Log::fatal "Invalid githubUrlPattern ${GITHUB_URL_PATTERN} provided, it should begin with https://github.com/" -fi - -if [[ "${TARGET_FILE:0:1}" != "/" ]]; then - TARGET_FILE="$(pwd)/${TARGET_FILE}" -fi -if ! Assert::validPath "${TARGET_FILE}"; then - Log::fatal "File ${TARGET_FILE} is not a valid path" -fi -if ! Assert::fileWritable "${TARGET_FILE}"; then - Log::fatal "File ${TARGET_FILE} is not writable" -fi - -# if minVersion arg provided, we have to compute current bin version -TRY_DOWNLOAD_NEW_VERSION=1 -if [[ -f "${TARGET_FILE}" ]]; then - if [[ -n "${MIN_VERSION}" ]]; then - if [[ -z "${CURRENT_VERSION}" && -n "${VERSION_ARG}" ]]; then - if Version::checkMinimal "${TARGET_FILE}" "${VERSION_ARG}" "${MIN_VERSION}"; then - TRY_DOWNLOAD_NEW_VERSION=0 +run() { + computeCurrentCommandVersion() { + if [[ -n "${optionCurrentVersion}" ]]; then + echo "${optionCurrentVersion}" + return 0 + fi + if [[ -n "${optionVersionArg}" ]]; then + # need eval here to correctly interpret --version-arg '-V | grep oq:' + eval "'${targetFileArg}' ${optionVersionArg} 2>&1" | Version::parse || return 3 + fi + } + + # if minVersion arg provided, we have to compute current bin version + local tryDownloadNewVersion=1 + if [[ -f "${targetFileArg}" ]]; then + local commandVersion + commandVersion="$(computeCurrentCommandVersion)" + + if [[ -n "${optionExactVersion}" ]]; then + if Version::compare "${commandVersion}" "${optionExactVersion}"; then + tryDownloadNewVersion=0 + Log::displayStatus "${targetFileArg} version is the exact required version ${optionExactVersion}" + else + Log::displayWarning "${targetFileArg} version ${commandVersion} is different than required version ${optionExactVersion}" fi - elif [[ -n "${CURRENT_VERSION}" ]]; then - versionCompare=0 - Version::compare "${CURRENT_VERSION}" "${MIN_VERSION}" || versionCompare=$? - # do not try to down version if current version is greater or equal to min version - if [[ "${versionCompare}" = "1" ]]; then - # current version > min version - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayWarning "${TARGET_FILE} version is ${CURRENT_VERSION} greater than ${MIN_VERSION}" - elif [[ "${versionCompare}" = "2" ]]; then - # current version < min version - Log::displayError "${TARGET_FILE} minimal version is ${MIN_VERSION}, your version is ${CURRENT_VERSION}" + else + if [[ -n "${optionMinimalVersion}" ]]; then + if ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionMinimalVersion}/g")"; then + Log::displayError "Minimal version ${optionMinimalVersion} doesn't exist on github" + return 5 + fi + local versionCompare=0 + Version::compare "${commandVersion}" "${optionMinimalVersion}" || versionCompare=$? + # do not try to down version if current version is greater or equal to min version + if [[ "${versionCompare}" = "1" ]]; then + local msg="${targetFileArg} version ${commandVersion} is greater than minimal version ${optionMinimalVersion}" + # current version > min version + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + versionCompare=0 + Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? + if [[ "${versionCompare}" = "2" ]]; then + # current version < remote version + Log::displayWarning "${msg} but new version ${optionExactVersion} is available on github" + else + Log::displayInfo "${msg}" + fi + return 0 + elif [[ "${versionCompare}" = "2" ]]; then + # current version < min version + Log::displayWarning "${targetFileArg} version ${commandVersion} is lesser than minimal version ${optionMinimalVersion}" + else + tryDownloadNewVersion=2 # need to check if a newer version exists + Log::displayStatus "${targetFileArg} version is the required minimal version ${optionMinimalVersion}" + fi else - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayStatus "${TARGET_FILE} version is the required minimal version ${MIN_VERSION}" + tryDownloadNewVersion="2" + fi + + # check if a newer version is available + if [[ "${tryDownloadNewVersion}" = "2" ]]; then + Log::displayInfo "compute last remote version" + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + versionCompare=0 + Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? + if [[ "${versionCompare}" = "1" ]]; then + # current version > remote version, shouldn't happen + tryDownloadNewVersion=0 + Log::displayWarning "${targetFileArg} version ${commandVersion} is greater than remote version ${optionExactVersion}" + elif [[ "${versionCompare}" = "2" ]]; then + # current version < remote version + tryDownloadNewVersion=1 + Log::displayWarning "${targetFileArg} version ${optionCurrentVersion} is lesser than remote version ${optionExactVersion}" + else + tryDownloadNewVersion=0 + Log::displayStatus "${targetFileArg} version is the same as remote version ${optionExactVersion}" + fi fi fi - elif [[ -n "${EXACT_VERSION}" ]]; then - if [[ -z "${CURRENT_VERSION}" && -n "${VERSION_ARG}" ]]; then - CURRENT_VERSION="$("${TARGET_FILE}" "${VERSION_ARG}" 2>&1 | Version::parse)" - fi - if Version::compare "${CURRENT_VERSION}" "${EXACT_VERSION}"; then - TRY_DOWNLOAD_NEW_VERSION=0 - Log::displayStatus "${TARGET_FILE} version is the exact required version ${EXACT_VERSION}" - else - Log::displayWarning "${TARGET_FILE} version ${CURRENT_VERSION} is different than required version ${EXACT_VERSION}" + fi + + if [[ "${tryDownloadNewVersion}" = "0" ]]; then + return 0 + fi + + # check if target file is writable + Assert::fileWritable "${targetFileArg}" + + if [[ -z "${optionExactVersion}" ]]; then + Log::displayInfo "compute last remote version" + optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 + if [[ -z "${optionExactVersion}" ]]; then + Log::displayError "${targetFileArg} latest version not found on github" + return 5 fi + elif ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")"; then + Log::displayError "${targetFileArg} version ${optionExactVersion} doesn't exist on github" + return 4 fi -fi -if [[ "${TRY_DOWNLOAD_NEW_VERSION}" = "0" ]]; then - exit 0 -fi + local githubUrl + githubUrl="$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")" + Log::displayInfo "Using url ${githubUrl}" -if [[ -z "${EXACT_VERSION}" ]]; then - EXACT_VERSION="$(Github::getLatestVersionFromUrl "${GITHUB_URL_PATTERN}")" -fi -GITHUB_URL="$(echo "${GITHUB_URL_PATTERN}" | sed -E "s/@version@/${EXACT_VERSION}/g")" -Log::displayInfo "Using url ${GITHUB_URL}" + newSoftware=$(Github::downloadReleaseVersion "${githubUrl}") + Github::defaultInstall "${newSoftware}" "${targetFileArg}" + Log::displayStatus "Version ${optionExactVersion} installed in ${targetFileArg}" +} -newSoftware=$(Github::downloadReleaseVersion "${GITHUB_URL}") -Github::defaultInstall "${newSoftware}" "${TARGET_FILE}" -Log::displayStatus "Version ${EXACT_VERSION} installed in ${TARGET_FILE}" +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/Utils/waitForIt.bats b/src/_binaries/Utils/waitForIt.bats new file mode 100755 index 00000000..b29d02f4 --- /dev/null +++ b/src/_binaries/Utils/waitForIt.bats @@ -0,0 +1,366 @@ +#!/usr/bin/env bash + +# shellcheck source=src/batsHeaders.sh +source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" + +setup() { + export TMPDIR="${BATS_TEST_TMPDIR}" + + export HOME="${BATS_TEST_TMPDIR}/home" + mkdir -p "${HOME}" + mkdir -p "${HOME}/bin" + export PATH="${HOME}/bin:${PATH}" +} + +teardown() { + unstub_all + rm -f "${HOME}/bin/nc" || true +} + +function Utils::waitForIt::display_help { #@test + # shellcheck disable=SC2154 + run "${binDir}/waitForIt" --help 2>&1 + assert_success + assert_line --index 0 "DESCRIPTION: wait for host:port to be available" +} + +function Utils::waitForIt::noArgs { #@test + run "${binDir}/waitForIt" 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForIt - Option '--host' should be provided at least 1 time(s)" +} + +function Utils::waitForIt::missingPort { #@test + run "${binDir}/waitForIt" --host localhost 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForIt - Option '--port' should be provided at least 1 time(s)" +} + +function Utils::waitForIt::missingHost { #@test + run "${binDir}/waitForIt" --port 8888 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForIt - Option '--host' should be provided at least 1 time(s)" +} + +function Utils::waitForIt::invalidTimeout { #@test + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout invalid 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "FATAL - waitForIt - invalid timeout option - must be greater or equal to 0" +} + +function Utils::waitForIt::invalidAlgo { #@test + run "${binDir}/waitForIt" --host localhost --port 8888 --algo invalid 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "FATAL - waitForIt - invalid algorithm 'invalid'" +} + +function Utils::waitForIt::algo::timeoutV1WithNc::WithoutCommand { #@test + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc" + stub nc "-z localhost 8888 -w 1 : exit 0" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting " + assert_line --index 1 --partial " seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 3 +} + +function Utils::waitForIt::algo::timeoutV1WithNc::ExecCommand { #@test + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc echo success : \ + ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc echo success" + stub nc "-z localhost 8888 -w 1 : exit 0" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV1WithNc echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting " + assert_line --index 1 --partial " seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 3 "success" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV1WithNc::NoCommandExecutedIfFailed { #@test + ( + echo "#!/bin/bash" + echo 'exit 1' + ) > "${HOME}/bin/nc" + chmod +x "${HOME}/bin/nc" + + stub timeout \ + "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithNc echo success : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithNc echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithNc echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting " + assert_line --index 1 --partial " seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after " + assert_line --index 2 --partial " seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV2WithNc::WithoutCommand { #@test + stub timeout "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc" + stub nc "-z localhost 8888 -w 1 : exit 0" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 3 +} + +function Utils::waitForIt::algo::timeoutV2WithNc::ExecCommand { #@test + stub timeout "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc echo success : \ + ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc echo success" + stub nc "-z localhost 8888 -w 1 : exit 0" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV2WithNc echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 3 "success" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV2WithNc::NoCommandExecutedIfFailed { #@test + ( + echo "#!/bin/bash" + echo 'exit 1' + ) > "${HOME}/bin/nc" + chmod +x "${HOME}/bin/nc" + + stub timeout \ + "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithNc echo success : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithNc echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithNc echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after " + assert_line --index 2 --partial " seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} + +# ----------------- TCP ------------------------------------------------------------ + +function Utils::waitForIt::algo::timeoutV1WithTcp::WithoutCommand { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + echo "mocked $@" + } + export -f mockedTcp + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 "mocked /dev/tcp/localhost/8888" + assert_line --index 3 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV1WithTcp::ExecCommand { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + echo "mocked $@" + } + export -f mockedTcp + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp echo success : \ + ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV1WithTcp echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 "mocked /dev/tcp/localhost/8888" + assert_line --index 3 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 4 "success" + assert_lines_count 5 +} + +function Utils::waitForIt::algo::timeoutV1WithTcp::NoCommandExecutedIfFailed { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + return 1 + } + export -f mockedTcp + stub timeout \ + "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithTcp echo success : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithTcp echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo timeoutV1WithTcp echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV1WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after " + assert_line --index 2 --partial " seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV2WithTcp::WithoutCommand { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + return 0 + } + export -f mockedTcp + stub timeout "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 3 +} + +function Utils::waitForIt::algo::timeoutV2WithTcp::ExecCommand { #@test + stub timeout "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp echo success : \ + ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo timeoutV2WithTcp echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 3 "success" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::timeoutV2WithTcp::NoCommandExecutedIfFailed { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + return 1 + } + export -f mockedTcp + stub timeout \ + "1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithTcp echo success : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithTcp echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo timeoutV2WithTcp echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm timeoutV2WithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after " + assert_line --index 2 --partial " seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} + +# ----------------- whileLoop ------------------------------------------------------------ + +function Utils::waitForIt::algo::whileLoopWithTcp::WithoutCommand { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + echo "mocked $@" + } + export -f mockedTcp + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp : ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 "mocked /dev/tcp/localhost/8888" + assert_line --index 3 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 4 +} + +function Utils::waitForIt::algo::whileLoopWithTcp::ExecCommand { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + echo "mocked $@" + } + export -f mockedTcp + stub timeout "-t 1 ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp echo success : \ + ${binDir}/waitForIt --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp echo success" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo whileLoopWithTcp echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 "mocked /dev/tcp/localhost/8888" + assert_line --index 3 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 4 "success" + assert_lines_count 5 +} + +function Utils::waitForIt::algo::whileLoopWithTcp::NoCommandExecutedIfFailed { #@test + export WAIT_FOR_IT_MOCKED_TCP=mockedTcp + function mockedTcp() { + return 1 + } + export -f mockedTcp + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo whileLoopWithTcp echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithTcp" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after 2 seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::whileLoopWithNc::WithoutCommand { #@test + ( + echo "#!/bin/bash" + echo 'exit 0' + ) > "${HOME}/bin/nc" + chmod +x "${HOME}/bin/nc" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo whileLoopWithNc 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_lines_count 3 +} + +function Utils::waitForIt::algo::whileLoopWithNc::ExecCommand { #@test + ( + echo "#!/bin/bash" + echo 'exit 0' + ) > "${HOME}/bin/nc" + chmod +x "${HOME}/bin/nc" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --algo whileLoopWithNc echo "success" 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "INFO - waitForIt - localhost:8888 is available after " + assert_line --index 3 "success" + assert_lines_count 4 +} + +function Utils::waitForIt::algo::whileLoopWithNc::NoCommandExecutedIfFailed { #@test + ( + echo "#!/bin/bash" + echo 'exit 1' + ) > "${HOME}/bin/nc" + chmod +x "${HOME}/bin/nc" + run "${binDir}/waitForIt" --host localhost --port 8888 --timeout 1 --strict --algo whileLoopWithNc echo "success" 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - waitForIt - using algorithm whileLoopWithNc" + assert_line --index 1 --partial "INFO - waitForIt - waiting 1 seconds for localhost:8888" + assert_line --index 2 --partial "ERROR - waitForIt - timeout occurred after 2 seconds for localhost:8888" + assert_line --index 3 --partial "ERROR - waitForIt - failed to connect - strict mode - command not executed" + assert_lines_count 4 +} diff --git a/src/_binaries/Utils/waitForIt.options.tpl b/src/_binaries/Utils/waitForIt.options.tpl new file mode 100644 index 00000000..58c58bb3 --- /dev/null +++ b/src/_binaries/Utils/waitForIt.options.tpl @@ -0,0 +1,148 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="waitForItCommand" +declare help="wait for host:port to be available" +# shellcheck disable=SC2016 +declare longDescription=""" +${__HELP_TITLE}EXIT STATUS CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}0${__HELP_NORMAL}: the host/port is available +${__HELP_OPTION_COLOR}1${__HELP_NORMAL}: indicates host/port is not available or argument error +${__HELP_OPTION_COLOR}2${__HELP_NORMAL}: timeout reached + +${__HELP_TITLE}AVAILABLE ALGORITHMS:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}timeoutV1WithNc${__HELP_NORMAL}: previous version of timeout command with --timeout option, base command nc +${__HELP_OPTION_COLOR}timeoutV2WithNc${__HELP_NORMAL}: newer version of timeout command using timeout as argument, base command nc +${__HELP_OPTION_COLOR}whileLoopWithNc${__HELP_NORMAL}: timeout command simulated using while loop, base command nc +${__HELP_OPTION_COLOR}timeoutV1WithTcp${__HELP_NORMAL}: previous version of timeout command with --timeout option +${__HELP_OPTION_COLOR}timeoutV2WithTcp${__HELP_NORMAL}: newer version of timeout command using timeout as argument +${__HELP_OPTION_COLOR}whileLoopWithTcp${__HELP_NORMAL}: timeout command simulated using while loop, base command tcp +""" +declare -a availableAlgos=( + "timeoutV1WithNc" + "timeoutV2WithNc" + "whileLoopWithNc" + "timeoutV1WithTcp" + "timeoutV2WithTcp" + "whileLoopWithTcp" +) +declare defaultTimeout="15" +% +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +% +# shellcheck source=/dev/null +source <( + Options::generateArg \ + --help "Execute command with args after the test finishes or exit with status code if no command provided." \ + --min 0 \ + --name "commandArgs" \ + --variable-name "commandArgs" \ + --function-name commandArgsFunction + + Options::generateOption \ + --help-value-name "hostOrIp" \ + --help "Host or IP under test." \ + --alt "--host" \ + --alt "-i" \ + --mandatory \ + --variable-type "String" \ + --variable-name "optionHostOrIp" \ + --function-name optionHostOrIpFunction + + Options::generateOption \ + --help-value-name "port" \ + --help "TCP port under test." \ + --alt "--port" \ + --alt "-p" \ + --mandatory \ + --variable-type "String" \ + --variable-name "optionPort" \ + --function-name optionPortFunction \ + --callback optionPortCallback + + Options::generateOption \ + --help-value-name "algorithm" \ + --help "$(echo \ + "Algorithm to use Check algorithms list below." $'\n' \ + "(default: automatic selection based on commands availability and timeout option value)." \ + )" \ + --alt "--algorithm" \ + --alt "--algo" \ + --variable-type "String" \ + --variable-name "optionAlgo" \ + --function-name optionAlgoFunction \ + --callback optionAlgoCallback + + Options::generateOption \ + --help "Only execute sub-command if the test succeeds." \ + --alt "--exec-command-on-success-only" \ + --alt "--strict" \ + --alt "-s" \ + --variable-name "optionStrict" \ + --function-name optionStrictFunction + + Options::generateOption \ + --help "legacy mode using nc command or while loop (uses timeout command by default)." \ + --alt "--user-nc" \ + --variable-name "optionLegacy" \ + --function-name optionLegacyFunction + + Options::generateOption \ + --help-value-name "timeout" \ + --help "Timeout in seconds, zero for no timeout." \ + --default-value "${defaultTimeout}" \ + --alt "--timeout" \ + --alt "-t" \ + --variable-type "String" \ + --variable-name "optionTimeout" \ + --function-name optionTimeoutFunction \ + --callback optionTimeoutCallback +) +options+=( + --unknown-option-callback unknownOption + --unknown-argument-callback unknownOption + --callback commandCallback + commandArgsFunction + optionHostOrIpFunction + optionPortFunction + optionAlgoFunction + optionStrictFunction + optionTimeoutFunction +) +Options::generateCommand "${options[@]}" +% + +# shellcheck disable=SC2317 # if function is overridden +unknownOption() { + commandArgs+=("$1") +} + +optionPortCallback() { + if [[ ! "${optionPort}" =~ ^[0-9]+$ ]] || (( optionPort == 0 )); then + Log::fatal "${SCRIPT_NAME} - invalid port option - must be greater than to 0" + fi +} + +optionTimeoutCallback() { + if [[ ! "${optionTimeout}" =~ ^[0-9]+$ ]]; then + Log::fatal "${SCRIPT_NAME} - invalid timeout option - must be greater or equal to 0" + fi +} + +optionAlgoCallback() { + if ! Array::contains "${optionAlgo}" "${availableAlgos[@]}"; then + Log::fatal "${SCRIPT_NAME} - invalid algorithm '${optionAlgo}'" + fi +} + +commandCallback() { + if [[ "${optionHostOrIp}" = "" || "${optionPort}" = "" ]]; then + Log::fatal "${SCRIPT_NAME} - you need to provide a host and port to test." + fi +} + +# default values +declare -a commandArgs=() +declare copyrightBeginYear="2020" +declare optionTimeout="<% ${defaultTimeout} %>" +declare optionAlgo="" +declare -a availableAlgos=(<% "${availableAlgos[@]}" %>) diff --git a/src/_binaries/Utils/waitForIt.sh b/src/_binaries/Utils/waitForIt.sh index 0e038107..cc632b7c 100755 --- a/src/_binaries/Utils/waitForIt.sh +++ b/src/_binaries/Utils/waitForIt.sh @@ -1,178 +1,167 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/waitForIt +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE +# shellcheck disable=SC2154 +# shellcheck disable=SC2317 -# Use this script to test if a given TCP host/port are available -# https://github.com/vishnubob/wait-for-it - -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" - -showHelp() { - cat < 0)); then - Log::displayInfo "${SCRIPT_NAME}: waiting ${TIMEOUT} seconds for ${HOST}:${PORT}" - else - Log::displayInfo "${SCRIPT_NAME}: waiting for ${HOST}:${PORT} without a timeout" - fi - local start_ts=${SECONDS} - while true; do - result=0 - if [[ "${ISBUSY}" = "1" ]]; then - (nc -z "${HOST}" "${PORT}") >/dev/null 2>&1 || result=$? || true +# Use this script to test if a given TCP host/port are available +# https://github.com/vishnubob/wait-for-it +waitForItCommand parse "${BASH_FRAMEWORK_ARGV[@]}" + +run() { + usingTcp() { + # couldn't find another way to mock this part + if [[ -n "${WAIT_FOR_IT_MOCKED_TCP:-}" ]]; then + "${WAIT_FOR_IT_MOCKED_TCP}" "/dev/tcp/${optionHostOrIp}/${optionPort}" 2>&1 else - (echo >"/dev/tcp/${HOST}/${PORT}") >/dev/null 2>&1 || result=$? || true + echo >"/dev/tcp/${optionHostOrIp}/${optionPort}" 2>&1 fi - if [[ "${result}" = "0" ]]; then - local end_ts=${SECONDS} - Log::displayInfo "${SCRIPT_NAME}: ${HOST}:${PORT} is available after $((end_ts - start_ts)) seconds" - break + } + + usingNc() { + nc -z "${optionHostOrIp}" "${optionPort}" -w 1 2>&1 + } + + whileLoop() { + local commandToUse="$1" + local reportTimeout="${2:-0}" + if ! Array::contains "${commandToUse}" "usingTcp" "usingNc"; then + Log::fatal "${SCRIPT_NAME} - can't call command ${commandToUse} in child mode" fi - sleep 1 - done - return "${result}" -} -waitForWrapper() { - local result - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - local -a ARGS=(--child "--host=${HOST}" "--port=${PORT}" "--timeout=${TIMEOUT}") - if [[ "${QUIET}" = "1" ]]; then - ARGS+=(--quiet) - fi - timeout "${BUSYTIMEFLAG}" "${TIMEOUT}" "$0" "${ARGS[@]}" & - - local pid=$! - # shellcheck disable=2064 - trap "kill -INT -${pid}" INT - wait "${pid}" - result=$? - if [[ "${result}" != "0" ]]; then - Log::displayError "${SCRIPT_NAME}: timeout occurred after waiting ${TIMEOUT} seconds for ${HOST}:${PORT}" - fi - return "${result}" -} + local -i start_ts=${SECONDS} + while true; do + if "${commandToUse}"; then + Log::displayInfo "${SCRIPT_NAME} - ${optionHostOrIp}:${optionPort} is available after $((SECONDS - start_ts)) seconds" + break + fi + if (( optionTimeout!=0 && SECONDS - start_ts >= optionTimeout)); then + if [[ "${reportTimeout}" = "1" ]]; then + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${optionHostOrIp}:${optionPort}" + fi + return 2 + fi + sleep 1 + done + return 0 + } -# process arguments -while [[ $# -gt 0 ]]; do - case "$1" in - *:*) - # shellcheck disable=2206 - hostPort=(${1//:/ }) - HOST=${hostPort[0]} - PORT=${hostPort[1]} - shift 1 || true - ;; - --child) - CHILD=1 - shift 1 || true - ;; - -q | --quiet) - QUIET=1 - shift 1 || true - ;; - -s | --strict) - STRICT=1 - shift 1 || true - ;; - -h) - HOST="$2" - if [[ "${HOST}" = "" ]]; then break; fi - shift 2 || true - ;; - --host=*) - HOST="${1#*=}" - shift 1 || true - ;; - -p) - PORT="$2" - if [[ "${PORT}" = "" ]]; then break; fi - shift 2 || true - ;; - --port=*) - PORT="${1#*=}" - shift 1 || true - ;; - -t) - TIMEOUT="$2" - if [[ "${TIMEOUT}" = "" ]]; then break; fi - shift 2 || true - ;; - --timeout=*) - TIMEOUT="${1#*=}" - shift 1 || true - ;; - --) - shift || true - CLI=("$@") - break - ;; - --help) - showHelp - exit 0 - ;; - *) - showHelp - Log::fatal "Unknown argument: $1" - ;; - esac -done - -if [[ "${HOST}" = "" || "${PORT}" = "" ]]; then - showHelp - Log::fatal "Error: you need to provide a host and port to test." -fi + timeoutCommand() { + local timeoutVersion="$1" + local commandToUse="$2" + local result + local -i start_ts=${SECONDS} -TIMEOUT=${TIMEOUT:-15} -STRICT=${STRICT:-0} -CHILD=${CHILD:-0} -QUIET=${QUIET:-0} - -# check to see if timeout is from busybox? -# check to see if timeout is from busybox? -TIMEOUT_PATH=$(dirname "$(command -v timeout)") -if [[ ${TIMEOUT_PATH} =~ "busybox" ]]; then - ISBUSY=1 - BUSYTIMEFLAG="-t" -else - ISBUSY=0 - BUSYTIMEFLAG="" -fi + if ! Array::contains "${commandToUse}" "usingTcp" "usingNc"; then + Log::fatal "${SCRIPT_NAME} - can't call command ${commandToUse} in timeout mode" + fi -if [[ ${CHILD} -gt 0 ]]; then - waitFor - RESULT=$? - exit "${RESULT}" -else - if [[ ${TIMEOUT} -gt 0 ]]; then - waitForWrapper - RESULT=$? + # compute timeout command + local -a timeoutCmd=(timeout) + if [[ "${timeoutVersion}" = "v1" ]]; then + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + timeoutCmd+=("-t") + fi + timeoutCmd+=( + "${optionTimeout}" + "$0" + "${ORIGINAL_BASH_FRAMEWORK_ARGV[@]}" + ) + WAIT_FOR_IT_TIMEOUT_CHILD_ALGO="${commandToUse}" "${timeoutCmd[@]}" & + + local pid=$! + # shellcheck disable=2064 + trap "kill -INT -${pid}" INT + wait "${pid}" + result=$? + if [[ "${result}" != "0" ]]; then + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${optionHostOrIp}:${optionPort}" + fi + return "${result}" + } + + # -------------------------------------- + # ALGORITHMS + timeoutV1WithNc() { + timeoutCommand "v1" "usingNc" + } + timeoutV2WithNc() { + timeoutCommand "v2" "usingNc" + } + whileLoopWithNc() { + whileLoop "usingNc" "1" + } + timeoutV1WithTcp() { + timeoutCommand "v1" "usingTcp" + } + timeoutV2WithTcp() { + timeoutCommand "v2" "usingTcp" + } + whileLoopWithTcp() { + whileLoop "usingTcp" "1" + } + # -------------------------------------- + + algorithmAutomaticSelection() { + if Array::contains "${optionAlgo}" "${availableAlgos[@]}"; then + echo "${optionAlgo}" + return 0 + fi + + local command="WithTcp" + if Assert::commandExists nc &>/dev/null; then + # nc has the -w option allowing for timeout + command="WithNc" + fi + + if (( optionTimeout > 0 )); then + if Assert::commandExists timeout &>/dev/null; then + if timeout --help 2>&1 | grep -q -E -e '--timeout '; then + echo "timeoutV1${command}" + else + echo "timeoutV2${command}" + fi + fi + return 0 + fi + echo "whileLoop${command}" + } + + local result="0" + if [[ -n "${WAIT_FOR_IT_TIMEOUT_CHILD_ALGO:-}" ]]; then + # parent process is executing timeout with current child process + # call algo nc or tcp inside whileLoop + whileLoop "${WAIT_FOR_IT_TIMEOUT_CHILD_ALGO}" "0" || result=$? else - waitFor - RESULT=$? - fi -fi -if [[ -n "${CLI+x}" && "${CLI[*]}" != "" ]]; then - if [[ "${RESULT}" != "0" && "${STRICT}" = "1" ]]; then - Log::displayError "${SCRIPT_NAME}: strict mode, refusing to execute sub-process" - exit "${RESULT}" + local algo="${optionAlgo}" + if [[ -z "${algo}" ]]; then + algo=$(algorithmAutomaticSelection) + fi + Log::displayInfo "${SCRIPT_NAME} - using algorithm ${algo}" + if ((optionTimeout > 0)); then + Log::displayInfo "${SCRIPT_NAME} - waiting ${optionTimeout} seconds for ${optionHostOrIp}:${optionPort}" + else + Log::displayInfo "${SCRIPT_NAME} - waiting for ${optionHostOrIp}:${optionPort} without a timeout" + fi + "${algo}" || result=$? + # when timed out, call command if any + if [[ -n "${commandArgs+x}" && "${commandArgs[*]}" != "" ]]; then + if [[ "${result}" != "0" && "${optionStrict}" = "1" ]]; then + Log::displayError "${SCRIPT_NAME} - failed to connect - strict mode - command not executed" + exit "${result}" + fi + exec "${commandArgs[@]}" + fi fi - exec "${CLI[@]}" + + exit "${result}" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null else - exit "${RESULT}" + run fi diff --git a/src/_binaries/Utils/waitForMysql.bats b/src/_binaries/Utils/waitForMysql.bats new file mode 100755 index 00000000..f607df46 --- /dev/null +++ b/src/_binaries/Utils/waitForMysql.bats @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +# shellcheck source=src/batsHeaders.sh +source "$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)/batsHeaders.sh" + +setup() { + export TMPDIR="${BATS_TEST_TMPDIR}" + + export HOME="${BATS_TEST_TMPDIR}/home" + mkdir -p "${HOME}" + mkdir -p "${HOME}/bin" + export PATH="${HOME}/bin:${PATH}" +} + +teardown() { + unstub_all + rm -f "${HOME}/bin/nc" || true +} + +function Utils::waitForMysql::display_help { #@test + # shellcheck disable=SC2154 + run "${binDir}/waitForMysql" --help 2>&1 + assert_success + assert_line --index 0 "DESCRIPTION: wait for mysql to be ready" +} + +function Utils::waitForMysql::missingHost { #@test + run "${binDir}/waitForMysql" 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForMysql - Argument 'mysqlHost' should be provided at least 1 time(s)" +} + +function Utils::waitForMysql::missingPort { #@test + run "${binDir}/waitForMysql" localhost 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForMysql - Argument 'mysqlPort' should be provided at least 1 time(s)" +} + +function Utils::waitForMysql::missingUser { #@test + run "${binDir}/waitForMysql" localhost 3306 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForMysql - Argument 'mysqlUserArg' should be provided at least 1 time(s)" +} + +function Utils::waitForMysql::missingPassword { #@test + run "${binDir}/waitForMysql" localhost 3306 user 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - Command waitForMysql - Argument 'mysqlPasswordArg' should be provided at least 1 time(s)" +} + +function Utils::waitForMysql::invalidTimeout { #@test + run "${binDir}/waitForMysql" localhost 3306 user password --timeout invalid 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "FATAL - waitForMysql - invalid timeout option - must be greater or equal to 0" +} + +function Utils::waitForMysql::mysqlCommandNotFound { #@test + stub commandNotFound '-v mysql : exit 1' + export BASH_FRAMEWORK_COMMAND=commandNotFound + run "${binDir}/waitForMysql" localhost 3306 user password --timeout 1 2>&1 + + assert_failure 1 + assert_lines_count 1 + assert_output --partial "ERROR - mysql is not installed, please install it" +} + +function Utils::waitForMysql::mysqlAvailable { #@test + stub commandExists '-v mysql : exit 0' + export BASH_FRAMEWORK_COMMAND=commandExists + stub mysql '-hlocalhost -P3306 -uuser -ppassword : exit 0' + run "${binDir}/waitForMysql" localhost 3306 user password --timeout 1 2>&1 + + assert_success + assert_line --index 0 --partial "INFO - Waiting for mysql" + assert_line --index 1 --partial "." + assert_line --index 2 --partial "INFO - mysql ready" + assert_lines_count 3 +} + +function Utils::waitForMysql::mysqlNotAvailableAfter1SecondTimeout { #@test + stub commandExists '-v mysql : exit 0' + export BASH_FRAMEWORK_COMMAND=commandExists + stub mysql '-hlocalhost -P3306 -uuser -ppassword : exit 1' + run "${binDir}/waitForMysql" localhost 3306 user password --timeout 1 2>&1 + + assert_failure 2 + assert_line --index 0 --partial "INFO - Waiting for mysql" + assert_line --index 1 --partial "." + assert_line --index 2 --partial "ERROR - waitForMysql - timeout occurred after 1 seconds for localhost:3306" + assert_lines_count 3 +} diff --git a/src/_binaries/Utils/waitForMysql.options.tpl b/src/_binaries/Utils/waitForMysql.options.tpl new file mode 100644 index 00000000..7ea1ad49 --- /dev/null +++ b/src/_binaries/Utils/waitForMysql.options.tpl @@ -0,0 +1,81 @@ +% +declare versionNumber="2.0" +declare commandFunctionName="waitForMysqlCommand" +declare help="wait for mysql to be ready" +# shellcheck disable=SC2016 +declare longDescription=""" +${__HELP_TITLE}EXIT STATUS CODES:${__HELP_NORMAL} +${__HELP_OPTION_COLOR}0${__HELP_NORMAL}: mysql is available +${__HELP_OPTION_COLOR}1${__HELP_NORMAL}: indicates mysql is not available or argument error +${__HELP_OPTION_COLOR}2${__HELP_NORMAL}: timeout reached +""" +declare defaultTimeout="15" +% +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +% +# shellcheck source=/dev/null +source <( + Options::generateArg \ + --help "Mysql host name" \ + --name "mysqlHost" \ + --variable-name "mysqlHostArg" \ + --function-name mysqlHostArgFunction + + mysqlPortArgCallback() { :; } + Options::generateArg \ + --help "Mysql port" \ + --name "mysqlPort" \ + --variable-name "mysqlPortArg" \ + --callback mysqlPortArgCallback \ + --function-name mysqlPortArgFunction + + Options::generateArg \ + --help "Mysql user name" \ + --name "mysqlUserArg" \ + --variable-name "mysqlUserArg" \ + --function-name mysqlUserArgFunction + + Options::generateArg \ + --help "Mysql password" \ + --name "mysqlPasswordArg" \ + --variable-name "mysqlPasswordArg" \ + --function-name mysqlPasswordArgFunction + + optionTimeoutCallback() { :; } + Options::generateOption \ + --help-value-name "timeout" \ + --help "Timeout in seconds, zero for no timeout." \ + --default-value "${defaultTimeout}" \ + --alt "--timeout" \ + --alt "-t" \ + --variable-type "String" \ + --variable-name "optionTimeout" \ + --function-name optionTimeoutFunction \ + --callback optionTimeoutCallback +) +options+=( + mysqlHostArgFunction + mysqlPortArgFunction + mysqlUserArgFunction + mysqlPasswordArgFunction + optionTimeoutFunction +) +Options::generateCommand "${options[@]}" +% + +mysqlPortArgCallback() { + if [[ ! "${mysqlPortArg}" =~ ^[0-9]+$ ]] || (( mysqlPortArg == 0 )); then + Log::fatal "${SCRIPT_NAME} - invalid port option - must be greater than to 0" + fi +} + +optionTimeoutCallback() { + if [[ ! "${optionTimeout}" =~ ^[0-9]+$ ]]; then + Log::fatal "${SCRIPT_NAME} - invalid timeout option - must be greater or equal to 0" + fi +} + + +# default values +declare copyrightBeginYear="2020" +declare optionTimeout="<% ${defaultTimeout} %>" diff --git a/src/_binaries/Utils/waitForMysql.sh b/src/_binaries/Utils/waitForMysql.sh index 2ce1ef90..74c72edb 100755 --- a/src/_binaries/Utils/waitForMysql.sh +++ b/src/_binaries/Utils/waitForMysql.sh @@ -1,29 +1,39 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/waitForMysql +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE +# shellcheck disable=SC2154 +# shellcheck disable=SC2317 -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/Utils/waitForMysql.options.tpl)" -HELP="$( - cat < - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/author.tpl" -EOF -)" -Args::defaultHelp "${HELP}" "$@" - -declare mysqlHost="$1" -declare mysqlPort="$2" -declare mysqlUser="$3" -declare mysqlPass="$4" - -(echo >&2 "Waiting for mysql") -until (echo "select 1" | mysql -h"${mysqlHost}" -P"${mysqlPort}" -u"${mysqlUser}" -p"${mysqlPass}" &>/dev/null); do +run() { + Assert::commandExists "mysql" + Log::displayInfo "Waiting for mysql" + local -i start_ts=${SECONDS} (printf >&2 ".") - sleep 1 -done + until (echo "select 1" | mysql \ + -h"${mysqlHostArg}" \ + -P"${mysqlPortArg}" \ + -u"${mysqlUserArg}" \ + -p"${mysqlPasswordArg}" &>/dev/null); do + (printf >&2 ".") + if (( optionTimeout!=0 && SECONDS - start_ts >= optionTimeout)); then + (echo >&2 "") + Log::displayError "${SCRIPT_NAME} - timeout occurred after $((SECONDS - start_ts)) seconds for ${mysqlHostArg}:${mysqlPortArg}" + return 2 + fi + sleep 1 + done + + (echo >&2 "") + Log::displayInfo "mysql ready" +} -(echo >&2 -e "\nmysql ready") +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/build/doc.options.tpl b/src/_binaries/build/doc.options.tpl new file mode 100644 index 00000000..59191f05 --- /dev/null +++ b/src/_binaries/build/doc.options.tpl @@ -0,0 +1,35 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="docCommand" +declare help="generate markdown documentation" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.skipDockerBuild.tpl)" + +% +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +declare -a RUN_CONTAINER_ARGV_FILTERED=() +updateOptionSkipDockerBuildCallback() { + if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + BASH_FRAMEWORK_ARGV_FILTERED+=("$1") + RUN_CONTAINER_ARGV_FILTERED+=("$1") + fi +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListInfoVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(--verbose) + BASH_FRAMEWORK_ARGV_FILTERED+=(--verbose) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListDebugVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(-vv) + BASH_FRAMEWORK_ARGV_FILTERED+=(-vv) +} +# shellcheck disable=SC2317 # if function is overridden +updateArgListTraceVerboseCallback() { + RUN_CONTAINER_ARGV_FILTERED+=(-vvv) + BASH_FRAMEWORK_ARGV_FILTERED+=(-vvv) +} diff --git a/src/_binaries/build/doc.sh b/src/_binaries/build/doc.sh index f9aa352e..ddf8e3bd 100755 --- a/src/_binaries/build/doc.sh +++ b/src/_binaries/build/doc.sh @@ -1,78 +1,80 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/doc +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" -showHelp() { -cat </,//d' \ - -e 's#https://fchastanet.github.io/bash-tools/#/#' \ - -e 's#^> \*\*_TIP:_\*\* (.*)$#> [!TIP|label:\1]#' \ - "${DOC_DIR}/README.md" + mkdir -p "${DOC_DIR}/src/_binaries/Converters/testsData" || true + cp "${BASH_TOOLS_ROOT_DIR}/src/_binaries/Converters/testsData/mysql2puml-model.png" "${DOC_DIR}/src/_binaries/Converters/testsData" -ShellDoc::fixMarkdownToc "${DOC_DIR}/README.md" -ShellDoc::fixMarkdownToc "${DOC_DIR}/Commands.md" + # copy other files + cp "${BASH_TOOLS_ROOT_DIR}/README.md" "${DOC_DIR}/README.md" + sed -i -E \ + -e '//,//d' \ + -e 's#https://fchastanet.github.io/bash-tools/#/#' \ + -e 's#^> \*\*_TIP:_\*\* (.*)$#> [!TIP|label:\1]#' \ + "${DOC_DIR}/README.md" -if ((TOKEN_NOT_FOUND_COUNT > 0)); then - exit 1 -fi + ShellDoc::fixMarkdownToc "${DOC_DIR}/README.md" + ShellDoc::fixMarkdownToc "${DOC_DIR}/Commands.md" + + if ((TOKEN_NOT_FOUND_COUNT > 0)); then + return 1 + fi -Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" + Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" +} + +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null +else + run +fi diff --git a/src/_binaries/build/install.options.tpl b/src/_binaries/build/install.options.tpl new file mode 100644 index 00000000..824894a2 --- /dev/null +++ b/src/_binaries/build/install.options.tpl @@ -0,0 +1,15 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="installCommand" +declare help="Install dependent softwares and configuration needed to use bash-tools +- GNU parallel +- Install default configuration files" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +Options::generateCommand "${options[@]}" +% +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" diff --git a/src/_binaries/build/install.sh b/src/_binaries/build/install.sh index 68d254c6..9222bc47 100755 --- a/src/_binaries/build/install.sh +++ b/src/_binaries/build/install.sh @@ -1,43 +1,45 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/install +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/build/install.options.tpl)" -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/executedAsUser.sh" +installCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -HELP="$( - cat </dev/null; then + Log::displayInfo "We will install GNU parallel software, please enter you sudo password" + sudo apt update || true + if sudo apt install -y parallel; then + # remove parallel nagware + mkdir -p ~/.parallel + touch ~/.parallel/will-cite + else + Log::displayWarning "Impossible to install GNU parallel, please install it manually" + fi + else + Log::displaySkipped "parallel is already installed" + fi -if ! command -v parallel 2>/dev/null; then - Log::displayInfo "We will install GNU parallel software, please enter you sudo password" - sudo apt update || true - if sudo apt install -y parallel; then - # remove parallel nagware - mkdir -p ~/.parallel - touch ~/.parallel/will-cite + if [[ -d "${HOME}/.bash-tools" ]]; then + Log::displayInfo "Updating configuration" + cp -R --no-clobber "${BASH_TOOLS_ROOT_DIR}/conf/." "${HOME}/.bash-tools" + if [[ "${BASE_DIR}/conf/.env" -nt "${HOME}/.bash-tools/.env" ]]; then + Log::displayWarning "${BASE_DIR}/conf/.env is newer than ${HOME}/.bash-tools/.env, compare the files to check if some updates need to be applied" + else + Log::displaySkipped "${HOME}/.bash-tools/.env is up to date" + fi else - Log::displayWarning "Impossible to install GNU parallel, please install it manually" + Log::displayInfo "Installing configuration in ~/.bash-tools" + mkdir -p ~/.bash-tools + cp -R conf/. ~/.bash-tools fi -fi +} -if [[ -d "${HOME}/.bash-tools" ]]; then - # update - cp -R --no-clobber "${BASH_TOOLS_ROOT_DIR}/conf/." "${HOME}/.bash-tools" - [[ "${BASE_DIR}/conf/.env" -nt "${HOME}/.bash-tools/.env" ]] && { - Log::displayWarning "${BASE_DIR}/conf/.env is newer than ${HOME}/.bash-tools/.env, compare the files to check if some updates need to be applied" - } +if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then + run &>/dev/null else - mkdir -p ~/.bash-tools - cp -R conf/. ~/.bash-tools + run fi diff --git a/src/_binaries/build/installRequirements.options.tpl b/src/_binaries/build/installRequirements.options.tpl new file mode 100644 index 00000000..47ef4b9c --- /dev/null +++ b/src/_binaries/build/installRequirements.options.tpl @@ -0,0 +1,31 @@ +% +declare -a externalBinaries=( + bin/awkLint + bin/buildBinFiles + bin/frameworkLint + bin/findShebangFiles + bin/megalinter + bin/runBuildContainer + bin/shellcheckLint + bin/test + bin/buildPushDockerImage +) +declare versionNumber="1.0" +declare commandFunctionName="installRequirementsCommand" +declare help="installs requirements" +declare longDescription=""" +${__HELP_TITLE}INSTALLS REQUIREMENTS:${__HELP_NORMAL} +- fchastanet/bash-tools-framework +- and fchastanet/bash-tools-framework useful binaries: + $(Array::join ', ' "${externalBinaries[@]}") +""" +% + +.INCLUDE "$(dynamicTemplateDir _binaries/options/options.base.tpl)" + +% +Options::generateCommand "${options[@]}" +declare -p externalBinaries +% +declare copyrightBeginYear="2020" +declare optionBashFrameworkConfig="${BASH_TOOLS_ROOT_DIR}/.framework-config" diff --git a/src/_binaries/build/installRequirements.sh b/src/_binaries/build/installRequirements.sh index 97db8ae4..b6bc059b 100755 --- a/src/_binaries/build/installRequirements.sh +++ b/src/_binaries/build/installRequirements.sh @@ -1,37 +1,32 @@ #!/usr/bin/env bash # BIN_FILE=${FRAMEWORK_ROOT_DIR}/bin/installRequirements +# VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. +# FACADE -.INCLUDE "$(dynamicTemplateDir _includes/_header.tpl)" -.INCLUDE "$(dynamicTemplateDir _includes/_load.tpl)" +.INCLUDE "$(dynamicTemplateDir _binaries/build/installRequirements.options.tpl)" -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/executedAsUser.sh" +installRequirementsCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -HELP="$( - cat </dev/null +else + run +fi diff --git a/src/_binaries/options/options.mysql.collationName.tpl b/src/_binaries/options/options.mysql.collationName.tpl new file mode 100644 index 00000000..affb83b8 --- /dev/null +++ b/src/_binaries/options/options.mysql.collationName.tpl @@ -0,0 +1,27 @@ +% +declare defaultTargetCollationName="utf8_general_ci" + +# shellcheck source=/dev/null +source <( + + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo \ + "change the collation name used during database creation" \ + "(default value: ${defaultTargetCollationName})" \ + )" \ + --variable-type "String" \ + --group groupTargetOptionsFunction \ + --alt "--collation-name" \ + --alt "-o" \ + --variable-name "optionCollationName" \ + --function-name optionCollationNameFunction + + ) + options+=( + optionCollationNameFunction + ) +% + +declare optionCollationName="" # old COLLATION_NAME +declare defaultTargetCollationName="<% ${defaultTargetCollationName} %>" diff --git a/src/_binaries/options/options.mysql.target.tpl b/src/_binaries/options/options.mysql.target.tpl new file mode 100644 index 00000000..8689c936 --- /dev/null +++ b/src/_binaries/options/options.mysql.target.tpl @@ -0,0 +1,61 @@ +% +declare defaultTargetDsn="default.local" +declare defaultTargetCharacterSet="utf8" + +# shellcheck source=/dev/null +source <( + Options::generateGroup \ + --title "TARGET OPTIONS:" \ + --function-name groupTargetOptionsFunction + + + Options::generateOption \ + --help "dsn to use for target database (Default: ${defaultTargetDsn})" \ + --help-value-name "targetDsn" \ + --variable-type "String" \ + --group groupTargetOptionsFunction \ + --alt "--target-dsn" \ + --alt "-t" \ + --variable-name "optionTargetDsn" \ + --function-name optionTargetDsnFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help "$(echo \ + "change the character set used during database creation" \ + "(default value: ${defaultTargetCharacterSet})" \ + )" \ + --variable-type "String" \ + --group groupTargetOptionsFunction \ + --alt "--character-set" \ + --alt "-c" \ + --variable-name "optionCharacterSet" \ + --function-name optionCharacterSetFunction +) +options+=( + optionTargetDsnFunction + optionCharacterSetFunction +) +% + +declare optionTargetDsn="<% ${defaultTargetDsn} %>" # old TARGET_DSN +declare optionCharacterSet="" # old CHARACTER_SET +declare defaultTargetCharacterSet="<% ${defaultTargetCharacterSet} %>" + + +initializeDefaultTargetMysqlOptions() { + local -n dbFromInstanceTargetMysql=$1 + local fromDbName="$2" + + # get remote db collation name + if [[ -n ${optionCollationName+x} && -z "${optionCollationName}" ]]; then + optionCollationName=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_collation_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi + + # get remote db character set + if [[ -z "${optionCharacterSet}" ]]; then + optionCharacterSet=$(Database::query dbFromInstanceTargetMysql \ + "SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = \"${fromDbName}\";" "information_schema") + fi +} diff --git a/src/_binaries/options/options.profile.tpl b/src/_binaries/options/options.profile.tpl new file mode 100644 index 00000000..38e6328d --- /dev/null +++ b/src/_binaries/options/options.profile.tpl @@ -0,0 +1,91 @@ +% +# shellcheck source=/dev/null +source <( + Options::generateGroup \ + --title "PROFILE OPTIONS:" \ + --function-name groupProfileOptionsFunction + + profileOptionHelpCallback() { :; } + Options::generateOption \ + --variable-type String \ + --group groupProfileOptionsFunction \ + --help profileOptionHelpCallback \ + --alt "--profile" \ + --alt "-p" \ + --callback "profileOptionCallback" \ + --variable-name "optionProfile" \ + --function-name optionProfileFunction + + # shellcheck disable=SC2116 + Options::generateOption \ + --help-value-name "table1,table2,..." \ + --help "$(echo \ + "import only table specified in the list. " \ + "If aws mode, ignore profile option" \ + )" \ + --group groupProfileOptionsFunction \ + --alt "--tables" \ + --callback optionTablesCallback \ + --variable-type "String" \ + --variable-name "optionTables" \ + --function-name optionTablesFunction + +) +options+=( + optionProfileFunction + optionTablesFunction + --callback initProfileCommandCallback +) +% + +# default values +declare optionProfile="default" +declare optionTables="" +declare profileCommand="" + +profileOptionHelpCallback() { + echo "the name of the profile to use in order to include or exclude tables" + echo "(if not specified ${HOME_PROFILES_DIR}/default.sh is used if exists otherwise ${PROFILES_DIR}/default.sh)" +} + +optionTablesCallback() { + if [[ ! ${optionTables} =~ ^[A-Za-z0-9_]+(,[A-Za-z0-9_]+)*$ ]]; then + Log::fatal "Command ${SCRIPT_NAME} - Table list is not valid : ${optionTables}" + fi +} + +profileOptionCallback() { + local -a profilesList + readarray -t profilesList < <(Conf::getMergedList "dbImportProfiles" "sh" "" || true) + if ! Array::contains "$2" "${profilesList[@]}"; then + Log::displayError "${SCRIPT_NAME} - invalid profile '$2' provided" + return 1 + fi +} +initProfileCommandCallback() { + if [[ "${optionProfile}" != "default" && -n "${optionTables}" ]]; then + Log::fatal "Command ${SCRIPT_NAME} - you cannot use table and profile options at the same time" + fi + + # Profile selection + local profileMsgInfo + # shellcheck disable=SC2154 + if [[ "${optionProfile}" = 'default' && -n "${optionTables}" ]]; then + profileCommand=$(Framework::createTempFile "profileCmd.XXXXXXXXXXXX") + profileMsgInfo="only ${optionTables} will be imported" + ( + echo '#!/usr/bin/env bash' + if [[ -n "${optionTables}" ]]; then + echo "${optionTables}" | sed -E 's/([A-Za-z0-9_]+),?/echo "\1"\n/g' + else + # tables option not specified, we will import all tables of the profile + echo 'cat' + fi + ) >"${profileCommand}" + else + profileCommand="$(Conf::getAbsoluteFile "dbImportProfiles" "${optionProfile}" "sh")" || exit 1 + profileMsgInfo="Using profile ${profileCommand}" + fi + chmod +x "${profileCommand}" + Log::displayInfo "${profileMsgInfo}" +} diff --git a/src/_includes/_header.tpl b/src/_includes/_header.tpl deleted file mode 100755 index df3cf6a1..00000000 --- a/src/_includes/_header.tpl +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/_header.tpl" - -BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/.." && pwd -P)" -if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then - FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" -else - # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR - FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" -fi -export BASH_TOOLS_ROOT_DIR FRAMEWORK_ROOT_DIR - -if [[ -f "${HOME}/.bash-tools/.env" ]]; then - export BASH_FRAMEWORK_ENV_FILEPATH="${HOME}/.bash-tools/.env" -fi diff --git a/src/_includes/_initFrameworkVariables.tpl b/src/_includes/_initFrameworkVariables.tpl new file mode 100644 index 00000000..1bc562b1 --- /dev/null +++ b/src/_includes/_initFrameworkVariables.tpl @@ -0,0 +1,19 @@ +.DELIMS stmt="%" +% if [[ -n "${RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR}" ]]; then +BASH_TOOLS_ROOT_DIR="$(cd "${CURRENT_DIR}/<% ${RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR} %>" && pwd -P)" +if [[ -d "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework/" ]]; then + FRAMEWORK_ROOT_DIR="$(cd "${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" && pwd -P)" +else + # if the directory does not exist yet, give a value to FRAMEWORK_ROOT_DIR + FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" +fi +FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" +FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" +FRAMEWORK_VENDOR_DIR="${FRAMEWORK_ROOT_DIR}/vendor" +FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" + +if [[ -f "${HOME}/.bash-tools/.env" ]]; then + export BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") +fi +% fi +.RESET-DELIMS diff --git a/src/_includes/_load.tpl b/src/_includes/_load.tpl deleted file mode 100755 index b2607d19..00000000 --- a/src/_includes/_load.tpl +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC2034 -.INCLUDE "${ORIGINAL_TEMPLATE_DIR}/_includes/_load.tpl" diff --git a/src/batsHeaders.sh b/src/batsHeaders.sh index dd32f0b7..3084f0c6 100755 --- a/src/batsHeaders.sh +++ b/src/batsHeaders.sh @@ -13,8 +13,8 @@ load "${FRAMEWORK_ROOT_DIR}/vendor/bats-mock-Flamefire/load.bash" # shellcheck source=vendor/bash-tools-framework/src/_standalone/Bats/assert_lines_count.sh source "${FRAMEWORK_ROOT_DIR}/src/_standalone/Bats/assert_lines_count.sh" -# shellcheck source=vendor/bash-tools-framework/src/Env/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Env/load.sh" +# shellcheck source=vendor/bash-tools-framework/src/Env/__all.sh +source "${FRAMEWORK_ROOT_DIR}/src/Env/__all.sh" # shellcheck source=vendor/bash-tools-framework/src/Log/_.sh source "${FRAMEWORK_ROOT_DIR}/src/Log/_.sh" # shellcheck source=vendor/bash-tools-framework/src/Log/displayDebug.sh @@ -53,10 +53,10 @@ source "${FRAMEWORK_ROOT_DIR}/src/Log/logSuccess.sh" source "${FRAMEWORK_ROOT_DIR}/src/Log/logWarning.sh" # shellcheck source=vendor/bash-tools-framework/src/Log/rotate.sh source "${FRAMEWORK_ROOT_DIR}/src/Log/rotate.sh" -# shellcheck source=vendor/bash-tools-framework/src/Log/load.sh -source "${FRAMEWORK_ROOT_DIR}/src/Log/load.sh" +# shellcheck source=vendor/bash-tools-framework/src/Log/requireLoad.sh +source "${FRAMEWORK_ROOT_DIR}/src/Log/requireLoad.sh" export BASH_FRAMEWORK_LOG_FILE="${BATS_TEST_TMPDIR}/logFile" export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_INFO}" -Env::load -Log::load +Env::requireLoad +Log::requireLoad diff --git a/waitForIt.log b/waitForIt.log new file mode 100644 index 00000000..78f995b6 --- /dev/null +++ b/waitForIt.log @@ -0,0 +1,980 @@ ++ facade_main_d396e2bc1f6e43a7b79e8a25ad41ac25 --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp +++ cd /home/wsl/fchastanet/bash-tools/bin/.. +++ pwd -P ++ BASH_TOOLS_ROOT_DIR=/home/wsl/fchastanet/bash-tools ++ [[ -d /home/wsl/fchastanet/bash-tools/vendor/bash-tools-framework/ ]] +++ cd /home/wsl/fchastanet/bash-tools/vendor/bash-tools-framework +++ pwd -P ++ FRAMEWORK_ROOT_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ FRAMEWORK_SRC_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/src ++ FRAMEWORK_BIN_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/bin ++ FRAMEWORK_VENDOR_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/vendor ++ FRAMEWORK_VENDOR_BIN_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/vendor/bin ++ [[ -f /home/wsl/.bash-tools/.env ]] ++ BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") ++ export BASH_FRAMEWORK_ENV_FILES ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n 1 ]] +++ configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ++++ echo BASH_FRAMEWORK_THEME=default ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=2 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /home/wsl/.bash-tools/.env ]] +++ [[ ! -r /home/wsl/.bash-tools/.env ]] +++ echo /home/wsl/.bash-tools/.env +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ]] +++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ++ configFilesStr='/home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf' ++ local -a configFiles ++ readarray -t configFiles ++ (( 2 == 0 )) ++ [[ -z /home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ]] ++ Env::mergeConfFiles /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ++ configFileList=("$@") ++ local -a configFileList ++ (( 2 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.B27smZI9i3Hj ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.mjXFmo1Wm5qf ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.B27smZI9i3Hj +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=default +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.B27smZI9i3Hj +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=default +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ UI::requireTheme ++ UI::theme default ++ local theme=default ++ [[ ! default =~ -force$ ]] ++ Assert::tty ++ [[ 0 = \1 ]] ++ [[ 0 = \1 ]] ++ [[ -t 1 ]] ++ [[ -t 2 ]] ++ theme=noColor ++ case "${theme}" in ++ [[ noColor = \d\e\f\a\u\l\t ]] ++ export BASH_FRAMEWORK_THEME=noColor ++ BASH_FRAMEWORK_THEME=noColor ++ export __ERROR_COLOR= ++ __ERROR_COLOR= ++ export __INFO_COLOR= ++ __INFO_COLOR= ++ export __SUCCESS_COLOR= ++ __SUCCESS_COLOR= ++ export __WARNING_COLOR= ++ __WARNING_COLOR= ++ export __SKIPPED_COLOR= ++ __SKIPPED_COLOR= ++ export __DEBUG_COLOR= ++ __DEBUG_COLOR= ++ export __HELP_COLOR= ++ __HELP_COLOR= ++ export __TEST_COLOR= ++ __TEST_COLOR= ++ export __TEST_ERROR_COLOR= ++ __TEST_ERROR_COLOR= ++ export __HELP_TITLE_COLOR= ++ __HELP_TITLE_COLOR= ++ export __HELP_OPTION_COLOR= ++ __HELP_OPTION_COLOR= ++ export __RESET_COLOR= ++ __RESET_COLOR= ++ export __HELP_EXAMPLE= ++ __HELP_EXAMPLE= ++ export __HELP_TITLE= ++ __HELP_TITLE= ++ export __HELP_NORMAL= ++ __HELP_NORMAL= ++ Log::requireLoad ++ [[ -z /home/wsl/.bash-tools/logs/bash.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ Compiler::Facade::requireCommandBinDir ++ COMMAND_BIN_DIR=/home/wsl/fchastanet/bash-tools/bin ++ Env::pathPrepend /home/wsl/fchastanet/bash-tools/bin ++ local arg ++ for arg in "$@" ++ [[ -d /home/wsl/fchastanet/bash-tools/bin ]] ++ [[ :/home/wsl/.virtualenv/python3.9/bin:/home/wsl/.bin:/home/wsl/.local/bin:/home/wsl/fchastanet/bash-tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/c/Python311/Scripts/:/c/Python311/:/c/Program Files/Common Files/Oracle/Java/javapath:/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/c/PROGRA~1/AdoptOpenJDK/jdk-13.0.2.8-hotspot/bin:/c/Windows/system32:/c/Windows:/c/Windows/System32/Wbem:/c/Windows/System32/WindowsPowerShell/v1.0/:/c/Windows/System32/OpenSSH/:/c/PROGRA~1/WindowsPowerShell/Scripts:/c/PROGRA~1/IcedTeaWeb/WebStart/bin:/c/PROGRA~1/dotnet/:/c/PROGRA~2/WI3CF2~1/10/WINDOW~1:/c/PROGRA~2/Meld/:/c/WINDOWS/system32:/c/WINDOWS:/c/WINDOWS/System32/Wbem:/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/WINDOWS/System32/OpenSSH/:/c/PROGRA~1/Git/cmd:/c/Program Files/dotnet/:/c/ProgramData/chocolatey/bin:/c/Program Files/PowerShell/7/:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/PROGRA~1/JetBrains/PHPSTO~1.2/bin:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/c/Users/fchastanet/AppData/Local/JetBrains/Toolbox/scripts:/c/Users/fchastanet/.dotnet/tools:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/Users/fchastanet/AppData/Local/Programs/Microsoft VS Code/bin:/c/WINDOWS/system32:/mnt/c/Windows:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/usr/local/.composer/vendor/bin:/home/wsl/go/bin:/home/wsl/n/bin:/opt/kubectx:/home/wsl/.fzf/bin: != *\:\/\h\o\m\e\/\w\s\l\/\f\c\h\a\s\t\a\n\e\t\/\b\a\s\h\-\t\o\o\l\s\/\b\i\n\:* ]] ++ Linux::requireExecutedAsUser +++ id -u ++ [[ 1000 = \0 ]] ++ BASH_FRAMEWORK_ARGV_FILTERED=() ++ declare -a BASH_FRAMEWORK_ARGV_FILTERED ++ commandArgs=() ++ declare -a commandArgs ++ declare copyrightBeginYear=2020 ++ declare optionTimeout=15 ++ declare optionAlgo= ++ availableAlgos=(timeoutV1WithNc timeoutV2WithNc whileLoopWithNc timeoutV1WithTcp timeoutV2WithTcp whileLoopWithTcp) ++ declare -a availableAlgos ++ waitForItCommand parse --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp ++ local options_parse_cmd=parse ++ shift ++ [[ parse = \p\a\r\s\e ]] ++ local -i options_parse_optionParsedCountOptionHostOrIp ++ (( options_parse_optionParsedCountOptionHostOrIp = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionPort ++ (( options_parse_optionParsedCountOptionPort = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionAlgo ++ (( options_parse_optionParsedCountOptionAlgo = 0 )) ++ true ++ optionStrict=0 ++ local -i options_parse_optionParsedCountOptionStrict ++ (( options_parse_optionParsedCountOptionStrict = 0 )) ++ true ++ optionTimeout=15 ++ local -i options_parse_optionParsedCountOptionTimeout ++ (( options_parse_optionParsedCountOptionTimeout = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionBashFrameworkConfig ++ (( options_parse_optionParsedCountOptionBashFrameworkConfig = 0 )) ++ true ++ optionConfig=0 ++ local -i options_parse_optionParsedCountOptionConfig ++ (( options_parse_optionParsedCountOptionConfig = 0 )) ++ true ++ optionInfoVerbose=0 ++ local -i options_parse_optionParsedCountOptionInfoVerbose ++ (( options_parse_optionParsedCountOptionInfoVerbose = 0 )) ++ true ++ optionDebugVerbose=0 ++ local -i options_parse_optionParsedCountOptionDebugVerbose ++ (( options_parse_optionParsedCountOptionDebugVerbose = 0 )) ++ true ++ optionTraceVerbose=0 ++ local -i options_parse_optionParsedCountOptionTraceVerbose ++ (( options_parse_optionParsedCountOptionTraceVerbose = 0 )) ++ true ++ optionNoColor=0 ++ local -i options_parse_optionParsedCountOptionNoColor ++ (( options_parse_optionParsedCountOptionNoColor = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionTheme ++ (( options_parse_optionParsedCountOptionTheme = 0 )) ++ true ++ optionHelp=0 ++ local -i options_parse_optionParsedCountOptionHelp ++ (( options_parse_optionParsedCountOptionHelp = 0 )) ++ true ++ optionVersion=0 ++ local -i options_parse_optionParsedCountOptionVersion ++ (( options_parse_optionParsedCountOptionVersion = 0 )) ++ true ++ optionQuiet=0 ++ local -i options_parse_optionParsedCountOptionQuiet ++ (( options_parse_optionParsedCountOptionQuiet = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogLevel ++ (( options_parse_optionParsedCountOptionLogLevel = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogFile ++ (( options_parse_optionParsedCountOptionLogFile = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionDisplayLevel ++ (( options_parse_optionParsedCountOptionDisplayLevel = 0 )) ++ true ++ local -i options_parse_argParsedCountCommandArgs ++ (( options_parse_argParsedCountCommandArgs = 0 )) ++ true ++ local -i options_parse_parsedArgIndex=0 ++ (( 8 > 0 )) ++ local options_parse_arg=--host ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 7 == 0 )) ++ (( options_parse_optionParsedCountOptionHostOrIp >= 1 )) ++ (( ++options_parse_optionParsedCountOptionHostOrIp )) ++ optionHostOrIp=localhost ++ shift ++ (( 6 > 0 )) ++ local options_parse_arg=--port ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 5 == 0 )) ++ (( options_parse_optionParsedCountOptionPort >= 1 )) ++ (( ++options_parse_optionParsedCountOptionPort )) ++ optionPort=888 ++ optionPortCallback --port 888 ++ [[ ! 888 =~ ^[0-9]+$ ]] ++ (( optionPort == 0 )) ++ shift ++ (( 4 > 0 )) ++ local options_parse_arg=--timeout ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 3 == 0 )) ++ (( options_parse_optionParsedCountOptionTimeout >= 1 )) ++ (( ++options_parse_optionParsedCountOptionTimeout )) ++ optionTimeout=1 ++ optionTimeoutCallback --timeout 1 ++ [[ ! 1 =~ ^[0-9]+$ ]] ++ shift ++ (( 2 > 0 )) ++ local options_parse_arg=--algo ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 1 == 0 )) ++ (( options_parse_optionParsedCountOptionAlgo >= 1 )) ++ (( ++options_parse_optionParsedCountOptionAlgo )) ++ optionAlgo=timeoutV2WithTcp ++ optionAlgoCallback --algo timeoutV2WithTcp ++ Array::contains timeoutV2WithTcp timeoutV1WithNc timeoutV2WithNc whileLoopWithNc timeoutV1WithTcp timeoutV2WithTcp whileLoopWithTcp ++ local element ++ for element in "${@:2}" ++ [[ timeoutV1WithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV2WithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ whileLoopWithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV1WithTcp = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV2WithTcp = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ return 0 ++ shift ++ (( 0 > 0 )) ++ (( options_parse_optionParsedCountOptionHostOrIp < 1 )) ++ (( options_parse_optionParsedCountOptionPort < 1 )) ++ commandOptionParseFinished ++ [[ -z 1 ]] ++ BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") ++ export BASH_FRAMEWORK_ENV_FILES ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n 1 ]] +++ configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ++++ echo BASH_FRAMEWORK_THEME=noColor ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=3 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /home/wsl/.bash-tools/.env ]] +++ [[ ! -r /home/wsl/.bash-tools/.env ]] +++ echo /home/wsl/.bash-tools/.env +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ]] +++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ++ configFilesStr='/home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X' ++ local -a configFiles ++ readarray -t configFiles ++ (( 2 == 0 )) ++ [[ -z /home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ]] ++ Env::mergeConfFiles /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ++ configFileList=("$@") ++ local -a configFileList ++ (( 2 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.veDxMH82WHx7 ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/createDefaultEnvFileEnvFile.YCaOFUSF4U8X ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.veDxMH82WHx7 +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/mergeConfFiles.veDxMH82WHx7 +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ Log::requireLoad ++ [[ -z /home/wsl/.bash-tools/logs/bash.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ [[ -n '' ]] ++ BASH_FRAMEWORK_CONFIG_FILE= ++ Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ local -n loadConfig_loadedConfigFile=BASH_FRAMEWORK_CONFIG_FILE ++ shift ++ Conf::loadNearestFile .framework-config loadConfig_loadedConfigFile /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ local configFileName=.framework-config ++ local -n loadedFile=loadConfig_loadedConfigFile ++ shift 2 ++ srcDirs=("$@") ++ local -a srcDirs ++ for srcDir in "${srcDirs[@]}" +++ File::upFind /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework .framework-config +++ local fromPath=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework +++ shift +++ local fileName=.framework-config +++ shift +++ local untilInclusivePath=/ +++ shift +++ true +++ [[ -f /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ]] +++ true +++ [[ -f /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ]] +++ echo /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config +++ return 0 ++ configFile=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ++ [[ -n /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ]] ++ source /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config +++ FRAMEWORK_FUNCTIONS_IGNORE_REGEXP='^(Namespace::functions|Functions::myFunction|Namespace::requireSomething|IMPORT::dir::file|Acquire::ForceIPv4)$' +++ NON_FRAMEWORK_FILES_REGEXP='(^bin/|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/_binaries|^src/_includes|^src/batsHeaders.sh$|^src/_standalone)' +++ BATS_FILE_NOT_NEEDED_REGEXP='(^bin/|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/batsHeaders.sh$|^src/_includes)' +++ FRAMEWORK_FILES_FUNCTION_MATCHING_IGNORE_REGEXP='^bin/|^\.framework-config$|^build.sh$|\.tpl$|/testsData/|^manualTests/|\.bats$' +++ FRAMEWORK_SRC_DIRS=("${FRAMEWORK_ROOT_DIR}/src") +++ export REPOSITORY_URL=https://github.com/fchastanet/bash-tools-framework +++ REPOSITORY_URL=https://github.com/fchastanet/bash-tools-framework ++ Log::displayDebug 'Config file /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config is loaded' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Config file /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config is loaded' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ loadedFile=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ++ return 0 ++ [[ 0 = \1 ]] ++ commandCallback ++ [[ localhost = '' ]] ++ [[ 888 = '' ]] ++ Log::displayDebug 'Command waitForIt - parse arguments: --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Command waitForIt - parse arguments: --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ Log::displayDebug 'Command waitForIt - parse filtered arguments: ' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Command waitForIt - parse filtered arguments: ' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ [[ 0 = \1 ]] ++ run ++ local result=0 ++ [[ -n '' ]] ++ local algo=timeoutV2WithTcp ++ [[ -z timeoutV2WithTcp ]] ++ Log::displayInfo 'waitForIt - using algorithm timeoutV2WithTcp' ++ local type=INFO ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO )) ++ echo -e 'INFO - waitForIt - using algorithm timeoutV2WithTcp' +INFO - waitForIt - using algorithm timeoutV2WithTcp ++ Log::logInfo 'waitForIt - using algorithm timeoutV2WithTcp' INFO ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO )) ++ (( optionTimeout > 0 )) ++ Log::displayInfo 'waitForIt - waiting 1 seconds for localhost:888' ++ local type=INFO ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO )) ++ echo -e 'INFO - waitForIt - waiting 1 seconds for localhost:888' +INFO - waitForIt - waiting 1 seconds for localhost:888 ++ Log::logInfo 'waitForIt - waiting 1 seconds for localhost:888' INFO ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO )) ++ timeoutV2WithTcp ++ timeoutCommand v2 usingTcp ++ local timeoutVersion=v2 ++ local commandToUse=usingTcp ++ local result ++ local -i start_ts=0 ++ Array::contains usingTcp usingTcp usingNc ++ local element ++ for element in "${@:2}" ++ [[ usingTcp = \u\s\i\n\g\T\c\p ]] ++ return 0 ++ timeoutCmd=(timeout) ++ local -a timeoutCmd ++ [[ v2 = \v\1 ]] ++ timeoutCmd+=("${optionTimeout}" "$0" "${ORIGINAL_BASH_FRAMEWORK_ARGV[@]}") ++ local pid=1781756 ++ WAIT_FOR_IT_TIMEOUT_CHILD_ALGO=usingTcp ++ trap 'kill -INT -1781756' INT ++ timeout 1 bin/waitForIt --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp ++ wait 1781756 ++ facade_main_d396e2bc1f6e43a7b79e8a25ad41ac25 --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp +++ cd /home/wsl/fchastanet/bash-tools/bin/.. +++ pwd -P ++ BASH_TOOLS_ROOT_DIR=/home/wsl/fchastanet/bash-tools ++ [[ -d /home/wsl/fchastanet/bash-tools/vendor/bash-tools-framework/ ]] +++ cd /home/wsl/fchastanet/bash-tools/vendor/bash-tools-framework +++ pwd -P ++ FRAMEWORK_ROOT_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ FRAMEWORK_SRC_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/src ++ FRAMEWORK_BIN_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/bin ++ FRAMEWORK_VENDOR_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/vendor ++ FRAMEWORK_VENDOR_BIN_DIR=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/vendor/bin ++ [[ -f /home/wsl/.bash-tools/.env ]] ++ BASH_FRAMEWORK_ENV_FILES=("${HOME}/.bash-tools/.env") ++ export BASH_FRAMEWORK_ENV_FILES ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n 1 ]] +++ configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ++++ echo BASH_FRAMEWORK_THEME=noColor ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=3 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /home/wsl/.bash-tools/.env ]] +++ [[ ! -r /home/wsl/.bash-tools/.env ]] +++ echo /home/wsl/.bash-tools/.env +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ]] +++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ++ configFilesStr='/home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq' ++ local -a configFiles ++ readarray -t configFiles ++ (( 2 == 0 )) ++ [[ -z /home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ]] ++ Env::mergeConfFiles /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ++ configFileList=("$@") ++ local -a configFileList ++ (( 2 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.qmGWA7wQgTpH ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.6GYY1uLmH8qq ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.qmGWA7wQgTpH +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.qmGWA7wQgTpH +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ UI::requireTheme ++ UI::theme noColor ++ local theme=noColor ++ [[ ! noColor =~ -force$ ]] ++ Assert::tty ++ [[ 0 = \1 ]] ++ [[ 0 = \1 ]] ++ [[ -t 1 ]] ++ [[ -t 2 ]] ++ theme=noColor ++ case "${theme}" in ++ [[ noColor = \d\e\f\a\u\l\t ]] ++ export BASH_FRAMEWORK_THEME=noColor ++ BASH_FRAMEWORK_THEME=noColor ++ export __ERROR_COLOR= ++ __ERROR_COLOR= ++ export __INFO_COLOR= ++ __INFO_COLOR= ++ export __SUCCESS_COLOR= ++ __SUCCESS_COLOR= ++ export __WARNING_COLOR= ++ __WARNING_COLOR= ++ export __SKIPPED_COLOR= ++ __SKIPPED_COLOR= ++ export __DEBUG_COLOR= ++ __DEBUG_COLOR= ++ export __HELP_COLOR= ++ __HELP_COLOR= ++ export __TEST_COLOR= ++ __TEST_COLOR= ++ export __TEST_ERROR_COLOR= ++ __TEST_ERROR_COLOR= ++ export __HELP_TITLE_COLOR= ++ __HELP_TITLE_COLOR= ++ export __HELP_OPTION_COLOR= ++ __HELP_OPTION_COLOR= ++ export __RESET_COLOR= ++ __RESET_COLOR= ++ export __HELP_EXAMPLE= ++ __HELP_EXAMPLE= ++ export __HELP_TITLE= ++ __HELP_TITLE= ++ export __HELP_NORMAL= ++ __HELP_NORMAL= ++ Log::requireLoad ++ [[ -z /home/wsl/.bash-tools/logs/bash.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ Compiler::Facade::requireCommandBinDir ++ COMMAND_BIN_DIR=/home/wsl/fchastanet/bash-tools/bin ++ Env::pathPrepend /home/wsl/fchastanet/bash-tools/bin ++ local arg ++ for arg in "$@" ++ [[ -d /home/wsl/fchastanet/bash-tools/bin ]] ++ [[ :/home/wsl/.virtualenv/python3.9/bin:/home/wsl/.bin:/home/wsl/.local/bin:/home/wsl/fchastanet/bash-tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/c/Python311/Scripts/:/c/Python311/:/c/Program Files/Common Files/Oracle/Java/javapath:/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/c/PROGRA~1/AdoptOpenJDK/jdk-13.0.2.8-hotspot/bin:/c/Windows/system32:/c/Windows:/c/Windows/System32/Wbem:/c/Windows/System32/WindowsPowerShell/v1.0/:/c/Windows/System32/OpenSSH/:/c/PROGRA~1/WindowsPowerShell/Scripts:/c/PROGRA~1/IcedTeaWeb/WebStart/bin:/c/PROGRA~1/dotnet/:/c/PROGRA~2/WI3CF2~1/10/WINDOW~1:/c/PROGRA~2/Meld/:/c/WINDOWS/system32:/c/WINDOWS:/c/WINDOWS/System32/Wbem:/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/WINDOWS/System32/OpenSSH/:/c/PROGRA~1/Git/cmd:/c/Program Files/dotnet/:/c/ProgramData/chocolatey/bin:/c/Program Files/PowerShell/7/:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/PROGRA~1/JetBrains/PHPSTO~1.2/bin:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/c/Users/fchastanet/AppData/Local/JetBrains/Toolbox/scripts:/c/Users/fchastanet/.dotnet/tools:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/Users/fchastanet/AppData/Local/Programs/Microsoft VS Code/bin:/c/WINDOWS/system32:/mnt/c/Windows:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/usr/local/.composer/vendor/bin:/home/wsl/go/bin:/home/wsl/n/bin:/opt/kubectx:/home/wsl/.fzf/bin: != *\:\/\h\o\m\e\/\w\s\l\/\f\c\h\a\s\t\a\n\e\t\/\b\a\s\h\-\t\o\o\l\s\/\b\i\n\:* ]] ++ Linux::requireExecutedAsUser +++ id -u ++ [[ 1000 = \0 ]] ++ BASH_FRAMEWORK_ARGV_FILTERED=() ++ declare -a BASH_FRAMEWORK_ARGV_FILTERED ++ commandArgs=() ++ declare -a commandArgs ++ declare copyrightBeginYear=2020 ++ declare optionTimeout=15 ++ declare optionAlgo= ++ availableAlgos=(timeoutV1WithNc timeoutV2WithNc whileLoopWithNc timeoutV1WithTcp timeoutV2WithTcp whileLoopWithTcp) ++ declare -a availableAlgos ++ waitForItCommand parse --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp ++ local options_parse_cmd=parse ++ shift ++ [[ parse = \p\a\r\s\e ]] ++ local -i options_parse_optionParsedCountOptionHostOrIp ++ (( options_parse_optionParsedCountOptionHostOrIp = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionPort ++ (( options_parse_optionParsedCountOptionPort = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionAlgo ++ (( options_parse_optionParsedCountOptionAlgo = 0 )) ++ true ++ optionStrict=0 ++ local -i options_parse_optionParsedCountOptionStrict ++ (( options_parse_optionParsedCountOptionStrict = 0 )) ++ true ++ optionTimeout=15 ++ local -i options_parse_optionParsedCountOptionTimeout ++ (( options_parse_optionParsedCountOptionTimeout = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionBashFrameworkConfig ++ (( options_parse_optionParsedCountOptionBashFrameworkConfig = 0 )) ++ true ++ optionConfig=0 ++ local -i options_parse_optionParsedCountOptionConfig ++ (( options_parse_optionParsedCountOptionConfig = 0 )) ++ true ++ optionInfoVerbose=0 ++ local -i options_parse_optionParsedCountOptionInfoVerbose ++ (( options_parse_optionParsedCountOptionInfoVerbose = 0 )) ++ true ++ optionDebugVerbose=0 ++ local -i options_parse_optionParsedCountOptionDebugVerbose ++ (( options_parse_optionParsedCountOptionDebugVerbose = 0 )) ++ true ++ optionTraceVerbose=0 ++ local -i options_parse_optionParsedCountOptionTraceVerbose ++ (( options_parse_optionParsedCountOptionTraceVerbose = 0 )) ++ true ++ optionNoColor=0 ++ local -i options_parse_optionParsedCountOptionNoColor ++ (( options_parse_optionParsedCountOptionNoColor = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionTheme ++ (( options_parse_optionParsedCountOptionTheme = 0 )) ++ true ++ optionHelp=0 ++ local -i options_parse_optionParsedCountOptionHelp ++ (( options_parse_optionParsedCountOptionHelp = 0 )) ++ true ++ optionVersion=0 ++ local -i options_parse_optionParsedCountOptionVersion ++ (( options_parse_optionParsedCountOptionVersion = 0 )) ++ true ++ optionQuiet=0 ++ local -i options_parse_optionParsedCountOptionQuiet ++ (( options_parse_optionParsedCountOptionQuiet = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogLevel ++ (( options_parse_optionParsedCountOptionLogLevel = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogFile ++ (( options_parse_optionParsedCountOptionLogFile = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionDisplayLevel ++ (( options_parse_optionParsedCountOptionDisplayLevel = 0 )) ++ true ++ local -i options_parse_argParsedCountCommandArgs ++ (( options_parse_argParsedCountCommandArgs = 0 )) ++ true ++ local -i options_parse_parsedArgIndex=0 ++ (( 8 > 0 )) ++ local options_parse_arg=--host ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 7 == 0 )) ++ (( options_parse_optionParsedCountOptionHostOrIp >= 1 )) ++ (( ++options_parse_optionParsedCountOptionHostOrIp )) ++ optionHostOrIp=localhost ++ shift ++ (( 6 > 0 )) ++ local options_parse_arg=--port ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 5 == 0 )) ++ (( options_parse_optionParsedCountOptionPort >= 1 )) ++ (( ++options_parse_optionParsedCountOptionPort )) ++ optionPort=888 ++ optionPortCallback --port 888 ++ [[ ! 888 =~ ^[0-9]+$ ]] ++ (( optionPort == 0 )) ++ shift ++ (( 4 > 0 )) ++ local options_parse_arg=--timeout ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 3 == 0 )) ++ (( options_parse_optionParsedCountOptionTimeout >= 1 )) ++ (( ++options_parse_optionParsedCountOptionTimeout )) ++ optionTimeout=1 ++ optionTimeoutCallback --timeout 1 ++ [[ ! 1 =~ ^[0-9]+$ ]] ++ shift ++ (( 2 > 0 )) ++ local options_parse_arg=--algo ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ shift ++ (( 1 == 0 )) ++ (( options_parse_optionParsedCountOptionAlgo >= 1 )) ++ (( ++options_parse_optionParsedCountOptionAlgo )) ++ optionAlgo=timeoutV2WithTcp ++ optionAlgoCallback --algo timeoutV2WithTcp ++ Array::contains timeoutV2WithTcp timeoutV1WithNc timeoutV2WithNc whileLoopWithNc timeoutV1WithTcp timeoutV2WithTcp whileLoopWithTcp ++ local element ++ for element in "${@:2}" ++ [[ timeoutV1WithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV2WithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ whileLoopWithNc = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV1WithTcp = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ for element in "${@:2}" ++ [[ timeoutV2WithTcp = \t\i\m\e\o\u\t\V\2\W\i\t\h\T\c\p ]] ++ return 0 ++ shift ++ (( 0 > 0 )) ++ (( options_parse_optionParsedCountOptionHostOrIp < 1 )) ++ (( options_parse_optionParsedCountOptionPort < 1 )) ++ commandOptionParseFinished ++ [[ -z 1 ]] ++ BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") ++ export BASH_FRAMEWORK_ENV_FILES ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n 1 ]] +++ configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ++++ echo BASH_FRAMEWORK_THEME=noColor ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=3 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /home/wsl/.bash-tools/.env ]] +++ [[ ! -r /home/wsl/.bash-tools/.env ]] +++ echo /home/wsl/.bash-tools/.env +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ]] +++ echo /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ++ configFilesStr='/home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88' ++ local -a configFiles ++ readarray -t configFiles ++ (( 2 == 0 )) ++ [[ -z /home/wsl/.bash-tools/.env +/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ]] ++ Env::mergeConfFiles /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ++ configFileList=("$@") ++ local -a configFileList ++ (( 2 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.VYUdfc7MtOyE ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /home/wsl/.bash-tools/.env /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/createDefaultEnvFileEnvFile.g0TBpne4LK88 ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.VYUdfc7MtOyE +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE/mergeConfFiles.VYUdfc7MtOyE +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=3 +++ BASH_FRAMEWORK_LOG_FILE=/home/wsl/.bash-tools/logs/bash.log +++ DB_IMPORT_DUMP_DIR=/home/wsl/.bash-tools/dbImportDumps +++ DB_IMPORT_GARBAGE_COLLECT_DAYS=+30 +++ SCRIPTS_FOLDER=/home/wsl/.bash-tools/conf/dbScripts +++ BASH_TOOLS_FOLDER=/home/wsl/projects/bash-tools +++ S3_BASE_URL=s3://ck-dev-frsa-devsql/exports/ +++ TEMP_FOLDER=/tmp +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ Log::requireLoad ++ [[ -z /home/wsl/.bash-tools/logs/bash.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ [[ -n '' ]] ++ BASH_FRAMEWORK_CONFIG_FILE= ++ Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ local -n loadConfig_loadedConfigFile=BASH_FRAMEWORK_CONFIG_FILE ++ shift ++ Conf::loadNearestFile .framework-config loadConfig_loadedConfigFile /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ++ local configFileName=.framework-config ++ local -n loadedFile=loadConfig_loadedConfigFile ++ shift 2 ++ srcDirs=("$@") ++ local -a srcDirs ++ for srcDir in "${srcDirs[@]}" +++ File::upFind /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework .framework-config +++ local fromPath=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework +++ shift +++ local fileName=.framework-config +++ shift +++ local untilInclusivePath=/ +++ shift +++ true +++ [[ -f /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework ]] +++ true +++ [[ -f /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ]] +++ echo /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config +++ return 0 ++ configFile=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ++ [[ -n /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ]] ++ source /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config +++ FRAMEWORK_FUNCTIONS_IGNORE_REGEXP='^(Namespace::functions|Functions::myFunction|Namespace::requireSomething|IMPORT::dir::file|Acquire::ForceIPv4)$' +++ NON_FRAMEWORK_FILES_REGEXP='(^bin/|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/_binaries|^src/_includes|^src/batsHeaders.sh$|^src/_standalone)' +++ BATS_FILE_NOT_NEEDED_REGEXP='(^bin/|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/batsHeaders.sh$|^src/_includes)' +++ FRAMEWORK_FILES_FUNCTION_MATCHING_IGNORE_REGEXP='^bin/|^\.framework-config$|^build.sh$|\.tpl$|/testsData/|^manualTests/|\.bats$' +++ FRAMEWORK_SRC_DIRS=("${FRAMEWORK_ROOT_DIR}/src") +++ export REPOSITORY_URL=https://github.com/fchastanet/bash-tools-framework +++ REPOSITORY_URL=https://github.com/fchastanet/bash-tools-framework ++ Log::displayDebug 'Config file /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config is loaded' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Config file /home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config is loaded' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ loadedFile=/home/wsl/fchastanet/bash-dev-env/vendor/bash-tools-framework/.framework-config ++ return 0 ++ [[ 0 = \1 ]] ++ commandCallback ++ [[ localhost = '' ]] ++ [[ 888 = '' ]] ++ Log::displayDebug 'Command waitForIt - parse arguments: --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Command waitForIt - parse arguments: --host localhost --port 888 --timeout 1 --algo timeoutV2WithTcp' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ Log::displayDebug 'Command waitForIt - parse filtered arguments: ' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'Command waitForIt - parse filtered arguments: ' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ [[ 0 = \1 ]] ++ run ++ local result=0 ++ [[ -n usingTcp ]] ++ whileLoop usingTcp 0 ++ local commandToUse=usingTcp ++ local reportTimeout=0 ++ Array::contains usingTcp usingTcp usingNc ++ local element ++ for element in "${@:2}" ++ [[ usingTcp = \u\s\i\n\g\T\c\p ]] ++ return 0 ++ local -i start_ts=1 ++ true ++ usingTcp ++ [[ -n '' ]] ++ echo +bin/waitForIt: connect: Connection refused +bin/waitForIt: line 1635: /dev/tcp/localhost/888: Connection refused ++ (( optionTimeout!=0 && SECONDS - start_ts > optionTimeout )) ++ sleep 1 +Terminated +++ cleanOnExit +++ [[ 0 = \1 ]] +++ [[ -n xxx ]] +++ Log::displayDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE'\''' +++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) +++ Log::logDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE'\''' +++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) +++ rm -Rf /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE ++ true ++ usingTcp ++ [[ -n '' ]] ++ echo +bin/waitForIt: connect: Connection refused +bin/waitForIt: line 1635: /dev/tcp/localhost/888: Connection refused ++ (( optionTimeout!=0 && SECONDS - start_ts > optionTimeout )) ++ sleep 1 ++ true ++ usingTcp ++ [[ -n '' ]] ++ echo +bin/waitForIt: connect: Connection refused +bin/waitForIt: line 1635: /dev/tcp/localhost/888: Connection refused ++ (( optionTimeout!=0 && SECONDS - start_ts > optionTimeout )) ++ sleep 1 ++ true ++ usingTcp ++ [[ -n '' ]] ++ echo +bin/waitForIt: connect: Connection refused +bin/waitForIt: line 1635: /dev/tcp/localhost/888: Connection refused ++ (( optionTimeout!=0 && SECONDS - start_ts > optionTimeout )) ++ [[ 0 = \1 ]] ++ return 2 ++ result=2 ++ exit 2 ++ cleanOnExit ++ [[ 0 = \1 ]] ++ [[ -n xxx ]] ++ Log::displayDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE'\''' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE'\''' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ rm -Rf /tmp/bash-framework/bash-framework-1781721-rBgQna/bash-framework-1781757-68jUsE ++ result=124 ++ [[ 124 != \0 ]] ++ Log::displayError 'waitForIt - timeout occurred after 3 seconds for localhost:888' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR )) ++ echo -e 'ERROR - waitForIt - timeout occurred after 3 seconds for localhost:888' +ERROR - waitForIt - timeout occurred after 3 seconds for localhost:888 ++ Log::logError 'waitForIt - timeout occurred after 3 seconds for localhost:888' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_ERROR )) ++ return 124 ++ result=124 ++ [[ -n '' ]] ++ exit 124 ++ cleanOnExit ++ [[ 0 = \1 ]] ++ [[ -n xxx ]] ++ Log::displayDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna'\''' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781721-rBgQna'\''' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ rm -Rf /tmp/bash-framework/bash-framework-1781721-rBgQna diff --git a/waitForMysql.log b/waitForMysql.log new file mode 100644 index 00000000..6061c88f --- /dev/null +++ b/waitForMysql.log @@ -0,0 +1,376 @@ ++ facade_main_665f5dabe75f418ea1c10f53fac6da5e localhost 3306 mysql mysql ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n '' ]] +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781863-ah8kM7 -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ++++ echo BASH_FRAMEWORK_THEME=default ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=2 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ]] +++ echo /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ++ configFilesStr=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ++ local -a configFiles ++ readarray -t configFiles ++ (( 1 == 0 )) ++ [[ -z /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ]] ++ Env::mergeConfFiles /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ++ configFileList=("$@") ++ local -a configFileList ++ (( 1 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781863-ah8kM7 -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.vhNqn9bXvxy0 ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.lyQQz9jGy7RO ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.vhNqn9bXvxy0 +++ BASH_FRAMEWORK_THEME=default +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=2 +++ BASH_FRAMEWORK_LOG_FILE=/logs/waitForMysql.log +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.vhNqn9bXvxy0 +++ BASH_FRAMEWORK_THEME=default +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=2 +++ BASH_FRAMEWORK_LOG_FILE=/logs/waitForMysql.log +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ UI::requireTheme ++ UI::theme default ++ local theme=default ++ [[ ! default =~ -force$ ]] ++ Assert::tty ++ [[ 0 = \1 ]] ++ [[ 0 = \1 ]] ++ [[ -t 1 ]] ++ [[ -t 2 ]] ++ theme=noColor ++ case "${theme}" in ++ [[ noColor = \d\e\f\a\u\l\t ]] ++ export BASH_FRAMEWORK_THEME=noColor ++ BASH_FRAMEWORK_THEME=noColor ++ export __ERROR_COLOR= ++ __ERROR_COLOR= ++ export __INFO_COLOR= ++ __INFO_COLOR= ++ export __SUCCESS_COLOR= ++ __SUCCESS_COLOR= ++ export __WARNING_COLOR= ++ __WARNING_COLOR= ++ export __SKIPPED_COLOR= ++ __SKIPPED_COLOR= ++ export __DEBUG_COLOR= ++ __DEBUG_COLOR= ++ export __HELP_COLOR= ++ __HELP_COLOR= ++ export __TEST_COLOR= ++ __TEST_COLOR= ++ export __TEST_ERROR_COLOR= ++ __TEST_ERROR_COLOR= ++ export __HELP_TITLE_COLOR= ++ __HELP_TITLE_COLOR= ++ export __HELP_OPTION_COLOR= ++ __HELP_OPTION_COLOR= ++ export __RESET_COLOR= ++ __RESET_COLOR= ++ export __HELP_EXAMPLE= ++ __HELP_EXAMPLE= ++ export __HELP_TITLE= ++ __HELP_TITLE= ++ export __HELP_NORMAL= ++ __HELP_NORMAL= ++ Log::requireLoad ++ [[ -z /logs/waitForMysql.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ Compiler::Facade::requireCommandBinDir ++ COMMAND_BIN_DIR=/home/wsl/fchastanet/bash-tools/bin ++ Env::pathPrepend /home/wsl/fchastanet/bash-tools/bin ++ local arg ++ for arg in "$@" ++ [[ -d /home/wsl/fchastanet/bash-tools/bin ]] ++ [[ :/home/wsl/.virtualenv/python3.9/bin:/home/wsl/.bin:/home/wsl/.local/bin:/home/wsl/fchastanet/bash-tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/c/Python311/Scripts/:/c/Python311/:/c/Program Files/Common Files/Oracle/Java/javapath:/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/c/PROGRA~1/AdoptOpenJDK/jdk-13.0.2.8-hotspot/bin:/c/Windows/system32:/c/Windows:/c/Windows/System32/Wbem:/c/Windows/System32/WindowsPowerShell/v1.0/:/c/Windows/System32/OpenSSH/:/c/PROGRA~1/WindowsPowerShell/Scripts:/c/PROGRA~1/IcedTeaWeb/WebStart/bin:/c/PROGRA~1/dotnet/:/c/PROGRA~2/WI3CF2~1/10/WINDOW~1:/c/PROGRA~2/Meld/:/c/WINDOWS/system32:/c/WINDOWS:/c/WINDOWS/System32/Wbem:/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/WINDOWS/System32/OpenSSH/:/c/PROGRA~1/Git/cmd:/c/Program Files/dotnet/:/c/ProgramData/chocolatey/bin:/c/Program Files/PowerShell/7/:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/PROGRA~1/JetBrains/PHPSTO~1.2/bin:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/c/Users/fchastanet/AppData/Local/JetBrains/Toolbox/scripts:/c/Users/fchastanet/.dotnet/tools:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/c/Users/fchastanet/AppData/Local/Programs/Microsoft VS Code/bin:/c/WINDOWS/system32:/mnt/c/Windows:/c/Users/fchastanet/AppData/Local/Microsoft/WindowsApps:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/c/Users/fchastanet/AppData/Local/Programs/MICROS~1/bin:/usr/local/.composer/vendor/bin:/home/wsl/go/bin:/home/wsl/n/bin:/opt/kubectx:/home/wsl/.fzf/bin: != *\:\/\h\o\m\e\/\w\s\l\/\f\c\h\a\s\t\a\n\e\t\/\b\a\s\h\-\t\o\o\l\s\/\b\i\n\:* ]] ++ BASH_FRAMEWORK_ARGV_FILTERED=() ++ declare -a BASH_FRAMEWORK_ARGV_FILTERED ++ declare copyrightBeginYear=2020 ++ declare optionTimeout=15 ++ waitForMysqlCommand parse localhost 3306 mysql mysql ++ local options_parse_cmd=parse ++ shift ++ [[ parse = \p\a\r\s\e ]] ++ optionTimeout=15 ++ local -i options_parse_optionParsedCountOptionTimeout ++ (( options_parse_optionParsedCountOptionTimeout = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionBashFrameworkConfig ++ (( options_parse_optionParsedCountOptionBashFrameworkConfig = 0 )) ++ true ++ optionConfig=0 ++ local -i options_parse_optionParsedCountOptionConfig ++ (( options_parse_optionParsedCountOptionConfig = 0 )) ++ true ++ optionInfoVerbose=0 ++ local -i options_parse_optionParsedCountOptionInfoVerbose ++ (( options_parse_optionParsedCountOptionInfoVerbose = 0 )) ++ true ++ optionDebugVerbose=0 ++ local -i options_parse_optionParsedCountOptionDebugVerbose ++ (( options_parse_optionParsedCountOptionDebugVerbose = 0 )) ++ true ++ optionTraceVerbose=0 ++ local -i options_parse_optionParsedCountOptionTraceVerbose ++ (( options_parse_optionParsedCountOptionTraceVerbose = 0 )) ++ true ++ optionNoColor=0 ++ local -i options_parse_optionParsedCountOptionNoColor ++ (( options_parse_optionParsedCountOptionNoColor = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionTheme ++ (( options_parse_optionParsedCountOptionTheme = 0 )) ++ true ++ optionHelp=0 ++ local -i options_parse_optionParsedCountOptionHelp ++ (( options_parse_optionParsedCountOptionHelp = 0 )) ++ true ++ optionVersion=0 ++ local -i options_parse_optionParsedCountOptionVersion ++ (( options_parse_optionParsedCountOptionVersion = 0 )) ++ true ++ optionQuiet=0 ++ local -i options_parse_optionParsedCountOptionQuiet ++ (( options_parse_optionParsedCountOptionQuiet = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogLevel ++ (( options_parse_optionParsedCountOptionLogLevel = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionLogFile ++ (( options_parse_optionParsedCountOptionLogFile = 0 )) ++ true ++ local -i options_parse_optionParsedCountOptionDisplayLevel ++ (( options_parse_optionParsedCountOptionDisplayLevel = 0 )) ++ true ++ local -i options_parse_argParsedCountMysqlHostArg ++ (( options_parse_argParsedCountMysqlHostArg = 0 )) ++ true ++ local -i options_parse_argParsedCountMysqlPortArg ++ (( options_parse_argParsedCountMysqlPortArg = 0 )) ++ true ++ local -i options_parse_argParsedCountMysqlUserArg ++ (( options_parse_argParsedCountMysqlUserArg = 0 )) ++ true ++ local -i options_parse_argParsedCountMysqlPasswordArg ++ (( options_parse_argParsedCountMysqlPasswordArg = 0 )) ++ true ++ local -i options_parse_parsedArgIndex=0 ++ (( 4 > 0 )) ++ local options_parse_arg=localhost ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ (( 0 )) ++ (( options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1 )) ++ (( options_parse_argParsedCountMysqlHostArg >= 1 )) ++ (( ++options_parse_argParsedCountMysqlHostArg )) ++ mysqlHostArg=localhost ++ (( ++options_parse_parsedArgIndex )) ++ shift ++ (( 3 > 0 )) ++ local options_parse_arg=3306 ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ (( 0 )) ++ (( options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1 )) ++ (( options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2 )) ++ (( options_parse_argParsedCountMysqlPortArg >= 1 )) ++ (( ++options_parse_argParsedCountMysqlPortArg )) ++ mysqlPortArg=3306 ++ mysqlPortArgCallback 3306 -- mysql mysql ++ [[ ! 3306 =~ ^[0-9]+$ ]] ++ (( mysqlPortArg == 0 )) ++ (( ++options_parse_parsedArgIndex )) ++ shift ++ (( 2 > 0 )) ++ local options_parse_arg=mysql ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ (( 0 )) ++ (( options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1 )) ++ (( options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2 )) ++ (( options_parse_parsedArgIndex >= 2 && options_parse_parsedArgIndex < 3 )) ++ (( options_parse_argParsedCountMysqlUserArg >= 1 )) ++ (( ++options_parse_argParsedCountMysqlUserArg )) ++ mysqlUserArg=mysql ++ (( ++options_parse_parsedArgIndex )) ++ shift ++ (( 1 > 0 )) ++ local options_parse_arg=mysql ++ local argOptDefaultBehavior=0 ++ case "${options_parse_arg}" in ++ (( 0 )) ++ (( options_parse_parsedArgIndex >= 0 && options_parse_parsedArgIndex < 1 )) ++ (( options_parse_parsedArgIndex >= 1 && options_parse_parsedArgIndex < 2 )) ++ (( options_parse_parsedArgIndex >= 2 && options_parse_parsedArgIndex < 3 )) ++ (( options_parse_parsedArgIndex >= 3 && options_parse_parsedArgIndex < 4 )) ++ (( options_parse_argParsedCountMysqlPasswordArg >= 1 )) ++ (( ++options_parse_argParsedCountMysqlPasswordArg )) ++ mysqlPasswordArg=mysql ++ (( ++options_parse_parsedArgIndex )) ++ shift ++ (( 0 > 0 )) ++ (( options_parse_argParsedCountMysqlHostArg < 1 )) ++ (( options_parse_argParsedCountMysqlPortArg < 1 )) ++ (( options_parse_argParsedCountMysqlUserArg < 1 )) ++ (( options_parse_argParsedCountMysqlPasswordArg < 1 )) ++ commandOptionParseFinished ++ [[ -z '' ]] ++ BASH_FRAMEWORK_ENV_FILES=() ++ BASH_FRAMEWORK_ENV_FILES+=("${optionEnvFiles[@]}") ++ export BASH_FRAMEWORK_ENV_FILES ++ Env::requireLoad ++ local configFilesStr +++ Env::getOrderedConfFiles +++ configFiles=() +++ local -a configFiles +++ [[ -n '' ]] +++ local defaultEnvFile ++++ Env::createDefaultEnvFile ++++ local envFile +++++ Framework::createTempFile createDefaultEnvFileEnvFile +++++ mktemp -p /tmp/bash-framework/bash-framework-1781863-ah8kM7 -t createDefaultEnvFileEnvFile.XXXXXXXXXXXX ++++ envFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ++++ echo BASH_FRAMEWORK_THEME=noColor ++++ echo BASH_FRAMEWORK_LOG_LEVEL=0 ++++ echo BASH_FRAMEWORK_DISPLAY_LEVEL=2 ++++ echo 'BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-"${FRAMEWORK_ROOT_DIR}/logs/${SCRIPT_NAME}.log"}"' ++++ echo BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++++ echo /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou +++ defaultEnvFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou +++ configFiles+=("${defaultEnvFile}") +++ local file +++ for file in "${configFiles[@]}" +++ [[ ! -f /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ]] +++ [[ ! -r /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ]] +++ echo /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ++ configFilesStr=/tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ++ local -a configFiles ++ readarray -t configFiles ++ (( 1 == 0 )) ++ [[ -z /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ]] ++ Env::mergeConfFiles /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ++ configFileList=("$@") ++ local -a configFileList ++ (( 1 == 0 )) ++ local combinedConfigFile +++ Framework::createTempFile mergeConfFiles +++ mktemp -p /tmp/bash-framework/bash-framework-1781863-ah8kM7 -t mergeConfFiles.XXXXXXXXXXXX ++ combinedConfigFile=/tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.JFHr71G8P6lK ++ sed -E -e 's/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"'\''].*)$/="\1"/' /tmp/bash-framework/bash-framework-1781863-ah8kM7/createDefaultEnvFileEnvFile.KYLUQ53jhaou ++ Filters::commentLines ++ grep -vxE '[[:blank:]]*(#.*)?' ++ awk -F= '!line[$1]++' ++ set -o allexport ++ source /tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.JFHr71G8P6lK +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=2 +++ BASH_FRAMEWORK_LOG_FILE=/logs/waitForMysql.log +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ source /tmp/bash-framework/bash-framework-1781863-ah8kM7/mergeConfFiles.JFHr71G8P6lK +++ BASH_FRAMEWORK_THEME=noColor +++ BASH_FRAMEWORK_LOG_LEVEL=0 +++ BASH_FRAMEWORK_DISPLAY_LEVEL=2 +++ BASH_FRAMEWORK_LOG_FILE=/logs/waitForMysql.log +++ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION=5 ++ set +o allexport ++ Log::requireLoad ++ [[ -z /logs/waitForMysql.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ [[ -n '' ]] ++ BASH_FRAMEWORK_CONFIG_FILE= ++ Framework::loadConfig BASH_FRAMEWORK_CONFIG_FILE '' ++ local -n loadConfig_loadedConfigFile=BASH_FRAMEWORK_CONFIG_FILE ++ shift ++ Conf::loadNearestFile .framework-config loadConfig_loadedConfigFile '' ++ local configFileName=.framework-config ++ local -n loadedFile=loadConfig_loadedConfigFile ++ shift 2 ++ srcDirs=("$@") ++ local -a srcDirs ++ for srcDir in "${srcDirs[@]}" +++ File::upFind '' .framework-config +++ local fromPath= +++ shift +++ local fileName=.framework-config +++ shift +++ local untilInclusivePath=/ +++ shift +++ true +++ [[ -f '' ]] +++ true +++ [[ -f /.framework-config ]] +++ Array::contains '' / / +++ local element +++ for element in "${@:2}" +++ [[ / = '' ]] +++ for element in "${@:2}" +++ [[ / = '' ]] +++ return 1 ++++ readlink -f /.. +++ fromPath=/ +++ true +++ [[ -f //.framework-config ]] +++ Array::contains / / / +++ local element +++ for element in "${@:2}" +++ [[ / = \/ ]] +++ return 0 +++ return 1 +++ true ++ configFile= ++ [[ -n '' ]] ++ Log::displayWarning 'Config file '\''.framework-config'\'' not found in any source directories provided' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING )) ++ echo -e 'WARN - Config file '\''.framework-config'\'' not found in any source directories provided' +WARN - Config file '.framework-config' not found in any source directories provided ++ Log::logWarning 'Config file '\''.framework-config'\'' not found in any source directories provided' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING )) ++ return 1 ++ Log::fatal 'Command waitForMysql - error while loading .framework-config file' ++ echo -e 'FATAL - Command waitForMysql - error while loading .framework-config file' +FATAL - Command waitForMysql - error while loading .framework-config file ++ Log::logFatal 'Command waitForMysql - error while loading .framework-config file' ++ Log::logMessage FATAL 'Command waitForMysql - error while loading .framework-config file' ++ local levelMsg=FATAL ++ local 'msg=Command waitForMysql - error while loading .framework-config file' ++ local date ++ [[ -n /logs/waitForMysql.log ]] ++ (( BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF )) ++ exit 1 ++ cleanOnExit ++ [[ 0 = \1 ]] ++ [[ -n xxx ]] ++ Log::displayDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781863-ah8kM7'\''' ++ (( BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG )) ++ Log::logDebug 'KEEP_TEMP_FILES=0 removing temp files '\''/tmp/bash-framework/bash-framework-1781863-ah8kM7'\''' ++ (( BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG )) ++ rm -Rf /tmp/bash-framework/bash-framework-1781863-ah8kM7