From ec6104afc5baa8176a71b94fcd0d52e46ae9f76b 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 --- .framework-config | 2 + .gitignore | 1 + .pre-commit-config.yaml | 5 +- .vscode/extensions.json | 3 +- .vscode/launch.json | 21 + TODO.md | 6 +- bin/cli | 2125 +++++++++---- bin/dbImport | 2822 +++++++++++------ bin/dbImportProfile | 2205 +++++++++---- bin/dbImportStream | 2424 +++++++++++--- bin/doc | 1921 +++++++---- bin/gitIsAncestorOf | 1813 ++++++++--- bin/installRequirements | 1884 +++++++---- bin/mysql2puml | 1963 ++++++++---- 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 | 1776 ++++++++--- 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 | 126 + src/_binaries/Docker/cli.sh | 187 +- src/_binaries/Git/gitIsAncestorOf.options.tpl | 44 + src/_binaries/Git/gitIsAncestorOf.sh | 39 +- src/_binaries/Git/gitRenameBranch.bats | 3 - src/_binaries/Git/upgradeGithubRelease.bats | 3 - 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 +- 52 files changed, 15084 insertions(+), 6395 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/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 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..a0afde63 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,333 @@ 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 # -# @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: +# @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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 -# @return 1 if directory does not exists +# @exitcode 1 if directory does not exists Conf::list() { local dir="$1" local prefix="${2:-}" @@ -658,115 +717,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 +966,904 @@ 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 +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() { + 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 + 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}" "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 +# @require Linux::requireExecutedAsUser +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..d850b5be 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 - - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' - fi - done +#set -x +#Array::wrap ":" 40 0 "Lorem ipsum dolor sit amet," "consectetur adipiscing elit." "Curabitur ac elit id massa" "condimentum finibus." - # 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 -# -# **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 +# @description dump db limited to optional table list # -# **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 +# @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 # -# **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 -# -# **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" } -# 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 } -# 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,46 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 +876,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 +1176,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 +1208,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 +# @description default env file with all default values +# @stdout the default env filepath +Env::createDefaultEnvFile() { + local envFile + envFile="$(Framework::createTempFile "createDefaultEnvFileEnvFile")" || return 2 - dir="$(dirname "${file}")" - - 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 +getLevel() { + local levelName="$1" + case "${levelName^^}" in + OFF) + echo "${__LEVEL_OFF}" ;; - -a | --from-aws) - FROM_AWS="1" - # structure is included in s3 file - SKIP_SCHEMA="1" - ;; - --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="" + +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)" +} -# 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 +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 [[ -n "${FROM_DSN}" ]]; then - Log::fatal "you cannot use from-dsn and from-aws at the same time" +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 - if [[ -z "${S3_BASE_URL}" ]]; then - Log::fatal "missing S3_BASE_URL, please provide a value in .env file" + # 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 -elif [[ -z "${FROM_DSN}" ]]; then - # default value for FROM_DSN if from-aws not set - FROM_DSN="${DEFAULT_FROM_DSN}" -fi + chmod +x "${profileCommand}" + Log::displayInfo "${profileMsgInfo}" +} -# load the profile -if [[ -z "${PROFILE}" ]]; then - showHelp - Log::fatal "you should specify a profile" -fi +declare optionTargetDsn="default.local" # old TARGET_DSN +declare optionCharacterSet="" # old CHARACTER_SET +declare defaultTargetCharacterSet="utf8" -[[ "${PROFILE}" != "default" && -n "${TABLES}" ]] && - Log::fatal "you cannot use table and profile options at the same time" +initializeDefaultTargetMysqlOptions() { + local -n dbFromInstanceTargetMysql=$1 + local fromDbName="$2" -# 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 + # 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 -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 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} -[[ -z "${DB_IMPORT_DUMP_DIR}" ]] && - Log::fatal "you have to specify a value for DB_IMPORT_DUMP_DIR env variable" +${__HELP_TITLE}List of available dsn:${__HELP_NORMAL} +${dsnList} -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 +${__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 +} -# create db instances -declare -Agx dbFromInstance dbTargetDatabase +# 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)" -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 + dbImportCommand help | envsubst + exit 0 +} -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 +dbImportCommandCallback() { + if [[ -z "${targetDbName}" ]]; then + targetDbName="${fromDbName}" + 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 + 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="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 +2281,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..26da2ddf 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 +# @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 # -# **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 -# -# **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,292 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 +711,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 +746,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 +1025,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 -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @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 - - 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" -# create db instance -declare -Agx dbFromInstance +optionHelpCallback() { + local profilesList="" + local dsnList="" + dsnList="$(Conf::getMergedList "dsn" "env")" + profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" -Database::newInstance dbFromInstance "${FROM_DSN}" -Database::setQueryOptions dbFromInstance "${dbFromInstance[QUERY_OPTIONS]} --connect-timeout=5" -Log::displayInfo "Using from dsn ${dbFromInstance['DSN_FILE']}" + dbImportProfileCommand help | envsubst + exit 0 +} -# check if from db exists -Database::ifDbExists dbFromInstance "${FROM_DB}" || { - Log::fatal "From Database ${FROM_DB} does not exist !" +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 } + +# 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..836c755a 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,988 @@ 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=() +# @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 - # Tag items to remove - local del - for del in "$@"; do valuesToRemoveKeys[${del}]=1; done + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" - # remove items - local k - for k in "${!arrayRemoveArray[@]}"; do - if [[ -n "${valuesToRemoveKeys[${arrayRemoveArray[k]}]+xxx}" ]]; then - unset 'arrayRemoveArray[k]' - fi - done + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" - # compaction (element re-indexing, because unset makes "holes" in array ) - arrayRemoveArray=("${arrayRemoveArray[@]}") -} - -# 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_WARNING)); then - Log::logWarning() { :; } - Log::logStatus() { :; } + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" fi - if ((BASH_FRAMEWORK_LOG_LEVEL < __LEVEL_ERROR)); then - Log::logError() { :; } + ( + # 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}#" + ) +} + +# @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 - 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 + ( + 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 - 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}" + if [[ -z "${HOSTNAME}" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" fi - 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 +1086,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 +2103,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..5a0ece33 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,277 @@ 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} - 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[@]}") -} - -# 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 - return 0 + local -i indentNextLine=$1 + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" "${BASH_FRAMEWORK_DEFAULT_ENV_FILE}" + shift || true + (($# != 0)) || return 0 - ( - # reset temp file - echo >"${BASH_FRAMEWORK_CACHED_ENV_FILE}" + local arg - # 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}") + # 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 - Log::displayWarning "env file not not found - ${BASH_FRAMEWORK_ENV_FILEPATH}" + 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 - - # 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 [[ "${arg}" = $'\n' ]]; then + printf $'\n\n' + ((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 + echo -e -n "${arg}" + needEcho="1" + ((currentLineLength += argNoAnsiLength)) + ((glueLength = ${#glue})) || true + shift || return 0 + arg="$1" + else + if ((argNoAnsiLength >= (maxLineLength - indentNextLine))); then + # 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="${indentStr}${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 + 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 +382,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 +433,244 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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_ARGV String[] list of arguments passed to the command (provided by _mandatoryHeaders.sh file) +# @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 +679,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 +688,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 +805,801 @@ 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 -# Public: create a temp file using default TMPDIR variable -# initialized in src/_includes/_header.tpl -# -# **Arguments**: -# @param $1 {String} template (optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "$1.XXXXXXXXXXXX" + ( + 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}" } -# Internal: common log message -# -# **Arguments**: -# * $1 - message's level description -# * $2 - message -# **Output**: -# [date]|[levelMsg]|message +# @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 +} - # 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_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_FILEPATH="${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" -Log::load + declare -a BASH_FRAMEWORK_ARGV_FILTERED=() + # 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 + } -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" -DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" -showHelp() { -cat < 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 + export optionSkipDockerBuild + export optionBashFrameworkConfig + export optionConfig + export optionInfoVerbose + export optionDebugVerbose + export optionTraceVerbose + export optionEnvFiles + export optionNoColor + export optionTheme + export optionHelp + export optionVersion + export optionQuiet + export optionLogLevel + export optionLogFile + export optionDisplayLevel + 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)" + echo -e " skip docker image build if option provided" + 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)" + echo -e " use alternate bash framework configuration." + printf " %b\n" "${__HELP_OPTION_COLOR}--config${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Display configuration" + printf " %b\n" "${__HELP_OPTION_COLOR}--verbose${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-v${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " info level verbose mode (alias of --display-level INFO)" + printf " %b\n" "${__HELP_OPTION_COLOR}-vv${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " debug level verbose mode (alias of --display-level DEBUG)" + printf " %b\n" "${__HELP_OPTION_COLOR}-vvv${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " trace level verbose mode (alias of --display-level TRACE)" + printf " %b\n" "${__HELP_OPTION_COLOR}--env-file ${__HELP_NORMAL} (optional)" + echo -e " Load the specified env file" + printf " %b\n" "${__HELP_OPTION_COLOR}--no-color${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Produce monochrome output. alias of --theme noColor." + printf " %b\n" "${__HELP_OPTION_COLOR}--theme ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " choose color theme (default, default-force or noColor) - default-force mean + s colors will be produced even if command is piped" + printf " %b\n" "${__HELP_OPTION_COLOR}--help${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-h${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Display this command help" + printf " %b\n" "${__HELP_OPTION_COLOR}--version${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Print version information and quit" + printf " %b\n" "${__HELP_OPTION_COLOR}--quiet${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-q${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " quiet mode, doesn't display any output" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-level ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Set log level (one of OFF, ERROR, WARNING, INFO, DEBUG, TRACE value)" + printf " %b\n" "${__HELP_OPTION_COLOR}--log-file ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " Set log file" + printf " %b\n" "${__HELP_OPTION_COLOR}--display-level ${__HELP_NORMAL} (optional) (at most 1 times)" + echo -e " set display level (one of OFF, ERROR, WARNING, INFO, DEBUG, TRACE value)" + 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 + echo 'Copyright (c) 2023 François Chastanet' + else + Log::displayError "Command ${SCRIPT_NAME} - Option command invalid: '${options_parse_cmd}'" + return 1 + fi + } + + 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 -Copyright (c) 2022 François Chastanet -EOF } -Args::defaultHelp showHelp "$@" - -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" "$@" - exit $? -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 - exit 1 -fi -Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" +facade_main_8fd47ebedea14305a1452a7b8820b174 "$@" diff --git a/bin/gitIsAncestorOf b/bin/gitIsAncestorOf index 4f059242..ffd43dc2 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,598 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 +696,790 @@ 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 claimedBranch="" +declare commit="" + +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/installRequirements b/bin/installRequirements index a5551614..82693ea9 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,488 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 +762,777 @@ 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 # -# @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_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" -Log::load +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" +} -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() { + installRequirementsCommand 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 +} + +# 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" +} + +# 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 +} + +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..ceab92fc 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,297 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 +625,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 +865,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 } -# Public: check if argument is a valid linux path +# @description ensure command realpath is available +# @exitcode 1 if realpath command not available +# @stderr diagnostics information is displayed +Linux::requireRealpathCommand() { + Assert::commandExists realpath +} + +# @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_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 + +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 +} +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 +} -if [[ -n "${sqlFile}" ]]; then - if [[ ! -f "${sqlFile}" ]]; then - Log::fatal "file ${sqlFile} does not exist" +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 - exec 3<"${sqlFile}" -elif [[ ! -t 0 ]]; then - exec 3<&0 -else - Log::fatal "No sql file provided..." -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 +1887,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/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..36202515 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,623 @@ 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 +# @stderr diagnostic information + help if second argument is provided +Assert::tty() { + [[ -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 } -# log message to file -# @param {String} $1 message +# @description merge and load conf files specified as argument +# - files are cleaned from ay comment +# - missing quotes after property = sign are added automatically +# - automatic remove of all whitespace before and after declarations +# - bash arrays are not supported +# - if a variable is declared in first file and overridden later on +# in the same file or in subsequent files, those overloads will be +# ignored +# @warning if an error occurs while loading one of the config file, exit code 3 but environment could be partially loaded +# @arg $@ args:String[] list of configuration files to load in order +# @set envVars String will set in environment all the variables that have been declared in the config files +# @env envVars String the env variables of the current script could be used to interpret variables during config files parsing +# @exitcode 0 if no config files provided or load completed successfully +# @exitcode 1 if error occurred during parsing the config files (file not found, grep, awk or sed error) +# @exitcode 2 if temporary file cannot be created +# @exitcode 3 if an error occurred during config file sourcing +# @stderr diagnostics information is displayed +# @see largely inspired but modified from https://opensource.com/article/21/5/processing-configuration-files-shell +Env::mergeConfFiles() { + local -a configFileList=("$@") + + if ((${#configFileList[@]} == 0)); then + return 0 + fi + + local combinedConfigFile + combinedConfigFile="$(Framework::createTempFile "mergeConfFiles")" || return 2 + + ( + # removes any trailing whitespace from each file, if any + # this is absolutely required when importing into ConfigMaps + # put quotes around values + sed -E -e $'s/\s*$// ; /^$/d ; /^#.*$/d ; s/=([^"\'].*)$/="\\1"/' "${configFileList[@]}" | + # remove all comment lines + Filters::commentLines | + # iterates over each file and prints (default awk behavior) + # each unique line; only takes first value and ignores duplicates + awk -F= '!line[$1]++' + ) >"${combinedConfigFile}" || return 1 + + # have to export everything, and source it twice: + # 1) first source is to realize variables + # 2) second time is to realize references + set -o allexport + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + # shellcheck source=.framework-config + source "${combinedConfigFile}" || return 3 + set +o allexport +} + +# @description prepend directories to the PATH environment variable +# @arg $@ args:String[] list of directories to prepend +# @set PATH update PATH with the directories prepended +Env::pathPrepend() { + local arg + for arg in "$@"; do + if [[ -d "${arg}" && ":${PATH}:" != *":${arg}:"* ]]; then + PATH="$(realpath "${arg}"):${PATH}" + fi + done +} + +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable +} + +# @description 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 +721,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 +} + +# shellcheck disable=SC2317 # if function is overridden +optionVersionCallback() { + echo "${SCRIPT_NAME} version 1.0" + exit 0 +} - dir="$(dirname "${file}")" +# 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 +} - Assert::validPath "${file}" && [[ -w "${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} } -# 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 +optionDebugVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_DEBUG} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} +} - # 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 +optionTraceVerboseCallback() { + BASH_FRAMEWORK_ARGS_VERBOSE_OPTION='-vvv' + BASH_FRAMEWORK_ARGS_VERBOSE=${__VERBOSE_LEVEL_TRACE} + BASH_FRAMEWORK_DISPLAY_LEVEL=${__LEVEL_DEBUG} } -# FUNCTIONS +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::load -export BASH_FRAMEWORK_DISPLAY_LEVEL="${__LEVEL_WARNING}" -Args::parseVerbose "${__LEVEL_INFO}" "$@" || true -declare -a args=("$@") -Array::remove args -v --verbose -set -- "${args[@]}" +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 +} -Log::load +# 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::pathPrepend "${COMMAND_BIN_DIR}" +# 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} +} -# 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 +optionLogFileCallback() { + local logFile="$2" + BASH_FRAMEWORK_LOG_FILE="${logFile}" +} -if [[ "$(id -u)" = "0" ]]; then - Log::fatal "this script should be executed as normal user" -fi +# 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" +} -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..aa5bcc3e --- /dev/null +++ b/src/_binaries/Docker/cli.options.tpl @@ -0,0 +1,126 @@ +% +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 + 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..c2500fdf 100755 --- a/src/_binaries/Docker/cli.sh +++ b/src/_binaries/Docker/cli.sh @@ -1,138 +1,75 @@ #!/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" +# @require Linux::requireExecutedAsUser +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..a5c902e9 --- /dev/null +++ b/src/_binaries/Git/gitIsAncestorOf.options.tpl @@ -0,0 +1,44 @@ +% +declare versionNumber="1.0" +declare commandFunctionName="gitIsAncestorOfCommand" +declare optionSkinDefault="default" +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 claimedBranch="" +declare commit="" 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/gitRenameBranch.bats b/src/_binaries/Git/gitRenameBranch.bats index 7471fd93..bef12792 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}" 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/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..0e3d36b4 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..29557d10 --- /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=""" +installs requirements: +- 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