From 951c11a3a98a56d794bb4d638bb845431c6d0a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Mon, 6 May 2024 20:30:34 +0200 Subject: [PATCH] config alignement - overall config alignment with bash-dev-env and bash-tools-framework 4.0.0 - refactored github workflow with 3 separated jobs - use plain docker instructions in lint-test.yml instead of relying on bin/test script - added test.sh script - recompiled all binaries using bash-tools-framework 4.0.0 - upgradeGithubRelease - removed argument --minimal-version - make megalinter lint all codebase - cancel previous build if several pushes - cspell - remove global dictionaries config key and adapt cspell rules accordingly - dbImport/dbImportStream/dbQueryAllDatabases check for requirements compliant in --version and --help options - fix UTs with Retry attempts set to 1 - stabilized unit tests - bin/doc - removed Docker::runBuildContainer dependency - replaced technote-space/workflow-conclusion-action@v3 by AbsoLouie/workflow-conclusion-status@v1.0.2 --- .cspell.json | 119 -- .cspell/bash.txt | 190 +- .cspell/codespellrc-dic.txt | 3 + .cspell/codespellrc-ignore.txt | 1 + .cspell/config.txt | 288 +-- .cspell/loremIpsum.txt | 54 +- .cspell/myAwk.txt | 7 + .cspell/plantUml.txt | 7 +- .cspell/postman.txt | 5 + .cspell/readme.txt | 73 +- .cspell/softwares.txt | 165 +- .deepsource.toml | 17 +- .eslintrc.js | 18 +- .framework-config | 43 +- .github/dependabot.yml | 8 +- .github/preCommitGeneration.sh | 16 + .github/workflows/docsify-gh-pages.yml | 29 +- .github/workflows/lint-test.yml | 390 ++-- .github/workflows/precommit-autoupdate.yml | 31 +- .../set-github-status-on-pr-approved.yml | 18 +- .gitignore | 29 +- .gitleaks.toml | 2 + .grype.yaml | 19 +- .jscpd.json | 16 +- .mega-linter-githubAction.yml | 5 + .mega-linter-light.yml | 1 - .mega-linter.yml | 103 +- .pre-commit-config-github.yaml | 207 ++ .pre-commit-config.yaml | 123 +- .prettierignore | 1 + .prettierrc.yaml | 2 +- .proselintrc | 5 + .secretlintignore | 4 +- .secretlintrc.yml | 7 +- .shellcheckrc | 2 + .stylelintrc.json | 3 + .v8rrc.yaml | 9 +- .vscode/.checkov.yml | 1 + .vscode/extensions.json | 21 +- .vscode/settings.json | 34 +- Commands.tmpl.md | 6 +- README.md | 75 +- bin/cli | 997 ++++----- bin/dbImport | 1785 +++++++++-------- bin/dbImportProfile | 1021 +++++----- bin/dbImportStream | 1629 ++++++++------- bin/dbQueryAllDatabases | 1623 ++++++++------- bin/dbScriptAllDatabases | 941 +++++---- bin/doc | 1316 ++++++------ bin/gitIsAncestorOf | 779 +++---- bin/gitIsBranch | 779 +++---- bin/gitRenameBranch | 765 +++---- bin/installRequirements | 899 +++++---- bin/mysql2puml | 963 ++++----- bin/postmanCli | 1190 +++++------ bin/upgradeGithubRelease | 1323 ++++++------ bin/waitForIt | 799 ++++---- bin/waitForMysql | 801 ++++---- commit-msg-template.md | 2 + conf/.env | 31 +- conf/dbScripts/extractData | 484 +++-- conf/mysql2pumlSkins/default.png | Bin 955 -> 954 bytes cspell.yaml | 166 ++ install | 787 ++++---- logs/.gitignore | 2 + logs/.gitkeep | 0 pages/_sidebar.md | 2 +- pages/index.html | 22 +- .../Converters/testsData/mysql2puml.help.txt | 1 - .../Converters/testsData/mysql2puml.png | Bin 47049 -> 47049 bytes .../testsData/mysql2pumlSkins/default.png | Bin 950 -> 950 bytes src/_binaries/DbImport/dbImport.options.tpl | 3 + src/_binaries/DbImport/dbImport.sh | 10 - src/_binaries/DbImport/dbImportProfile.bats | 2 +- .../DbImport/dbImportStream.options.tpl | 3 + src/_binaries/DbImport/dbImportStream.sh | 6 - .../DbImport/testsData/dbImport.help.txt | 1 - .../testsData/dbImportProfile.help.txt | 1 - .../testsData/dbImportStream.help.txt | 1 - .../dbQueryAllDatabases.options.tpl | 3 + .../dbQueryAllDatabases.sh | 8 - .../testsData/dbQueryAllDatabases.help.txt | 1 - .../testsData/dbScriptAllDatabases.help.txt | 1 - src/_binaries/Docker/testsData/cli.help.txt | 1 - .../Git/testsData/gitIsAncestorOf.help.txt | 1 - .../Git/testsData/gitRenameBranch.help.txt | 1 - .../testsData/upgradeGithubRelease.help.txt | 23 +- src/_binaries/Git/upgradeGithubRelease.bats | 302 ++- .../Git/upgradeGithubRelease.help.txt | 4 - .../Git/upgradeGithubRelease.options.tpl | 23 - src/_binaries/Git/upgradeGithubRelease.sh | 106 +- src/_binaries/Postman/postmanCli.bats | 11 +- .../Postman/testsData/postmanCli.help.txt | 1 - .../Utils/testsData/waitForIt.help.txt | 1 - .../Utils/testsData/waitForMysql.help.txt | 1 - src/_binaries/build/doc.options.tpl | 4 +- src/_binaries/build/doc.sh | 100 +- src/_includes/dbTools.requirements.tpl | 21 + src/batsHeaders.sh | 50 +- test.sh | 54 + 100 files changed, 11857 insertions(+), 10151 deletions(-) delete mode 100644 .cspell.json create mode 100644 .cspell/codespellrc-dic.txt create mode 100644 .cspell/codespellrc-ignore.txt create mode 100644 .cspell/myAwk.txt create mode 100644 .cspell/postman.txt create mode 100755 .github/preCommitGeneration.sh create mode 100644 .pre-commit-config-github.yaml create mode 100644 .proselintrc create mode 100644 .stylelintrc.json create mode 100644 cspell.yaml create mode 100644 logs/.gitignore delete mode 100644 logs/.gitkeep create mode 100644 src/_includes/dbTools.requirements.tpl create mode 100755 test.sh diff --git a/.cspell.json b/.cspell.json deleted file mode 100644 index bfc12c3c..00000000 --- a/.cspell.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "version": "0.2", - "language": "en", - "ignorePaths": [ - "**/node_modules/**", - "**/vscode-extension/**", - "**/vendor/**", - "**/.git/**", - "**/backup/**", - "**/logs/**", - "**/bin/**", - "**/*megalinter_file_names_cspell.txt", - ".history/**", - ".cspell/**", - ".vscode", - "megalinter", - "package-lock.json", - "report", - ".jscpd.json", - ".mega-linter.yml", - ".shellcheckrc", - "**/*.svg", - ".env", - "**/*.help.txt" - ], - "noConfigSearch": true, - "words": ["megalinter", "oxsecurity"], - "dictionaryDefinitions": [ - {"name": "bashCustom", "path": ".cspell/bash.txt"}, - {"name": "loremIpsum", "path": ".cspell/loremIpsum.txt"}, - {"name": "config", "path": ".cspell/config.txt"}, - {"name": "softwares", "path": ".cspell/softwares.txt"}, - {"name": "readme", "path": ".cspell/readme.txt"}, - {"name": "dirColors", "path": ".cspell/dirColors.txt"}, - {"name": "plantUml", "path": ".cspell/plantUml.txt"} - ], - "dictionaries": [ - "bash", - "bashCustom", - "config", - "softwares", - "plantUml", - "loremIpsum" - ], - "languageSettings": [ - { - "languageId": "dirColors", - "locale": "*", - "dictionaries": ["dirColors"] - }, - { - "languageId": "loremIpsum", - "locale": "*", - "dictionaries": ["loremIpsum"] - }, - { - "languageId": "bashCustom", - "locale": "*", - "dictionaries": ["bashCustom"] - }, - { - "languageId": "config", - "locale": "*", - "dictionaries": ["config"] - }, - { - "languageId": "softwares", - "locale": "*", - "dictionaries": ["softwares"] - }, - { - "languageId": "readme", - "locale": "*", - "dictionaries": ["readme"] - }, - { - "languageId": "plantUml", - "locale": "*", - "dictionaries": ["plantUml"] - } - ], - "overrides": [ - { - "filename": "**/conf/bash_profile/.dir_colors", - "languageId": "dirColors" - }, - { - "filename": "**/*.puml", - "languageId": "plantUml" - }, - { - "filename": "**/README.md", - "languageId": "readme" - }, - { - "filename": "README.tmpl.md", - "languageId": "readme" - }, - { - "filename": "LICENSE", - "languageId": "readme" - } - ], - "patterns": [ - { - "name": "urls", - "pattern": "/https?://([^ \t\"'()]+)/g" - }, - { - "name": "packages", - "pattern": "/[-A-Za-z0-9.]+/[-A-Za-z0-9.]+/g" - }, - { - "name": "markdownToc", - "pattern": "\\]\\(#[^)]+\\)$" - } - ], - "ignoreRegExpList": ["urls", "packages", "markdownToc"] -} diff --git a/.cspell/bash.txt b/.cspell/bash.txt index 04d67560..c1540018 100644 --- a/.cspell/bash.txt +++ b/.cspell/bash.txt @@ -1,124 +1,130 @@ ABRT +ABRT +addgroup +asort +AUTOCOMMIT +autojump +autoload +autoupdate +auwm +bashism +bindkey +BUSYTIMEFLAG +canonicalize +CDPATH +Chastanet +chgrp +cntrl +compinit +compinstall +CONV +cvzf +Datash +datetime +DELIMS deps Deps dhclient DISTRO +distutils +DOCKERHUB dotglob +envsubst +EPOCHREALTIME +EPOCHSECONDS +EXIT +exitcode extglob +Facadesh +fgrep +fmask +gensub +getline getopt +gpgsign +gsub +HISTFILE +hlocalhost +HOME HOSTIP +hqbj +HUP IBUS +inlines +installdir +installfile +installsh +INT +ISBUSY +keychain KHTML +killall +lastpipe +lastpipe +libkrb +libloadandcheckconfigsh +loadandcheckconfig +loadprofile LOCALAPPDATA maxdepth mindepth mkdir mktemp +MPFR MYPRUNEPATHS +newuser +noargs +nohup noninteractive NONINTERACTIVE nullglob +onbuild +PATHCONV +pgrep +ppassword +proot +Pubkey +pykerberos readlink -fmask realpath Referer -strfile -USERGROUP -USERGROUPID -USERHOME -HOME -USERID -USERPROFILE -Struct -SYMFONY -fgrep -HUP -bashism -killall -CDPATH -Pubkey -cntrl -newuser -HISTFILE +regextype SAVEHIST +Scrasnups +Scriptsh setopt -unsetopt -bindkey -compinstall -compinit -zstyle -autoload -strftime -textmate -autojump -pgrep -keychain -libkrb -pykerberos -distutils -libloadandcheckconfigsh -loadandcheckconfig -loadprofile -installfile -installdir setupsh -xdebug -xdebugini -wslprofile -undelete -EXIT -INT -TERM -ABRT -autoupdate -auwm -chgrp -nohup -DOCKERHUB -PATHCONV -CONV -uroot -proot -envsubst -regextype -gsub -willywonka -onbuild -TMPDIR +SIGINT +sigpipe SIGUSR -inlines +strfile +strftime +Struct +substr +SYMFONY +TERM +testsuites +textmate TIMEFORMAT -AUTOCOMMIT -zcat -getline -tolower -varchar -asort -ISBUSY -SIGINT -BUSYTIMEFLAG -hqbj TMDIR -substr +TMPDIR +tolower +undelete +unsetopt unstub -cvzf -sigpipe -WORKDIR +uroot userdel -addgroup -MPFR -canonicalize -Chastanet -Scrasnups -datetime -gensub -hlocalhost +USERGROUP +USERGROUPID +USERHOME +USERID +USERPROFILE uuser -ppassword -DELIMS -Facadesh -Scriptsh -noargs -exitcode -Datash -installsh +varchar +willywonka +WORKDIR +wslprofile +xdebug +xdebugini +zcat +zstyle diff --git a/.cspell/codespellrc-dic.txt b/.cspell/codespellrc-dic.txt new file mode 100644 index 00000000..91e1ab2c --- /dev/null +++ b/.cspell/codespellrc-dic.txt @@ -0,0 +1,3 @@ +adpat -> adapt +fileTest -> fileTest +tst -> test diff --git a/.cspell/codespellrc-ignore.txt b/.cspell/codespellrc-ignore.txt new file mode 100644 index 00000000..c6385707 --- /dev/null +++ b/.cspell/codespellrc-ignore.txt @@ -0,0 +1 @@ +softwares diff --git a/.cspell/config.txt b/.cspell/config.txt index c5b08d09..99df9c4d 100644 --- a/.cspell/config.txt +++ b/.cspell/config.txt @@ -1,170 +1,141 @@ -Jenkinsfile +Acked +Aftertabs +apcu APKAIKYTXOYEKD +apos +autocd +autocrlf +autohide autologin automount +Ayatana +backgroundfile +bbwe +bgcolor Cauto -ceph -commandline -dearmor -DEBCONF -EDITMSG -Fira -inputrc -keyrings -meslo -Meslo -Msys -nameserver -NOPASSWD -numlockx -ovpn -pagedown -pageup -pgdn -pgup -PLATFORMTHEME -PRUNENAMES -PRUNEPATHS -Resolv -setaf -srmo -sudoer -sysinfo -templatedir -tigrc -Titlebar -verysilent -vimrc -XDMCP -Xresources -xzvf -mutualized -schroot -Acked -pkexec -efww -pcpu -LIBGL -hushlogin -newermt -rprompt -nocompatible -noswapfile -noeb -mthis -setf -rxvt +Cblue ccomp -COMPREPLY -CWORD -dircolors -HISTCONTROL -HISTFILESIZE -HISTSIZE -HISTIGNORE -HISTCONTROL -histappend -ignorespace -nocasematch -dirspell -direxpand cdspell -autocd +ceph +changelist checkwinsize -cmdhist -histverify -erasedups -ignoreboth -unstaged -newmode -oldmode -bgcolor -fgcolor -expandtab -inet -publickey -Ayatana -Gettext -oneline -Cblue -Creset -numstat -autocrlf -twosuperior -fullscreen -maximised -shellcheckrc -soffice -widthtype -tintcolor -setdocktype -setpartialstrut -heightwhenhidden -usefontcolor -fontcolor -backgroundfile -autohide -Iconified -dclock Clearlooks -nuove -WSLCONFIG -wslconfig -wekyb -bbwe -Parens -tabwidth -nonprod -nifi -WORKON -upgrader -PHPDOC +cmdhist +commandline +COMPREPLY CONCAT +COND +cpes +Creset +CWORD +cyclonedx Datasource -Inplace -LPAREN -PHPCS -PHPDOC -Phar -RPAREN -SGEQS -SONARLINT -Squiz -Structs -Vsbycmmq -Xvsb -apcu -changelist datatable +dclock +dearmor +DEBCONF devicedetect +difftool +dircolors +direxpand +dirspell +EDITMSG +efww encrypter +erasedups exif +expandtab +fgcolor fileinfo fileurl +Fira +fontcolor +friday +fullscreen getallheaders +Gettext +GKHF +gtid +heightwhenhidden +histappend +HISTCONTROL +HISTCONTROL +HISTFILESIZE +HISTIGNORE +HISTSIZE +histverify +hpdy htmlpurifier +hushlogin +Iconified igbinary +ignoreboth +ignorespace imagick +inet inflector +Inplace +inputrc instantiator jdbc +Jenkinsfile jmespath +JSONLINT jsrouting +keyrings +LIBGL +logrus +LPAREN markdownlint +maximised mcrypt +mdformat +megalinter megalinter +meslo +Meslo +Msys +mthis multifile +mutualized mysqli mysqlnd +nameserver +newermt +newmode +nifi +nocasematch +nocompatible +noeb +nonprod +NOPASSWD +noswapfile +numlockx +numstat +nuove +oldmode +oneline +ovpn oxsecurity +oxsecurity +pagedown +pageup +Parens pcntl +pcpu pcre +pgdn +pgup +Phar phar phing phpcollection phpcolors +PHPCS phpcs phpcsfixer +PHPDOC +PHPDOC phpinfo phpmailer phpoption @@ -172,37 +143,72 @@ phpseclib phpspreadsheet phpstan phpunit +pkexec +PLATFORMTHEME predis +PRUNENAMES +PRUNEPATHS +publickey rearranger refact +Resolv riak +RPAREN +rprompt +rxvt +sbom +schroot secretlintrc +setaf +setdocktype +setf +setpartialstrut +SGEQS +shellcheckrc shmop +signingkey simplesamlphp +soffice +SONARLINT speedtrap spyc +Squiz +srmo +Structs subconverter +sudoer swiftmailer +sysinfo sysvmsg sysvsem sysvshm +tabwidth +tagname +templatedir +tigrc +tintcolor +Titlebar +twosuperior +UNINDEXED +unindexed +unstaged +upgrader +usefontcolor +verysilent +vimrc +Vsbycmmq wddx +wekyb +widthtype +WORKON +WSLCONFIG +wslconfig xapi +XDMCP xmlreader xmlseclibs xmlwriter -megalinter -oxsecurity +Xresources +Xvsb +xzvf zipstream -difftool -apos -hpdy -tagname -Aftertabs -GKHF -cyclonedx -cpes -UNINDEXED -unindexed -logrus -JSONLINT diff --git a/.cspell/loremIpsum.txt b/.cspell/loremIpsum.txt index 1419caab..c461d347 100644 --- a/.cspell/loremIpsum.txt +++ b/.cspell/loremIpsum.txt @@ -1,40 +1,40 @@ -Lorem -ipsum -dolor -sit +abitur +adipiscing +aliquet amet +amet +commodo +condimentum consectetur -adipiscing +Curabitur +cursus +dignissim +dolor elit -Maecenas -vel eros +et +fini +finibus id +id +in +ipsum ipsum +libero lobortis -cursus -id -dignissim -turpis -Nam -pretium -placerat +Lorem +Maecenas +Mauris +Name nulla -in +placerat posuere -Mauris -libero +pretium purus -aliquet -et -commodo quis +sapien semper sit -amet -sapien -Curabitur -condimentum -finibus -abitur -fini +sit +turpis +vel diff --git a/.cspell/myAwk.txt b/.cspell/myAwk.txt new file mode 100644 index 00000000..c753ab4d --- /dev/null +++ b/.cspell/myAwk.txt @@ -0,0 +1,7 @@ +asort +gensub +getline +gsub +substr +tolower +varchar diff --git a/.cspell/plantUml.txt b/.cspell/plantUml.txt index c043f218..ab6ee05e 100644 --- a/.cspell/plantUml.txt +++ b/.cspell/plantUml.txt @@ -1,3 +1,6 @@ -startuml -skinparam endfunction +endfunction +enduml +nullable +skinparam +startuml diff --git a/.cspell/postman.txt b/.cspell/postman.txt new file mode 100644 index 00000000..4dcf9bb9 --- /dev/null +++ b/.cspell/postman.txt @@ -0,0 +1,5 @@ +clairefro +EJSON +ejson +octocat +prerequest diff --git a/.cspell/readme.txt b/.cspell/readme.txt index 3e090fe0..13ba2d06 100644 --- a/.cspell/readme.txt +++ b/.cspell/readme.txt @@ -1,43 +1,50 @@ -setupsh -installdir -installfile -mandatorysoftware -Powerlevel -powerlevel -fchastanet -wekyb +Bazyli bbwe -openapi -Jetbrains -markdownlint +bincli +bindbimport +bindbimportprofile +bindbqueryalldatabases +bindbscriptalldatabases +bingitisancestorof +bingitrenamebranch +binmysql +Brzóska +Chastanet cklmurl +CONV +datetime +endfunction +enduml +fchastanet functionsretryparameterized -libloadandcheckconfigsh -loadandcheckconfig -libloadandcheckconfigsh functionsretryparameterized -loadprofile +installationconfiguration +installdir +installfile +Jetbrains libassertsh +libloadandcheckconfigsh +libloadandcheckconfigsh libutilssh +loadandcheckconfig +loadprofile +mandatorysoftware +markdownlint +openapi +PATHCONV +Powerlevel +powerlevel +proot refactorings -xdebug -xdebugini -Bazyli -Brzóska Scrasnups -installationconfiguration -bingitrenamebranch -bindbqueryalldatabases -bindbscriptalldatabases -bindbimport -bindbimportprofile -bincli -bingitisancestorof -binmysql -startuml +setupsh skinparam -endfunction -varchar -datetime -enduml +startuml +Struct +tput uniformization +uroot +varchar +wekyb +xdebug +xdebugini diff --git a/.cspell/softwares.txt b/.cspell/softwares.txt index 666eb7e2..68d3bb93 100644 --- a/.cspell/softwares.txt +++ b/.cspell/softwares.txt @@ -1,4 +1,6 @@ aacebedo +actionlint +adduser alacritty anacron Anacron @@ -9,28 +11,63 @@ Awsume AWSUME baincd Bashtools -MAILHOG buildkit +Buildx byobu +checkstyle +chsh +clairefro codesniffer +codespell +containerd +coreutils cowsay cygwin dbus +debconf +deepsource devcert +Distrib +distro +docsify +dotnet dpkg +ecma +EJSON +ejson +enduml +ETAG fasd Fasd +fchastanet +gdebi +getent +github +GITHUB +Github +gitleaks +gofmt +golxdebindings +goserver graphviz +GRYPE +gtid +gzipped hadolint hjson -hypens +htmlhintrc Hyperv +hyphens iconify +Inno +jekyll jetbrains Jetbrains JETBRAINS keygen keyscan +KICS +kics konsole kterm kube @@ -48,11 +85,17 @@ Kubie lastname launchbar lesspipe +libcairo libcanberra +libdbus +libgirepository libncurses libncursesw lightdm +linebuffer +linux lmbuebmw +ltrim lubuntu lxappearance lxde @@ -61,25 +104,73 @@ LXDE lxpanel lxsession lxterminal +MAILHOG mailserver +mattrose +mbstring +megalinter minikube Minikube mlocate Mlocate +MSYS +MULTIPARTS +mypy +mysql +MYSQL +mysqldump +mysqldump +mysqlshow +nojekyll +obconf +octocat +octocat +openbox +pcmanfm phpmd phpstorm Phpstorm +pixbuf plantuml Plantuml +plantuml +podman +powerlevel Powerline +precommit +prerequest +pstree +puml +pyupgrade +redis +resolvconf +rtrim +RUBOCOP +runc +screenlock +slurpfile stylelint Sume +timeago +tmpl tmuxinator +tomdoc +TRIVY +TRUFFLEHOG +tzdata +updatedb +usermod vcxsrv Vcxsrv venv +virtualenv +wholename +wincmd +winpty wslg Wslg +wslpath +wslvar xcalc xfonts xlaunch @@ -90,73 +181,3 @@ xsession Xsrv xvfb Xvfb -pixbuf -mattrose -openbox -obconf -chsh -debconf -gdebi -getent -golxdebindings -goserver -mbstring -pstree -tzdata -updatedb -usermod -wslpath -wslvar -pcmanfm -wincmd -screenlock -adduser -powerlevel -libdbus -libgirepository -libcairo -virtualenv -gofmt -pyupgrade -precommit -containerd -MULTIPARTS -ETAG -wholename -resolvconf -runc -gzipped -puml -tmpl -deepsource -checkstyle -Buildx -mysqldump -timeago -mysqlshow -gtid -tomdoc -enduml -rtrim -ltrim -Inno -linebuffer -winpty -fchastanet -coreutils -Distrib -docsify -htmlhintrc -gitleaks -nojekyll -RUBOCOP -TRIVY -KICS -TRUFFLEHOG -GRYPE -octocat -prerequest -slurpfile -EJSON -ejson -clairefro diff --git a/.deepsource.toml b/.deepsource.toml index fbf81476..4f0f3294 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -2,7 +2,12 @@ version = 1 test_patterns = ["tests/**"] -exclude_patterns = ["doc/**", "jekyll/**", "bin/deepsource"] +exclude_patterns = [ + "doc/**", + "jekyll/**", + "bin/deepsource", + "**/testsData/**" +] [[analyzers]] name = "test-coverage" @@ -13,13 +18,3 @@ name = "shell" enabled = true [analyzers.meta] dialect = "bash" - -[[analyzers]] -name = "docker" -enabled = true - [analyzers.meta] - dockerfile_paths = [ - ".docker/Dockerfile.alpine", - ".docker/Dockerfile.ubuntu", - ".docker/DockerfileUser" - ] diff --git a/.eslintrc.js b/.eslintrc.js index baf95796..ca275726 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,8 +5,8 @@ module.exports = { root: true, parserOptions: { ecmaVersion: 12, - sourceType: 'module', - parser: 'babel-eslint', + sourceType: "module", + parser: "babel-eslint", ecmaFeatures: { jsx: false, modules: true, @@ -24,20 +24,20 @@ module.exports = { commonjs: true, node: true, }, - plugins: ['json'], + plugins: ["json"], extends: [ - 'eslint:recommended', - 'plugin:json/recommended', - 'eslint-config-prettier', + "eslint:recommended", + "plugin:json/recommended", + "eslint-config-prettier", ], rules: { - 'json/*': ['error', {allowComments: false}], + "json/*": ["error", {allowComments: false}], }, overrides: [ { - files: ['.vscode/*.json'], + files: ["**/.vscode/*.json"], rules: { - 'json/*': ['error', {allowComments: true}], + "json/*": ["error", {allowComments: true}], }, }, ], diff --git a/.framework-config b/.framework-config index 8fe981c1..c7922347 100755 --- a/.framework-config +++ b/.framework-config @@ -1,7 +1,7 @@ #!/usr/bin/env bash # shellcheck disable=SC2034 -BASH_TOOLS_ROOT_DIR="$(cd -- "$(dirname -- "${CURRENT_LOADED_ENV_FILE}")" &>/dev/null && pwd -P)" +BASH_TOOLS_ROOT_DIR="$(cd -- "${CURRENT_LOADED_ENV_FILE%/*}" &>/dev/null && pwd -P)" FRAMEWORK_ROOT_DIR="${BASH_TOOLS_ROOT_DIR}/vendor/bash-tools-framework" FRAMEWORK_SRC_DIR="${FRAMEWORK_ROOT_DIR}/src" FRAMEWORK_BIN_DIR="${FRAMEWORK_ROOT_DIR}/bin" @@ -11,26 +11,28 @@ FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_VENDOR_DIR}/bin" # allows to generate bin file in the right directory export BASH_TOOLS_ROOT_DIR -# compile parameters -# srcFile : file that needs to be compiled -# templateDir : directory from which bash-tpl templates will be searched -# binDir : fallback bin directory in case BIN_FILE has not been provided -# rootDir : directory used to compute src file relative path -# srcDirs : additional directories where to find the functions -COMPILE_PARAMETERS=( - --src-dir "${BASH_TOOLS_ROOT_DIR}/src" - --src-dir "${FRAMEWORK_ROOT_DIR}/src" - --bin-dir "${BASH_TOOLS_ROOT_DIR}/bin" - --root-dir "${BASH_TOOLS_ROOT_DIR}" - --template-dir "${BASH_TOOLS_ROOT_DIR}/src" -) +if [[ ! -v COMPILE_PARAMETERS ]]; then + # compile parameters + # srcFile : file that needs to be compiled + # templateDir : directory from which bash-tpl templates will be searched + # binDir : fallback bin directory in case BIN_FILE has not been provided + # rootDir : directory used to compute src file relative path + # srcDirs : additional directories where to find the functions + COMPILE_PARAMETERS=( + --src-dir "${BASH_TOOLS_ROOT_DIR}/src" + --src-dir "${FRAMEWORK_ROOT_DIR}/src" + --bin-dir "${BASH_TOOLS_ROOT_DIR}/bin" + --root-dir "${BASH_TOOLS_ROOT_DIR}" + --template-dir "${BASH_TOOLS_ROOT_DIR}/src" + ) +fi # describe the functions that will be skipped from being imported FRAMEWORK_FUNCTIONS_IGNORE_REGEXP="${FRAMEWORK_FUNCTIONS_IGNORE_REGEXP:-^(Namespace::functions|Functions::myFunction|Namespace::requireSomething|IMPORT::dir::file|Acquire::ForceIPv4)$}" # describe the files that do not contain function to be imported -NON_FRAMEWORK_FILES_REGEXP="${NON_FRAMEWORK_FILES_REGEXP:-(^bin/|.framework-config|^install$|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/_binaries|^src/_includes|^src/batsHeaders.sh$|^conf/)}" +NON_FRAMEWORK_FILES_REGEXP="${NON_FRAMEWORK_FILES_REGEXP:-(^bin/|\.framework-config|^test\.sh$|^\.github/preCommitGeneration\.sh$|^install$|\.bats$|/testsData/|^manualTests/|/_\.sh$|/ZZZ\.sh$|/__all\.sh$|^src/_binaries|^src/_includes|^src/batsHeaders\.sh$|^conf/)}" # describe the files that are allowed to not have an associated bats file -BATS_FILE_NOT_NEEDED_REGEXP="${BATS_FILE_NOT_NEEDED_REGEXP:-(^conf/|^bin/|.framework-config|^install$|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/batsHeaders.sh$|^src/_includes)}" +BATS_FILE_NOT_NEEDED_REGEXP="${BATS_FILE_NOT_NEEDED_REGEXP:-(^conf/|^bin/|^\.github/preCommitGeneration\.sh$|.framework-config|^install$|\.bats$|/testsData/|^manualTests/|/_\.sh$|/ZZZ\.sh$|/__all\.sh$|^src/batsHeaders\.sh$|^src/_includes)}" # describe the files that are allowed to not have a function matching the filename FRAMEWORK_FILES_FUNCTION_MATCHING_IGNORE_REGEXP="${FRAMEWORK_FILES_FUNCTION_MATCHING_IGNORE_REGEXP:-^conf/|^bin/|^\.framework-config$|\.tpl$|testsData/binaryFile$}" # Source directories @@ -48,5 +50,12 @@ SRC_FILE_PATH="${CURRENT_COMPILED_RELATIVE_FILE#/}" BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" + +# display elapsed time since last log +DISPLAY_DURATION=${DISPLAY_DURATION:-1} + +BINARIES_DIR=( + src/_binaries +) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ea9353ca..9ef12bc8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,10 +2,10 @@ version: 2 updates: - - package-ecosystem: 'github-actions' - directory: '/' + - package-ecosystem: "github-actions" + directory: "/" schedule: # Check for updates to GitHub Actions every week - interval: 'weekly' - day: 'friday' + interval: "weekly" + day: "friday" open-pull-requests-limit: 1 diff --git a/.github/preCommitGeneration.sh b/.github/preCommitGeneration.sh new file mode 100755 index 00000000..175c8540 --- /dev/null +++ b/.github/preCommitGeneration.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +awk \ + ''' + 1;/---/{ + print "###############################################################################" + print "# AUTOMATICALLY GENERATED" + print "# DO NOT EDIT IT" + print "# @generated" + print "###############################################################################" + } + ''' .pre-commit-config.yaml >.pre-commit-config-github.yaml +sed -i -E \ + -e '0,/fail_fast: true/s//fail_fast: false/' \ + -e 's/stages: \[\] # GITHUB/stages: \[manual\] # GITHUB/' \ + .pre-commit-config-github.yaml diff --git a/.github/workflows/docsify-gh-pages.yml b/.github/workflows/docsify-gh-pages.yml index 7d020447..e82f4a50 100644 --- a/.github/workflows/docsify-gh-pages.yml +++ b/.github/workflows/docsify-gh-pages.yml @@ -6,34 +6,27 @@ name: Deploy Docsify on: # Runs on pushes targeting the default branch push: - branches: ['master'] + branches: ["master"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - # actions: read needed by actions/deploy-pages - actions: read - # Allow one concurrent deployment concurrency: - group: 'pages' + group: "pages" cancel-in-progress: true jobs: # Build job build: runs-on: ubuntu-22.04 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx - # kics-scan ignore-line uses: docker/setup-buildx-action@v3 - name: docker pull image @@ -46,16 +39,15 @@ jobs: - name: Check if doc up to date run: | - ./bin/doc + ./bin/doc --ci - name: Setup Pages - uses: actions/configure-pages@v2 + uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: 'pages' - if-no-files-found: error + path: "pages" # Deployment job deploy: @@ -64,7 +56,12 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-22.04 needs: build + permissions: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + actions: read # actions: read needed by actions/deploy-pages + steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index cb55911c..9ea25ae9 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -1,209 +1,341 @@ --- -# kics-scan disable=555ab8f9-2001-455e-a077-f2d0f41e2fb9 # Lint the code base and launch unit test at each push or pull request name: Lint and test on: # yamllint disable-line rule:truthy push: # execute when pushing only branches, not tags branches: - - '**' + - "**" + # avoid infinite loop for auto created PRs + - "!update/pre-commit-*" + tags: + - "*" workflow_dispatch: -permissions: read-all +# cancel previous build if several pushes +concurrency: + group: >- + ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + # Apply linter fixes configuration + # When active, APPLY_FIXES must also be defined as + # environment variable (in github/workflows/mega-linter.yml + # or other CI tool) + APPLY_FIXES: all + # Decide which event triggers application of fixes in a + # commit or a PR (pull_request, push, all) + APPLY_FIXES_EVENT: all + # If APPLY_FIXES is used, defines if the fixes are + # directly committed (commit) or posted in a PR (pull_request) + APPLY_FIXES_MODE: pull_request + # variables to compute complex conditions + COND_UPDATED_SOURCES: false + COND_APPLY_FIXES_NEEDED: false jobs: - build: + # ------------------------------------------------------- + # Pre-commit + # ------------------------------------------------------- + + pre-commit: runs-on: ubuntu-22.04 permissions: # needed by ouzi-dev/commit-status-updater@v2 statuses: write - # needed by peter-evans/create-pull-request@v5 + # needed by megalinter + issues: write + # needed by megalinter pull-requests: write - # needed by peter-evans/create-pull-request@v5 - contents: write - strategy: - fail-fast: true - matrix: - vendor: - - ubuntu - - alpine - bashTarVersion: - - '4.4' - - '5.0' - - '5.1' - include: - - vendor: ubuntu - bashImage: ubuntu:20.04 - batsOptions: -j 30 - bashTarVersion: 4.4 - runPrecommitTests: false - - vendor: ubuntu - bashImage: ubuntu:20.04 - bashTarVersion: 5.0 - batsOptions: -j 30 - runPrecommitTests: false - - vendor: ubuntu - bashImage: ubuntu:20.04 - bashTarVersion: 5.1 - batsOptions: -j 30 - runPrecommitTests: true - - vendor: alpine - bashTarVersion: 4.4 - bashImage: amd64/bash:4.4-alpine3.18 - batsOptions: -j 30 - runPrecommitTests: false - - vendor: alpine - bashTarVersion: 5.0 - bashImage: amd64/bash:5.0-alpine3.18 - batsOptions: -j 30 - runPrecommitTests: false - - vendor: alpine - bashTarVersion: 5.1 - bashImage: amd64/bash:5.1-alpine3.18 - batsOptions: -j 30 - runPrecommitTests: false steps: + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 + with: + name: build-bash-tools + status: pending + - name: Checkout - # kics-scan ignore-line uses: actions/checkout@v4 - name: Set up Docker Buildx - # kics-scan ignore-line uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - # kics-scan ignore-line uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # kics-scan ignore-line - - uses: ouzi-dev/commit-status-updater@v2 + - uses: crazy-max/ghaction-import-gpg@v6 + if: ${{ success() }} + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - uses: tibdex/github-app-token@v1 + if: ${{ success() }} + id: generate-token with: - name: build bash-tools-${{matrix.vendor}}-${{matrix.bashTarVersion}} + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 + with: + name: pre-commit-megalinter status: pending - # only if pre-commit + - name: Set env vars + id: vars + # shellcheck disable=SC2129 + run: | + ( + echo "branch_name=${GITHUB_REF##*/}" + ) >> "${GITHUB_ENV}" + - name: Set up Python - if: matrix.runPrecommitTests - # kics-scan ignore-line uses: actions/setup-python@v5 with: python-version: 3.9 - - name: Install pre-commit - if: matrix.runPrecommitTests - run: pip install pre-commit + - uses: fchastanet/github-action-setup-shfmt@v4.0.0 - # kics-scan ignore-line - - uses: ouzi-dev/commit-status-updater@v2 - if: matrix.runPrecommitTests - with: - name: lint - status: pending + - name: Install requirements + run: | + set -exo pipefail + + bin/installRequirements + vendor/bash-tools-framework/bin/installRequirements + vendor/bash-tools-framework/bin/installRequirements + docker pull scrasnups/build:bash-tools-ubuntu-5.3 - name: Run pre-commit - if: matrix.runPrecommitTests - run: pre-commit run -a --hook-stage manual + uses: pre-commit/action@v3.0.1 + id: preCommit + with: + extra_args: >- + -c .pre-commit-config-github.yaml -a --hook-stage manual - - name: Archive results - if: matrix.runPrecommitTests && always() - continue-on-error: true - # kics-scan ignore-line + - name: MegaLinter + id: ml + if: ${{ always() }} + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/flavors/ + uses: oxsecurity/megalinter/flavors/terraform@v7 + # All available variables are described in documentation + # https://megalinter.io/configuration/ + env: + # Validates all source when push on master, + # else just the git diff with master. + # Override with true if you always want to lint all sources + VALIDATE_ALL_CODEBASE: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEGALINTER_CONFIG: .mega-linter-githubAction.yml + CI_MODE: 1 + + - name: Upload MegaLinter artifacts + if: success() || failure() uses: actions/upload-artifact@v4 with: - name: linter-reports + name: MegaLinter reports path: | - megalinter-reports/** - bin/** + megalinter-reports + mega-linter.log + + - name: MegaLinter/Precommit has updated sources + if: > + steps.preCommit.outcome == 'failure' || ( + steps.ml.outputs.has_updated_sources == 1 && ( + env.APPLY_FIXES_EVENT == 'all' || + env.APPLY_FIXES_EVENT == github.event_name + ) + ) + run: | + echo "COND_UPDATED_SOURCES=true" >> "${GITHUB_ENV}" + + - name: is apply fixes needed ? + if: > + env.APPLY_FIXES_MODE == 'pull_request' && ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == + github.repository + ) + run: | + echo "COND_APPLY_FIXES_NEEDED=true" >> "${GITHUB_ENV}" - name: Create Pull Request - if: matrix.runPrecommitTests && failure() - # kics-scan ignore-line - uses: peter-evans/create-pull-request@v5 + id: cpr + # prettier-ignore + if: > + env.COND_UPDATED_SOURCES == 'true' && + env.COND_APPLY_FIXES_NEEDED == 'true' && + !contains(github.event.head_commit.message, 'skip fix') + uses: peter-evans/create-pull-request@v6 with: - branch: update/pre-commit-fixes + token: ${{ steps.generate-token.outputs.token }} + committer: fchastanet + branch: update/pre-commit-fixes-${{ env.branch_name }} + delete-branch: true title: lint fixes commit-message: Auto-update lint fixes body: | some auto fixes have been generated during pre-commit run - labels: updates + labels: pre-commit-fixes - # kics-scan ignore-line - - uses: ouzi-dev/commit-status-updater@v2 - if: matrix.runPrecommitTests && always() + - name: Print Pull request created + if: | + steps.cpr.outputs.pull-request-number && + steps.cpr.outcome == 'success' + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" + + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 + if: ${{ always() }} with: - name: lint + name: pre-commit-megalinter status: ${{ job.status }} - # Run unit tests + # ------------------------------------------------------- + # Unit tests + # ------------------------------------------------------- + + unit-tests: + runs-on: ubuntu-22.04 + permissions: + # needed by ouzi-dev/commit-status-updater@v2 + statuses: write + # needed by mikepenz/action-junit-report@v4 + checks: write + strategy: + fail-fast: true + matrix: + vendor: + - ubuntu + - alpine + bashTarVersion: + - "4.4" + - "5.0" + - "5.3" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: crazy-max/ghaction-import-gpg@v6 + if: ${{ success() }} + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - uses: tibdex/github-app-token@v1 + if: ${{ success() }} + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 + with: + name: unit-tests-${{matrix.vendor}}-${{matrix.bashTarVersion}} + status: pending + + - name: Set env vars + id: vars + # shellcheck disable=SC2129 + run: | + ( + echo "job_tag=${{github.run_id}}-${{matrix.vendor}}-${{matrix.bashTarVersion}}" + echo "image_tag=bash-tools-${{matrix.vendor}}-${{matrix.bashTarVersion}}" + echo "image_name=scrasnups/build" + echo "branch_name=${GITHUB_REF##*/}" + if [[ "${{ matrix.vendor }}" = "ubuntu" ]]; then + echo "bashImage=ubuntu:20.04" + echo "batsOptions=-j 30" + else + echo "bashImage=amd64/bash:${{ matrix.bashTarVersion }}-alpine3.19" + echo "batsOptions=-j 30 --filter-tags '!ubuntu_only'" + fi + ) >> "${GITHUB_ENV}" + + - name: install requirements + run: | + set -exo pipefail + chmod 777 logs + bin/installRequirements + vendor/bash-tools-framework/bin/installRequirements + - name: run unit tests + id: unitTests run: | set -exo pipefail - bin/installRequirements + status=0 + CI_MODE=1 ./test.sh "scrasnups/build:${{env.image_tag}}" \ + ${{env.batsOptions}} \ + --formatter junit -o logs -r src 2>&1 | + tee "logs/bats-${{ env.job_tag }}.log" || status=$? - chmod -R 777 logs - # shellcheck disable=SC2266 - USER_ID=1000 \ - GROUP_ID=1000 \ - vendor/bash-tools-framework/bin/test \ - --vendor "${{matrix.vendor}}" \ - --bash-version "${{matrix.bashTarVersion}}" \ - --bash-base-image "${{matrix.bashImage}}" \ - --branch-name "${GITHUB_REF##*/}" \ - ${{matrix.batsOptions}} --report-formatter junit -o logs -r src --ci + awk '/xml version="1.0"/{flag=1} flag; /<\/testsuites>/{flag=0}' \ + "logs/bats-${{ env.job_tag }}.log" >"logs/junit-${{ env.job_tag }}.xml" + exit "${status}" + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: ${{ always()}} + with: + token: ${{ github.token }} + check_name: JUnit ${{ env.image_tag }} + fail_on_failure: true + require_tests: true + require_passed_tests: true + report_paths: "logs/**.xml" + + - name: Checkstyle aggregation + uses: lcollins/checkstyle-github-action@v3.0.0 + with: + path: "logs/*.xml" - name: Upload Test Results - if: always() - # kics-scan ignore-line + if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: Test Results ${{matrix.vendor}} ${{matrix.bashTarVersion}} - path: logs/report.xml + name: Test Results ${{ env.image_tag }} + path: | + logs/** - # kics-scan ignore-line - - uses: ouzi-dev/commit-status-updater@v2 - if: always() + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: - name: build bash-tools-${{matrix.vendor}}-${{matrix.bashTarVersion}} + name: unit-tests-${{matrix.vendor}}-${{matrix.bashTarVersion}} status: ${{ job.status }} - publishTestResults: - name: 'Publish Tests Results' + overallTestResults: + name: "Overall Tests Results" if: ${{ always() }} - needs: [build] - runs-on: ubuntu-latest + needs: [unit-tests] + runs-on: ubuntu-22.04 permissions: - checks: write - # needed by ouzi-dev/commit-status-updater@v2 statuses: write - # only needed unless run with comment_mode: off - pull-requests: write steps: - - name: Download Artifacts - # kics-scan ignore-line - uses: actions/download-artifact@v3 - with: - path: artifacts - - - name: Checkstyle aggregation - # kics-scan ignore-line - uses: lcollins/checkstyle-github-action@v2.0.0 - with: - path: 'artifacts/**/*.xml' - # run this action to get the workflow conclusion # You can get the conclusion via env (env.WORKFLOW_CONCLUSION) - # kics-scan ignore-line - - uses: technote-space/workflow-conclusion-action@v3 + - uses: AbsoLouie/workflow-conclusion-status@v1.0.2 - # kics-scan ignore-line - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: - name: build + name: build-bash-tools # neutral, success, skipped, cancelled, timed_out, action_required, failure status: ${{ env.WORKFLOW_CONCLUSION }} diff --git a/.github/workflows/precommit-autoupdate.yml b/.github/workflows/precommit-autoupdate.yml index e6061bad..815760d1 100644 --- a/.github/workflows/precommit-autoupdate.yml +++ b/.github/workflows/precommit-autoupdate.yml @@ -1,27 +1,18 @@ --- -# kics-scan disable=555ab8f9-2001-455e-a077-f2d0f41e2fb9 # Check if precommit packages need to be updated and create PR if this is the case name: Pre-commit auto-update -on: +on: # yamllint disable-line rule:truthy workflow_dispatch: schedule: # https://crontab.cronhub.io/ - - cron: '30 10 * * *' - -permissions: read-all - + - cron: "30 10 * * *" jobs: auto-update: runs-on: ubuntu-22.04 - permissions: - pull-requests: write - contents: write steps: - # kics-scan ignore-line - uses: actions/checkout@v4 - name: Set up Python - # kics-scan ignore-line uses: actions/setup-python@v5 with: python-version: 3.9 @@ -32,10 +23,24 @@ jobs: - name: Run pre-commit autoupdate run: pre-commit autoupdate + - uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Create Pull Request - # kics-scan ignore-line - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: + token: ${{ steps.generate-token.outputs.token }} + committer: fchastanet branch: update/pre-commit-autoupdate title: Auto-update pre-commit hooks commit-message: Auto-update pre-commit hooks diff --git a/.github/workflows/set-github-status-on-pr-approved.yml b/.github/workflows/set-github-status-on-pr-approved.yml index b2885b2b..1c01d922 100644 --- a/.github/workflows/set-github-status-on-pr-approved.yml +++ b/.github/workflows/set-github-status-on-pr-approved.yml @@ -1,8 +1,7 @@ --- -# kics-scan disable=555ab8f9-2001-455e-a077-f2d0f41e2fb9 # set git commit status when PR is approved name: Set PR approved git status -on: +on: # yamllint disable-line rule:truthy pull_request_review: types: [submitted] @@ -10,17 +9,20 @@ permissions: read-all jobs: build: - if: github.event.review.state == 'approved' + # only review approved and + # the event is not triggered by a fork + if: | + github.event.review.state == 'approved' && + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-22.04 steps: - # kics-scan ignore-line - uses: actions/checkout@v4 + - name: Run the action # You would run your tests before this using the output to set state/desc - # kics-scan ignore-line uses: Sibz/github-status-action@v1 with: authToken: ${{secrets.GITHUB_TOKEN}} - context: 'PR approved' - description: 'Passed' - state: 'success' + context: "PR approved" + description: "Passed" + state: "success" sha: ${{github.event.pull_request.head.sha || github.sha}} diff --git a/.gitignore b/.gitignore index 0daf9e6c..9c847dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,25 @@ -/logs/* -!/logs/.gitkeep -/.history -/megalinter-reports -/vendor +vendor/ +.history/ *.bak - -# docsify github pages -/pages/*.md -/pages/bashDoc -/pages/tests -!/**/_sidebar.md -!/**/_navbar.md +commit-msg.md # node modules node_modules/ package*.json yarn.lock -commit-msg.md +# megalinter +megalinter-reports/ +*megalinter_file_names_cspell.txt + +# docsify github pages +pages/*.md +pages/_site/ +pages/doc/ +pages/bashDoc/ +pages/tests/ +!**/_sidebar.md +!**/_navbar.md bin/test +bin/buildBinFiles diff --git a/.gitleaks.toml b/.gitleaks.toml index 0579adfd..2671f053 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -12,6 +12,8 @@ useDefault = true '''.automation/test''', '''megalinter-reports''', '''.github/linters''', + '''node_modules''', + '''.mypy_cache''', '''(.*?)/testsData/''', '''(.*?)tests/data/''', '''(.*?)tests/tools/data/''', diff --git a/.grype.yaml b/.grype.yaml index a1c0278a..1e099512 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -1,3 +1,4 @@ +--- # enable/disable checking for application updates on startup # same as GRYPE_CHECK_FOR_APP_UPDATE env var # check-for-app-update: true @@ -13,7 +14,7 @@ # upon scanning, if a severity is found at or above the given severity then the return code will be 1 # default is unset which will skip this validation (options: negligible, low, medium, high, critical) # same as --fail-on ; GRYPE_FAIL_ON_SEVERITY env var -fail-on-severity: 'high' +fail-on-severity: "high" # the output format of the vulnerability report (options: table, json, cyclonedx) # same as -o ; GRYPE_OUTPUT env var @@ -44,19 +45,29 @@ fail-on-severity: 'high' # Explicitly specify a linux distribution to use as : like alpine:3.10 # distro: +# external-sources: +# enable: false +# maven: +# search-upstream-by-sha1: true +# base-url: https://search.maven.org/solrsearch/select + # db: # check for database updates on execution # same as GRYPE_DB_AUTO_UPDATE env var -# auto-update: true +auto-update: true # location to write the vulnerability database cache # same as GRYPE_DB_CACHE_DIR env var -# cache-dir: "$XDG_CACHE_HOME/grype/db" +cache-dir: "/tmp/lint/megalinter-reports/grype-db" # URL of the vulnerability database # same as GRYPE_DB_UPDATE_URL env var # update-url: "https://toolbox-data.anchore.io/grype/databases/listing.json" +# Timeout for downloading actual vulnerability DB +# The DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed +update-download-timeout: "240s" + # it ensures db build is no older than the max-allowed-built-age # set to false to disable check # validate-age: true @@ -64,7 +75,7 @@ fail-on-severity: 'high' # Max allowed age for vulnerability database, # age being the time since it was built # Default max age is 120h (or five days) -# max-allowed-built-age: "120h" +max-allowed-built-age: "120h" # search: # the search space to look for packages (options: all-layers, squashed) diff --git a/.jscpd.json b/.jscpd.json index bac9e1bc..3385a7d4 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -11,23 +11,25 @@ "**/logs/**", "**/megalinter-reports/**", "conf/localAppData/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/originalSettings.json", - "vendor/bash-tools-framework/**", - "vendor/bash-tools-framework/.cspell/*", - "src/_binaries/DbImport/dbImportProfile.options.tpl", - "testsData/**", "vendor/bats*/**", "pages/README.md", - "pages/Commands.md", - "**/snippets/**", "**/.venv/**", "**/*cache*/**", "**/.github/**", "**/.idea/**", "**/report/**", "**/*.svg", + ".pre-commit-config-github.yaml", + "vendor/**", "**/testsData/**", + "pages/doc/**", + "pages/Commands.md", + "vendor/bash-tools-framework/**", + "vendor/bash-tools-framework/.cspell/*", "conf/postmanCli/MongoDbData/MongoDBDataAPI.postman_collection.json", "conf/postmanCli/GithubAPI/GitHubAPI-01-Basic_no_auth_postman_collection.json", - "conf/postmanCli/GithubAPI/GitHubAPI-02-Advanced_with_auth_postman_collection.json" + "conf/postmanCli/GithubAPI/GitHubAPI-02-Advanced_with_auth_postman_collection.json", + "src/_binaries/DbImport/dbImportProfile.options.tpl", + "**/snippets/**" ] } diff --git a/.mega-linter-githubAction.yml b/.mega-linter-githubAction.yml index 9a3ef9c0..32775a16 100644 --- a/.mega-linter-githubAction.yml +++ b/.mega-linter-githubAction.yml @@ -2,3 +2,8 @@ EXTENDS: - .mega-linter.yml SHOW_ELAPSED_TIME: false +VALIDATE_ALL_CODEBASE: true + +GITHUB_COMMENT_REPORTER: true +GITHUB_STATUS_REPORTER: true +UPDATED_SOURCES_REPORTER: true diff --git a/.mega-linter-light.yml b/.mega-linter-light.yml index 40144cf8..4378a2da 100644 --- a/.mega-linter-light.yml +++ b/.mega-linter-light.yml @@ -15,4 +15,3 @@ DISABLE_LINTERS: - REPOSITORY_TRUFFLEHOG # REPOSITORY_GRYPE disabled because too slow - REPOSITORY_GRYPE - - JSON_JSONLINT diff --git a/.mega-linter.yml b/.mega-linter.yml index aa5a1924..2aadebe3 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -1,6 +1,6 @@ --- # Configuration file for MegaLinter -# See all available variables at https://megalinter.io/latest/configuration/ and in linters documentation +# See all available variables at https://megalinter.io/latest/config-file/ and in linters documentation APPLY_FIXES: all # all, none, or list of linter keys # ENABLE: # If you use ENABLE variable, all other languages/formats/tooling-formats will be disabled by default @@ -11,21 +11,23 @@ DISABLE_LINTERS: - JAVASCRIPT_ES - JAVASCRIPT_PRETTIER - RUBY_RUBOCOP - - TERRAFORM_CHECKOV - REPOSITORY_CHECKOV - REPOSITORY_TRIVY - REPOSITORY_KICS - SPELL_VALE - SPELL_LYCHEE - SPELL_PROSELINT - - JSON_JSONLINT + - MARKDOWN_MARKDOWN_TABLE_FORMATTER + - MARKDOWN_MARKDOWN_LINK_CHECK # temporary disabled as anchor links are consider bad + # DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass EXCLUDED_DIRECTORIES: - - '.history' - - '.git' - - '.idea' - - 'logs' - - 'node_modules' + - ".history" + - ".git" + - ".idea" + - "logs" + - "node_modules" + - "vendor" FILTER_REGEX_EXCLUDE: | (?x)( \.git/| @@ -37,21 +39,21 @@ FILTER_REGEX_EXCLUDE: | FILEIO_REPORTER: false PRE_COMMANDS: - command: env - cwd: 'workspace' + cwd: "workspace" - command: yarn add --dev eslint-plugin-json eslint-config-prettier - cwd: 'workspace' + cwd: "workspace" POST_COMMANDS: # FIX files set as root user # HOST_USER_ID and HOST_GROUP_ID set in package.json - command: | - if [[ "{HOST_USER_ID:-0}" != "0" && "{HOST_GROUP_ID:-0}" != "0" ]]; then + if [[ "${CI_MODE:0}" = "0" && "{HOST_USER_ID:-0}" != "0" && "{HOST_GROUP_ID:-0}" != "0" ]]; then find . -user 0 -exec chown ${HOST_USER_ID}:${HOST_GROUP_ID} {} ';' fi - cwd: 'workspace' + cwd: "workspace" # remove files generated by cspell - command: find . -name '*megalinter_file_names_cspell.txt' -delete - cwd: 'workspace' + cwd: "workspace" PRINT_ALPACA: false SHOW_ELAPSED_TIME: true @@ -59,28 +61,27 @@ SHOW_ELAPSED_TIME: true # Linters configurations BASH_SHELLCHECK_FILTER_REGEX_EXCLUDE: | (?x)( - ^vendor| + ^vendor/| /testsData/ ) BASH_SHELLCHECK_ARGUMENTS: --source-path=/tmp/lint BASH_SHFMT_ARGUMENTS: -i 2 -ci -BASH_SHFMT_FILTER_REGEX_EXCLUDE: .\.zsh$|/\.zshrc|/testsData/|^Gemfile.lock$ +BASH_SHFMT_FILTER_REGEX_EXCLUDE: /testsData/ -CREDENTIALS_SECRETLINT_CONFIG_FILE: .secretlintrc.yml +REPOSITORY_SECRETLINT_CONFIG_FILE: .secretlintrc.yml EDITORCONFIG_EDITORCONFIG_CHECKER_FILTER_REGEX_EXCLUDE: | (?x)( \.git/| /testsData/| ^manualTests/data/| - bin/bash-tpl| - ^doc/guides/Options/generate.*\.md$| - ^pages/Commands.md| - ^.*-megalinter_file_names_cspell.txt + \.md$| + ^.*-megalinter_file_names_cspell.txt| + testsData/.*\.result$ ) GIT_GIT_DIFF_PRE_COMMANDS: - - command: git config --global core.autocrlf input + - command: git config --global core.autocrlf false continue_if_failed: false - command: git config --global core.eol lf continue_if_failed: false @@ -88,6 +89,8 @@ GIT_GIT_DIFF_PRE_COMMANDS: continue_if_failed: false - command: git config --global core.excludesfile .gitignore continue_if_failed: false + - command: git config --global apply.whitespace fix + continue_if_failed: false - command: git config --global --add safe.directory .git continue_if_failed: false @@ -98,24 +101,66 @@ JAVASCRIPT_DEFAULT_STYLE: prettier JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.js JAVASCRIPT_ES_FILTER_REGEX_EXCLUDE: (report/) -MARKDOWN_MARKDOWN_LINK_CHECK_FILTER_REGEX_EXCLUDE: (report/) +JSON_ESLINT_PLUGIN_JSONC_FILE_NAME: .eslintrc.js -SPELL_CSPELL_FILTER_REGEX_EXCLUDE: (\.git/|\.history/|IDE/.*/.idea|.*-megalinter_file_names_cspell.txt) -SPELL_FILTER_REGEX_EXCLUDE: (\.git/|\.history/|IDE/.*/.idea) +JSON_JSONLINT_FILTER_REGEX_EXCLUDE: | + (?x)( + \.vscode/.*\.json$| + /testsData/ + ) -SPELL_LYCHEE_FILTER_REGEX_EXCLUDE: | +SPELL_CSPELL_CONFIG_FILE: cspell.yaml +SPELL_CSPELL_FILTER_REGEX_EXCLUDE: | (?x)( - ^pages/Commands.md$ + \.git/| + ^\.history/| + IDE/.*/\.idea| + ^.*-megalinter_file_names_cspell.txt| + ^megalinter-reports/| + ^pages/Commands.md| + ^commit-msg.md$| + /testsData/| + \.svg$ + ) +SPELL_FILTER_REGEX_EXCLUDE: | + (?x)( + \.git/| + \.history/| + \.idea/ ) -JSON_ESLINT_PLUGIN_JSONC_FILE_NAME: .eslintrc.js -JSON_PRETTIER_FILTER_REGEX_EXCLUDE: | +MARKDOWN_MARKDOWN_LINK_CHECK_FILTER_REGEX_EXCLUDE: | + (?x)( + report/ + ) + +ACTION_ACTIONLINT_FILTER_REGEX_EXCLUDE: | + (?x)( + ^\.github/workflows/dependabot\.yml$ + ) + +YAML_V8R_FILTER_REGEX_EXCLUDE: | + (?x)( + ^\.vscode/\.checkov\.yml| + ^\.github/workflows/dependabot\.yml$| + ^\.secretlintrc\.yml| + ^\.grype\.yaml + ) + +MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: | + (?x)( + ^pages/Commands.md| + ^pages/README.md + ) + +JSON_ESLINT_PLUGIN_JSONC_FILTER_REGEX_EXCLUDE: | (?x)( ^src/Postman/Model/testsData/pushMode/GithubAPI/notValidJsonFile.json$| ^src/Postman/Collection/testsData/postmanCollections_invalidJsonFile.json$| ^src/Postman/Model/testsData/getCollectionInvalid.json$ ) -JSON_ESLINT_PLUGIN_JSONC_FILTER_REGEX_EXCLUDE: | + +JSON_PRETTIER_FILTER_REGEX_EXCLUDE: | (?x)( ^src/Postman/Model/testsData/pushMode/GithubAPI/notValidJsonFile.json$| ^src/Postman/Collection/testsData/postmanCollections_invalidJsonFile.json$| diff --git a/.pre-commit-config-github.yaml b/.pre-commit-config-github.yaml new file mode 100644 index 00000000..6fa9aca4 --- /dev/null +++ b/.pre-commit-config-github.yaml @@ -0,0 +1,207 @@ +--- +############################################################################### +# AUTOMATICALLY GENERATED +# DO NOT EDIT IT +# @generated +############################################################################### +default_install_hook_types: [pre-commit, pre-push] +default_stages: [pre-commit, manual] +minimum_pre_commit_version: 3.5.0 +fail_fast: false +repos: + - repo: local + # this hook is not intended to be run on github + # it just allows to generate the same pre-commit + # file with some specific option to github + hooks: + - id: preCommitGeneration + name: preCommitGeneration + entry: .github/preCommitGeneration.sh + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [pre-commit, pre-push, manual] + + - repo: https://github.com/executablebooks/mdformat + # Do this before other tools "fixing" the line endings + rev: 0.7.17 + hooks: + - id: mdformat + name: Format Markdown + entry: mdformat # Executable to run, with fixed options + language: python + types: [markdown] + args: [--wrap, "80", --number] + additional_dependencies: + - mdformat-toc + - mdformat-shfmt + - mdformat-tables + - mdformat-config + - mdformat-web + - mdformat-gfm + + - repo: local + hooks: + - id: install-requirements + name: install-requirements + entry: bash -c './bin/installRequirements' + language: system + always_run: true + require_serial: true + fail_fast: true + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: mixed-line-ending + - id: end-of-file-fixer + exclude: | + (?x)( + .svg$| + ^src\/Postman\/Model\/testsData\/pullMode\/GithubAPI\/notWritableFile.json$ + ) + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-xml + - id: check-merge-conflict + - id: detect-private-key + - id: fix-byte-order-marker + - id: check-yaml + - id: trailing-whitespace + exclude: | + (?x)( + testsData/ + ) + stages: [pre-commit, pre-push, manual] + - id: check-added-large-files + - id: forbid-new-submodules + - id: mixed-line-ending + args: [--fix=lf] + - id: file-contents-sorter + files: .cspell/.*\.txt + args: [--ignore-case] + stages: [pre-commit, pre-push, manual] + - id: check-json + # x modifier: extended. Spaces and text after a # in the pattern are ignored + exclude: | + (?x)^( + \.vscode\/.*\.json$| + src\/Postman\/Collection\/testsData\/postmanCollections_invalidJsonFile.json| + src\/Postman\/Model\/testsData\/pushMode\/GithubAPI\/notValidJsonFile.json| + src\/Postman\/Model\/testsData\/getCollectionInvalid.json| + src\/Postman\/Model\/testsData\/pullMode\/GithubAPI\/notWritableFile.json + )$ + + - repo: https://github.com/rhysd/actionlint + rev: v1.6.27 + hooks: + - id: actionlint + stages: [pre-commit, pre-push, manual] + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: + - --dictionary + - "-" + - --dictionary + - .cspell/codespellrc-dic.txt + - --ignore-words + - .cspell/codespellrc-ignore.txt + - --quiet-level + - "2" + - --interactive + - "0" + - --check-filenames + - --check-hidden + - --write-changes + exclude: > + (?x)( + ^installScripts/| + ^bin/| + ^install$| + ^distro$| + ^srcAlt| + ^.cspell/codespellrc-.*.txt$ + ) + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + exclude: | + (?x)^( + src\/Postman\/Collection\/testsData\/postmanCollections_invalidJsonFile.json| + src\/Postman\/Model\/testsData\/getCollectionInvalid.json| + src\/Postman\/Model\/testsData\/pushMode\/GithubAPI\/notValidJsonFile.json + )$ + + - repo: https://github.com/fchastanet/jumanjihouse-pre-commit-hooks + rev: 3.0.2 + hooks: + - id: shfmt + args: [-i, "2", -ci] + # x modifier: extended. Spaces and text after a # in the pattern are ignored + exclude: | + (?x)( + ^vendor/| + ^bin/| + \.tpl$| + /testsData/| + ^conf/dbScripts/| + ^install + ) + + # Check both committed and uncommitted files for git conflict + # markers and whitespace errors according to core.whitespace + # and conflict-marker-size configuration in a git repo. + - id: git-check + exclude: /testsData/ + + - repo: https://github.com/fchastanet/bash-tools-framework + rev: 4.0.0 + hooks: + - id: fixShebangExecutionBit + - id: awkLint + - id: buildShFiles + - id: buildShFilesGithubAction + - id: shellcheckLint + - id: shellcheckLintGithubAction + - id: frameworkLint + args: + [ + --expected-warnings-count, + "6", + --format, + plain, + --theme, + default-force, + --display-level, + WARNING, + ] + - id: frameworkLintGithubAction + args: + [ + --expected-warnings-count, + "6", + --format, + checkstyle, + --theme, + default-force, + --display-level, + WARNING, + ] + - id: plantuml + + - repo: local + hooks: + - id: buildDocFilesForGithubActions + name: build doc files for github actions + language: script + entry: bin/doc + args: [--ci, --verbose] + pass_filenames: false + require_serial: true + stages: [manual] # GITHUB diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63b4d9a3..aa20b251 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,40 @@ default_install_hook_types: [pre-commit, pre-push] default_stages: [pre-commit, manual] minimum_pre_commit_version: 3.5.0 +fail_fast: true repos: + - repo: local + # this hook is not intended to be run on github + # it just allows to generate the same pre-commit + # file with some specific option to github + hooks: + - id: preCommitGeneration + name: preCommitGeneration + entry: .github/preCommitGeneration.sh + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [pre-commit, pre-push, manual] + + - repo: https://github.com/executablebooks/mdformat + # Do this before other tools "fixing" the line endings + rev: 0.7.17 + hooks: + - id: mdformat + name: Format Markdown + entry: mdformat # Executable to run, with fixed options + language: python + types: [markdown] + args: [--wrap, "80", --number] + additional_dependencies: + - mdformat-toc + - mdformat-shfmt + - mdformat-tables + - mdformat-config + - mdformat-web + - mdformat-gfm + - repo: local hooks: - id: install-requirements @@ -14,7 +47,7 @@ repos: fail_fast: true - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: mixed-line-ending - id: end-of-file-fixer @@ -23,27 +56,72 @@ repos: .svg$| ^src\/Postman\/Model\/testsData\/pullMode\/GithubAPI\/notWritableFile.json$ ) - - id: trailing-whitespace - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-xml + - id: check-merge-conflict + - id: detect-private-key + - id: fix-byte-order-marker - id: check-yaml + - id: trailing-whitespace + exclude: | + (?x)( + testsData/ + ) + stages: [pre-commit, pre-push, manual] - id: check-added-large-files - id: forbid-new-submodules - id: mixed-line-ending args: [--fix=lf] + - id: file-contents-sorter + files: .cspell/.*\.txt + args: [--ignore-case] + stages: [pre-commit, pre-push, manual] - id: check-json + # x modifier: extended. Spaces and text after a # in the pattern are ignored exclude: | (?x)^( - conf\/.vscode\/settings.json| - .vscode\/settings.json| - .vscode\/launch.json| + \.vscode\/.*\.json$| src\/Postman\/Collection\/testsData\/postmanCollections_invalidJsonFile.json| src\/Postman\/Model\/testsData\/pushMode\/GithubAPI\/notValidJsonFile.json| src\/Postman\/Model\/testsData\/getCollectionInvalid.json| src\/Postman\/Model\/testsData\/pullMode\/GithubAPI\/notWritableFile.json )$ + - repo: https://github.com/rhysd/actionlint + rev: v1.6.27 + hooks: + - id: actionlint + stages: [pre-commit, pre-push, manual] + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: + - --dictionary + - "-" + - --dictionary + - .cspell/codespellrc-dic.txt + - --ignore-words + - .cspell/codespellrc-ignore.txt + - --quiet-level + - "2" + - --interactive + - "0" + - --check-filenames + - --check-hidden + - --write-changes + exclude: > + (?x)( + ^installScripts/| + ^bin/| + ^install$| + ^distro$| + ^srcAlt| + ^.cspell/codespellrc-.*.txt$ + ) + - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: @@ -59,15 +137,17 @@ repos: rev: 3.0.2 hooks: - id: shfmt - args: [-i, '2', -ci] + args: [-i, "2", -ci] + # x modifier: extended. Spaces and text after a # in the pattern are ignored exclude: | (?x)( - /testsData/| + ^vendor/| ^bin/| + \.tpl$| + /testsData/| ^conf/dbScripts/| ^install ) - stages: [pre-commit] # Check both committed and uncommitted files for git conflict # markers and whitespace errors according to core.whitespace @@ -76,18 +156,19 @@ repos: exclude: /testsData/ - repo: https://github.com/fchastanet/bash-tools-framework - rev: 2.3.1 + rev: 4.0.0 hooks: - id: fixShebangExecutionBit - - id: fixShebangExecutionBitGithubActions - id: awkLint + - id: buildShFiles + - id: buildShFilesGithubAction - id: shellcheckLint - id: shellcheckLintGithubAction - - id: frameworkLinter + - id: frameworkLint args: [ --expected-warnings-count, - '6', + "6", --format, plain, --theme, @@ -95,11 +176,11 @@ repos: --display-level, WARNING, ] - - id: frameworkLinterGithubAction + - id: frameworkLintGithubAction args: [ --expected-warnings-count, - '6', + "6", --format, checkstyle, --theme, @@ -108,20 +189,14 @@ repos: WARNING, ] - id: plantuml - - id: buildShFiles - - id: buildShFilesGithubAction - - id: megalinterCheckVersion - - id: megalinterGithubAction - repo: local hooks: - - id: buildDocFilesGithubAction - name: build doc files for Github Actions + - id: buildDocFilesForGithubActions + name: build doc files for github actions language: script entry: bin/doc - args: [-vvv] + args: [--ci, --verbose] pass_filenames: false require_serial: true - always_run: true - fail_fast: false - stages: [manual] + stages: [] # GITHUB diff --git a/.prettierignore b/.prettierignore index d5ff2f24..d3389e63 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ _layouts +**/testsData/** diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 5158ae73..977ee41c 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -11,7 +11,7 @@ proseWrap: always quoteProps: as-needed requirePragma: false semi: true -singleQuote: true +singleQuote: false tabWidth: 2 trailingComma: es5 useTabs: false diff --git a/.proselintrc b/.proselintrc new file mode 100644 index 00000000..6fe6418a --- /dev/null +++ b/.proselintrc @@ -0,0 +1,5 @@ +{ + "checks": { + "typography.diacritical_marks": false + } +} diff --git a/.secretlintignore b/.secretlintignore index 8a98b29c..9535811d 100644 --- a/.secretlintignore +++ b/.secretlintignore @@ -1,2 +1,2 @@ -backup -megalinter-reports +backup/ +megalinter-reports/ diff --git a/.secretlintrc.yml b/.secretlintrc.yml index b2ebd6b5..95c468de 100644 --- a/.secretlintrc.yml +++ b/.secretlintrc.yml @@ -1,9 +1,8 @@ --- rules: - - id: '@secretlint/secretlint-rule-preset-recommend' + - id: "@secretlint/secretlint-rule-preset-recommend" rules: - - id: '@secretlint/secretlint-rule-aws' + - id: "@secretlint/secretlint-rule-aws" options: allows: - - '123456789012' # conf/zsh_profile/.p10k.zsh example - - id: '@secretlint/secretlint-rule-filter-comments' + - "123456789012" # conf/zsh_profile/.p10k.zsh example diff --git a/.shellcheckrc b/.shellcheckrc index 13ea5e26..e1935eb6 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -6,3 +6,5 @@ enable=quote-safe-variables enable=require-double-brackets source-path=SCRIPTDIR source-path=vendor/bash-tools-framework +# special property used by shellcheckLint +exclude=(\.bats$|\.tpl$|/testsData/|\.cache/pre-commit) diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 00000000..40db42c6 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard" +} diff --git a/.v8rrc.yaml b/.v8rrc.yaml index d68b9cfa..d3728c3d 100644 --- a/.v8rrc.yaml +++ b/.v8rrc.yaml @@ -1,3 +1,4 @@ +--- # - Level of verbose logging. 0 is standard, higher numbers are more verbose # - overridden by passing --verbose / -v # - default = 0 @@ -13,9 +14,13 @@ customCatalog: schemas: - name: prettier description: prettier - fileMatch: ['.prettierrc.yaml'] + fileMatch: [".prettierrc.yaml"] location: https://json.schemastore.org/prettierrc.json - name: megalinter description: megalinter - fileMatch: ['.mega-linter*.yml'] + fileMatch: [".mega-linter*.yml"] location: https://raw.githubusercontent.com/megalinter/megalinter/main/megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json + - name: pre-commit + description: pre-commit + fileMatch: [".pre-commit-config*.yml", ".pre-commit-config*.yaml"] + location: https://json.schemastore.org/pre-commit-config.json diff --git a/.vscode/.checkov.yml b/.vscode/.checkov.yml index 97cda231..1b927fac 100644 --- a/.vscode/.checkov.yml +++ b/.vscode/.checkov.yml @@ -1,3 +1,4 @@ +--- # You can see all available properties here: https://github.com/bridgecrewio/checkov#configuration-using-a-config-file quiet: true skip-check: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3858c590..13e86b80 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,18 +1,21 @@ { "recommendations": [ - "ms-azuretools.vscode-docker", - "timonwong.shellcheck", - "lizebang.bash-extension-pack", - "luggage66.awk", "streetsidesoftware.code-spell-checker", - "jetmartin.bats", + "timonwong.shellcheck", + "standard.vscode-standard", + "stylelint.vscode-stylelint", + "foxundermoon.shell-format", "esbenp.prettier-vscode", - "HTMLHint.vscode-htmlhint", + "dbaeumer.vscode-eslint", + "shd101wyy.markdown-preview-enhanced", "yzhang.markdown-all-in-one", + "luggage66.awk", + "lizebang.bash-extension-pack", + "rogalmic.bash-debug", + "jetmartin.bats", + "ms-azuretools.vscode-docker", "exiasr.hadolint", - "shd101wyy.markdown-preview-enhanced", "monosans.djlint", - "foxundermoon.shell-format", - "rogalmic.bash-debug" + "HTMLHint.vscode-htmlhint" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 43444ef5..58d535c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,13 @@ { "search.useIgnoreFiles": true, "search.exclude": { - "/report": true, - "/backup": true, - "/megalinter-reports": true, - "/bin": true, - "/vendor": true, - "/logs": true, + "report": true, + "bin": true, + "logs": true, + "node_modules": true, + "backup": true, + "megalinter-reports": true, + "vendor": true, "pages/assets": true, "pages/**/*.md": true, "pages/_navbar.md": false, @@ -15,16 +16,25 @@ "/install": true, "/conf/dbScripts": true }, + "explorer.autoReveal": false, "files.exclude": { - "**/.git": true - }, - "workbench.editorAssociations": { - "*.md": "default" + "**/.git": true, + "node_modules": true }, - "explorer.autoReveal": false, "markdown.extension.toc.omittedFromToc": { "*.md": ["# %%"] }, + "editor.detectIndentation": false, + "editor.insertSpaces": true, + "editor.indentSize": "tabSize", + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true, + "workbench.editorAssociations": { + "*.md": "default" + }, "markdown.extension.toc.levels": "2..6", - "shellcheck.customArgs": ["--source-path=SCRIPTDIR", "--external-sources"] + "shellcheck.customArgs": ["--source-path=SCRIPTDIR", "--external-sources"], + "shellcheck.ignorePatterns": { + "**/*.tpl": true + } } diff --git a/Commands.tmpl.md b/Commands.tmpl.md index 609c49a0..60f6a14b 100644 --- a/Commands.tmpl.md +++ b/Commands.tmpl.md @@ -80,8 +80,8 @@ mysqldump --skip-add-drop-table --skip-add-locks \ --host=127.0.0.1 --port=3345 --user=root --password=root \ --no-data skills \ $(mysql --host=127.0.0.1 --port=3345 --user=root --password=root skills \ - -Bse "show tables like 'core\_%'") \ - | grep -v '^\/\*![0-9]\{5\}.*\/;$' > doc/schema.sql + -Bse "show tables like 'core\_%'") | + grep -v '^\/\*![0-9]\{5\}.*\/;$' >doc/schema.sql ``` Transform mysql dump to plant uml format @@ -89,7 +89,7 @@ Transform mysql dump to plant uml format ```bash mysql2puml \ src/_binaries/Converters/testsData/mysql2puml.dump.sql \ - -s default > src/_binaries/Converters/testsData/mysql2puml.dump.puml + -s default >src/_binaries/Converters/testsData/mysql2puml.dump.puml ``` Plantuml diagram generated diff --git a/README.md b/README.md index 37cd31a1..aeca78ad 100644 --- a/README.md +++ b/README.md @@ -14,47 +14,21 @@ > - [Bash Dev Env](https://fchastanet.github.io/bash-dev-env/) + + -[![GitHubLicense]( - https://img.shields.io/github/license/Naereen/StrapDown.js.svg -)]( - https://github.com/fchastanet/bash-tools/blob/master/LICENSE -) -[![CI/CD]( - https://github.com/fchastanet/bash-tools/actions/workflows/lint-test.yml/badge.svg -)]( - https://github.com/fchastanet/bash-tools/actions?query=workflow%3A%22Lint+and+test%22+branch%3Amaster -) -[![ProjectStatus]( - http://opensource.box.com/badges/active.svg -)]( - http://opensource.box.com/badges - 'Project Status' -) -[![DeepSource]( - https://deepsource.io/gh/fchastanet/bash-tools.svg/?label=active+issues&show_trend=true -)]( - https://deepsource.io/gh/fchastanet/bash-tools/?ref=repository-badge -) -[![DeepSource]( - https://deepsource.io/gh/fchastanet/bash-tools.svg/?label=resolved+issues&show_trend=true -)]( - https://deepsource.io/gh/fchastanet/bash-tools/?ref=repository-badge -) -[![AverageTimeToResolveAnIssue]( - http://isitmaintained.com/badge/resolution/fchastanet/bash-tools.svg -)]( - http://isitmaintained.com/project/fchastanet/bash-tools - 'Average time to resolve an issue' -) -[![PercentageOfIssuesStillOpen]( - http://isitmaintained.com/badge/open/fchastanet/bash-tools.svg -)]( - http://isitmaintained.com/project/fchastanet/bash-tools - 'Percentage of issues still open' -) + +[![GitHubLicense](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/fchastanet/bash-tools/blob/master/LICENSE) +[![CI/CD](https://github.com/fchastanet/bash-tools/actions/workflows/lint-test.yml/badge.svg)](https://github.com/fchastanet/bash-tools/actions?query=workflow%3A%22Lint+and+test%22+branch%3Amaster) +[![ProjectStatus](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges "Project Status") +[![DeepSource](https://deepsource.io/gh/fchastanet/bash-tools.svg/?label=active+issues&show_trend=true)](https://deepsource.io/gh/fchastanet/bash-tools/?ref=repository-badge) +[![DeepSource](https://deepsource.io/gh/fchastanet/bash-tools.svg/?label=resolved+issues&show_trend=true)](https://deepsource.io/gh/fchastanet/bash-tools/?ref=repository-badge) +[![AverageTimeToResolveAnIssue](http://isitmaintained.com/badge/resolution/fchastanet/bash-tools.svg)](http://isitmaintained.com/project/fchastanet/bash-tools "Average time to resolve an issue") +[![PercentageOfIssuesStillOpen](http://isitmaintained.com/badge/open/fchastanet/bash-tools.svg)](http://isitmaintained.com/project/fchastanet/bash-tools "Percentage of issues still open") + + - [1. Excerpt](#1-excerpt) @@ -145,8 +119,8 @@ touch ~/.parallel/will-cite Dependencies are automatically installed when used. -`vendor/bash-tools-framework/bin/test` script will install the following -libraries inside `vendor` folder: +`bin/installRequirements` script will install the following libraries inside +`vendor` folder: - [bats-core/bats-core](https://github.com/bats-core/bats-core.git) - [bats-core/bats-support](https://github.com/bats-core/bats-support.git) @@ -182,25 +156,18 @@ All the commands are unit tested, you can run the unit tests using the following command ```bash -vendor/bash-tools-framework/bin/test -r src +./test.sh -r src ``` Launch UT on different environments: ```bash -VENDOR="alpine" BASH_TAR_VERSION=4.4 BASH_IMAGE=bash \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 16 -VENDOR="alpine" BASH_TAR_VERSION=5.0 BASH_IMAGE=bash \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 16 -VENDOR="alpine" BASH_TAR_VERSION=5.1 BASH_IMAGE=bash \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 16 - -VENDOR="ubuntu" BASH_TAR_VERSION=4.4 BASH_IMAGE=ubuntu:20.04 \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 2 -VENDOR="ubuntu" BASH_TAR_VERSION=5.0 BASH_IMAGE=ubuntu:20.04 \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 2 -VENDOR="ubuntu" BASH_TAR_VERSION=5.1 BASH_IMAGE=ubuntu:20.04 \ - SKIP_BUILD=1 SKIP_USER=1 vendor/bash-tools-framework/bin/test -r src -j 2 +./test.sh scrasnups/build:bash-tools-ubuntu-4.4 -r src -j 30 +./test.sh scrasnups/build:bash-tools-ubuntu-5.0 -r src -j 30 +./test.sh scrasnups/build:bash-tools-ubuntu-5.3 -r src -j 30 +./test.sh scrasnups/build:bash-tools-alpine-4.4 -r src -j 30 +./test.sh scrasnups/build:bash-tools-alpine-5.0 -r src -j 30 +./test.sh scrasnups/build:bash-tools-alpine-5.3 -r src -j 30 ``` ### 3.4. auto generated bash doc diff --git a/bin/cli b/bin/cli index b0abddf5..4c72b512 100755 --- a/bin/cli +++ b/bin/cli @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,39 +502,71 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @description check if command specified exists or return 1 -# with error and message if not +# @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 # -# @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 +# @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) # -# @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" +# @exitcode 1 if file not found or error during file loading +Conf::load() { + local confFolder="$1" + local conf="$2" + local extension="${3:-sh}" - "${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}" + if [[ -n "${extension}" && "${extension:0:1}" != "." ]]; then + extension=".${extension}" + fi + # if conf is absolute + local confFile + if [[ "${conf}" == /* ]]; then + # file contains /, consider it as absolute filename + confFile="${conf}" + else + # shellcheck source=/conf/dsn/default.local.env + confFile="${HOME}/.bash-tools/${confFolder}/${conf}${extension}" + if [[ ! -f "${confFile}" ]]; then + confFile="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}/${conf}${extension}" fi + fi + if [[ ! -f "${confFile}" ]]; then return 1 - } - return 0 + fi + # shellcheck disable=SC1090 + source "${confFile}" } -# @description determine if the script is executed under windows (using wsl) -# cspell:disable -# @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 +# @description list the conf files list available in bash-tools/conf/ folder +# and those overridden in ${HOME}/.bash-tools/ folder # -# @exitcode 1 on error -Assert::windows() { - [[ "$(uname -o)" = "Msys" ]] +# @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 + if [[ -d "${HOME_CONF_DIR}" ]]; then + Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" + fi + ) | sort | uniq } # @description get absolute conf file from specified conf folder deduced using these rules @@ -322,73 +630,6 @@ Conf::getAbsoluteFile() { return 1 } -# @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 - if [[ -d "${HOME_CONF_DIR}" ]]; then - Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" - fi - ) | sort | uniq -} - -# @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 -# -# @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) -# -# @exitcode 1 if file not found or error during file loading -Conf::load() { - local confFolder="$1" - local conf="$2" - local extension="${3:-sh}" - - if [[ -n "${extension}" && "${extension:0:1}" != "." ]]; then - extension=".${extension}" - fi - # if conf is absolute - local confFile - if [[ "${conf}" == /* ]]; then - # file contains /, consider it as absolute filename - confFile="${conf}" - else - # shellcheck source=/conf/dsn/default.local.env - confFile="${HOME}/.bash-tools/${confFolder}/${conf}${extension}" - if [[ ! -f "${confFile}" ]]; then - confFile="${FRAMEWORK_ROOT_DIR}/conf/${confFolder}/${conf}${extension}" - fi - fi - if [[ ! -f "${confFile}" ]]; then - return 1 - fi - # shellcheck disable=SC1090 - source "${confFile}" -} - # @description check if dsn file has all the mandatory variables set # Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # @@ -430,260 +671,49 @@ Database::checkDsnFile() { 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 - ) -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi + 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 + ) +} + +# @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 +} + +# @description determine if the script is executed under windows (using wsl) +# cspell:disable +# @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 +# +# @exitcode 1 on error +Assert::windows() { + [[ "$(uname -o)" = "Msys" ]] } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -692,17 +722,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -716,6 +735,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -724,6 +751,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -786,79 +825,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" +# @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 - ( - # 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 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 } # @description log message to file @@ -869,6 +879,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -877,18 +895,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -918,14 +943,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -934,10 +951,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -947,21 +965,74 @@ Log::rotate() { fi } +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} + +# @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 +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -971,14 +1042,6 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } -# @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 -} - # FUNCTIONS facade_main_clish() { @@ -1001,8 +1064,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir @@ -1210,7 +1273,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1237,7 +1300,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1580,7 +1643,7 @@ cliCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "easy connection to docker container")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "easy connection to docker container" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/dbImport b/bin/dbImport index a810f18f..7803aa0d 100755 --- a/bin/dbImport +++ b/bin/dbImport @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,270 +91,287 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 -} - -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true - - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi - - ((argLength = ${#arg})) || true +# @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 - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi +# @description log level off +export __LEVEL_OFF=0 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" +# @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 - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" } -# @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 +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" } -# @description ignore exit code 141 from simple command pipes -# @example use with: -# local resultingStatus=0 -# local -a originalPipeStatus=() -# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true -# [[ "${resultingStatus}" = "0" ]] -# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code -# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array -# @env PIPESTATUS assuming that this function is called like in the example provided -# @see https://unix.stackexchange.com/a/709880/582856 -Bash::handlePipelineFailure() { - local -a pipeStatusBackup=("${PIPESTATUS[@]}") - local -n handlePipelineFailure_resultingStatusCode=$1 - local -n handlePipelineFailure_originalStatus=$2 - # shellcheck disable=SC2034 - handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") - handlePipelineFailure_resultingStatusCode=0 - local statusCode - for statusCode in "${pipeStatusBackup[@]}"; do - if ((statusCode == 141)); then - return 0 - elif ((statusCode > 0)); then - # shellcheck disable=SC2034 - handlePipelineFailure_resultingStatusCode="${statusCode}" - break - fi - done - return "${handlePipelineFailure_resultingStatusCode}" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" } -# @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}" +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi + Log::logError "$1" +} - 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 - } +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} - # 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 +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} - # relative to where script is executed (including extension) - if [[ -n "${CURRENT_DIR+xxx}" ]]; then - testAbs "$(File::concatenatePath "${CURRENT_DIR}" "${confFolder}")/${conf}${extension}" && return 0 +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 - # from home/.bash-tools/ - testAbs "$(File::concatenatePath "${HOME}/.bash-tools" "${confFolder}")/${conf}${extension}" && return 0 + local localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") - if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # from framework conf/ (including extension) - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} - # from framework conf/ - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 +# @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 - # file not found - Log::displayError "conf file '${conf}' not found" + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + fi - return 1 + 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 } # @description list the conf files list available in bash-tools/conf/ folder @@ -385,125 +404,81 @@ Conf::getMergedList() { ) | sort | uniq } -# @description dump db limited to optional table list +# @description check if an element is contained in an array # -# @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 - shift || true - local db="$1" - shift || true - # optional table list - local optionalTableList="" - if [[ -n "${1+x}" ]]; then - optionalTableList="$1" - shift || true - fi - local -a dumpAdditionalOptions=() - local -a mysqlCommand=() - - # additional options - if [[ -n "${1+x}" ]]; then - dumpAdditionalOptions=("$@") - fi - - mysqlCommand+=(mysqldump) - mysqlCommand+=("--defaults-extra-file=${instanceDump['AUTH_FILE']}") - IFS=' ' read -r -a dumpOptions <<<"${instanceDump['DUMP_OPTIONS']}" - mysqlCommand+=("${dumpOptions[@]}") - mysqlCommand+=("${dumpAdditionalOptions[@]}") - mysqlCommand+=("${db}") - # shellcheck disable=SC2206 - mysqlCommand+=(${optionalTableList}) - - Log::displayDebug "execute command: '${mysqlCommand[*]}'" - "${mysqlCommand[@]}" +# @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 } -# @description check if given database exists +# @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 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="$2" - local result - local -a mysqlCommand=() - - mysqlCommand+=(mysqlshow) - mysqlCommand+=("--defaults-extra-file=${instanceIfDbExists['AUTH_FILE']}") - # shellcheck disable=SC2206 - mysqlCommand+=(${instanceIfDbExists['SSL_OPTIONS']}) - mysqlCommand+=("${dbName}") - Log::displayDebug "execute command: '${mysqlCommand[*]}'" - result="$(MSYS_NO_PATHCONV=1 "${mysqlCommand[@]}" 2>/dev/null | grep '^Database: ' | grep -o "${dbName}")" - [[ "${result}" = "${dbName}" ]] -} - -# @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" +# @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) # -# @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 +# @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 - # final auth file generated from dns file - instanceNewInstance['AUTH_FILE']="" - instanceNewInstance['DSN_FILE']="" + 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 + } - # check dsn file - DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 - Database::checkDsnFile "${DSN_FILE}" || return 1 - instanceNewInstance['DSN_FILE']="${DSN_FILE}" + # 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 - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${instanceNewInstance['DSN_FILE']}" + # 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 - instanceNewInstance['USER']="${USER}" - instanceNewInstance['PASSWORD']="${PASSWORD}" - instanceNewInstance['HOSTNAME']="${HOSTNAME}" - instanceNewInstance['PORT']="${PORT}" + if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then + # from framework conf/ (including extension) + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 - # generate authFile for easy authentication - instanceNewInstance['AUTH_FILE']=$(mktemp -p "${TMPDIR:-/tmp}" -t "mysql.XXXXXXXXXXXX") - ( - echo "[client]" - echo "user = ${USER}" - echo "password = ${PASSWORD}" - echo "host = ${HOSTNAME}" - echo "port = ${PORT}" - ) >"${instanceNewInstance['AUTH_FILE']}" + # from framework conf/ + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 + fi - # 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}" + # file not found + Log::displayError "conf file '${conf}' not found" - instanceNewInstance['INITIALIZED']=1 + return 1 } # @description mysql query on a given db @@ -545,288 +520,154 @@ Database::query() { fi } -# @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 +# @description concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # -# @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" -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true - if [[ ! -e "${path}" ]]; then - # path already removed - return 0 - fi + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + 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 -} + ((argLength = ${#arg})) || true -# @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" -} + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi -# @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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" 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 - - fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" - 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}" + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine fi - fi -} - -# @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}" + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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 +# @description check if command specified exists or return 1 +# with error and message if not # -# @set __RESET_COLOR String reset default color +# @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 # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi +# @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 } # @description Check that command version is greater than expected minimal version @@ -847,28 +688,217 @@ Version::checkMinimal() { local parseVersionCallback=${4:-Version::parse} local help="${5:-}" - Assert::commandExists "${commandName}" "${help}" || return 2 + Assert::commandExists "${commandName}" "${help}" || return 2 + + # shellcheck disable=SC2034 + local status=0 + # shellcheck disable=SC2034 + local -a pipeStatus=() + local version + version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" + + Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + + Version::compare "${version}" "${minimalVersion}" || { + local result=$? + if [[ "${result}" = "1" ]]; then + Log::displayInfo "${commandName} version is ${version} greater than ${minimalVersion}" + elif [[ "${result}" = "2" ]]; then + Log::displayError "${commandName} minimal version is ${minimalVersion}, your version is ${version}" + return 1 + fi + return 0 + } + +} + +# @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 + + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" + + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" + + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${instanceNewInstance['DSN_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") + ( + 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 +} + +# @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" +} + +# @description check if given database exists +# +# @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="$2" + local result + local -a mysqlCommand=() + + mysqlCommand+=(mysqlshow) + mysqlCommand+=("--defaults-extra-file=${instanceIfDbExists['AUTH_FILE']}") + # shellcheck disable=SC2206 + mysqlCommand+=(${instanceIfDbExists['SSL_OPTIONS']}) + mysqlCommand+=("${dbName}") + Log::displayDebug "execute command: '${mysqlCommand[*]}'" + result="$(MSYS_NO_PATHCONV=1 "${mysqlCommand[@]}" 2>/dev/null | grep '^Database: ' | grep -o "${dbName}")" + [[ "${result}" = "${dbName}" ]] +} + +# @description dump db limited to optional table list +# +# @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 + shift || true + local db="$1" + shift || true + # optional table list + local optionalTableList="" + if [[ -n "${1+x}" ]]; then + optionalTableList="$1" + shift || true + fi + local -a dumpAdditionalOptions=() + local -a mysqlCommand=() + + # additional options + if [[ -n "${1+x}" ]]; then + dumpAdditionalOptions=("$@") + fi - # shellcheck disable=SC2034 - local status=0 - # shellcheck disable=SC2034 - local -a pipeStatus=() - local version - version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" + mysqlCommand+=(mysqldump) + mysqlCommand+=("--defaults-extra-file=${instanceDump['AUTH_FILE']}") + IFS=' ' read -r -a dumpOptions <<<"${instanceDump['DUMP_OPTIONS']}" + mysqlCommand+=("${dumpOptions[@]}") + mysqlCommand+=("${dumpAdditionalOptions[@]}") + mysqlCommand+=("${db}") + # shellcheck disable=SC2206 + mysqlCommand+=(${optionalTableList}) - Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + Log::displayDebug "execute command: '${mysqlCommand[*]}'" + "${mysqlCommand[@]}" +} - Version::compare "${version}" "${minimalVersion}" || { - local result=$? - if [[ "${result}" = "1" ]]; then - Log::displayDebug "${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 +# @description ignore exit code 141 from simple command pipes +# @example use with: +# local resultingStatus=0 +# local -a originalPipeStatus=() +# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true +# [[ "${resultingStatus}" = "0" ]] +# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code +# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array +# @env PIPESTATUS assuming that this function is called like in the example provided +# @see https://unix.stackexchange.com/a/709880/582856 +Bash::handlePipelineFailure() { + local -a pipeStatusBackup=("${PIPESTATUS[@]}") + local -n handlePipelineFailure_resultingStatusCode=$1 + local -n handlePipelineFailure_originalStatus=$2 + # shellcheck disable=SC2034 + handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") + handlePipelineFailure_resultingStatusCode=0 + local statusCode + for statusCode in "${pipeStatusBackup[@]}"; do + if ((statusCode == 141)); then + return 0 + elif ((statusCode > 0)); then + # shellcheck disable=SC2034 + handlePipelineFailure_resultingStatusCode="${statusCode}" + break fi + done + return "${handlePipelineFailure_resultingStatusCode}" +} + +# @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 +File::garbageCollect() { + local path="$1" + local mtime="$2" + local maxdepth="${3:-1}" + + 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 } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -877,17 +907,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -901,6 +920,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -909,6 +936,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -939,171 +978,91 @@ BashTools::Conf::requireLoad() { local envFile="${HOME}/.bash-tools/.env" if [[ ! -f "${envFile}" ]]; then mkdir -p "${HOME}/.bash-tools" - ( - echo "#!/usr/bin/env bash" - echo "${bashToolsDefaultConfigTemplate}" - ) >"${envFile}" - Log::displayInfo "Configuration file '${envFile}' created" - else - if ! grep -q '^POSTMAN_API_KEY=' "${envFile}"; then - ( - echo '# -----------------------------------------------------' - echo '# Postman Parameters' - echo '# -----------------------------------------------------' - echo 'POSTMAN_API_KEY=' - ) >>"${envFile}" - fi - fi - # shellcheck source=/conf/.env - source "${envFile}" || { - Log::displayError "impossible to load '${envFile}'" - exit 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}" -} - -# @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 -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} - -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" - fi - ( - # shellcheck disable=SC2086 - cd "${dir}" && - find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | - sed -E "s#^\./${prefix}##g" | - sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" - ) -} - -# @description check if dsn file has all the mandatory variables set -# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT -# -# @arg $1 dsnFileName:String dsn absolute filename -# @set HOSTNAME loaded from dsn file -# @set PORT loaded from dsn file -# @set USER loaded from dsn file -# @set PASSWORD loaded from dsn file -# @exitcode 0 on valid file -# @exitcode 1 if one of the properties of the conf file is invalid or if file not found -# @stderr log output if error found in conf file -Database::checkDsnFile() { - local dsnFileName="$1" - if [[ ! -f "${dsnFileName}" ]]; then - Log::displayError "dsn file ${dsnFileName} not found" - return 1 - fi - - ( - unset HOSTNAME PORT PASSWORD USER - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${dsnFileName}" - if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" - return 1 - fi - if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" - fi - if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" - fi - if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT not provided" - return 1 - fi - if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT invalid" - return 1 - fi - if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : USER not provided" - return 1 - fi - if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" - return 1 + ( + echo "#!/usr/bin/env bash" + echo "${bashToolsDefaultConfigTemplate}" + ) >"${envFile}" + Log::displayInfo "Configuration file '${envFile}' created" + else + if ! grep -q '^POSTMAN_API_KEY=' "${envFile}"; then + ( + echo '# -----------------------------------------------------' + echo '# Postman Parameters' + echo '# -----------------------------------------------------' + echo 'POSTMAN_API_KEY=' + ) >>"${envFile}" fi - ) + fi + # shellcheck source=/conf/.env + source "${envFile}" || { + Log::displayError "impossible to load '${envFile}'" + exit 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 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 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}" +# @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 +} - realpath -m "${fullPath}" 2>/dev/null +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi +} + +# @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 } # @description log message to file @@ -1114,6 +1073,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -1122,18 +1089,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -1163,14 +1137,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -1179,10 +1145,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -1192,6 +1159,68 @@ Log::rotate() { fi } +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} + +# @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 +} + +# @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() { + # match anything, print(p), exit on first match(Q) + sed -En \ + -e 's/\x1b\[[0-9;]*[mGKHF]//g' \ + -e 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/' \ + -e '//{p;Q}' \ + "$@" +} + # @description compare 2 version numbers # @arg $1 version1:String version 1 # @arg $2 version2:String version 2 @@ -1224,31 +1253,78 @@ Version::compare() { return 0 } -# @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 check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file +Database::checkDsnFile() { + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" + return 1 + fi + + ( + unset HOSTNAME PORT PASSWORD USER + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${dsnFileName}" + if [[ -z ${HOSTNAME+x} ]]; then + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" + return 1 + fi + if [[ -z "${HOSTNAME}" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" + fi + if [[ "${HOSTNAME}" = "localhost" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + fi + if [[ -z "${PORT+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT not provided" + return 1 + fi + if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT invalid" + return 1 + fi + if [[ -z "${USER+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : USER not provided" + return 1 + fi + if [[ -z "${PASSWORD+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" + return 1 + fi + ) +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -1258,14 +1334,6 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } -# @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 -} - # FUNCTIONS facade_main_dbImportsh() { @@ -1288,8 +1356,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir Linux::requireExecutedAsUser @@ -1504,7 +1572,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1531,7 +1599,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -2095,7 +2163,7 @@ dbImportCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Import source db into target db using eventual table filter")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Import source db into target db using eventual table filter" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" @@ -2278,6 +2346,28 @@ ${__HELP_EXAMPLE}TODO${__HELP_NORMAL}""" fi } +checkRequirements() { + if [[ "${SKIP_REQUIREMENTS_CHECKS:-0}" = "1" ]]; then + return 0 + fi + local -i failures=0 + echo + Assert::commandExists mysql "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists pv "sudo apt-get install -y pv" || ((++failures)) + Assert::commandExists gawk "sudo apt-get install -y gawk" || ((++failures)) + Assert::commandExists awk "sudo apt-get install -y gawk" || ((++failures)) + Version::checkMinimal "gawk" "--version" "5.0.1" || ((++failures)) + return "${failures}" +} + +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + checkRequirements + exit 0 +} + optionHelpCallback() { local profilesList="" local dsnList="" @@ -2285,6 +2375,7 @@ optionHelpCallback() { profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" dbImportCommand help | envsubst + checkRequirements exit 0 } @@ -2347,16 +2438,6 @@ EOF # @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 diff --git a/bin/dbImportProfile b/bin/dbImportProfile index a9564c9d..8be14864 100755 --- a/bin/dbImportProfile +++ b/bin/dbImportProfile @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,34 +502,12 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @description check if command specified exists or return 1 -# with error and message if not +# @description list the conf files list available in bash-tools/conf/ folder +# and those overridden in ${HOME}/.bash-tools/ folder # -# @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 -} - -# @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 / +# @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 @@ -278,26 +532,26 @@ Conf::getMergedList() { ) | sort | uniq } -# @description check if given database exists +# @description check if command specified exists or return 1 +# with error and message if not # -# @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="$2" - local result - local -a mysqlCommand=() +# @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" - mysqlCommand+=(mysqlshow) - mysqlCommand+=("--defaults-extra-file=${instanceIfDbExists['AUTH_FILE']}") - # shellcheck disable=SC2206 - mysqlCommand+=(${instanceIfDbExists['SSL_OPTIONS']}) - mysqlCommand+=("${dbName}") - Log::displayDebug "execute command: '${mysqlCommand[*]}'" - result="$(MSYS_NO_PATHCONV=1 "${mysqlCommand[@]}" 2>/dev/null | grep '^Database: ' | grep -o "${dbName}")" - [[ "${result}" = "${dbName}" ]] + "${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 } # @description create a new db instance @@ -358,6 +612,39 @@ Database::newInstance() { instanceNewInstance['INITIALIZED']=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" +} + +# @description check if given database exists +# +# @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="$2" + local result + local -a mysqlCommand=() + + mysqlCommand+=(mysqlshow) + mysqlCommand+=("--defaults-extra-file=${instanceIfDbExists['AUTH_FILE']}") + # shellcheck disable=SC2206 + mysqlCommand+=(${instanceIfDbExists['SSL_OPTIONS']}) + mysqlCommand+=("${dbName}") + Log::displayDebug "execute command: '${mysqlCommand[*]}'" + result="$(MSYS_NO_PATHCONV=1 "${mysqlCommand[@]}" 2>/dev/null | grep '^Database: ' | grep -o "${dbName}")" + [[ "${result}" = "${dbName}" ]] +} + # @description mysql query on a given db # @warning could use QUERY_OPTIONS variable from dsn if defined # @example @@ -397,280 +684,12 @@ Database::query() { fi } -# @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" -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi -} - bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( cat <<'EOF' # shellcheck disable=SC2034 # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -684,6 +703,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -692,6 +719,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -737,29 +776,99 @@ BashTools::Conf::requireLoad() { ) >>"${envFile}" fi fi - # shellcheck source=/conf/.env - source "${envFile}" || { - Log::displayError "impossible to load '${envFile}'" - exit 1 - } + # shellcheck source=/conf/.env + source "${envFile}" || { + Log::displayError "impossible to load '${envFile}'" + exit 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}" +} + +# @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 +} + +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi +} + +# @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 +} + +# @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 } -# @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 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 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" +# @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 } @@ -768,7 +877,6 @@ Linux::requireExecutedAsUser() { # @exitcode 1 if tty not active # @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive # @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided Assert::tty() { if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then return 1 @@ -776,7 +884,97 @@ Assert::tty() { if [[ "${INTERACTIVE:-0}" = "1" ]]; then return 0 fi - [[ -t 1 || -t 2 ]] + tty -s +} + +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$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 +} + +# @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}" + + if [[ ! -f "${file}" ]]; then + Log::displayDebug "Log file ${file} doesn't exist yet" + return 0 + fi + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do + Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" + mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true + done + if cp "${file}" "${file}.1" &>/dev/null; then + echo >"${file}" # reset log file + Log::displayInfo "Log rotation ${file} to ${file}.1" + fi +} + +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) } # @description get absolute conf file from specified conf folder deduced using these rules @@ -840,41 +1038,6 @@ Conf::getAbsoluteFile() { return 1 } -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" - fi - ( - # shellcheck disable=SC2086 - cd "${dir}" && - find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | - sed -E "s#^\./${prefix}##g" | - sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" - ) -} - # @description check if dsn file has all the mandatory variables set # Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # @@ -938,98 +1101,15 @@ Env::pathPrepend() { done } -# @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 -} - -# @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 -} - -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$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 -} - -# @description Internal: common log message -# @example text -# [date]|[levelMsg]|message -# -# @example text -# 2020-01-19 19:20:21|ERROR |log error -# 2020-01-19 19:20:21|SKIPPED|log skipped -# -# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) -# @arg $2 msg:String the message to display -# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty -# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages -# @stderr diagnostics information is displayed -# @require Env::requireLoad -# @require Log::requireLoad -Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date - - if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - fi -} - -# @description 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 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}" - - if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" - return 0 - fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do - Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" - mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true - done - if cp "${file}" "${file}.1" &>/dev/null; then - echo >"${file}" # reset log file - Log::displayInfo "Log rotation ${file} to ${file}.1" - fi -} - # @description load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" + fi } # @description concatenate 2 paths and ensure the path is correct using realpath -m @@ -1044,23 +1124,6 @@ File::concatenatePath() { realpath -m "${fullPath}" 2>/dev/null } -# @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 log message to file -# @arg $1 message:String the message to display -Log::logSkipped() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then - Log::logMessage "${2:-SKIPPED}" "$1" - fi -} - # @description ensure command realpath is available # @exitcode 1 if realpath command not available # @stderr diagnostics information is displayed @@ -1297,7 +1360,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1324,7 +1387,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1708,7 +1771,7 @@ dbImportProfileCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate optimized profiles to be used by dbImport")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate optimized profiles to be used by dbImport" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/dbImportStream b/bin/dbImportStream index 1e34e5db..c86019d9 100755 --- a/bin/dbImportStream +++ b/bin/dbImportStream @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,270 +91,287 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 -} - -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true - - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi - - ((argLength = ${#arg})) || true +# @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 - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi +# @description log level off +export __LEVEL_OFF=0 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" +# @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 - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" } -# @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 +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" } -# @description ignore exit code 141 from simple command pipes -# @example use with: -# local resultingStatus=0 -# local -a originalPipeStatus=() -# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true -# [[ "${resultingStatus}" = "0" ]] -# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code -# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array -# @env PIPESTATUS assuming that this function is called like in the example provided -# @see https://unix.stackexchange.com/a/709880/582856 -Bash::handlePipelineFailure() { - local -a pipeStatusBackup=("${PIPESTATUS[@]}") - local -n handlePipelineFailure_resultingStatusCode=$1 - local -n handlePipelineFailure_originalStatus=$2 - # shellcheck disable=SC2034 - handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") - handlePipelineFailure_resultingStatusCode=0 - local statusCode - for statusCode in "${pipeStatusBackup[@]}"; do - if ((statusCode == 141)); then - return 0 - elif ((statusCode > 0)); then - # shellcheck disable=SC2034 - handlePipelineFailure_resultingStatusCode="${statusCode}" - break - fi - done - return "${handlePipelineFailure_resultingStatusCode}" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" } -# @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}" +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi + Log::logError "$1" +} - 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 - } +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} - # 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 +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} - # relative to where script is executed (including extension) - if [[ -n "${CURRENT_DIR+xxx}" ]]; then - testAbs "$(File::concatenatePath "${CURRENT_DIR}" "${confFolder}")/${conf}${extension}" && return 0 +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 - # from home/.bash-tools/ - testAbs "$(File::concatenatePath "${HOME}/.bash-tools" "${confFolder}")/${conf}${extension}" && return 0 + local localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") - if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # from framework conf/ (including extension) - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} - # from framework conf/ - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 +# @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 - # file not found - Log::displayError "conf file '${conf}' not found" + if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then + if [[ ! -f "${BASH_FRAMEWORK_LOG_FILE}" ]]; then + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + fi - return 1 + 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 } # @description list the conf files list available in bash-tools/conf/ folder @@ -385,62 +404,81 @@ Conf::getMergedList() { ) | sort | uniq } -# @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 +# @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 -# 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 +# Array::contains "${libPath}" "${__BASH_FRAMEWORK_IMPORTED_FILES[@]}" +Array::contains() { + local element + for element in "${@:2}"; do + [[ "${element}" = "$1" ]] && return 0 + done + return 1 +} - if [[ -v instanceNewInstance['INITIALIZED'] && "${instanceNewInstance['INITIALIZED']:-0}" == "1" ]]; then - return +# @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 - # final auth file generated from dns file - instanceNewInstance['AUTH_FILE']="" - instanceNewInstance['DSN_FILE']="" + 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 + } - # check dsn file - DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 - Database::checkDsnFile "${DSN_FILE}" || return 1 - instanceNewInstance['DSN_FILE']="${DSN_FILE}" + # 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 - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${instanceNewInstance['DSN_FILE']}" + # 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 - instanceNewInstance['USER']="${USER}" - instanceNewInstance['PASSWORD']="${PASSWORD}" - instanceNewInstance['HOSTNAME']="${HOSTNAME}" - instanceNewInstance['PORT']="${PORT}" + if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then + # from framework conf/ (including extension) + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 - # generate authFile for easy authentication - instanceNewInstance['AUTH_FILE']=$(mktemp -p "${TMPDIR:-/tmp}" -t "mysql.XXXXXXXXXXXX") - ( - echo "[client]" - echo "user = ${USER}" - echo "password = ${PASSWORD}" - echo "host = ${HOSTNAME}" - echo "port = ${PORT}" - ) >"${instanceNewInstance['AUTH_FILE']}" + # from framework conf/ + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 + fi - # 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}" + # file not found + Log::displayError "conf file '${conf}' not found" - instanceNewInstance['INITIALIZED']=1 + return 1 } # @description mysql query on a given db @@ -482,303 +520,295 @@ Database::query() { fi } -# @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 +# @description concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # -# @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" -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true -# @description log level off -export __LEVEL_OFF=0 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + fi -# @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 + ((argLength = ${#arg})) || true -# @description Display message using debug color (grey) -# @arg $1 message:String the message to display -Log::displayDebug() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi -# @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" -} + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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" -} +# @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" -# @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 + "${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 } -# @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 +# @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:-}" - 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 + Assert::commandExists "${commandName}" "${help}" || return 2 - fi + # shellcheck disable=SC2034 + local status=0 + # shellcheck disable=SC2034 + local -a pipeStatus=() + local version + version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" - 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}" + Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + + Version::compare "${version}" "${minimalVersion}" || { + local result=$? + if [[ "${result}" = "1" ]]; then + Log::displayInfo "${commandName} version is ${version} greater than ${minimalVersion}" + elif [[ "${result}" = "2" ]]; then + Log::displayError "${commandName} minimal version is ${minimalVersion}, your version is ${version}" + return 1 fi - fi -} + return 0 + } -# @description draw a line with the character passed in parameter repeated depending on terminal width -# @arg $1 character:String character to use as separator (default value #) -UI::drawLine() { - local character="${1:-#}" - printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" } -# @description load colors theme constants -# @warning if tty not opened, noColor theme will be chosen -# @arg $1 theme:String the theme to use (default, noColor) -# @arg $@ args:String[] -# @set __ERROR_COLOR String indicate error status -# @set __INFO_COLOR String indicate info status -# @set __SUCCESS_COLOR String indicate success status -# @set __WARNING_COLOR String indicate warning status -# @set __SKIPPED_COLOR String indicate skipped status -# @set __DEBUG_COLOR String indicate debug status -# @set __HELP_COLOR String indicate help status -# @set __TEST_COLOR String not used -# @set __TEST_ERROR_COLOR String not used -# @set __HELP_TITLE_COLOR String used to display help title in help strings -# @set __HELP_OPTION_COLOR String used to display highlight options in help strings +# @description create a new db instance +# Returns immediately if the instance is already initialized # -# @set __RESET_COLOR String reset default color +# @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 # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' +# @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 -} -# @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:-}" + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" + + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" + + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${instanceNewInstance['DSN_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") + ( + echo "[client]" + echo "user = ${USER}" + echo "password = ${PASSWORD}" + echo "host = ${HOSTNAME}" + echo "port = ${PORT}" + ) >"${instanceNewInstance['AUTH_FILE']}" - Assert::commandExists "${commandName}" "${help}" || return 2 + # 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}" - # shellcheck disable=SC2034 - local status=0 - # shellcheck disable=SC2034 - local -a pipeStatus=() - local version - version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" + instanceNewInstance['INITIALIZED']=1 +} - Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" +# @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" +} - Version::compare "${version}" "${minimalVersion}" || { - local result=$? - if [[ "${result}" = "1" ]]; then - Log::displayDebug "${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 +# @description ignore exit code 141 from simple command pipes +# @example use with: +# local resultingStatus=0 +# local -a originalPipeStatus=() +# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true +# [[ "${resultingStatus}" = "0" ]] +# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code +# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array +# @env PIPESTATUS assuming that this function is called like in the example provided +# @see https://unix.stackexchange.com/a/709880/582856 +Bash::handlePipelineFailure() { + local -a pipeStatusBackup=("${PIPESTATUS[@]}") + local -n handlePipelineFailure_resultingStatusCode=$1 + local -n handlePipelineFailure_originalStatus=$2 + # shellcheck disable=SC2034 + handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") + handlePipelineFailure_resultingStatusCode=0 + local statusCode + for statusCode in "${pipeStatusBackup[@]}"; do + if ((statusCode == 141)); then + return 0 + elif ((statusCode > 0)); then + # shellcheck disable=SC2034 + handlePipelineFailure_resultingStatusCode="${statusCode}" + break fi - return 0 - } - + done + return "${handlePipelineFailure_resultingStatusCode}" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -787,17 +817,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -811,6 +830,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -819,6 +846,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -848,172 +887,92 @@ EOF BashTools::Conf::requireLoad() { local envFile="${HOME}/.bash-tools/.env" if [[ ! -f "${envFile}" ]]; then - mkdir -p "${HOME}/.bash-tools" - ( - echo "#!/usr/bin/env bash" - echo "${bashToolsDefaultConfigTemplate}" - ) >"${envFile}" - Log::displayInfo "Configuration file '${envFile}' created" - else - if ! grep -q '^POSTMAN_API_KEY=' "${envFile}"; then - ( - echo '# -----------------------------------------------------' - echo '# Postman Parameters' - echo '# -----------------------------------------------------' - echo 'POSTMAN_API_KEY=' - ) >>"${envFile}" - fi - fi - # shellcheck source=/conf/.env - source "${envFile}" || { - Log::displayError "impossible to load '${envFile}'" - exit 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}" -} - -# @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 -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} - -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" - fi - ( - # shellcheck disable=SC2086 - cd "${dir}" && - find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | - sed -E "s#^\./${prefix}##g" | - sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" - ) -} - -# @description check if dsn file has all the mandatory variables set -# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT -# -# @arg $1 dsnFileName:String dsn absolute filename -# @set HOSTNAME loaded from dsn file -# @set PORT loaded from dsn file -# @set USER loaded from dsn file -# @set PASSWORD loaded from dsn file -# @exitcode 0 on valid file -# @exitcode 1 if one of the properties of the conf file is invalid or if file not found -# @stderr log output if error found in conf file -Database::checkDsnFile() { - local dsnFileName="$1" - if [[ ! -f "${dsnFileName}" ]]; then - Log::displayError "dsn file ${dsnFileName} not found" - return 1 - fi - - ( - unset HOSTNAME PORT PASSWORD USER - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${dsnFileName}" - if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" - return 1 - fi - if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" - fi - if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" - fi - if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT not provided" - return 1 - fi - if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT invalid" - return 1 - fi - if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : USER not provided" - return 1 - fi - if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" - return 1 + mkdir -p "${HOME}/.bash-tools" + ( + echo "#!/usr/bin/env bash" + echo "${bashToolsDefaultConfigTemplate}" + ) >"${envFile}" + Log::displayInfo "Configuration file '${envFile}' created" + else + if ! grep -q '^POSTMAN_API_KEY=' "${envFile}"; then + ( + echo '# -----------------------------------------------------' + echo '# Postman Parameters' + echo '# -----------------------------------------------------' + echo 'POSTMAN_API_KEY=' + ) >>"${envFile}" fi - ) + fi + # shellcheck source=/conf/.env + source "${envFile}" || { + Log::displayError "impossible to load '${envFile}'" + exit 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 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 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}" +# @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 +} - realpath -m "${fullPath}" 2>/dev/null +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi +} + +# @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 } # @description log message to file @@ -1024,6 +983,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -1032,18 +999,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -1073,14 +1047,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -1089,10 +1055,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -1102,6 +1069,68 @@ Log::rotate() { fi } +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} + +# @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 +} + +# @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() { + # match anything, print(p), exit on first match(Q) + sed -En \ + -e 's/\x1b\[[0-9;]*[mGKHF]//g' \ + -e 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/' \ + -e '//{p;Q}' \ + "$@" +} + # @description compare 2 version numbers # @arg $1 version1:String version 1 # @arg $2 version2:String version 2 @@ -1134,31 +1163,78 @@ Version::compare() { return 0 } -# @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 check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file +Database::checkDsnFile() { + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" + return 1 + fi + + ( + unset HOSTNAME PORT PASSWORD USER + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${dsnFileName}" + if [[ -z ${HOSTNAME+x} ]]; then + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" + return 1 + fi + if [[ -z "${HOSTNAME}" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" + fi + if [[ "${HOSTNAME}" = "localhost" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + fi + if [[ -z "${PORT+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT not provided" + return 1 + fi + if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT invalid" + return 1 + fi + if [[ -z "${USER+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : USER not provided" + return 1 + fi + if [[ -z "${PASSWORD+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" + return 1 + fi + ) +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -1168,14 +1244,6 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } -# @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 -} - # FUNCTIONS facade_main_dbImportStreamsh() { @@ -1198,8 +1266,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir Linux::requireExecutedAsUser @@ -1408,7 +1476,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1435,7 +1503,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1929,7 +1997,7 @@ dbImportStreamCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "stream tar.gz file or gz file through mysql")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "stream tar.gz file or gz file through mysql" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" @@ -2081,6 +2149,28 @@ ${dsnList}""" fi } +checkRequirements() { + if [[ "${SKIP_REQUIREMENTS_CHECKS:-0}" = "1" ]]; then + return 0 + fi + local -i failures=0 + echo + Assert::commandExists mysql "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists pv "sudo apt-get install -y pv" || ((++failures)) + Assert::commandExists gawk "sudo apt-get install -y gawk" || ((++failures)) + Assert::commandExists awk "sudo apt-get install -y gawk" || ((++failures)) + Version::checkMinimal "gawk" "--version" "5.0.1" || ((++failures)) + return "${failures}" +} + +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + checkRequirements + exit 0 +} + optionHelpCallback() { local profilesList="" local dsnList="" @@ -2088,6 +2178,7 @@ optionHelpCallback() { profilesList="$(Conf::getMergedList "dbImportProfiles" "sh" || true)" dbImportStreamCommand help | envsubst + checkRequirements exit 0 } @@ -2150,12 +2241,6 @@ EOF # @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 local -Agx dbTargetInstance diff --git a/bin/dbQueryAllDatabases b/bin/dbQueryAllDatabases index b26beb15..e09b6a08 100755 --- a/bin/dbQueryAllDatabases +++ b/bin/dbQueryAllDatabases @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,368 +91,243 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true +# @description Display message using info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} - ((argLength = ${#arg})) || true +# @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 +} - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi +# @description used to execute given query when using +# dbScriptAllDatabases +# @arg $1 dsn:String +# @arg $2 db:String +# @env query String +# @env optionSeparator String +# @require Linux::requireExecutedAsUser +Db::queryOneDatabase() { + # query and optionSeparator are passed via export + local dsn="$1" + local db="$2" - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" + local -A dbInstance + Database::newInstance dbInstance "${dsn}" + Database::setQueryOptions dbInstance "${dbInstance[QUERY_OPTIONS]} --connect-timeout=5" - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' + # identify columns header + echo -n "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + Database::skipColumnNames dbInstance 0 + + # shellcheck disable=SC2154 + if ! Database::query dbInstance "${query}" "${db}" | sed "s/\t/${optionSeparator}/g"; then + Log::fatal "database ${db} error" 1>&2 + fi } -# @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" +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} - "${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 +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logError "$1" } -# @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/ +# @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 # -# @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) +# @set __RESET_COLOR String reset default color # -# @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 - - 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 - } - - # 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 +# @set __HELP_EXAMPLE String to remove +# @set __HELP_TITLE String to remove +# @set __HELP_NORMAL String to remove +# shellcheck disable=SC2034 +UI::theme() { + local theme="${1-default}" + if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then + theme="noColor" fi - # from home/.bash-tools/ - testAbs "$(File::concatenatePath "${HOME}/.bash-tools" "${confFolder}")/${conf}${extension}" && return 0 - - if [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # from framework conf/ (including extension) - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 - - # from framework conf/ - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 + case "${theme}" in + default | default-force) + theme="default" + ;; + noColor) ;; + *) + Log::fatal "invalid theme provided" + ;; + esac + if [[ "${theme}" = "default" ]]; then + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' fi - - # file not found - Log::displayError "conf file '${conf}' not found" - - return 1 -} - -# @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 - if [[ -d "${HOME_CONF_DIR}" ]]; then - Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" - fi - ) | sort | uniq -} - -# @description databases's list of given mysql server -# -# @example text -# - information_schema -# - mysql -# - performance_schema -# - sys -# -# @arg $1 instanceUserDbList:&Map (passed by reference) database instance to use -# @stdout the list of db except mysql admin ones (see example) -# @exitcode * the query exit code -Database::getUserDbList() { - # shellcheck disable=SC2034 - local -n instanceUserDbList=$1 - # shellcheck disable=SC2016 - local sql='SELECT `schema_name` from INFORMATION_SCHEMA.SCHEMATA WHERE `schema_name` NOT IN("information_schema", "mysql", "performance_schema", "sys")' - Database::query instanceUserDbList "${sql}" } -# @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 +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) fi - - # final auth file generated from dns file - instanceNewInstance['AUTH_FILE']="" - instanceNewInstance['DSN_FILE']="" - - # check dsn file - DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 - Database::checkDsnFile "${DSN_FILE}" || return 1 - instanceNewInstance['DSN_FILE']="${DSN_FILE}" - - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${instanceNewInstance['DSN_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") - ( - 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 + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo } -# @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" +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 1 } -# @description used to execute given query when using -# dbScriptAllDatabases -# @arg $1 dsn:String -# @arg $2 db:String -# @env query String -# @env optionSeparator String -# @require Linux::requireExecutedAsUser -Db::queryOneDatabase() { - # query and optionSeparator are passed via export - local dsn="$1" - local db="$2" - - local -A dbInstance - Database::newInstance dbInstance "${dsn}" - Database::setQueryOptions dbInstance "${dbInstance[QUERY_OPTIONS]} --connect-timeout=5" - - # identify columns header - echo -n "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" - Database::skipColumnNames dbInstance 0 - - # shellcheck disable=SC2154 - if ! Database::query dbInstance "${query}" "${db}" | sed "s/\t/${optionSeparator}/g"; then - Log::fatal "database ${db} error" 1>&2 - fi +# @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 ensure env files are loaded @@ -466,8 +343,10 @@ Env::requireLoad() { # BASH_FRAMEWORK_ENV_FILES is an array configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") fi - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") + local localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") fi if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") @@ -484,98 +363,6 @@ Env::requireLoad() { done } -# @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 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 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - # @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 @@ -598,10 +385,12 @@ Log::requireLoad() { 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 + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 @@ -609,7 +398,6 @@ Log::requireLoad() { BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then @@ -621,133 +409,375 @@ Log::requireLoad() { fi } -# @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 concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + fi + + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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 +# @description check if command specified exists or return 1 +# with error and message if not # -# @set __RESET_COLOR String reset default color +# @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 # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -UI::theme() { - local theme="${1-default}" - if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then - theme="noColor" +# @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 +} + +# @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 + + # shellcheck disable=SC2034 + local status=0 + # shellcheck disable=SC2034 + local -a pipeStatus=() + local version + version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" + + Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + + Version::compare "${version}" "${minimalVersion}" || { + local result=$? + if [[ "${result}" = "1" ]]; then + Log::displayInfo "${commandName} version is ${version} greater than ${minimalVersion}" + elif [[ "${result}" = "2" ]]; then + Log::displayError "${commandName} minimal version is ${minimalVersion}, your version is ${version}" + return 1 + fi + return 0 + } + +} + +# @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 + if [[ -d "${HOME_CONF_DIR}" ]]; then + Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" + fi + ) | sort | uniq +} + +# @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 - case "${theme}" in - default | default-force) - theme="default" - ;; - noColor) ;; - *) - Log::fatal "invalid theme provided" - ;; - esac - if [[ "${theme}" = "default" ]]; then - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' + + 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 + } + + # 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 [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then + # from framework conf/ (including extension) + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 + + # from framework conf/ + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 fi + + # file not found + Log::displayError "conf file '${conf}' not found" + + return 1 } -# @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:-}" +# @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 + + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" + + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" - Assert::commandExists "${commandName}" "${help}" || return 2 + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${instanceNewInstance['DSN_FILE']}" - # shellcheck disable=SC2034 - local status=0 - # shellcheck disable=SC2034 - local -a pipeStatus=() - local version - version="$("${commandName}" "${argVersion}" 2>&1 | ${parseVersionCallback} || Bash::handlePipelineFailure status pipeStatus)" + instanceNewInstance['USER']="${USER}" + instanceNewInstance['PASSWORD']="${PASSWORD}" + instanceNewInstance['HOSTNAME']="${HOSTNAME}" + instanceNewInstance['PORT']="${PORT}" - Log::displayDebug "check ${commandName} version ${version} against minimal ${minimalVersion}" + # generate authFile for easy authentication + instanceNewInstance['AUTH_FILE']=$(mktemp -p "${TMPDIR:-/tmp}" -t "mysql.XXXXXXXXXXXX") + ( + echo "[client]" + echo "user = ${USER}" + echo "password = ${PASSWORD}" + echo "host = ${HOSTNAME}" + echo "port = ${PORT}" + ) >"${instanceNewInstance['AUTH_FILE']}" - Version::compare "${version}" "${minimalVersion}" || { - local result=$? - if [[ "${result}" = "1" ]]; then - Log::displayDebug "${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 - } + # 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 +} + +# @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" +} +# @description databases's list of given mysql server +# +# @example text +# - information_schema +# - mysql +# - performance_schema +# - sys +# +# @arg $1 instanceUserDbList:&Map (passed by reference) database instance to use +# @stdout the list of db except mysql admin ones (see example) +# @exitcode * the query exit code +Database::getUserDbList() { + # shellcheck disable=SC2034 + local -n instanceUserDbList=$1 + # shellcheck disable=SC2016 + local sql='SELECT `schema_name` from INFORMATION_SCHEMA.SCHEMATA WHERE `schema_name` NOT IN("information_schema", "mysql", "performance_schema", "sys")' + Database::query instanceUserDbList "${sql}" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -756,17 +786,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -780,6 +799,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -788,6 +815,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -850,136 +889,70 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active -# @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" -# @description ignore exit code 141 from simple command pipes -# @example use with: -# local resultingStatus=0 -# local -a originalPipeStatus=() -# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true -# [[ "${resultingStatus}" = "0" ]] -# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code -# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array -# @env PIPESTATUS assuming that this function is called like in the example provided -# @see https://unix.stackexchange.com/a/709880/582856 -Bash::handlePipelineFailure() { - local -a pipeStatusBackup=("${PIPESTATUS[@]}") - local -n handlePipelineFailure_resultingStatusCode=$1 - local -n handlePipelineFailure_originalStatus=$2 - # shellcheck disable=SC2034 - handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") - handlePipelineFailure_resultingStatusCode=0 - local statusCode - for statusCode in "${pipeStatusBackup[@]}"; do - if ((statusCode == 141)); then - return 0 - elif ((statusCode > 0)); then - # shellcheck disable=SC2034 - handlePipelineFailure_resultingStatusCode="${statusCode}" - break +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" fi - done - return "${handlePipelineFailure_resultingStatusCode}" + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi } -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" +# @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 - ( - # 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 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 +# @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 +} - ( - unset HOSTNAME PORT PASSWORD USER - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${dsnFileName}" - if [[ -z ${HOSTNAME+x} ]]; then - Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" - return 1 - fi - if [[ -z "${HOSTNAME}" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" - fi - if [[ "${HOSTNAME}" = "localhost" ]]; then - Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" - fi - if [[ -z "${PORT+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT not provided" - return 1 - fi - if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then - Log::displayError "dsn file ${dsnFileName} : PORT invalid" - return 1 - fi - if [[ -z "${USER+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : USER not provided" - return 1 - fi - if [[ -z "${PASSWORD+x}" ]]; then - Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" - return 1 - fi - ) +# @description by default we skip the column names +# but sometimes we need column names to display some results +# disable this option temporarily and then restore it to true +# +# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use +# @arg $2 skipColumnNames:Boolean 0 to disable, 1 to enable (hide column names) +Database::skipColumnNames() { + local -n instanceSkipColumnNames=$1 + # shellcheck disable=SC2034 + instanceSkipColumnNames['SKIP_COLUMN_NAMES']="$2" } # @description mysql query on a given db @@ -1021,47 +994,11 @@ Database::query() { fi } -# @description by default we skip the column names -# but sometimes we need column names to display some results -# disable this option temporarily and then restore it to true -# -# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use -# @arg $2 skipColumnNames:Boolean 0 to disable, 1 to enable (hide column names) -Database::skipColumnNames() { - local -n instanceSkipColumnNames=$1 - # shellcheck disable=SC2034 - instanceSkipColumnNames['SKIP_COLUMN_NAMES']="$2" -} - -# @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 -} - # @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" +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" fi } @@ -1073,18 +1010,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -1114,14 +1058,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -1130,10 +1066,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -1143,6 +1080,51 @@ Log::rotate() { fi } +# @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() { + # match anything, print(p), exit on first match(Q) + sed -En \ + -e 's/\x1b\[[0-9;]*[mGKHF]//g' \ + -e 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/' \ + -e '//{p;Q}' \ + "$@" +} + +# @description ignore exit code 141 from simple command pipes +# @example use with: +# local resultingStatus=0 +# local -a originalPipeStatus=() +# cmd1 | cmd2 || Bash::handlePipelineFailure resultingStatus originalPipeStatus || true +# [[ "${resultingStatus}" = "0" ]] +# @arg $1 resultingStatusCode:&int (passed by reference) (optional) resulting status code +# @arg $2 originalStatus:int[] (passed by reference) (optional) copy of original PIPESTATUS array +# @env PIPESTATUS assuming that this function is called like in the example provided +# @see https://unix.stackexchange.com/a/709880/582856 +Bash::handlePipelineFailure() { + local -a pipeStatusBackup=("${PIPESTATUS[@]}") + local -n handlePipelineFailure_resultingStatusCode=$1 + local -n handlePipelineFailure_originalStatus=$2 + # shellcheck disable=SC2034 + handlePipelineFailure_originalStatus=("${pipeStatusBackup[@]}") + handlePipelineFailure_resultingStatusCode=0 + local statusCode + for statusCode in "${pipeStatusBackup[@]}"; do + if ((statusCode == 141)); then + return 0 + elif ((statusCode > 0)); then + # shellcheck disable=SC2034 + handlePipelineFailure_resultingStatusCode="${statusCode}" + break + fi + done + return "${handlePipelineFailure_resultingStatusCode}" +} + # @description compare 2 version numbers # @arg $1 version1:String version 1 # @arg $2 version2:String version 2 @@ -1175,31 +1157,125 @@ Version::compare() { return 0 } -# @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 list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} + +# @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 +} + +# @description check if dsn file has all the mandatory variables set +# Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT +# +# @arg $1 dsnFileName:String dsn absolute filename +# @set HOSTNAME loaded from dsn file +# @set PORT loaded from dsn file +# @set USER loaded from dsn file +# @set PASSWORD loaded from dsn file +# @exitcode 0 on valid file +# @exitcode 1 if one of the properties of the conf file is invalid or if file not found +# @stderr log output if error found in conf file +Database::checkDsnFile() { + local dsnFileName="$1" + if [[ ! -f "${dsnFileName}" ]]; then + Log::displayError "dsn file ${dsnFileName} not found" + return 1 + fi + + ( + unset HOSTNAME PORT PASSWORD USER + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${dsnFileName}" + if [[ -z ${HOSTNAME+x} ]]; then + Log::displayError "dsn file ${dsnFileName} : HOSTNAME not provided" + return 1 + fi + if [[ -z "${HOSTNAME}" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : HOSTNAME value not provided" + fi + if [[ "${HOSTNAME}" = "localhost" ]]; then + Log::displayWarning "dsn file ${dsnFileName} : check that HOSTNAME should not be 127.0.0.1 instead of localhost" + fi + if [[ -z "${PORT+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT not provided" + return 1 + fi + if ! [[ ${PORT} =~ ^[0-9]+$ ]]; then + Log::displayError "dsn file ${dsnFileName} : PORT invalid" + return 1 + fi + if [[ -z "${USER+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : USER not provided" + return 1 + fi + if [[ -z "${PASSWORD+x}" ]]; then + Log::displayError "dsn file ${dsnFileName} : PASSWORD not provided" + return 1 + fi + ) +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -1209,14 +1285,6 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } -# @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 -} - # FUNCTIONS facade_main_dbQueryAllDatabasessh() { @@ -1240,8 +1308,8 @@ fi Linux::requireExecutedAsUser Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir @@ -1456,7 +1524,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1483,7 +1551,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1906,7 +1974,7 @@ dbQueryAllDatabasesCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Execute a query on multiple databases in order to generate a report with tsv format, query can be parallelized on multiple databases")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Execute a query on multiple databases in order to generate a report with tsv format, query can be parallelized on multiple databases" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" @@ -2058,12 +2126,35 @@ ${__HELP_EXAMPLE}${example1}${__HELP_NORMAL}""" fi } +checkRequirements() { + if [[ "${SKIP_REQUIREMENTS_CHECKS:-0}" = "1" ]]; then + return 0 + fi + local -i failures=0 + echo + Assert::commandExists mysql "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqlshow "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists mysqldump "sudo apt-get install -y mysql-client" || ((++failures)) + Assert::commandExists pv "sudo apt-get install -y pv" || ((++failures)) + Assert::commandExists gawk "sudo apt-get install -y gawk" || ((++failures)) + Assert::commandExists awk "sudo apt-get install -y gawk" || ((++failures)) + Version::checkMinimal "gawk" "--version" "5.0.1" || ((++failures)) + return "${failures}" +} + +optionVersionCallback() { + echo "${SCRIPT_NAME} version 2.0" + checkRequirements + exit 0 +} + optionHelpCallback() { local dsnList queriesList dsnList="$(Conf::getMergedList "dsn" "env")" queriesList="$(Conf::getMergedList "dbQueries" "sql" || true)" dbQueryAllDatabasesCommand help | envsubst + checkRequirements exit 0 } @@ -2126,14 +2217,6 @@ EOF # @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 parallel "sudo apt-get install -y parallel" - 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" - # query contains the sql from argQuery or from query string if -q option is provided declare query="${argQuery}" if [[ "${queryIsFile}" = "1" ]]; then diff --git a/bin/dbScriptAllDatabases b/bin/dbScriptAllDatabases index 99c7e4ac..ea2a17f3 100755 --- a/bin/dbScriptAllDatabases +++ b/bin/dbScriptAllDatabases @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,26 +502,34 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @description check if command specified exists or return 1 -# with error and message if not +# @description list the conf files list available in bash-tools/conf/ folder +# and those overridden in ${HOME}/.bash-tools/ folder # -# @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 +# @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 / # -# @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 +# @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 + if [[ -d "${HOME_CONF_DIR}" ]]; then + Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" + fi + ) | sort | uniq } # @description get absolute conf file from specified conf folder deduced using these rules @@ -309,53 +593,26 @@ Conf::getAbsoluteFile() { return 1 } -# @description list the conf files list available in bash-tools/conf/ folder -# and those overridden in ${HOME}/.bash-tools/ folder +# @description check if command specified exists or return 1 +# with error and message if not # -# @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 / +# @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 # -# @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}" +# @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" - ( - if [[ -d "${DEFAULT_CONF_DIR}" ]]; then - Conf::list "${DEFAULT_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" - fi - if [[ -d "${HOME_CONF_DIR}" ]]; then - Conf::list "${HOME_CONF_DIR}" "" "${extension}" "-type f" "${indentStr}" + "${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 - ) | sort | uniq -} - -# @description databases's list of given mysql server -# -# @example text -# - information_schema -# - mysql -# - performance_schema -# - sys -# -# @arg $1 instanceUserDbList:&Map (passed by reference) database instance to use -# @stdout the list of db except mysql admin ones (see example) -# @exitcode * the query exit code -Database::getUserDbList() { - # shellcheck disable=SC2034 - local -n instanceUserDbList=$1 - # shellcheck disable=SC2016 - local sql='SELECT `schema_name` from INFORMATION_SCHEMA.SCHEMATA WHERE `schema_name` NOT IN("information_schema", "mysql", "performance_schema", "sys")' - Database::query instanceUserDbList "${sql}" + return 1 + } + return 0 } # @description create a new db instance @@ -427,250 +684,23 @@ Database::setQueryOptions() { instanceSetQueryOptions['QUERY_OPTIONS']="$2" } -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @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 +# @description databases's list of given mysql server # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi +# @example text +# - information_schema +# - mysql +# - performance_schema +# - sys +# +# @arg $1 instanceUserDbList:&Map (passed by reference) database instance to use +# @stdout the list of db except mysql admin ones (see example) +# @exitcode * the query exit code +Database::getUserDbList() { + # shellcheck disable=SC2034 + local -n instanceUserDbList=$1 + # shellcheck disable=SC2016 + local sql='SELECT `schema_name` from INFORMATION_SCHEMA.SCHEMATA WHERE `schema_name` NOT IN("information_schema", "mysql", "performance_schema", "sys")' + Database::query instanceUserDbList "${sql}" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -679,17 +709,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -703,6 +722,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -711,6 +738,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -782,12 +821,81 @@ Linux::requireExecutedAsUser() { fi } +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi +} + +# @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 +} + +# @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 +} + +# @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 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 +} + # @description check if tty (interactive mode) is active # @noargs # @exitcode 1 if tty not active # @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive # @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided Assert::tty() { if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then return 1 @@ -795,7 +903,62 @@ Assert::tty() { if [[ "${INTERACTIVE:-0}" = "1" ]]; then return 0 fi - [[ -t 1 || -t 2 ]] + tty -s +} + +# @description log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$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 +} + +# @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}" + + if [[ ! -f "${file}" ]]; then + Log::displayDebug "Log file ${file} doesn't exist yet" + return 0 + fi + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do + Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" + mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true + done + if cp "${file}" "${file}.1" &>/dev/null; then + echo >"${file}" # reset log file + Log::displayInfo "Log rotation ${file} to ${file}.1" + fi } # @description list files of dir with given extension and display it as a list one by line @@ -833,6 +996,18 @@ Conf::list() { ) } +# @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 +} + # @description check if dsn file has all the mandatory variables set # Mandatory variables are: HOSTNAME, USER, PASSWORD, PORT # @@ -935,119 +1110,15 @@ Env::pathPrepend() { 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 -} - -# @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 -} - -# @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 -} - -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$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 -} - -# @description Internal: common log message -# @example text -# [date]|[levelMsg]|message -# -# @example text -# 2020-01-19 19:20:21|ERROR |log error -# 2020-01-19 19:20:21|SKIPPED|log skipped -# -# @arg $1 levelMsg:String message's level description (eg: STATUS, ERROR, ...) -# @arg $2 msg:String the message to display -# @env BASH_FRAMEWORK_LOG_FILE String log file to use, do nothing if empty -# @env BASH_FRAMEWORK_LOG_LEVEL int log level log only if > OFF or fatal messages -# @stderr diagnostics information is displayed -# @require Env::requireLoad -# @require Log::requireLoad -Log::logMessage() { - local levelMsg="$1" - local msg="$2" - local date - - if [[ -n "${BASH_FRAMEWORK_LOG_FILE}" ]] && ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then - date="$(date '+%Y-%m-%d %H:%M:%S')" - touch "${BASH_FRAMEWORK_LOG_FILE}" - printf "%s|%7s|%s\n" "${date}" "${levelMsg}" "${msg}" >>"${BASH_FRAMEWORK_LOG_FILE}" - fi -} - -# @description 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 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}" - - if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" - return 0 - fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do - Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" - mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true - done - if cp "${file}" "${file}.1" &>/dev/null; then - echo >"${file}" # reset log file - Log::displayInfo "Log rotation ${file} to ${file}.1" - fi -} - # @description load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -1057,14 +1128,6 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } -# @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 -} - # FUNCTIONS facade_main_dbScriptAllDatabasessh() { @@ -1087,8 +1150,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir Linux::requireExecutedAsUser @@ -1297,7 +1360,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1324,7 +1387,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1788,7 +1851,7 @@ dbScriptAllDatabasesCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Allows to execute a script on each database of specified mysql server")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Allows to execute a script on each database of specified mysql server" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/doc b/bin/doc index a7bf684b..1608095c 100755 --- a/bin/doc +++ b/bin/doc @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,307 +502,38 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @description checkout usage doc below -# -# [DockerNamespace usage](DockerUsage.md ':include') - -# @description run the container specified by args provided. -# build and push the image if needed -# -# @env DOCKER_BUILD_OPTIONS -# @env SKIP_USER -# @env USER_ID -# @env GROUP_ID -# @env FRAMEWORK_ROOT_DIR -# @env DOCKER_OPTION_IMAGE_TAG -# @env BASH_FRAMEWORK_ARGV_FILTERED -Docker::runBuildContainer() { - local optionVendor="$1" - local optionBashVersion="$2" - local optionBashBaseImage="$3" - local optionSkipDockerBuild="$4" - local optionTraceVerbose="$5" - local optionContinuousIntegrationMode="$6" - local -n localDockerRunCmd=$7 - local -n localDockerRunArgs=$8 - if tty -s; then - localDockerRunArgs+=("-it") - fi - if [[ -d "$(pwd)/vendor/bash-tools-framework" ]]; then - localDockerRunArgs+=( - -v "$(cd "$(pwd)/vendor/bash-tools-framework" && pwd -P):/bash/vendor/bash-tools-framework" - ) - fi - - # shellcheck disable=SC2154 - if [[ "${optionContinuousIntegrationMode}" = "0" ]]; then - localDockerRunArgs+=(-v "/tmp:/tmp") - fi - localDockerRunArgs+=(-e KEEP_TEMP_FILES="${KEEP_TEMP_FILES}") - localDockerRunArgs+=(-e BATS_FIX_TEST="${BATS_FIX_TEST:-0}") - - # shellcheck disable=SC2154 - Log::displayInfo "Using ${optionVendor}:${optionBashVersion}" +BASH_FRAMEWORK_SHDOC_INSTALLED_PATH="vendor/.shDocInstalled" +BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT=86400 # 1 day - local imageRef="${DOCKER_OPTION_IMAGE_TAG:-build:bash-tools-${optionVendor}-${optionBashVersion}}" - if [[ "${optionSkipDockerBuild:-0}" != "1" ]]; then - Log::displayInfo "Build docker image ${imageRef}" - # shellcheck disable=SC2154 - ( - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x - fi - Docker::buildPushDockerImage \ - "${optionVendor}" \ - "${optionBashVersion}" \ - "${optionBashBaseImage}" \ - "${optionPush}" \ - "${optionTraceVerbose}" - ) +# @description install requirements to execute shdoc +# @warning cloning is skipped if vendor/.shDocInstalled file exists +# @warning a new check is done everyday +# @warning repository is not updated if a change is detected +# @env BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT int default value 86400 (86400 seconds = 1 day) +# @set BASH_FRAMEWORK_SHDOC_INSTALLED String the file created when git clone succeeded +# @see https://github.com/fchastanet/shdoc +# @stderr diagnostics information is displayed +# @feature Git::cloneOrPullIfNoChanges +ShellDoc::installRequirementsIfNeeded() { + local BASH_FRAMEWORK_SHDOC_INSTALLED="${FRAMEWORK_ROOT_DIR}/${BASH_FRAMEWORK_SHDOC_INSTALLED_PATH}" + if [[ -d "${FRAMEWORK_ROOT_DIR}/vendor" ]]; then + mkdir -p "${FRAMEWORK_ROOT_DIR}/vendor" || return 1 fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.docker/DockerfileUser" ]]; then - local imageRefUser="${imageRef}-user" - if [[ "${optionSkipDockerBuild:-0}" != "1" ]]; then - Log::displayInfo "build docker image ${imageRefUser} with user configuration" - # shellcheck disable=SC2154 - ( - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x - fi - # shellcheck disable=SC2086 - DOCKER_BUILDKIT=1 docker build \ - ${DOCKER_BUILD_OPTIONS} \ - --cache-from "scrasnups/${imageRef}" \ - --build-arg "BASH_IMAGE=scrasnups/${imageRef}" \ - --build-arg SKIP_USER="${SKIP_USER:-0}" \ - --build-arg USER_ID="${USER_ID:-$(id -u)}" \ - --build-arg GROUP_ID="${GROUP_ID:-$(id -g)}" \ - -f "${FRAMEWORK_ROOT_DIR}/.docker/DockerfileUser" \ - -t "${imageRefUser}" \ - "${FRAMEWORK_ROOT_DIR}/.docker" - ) + if [[ "$( + Cache::getFileContentIfNotExpired \ + "${BASH_FRAMEWORK_SHDOC_INSTALLED}" \ + "${BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT}" + )" != "1" ]]; then + Log::displayInfo "Check if shdoc is up to date" + if GIT_CLONE_OPTIONS="--recursive" Git::cloneOrPullIfNoChanges \ + "${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}/shdoc" \ + "https://github.com/fchastanet/shdoc.git"; then + echo "1" >"${BASH_FRAMEWORK_SHDOC_INSTALLED}" + else + Log::fatal "unable to install shdoc library" fi fi - - Log::displayDebug "Run container with localDockerRunCmd: ${localDockerRunCmd[*]}" - Log::displayDebug "Run container with localDockerRunArgs: ${localDockerRunArgs[*]}" - Log::displayDebug "Run container with BASH_FRAMEWORK_ARGV_FILTERED: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" - ( - # shellcheck disable=SC2154 - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x - fi - # shellcheck disable=SC2086 - docker run \ - --rm \ - "${localDockerRunArgs[@]}" \ - ${DOCKER_RUN_OPTIONS} \ - -w /bash \ - -v "$(pwd):/bash" \ - --user "${USER_ID:-$(id -u)}:${GROUP_ID:-$(id -g)}" \ - "${imageRefUser}" \ - "${localDockerRunCmd[@]}" - ) -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @description Display message using info color (blue) but warning level -# @arg $1 message:String the message to display -Log::displayStatus() { - local type="${2:-STATUS}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logStatus "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @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 - - fixMarkdownToc="$( - cat <<'EOF' -{ - line=$0 - if (match(line, /^(\s*- \[([0-9]+\.)+ [^]]+\]\(#)([^)]+)\)/, arr)) { - print arr[1] "_" rewrite(arr[3]) ")" - } else { - print line - } -} -function rewrite(str) -{ - gsub(/-(-)+/, "-", str) - return str -} -EOF - )" - - awk -i inplace "${fixMarkdownToc}" "${file}" -} +} # @description generates markdown file from template by # replacing @@@command_help@@@ by the help of the command @@ -571,7 +578,7 @@ ShellDoc::generateMdFileFromTemplate() { ) || Log::displayError "$(realpath "${fromDir}/${relativeFile}" --relative-to="${FRAMEWORK_ROOT_DIR}") --help error caught" else ((++tokenNotFoundCount)) - Log::displayWarning "token ${token} not found in ${targetFile}" + Log::displayError "token ${token} not found in ${targetFile}" fi ((nbTokensGenerated++)) || true done < <(cd "${fromDir}" && find . -type f -executable | "${grepExclude[@]}") @@ -579,91 +586,47 @@ ShellDoc::generateMdFileFromTemplate() { Log::displayInfo "${nbTokensGenerated} commands' help replaced in $(echo "scale=3; ${endTime} - ${startTime}" | bc)seconds" } -# @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 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 + + fixMarkdownToc="$( + cat <<'EOF' +{ + line=$0 + if (match(line, /^(\s*- \[([0-9]+\.)+ [^]]+\]\(#)([^)]+)\)/, arr)) { + print arr[1] "_" rewrite(arr[3]) ")" + } else { + print line + } } +function rewrite(str) +{ + gsub(/-(-)+/, "-", str) + return str +} +EOF + )" -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' + awk -i inplace "${fixMarkdownToc}" "${file}" +} + +# @description Display message using info color (blue) but warning level +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayStatus() { + local type="${2:-STATUS}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi + Log::logStatus "$1" "${type}" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -672,17 +635,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -696,13 +648,33 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### -### all log messages will be redirected to log file specified -### this same path will be used inside and outside of the container +### all log messages will be redirected to log file specified +### this same path will be used inside and outside of the container +### +BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} + +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG ### -BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -766,112 +738,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active -# @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} - -# @description build image and push it ot registry -# @env DOCKER_OPTION_IMAGE_TAG String computed from optionVendor and optionBashVersion if not provided -# @env DOCKER_OPTION_IMAGE String default scrasnups/${DOCKER_OPTION_IMAGE_TAG} -# @env DOCKER_BUILD_OPTIONS String list of docker arguments to pass to docker build command -# @env FRAMEWORK_ROOT_DIR String path allowing to deduce .docker/Dockerfile.{vendor} -Docker::buildPushDockerImage() { - local optionVendor="$1" - local optionBashVersion="$2" - local optionBashBaseImage="$3" - local optionPush="$4" - local optionTraceVerbose="$5" - # parameters based on env variables - local imageTag="${DOCKER_OPTION_IMAGE_TAG:-build:bash-tools-${optionVendor}-${optionBashVersion}}" - local image="${DOCKER_OPTION_IMAGE:-scrasnups/${imageTag}}" - local DOCKER_BUILD_OPTIONS="${DOCKER_BUILD_OPTIONS:-}" - - Log::displayInfo "Pull image ${image}" - ( - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x - fi - docker pull "${image}" || true - ) +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" - Log::displayInfo "Build image ${image}" - # shellcheck disable=SC2086 - ( - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" fi - DOCKER_BUILDKIT=1 docker build \ - ${DOCKER_BUILD_OPTIONS} \ - -f "${FRAMEWORK_ROOT_DIR}/.docker/Dockerfile.${optionVendor}" \ - --cache-from "${image}" \ - --build-arg BUILDKIT_INLINE_CACHE=1 \ - --build-arg argBashVersion="${optionBashVersion}" \ - --build-arg BASH_IMAGE="${optionBashBaseImage}" \ - -t "${imageTag}" \ - -t "${image}" \ - "${FRAMEWORK_ROOT_DIR}/.docker" - ) - - Log::displayInfo "Image ${image} - bash version check" - docker run --rm "${imageTag}" bash --version - - # shellcheck disable=SC2154 - if [[ "${optionPush}" = "1" ]]; then - Log::displayInfo "Push image ${image}" - ( - if [[ "${optionTraceVerbose}" = "1" ]]; then - set -x - fi - docker push "scrasnups/${imageTag}" - ) + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi } -# @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" - - ( - local tokenFile - tokenFile="$(Framework::createTempFile "replaceTokenByInput")" - - cat - | Filters::removeAnsiCodes >"${tokenFile}" - - sed -E -i \ - -e "/${token}/r ${tokenFile}" \ - -e "/${token}/d" \ - "${targetFile}" - ) +# @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 } # @description log message to file @@ -882,6 +792,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -890,18 +808,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -931,22 +856,6 @@ Log::logMessage() { fi } -# @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 -} - -# @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 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 @@ -955,10 +864,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -968,12 +878,190 @@ Log::rotate() { fi } +# @description get file content if file not expired +# @arg $1 file:String the file to get content from +# @arg $2 maxDuration:int number of seconds after which the file is considered expired +# @stdout {String} the file content if not expired +# @exitcode 1 if file does not exists +# @exitcode 2 if file expired +Cache::getFileContentIfNotExpired() { + local file="$1" + local maxDuration="$2" + + if [[ ! -f "${file}" ]]; then + return 1 + fi + if (($(File::elapsedTimeSinceLastModification "${file}") > maxDuration)); then + return 2 + fi + cat "${file}" +} + +# @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 +# @env SUDO String allows to use custom sudo prefix command +# @exitcode 0 on successful pulling/cloning, 1 on failure +Git::cloneOrPullIfNoChanges() { + local dir="$1" + shift || true + local repo="$1" + shift || true + local cloneCallback=${1:-} + shift || true + local pullCallback=${1:-} + shift || true + + if [[ -d "${dir}/.git" ]]; then + local exitCode=0 + Git::pullIfNoChanges "${dir}" || exitCode=$? + if Array::contains "${exitCode}" "2" "4"; then + # changes detected + return 0 + fi + if [[ "${exitCode}" != "0" ]]; then + return "${exitCode}" + fi + # shellcheck disable=SC2086 + if [[ "$(type -t ${pullCallback})" = "function" ]]; then + ${pullCallback} "${dir}" + fi + else + Log::displayInfo "cloning ${repo} ..." + if ! ${SUDO:-} test -d "${dir%/*}"; then + ${SUDO:-} mkdir -p "${dir%/*}" + fi + # shellcheck disable=SC2086,SC2248 + if ${SUDO:-} 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 +} + +# @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" + + ( + local tokenFile + tokenFile="$(Framework::createTempFile "replaceTokenByInput")" + + cat - | Filters::removeAnsiCodes >"${tokenFile}" + # ensure blank final line + echo >>"${tokenFile}" + + sed -E -i \ + -e "/${token}/r ${tokenFile}" \ + -e "/${token}/d" \ + "${targetFile}" + ) +} + +# @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 +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" + fi +} + +# @description get number of seconds since last modification of the file +# @arg $1 file:String file path +# @exitcode 1 if file does not exist +# @stdout number of seconds since last modification of the file +File::elapsedTimeSinceLastModification() { + local file="$1" + if [[ ! -f "${file}" ]]; then + return 1 + fi + local lastModificationTimeSeconds diff + lastModificationTimeSeconds="$(stat -c %Y "${file}")" + ((diff = $(date +%s) - lastModificationTimeSeconds)) + echo -n "${diff}" +} + +# @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 +# @env SUDO String allows to use custom sudo prefix command +# @require Git::requireGitCommand +Git::pullIfNoChanges() { + local dir="$1" + if [[ ! -d "${dir}/.git" ]]; then + return 3 + fi + ( + cd "${dir}" || return 3 + if ! ${SUDO:-} git update-index --refresh &>/dev/null; then + Log::displayWarning "Impossible to update git index of '${dir}' - check if you have modified file" + return 4 + fi + if ! ${SUDO:-} git diff-index --quiet HEAD --; then + Log::displayWarning "Pulling git repository '${dir}' avoided as changes detected" + return 2 + fi + Log::displayInfo "Pull git repository '${dir}' as no changes detected" + ${SUDO:-} git pull --progress + ) +} + +# @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 } # @description remove ansi codes from input or files given as argument @@ -989,21 +1077,33 @@ Filters::removeAnsiCodes() { # cspell:enable } -# @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 ensure command git is available +# @exitcode 1 if git command not available +# @stderr diagnostics information is displayed +Git::requireGitCommand() { + Assert::commandExists git } -# @description log message to file -# @arg $1 message:String the message to display -Log::logSkipped() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then - Log::logMessage "${2:-SKIPPED}" "$1" - fi +# @description check if 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 } # FUNCTIONS @@ -1029,13 +1129,13 @@ fi Env::requireLoad UI::requireTheme Log::requireLoad +Git::requireGitCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir # @require Compiler::Facade::requireCommandBinDir # shellcheck disable=SC2034 -DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" declare copyrightBeginYear="2020" declare -a BASH_FRAMEWORK_ARGV_FILTERED=() @@ -1082,7 +1182,7 @@ optionHelpCallback() { # shellcheck disable=SC2317 # if function is overridden optionVersionCallback() { - echo "${SCRIPT_NAME} version 1.0" + echo "${SCRIPT_NAME} version 1.1" exit 0 } @@ -1225,7 +1325,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1252,7 +1352,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1279,11 +1379,10 @@ commandOptionParseFinished() { displayConfig fi } - -declare optionSkipDockerBuild=0 +declare optionContinuousIntegrationMode=0 # shellcheck disable=SC2317 # if function is overridden -updateOptionSkipDockerBuildCallback() { +updateOptionContinuousIntegrationMode() { BASH_FRAMEWORK_ARGV_FILTERED+=("$1") } @@ -1292,9 +1391,9 @@ docCommand() { shift || true if [[ "${options_parse_cmd}" = "parse" ]]; then - optionSkipDockerBuild="0" - local -i options_parse_optionParsedCountOptionSkipDockerBuild - ((options_parse_optionParsedCountOptionSkipDockerBuild = 0)) || true + optionContinuousIntegrationMode="0" + local -i options_parse_optionParsedCountOptionContinuousIntegrationMode + ((options_parse_optionParsedCountOptionContinuousIntegrationMode = 0)) || true local -i options_parse_optionParsedCountOptionBashFrameworkConfig ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true optionConfig="0" @@ -1337,16 +1436,16 @@ docCommand() { 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) + # Option optionContinuousIntegrationMode --ci variableType Boolean min 0 max 1 authorizedValues '' regexp '' + --ci) # shellcheck disable=SC2034 - optionSkipDockerBuild="1" - if ((options_parse_optionParsedCountOptionSkipDockerBuild >= 1)); then + optionContinuousIntegrationMode="1" + if ((options_parse_optionParsedCountOptionContinuousIntegrationMode >= 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}" + ((++options_parse_optionParsedCountOptionContinuousIntegrationMode)) + updateOptionContinuousIntegrationMode "${options_parse_arg}" ;; # Option 2/15 # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' @@ -1581,19 +1680,19 @@ docCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate markdown documentation")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "generate markdown documentation" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" echo -e "$(Array::wrap2 " " 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 ]")" + "[--ci]" "[--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}" - echo -e " ${__HELP_OPTION_COLOR}--skip-docker-build${__HELP_NORMAL} {single}" + echo -e " ${__HELP_OPTION_COLOR}--ci${__HELP_NORMAL} {single}" local -a helpArray # shellcheck disable=SC2054 - helpArray=(skip\ docker\ image\ build\ if\ option\ provided) + helpArray=(activate\ continuous\ integration\ mode\ \(tmp\ folder\ not\ shared\ with\ host\)) echo -e " $(Array::wrap2 " " 76 4 "${helpArray[@]}")" echo echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" @@ -1673,7 +1772,7 @@ docCommand() { echo ' Possible values: OFF|ERR|ERROR|WARN|WARNING|INFO|DEBUG|TRACE' echo echo -n -e "${__HELP_TITLE_COLOR}VERSION: ${__RESET_COLOR}" - echo '1.0' + echo '1.1' echo echo -e "${__HELP_TITLE_COLOR}AUTHOR:${__RESET_COLOR}" echo '[François Chastanet](https://github.com/fchastanet)' @@ -1693,33 +1792,58 @@ docCommand() { docCommand parse "${BASH_FRAMEWORK_ARGV[@]}" -run() { - if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then - local -a dockerRunCmd=( - "/bash/bin/doc" - "${BASH_FRAMEWORK_ARGV_FILTERED[@]}" +installRequirements() { + ShellDoc::installRequirementsIfNeeded +} + +runContainer() { + local image="scrasnups/build:bash-tools-ubuntu-5.3" + local -a dockerRunCmd=( + "/bash/bin/doc" + "${BASH_FRAMEWORK_ARGV_FILTERED[@]}" + ) + + if ! docker inspect --type=image "${image}" &>/dev/null; then + docker pull "${image}" + fi + # run docker image + local -a localDockerRunArgs=( + --rm + -e KEEP_TEMP_FILES="${KEEP_TEMP_FILES:-0}" + -e BATS_FIX_TEST="${BATS_FIX_TEST:-0}" + -e ORIGINAL_DOC_DIR="${BASH_TOOLS_ROOT_DIR}/pages" + -e SKIP_REQUIREMENTS_CHECKS=1 + --user "www-data:www-data" + -w /bash + -v "${BASH_TOOLS_ROOT_DIR}:/bash" + --entrypoint /usr/local/bin/bash + ) + + # shellcheck disable=SC2154 + if [[ "${optionContinuousIntegrationMode}" = "0" ]]; then + localDockerRunArgs+=( + -v "/tmp:/tmp" + -it ) - # shellcheck disable=SC2034 - local -a dockerArgvFiltered=( - -e ORIGINAL_DOC_DIR="${DOC_DIR}" + fi + if [[ -d "${FRAMEWORK_ROOT_DIR}" ]]; then + localDockerRunArgs+=( + -v "$(cd "${FRAMEWORK_ROOT_DIR}" && pwd -P):/bash/vendor/bash-tools-framework" ) - # shellcheck disable=SC2154 - Docker::runBuildContainer \ - "${optionVendor:-ubuntu}" \ - "${optionBashVersion:-5.1}" \ - "${optionBashBaseImage:-ubuntu:20.04}" \ - "${optionSkipDockerBuild}" \ - "${optionTraceVerbose}" \ - "${optionContinuousIntegrationMode}" \ - dockerRunCmd \ - dockerArgvFiltered - - return $? fi - #----------------------------- - # configure docker environment - #----------------------------- + # shellcheck disable=SC2154 + if [[ "${optionTraceVerbose}" = "1" ]]; then + set -x + fi + docker run \ + "${localDockerRunArgs[@]}" \ + "${image}" \ + "${dockerRunCmd[@]}" + set +x +} + +configureContainer() { mkdir -p "${HOME}/.bash-tools" ( @@ -1733,11 +1857,11 @@ run() { chmod 755 /tmp/docker ) export PATH=/tmp:${PATH} +} - #----------------------------- - # doc generation - #----------------------------- - +generateDoc() { + local ROOT_DIR=/bash + local DOC_DIR="${ROOT_DIR}/pages" Log::displayInfo 'generate Commands.md' ((TOKEN_NOT_FOUND_COUNT = 0)) || true ShellDoc::generateMdFileFromTemplate \ @@ -1745,7 +1869,7 @@ run() { "${DOC_DIR}/Commands.md" \ "${BASH_TOOLS_ROOT_DIR}/bin" \ TOKEN_NOT_FOUND_COUNT \ - '(test)$' + '(test|buildBinFiles)$' # inject plantuml diagram source code into command sed -E -i \ @@ -1774,6 +1898,24 @@ run() { Log::displayStatus "Doc generated in ${ORIGINAL_DOC_DIR} folder" } +run() { + if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + installRequirements + if [[ "${optionContinuousIntegrationMode}" = "1" ]]; then + chmod -R 777 pages + fi + runContainer + if [[ "${optionContinuousIntegrationMode}" = "1" ]]; then + # restore previous rights + find pages -type d -exec chmod 755 {} ';' + find pages -type f -exec chmod 644 {} ';' + fi + else + configureContainer + generateDoc + fi +} + if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then run &>/dev/null else diff --git a/bin/gitIsAncestorOf b/bin/gitIsAncestorOf index 3c659635..7d8725b8 100755 --- a/bin/gitIsAncestorOf +++ b/bin/gitIsAncestorOf @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,182 +91,17 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true - - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi - - ((argLength = ${#arg})) || true - - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - # @description Log namespace provides 2 kind of functions # - Log::display* allows to display given message with # given display level @@ -295,51 +132,202 @@ export __VERBOSE_LEVEL_DEBUG=2 # @description verbose level info export __VERBOSE_LEVEL_TRACE=3 -# @description Display message using debug color (grey) +# @description Display message using info color (bg light blue/fg white) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayDebug() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi Log::logDebug "$1" } +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + # @description Display message using error color (red) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayError() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' fi - Log::logInfo "$1" "${type}" } -# @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 +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) fi - Log::logWarning "$1" + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo } # @description Display message using error color (red) and exit immediately with error status 1 # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::fatal() { - echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + # @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 @@ -362,114 +350,156 @@ Log::requireLoad() { 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 + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 + 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 +} + +# @description concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + fi - fi + ((argLength = ${#arg})) || true - 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 -} + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi -# @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}" -} + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -478,17 +508,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -502,6 +521,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -510,6 +537,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -581,32 +620,50 @@ Linux::requireExecutedAsUser() { fi } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -617,6 +674,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -625,18 +690,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -666,14 +738,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -682,10 +746,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -695,28 +760,26 @@ Log::rotate() { fi } +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 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" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi } @@ -941,7 +1004,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -968,7 +1031,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1313,7 +1376,7 @@ gitIsAncestorOfCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "check if commit is inside a given branch")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "check if commit is inside a given branch" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/gitIsBranch b/bin/gitIsBranch index d0fae218..6a7c3c20 100755 --- a/bin/gitIsBranch +++ b/bin/gitIsBranch @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,182 +91,17 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true - - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi - - ((argLength = ${#arg})) || true - - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - # @description Log namespace provides 2 kind of functions # - Log::display* allows to display given message with # given display level @@ -295,51 +132,202 @@ export __VERBOSE_LEVEL_DEBUG=2 # @description verbose level info export __VERBOSE_LEVEL_TRACE=3 -# @description Display message using debug color (grey) +# @description Display message using info color (bg light blue/fg white) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayDebug() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi Log::logDebug "$1" } +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + # @description Display message using error color (red) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayError() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' fi - Log::logInfo "$1" "${type}" } -# @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 +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) fi - Log::logWarning "$1" + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo } # @description Display message using error color (red) and exit immediately with error status 1 # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::fatal() { - echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 Log::logFatal "$1" exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + # @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 @@ -362,114 +350,156 @@ Log::requireLoad() { 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 + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 + 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 +} + +# @description concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + fi - fi + ((argLength = ${#arg})) || true - 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 -} + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi -# @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}" -} + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -478,17 +508,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -502,6 +521,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -510,6 +537,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -581,32 +620,50 @@ Linux::requireExecutedAsUser() { fi } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -617,6 +674,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -625,18 +690,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -666,14 +738,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -682,10 +746,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -695,28 +760,26 @@ Log::rotate() { fi } +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 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" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi } @@ -940,7 +1003,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -967,7 +1030,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1296,7 +1359,7 @@ gitIsBranchCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "show an error if branchName is not a known branch")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "show an error if branchName is not a known branch" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/gitRenameBranch b/bin/gitRenameBranch index f6471c78..b9026f1c 100755 --- a/bin/gitRenameBranch +++ b/bin/gitRenameBranch @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -189,216 +465,56 @@ Array::wrap2() { if ((isNewline == 0)); then # isNewline = 0 means currentLine is not empty printCurrentLine - fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @description check if tty (interactive mode) is active -# @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} + fi + continue + fi -# @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 ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" - 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 + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" 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 + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine fi + ) | sed -E -e 's/[[:blank:]]+$//' +} +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 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 + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 fi + tty -s } # @description Ask user to enter y or n, retry until answer is correct @@ -423,110 +539,12 @@ UI::askYesNo() { 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}" -} - -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi -} - bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( cat <<'EOF' # shellcheck disable=SC2034 # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -540,6 +558,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -548,6 +574,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -619,16 +657,50 @@ Linux::requireExecutedAsUser() { fi } -# @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}" +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" fi - done + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi +} + +# @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 } # @description log message to file @@ -639,6 +711,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -653,14 +733,6 @@ Log::logFatal() { Log::logMessage "${2:-FATAL}" "$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 -} - # @description Internal: common log message # @example text # [date]|[levelMsg]|message @@ -688,14 +760,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -704,10 +768,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -717,28 +782,26 @@ Log::rotate() { fi } +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 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" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi } @@ -969,7 +1032,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -996,7 +1059,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1380,7 +1443,7 @@ gitRenameBranchCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "rename git local branch, push new branch and delete old branch")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "rename git local branch, push new branch and delete old branch" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/installRequirements b/bin/installRequirements index 1c5bb714..7475530f 100755 --- a/bin/installRequirements +++ b/bin/installRequirements @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,223 +91,17 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @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 it can. -# - if an arg can be placed on current line it will be, -# otherwise current line is printed and arg is added to the new -# current line -# - Empty arg is interpreted as a new line. -# - Add \r to arg in order to force break line and avoid following -# arg to be concatenated with current arg. -# -# @arg $1 glue:String -# @arg $2 maxLineLength:int -# @arg $3 indentNextLine:int -# @arg $@ array:String[] -Array::wrap2() { - local glue="${1-}" - local -i glueLength="${#glue}" - shift || true - local -i maxLineLength=$1 - shift || true - local -i indentNextLine=$1 - shift || true - local indentStr="" - if ((indentNextLine > 0)); then - indentStr="$(head -c "${indentNextLine}" 0)); do - arg="$1" - shift || true - - # replace tab by 2 spaces - arg="${arg//$'\t'/ }" - # remove trailing spaces - arg="${arg%[[:blank:]]}" - if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then - printCurrentLine - ((previousLineEmpty = 1)) - continue - else - if ((previousLineEmpty == 1)); then - printCurrentLine - fi - ((previousLineEmpty = 0)) || true - fi - # convert eol to args - mapfile -t additionalLines <<<"${arg}" - if ((${#additionalLines[@]} > 1)); then - set -- "${additionalLines[@]}" "$@" - continue - fi - - ((argLength = ${#arg})) || true - - # empty arg - if ((argLength == 0)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @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 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 - local repo="$1" - shift || true - local cloneCallback=${1:-} - shift || true - local pullCallback=${1:-} - shift || true - - if [[ -d "${dir}/.git" ]]; then - if ! Git::pullIfNoChanges "${dir}"; then - return 1 - fi - # shellcheck disable=SC2086 - if [[ "$(type -t ${pullCallback})" = "function" ]]; then - ${pullCallback} "${dir}" - fi - else - Log::displayInfo "cloning ${repo} ..." - mkdir -p "$(dirname "${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 -} - # @description Log namespace provides 2 kind of functions # - Log::display* allows to display given message with # given display level @@ -336,101 +132,53 @@ 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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayInfo() { local type="${2:-INFO}" if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi Log::logInfo "$1" "${type}" } +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + # @description Display message using warning color (yellow) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayWarning() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi Log::logWarning "$1" } -# @description Display message using error color (red) and exit immediately with error status 1 +# @description Display message using error color (red) # @arg $1 message:String the message to display -Log::fatal() { - echo -e "${__ERROR_COLOR}FATAL - ${1}${__RESET_COLOR}" >&2 - Log::logFatal "$1" - exit 1 -} - -# @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 - - 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 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi -} - -# @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}" + Log::logError "$1" } # @description load colors theme constants @@ -477,7 +225,7 @@ UI::theme() { __SUCCESS_COLOR='\e[32m' # Green __WARNING_COLOR='\e[33m' # Yellow __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey + __DEBUG_COLOR='\e[37m' # Gray __HELP_COLOR='\e[7;49;33m' # Black on Gold __TEST_COLOR='\e[100m' # Light magenta __TEST_ERROR_COLOR='\e[41m' # white on red @@ -492,24 +240,315 @@ UI::theme() { # shellcheck disable=SC2155,SC2034 __HELP_NORMAL="$(echo -e "\033[0m")" else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. +# - if an arg can be placed on current line it will be, +# otherwise current line is printed and arg is added to the new +# current line +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. +# +# @arg $1 glue:String +# @arg $2 maxLineLength:int +# @arg $3 indentNextLine:int +# @arg $@ array:String[] +Array::wrap2() { + local glue="${1-}" + local -i glueLength="${#glue}" + shift || true + local -i maxLineLength=$1 + shift || true + local -i indentNextLine=$1 + shift || true + local indentStr="" + if ((indentNextLine > 0)); then + indentStr="$(head -c "${indentNextLine}" 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then + printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true + fi + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue + fi + + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' +} + +# @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 +# @env SUDO String allows to use custom sudo prefix command +# @exitcode 0 on successful pulling/cloning, 1 on failure +Git::cloneOrPullIfNoChanges() { + local dir="$1" + shift || true + local repo="$1" + shift || true + local cloneCallback=${1:-} + shift || true + local pullCallback=${1:-} + shift || true + + if [[ -d "${dir}/.git" ]]; then + local exitCode=0 + Git::pullIfNoChanges "${dir}" || exitCode=$? + if Array::contains "${exitCode}" "2" "4"; then + # changes detected + return 0 + fi + if [[ "${exitCode}" != "0" ]]; then + return "${exitCode}" + fi + # shellcheck disable=SC2086 + if [[ "$(type -t ${pullCallback})" = "function" ]]; then + ${pullCallback} "${dir}" + fi + else + Log::displayInfo "cloning ${repo} ..." + if ! ${SUDO:-} test -d "${dir%/*}"; then + ${SUDO:-} mkdir -p "${dir%/*}" + fi + # shellcheck disable=SC2086,SC2248 + if ${SUDO:-} 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 } @@ -519,17 +558,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -543,6 +571,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -551,6 +587,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -622,61 +670,50 @@ Linux::requireExecutedAsUser() { fi } -# @description check if tty (interactive mode) is active -# @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] -} +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" -# @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}" +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" fi - done + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" + fi } -# @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 +# @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 - ( - cd "${dir}" || return 3 - if ! git update-index --refresh &>/dev/null; then - Log::displayWarning "Impossible to update git index of '${dir}' - check if you have modified file" - return 4 - fi - if ! git diff-index --quiet HEAD --; then - Log::displayWarning "Pulling git repository '${dir}' avoided as changes detected" - return 2 - fi - Log::displayInfo "Pull git repository '${dir}' as no changes detected" - git pull --progress - ) } # @description log message to file @@ -687,6 +724,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -695,18 +740,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -736,14 +788,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -752,10 +796,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -765,21 +810,73 @@ Log::rotate() { fi } +# @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 +# @env SUDO String allows to use custom sudo prefix command +# @require Git::requireGitCommand +Git::pullIfNoChanges() { + local dir="$1" + if [[ ! -d "${dir}/.git" ]]; then + return 3 + fi + ( + cd "${dir}" || return 3 + if ! ${SUDO:-} git update-index --refresh &>/dev/null; then + Log::displayWarning "Impossible to update git index of '${dir}' - check if you have modified file" + return 4 + fi + if ! ${SUDO:-} git diff-index --quiet HEAD --; then + Log::displayWarning "Pulling git repository '${dir}' avoided as changes detected" + return 2 + fi + Log::displayInfo "Pull git repository '${dir}' as no changes detected" + ${SUDO:-} git pull --progress + ) +} + +# @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 +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command git is available @@ -811,14 +908,6 @@ Assert::commandExists() { return 0 } -# @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 -} - # FUNCTIONS facade_main_installRequirementssh() { @@ -841,8 +930,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Git::requireGitCommand Log::requireLoad +Git::requireGitCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir Linux::requireExecutedAsUser @@ -1040,7 +1129,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1067,7 +1156,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1374,7 +1463,7 @@ installRequirementsCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "installs requirements")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "installs requirements" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" diff --git a/bin/mysql2puml b/bin/mysql2puml index c5c6a79f..a91470cb 100755 --- a/bin/mysql2puml +++ b/bin/mysql2puml @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,32 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description check if an element is contained in an array +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 # -# @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 +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } done - return 1 } -# @description concat each element of an array with a separator +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -242,73 +502,12 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @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/ +# @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 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 - - 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 - } - - # 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 [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then - # from framework conf/ (including extension) - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 - - # from framework conf/ - testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 - fi - - # file not found - Log::displayError "conf file '${conf}' not found" - - return 1 -} - -# @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 / +# @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 @@ -333,266 +532,97 @@ Conf::getMergedList() { ) | sort | uniq } -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } +# @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 } -# @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" -} - -# @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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - +# @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 - 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}" + testAbs() { + local result + result="$(realpath -e "$1" 2>/dev/null)" + # shellcheck disable=SC2181 + if [[ "$?" = "0" && -f "${result}" ]]; then + echo "${result}" + return 0 fi - fi -} - -# @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}" -} + return 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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' + # 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 [[ -n "${FRAMEWORK_ROOT_DIR+xxx}" ]]; then + # from framework conf/ (including extension) + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}${extension}" && return 0 + + # from framework conf/ + testAbs "$(File::concatenatePath "${FRAMEWORK_ROOT_DIR}/conf" "${confFolder}")/${conf}" && return 0 fi + + # file not found + Log::displayError "conf file '${conf}' not found" + + return 1 +} + +# @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} + ' "$@" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -601,17 +631,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -625,6 +644,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -633,6 +660,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -695,79 +734,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @description list files of dir with given extension and display it as a list one by line -# -# @arg $1 dir:String the directory to list -# @arg $2 prefix:String the profile file prefix (default: "") -# @arg $3 ext:String the extension -# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') -# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') -# @stdout list of files without extension/directory -# @example text -# - default.local -# - default.remote -# - localhost-root -# @exitcode 1 if directory does not exists -Conf::list() { - local dir="$1" - local prefix="${2:-}" - local ext="${3}" - local findOptions="${4--type f}" - local indentStr="${5- - }" - - if [[ ! -d "${dir}" ]]; then - Log::displayError "Directory ${dir} does not exist" - fi - if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then - ext=".${ext}" +# @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 - ( - # 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 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 } # @description log message to file @@ -778,6 +788,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -786,18 +804,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -827,14 +852,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -843,10 +860,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -856,21 +874,74 @@ Log::rotate() { fi } +# @description list files of dir with given extension and display it as a list one by line +# +# @arg $1 dir:String the directory to list +# @arg $2 prefix:String the profile file prefix (default: "") +# @arg $3 ext:String the extension +# @arg $4 findOptions:String find options, eg: -type d (Default value: '-type f') +# @arg $5 indentStr:String the indentation can be any string compatible with sed not containing any / (Default value: ' - ') +# @stdout list of files without extension/directory +# @example text +# - default.local +# - default.remote +# - localhost-root +# @exitcode 1 if directory does not exists +Conf::list() { + local dir="$1" + local prefix="${2:-}" + local ext="${3}" + local findOptions="${4--type f}" + local indentStr="${5- - }" + + if [[ ! -d "${dir}" ]]; then + Log::displayError "Directory ${dir} does not exist" + fi + if [[ -n "${ext}" && "${ext:0:1}" != "." ]]; then + ext=".${ext}" + fi + ( + # shellcheck disable=SC2086 + cd "${dir}" && + find . -maxdepth 1 ${findOptions} -name "${prefix}*${ext}" | + sed -E "s#^\./${prefix}##g" | + sed -E "s#${ext}\$##g" | sort | sed -E "s#^#${indentStr}#" + ) +} + +# @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 +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description ensure command realpath is available @@ -902,14 +973,6 @@ Assert::commandExists() { return 0 } -# @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 -} - # FUNCTIONS facade_main_mysql2pumlsh() { @@ -932,8 +995,8 @@ fi # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireRealpathCommand Log::requireLoad +Linux::requireRealpathCommand BashTools::Conf::requireLoad Compiler::Facade::requireCommandBinDir @@ -1131,7 +1194,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1158,7 +1221,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1504,7 +1567,7 @@ mysql2pumlCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "convert mysql dump sql schema to plantuml format")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "convert mysql dump sql schema to plantuml format" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/postmanCli b/bin/postmanCli index e4f6a122..b110b402 100755 --- a/bin/postmanCli +++ b/bin/postmanCli @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,227 +502,10 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @description validate arguments before calling Postman::Commands::pullCollections -# @arg $1 modelFile:String model file containing the collections to be pulled -# @arg $@ list of collection references to pull (all if not provided) -# @stderr diagnostic logs -# @exitcode * if one of sub commands fails -Postman::Commands::pullCommand() { - local modelFile="$1" - shift || true - - Postman::Model::validate "${modelFile}" "pull" || return 1 - - local -a refs - # shellcheck disable=SC2154 - Postman::Model::getCollectionRefs "${modelFile}" refs || return 1 - if (($# > 0)); then - # shellcheck disable=SC2154 - Postman::Model::checkIfValidCollectionRefs "${modelFile}" refs "$@" || return 1 - refs=("$@") - fi - - if ((${#refs} == 0)); then - Log::displayError "No collection refs to pull" - return 1 - else - Log::displayDebug "Collection refs to pull ${refs[*]}" - Postman::Commands::pullCollections "${modelFile}" "${refs[@]}" || return 1 - fi -} - -# @description validate arguments before calling Postman::Commands::pushCollections -# @arg $1 modelFile:String model file containing the collections to be pushed -# @arg $@ list of collection references to push (all if not provided) -# @stderr diagnostic logs -# @exitcode * if one of sub commands fails -Postman::Commands::pushCommand() { - local modelFile="$1" - shift || true - - Postman::Model::validate "${modelFile}" "push" || return 1 - - local -a refs - # shellcheck disable=SC2154 - Postman::Model::getCollectionRefs "${modelFile}" refs || return 1 - if (($# > 0)); then - # shellcheck disable=SC2154 - Postman::Model::checkIfValidCollectionRefs "${modelFile}" refs "$@" || return 1 - refs=("$@") - fi - - if ((${#refs} == 0)); then - Log::displayError "No collection refs to push" - return 1 - else - Log::displayDebug "Collection refs to push ${refs[*]}" - Postman::Commands::pushCollections "${modelFile}" "${refs[@]}" || return 1 - fi -} - -# @description validates the model file and checks for file existence -# @arg $1 optionModelFile:String the model file to validate -# @arg $2 mode:Enum(pull|push|config) eg: pull/config don't check for file existence -# @exitcode 1 if file optionModelFile does not exists or invalid +# @description validates the model file and checks for file existence +# @arg $1 optionModelFile:String the model file to validate +# @arg $2 mode:Enum(pull|push|config) eg: pull/config don't check for file existence +# @exitcode 1 if file optionModelFile does not exists or invalid # @stderr diagnostics information is displayed Postman::Model::validate() { local modelFile="$1" @@ -554,90 +613,61 @@ Postman::Model::validate() { ((errorCount == 0)) } -# @description draw a line with the character passed in parameter repeated depending on terminal width -# @arg $1 character:String character to use as separator (default value #) -UI::drawLine() { - local character="${1:-#}" - printf '%*s\n' "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo)}" '' | tr ' ' "${character}" +# @description validate arguments before calling Postman::Commands::pullCollections +# @arg $1 modelFile:String model file containing the collections to be pulled +# @arg $@ list of collection references to pull (all if not provided) +# @stderr diagnostic logs +# @exitcode * if one of sub commands fails +Postman::Commands::pullCommand() { + local modelFile="$1" + shift || true + + Postman::Model::validate "${modelFile}" "pull" || return 1 + + local -a refs + # shellcheck disable=SC2154 + Postman::Model::getCollectionRefs "${modelFile}" refs || return 1 + if (($# > 0)); then + # shellcheck disable=SC2154 + Postman::Model::checkIfValidCollectionRefs "${modelFile}" refs "$@" || return 1 + refs=("$@") + fi + + if ((${#refs} == 0)); then + Log::displayError "No collection refs to pull" + return 1 + else + Log::displayDebug "Collection refs to pull ${refs[*]}" + Postman::Commands::pullCollections "${modelFile}" "${refs[@]}" || return 1 + fi } -# @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 -# shellcheck disable=SC2034 -UI::theme() { - local theme="${1-default}" - if [[ ! "${theme}" =~ -force$ ]] && ! Assert::tty; then - theme="noColor" +# @description validate arguments before calling Postman::Commands::pushCollections +# @arg $1 modelFile:String model file containing the collections to be pushed +# @arg $@ list of collection references to push (all if not provided) +# @stderr diagnostic logs +# @exitcode * if one of sub commands fails +Postman::Commands::pushCommand() { + local modelFile="$1" + shift || true + + Postman::Model::validate "${modelFile}" "push" || return 1 + + local -a refs + # shellcheck disable=SC2154 + Postman::Model::getCollectionRefs "${modelFile}" refs || return 1 + if (($# > 0)); then + # shellcheck disable=SC2154 + Postman::Model::checkIfValidCollectionRefs "${modelFile}" refs "$@" || return 1 + refs=("$@") fi - case "${theme}" in - default | default-force) - theme="default" - ;; - noColor) ;; - *) - Log::fatal "invalid theme provided" - ;; - esac - if [[ "${theme}" = "default" ]]; then - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" + + if ((${#refs} == 0)); then + Log::displayError "No collection refs to push" + return 1 else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' + Log::displayDebug "Collection refs to push ${refs[*]}" + Postman::Commands::pushCollections "${modelFile}" "${refs[@]}" || return 1 fi } @@ -647,17 +677,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -671,6 +690,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -679,6 +706,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -741,48 +780,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_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 -} +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" -# @description check if tty (interactive mode) is active +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -793,6 +834,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -801,18 +850,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -842,14 +898,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -858,10 +906,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -871,6 +920,72 @@ Log::rotate() { fi } +# @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 +} + +# @description config directory path relative to current execution directory +# @arg $1 configFile:String the config file +# @stdout the parent directory of config file relative to current execution directory +# @example +# executionPath=/home/wsl/bash-tools +# configPath=/home/wsl/bash-tools/conf/postmanCli/openApis.json +# result=conf/postmanCli +Postman::Model::getRelativeConfigDirectory() { + local configFile="$1" + local configDir + configDir="$(cd -- "$(dirname -- "${configFile}")" &>/dev/null && pwd -P)" + File::relativeToDir "${configDir}" "$(pwd -P)" +} + +# @description get the list of collection references id from given config file +# @arg $1 configFile:String the config file to parse +# @arg $2 getCollectionRefs:&String[] (passed by reference) list of collection +# references +# @exitcode 1 - if jq parsing error, file not found or any other error +# @stderr jq error messages on failure +Postman::Model::getCollectionRefs() { + local configFile="$1" + local -n getCollectionRefs=$2 + # shellcheck disable=SC2034 + jq -cre '.collections | try keys[]' <"${configFile}" | readarray -t getCollectionRefs +} + +# @description check that each collection references passed as parameter +# exists in the model file +# @arg $1 modelFile:String model file in which availableRefs have been retrieved +# @arg $2 availableRefs:&String[] list of known collection references +# @arg $3 modelCollectionRefs:&String[] list of collection references to check +Postman::Model::checkIfValidCollectionRefs() { + local modelFile="$1" + local -n availableRefs=$2 + shift 2 || true + local -a modelCollectionRefs=("$@") + + # shellcheck disable=SC2154 + Log::displayDebug "Checking collection refs using config ${modelFile}" + + local ref + for ref in "${modelCollectionRefs[@]}"; do + if ! Array::contains "${ref}" "${availableRefs[@]}"; then + Log::displayError "Collection ref '${ref}' is not known in '${modelFile}'" + return 1 + fi + done +} + # @description pull collections specified by modelFile # @arg $1 modelFile:String model file containing the collections to be pulled # @arg $@ list of collection references to pull (all if not provided) @@ -944,72 +1059,37 @@ Postman::Commands::pushCollections() { Log::displayInfo "Creating collection '${collectionRef}'" Postman::api createCollectionFromFile "${collectionFile}" Log::displaySuccess "collection '${collectionRef}' has been created successfully" - else - Log::displayInfo "Updating collection '${collectionRef}' with id '${postmanCollectionId}'" - Postman::api updateCollectionFromFile "${collectionFile}" "${postmanCollectionId}" - Log::displaySuccess "collection '${collectionRef}' has been updated successfully" - fi - } - - Postman::Commands::forEachCollection "${modelFile}" pushCollectionsCallback "$@" -} - -# @description check that each collection references passed as parameter -# exists in the model file -# @arg $1 modelFile:String model file in which availableRefs have been retrieved -# @arg $2 availableRefs:&String[] list of known collection references -# @arg $3 modelCollectionRefs:&String[] list of collection references to check -Postman::Model::checkIfValidCollectionRefs() { - local modelFile="$1" - local -n availableRefs=$2 - shift 2 || true - local -a modelCollectionRefs=("$@") - - # shellcheck disable=SC2154 - Log::displayDebug "Checking collection refs using config ${modelFile}" - - local ref - for ref in "${modelCollectionRefs[@]}"; do - if ! Array::contains "${ref}" "${availableRefs[@]}"; then - Log::displayError "Collection ref '${ref}' is not known in '${modelFile}'" - return 1 - fi - done -} - -# @description get the list of collection references id from given config file -# @arg $1 configFile:String the config file to parse -# @arg $2 getCollectionRefs:&String[] (passed by reference) list of collection -# references -# @exitcode 1 - if jq parsing error, file not found or any other error -# @stderr jq error messages on failure -Postman::Model::getCollectionRefs() { - local configFile="$1" - local -n getCollectionRefs=$2 - # shellcheck disable=SC2034 - jq -cre '.collections | try keys[]' <"${configFile}" | readarray -t getCollectionRefs -} - -# @description config directory path relative to current execution directory -# @arg $1 configFile:String the config file -# @stdout the parent directory of config file relative to current execution directory -# @example -# executionPath=/home/wsl/bash-tools -# configPath=/home/wsl/bash-tools/conf/postmanCli/openApis.json -# result=conf/postmanCli -Postman::Model::getRelativeConfigDirectory() { - local configFile="$1" - local configDir - configDir="$(cd -- "$(dirname -- "${configFile}")" &>/dev/null && pwd -P)" - File::relativeToDir "${configDir}" "$(pwd -P)" + else + Log::displayInfo "Updating collection '${collectionRef}' with id '${postmanCollectionId}'" + Postman::api updateCollectionFromFile "${collectionFile}" "${postmanCollectionId}" + Log::displaySuccess "collection '${collectionRef}' has been updated successfully" + fi + } + + Postman::Commands::forEachCollection "${modelFile}" pushCollectionsCallback "$@" +} + +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" + fi } # @description print the resolved path relative to DIR @@ -1024,78 +1104,6 @@ File::relativeToDir() { realpath -m --relative-to="${relativeTo}" "${srcFile}" } -# @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 success color (bg green/fg white) -# @arg $1 message:String the message to display -Log::displaySuccess() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__SUCCESS_COLOR}SUCCESS - ${1}${__RESET_COLOR}" >&2 - fi - Log::logSuccess "$1" -} - -# @description apply callback on each collection specified by modelFile -# #### callback arguments -# - modelFile -# - postmanCollectionsFile -# - collectionRef -# - collectionFile -# - collectionName -# - postmanCollectionId -# - postmanCollectionIdStatus -# @arg $1 modelFile:String model file containing the collections to be processed -# @arg $2 callback:Function callback to apply on each collection selected -# @arg $@ list of collection references to process (all if not provided) -# @stderr diagnostic logs -# @exitcode 2 if no refs specified -# @exitcode * if one of sub commands fails -Postman::Commands::forEachCollection() { - local modelFile="$1" - local callback="$2" - shift 2 || true - local -a refs=("$@") - - if ((${#refs[@]} == 0)); then - return 2 - fi - - Postman::checkApiKey "${HOME}/.bash-tools/.env" || return 1 - local postmanCollectionsFile - postmanCollectionsFile="$(Framework::createTempFile "postmanCollections")" - # shellcheck disable=SC2154 - Log::displayDebug "Retrieving collections from postman in ${postmanCollectionsFile}" - Postman::api getCollections >"${postmanCollectionsFile}" || return 1 - - local collectionRef - for collectionRef in "${refs[@]}"; do - local collectionFile collectionName postmanCollectionId - Log::displayDebug "Retrieving collection file from collection reference ${collectionRef}" - collectionFile="$(Postman::Model::getCollectionFileByRef "${modelFile}" "${collectionRef}")" - Log::displayDebug "Retrieving collection name from collection file ${collectionFile}" - collectionName="$(Postman::Collection::getName "${collectionFile}")" - Log::displayDebug "Deducing postman collection id using ${postmanCollectionsFile} and collection name '${collectionName}'" - local postmanCollectionIdStatus="0" - postmanCollectionId="$(Postman::Collection::getCollectionIdByName "${postmanCollectionsFile}" "${collectionName}")" || status=$? - local status=0 - "${callback}" \ - "${modelFile}" "${postmanCollectionsFile}" \ - "${collectionRef}" "${collectionFile}" "${collectionName}" \ - "${postmanCollectionId}" "${postmanCollectionIdStatus}" || status=$? - case "${status}" in - 2 | 0) continue ;; - *) return 1 ;; - esac - done -} - # @description call postman REST api # @arg $1 action:String action to call # @arg $@ args:String[] rest of arguments @@ -1195,6 +1203,72 @@ Postman::api() { esac } +# @description Display message using success color (bg green/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displaySuccess() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__SUCCESS_COLOR}SUCCESS - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logSuccess "$1" +} + +# @description apply callback on each collection specified by modelFile +# #### callback arguments +# - modelFile +# - postmanCollectionsFile +# - collectionRef +# - collectionFile +# - collectionName +# - postmanCollectionId +# - postmanCollectionIdStatus +# @arg $1 modelFile:String model file containing the collections to be processed +# @arg $2 callback:Function callback to apply on each collection selected +# @arg $@ list of collection references to process (all if not provided) +# @stderr diagnostic logs +# @exitcode 2 if no refs specified +# @exitcode * if one of sub commands fails +Postman::Commands::forEachCollection() { + local modelFile="$1" + local callback="$2" + shift 2 || true + local -a refs=("$@") + + if ((${#refs[@]} == 0)); then + return 2 + fi + + Postman::checkApiKey "${HOME}/.bash-tools/.env" || return 1 + local postmanCollectionsFile + postmanCollectionsFile="$(Framework::createTempFile "postmanCollections")" + # shellcheck disable=SC2154 + Log::displayDebug "Retrieving collections from postman in ${postmanCollectionsFile}" + Postman::api getCollections >"${postmanCollectionsFile}" || return 1 + + local collectionRef + for collectionRef in "${refs[@]}"; do + local collectionFile collectionName postmanCollectionId + Log::displayDebug "Retrieving collection file from collection reference ${collectionRef}" + collectionFile="$(Postman::Model::getCollectionFileByRef "${modelFile}" "${collectionRef}")" + Log::displayDebug "Retrieving collection name from collection file ${collectionFile}" + collectionName="$(Postman::Collection::getName "${collectionFile}")" + Log::displayDebug "Deducing postman collection id using ${postmanCollectionsFile} and collection name '${collectionName}'" + local postmanCollectionIdStatus="0" + postmanCollectionId="$(Postman::Collection::getCollectionIdByName "${postmanCollectionsFile}" "${collectionName}")" || status=$? + local status=0 + "${callback}" \ + "${modelFile}" "${postmanCollectionsFile}" \ + "${collectionRef}" "${collectionFile}" "${collectionName}" \ + "${postmanCollectionId}" "${postmanCollectionIdStatus}" || status=$? + case "${status}" in + 2 | 0) continue ;; + *) return 1 ;; + esac + done +} + # @description ignore exit code 141 from simple command pipes # @example use with: # local resultingStatus=0 @@ -1225,11 +1299,19 @@ Bash::handlePipelineFailure() { return "${handlePipelineFailure_resultingStatusCode}" } -# @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" +# @description display curl response only if verbose mode is not off +# @arg $1 type:String type of response displayed +# @arg $2 responseFile:String file containing curl response +# @env BASH_FRAMEWORK_ARGS_VERBOSE +# @exitcode 1 if responseFile not found +Postman::displayResponse() { + local type="$1" + local responseFile="$2" + if ((BASH_FRAMEWORK_ARGS_VERBOSE > __VERBOSE_LEVEL_OFF)); then + (UI::drawLine >&2 "-") + (echo >&2 -e "${__DEBUG_COLOR}${type}${__RESET_COLOR}") + (cat >&2 "${responseFile}") + (echo >&2) fi } @@ -1241,6 +1323,43 @@ Log::logSuccess() { fi } +# @description check if postman api key is set in .env file +# @arg $1 envFile:String .env file that should contain POSTMAN_API_KEY variable +# @stderr display warning message if postman api key is not filled +# @exitcode 0 always successful +Postman::checkApiKey() { + local envFile="$1" + + if grep -q '^POSTMAN_API_KEY=$' "${envFile}" 2>/dev/null || + ! grep -q '^POSTMAN_API_KEY=' "${envFile}" 2>/dev/null; then + Log::displayWarning "Please update POSTMAN_API_KEY in '${envFile}'" + fi +} + +# @description retrieve the file associated to the collection ref +# @arg $1 configFile:String the config file to parse +# @arg $2 ref:String the collection reference to get +# @stdout the file relative to current execution directory +# @exitcode 1 - if jq parsing error, file not found or any other error +Postman::Model::getCollectionFileByRef() { + local configFile="$1" + local ref="$2" + local file + file="$(jq -cre ".collections.${ref}.file" <"${configFile}")" || return 1 + echo "$(Postman::Model::getRelativeConfigDirectory "${configFile}")/${file}" +} + +# @description retrieve the name of the collection file +# from the postman collection file +# @arg $1 collectionFile:String +# @exitcode 1 if error while parsing the collection file +# @exitcode * jq exit code, 4 for invalid file +# @stdout the collection name of the collection file +Postman::Collection::getName() { + local collectionFile="$1" + jq -cre '.info.name' <"${collectionFile}" +} + # @description retrieve the collection id # associated to the given collection name # from the postman collection file @@ -1274,59 +1393,6 @@ Postman::Collection::getCollectionIdByName() { echo "${result}" } -# @description retrieve the name of the collection file -# from the postman collection file -# @arg $1 collectionFile:String -# @exitcode 1 if error while parsing the collection file -# @exitcode * jq exit code, 4 for invalid file -# @stdout the collection name of the collection file -Postman::Collection::getName() { - local collectionFile="$1" - jq -cre '.info.name' <"${collectionFile}" -} - -# @description retrieve the file associated to the collection ref -# @arg $1 configFile:String the config file to parse -# @arg $2 ref:String the collection reference to get -# @stdout the file relative to current execution directory -# @exitcode 1 - if jq parsing error, file not found or any other error -Postman::Model::getCollectionFileByRef() { - local configFile="$1" - local ref="$2" - local file - file="$(jq -cre ".collections.${ref}.file" <"${configFile}")" || return 1 - echo "$(Postman::Model::getRelativeConfigDirectory "${configFile}")/${file}" -} - -# @description check if postman api key is set in .env file -# @arg $1 envFile:String .env file that should contain POSTMAN_API_KEY variable -# @stderr display warning message if postman api key is not filled -# @exitcode 0 always successful -Postman::checkApiKey() { - local envFile="$1" - - if grep -q '^POSTMAN_API_KEY=$' "${envFile}" 2>/dev/null || - ! grep -q '^POSTMAN_API_KEY=' "${envFile}" 2>/dev/null; then - Log::displayWarning "Please update POSTMAN_API_KEY in '${envFile}'" - fi -} - -# @description display curl response only if verbose mode is not off -# @arg $1 type:String type of response displayed -# @arg $2 responseFile:String file containing curl response -# @env BASH_FRAMEWORK_ARGS_VERBOSE -# @exitcode 1 if responseFile not found -Postman::displayResponse() { - local type="$1" - local responseFile="$2" - if ((BASH_FRAMEWORK_ARGS_VERBOSE > __VERBOSE_LEVEL_OFF)); then - (UI::drawLine >&2 "-") - (echo >&2 -e "${__DEBUG_COLOR}${type}${__RESET_COLOR}") - (cat >&2 "${responseFile}") - (echo >&2) - fi -} - # FUNCTIONS facade_main_postmanClish() { @@ -1542,7 +1608,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1569,7 +1635,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1918,7 +1984,7 @@ postmanCliCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Push/Pull postman collections of all the configured repositories")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Push/Pull postman collections of all the configured repositories" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/upgradeGithubRelease b/bin/upgradeGithubRelease index fe69d18d..6b64b222 100755 --- a/bin/upgradeGithubRelease +++ b/bin/upgradeGithubRelease @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -226,6 +502,24 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } +# @description check if argument is a valid linux path +# invalid path are those with: +# - invalid characters +# - component beginning by a - (because could be considered as a command's option) +# - not beginning with a slash +# - relative +# +# @arg $1 path:string path that needs to be checked +# @exitcode 1 if path is invalid +# @see https://regex101.com/r/afLrmM/2 +# @see Assert::validPosixPath if you need more restrictive check +Assert::validPath() { + local path="$1" + + [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && + [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +} + # Checks if file can be created in folder # @@ -238,73 +532,70 @@ Array::wrap2() { # @see Assert::validPath Assert::fileWritable() { local file="$1" - local dir Assert::validPath "${file}" || return 1 if [[ -f "${file}" ]]; then [[ -w "${file}" ]] || return 3 else - dir="$(dirname "${file}")" - [[ -w "${dir}" ]] || return 2 + [[ -w "${file%/*}" ]] || return 2 fi } -# @description check if argument is a valid linux path -# invalid path are those with: -# - invalid characters -# - component beginning by a - (because could be considered as a command's option) -# - not beginning with a slash -# - relative -# -# @arg $1 path:string path that needs to be checked -# @exitcode 1 if path is invalid -# @see https://regex101.com/r/afLrmM/2 -# @see Assert::validPosixPath if you need more restrictive check -Assert::validPath() { - local path="$1" - - [[ "${path}" =~ ^\/$|^(\/[.a-zA-Z_0-9][.a-zA-Z_0-9-]*)+$ ]] && - [[ ! "${path}" =~ (\/\.\.)|(\.\.\/)|^\.$|^\.\.$ ]] # avoid relative +# @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() { + # match anything, print(p), exit on first match(Q) + sed -En \ + -e 's/\x1b\[[0-9;]*[mGKHF]//g' \ + -e 's/[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/' \ + -e '//{p;Q}' \ + "$@" } -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") +# @description upgrade given binary to latest github release using retry +# +# downloadReleaseUrl argument : the placeholder @latestVersion@ will be replaced by the latest release version +# @arg $1 targetFile:String target binary file (eg: /usr/local/bin/kind) +# @arg $2 downloadReleaseUrl:String github release url (eg: https://github.com/kubernetes-sigs/kind/releases/download/@latestVersion@/kind-linux-amd64) +# @arg $3 softVersionArg:String parameter to add to existing command to compute current version +# @arg $4 softVersionCallback:Function function called to get software version (default: Version::getCommandVersionFromPlainText will call software with argument --version) +# @arg $5 installCallback:Function function called to install the file retrieved on github (default copy as is and set execution bit) +# @arg $6 softVersionCallback:Function function to call to filter the version retrieved from github (Default: Version::parse) +# @stdout log messages about retry, install, upgrade +# @env SOFT_VERSION_CALLBACK pass softVersionCallback by env variable instead of passing it by arg +# @env INSTALL_CALLBACK pass installCallback by env variable instead of passing it by arg +# @env CURL_CONNECT_TIMEOUT number of seconds before giving up host connection +# @env EXACT_VERSION if provided, retrieve exact version instead of the latest +Github::upgradeRelease() { + local targetFile="$1" + local downloadReleaseUrl="$2" + local softVersionArg="${3:---version}" + local softVersionCallback="${4:-${SOFT_VERSION_CALLBACK:-Version::getCommandVersionFromPlainText}}" + # shellcheck disable=SC2034 + local installCallback="${5:-${INSTALL_CALLBACK:-}}" + local parseGithubVersionCallback="${6:-${PARSE_VERSION_CALLBACK:-Version::parse}}" - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} + local repo + repo="$(Github::extractRepoFromGithubUrl "${downloadReleaseUrl}")" + local releasesUrl="https://api.github.com/repos/${repo}/releases/latest" -# @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" + # shellcheck disable=SC2317 + extractVersion() { + Version::githubApiExtractVersion | "${parseGithubVersionCallback}" + } + FILTER_LAST_VERSION_CALLBACK=${FILTER_LAST_VERSION_CALLBACK:-extractVersion} \ + SOFT_VERSION_CALLBACK="${softVersionCallback}" \ + Web::upgradeRelease \ + "${targetFile}" \ + "${releasesUrl}" \ + "${downloadReleaseUrl}" \ + "${softVersionArg}" \ + "${EXACT_VERSION:-}" } # @description intermediate callback that is used by Github::upgradeRelease @@ -320,6 +611,7 @@ Framework::createTempFile() { # @arg $2 targetFile:String where we want to copy the file # @arg $3 version:String the version that has been downloaded # @arg $4 installCallback:Function (optional) the callback to call with 3 first arguments +# @env SUDO String allows to use custom sudo prefix command # @exitcode * on failure # @see Github::upgradeRelease # @see Github::installRelease @@ -330,328 +622,18 @@ Github::defaultInstall() { local version="$3" local installCallback=$4 # shellcheck disable=SC2086 - mkdir -p "$(dirname "${targetFile}")" + if ! ${SUDO:-} test -d "${targetFile%/*}"; then + ${SUDO:-} mkdir -p "${targetFile%/*}" + fi if [[ "$(type -t "${installCallback}")" = "function" ]]; then ${installCallback} "${newSoftware}" "${targetFile}" "${version}" else - mv "${newSoftware}" "${targetFile}" - chmod +x "${targetFile}" + ${SUDO:-} mv "${newSoftware}" "${targetFile}" + ${SUDO:-} chmod +x "${targetFile}" hash -r - rm -f "${newSoftware}" || true - fi -} - -# @description download specified release software version from github -# @arg $1 releaseUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 -# @exitcode 1 on failure -# @stdout the path to the downloaded release -Github::downloadReleaseVersion() { - local releaseUrl="$1" - local newSoftwarePath - - # download specified version - newSoftwarePath=$(mktemp -p "${TMPDIR:-/tmp}" -t github.newSoftware.XXXX) - Retry::default curl \ - -L \ - -o "${newSoftwarePath}" \ - --fail \ - "${releaseUrl}" || return 1 - echo "${newSoftwarePath}" -} - -# @description Retrieve the latest version number for given github url -# @arg $1 releaseUrl:String github url from which repository will be extracted -# @stderr log messages about retry -# @stdout the version number retrieved -Github::getLatestVersionFromUrl() { - local releaseUrl="$1" - local repo - local latestVersion - # extract repo from github url - repo="$(Github::extractRepoFromGithubUrl "${releaseUrl}")" || return 1 - - # get latest release version - if ! Github::getLatestRelease "${repo}" latestVersion; then - Log::displayError "Repository ${repo} latest version not found" - return 1 + ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" fi - Log::displayInfo "Repo ${repo} latest version found is ${latestVersion}" - echo "${latestVersion}" -} - -# @description check if specified release software version exists in github -# @arg $1 releaseUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 -# @exitcode 1 on failure -# @exitcode 0 if release version exists -Github::isReleaseVersionExist() { - local releaseUrl="$1" - - curl \ - -L \ - -o /dev/null \ - --silent \ - --head \ - --fail \ - "${releaseUrl}" -} - -# @description Log namespace provides 2 kind of functions -# - Log::display* allows to display given message with -# given display level -# - Log::log* allows to log given message with -# given log level -# Log::display* functions automatically log the message too -# @see Env::requireLoad to load the display and log level from .env file - -# @description log level off -export __LEVEL_OFF=0 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @description Display message using info color (blue) but warning level -# @arg $1 message:String the message to display -Log::displayStatus() { - local type="${2:-STATUS}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logStatus "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - 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 -} - -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi -} - -# @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 -} - -# @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 } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -660,17 +642,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -684,6 +655,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -692,6 +671,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -749,80 +740,55 @@ BashTools::Conf::requireLoad() { # @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 -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 - fi - [[ -t 1 || -t 2 ]] +Compiler::Facade::requireCommandBinDir() { + COMMAND_BIN_DIR="${CURRENT_DIR}" + Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @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 -} +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" -# @description github repository eg: kubernetes-sigs/kind -# @arg $1 githubUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/@latestVersion@/kind-linux-amd64 -# @exitcode 1 if no matching repo found in provided url, 0 otherwise -# @stdout the repo in the form owner/repo -Github::extractRepoFromGithubUrl() { - local githubUrl="$1" - local result - result="$(sed -n -E 's#^https://github.com/([^/]+/[^/]+)/.*$#\1#p' <<<"${githubUrl}")" - if [[ -z "${result}" ]]; then - return 1 +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - echo "${result}" } -# @description Retrieve the latest version number of a github release using Github API using retry -# repo arg with fchastanet/bash-tools value would match https://github.com/fchastanet/bash-tools -# @arg $1 repo:String repository in the format fchastanet/bash-tools -# @arg $2 resultRef:&String reference to a variable that will contain the result of the command -# @stdout log messages about retry -Github::getLatestRelease() { - local repo="$1" - # we need to pass the result through a reference instead of output directly - # because retry can output too - local -n resultRef=$2 - resultRef="" - local resultFile - resultFile="$(mktemp -p "${TMPDIR:-/tmp}" -t githubLatestRelease.XXXX)" - # Get latest release from GitHub api - if Retry::default curl \ - -L \ - -o "${resultFile}" \ - --fail \ - --silent \ - "https://api.github.com/repos/${repo}/releases/latest"; then - # shellcheck disable=SC2034 - resultRef="$(Version::githubApiExtractVersion <"${resultFile}")" - return 0 +# @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 - # display curl result in case of failure - cat >&2 "${resultFile}" - rm -f "${resultFile}" } # @description log message to file @@ -833,6 +799,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -841,18 +815,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -882,22 +863,6 @@ Log::logMessage() { fi } -# @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 -} - -# @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 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 @@ -906,10 +871,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -919,29 +885,197 @@ Log::rotate() { fi } -# @description Retry a command 5 times with a delay of 15 seconds between each attempt -# @arg $@ command:String[] the command to run -# @exitcode 0 on success -# @exitcode 1 if max retries count reached -Retry::default() { - Retry::parameterized 5 15 "" "$@" +# @description extract software version number +# @arg $1 command:String the command that will be called with --version parameter +# @arg $2 argVersion:String allows to override default --version parameter +Version::getCommandVersionFromPlainText() { + local command="$1" + local argVersion="${2:---version}" + "${command}" "${argVersion}" 2>&1 | + Version::parse # keep only version numbers +} + +# @description github repository eg: kubernetes-sigs/kind +# @arg $1 githubUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/@latestVersion@/kind-linux-amd64 +# @exitcode 1 if no matching repo found in provided url, 0 otherwise +# @stdout the repo in the form owner/repo +Github::extractRepoFromGithubUrl() { + local githubUrl="$1" + local result + result="$(sed -n -E 's#^https://github.com/([^/]+/[^/]+)/.*$#\1#p' <<<"${githubUrl}")" + if [[ -z "${result}" ]]; then + return 1 + fi + echo "${result}" +} + +# @description extract version number from github api +# @noargs +# @stdin json result of github API +# @exitcode 1 if jq or Version::parse fails +# @stdout the version parsed +# @require Linux::requireJqCommand +Version::githubApiExtractVersion() { + jq -r ".tag_name" +} + +# @description upgrade given binary to latest release using retry +# +# releasesUrl argument : the placeholder @latestVersion@ will be replaced by the latest release version +# @arg $1 targetFile:String target binary file (eg: /usr/local/bin/kind) +# @arg $2 releasesUrl:String url on which we can query all available versions (eg: "https://go.dev/dl/?mode=json") +# @arg $3 downloadReleaseUrl:String url from which the software will be downloaded (eg: https://storage.googleapis.com/golang/go@latestVersion@.linux-amd64.tar.gz) +# @arg $4 softVersionArg:String parameter to add to existing command to compute current version +# @arg $5 exactVersion:String if you want to retrieve a specific version instead of the latest +# @stdout log messages about retry, install, upgrade +# @env FILTER_LAST_VERSION_CALLBACK a callback to filter the latest version from releasesUrl +# @env SOFT_VERSION_CALLBACK a callback to execute command version +# @env PARSE_VERSION_CALLBACK a callback to parse the version of the existing command +# @env INSTALL_CALLBACK a callback to install the software downloaded +# @env CURL_CONNECT_TIMEOUT number of seconds before giving up host connection +Web::upgradeRelease() { + local targetFile="$1" + local releasesUrl="$2" + local downloadReleaseUrl="$3" + local softVersionArg="${4:---version}" + local exactVersion="${5:-}" + # options from env variables + local filterLastVersionCallback="${FILTER_LAST_VERSION_CALLBACK:-Version::parse}" + local softVersionCallback="${SOFT_VERSION_CALLBACK:-Version::getCommandVersionFromPlainText}" + local installCallback="${INSTALL_CALLBACK:-}" + local latestVersion + latestVersion="$(Web::getReleases "${releasesUrl}" | ${filterLastVersionCallback})" || { + Log::displayError "latest version not found on ${releasesUrl}" + return 1 + } + Log::displayInfo "Latest version found is ${latestVersion}" + + local currentVersion="not existing" + if [[ -f "${targetFile}" ]]; then + currentVersion="$(${softVersionCallback} "${targetFile}" "${softVersionArg}" 2>&1 || true)" + fi + if [[ -z "${exactVersion}" ]]; then + exactVersion="${latestVersion}" + fi + local url="${downloadReleaseUrl//@latestVersion@/${exactVersion}}" + if [[ -n "${exactVersion}" ]] && ! Github::isReleaseVersionExist "${url}"; then + Log::displayError "${targetFile} version ${exactVersion} doesn't exist on github" + return 2 + fi + if [[ "${currentVersion}" = "${exactVersion}" ]]; then + Log::displayInfo "${targetFile} version ${exactVersion} already installed" + else + if [[ -z "${currentVersion}" ]]; then + Log::displayInfo "Installing ${targetFile} with version ${exactVersion}" + else + Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${exactVersion}" + fi + Log::displayInfo "Using url ${url}" + newSoftware=$(mktemp -p "${TMPDIR:-/tmp}" -t web.newSoftware.XXXX) + Retry::default curl \ + -L \ + --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ + -o "${newSoftware}" \ + --fail \ + "${url}" + + Github::defaultInstall "${newSoftware}" "${targetFile}" "${exactVersion}" "${installCallback}" + fi +} + +# @description Display message using success color (bg green/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displaySuccess() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__SUCCESS_COLOR}SUCCESS - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logSuccess "$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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" + fi +} + +# @description Retrieve the latest version number of a web release +# @arg $1 releaseListUrl:String the url from which version list can be retrieved +# @stdout log messages about retry +# @env CURL_CONNECT_TIMEOUT number of seconds before giving up host connection +Web::getReleases() { + local releaseListUrl="$1" + # Get latest release from GitHub api + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "Retrieving release versions list ..." curl \ + -L \ + --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ + --fail \ + --silent \ + "${releaseListUrl}" +} + +# @description check if specified release software version exists in github +# @arg $1 releaseUrl:String eg: https://github.com/kubernetes-sigs/kind/releases/download/v1.0.0/kind-linux-amd64 +# @exitcode 1 on failure +# @exitcode 0 if release version exists +# @env CURL_CONNECT_TIMEOUT number of seconds before giving up host connection +Github::isReleaseVersionExist() { + local releaseUrl="$1" + + curl \ + -L \ + --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ + -o /dev/null \ + --silent \ + --head \ + --fail \ + "${releaseUrl}" +} + +# @description Retry a command 5 times with a delay of 15 seconds between each attempt +# @arg $@ command:String[] the command to run +# @exitcode 0 on success +# @exitcode 1 if max retries count reached +# @env RETRY_MAX_RETRY int max retries +# @env RETRY_DELAY_BETWEEN_RETRIES int delay between attempts +Retry::default() { + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "" "$@" } -# @description Display message using skip color (yellow) +# @description log message to file # @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 +Log::logSuccess() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SUCCESS}" "$1" + fi +} + +# @description ensure command jq is available +# @exitcode 1 if jq command not available +# @stderr diagnostics information is displayed +Linux::requireJqCommand() { + if [[ "${SKIP_REQUIRE_JQ:-0}" = "0" && "${SKIP_REQUIRES:-0}" = "0" ]]; then + Assert::commandExists jq fi - Log::logSkipped "$1" } # @description Retry a command several times depending on parameters @@ -970,7 +1104,7 @@ Retry::parameterized() { if "$@"; then break elif [[ "${retriesCount}" -lt "${maxRetries}" ]]; then - Log::displayWarning "Command failed. Wait for ${delayBetweenTries} seconds" + Log::displayDebug "Command failed. Wait for ${delayBetweenTries} seconds" ((retriesCount++)) sleep "${delayBetweenTries}" else @@ -981,31 +1115,6 @@ Retry::parameterized() { return 0 } -# @description extract version number from github api -# @noargs -# @stdin json result of github API -# @exitcode 1 if jq or Version::parse fails -# @stdout the version parsed -# @require Linux::requireJqCommand -Version::githubApiExtractVersion() { - jq -r ".tag_name" -} - -# @description log message to file -# @arg $1 message:String the message to display -Log::logSkipped() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then - Log::logMessage "${2:-SKIPPED}" "$1" - fi -} - -# @description ensure command jq is available -# @exitcode 1 if jq command not available -# @stderr diagnostics information is displayed -Linux::requireJqCommand() { - Assert::commandExists jq -} - # @description check if command specified exists or return 1 # with error and message if not # @@ -1066,7 +1175,6 @@ declare targetFileArg="" declare githubUrlPatternArg="" declare optionVersionArg="${defaultVersionArg}" declare optionCurrentVersion="" -declare optionMinimalVersion="" declare optionExactVersion="" # other values @@ -1259,7 +1367,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1286,7 +1394,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1326,8 +1434,6 @@ upgradeGithubReleaseCommand() { ((options_parse_optionParsedCountOptionCurrentVersion = 0)) || true local -i options_parse_optionParsedCountOptionExactVersion ((options_parse_optionParsedCountOptionExactVersion = 0)) || true - local -i options_parse_optionParsedCountOptionMinimalVersion - ((options_parse_optionParsedCountOptionMinimalVersion = 0)) || true local -i options_parse_optionParsedCountOptionBashFrameworkConfig ((options_parse_optionParsedCountOptionBashFrameworkConfig = 0)) || true optionConfig="0" @@ -1373,7 +1479,7 @@ upgradeGithubReleaseCommand() { local options_parse_arg="$1" local argOptDefaultBehavior=0 case "${options_parse_arg}" in - # Option 1/18 + # Option 1/17 # Option optionVersionArg --version-arg variableType String min 0 max 1 authorizedValues '' regexp '' --version-arg) shift @@ -1389,7 +1495,7 @@ upgradeGithubReleaseCommand() { # shellcheck disable=SC2034 optionVersionArg="$1" ;; - # Option 2/18 + # Option 2/17 # Option optionCurrentVersion --current-version|-c variableType String min 0 max 1 authorizedValues '' regexp '' --current-version | -c) shift @@ -1405,7 +1511,7 @@ upgradeGithubReleaseCommand() { # shellcheck disable=SC2034 optionCurrentVersion="$1" ;; - # Option 3/18 + # Option 3/17 # Option optionExactVersion --exact-version|-e variableType String min 0 max 1 authorizedValues '' regexp '' --exact-version | -e) shift @@ -1421,23 +1527,7 @@ upgradeGithubReleaseCommand() { # shellcheck disable=SC2034 optionExactVersion="$1" ;; - # Option 4/18 - # Option optionMinimalVersion --minimal-version|-m variableType String min 0 max 1 authorizedValues '' regexp '' - --minimal-version | -m) - shift - if (($# == 0)); then - Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - a value needs to be specified" - return 1 - fi - if ((options_parse_optionParsedCountOptionMinimalVersion >= 1)); then - Log::displayError "Command ${SCRIPT_NAME} - Option ${options_parse_arg} - Maximum number of option occurrences reached(1)" - return 1 - fi - ((++options_parse_optionParsedCountOptionMinimalVersion)) - # shellcheck disable=SC2034 - optionMinimalVersion="$1" - ;; - # Option 5/18 + # Option 4/17 # Option optionBashFrameworkConfig --bash-framework-config variableType String min 0 max 1 authorizedValues '' regexp '' --bash-framework-config) shift @@ -1454,7 +1544,7 @@ upgradeGithubReleaseCommand() { optionBashFrameworkConfig="$1" optionBashFrameworkConfigCallback "${options_parse_arg}" "${optionBashFrameworkConfig}" ;; - # Option 6/18 + # Option 5/17 # Option optionConfig --config variableType Boolean min 0 max 1 authorizedValues '' regexp '' --config) # shellcheck disable=SC2034 @@ -1465,7 +1555,7 @@ upgradeGithubReleaseCommand() { fi ((++options_parse_optionParsedCountOptionConfig)) ;; - # Option 7/18 + # Option 6/17 # Option optionInfoVerbose --verbose|-v variableType Boolean min 0 max 1 authorizedValues '' regexp '' --verbose | -v) # shellcheck disable=SC2034 @@ -1478,7 +1568,7 @@ upgradeGithubReleaseCommand() { optionInfoVerboseCallback "${options_parse_arg}" updateArgListInfoVerboseCallback "${options_parse_arg}" ;; - # Option 8/18 + # Option 7/17 # Option optionDebugVerbose -vv variableType Boolean min 0 max 1 authorizedValues '' regexp '' -vv) # shellcheck disable=SC2034 @@ -1491,7 +1581,7 @@ upgradeGithubReleaseCommand() { optionDebugVerboseCallback "${options_parse_arg}" updateArgListDebugVerboseCallback "${options_parse_arg}" ;; - # Option 9/18 + # Option 8/17 # Option optionTraceVerbose -vvv variableType Boolean min 0 max 1 authorizedValues '' regexp '' -vvv) # shellcheck disable=SC2034 @@ -1504,7 +1594,7 @@ upgradeGithubReleaseCommand() { optionTraceVerboseCallback "${options_parse_arg}" updateArgListTraceVerboseCallback "${options_parse_arg}" ;; - # Option 10/18 + # Option 9/17 # Option optionEnvFiles --env-file variableType StringArray min 0 max -1 authorizedValues '' regexp '' --env-file) shift @@ -1517,7 +1607,7 @@ upgradeGithubReleaseCommand() { optionEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" updateArgListEnvFileCallback "${options_parse_arg}" "${optionEnvFiles[@]}" ;; - # Option 11/18 + # Option 10/17 # Option optionNoColor --no-color variableType Boolean min 0 max 1 authorizedValues '' regexp '' --no-color) # shellcheck disable=SC2034 @@ -1530,7 +1620,7 @@ upgradeGithubReleaseCommand() { optionNoColorCallback "${options_parse_arg}" updateArgListNoColorCallback "${options_parse_arg}" ;; - # Option 12/18 + # Option 11/17 # Option optionTheme --theme variableType String min 0 max 1 authorizedValues 'default|default-force|noColor' regexp '' --theme) shift @@ -1552,7 +1642,7 @@ upgradeGithubReleaseCommand() { optionThemeCallback "${options_parse_arg}" "${optionTheme}" updateArgListThemeCallback "${options_parse_arg}" "${optionTheme}" ;; - # Option 13/18 + # Option 12/17 # Option optionHelp --help|-h variableType Boolean min 0 max 1 authorizedValues '' regexp '' --help | -h) # shellcheck disable=SC2034 @@ -1564,7 +1654,7 @@ upgradeGithubReleaseCommand() { ((++options_parse_optionParsedCountOptionHelp)) optionHelpCallback "${options_parse_arg}" ;; - # Option 14/18 + # Option 13/17 # Option optionVersion --version variableType Boolean min 0 max 1 authorizedValues '' regexp '' --version) # shellcheck disable=SC2034 @@ -1576,7 +1666,7 @@ upgradeGithubReleaseCommand() { ((++options_parse_optionParsedCountOptionVersion)) optionVersionCallback "${options_parse_arg}" ;; - # Option 15/18 + # Option 14/17 # Option optionQuiet --quiet|-q variableType Boolean min 0 max 1 authorizedValues '' regexp '' --quiet | -q) # shellcheck disable=SC2034 @@ -1589,7 +1679,7 @@ upgradeGithubReleaseCommand() { optionQuietCallback "${options_parse_arg}" updateArgListQuietCallback "${options_parse_arg}" ;; - # Option 16/18 + # Option 15/17 # Option optionLogLevel --log-level variableType String min 0 max 1 authorizedValues 'OFF|ERR|ERROR|WARN|WARNING|INFO|DEBUG|TRACE' regexp '' --log-level) shift @@ -1611,7 +1701,7 @@ upgradeGithubReleaseCommand() { optionLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" updateArgListLogLevelCallback "${options_parse_arg}" "${optionLogLevel}" ;; - # Option 17/18 + # Option 16/17 # Option optionLogFile --log-file variableType String min 0 max 1 authorizedValues '' regexp '' --log-file) shift @@ -1629,7 +1719,7 @@ upgradeGithubReleaseCommand() { optionLogFileCallback "${options_parse_arg}" "${optionLogFile}" updateArgListLogFileCallback "${options_parse_arg}" "${optionLogFile}" ;; - # Option 18/18 + # Option 17/17 # Option optionDisplayLevel --display-level variableType String min 0 max 1 authorizedValues 'OFF|ERR|ERROR|WARN|WARNING|INFO|DEBUG|TRACE' regexp '' --display-level) shift @@ -1703,17 +1793,16 @@ upgradeGithubReleaseCommand() { return 1 fi commandOptionParseFinished - upgradeGithubReleaseCommandCallback Log::displayDebug "Command ${SCRIPT_NAME} - parse arguments: ${BASH_FRAMEWORK_ARGV[*]}" Log::displayDebug "Command ${SCRIPT_NAME} - parse filtered arguments: ${BASH_FRAMEWORK_ARGV_FILTERED[*]}" elif [[ "${options_parse_cmd}" = "help" ]]; then - echo -e "$(Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "retrieve latest binary release from github and install it")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "retrieve latest binary release from github and install it" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" \ "${SCRIPT_NAME}" \ - "[--version-arg ]" "[--current-version|-c ]" "[--exact-version|-e ]" "[--minimal-version|-m ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" + "[--version-arg ]" "[--current-version|-c ]" "[--exact-version|-e ]" "[--bash-framework-config ]" "[--config]" "[--verbose|-v]" "[-vv]" "[-vvv]" "[--env-file ]" "[--no-color]" "[--theme ]" "[--help|-h]" "[--version]" "[--quiet|-q]" "[--log-level ]" "[--log-file ]" "[--display-level ]")" echo echo -e "${__HELP_TITLE_COLOR}ARGUMENTS:${__RESET_COLOR}" echo -e " ${__HELP_OPTION_COLOR}targetFile${__HELP_NORMAL} {single} (mandatory)" @@ -1744,11 +1833,6 @@ upgradeGithubReleaseCommand() { # shellcheck disable=SC2054 helpArray=($'if provided and currently installed binary is not this exactVersion,\n\n This exact version of the binary will be installed.\n\n See options constraints below.') echo -e " $(Array::wrap2 " " 76 4 "${helpArray[@]}")" - echo -e " ${__HELP_OPTION_COLOR}--minimal-version${__HELP_NORMAL}, ${__HELP_OPTION_COLOR}-m ${__HELP_NORMAL} {single}" - local -a helpArray - # shellcheck disable=SC2054 - helpArray=($'if provided and currently installed binary is below this \n\n minimalVersion, a new version of the binary will be installed. \n\n If this argument is not provided, the latest binary is unconditionally \n\n downloaded from github. \n\n See options constraints below.') - echo -e " $(Array::wrap2 " " 76 4 "${helpArray[@]}")" echo echo -e "${__HELP_TITLE_COLOR}GLOBAL OPTIONS:${__RESET_COLOR}" echo -e " ${__HELP_OPTION_COLOR}--bash-framework-config ${__HELP_NORMAL} {single}" @@ -1828,12 +1912,8 @@ upgradeGithubReleaseCommand() { echo -e """ ${__HELP_TITLE}OPTIONS CONSTRAINTS:${__HELP_NORMAL} -${__HELP_OPTION_COLOR}--version-arg${__HELP_NORMAL} parameter is needed if ${__HELP_OPTION_COLOR}--minimal-version${__HELP_NORMAL} argument is used and is different than default value. - ${__HELP_OPTION_COLOR}--current-version${__HELP_NORMAL}|${__HELP_OPTION_COLOR}-c${__HELP_NORMAL} and ${__HELP_OPTION_COLOR}--version-arg${__HELP_NORMAL} are mutually exclusive, you cannot use both argument at the same time. -${__HELP_OPTION_COLOR}--exact-version${__HELP_NORMAL}|${__HELP_OPTION_COLOR}-e${__HELP_NORMAL} and ${__HELP_OPTION_COLOR}--minimal-version${__HELP_NORMAL}|${__HELP_OPTION_COLOR}-m${__HELP_NORMAL} are mutually exclusive, you cannot use both argument at the same time. - ${__HELP_TITLE}GITHUB TEMPLATE URLS EXAMPLES:${__HELP_NORMAL} Simple ones(Sometimes @version@ template variable has to be specified twice):${__HELP_EXAMPLE} @@ -1883,12 +1963,6 @@ ${__HELP_EXAMPLE}upgradeGithubRelease /usr/local/bin/oq --exact-version 1.3.4 -- fi } -upgradeGithubReleaseCommandCallback() { - if [[ -n "${optionExactVersion}" && -n "${optionMinimalVersion}" ]]; then - Log::fatal "--exact-version|-e and --minimal-version|-m are mutually exclusive, you cannot use both argument at the same time." - fi -} - githubUrlPatternArgCallback() { if [[ ! "${githubUrlPatternArg}" =~ ^https://github.com/ ]]; then Log::fatal "Invalid githubUrlPattern ${githubUrlPatternArg} provided, it should begin with https://github.com/" @@ -1910,111 +1984,24 @@ targetFileArgCallback() { upgradeGithubReleaseCommand parse "${BASH_FRAMEWORK_ARGV[@]}" run() { + computeCurrentCommandVersion() { if [[ -n "${optionCurrentVersion}" ]]; then echo "${optionCurrentVersion}" return 0 fi if [[ -n "${optionVersionArg}" ]]; then - # need eval here to correctly interpret --version-arg '-V | grep oq:' - eval "'${targetFileArg}' ${optionVersionArg} 2>&1" | Version::parse || return 3 + "${targetFileArg}" "${optionVersionArg}" 2>&1 | Version::parse || return 3 fi } - # if minVersion arg provided, we have to compute current bin version - local tryDownloadNewVersion=1 - if [[ -f "${targetFileArg}" ]]; then - local commandVersion - commandVersion="$(computeCurrentCommandVersion)" - - if [[ -n "${optionExactVersion}" ]]; then - if Version::compare "${commandVersion}" "${optionExactVersion}"; then - tryDownloadNewVersion=0 - Log::displayStatus "${targetFileArg} version is the exact required version ${optionExactVersion}" - else - Log::displayWarning "${targetFileArg} version ${commandVersion} is different than required version ${optionExactVersion}" - fi - else - if [[ -n "${optionMinimalVersion}" ]]; then - if ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionMinimalVersion}/g")"; then - Log::displayError "Minimal version ${optionMinimalVersion} doesn't exist on github" - return 5 - fi - local versionCompare=0 - Version::compare "${commandVersion}" "${optionMinimalVersion}" || versionCompare=$? - # do not try to down version if current version is greater or equal to min version - if [[ "${versionCompare}" = "1" ]]; then - local msg="${targetFileArg} version ${commandVersion} is greater than minimal version ${optionMinimalVersion}" - # current version > min version - optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 - versionCompare=0 - Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? - if [[ "${versionCompare}" = "2" ]]; then - # current version < remote version - Log::displayWarning "${msg} but new version ${optionExactVersion} is available on github" - else - Log::displayInfo "${msg}" - fi - return 0 - elif [[ "${versionCompare}" = "2" ]]; then - # current version < min version - Log::displayWarning "${targetFileArg} version ${commandVersion} is lesser than minimal version ${optionMinimalVersion}" - else - tryDownloadNewVersion=2 # need to check if a newer version exists - Log::displayStatus "${targetFileArg} version is the required minimal version ${optionMinimalVersion}" - fi - else - tryDownloadNewVersion="2" - fi - - # check if a newer version is available - if [[ "${tryDownloadNewVersion}" = "2" ]]; then - Log::displayInfo "compute last remote version" - optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 - versionCompare=0 - Version::compare "${commandVersion}" "${optionExactVersion}" || versionCompare=$? - if [[ "${versionCompare}" = "1" ]]; then - # current version > remote version, shouldn't happen - tryDownloadNewVersion=0 - Log::displayWarning "${targetFileArg} version ${commandVersion} is greater than remote version ${optionExactVersion}" - elif [[ "${versionCompare}" = "2" ]]; then - # current version < remote version - tryDownloadNewVersion=1 - Log::displayWarning "${targetFileArg} version ${optionCurrentVersion} is lesser than remote version ${optionExactVersion}" - else - tryDownloadNewVersion=0 - Log::displayStatus "${targetFileArg} version is the same as remote version ${optionExactVersion}" - fi - fi - fi - fi - - if [[ "${tryDownloadNewVersion}" = "0" ]]; then - return 0 - fi - - # check if target file is writable - Assert::fileWritable "${targetFileArg}" - - if [[ -z "${optionExactVersion}" ]]; then - Log::displayInfo "compute last remote version" - optionExactVersion="$(Github::getLatestVersionFromUrl "${githubUrlPatternArg}")" || return 1 - if [[ -z "${optionExactVersion}" ]]; then - Log::displayError "${targetFileArg} latest version not found on github" - return 5 - fi - elif ! Github::isReleaseVersionExist "$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")"; then - Log::displayError "${targetFileArg} version ${optionExactVersion} doesn't exist on github" - return 4 - fi - - local githubUrl - githubUrl="$(echo "${githubUrlPatternArg}" | sed -E "s/@version@/${optionExactVersion}/g")" - Log::displayInfo "Using url ${githubUrl}" - - newSoftware=$(Github::downloadReleaseVersion "${githubUrl}") - Github::defaultInstall "${newSoftware}" "${targetFileArg}" - Log::displayStatus "Version ${optionExactVersion} installed in ${targetFileArg}" + EXACT_VERSION="${optionExactVersion}" \ + Github::upgradeRelease \ + "${targetFileArg}" \ + "${githubUrlPatternArg}" \ + "${optionVersionArg}" \ + computeCurrentCommandVersion \ + Github::defaultInstall } if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then diff --git a/bin/waitForIt b/bin/waitForIt index 8bb49f99..8d463fa4 100755 --- a/bin/waitForIt +++ b/bin/waitForIt @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,32 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description check if an element is contained in an array +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 # -# @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 +# @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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } done - return 1 } -# @description concat each element of an array with a separator +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -218,296 +478,66 @@ Array::wrap2() { if ((argLength + glueLength > maxLineLength)); then # arg is too long to even fit on one line # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @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 -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 - - fi + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" - 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}" + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine fi - fi + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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 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 } -# @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 +# @description check if command specified exists or return 1 +# with error and message if not # -# @set __RESET_COLOR String reset default color +# @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 # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi +# @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 } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -516,17 +546,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -540,6 +559,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -548,6 +575,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -610,32 +649,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -646,6 +703,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -654,18 +719,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -695,14 +767,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -711,10 +775,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -724,28 +789,26 @@ Log::rotate() { fi } +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 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" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi } @@ -983,7 +1046,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -1010,7 +1073,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1430,7 +1493,7 @@ waitForItCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for host:port to be available")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for host:port to be available" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/bin/waitForMysql b/bin/waitForMysql index 6f601176..e3406742 100755 --- a/bin/waitForMysql +++ b/bin/waitForMysql @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -190,308 +466,62 @@ Array::wrap2() { # isNewline = 0 means currentLine is not empty printCurrentLine fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @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 -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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" -} - -# @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 -} - -# @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 + continue + fi - fi + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" - 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}" + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine fi - fi -} - -# @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}" + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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 +# @description check if command specified exists or return 1 +# with error and message if not # -# @set __RESET_COLOR String reset default color +# @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 # -# @set __HELP_EXAMPLE String to remove -# @set __HELP_TITLE String to remove -# @set __HELP_NORMAL String to remove -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' - fi +# @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 } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -500,17 +530,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -524,6 +543,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -532,6 +559,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -594,32 +633,50 @@ Compiler::Facade::requireCommandBinDir() { Env::pathPrepend "${COMMAND_BIN_DIR}" } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -630,6 +687,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -638,18 +703,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -679,14 +751,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -695,10 +759,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -708,28 +773,26 @@ Log::rotate() { fi } +# @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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 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" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi } @@ -960,7 +1023,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -987,7 +1050,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1391,7 +1454,7 @@ waitForMysqlCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for mysql to be ready")" + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "wait for mysql to be ready" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]" "[ARGUMENTS]")" diff --git a/commit-msg-template.md b/commit-msg-template.md index 177901d3..73c3203d 100644 --- a/commit-msg-template.md +++ b/commit-msg-template.md @@ -2,6 +2,8 @@ Title Short 3 lines summary +tag: 1.1.5 + # Breaking changes # Bug fixes diff --git a/conf/.env b/conf/.env index 82eb07de..63f8de04 100755 --- a/conf/.env +++ b/conf/.env @@ -3,17 +3,6 @@ # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -27,6 +16,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -35,6 +32,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} diff --git a/conf/dbScripts/extractData b/conf/dbScripts/extractData index 55ac1b62..ffe75f11 100755 --- a/conf/dbScripts/extractData +++ b/conf/dbScripts/extractData @@ -66,7 +66,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -79,7 +79,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -88,15 +90,72 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + # @description exits with message if current user is root # @noargs # @exitcode 1 if current user is root @@ -107,6 +166,86 @@ Assert::expectNonRootUser() { fi } +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 1 +} + +# @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 + + # final auth file generated from dns file + instanceNewInstance['AUTH_FILE']="" + instanceNewInstance['DSN_FILE']="" + + # check dsn file + DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 + Database::checkDsnFile "${DSN_FILE}" || return 1 + instanceNewInstance['DSN_FILE']="${DSN_FILE}" + + # shellcheck source=/src/Database/testsData/dsn_valid.env + source "${instanceNewInstance['DSN_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") + ( + 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 +} + +# @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" +} + # @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 ) @@ -168,62 +307,16 @@ Conf::getAbsoluteFile() { return 1 } -# @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" +# @description by default we skip the column names +# but sometimes we need column names to display some results +# disable this option temporarily and then restore it to true # -# @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 - - # final auth file generated from dns file - instanceNewInstance['AUTH_FILE']="" - instanceNewInstance['DSN_FILE']="" - - # check dsn file - DSN_FILE="$(Conf::getAbsoluteFile "dsn" "${dsn}" "env")" || return 1 - Database::checkDsnFile "${DSN_FILE}" || return 1 - instanceNewInstance['DSN_FILE']="${DSN_FILE}" - - # shellcheck source=/src/Database/testsData/dsn_valid.env - source "${instanceNewInstance['DSN_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") - ( - 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 +# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use +# @arg $2 skipColumnNames:Boolean 0 to disable, 1 to enable (hide column names) +Database::skipColumnNames() { + local -n instanceSkipColumnNames=$1 + # shellcheck disable=SC2034 + instanceSkipColumnNames['SKIP_COLUMN_NAMES']="$2" } # @description mysql query on a given db @@ -265,94 +358,74 @@ Database::query() { fi } -# @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" -} - -# @description by default we skip the column names -# but sometimes we need column names to display some results -# disable this option temporarily and then restore it to true -# -# @arg $1 instanceSetQueryOptions:&Map (passed by reference) database instance to use -# @arg $2 skipColumnNames:Boolean 0 to disable, 1 to enable (hide column names) -Database::skipColumnNames() { - local -n instanceSkipColumnNames=$1 - # shellcheck disable=SC2034 - instanceSkipColumnNames['SKIP_COLUMN_NAMES']="$2" +# @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 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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 +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" -# @description Display message using debug color (grey) -# @arg $1 message:String the message to display -Log::displayDebug() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) +# @noargs +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - Log::logDebug "$1" } -# @description Display message using info color (bg light blue/fg white) +# @description log message to file # @arg $1 message:String the message to display -Log::displayInfo() { - local type="${2:-INFO}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 +Log::logInfo() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-INFO}" "$1" fi - Log::logInfo "$1" "${type}" } -# @description Display message using error color (red) and exit immediately with error status 1 +# @description log message to file # @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::logDebug() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_DEBUG)); then + Log::logMessage "${2:-DEBUG}" "$1" + 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 log message to file +# @arg $1 message:String the message to display +Log::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description check if dsn file has all the mandatory variables set @@ -406,18 +479,6 @@ Database::checkDsnFile() { ) } -# @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 @@ -432,50 +493,26 @@ File::concatenatePath() { # @description Display message using error color (red) # @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log Log::displayError() { if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then - echo -e "${__ERROR_COLOR}ERROR - ${1}${__RESET_COLOR}" >&2 + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi Log::logError "$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 -} - -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$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 -} - -# @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" -} - -# @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 +# @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 Internal: common log message @@ -505,6 +542,26 @@ Log::logMessage() { fi } +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$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 +} + # @description ensure command realpath is available # @exitcode 1 if realpath command not available # @stderr diagnostics information is displayed @@ -512,6 +569,14 @@ Linux::requireRealpathCommand() { Assert::commandExists realpath } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description check if command specified exists or return 1 # with error and message if not # @@ -534,14 +599,6 @@ Assert::commandExists() { return 0 } -# @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 env files are loaded # @arg $@ list of default files to load at the end # @exitcode 1 if one of env files fails to load @@ -555,8 +612,10 @@ Env::requireLoad() { # BASH_FRAMEWORK_ENV_FILES is an array configFiles+=("${BASH_FRAMEWORK_ENV_FILES[@]}") fi - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") + local localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") fi if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") @@ -595,10 +654,12 @@ Log::requireLoad() { 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 + if [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 @@ -606,7 +667,6 @@ Log::requireLoad() { BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} echo -e "${__ERROR_COLOR}ERROR - File ${BASH_FRAMEWORK_LOG_FILE} is not writable${__RESET_COLOR}" >&2 fi - fi if ((BASH_FRAMEWORK_LOG_LEVEL > __LEVEL_OFF)); then @@ -626,10 +686,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -642,18 +703,12 @@ Log::rotate() { # @description load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" -} - -# @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 + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" fi - Log::logSkipped "$1" } # @description load colors theme constants @@ -700,7 +755,7 @@ UI::theme() { __SUCCESS_COLOR='\e[32m' # Green __WARNING_COLOR='\e[33m' # Yellow __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey + __DEBUG_COLOR='\e[37m' # Gray __HELP_COLOR='\e[7;49;33m' # Black on Gold __TEST_COLOR='\e[100m' # Light magenta __TEST_ERROR_COLOR='\e[41m' # white on red @@ -741,7 +796,6 @@ UI::theme() { # @exitcode 1 if tty not active # @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive # @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided Assert::tty() { if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then return 1 @@ -749,15 +803,7 @@ Assert::tty() { if [[ "${INTERACTIVE:-0}" = "1" ]]; then return 0 fi - [[ -t 1 || -t 2 ]] -} - -# @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 + tty -s } # FUNCTIONS diff --git a/conf/mysql2pumlSkins/default.png b/conf/mysql2pumlSkins/default.png index 76812881d32d9cde50df8edac542231a387fae03..aaf7784b43255f532035d355498ed8d50af4ddee 100644 GIT binary patch delta 840 zcmV-O1GoIU2f7E4T7NESR9JLyY+-J6b!}__0RR91c$}qGZBHUG5dOZuVq6JAL_j6x zvH_xpa#27z5x-svJ3zy3cdy$@xL<$M?t*X%r*|Zz-OkK2Pp9*8nJ6Ze4g=>1v=~QW zh(dvyAp~3?j4+56z$dVb^pQl+2^Ui!wOA{Zu;6M23?wQp7=QRYi37GIq-4%T%p?oJ zk2#!ItIolM&=rpaJh4%Lg`8j^79D^CGS*UnI}TWqfD6Z9Wwg3buS4Ofbl7fR(XR;2 zrt2J_@K;#mX<;}K&nWO=5=n?951CxT91V}DL=;3fAc>J`!GCHb0TVub_$ZTcMAtGh z5vd_Wd& zf;Rm}k(!k#j|Fb$->VO>;JGxf2gJ=t*VX2w)o0TD^ZB`P5#8L#RSUV{&?sanQm^QL zP2Tss*?&a-l)KO2r&<0{2fF|_Y_}eI)dy3>KieF=EVWz3rU|eY#%+OkbYAD=l{d|9 z#Kv>4_0WBIT-RU;7G#E5o^Z0ww)V+sovAR$j>eI18e_pW+g7V+x;UaoDk}CW6-q&2 z#zdKVf5oL^X^2B>hNeK7U|jaw&0M$APuHQcCg>4Uk(ZuL1vs zP<6;14IZvIsbA9F%|vo8*Td=m@z_?&wM2Q4{@;0`?H2xV!GG4&X+-a2W|R zS3RA(=iaNrMeXD(S1>?HOyIfaT;qi4K#>JK2F+QIythxjI9(>DnoS8Ch0}@%S1w65 SaDD^VX-*y)*02wgk^?D>f{!Es delta 841 zcmV-P1GfCS2fGK5T7NHTR9JLyY+-J6b!}__0RR91c$}qGZExBz5dNNDaioZXN`byj zTLh>bEmK?CvI6}&IphF~#K|1Hi2eGVozTMCvUWp4c6@iwJwD&dX{wl1Itra#&|(}% z5efxrh7fXr;A0psfKOl<>s^VUQ!b`JYKc}TVZqf57)VrHFnu5gh>`b zkZ?F^G@QbO&=rpb44EHdxhPnK6$hX|###z+$018oaA6Ouj8+$#O(+kmJKgRX{VLFI zyG{W`u)-ow3&V+c#$f=HSVAIs#N-m@Xn0H|q9D24A-cVp?{7<#Tu93>k$FTHv}4eec3~79Y!v zZ>seh$$JL>gb5Xm=t2Hj`kP3P-+ zNw|eX_L7?v@-stXbbI-Ibg`8N(*ck7(*(y#>gZjJUXM#&RgfuiVT)7p=}hH`UG863 zp-X>1RVui$pKXp_mfEdi(-hbv;P-1YA6pK7oQ3o^qzPdM4;Tl?g+&QuiUN8>m!jj?Q-ZKqQ)T^!LP6_t4PI;Eg6 zW1>dAziwMKrZr=}b3+Z(nehN2LN$wnH2X+tK7U|raw&D5$APsxQ%Z852FN9q*MR>* zXgK7K1`k)9)KBT|W+FMKPs8c|@t;8Z<|dfz>D;nVT{SFT`yl9<55jdP9@rb9&*^cXZ}J@Ve&|LpXbm})j9Y!pr>CS18B T*}(Y?(gjWyBnDRNlaT`{;s%%~ diff --git a/cspell.yaml b/cspell.yaml new file mode 100644 index 00000000..9e401cb4 --- /dev/null +++ b/cspell.yaml @@ -0,0 +1,166 @@ +--- +$schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json +version: "0.2" +language: en +noConfigSearch: true +caseSensitive: true +useGitignore: true +enableGlobDot: true +ignorePaths: + - "**/testsData/**" + - package-lock.json + - "**/*/svg" + - .vscode + - ".cspell/**" + - .git/** + - "**/.git/**" + - ".history/**" + - "**/node_modules/**" + - "**/vscode-extension/**" + - "**/logs/**" + - "**/*.svg" + - megalinter-reports + - report + - .jscpd.json + - "*-megalinter_file_names_cspell.txt" + - "**/*megalinter_file_names_cspell.txt" + - .shellcheckrc + - "**/bin/**" + - "vendor/**" + - "**/backup/**" + - commit-msg.md + - ".mega-linter*.yml" + - ".env" + - "**/*.help.txt" + - "conf/dbScripts/**" + - "install" +dictionaryDefinitions: + - name: myBash + path: ".cspell/bash.txt" + addWords: true + - name: loremIpsum + path: ".cspell/loremIpsum.txt" + addWords: true + - name: config + path: ".cspell/config.txt" + addWords: true + - name: mySoftwares + path: ".cspell/softwares.txt" + addWords: true + - name: readme + path: ".cspell/readme.txt" + addWords: true + - name: dirColors + path: ".cspell/dirColors.txt" + addWords: false + - name: plantUml + path: ".cspell/plantUml.txt" + addWords: false + - name: myAwk + path: ".cspell/myAwk.txt" + addWords: false + - name: postman + path: ".cspell/postman.txt" + addWords: false + +# https://github.com/streetsidesoftware/cspell/blob/main/packages/cspell/README.md#languagesettings +languageSettings: + - languageId: dirColors + locale: "*" + dictionaries: + - dirColors + + - languageId: shellscript + locale: "*" + dictionaries: + - bash + - myBash + - mySoftwares + - software + + - languageId: markdown + locale: "*" + dictionaries: + - readme + - mySoftwares + - softwareTerms + + - languageId: plantUml + locale: "*" + dictionaries: + - bash + - plantUml + - mySoftwares + - software + +# OVERRIDES +overrides: + - filename: + - "**/*.{bats,tpl}" + - "**/*.env" + - "src/Array/wrap.sh" + languageId: shellscript + dictionaries: + - loremIpsum + + - filename: "*.yml" + dictionaries: + - lintersConfig + + - filename: "**/*.puml" + languageId: plantUml + + - filename: "**/*.html" + dictionaries: + - mySoftwares + - software + + - filename: + - "**/*.md" + - "LICENSE" + languageId: markdown + + - filename: LICENSE + dictionaries: + - readme + + - filename: + - .github/** + - .* + - "*.{yaml,yml}" + languageId: shellscript + dictionaries: + - config + + - filename: "**/*.awk" + dictionaries: + - myAwk + - mySoftwares + - software + + - filename: + - "conf/postmanCli/GithubAPI/*.json" + - "conf/postmanCli/MongoDbData/*.json" + dictionaries: + - postman + - mySoftwares + - software + +patterns: + - name: urls + pattern: "/https?://([^ \t\"'()]+)/g" + - name: packages + pattern: "/[-A-Za-z0-9.]+/[-A-Za-z0-9.]+/g" + - name: markdownToc + pattern: "\\]\\(#[^)]+\\)$" + +ignoreRegExpList: + - urls + - packages + - markdownToc + +enableFiletypes: + - shellscript + - dirColors + - markdown + - plantUml diff --git a/install b/install index b6784644..f605bf01 100755 --- a/install +++ b/install @@ -67,7 +67,7 @@ REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" if [[ -n "${EMBED_CURRENT_DIR}" ]]; then CURRENT_DIR="${EMBED_CURRENT_DIR}" else - CURRENT_DIR="$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")" && pwd -P)" + CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" fi ################################################ @@ -80,7 +80,9 @@ export KEEP_TEMP_FILES # PERSISTENT_TMPDIR is not deleted by traps PERSISTENT_TMPDIR="${TMPDIR:-/tmp}/bash-framework" export PERSISTENT_TMPDIR -mkdir -p "${PERSISTENT_TMPDIR}" +if [[ ! -d "${PERSISTENT_TMPDIR}" ]]; then + mkdir -p "${PERSISTENT_TMPDIR}" +fi # shellcheck disable=SC2034 TMPDIR="$(mktemp -d -p "${PERSISTENT_TMPDIR:-/tmp}" -t bash-framework-$$-XXXXXX)" @@ -89,16 +91,290 @@ export TMPDIR # temp dir cleaning # shellcheck disable=SC2317 cleanOnExit() { + local rc=$? if [[ "${KEEP_TEMP_FILES:-0}" = "1" ]]; then Log::displayInfo "KEEP_TEMP_FILES=1 temp files kept here '${TMPDIR}'" elif [[ -n "${TMPDIR+xxx}" ]]; then Log::displayDebug "KEEP_TEMP_FILES=0 removing temp files '${TMPDIR}'" rm -Rf "${TMPDIR:-/tmp/fake}" >/dev/null 2>&1 fi + exit "${rc}" } trap cleanOnExit EXIT HUP QUIT ABRT TERM -# @description concat each element of an array with a separator +# @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 +# @description log level error +export __LEVEL_ERROR=1 +# @description log level warning +export __LEVEL_WARNING=2 +# @description log level info +export __LEVEL_INFO=3 +# @description log level success +export __LEVEL_SUCCESS=3 +# @description log level debug +export __LEVEL_DEBUG=4 + +# @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 info color (bg light blue/fg white) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayInfo() { + local type="${2:-INFO}" + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__INFO_COLOR}${type} - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logInfo "$1" "${type}" +} + +# @description Display message using debug color (gray) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayDebug() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then + Log::computeDuration + echo -e "${__DEBUG_COLOR}DEBUG - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logDebug "$1" +} + +# @description Display message using warning color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayWarning() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then + Log::computeDuration + echo -e "${__WARNING_COLOR}WARN - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + Log::logWarning "$1" +} + +# @description Display message using error color (red) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displayError() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_ERROR)); then + Log::computeDuration + echo -e "${__ERROR_COLOR}ERROR - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + fi + 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 +# shellcheck disable=SC2034 +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 + BASH_FRAMEWORK_THEME="default" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='\e[31m' # Red + __INFO_COLOR='\e[44m' # white on lightBlue + __SUCCESS_COLOR='\e[32m' # Green + __WARNING_COLOR='\e[33m' # Yellow + __SKIPPED_COLOR='\e[33m' # Yellow + __DEBUG_COLOR='\e[37m' # Gray + __HELP_COLOR='\e[7;49;33m' # Black on Gold + __TEST_COLOR='\e[100m' # Light magenta + __TEST_ERROR_COLOR='\e[41m' # white on red + __HELP_TITLE_COLOR="\e[1;37m" # Bold + __HELP_OPTION_COLOR="\e[1;34m" # Blue + # Internal: reset color + __RESET_COLOR='\e[0m' # Reset Color + # shellcheck disable=SC2155,SC2034 + __HELP_EXAMPLE="$(echo -e "\e[2;97m")" + # shellcheck disable=SC2155,SC2034 + __HELP_TITLE="$(echo -e "\e[1;37m")" + # shellcheck disable=SC2155,SC2034 + __HELP_NORMAL="$(echo -e "\033[0m")" + else + BASH_FRAMEWORK_THEME="noColor" + # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting + __ERROR_COLOR='' + __INFO_COLOR='' + __SUCCESS_COLOR='' + __WARNING_COLOR='' + __SKIPPED_COLOR='' + __DEBUG_COLOR='' + __HELP_COLOR='' + __TEST_COLOR='' + __TEST_ERROR_COLOR='' + __HELP_TITLE_COLOR='' + __HELP_OPTION_COLOR='' + # Internal: reset color + __RESET_COLOR='' + __HELP_EXAMPLE='' + __HELP_TITLE='' + __HELP_NORMAL='' + fi +} + +# @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:-#}" + local -i width=${COLUMNS:-0} + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) + fi + if ((width == 0)); then + width=80 + fi + printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 1 ]] && tput cols || echo '80')}") + echo +} + +# @description Display message using error color (red) and exit immediately with error status 1 +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::fatal() { + Log::computeDuration + echo -e "${__ERROR_COLOR}FATAL - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 + Log::logFatal "$1" + exit 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 ensure env files are loaded +# @arg $@ list of default files to load at the end +# @exitcode 1 if one of env files fails to load +# @stderr diagnostics information is displayed +# shellcheck disable=SC2120 +Env::requireLoad() { + local -a defaultFiles=("$@") + # get list of possible config files + 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 localFrameworkConfigFile + localFrameworkConfigFile="$(pwd)/.framework-config" + if [[ -f "${localFrameworkConfigFile}" ]]; then + configFiles+=("${localFrameworkConfigFile}") + fi + if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then + configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") + fi + configFiles+=("${optionEnvFiles[@]}") + configFiles+=("${defaultFiles[@]}") + + for file in "${configFiles[@]}"; do + # shellcheck source=/.framework-config + CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { + Log::displayError "while loading config file: ${file}" + return 1 + } + done +} + +# @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 [[ ! -d "${BASH_FRAMEWORK_LOG_FILE%/*}" ]]; then + if ! mkdir -p "${BASH_FRAMEWORK_LOG_FILE%/*}" 2>/dev/null; then + BASH_FRAMEWORK_LOG_LEVEL=${__LEVEL_OFF} + echo -e "${__ERROR_COLOR}ERROR - directory ${BASH_FRAMEWORK_LOG_FILE%/*} is not writable${__RESET_COLOR}" >&2 + fi + elif ! 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 + 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 +} + +# @description concatenate 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 it can. # - if an arg can be placed on current line it will be, @@ -187,298 +463,55 @@ Array::wrap2() { # empty arg if ((argLength == 0)); then if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - continue - fi - - if ((isNewline == 0)); then - glueLength="${#glue}" - else - glueLength="0" - fi - if ((currentLineLength + argLength + glueLength > maxLineLength)); then - if ((argLength + glueLength > maxLineLength)); then - # arg is too long to even fit on one line - # we have to split the arg on current and next line - local -i remainingLineLength - ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) - appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" - printCurrentLine - arg="${arg:${remainingLineLength}}" - # remove leading spaces - arg="${arg##[[:blank:]]}" - - set -- "${arg}" "$@" - else - # the arg can fit on next line - printCurrentLine - appendToCurrentLine "${arg}" "${argLength}" - fi - else - appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" - fi - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi - ) | sed -E -e 's/[[:blank:]]+$//' -} - -# @description ensure env files are loaded -# @arg $@ list of default files to load at the end -# @exitcode 1 if one of env files fails to load -# @stderr diagnostics information is displayed -# shellcheck disable=SC2120 -Env::requireLoad() { - local -a defaultFiles=("$@") - # get list of possible config files - 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 - if [[ -f "$(pwd)/.framework-config" ]]; then - configFiles+=("$(pwd)/.framework-config") - fi - if [[ -f "${FRAMEWORK_ROOT_DIR}/.framework-config" ]]; then - configFiles+=("${FRAMEWORK_ROOT_DIR}/.framework-config") - fi - configFiles+=("${optionEnvFiles[@]}") - configFiles+=("${defaultFiles[@]}") - - for file in "${configFiles[@]}"; do - # shellcheck source=/.framework-config - CURRENT_LOADED_ENV_FILE="${file}" source "${file}" || { - Log::displayError "while loading config file: ${file}" - return 1 - } - done -} - -# @description create a temp file using default TMPDIR variable -# initialized in _includes/_commonHeader.sh -# @env TMPDIR String (default value /tmp) -# @arg $1 templateName:String template name to use(optional) -Framework::createTempFile() { - mktemp -p "${TMPDIR:-/tmp}" -t "${1:-}.XXXXXXXXXXXX" -} - -# @description Log 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 -# @description log level error -export __LEVEL_ERROR=1 -# @description log level warning -export __LEVEL_WARNING=2 -# @description log level info -export __LEVEL_INFO=3 -# @description log level success -export __LEVEL_SUCCESS=3 -# @description log level debug -export __LEVEL_DEBUG=4 - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_DEBUG)); then - echo -e "${__DEBUG_COLOR}DEBUG - ${1}${__RESET_COLOR}" >&2 - fi - Log::logDebug "$1" -} - -# @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}" - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then - echo -e "${__INFO_COLOR}${type} - ${1}${__RESET_COLOR}" >&2 - fi - Log::logInfo "$1" "${type}" -} - -# @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() { - if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_WARNING)); then - echo -e "${__WARNING_COLOR}WARN - ${1}${__RESET_COLOR}" >&2 - fi - Log::logWarning "$1" -} - -# @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 -} - -# @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 + # isNewline = 0 means currentLine is not empty + printCurrentLine + fi + continue 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 - fi + if ((isNewline == 0)); then + glueLength="${#glue}" + else + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" - 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}" + set -- "${arg}" "$@" + else + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" + fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" + fi + done + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine fi - fi -} - -# @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}" + ) | sed -E -e 's/[[:blank:]]+$//' } -# @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 -# shellcheck disable=SC2034 -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 - BASH_FRAMEWORK_THEME="default" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='\e[31m' # Red - __INFO_COLOR='\e[44m' # white on lightBlue - __SUCCESS_COLOR='\e[32m' # Green - __WARNING_COLOR='\e[33m' # Yellow - __SKIPPED_COLOR='\e[33m' # Yellow - __DEBUG_COLOR='\e[37m' # Grey - __HELP_COLOR='\e[7;49;33m' # Black on Gold - __TEST_COLOR='\e[100m' # Light magenta - __TEST_ERROR_COLOR='\e[41m' # white on red - __HELP_TITLE_COLOR="\e[1;37m" # Bold - __HELP_OPTION_COLOR="\e[1;34m" # Blue - # Internal: reset color - __RESET_COLOR='\e[0m' # Reset Color - # shellcheck disable=SC2155,SC2034 - __HELP_EXAMPLE="$(echo -e "\e[2;97m")" - # shellcheck disable=SC2155,SC2034 - __HELP_TITLE="$(echo -e "\e[1;37m")" - # shellcheck disable=SC2155,SC2034 - __HELP_NORMAL="$(echo -e "\033[0m")" - else - BASH_FRAMEWORK_THEME="noColor" - # check colors applicable https://misc.flogisoft.com/bash/tip_colors_and_formatting - __ERROR_COLOR='' - __INFO_COLOR='' - __SUCCESS_COLOR='' - __WARNING_COLOR='' - __SKIPPED_COLOR='' - __DEBUG_COLOR='' - __HELP_COLOR='' - __TEST_COLOR='' - __TEST_ERROR_COLOR='' - __HELP_TITLE_COLOR='' - __HELP_OPTION_COLOR='' - # Internal: reset color - __RESET_COLOR='' - __HELP_EXAMPLE='' - __HELP_TITLE='' - __HELP_NORMAL='' +# @description Display message using skip color (yellow) +# @arg $1 message:String the message to display +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @env LOG_CONTEXT String allows to contextualize the log +Log::displaySkipped() { + if ((BASH_FRAMEWORK_DISPLAY_LEVEL >= __LEVEL_INFO)); then + Log::computeDuration + echo -e "${__SKIPPED_COLOR}SKIPPED - ${LOG_CONTEXT:-}${LOG_LAST_DURATION_STR:-}${1}${__RESET_COLOR}" >&2 fi + Log::logSkipped "$1" } bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( @@ -487,17 +520,6 @@ bashToolsDefaultConfigTemplate="${bashToolsDefaultConfigTemplate:-$( # Default settings # you can override these settings by creating ${HOME}/.bash-tools/.env file -### -### LOG Level -### minimum level of the messages that will be logged into LOG_FILE -### -### 0: NO LOG -### 1: ERROR -### 2: WARNING -### 3: INFO -### 4: DEBUG -### -BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### ### DISPLAY Level @@ -511,6 +533,14 @@ BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} ### BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} +### +### DISPLAY duration +### 0: no duration is displayed on the messages +### 1: duration between previous message and current is displayed +### with the message +### +DISPLAY_DURATION=${DISPLAY_DURATION:0} + ### ### Log to file ### @@ -519,6 +549,18 @@ BASH_FRAMEWORK_DISPLAY_LEVEL=${BASH_FRAMEWORK_DISPLAY_LEVEL:-3} ### BASH_FRAMEWORK_LOG_FILE=${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/bash.log} +### +### LOG Level +### minimum level of the messages that will be logged into LOG_FILE +### +### 0: NO LOG +### 1: ERROR +### 2: WARNING +### 3: INFO +### 4: DEBUG +### +BASH_FRAMEWORK_LOG_LEVEL=${BASH_FRAMEWORK_LOG_LEVEL:-0} + # absolute directory containing db import sql dumps DB_IMPORT_DUMP_DIR=${DB_IMPORT_DUMP_DIR:-${HOME}/.bash-tools/dbImportDumps} @@ -590,32 +632,50 @@ Linux::requireExecutedAsUser() { fi } -# @description check if tty (interactive mode) is active +declare -g FIRST_LOG_DATE LOG_LAST_LOG_DATE LOG_LAST_LOG_DATE_INIT LOG_LAST_DURATION_STR +FIRST_LOG_DATE="${EPOCHREALTIME/[^0-9]/}" +LOG_LAST_LOG_DATE="${FIRST_LOG_DATE}" +LOG_LAST_LOG_DATE_INIT=1 +LOG_LAST_DURATION_STR="" + +# @description compute duration since last call to this function +# the result is set in following env variables. +# in ss.sss (seconds followed by milliseconds precision 3 decimals) # @noargs -# @exitcode 1 if tty not active -# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive -# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive -# @stderr diagnostic information + help if second argument is provided -Assert::tty() { - if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then - return 1 - fi - if [[ "${INTERACTIVE:-0}" = "1" ]]; then - return 0 +# @env DISPLAY_DURATION int (default 0) if 1 display elapsed time information between 2 info logs +# @set LOG_LAST_LOG_DATE_INIT int (default 1) set to 0 at first call, allows to detect reference log +# @set LOG_LAST_DURATION_STR String the last duration displayed +# @set LOG_LAST_LOG_DATE String the last log date that will be used to compute next diff +Log::computeDuration() { + if ((${DISPLAY_DURATION:-0} == 1)); then + local -i duration=0 + local -i delta=0 + local -i currentLogDate + currentLogDate="${EPOCHREALTIME/[^0-9]/}" + if ((LOG_LAST_LOG_DATE_INIT == 1)); then + LOG_LAST_LOG_DATE_INIT=0 + LOG_LAST_DURATION_STR="Ref" + else + duration=$(((currentLogDate - FIRST_LOG_DATE) / 1000000)) + delta=$(((currentLogDate - LOG_LAST_LOG_DATE) / 1000000)) + LOG_LAST_DURATION_STR="${duration}s/+${delta}s" + fi + LOG_LAST_LOG_DATE="${currentLogDate}" + # shellcheck disable=SC2034 + local microSeconds="${EPOCHREALTIME#*.}" + LOG_LAST_DURATION_STR="$(printf '%(%T)T.%03.0f\n' "${EPOCHSECONDS}" "${microSeconds:0:3}")(${LOG_LAST_DURATION_STR}) - " + else + # shellcheck disable=SC2034 + LOG_LAST_DURATION_STR="" fi - [[ -t 1 || -t 2 ]] } -# @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 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 } # @description log message to file @@ -626,6 +686,14 @@ Log::logDebug() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logWarning() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then + Log::logMessage "${2:-WARNING}" "$1" + fi +} + # @description log message to file # @arg $1 message:String the message to display Log::logError() { @@ -634,18 +702,25 @@ Log::logError() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logFatal() { - Log::logMessage "${2:-FATAL}" "$1" +# @description check if tty (interactive mode) is active +# @noargs +# @exitcode 1 if tty not active +# @env NON_INTERACTIVE if 1 consider as not interactive even if environment is interactive +# @env INTERACTIVE if 1 consider as interactive even if environment is not interactive +Assert::tty() { + if [[ "${NON_INTERACTIVE:-0}" = "1" ]]; then + return 1 + fi + if [[ "${INTERACTIVE:-0}" = "1" ]]; then + return 0 + fi + tty -s } # @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::logFatal() { + Log::logMessage "${2:-FATAL}" "$1" } # @description Internal: common log message @@ -675,22 +750,6 @@ Log::logMessage() { fi } -# @description log message to file -# @arg $1 message:String the message to display -Log::logSkipped() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then - Log::logMessage "${2:-SKIPPED}" "$1" - fi -} - -# @description log message to file -# @arg $1 message:String the message to display -Log::logWarning() { - if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_WARNING)); then - Log::logMessage "${2:-WARNING}" "$1" - fi -} - # @description 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 @@ -699,10 +758,11 @@ Log::rotate() { local maxLogFilesCount="${2:-5}" if [[ ! -f "${file}" ]]; then - Log::displaySkipped "Log file ${file} doesn't exist yet" + Log::displayDebug "Log file ${file} doesn't exist yet" return 0 fi - for i in $(seq $((maxLogFilesCount - 1)) -1 1); do + local i + for ((i = maxLogFilesCount - 1; i > 0; i--)); do Log::displayInfo "Log rotation ${file}.${i} to ${file}.$((i + 1))" mv "${file}."{"${i}","$((i + 1))"} &>/dev/null || true done @@ -712,12 +772,35 @@ Log::rotate() { fi } +# @description log message to file +# @arg $1 message:String the message to display +Log::logSkipped() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SKIPPED}" "$1" + fi +} + +# @description 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 load color theme # @noargs # @env BASH_FRAMEWORK_THEME String theme to use +# @env LOAD_THEME int 0 to avoid loading theme # @exitcode 0 always successful UI::requireTheme() { - UI::theme "${BASH_FRAMEWORK_THEME-default}" + if [[ "${LOAD_THEME:-1}" = "1" ]]; then + UI::theme "${BASH_FRAMEWORK_THEME-default}" + fi } # FUNCTIONS @@ -940,7 +1023,7 @@ defaultFrameworkConfig="$( # shellcheck disable=SC2034 REAL_SCRIPT_FILE="${REAL_SCRIPT_FILE:-$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")}" -FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-$(cd "$(readlink -e "${REAL_SCRIPT_FILE%/*}")/../.." && pwd -P)}" +FRAMEWORK_ROOT_DIR="${FRAMEWORK_ROOT_DIR:-${REAL_SCRIPT_FILE%/*/*}}" FRAMEWORK_SRC_DIR="${FRAMEWORK_SRC_DIR:-${FRAMEWORK_ROOT_DIR}/src}" FRAMEWORK_BIN_DIR="${FRAMEWORK_BIN_DIR:-${FRAMEWORK_ROOT_DIR}/bin}" FRAMEWORK_VENDOR_DIR="${FRAMEWORK_VENDOR_DIR:-${FRAMEWORK_ROOT_DIR}/vendor}" @@ -967,7 +1050,7 @@ export REPOSITORY_URL="${REPOSITORY_URL:-https://github.com/fchastanet/bash-tool BASH_FRAMEWORK_THEME="${BASH_FRAMEWORK_THEME:-default}" BASH_FRAMEWORK_LOG_LEVEL="${BASH_FRAMEWORK_LOG_LEVEL:-0}" BASH_FRAMEWORK_DISPLAY_LEVEL="${BASH_FRAMEWORK_DISPLAY_LEVEL:-3}" -BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$(basename "$0").log}" +BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/${0##*/}.log}" BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" EOF )" @@ -1274,9 +1357,9 @@ installCommand() { 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::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Install dependent softwares and configuration needed to use bash-tools + Array::wrap2 " " 80 0 "${__HELP_TITLE_COLOR}DESCRIPTION:${__RESET_COLOR}" "Install dependent softwares and configuration needed to use bash-tools - GNU parallel -- Install default configuration files")" +- Install default configuration files" echo echo -e "$(Array::wrap2 " " 80 2 "${__HELP_TITLE_COLOR}USAGE:${__RESET_COLOR}" "${SCRIPT_NAME}" "[OPTIONS]")" diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/pages/_sidebar.md b/pages/_sidebar.md index 7926c9dd..f3d39ac0 100644 --- a/pages/_sidebar.md +++ b/pages/_sidebar.md @@ -1,7 +1,7 @@ - [Home](/) -- [Commands](Commands.md 'The greatest commands in the world') +- [Commands](Commands.md "The greatest commands in the world") - Bash projects suite - [Bash Tools Framework](https://fchastanet.github.io/bash-tools-framework/) - [Bash Dev Env](https://fchastanet.github.io/bash-dev-env/) diff --git a/pages/index.html b/pages/index.html index dd22c904..1f891e30 100644 --- a/pages/index.html +++ b/pages/index.html @@ -19,8 +19,8 @@ href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple-dark.css" />