From 71dfc8d29bfd2561482440a14b1b06077118fec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Wed, 11 Oct 2023 21:55:22 +0200 Subject: [PATCH] binaries using facade: - doc - installRequirements - install - mysql2puml - dbImport - dbImportProfile - dbImportStream - cli - gitIsAncestorOf - gitIsBranch - gitRenameBranch - waitForIt - waitForMysql --- .framework-config | 2 + .gitignore | 1 + .pre-commit-config.yaml | 5 +- .vscode/extensions.json | 3 +- .vscode/launch.json | 21 + TODO.md | 6 +- bin/cli | 2118 ++++++++---- bin/dbImport | 2830 +++++++++++------ bin/dbImportProfile | 2213 +++++++++---- bin/dbImportStream | 2432 +++++++++++--- bin/doc | 1953 ++++++++---- bin/gitIsAncestorOf | 1820 ++++++++--- bin/gitIsBranch | 1788 ++++++++--- bin/gitRenameBranch | 2005 ++++++++---- bin/installRequirements | 1891 +++++++---- bin/mysql2puml | 1971 ++++++++---- bin/waitForIt | 2177 +++++++++---- bin/waitForMysql | 1916 ++++++++--- conf/cliProfiles/mysql.remote.sh | 4 +- conf/cliProfiles/mysql.sh | 2 +- conf/cliProfiles/node.sh | 2 +- conf/cliProfiles/redis.sh | 2 +- conf/cliProfiles/web.sh | 2 +- install | 1784 ++++++++--- src/_binaries/Converters/mysql2puml.bats | 11 +- .../Converters/mysql2puml.options.tpl | 81 + src/_binaries/Converters/mysql2puml.sh | 119 +- src/_binaries/DbImport/dbImport.bats | 88 +- src/_binaries/DbImport/dbImport.options.tpl | 161 + src/_binaries/DbImport/dbImport.sh | 545 +--- src/_binaries/DbImport/dbImportProfile.bats | 10 +- .../DbImport/dbImportProfile.options.tpl | 119 + src/_binaries/DbImport/dbImportProfile.sh | 217 +- .../DbImport/dbImportStream.options.tpl | 82 + src/_binaries/DbImport/dbImportStream.sh | 99 +- src/_binaries/DbImport/dumpSizeQuery.sql | 4 + src/_binaries/Docker/cli.bats | 2 +- src/_binaries/Docker/cli.options.tpl | 127 + src/_binaries/Docker/cli.sh | 186 +- src/_binaries/Git/gitIsAncestorOf.options.tpl | 43 + src/_binaries/Git/gitIsAncestorOf.sh | 39 +- src/_binaries/Git/gitIsBranch.options.tpl | 28 + src/_binaries/Git/gitIsBranch.sh | 34 +- src/_binaries/Git/gitRenameBranch.bats | 111 +- src/_binaries/Git/gitRenameBranch.options.tpl | 95 + src/_binaries/Git/gitRenameBranch.sh | 174 +- src/_binaries/Git/upgradeGithubRelease.bats | 3 - src/_binaries/Utils/waitForIt.bats | 366 +++ src/_binaries/Utils/waitForIt.options.tpl | 148 + src/_binaries/Utils/waitForIt.sh | 315 +- src/_binaries/Utils/waitForMysql.bats | 101 + src/_binaries/Utils/waitForMysql.options.tpl | 81 + src/_binaries/Utils/waitForMysql.sh | 54 +- src/_binaries/build/doc.options.tpl | 35 + src/_binaries/build/doc.sh | 126 +- src/_binaries/build/install.options.tpl | 15 + src/_binaries/build/install.sh | 66 +- .../build/installRequirements.options.tpl | 31 + src/_binaries/build/installRequirements.sh | 53 +- .../options/options.mysql.collationName.tpl | 27 + .../options/options.mysql.target.tpl | 61 + src/_binaries/options/options.profile.tpl | 91 + src/_includes/_header.tpl | 16 - src/_includes/_initFrameworkVariables.tpl | 19 + src/_includes/_load.tpl | 4 - src/batsHeaders.sh | 12 +- waitForIt.log | 980 ++++++ waitForMysql.log | 376 +++ 68 files changed, 23400 insertions(+), 8903 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/_binaries/Converters/mysql2puml.options.tpl create mode 100644 src/_binaries/DbImport/dbImport.options.tpl create mode 100644 src/_binaries/DbImport/dbImportProfile.options.tpl create mode 100644 src/_binaries/DbImport/dbImportStream.options.tpl create mode 100644 src/_binaries/DbImport/dumpSizeQuery.sql create mode 100644 src/_binaries/Docker/cli.options.tpl create mode 100644 src/_binaries/Git/gitIsAncestorOf.options.tpl create mode 100644 src/_binaries/Git/gitIsBranch.options.tpl create mode 100644 src/_binaries/Git/gitRenameBranch.options.tpl create mode 100755 src/_binaries/Utils/waitForIt.bats create mode 100644 src/_binaries/Utils/waitForIt.options.tpl create mode 100755 src/_binaries/Utils/waitForMysql.bats create mode 100644 src/_binaries/Utils/waitForMysql.options.tpl create mode 100644 src/_binaries/build/doc.options.tpl create mode 100644 src/_binaries/build/install.options.tpl create mode 100644 src/_binaries/build/installRequirements.options.tpl create mode 100644 src/_binaries/options/options.mysql.collationName.tpl create mode 100644 src/_binaries/options/options.mysql.target.tpl create mode 100644 src/_binaries/options/options.profile.tpl delete mode 100755 src/_includes/_header.tpl create mode 100644 src/_includes/_initFrameworkVariables.tpl delete mode 100755 src/_includes/_load.tpl create mode 100644 waitForIt.log create mode 100644 waitForMysql.log 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/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/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..a873b5ac 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" 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