diff --git a/.checkov.yml b/.checkov.yml new file mode 100644 index 00000000..f0bf6caf --- /dev/null +++ b/.checkov.yml @@ -0,0 +1,2 @@ +skip-path: + - vendor diff --git a/.deepsource.toml b/.deepsource.toml index 247af8bf..ee342ba2 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -15,6 +15,8 @@ enabled = true [[analyzers]] name = "shell" enabled = true + [analyzers.meta] + dialect = "bash" [[analyzers]] name = "docker" diff --git a/.ecrc b/.ecrc index 5b62203a..d5485de8 100644 --- a/.ecrc +++ b/.ecrc @@ -4,7 +4,9 @@ "IgnoreDefaults": false, "SpacesAftertabs": false, "NoColor": false, - "Exclude": [], + "Exclude": [ + "/testsData/" + ], "AllowedContentTypes": [], "PassedFiles": [], "Disable": { diff --git a/.editorconfig b/.editorconfig index ef6f79f3..d2e3815a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,6 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[.vscode/*.json] +indent_size = unset diff --git a/.framework-config b/.framework-config index 5dce3ab3..fa780342 100755 --- a/.framework-config +++ b/.framework-config @@ -25,7 +25,7 @@ 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/|^hooks/|^.github/|^.docker/createUser.|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/_binaries|^src/_includes|^src/batsHeaders.sh$|^src/_standalone)}" +NON_FRAMEWORK_FILES_REGEXP="${NON_FRAMEWORK_FILES_REGEXP:-(^bin/|^hooks/|^test.sh$|^.github/|^.docker/createUser.|.framework-config|.bats$|/testsData/|^manualTests/|/_.sh$|/ZZZ.sh$|/__all.sh$|^src/_binaries|^src/_includes|^src/batsHeaders.sh$|^src/_standalone)}" # describe the files that are allowed to not have an associated bats file BATS_FILE_NOT_NEEDED_REGEXP="${BATS_FILE_NOT_NEEDED_REGEXP:-(^bin/|.framework-config|^.docker/|^.github/|.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 @@ -47,4 +47,4 @@ BASH_FRAMEWORK_LOG_FILE="${BASH_FRAMEWORK_LOG_FILE:-${FRAMEWORK_ROOT_DIR}/logs/$ BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION="${BASH_FRAMEWORK_LOG_FILE_MAX_ROTATION:-5}" # display elapsed time since last log -DISPLAY_DURATION=1 +DISPLAY_DURATION=${DISPLAY_DURATION:-1} diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 5d0d59f9..d3a9d73e 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -8,10 +8,18 @@ on: # yamllint disable-line rule:truthy - "**" # avoid infinite loop for auto created PRs - "!update/pre-commit-*" + # only rely on tag push for master branch + - "!master" tags: - "*" workflow_dispatch: +# 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 @@ -35,7 +43,7 @@ jobs: build-docker-images: runs-on: ubuntu-22.04 permissions: - # needed by ouzi-dev/commit-status-updater@v2 + # needed by akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 statuses: write strategy: fail-fast: true @@ -61,7 +69,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} # overall process - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: build-bash-tools status: pending @@ -81,8 +89,7 @@ jobs: echo "bashImage=amd64/bash:${{ matrix.bashTarVersion }}-alpine3.19" fi ) >> "${GITHUB_ENV}" - - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: build-${{ env.image_tag }} status: pending @@ -103,7 +110,6 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha - - uses: docker/build-push-action@v5 continue-on-error: false with: @@ -117,15 +123,12 @@ jobs: build-args: | BASH_IMAGE: "${{ env.bashImage }}" BASH_TAR_VERSION: "${{ matrix.bashTarVersion }}" - cache-from: | - type=gha,ref=${{ env.image_name }}:${{ env.image_tag }} - type=registry,ref=${{ env.image_name }}:${{ env.image_tag }} - + cache-from: type=gha,scope=${{ env.image_tag }} + cache-to: type=gha,mode=max,scope=${{ env.image_tag }} - name: Check image continue-on-error: false run: | docker run --rm "${{ env.image_name }}:${{ env.image_tag }}" bash --version - - uses: docker/build-push-action@v5 continue-on-error: false with: @@ -138,9 +141,9 @@ jobs: ${{ env.image_name }}:${{ env.image_tag }} ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-to: type=gha,mode=max + cache-to: type=gha,mode=max,scope=${{ env.image_tag }} - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: build-${{ env.image_tag }} status: ${{ job.status }} @@ -153,7 +156,7 @@ jobs: runs-on: ubuntu-22.04 needs: [build-docker-images] permissions: - # needed by ouzi-dev/commit-status-updater@v2 + # needed by akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 statuses: write steps: - name: Checkout @@ -183,7 +186,7 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: pre-commit-megalinter status: pending @@ -199,7 +202,7 @@ jobs: - name: Cache pre-commit uses: actions/cache@v4 env: - cache_name: pre-commit-${{ env.cache_version }} + cache_name: pre-commit hash: ${{hashFiles('**/.pre-commit-config-github.yaml')}} with: path: ~/.cache/pre-commit @@ -235,11 +238,7 @@ jobs: # 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: >- - ${{ - github.event_name == 'push' && - github.ref == 'refs/heads/master' - }} + VALIDATE_ALL_CODEBASE: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MEGALINTER_CONFIG: .mega-linter-githubAction.yml CI_MODE: 1 @@ -301,7 +300,7 @@ jobs: echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 if: ${{ always() }} with: name: pre-commit-megalinter @@ -315,7 +314,7 @@ jobs: runs-on: ubuntu-22.04 needs: [build-docker-images] permissions: - # needed by ouzi-dev/commit-status-updater@v2 + # needed by akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 statuses: write # needed by mikepenz/action-junit-report@v4 checks: write @@ -357,7 +356,7 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: unit-tests-${{matrix.vendor}}-${{matrix.bashTarVersion}} status: pending @@ -382,7 +381,6 @@ jobs: - name: run unit tests id: unitTests - continue-on-error: true run: | set -x set -o errexit @@ -428,7 +426,7 @@ jobs: path: | logs/** - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: unit-tests-${{matrix.vendor}}-${{matrix.bashTarVersion}} status: ${{ job.status }} @@ -439,7 +437,7 @@ jobs: needs: [unit-tests] runs-on: ubuntu-22.04 permissions: - # needed by ouzi-dev/commit-status-updater@v2 + # needed by akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 statuses: write steps: @@ -447,7 +445,7 @@ jobs: # You can get the conclusion via env (env.WORKFLOW_CONCLUSION) - uses: technote-space/workflow-conclusion-action@v3 - - uses: ouzi-dev/commit-status-updater@v2 + - uses: akatov/commit-status-updater@a9e988ec5454692ff7745a509452422a35172ad6 with: name: build-bash-tools # neutral, success, skipped, cancelled, timed_out, action_required, failure diff --git a/.gitignore b/.gitignore index 2d7aad62..de108ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -vendor/ .history/ *.bak commit-msg.md + +# node modules node_modules/ +package*.json +yarn.lock # megalinter megalinter-reports/ diff --git a/.jscpd.json b/.jscpd.json index ce09fff2..f364b131 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -11,7 +11,6 @@ "**/logs/**", "**/megalinter-reports/**", "conf/localAppData/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/originalSettings.json", - "**/snippets/**", "vendor/bats*/**", "pages/README.md", "**/.venv/**", @@ -22,10 +21,10 @@ "**/*.svg", ".pre-commit-config-github.yaml", ".pre-commit-hooks.yaml", - "pages/doc/**", "vendor/**", "**/testsData/**", "manualTests/**", + "pages/doc/**", "pages/BestPractices.md", "pages/CompileCommand.md", "pages/Commands.md", diff --git a/.mega-linter.yml b/.mega-linter.yml index d83b59f1..a47fcb8f 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -105,9 +105,7 @@ JSON_ESLINT_PLUGIN_JSONC_FILE_NAME: .eslintrc.js JSON_JSONLINT_FILTER_REGEX_EXCLUDE: | (?x)( - ^\.vscode/settings\.json| - ^conf/\.vscode/settings\.json| - ^\.vscode/launch\.json + \.vscode/.*\.json$ ) SPELL_CSPELL_CONFIG_FILE: cspell.yaml diff --git a/.pre-commit-config-github.yaml b/.pre-commit-config-github.yaml index 8a7ec46a..bb08732c 100644 --- a/.pre-commit-config-github.yaml +++ b/.pre-commit-config-github.yaml @@ -4,7 +4,7 @@ # DO NOT EDIT IT # @generated ############################################################################### -default_install_hook_types: [pre-commit] +default_install_hook_types: [pre-commit, pre-push] default_stages: [pre-commit, manual] minimum_pre_commit_version: 3.5.0 fail_fast: false @@ -155,7 +155,7 @@ repos: ) - repo: https://github.com/fchastanet/bash-tools-framework - rev: 3.1.0 + rev: 3.2.0 hooks: - id: fixShebangExecutionBit - id: awkLint @@ -165,7 +165,7 @@ repos: args: [ --expected-warnings-count, - "75", + "72", --format, plain, --theme, @@ -177,7 +177,7 @@ repos: args: [ --expected-warnings-count, - "75", + "72", --format, checkstyle, --theme, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27d4eb3d..14b3f62c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ --- -default_install_hook_types: [pre-commit] +default_install_hook_types: [pre-commit, pre-push] default_stages: [pre-commit, manual] minimum_pre_commit_version: 3.5.0 fail_fast: true @@ -150,7 +150,7 @@ repos: ) - repo: https://github.com/fchastanet/bash-tools-framework - rev: 3.1.0 + rev: 3.2.0 hooks: - id: fixShebangExecutionBit - id: awkLint @@ -160,7 +160,7 @@ repos: args: [ --expected-warnings-count, - "75", + "72", --format, plain, --theme, @@ -172,7 +172,7 @@ repos: args: [ --expected-warnings-count, - "75", + "72", --format, checkstyle, --theme, diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a201b4f..06b2d088 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ // all bin except bin/compile "bin/*": true, "bin/compile": false, + "logs": true, "node_modules": true, "backup": true, "megalinter-reports": true, diff --git a/bin/awkLint b/bin/awkLint index f9429513..52699d34 100755 --- a/bin/awkLint +++ b/bin/awkLint @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/buildBinFiles b/bin/buildBinFiles index e4b5d43d..cbeb0657 100755 --- a/bin/buildBinFiles +++ b/bin/buildBinFiles @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -1534,6 +1542,7 @@ runContainer() { --rm -w /bash -v "$(pwd):/bash" + -v "${FRAMEWORK_ROOT_DIR}:/bash/vendor/bash-tools-framework" --entrypoint /usr/local/bin/bash ) # shellcheck disable=SC2154 diff --git a/bin/buildPushDockerImage b/bin/buildPushDockerImage index 16d72028..7ad7dca3 100755 --- a/bin/buildPushDockerImage +++ b/bin/buildPushDockerImage @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/definitionLint b/bin/definitionLint index 11274d70..24a7a0f8 100755 --- a/bin/definitionLint +++ b/bin/definitionLint @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/doc b/bin/doc index 2621f2e5..bbf5c4da 100755 --- a/bin/doc +++ b/bin/doc @@ -157,36 +157,6 @@ Log::displayDebug() { Log::logDebug "$1" } -BASH_FRAMEWORK_SHDOC_INSTALLED_PATH="vendor/.shDocInstalled" -BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT=86400 # 1 day - -# @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 [[ "$( - 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 -} - # @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 @@ -295,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -524,6 +502,74 @@ Array::wrap2() { ) | sed -E -e 's/[[:blank:]]+$//' } +BASH_FRAMEWORK_SHDOC_INSTALLED_PATH="vendor/.shDocInstalled" +BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT=86400 # 1 day + +# @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 [[ "$( + 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 +} + +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installHadolint() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/hadolint}" + Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/hadolint/hadolint/releases/download/v@latestVersion@/hadolint-Linux-x86_64" +} + +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installShellcheck() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck}" + # shellcheck disable=SC2317 + install() { + local file="$1" + local targetFile="$2" + local version="$3" + local tempDir + tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" + ( + cd "${tempDir}" || exit 1 + tar -xJvf "${file}" >&2 + mv "shellcheck-v${version}/shellcheck" "${targetFile}" + chmod +x "${targetFile}" + rm -f "${file}" || true + ) + } + INSTALL_CALLBACK=install Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" +} + # @description checkout usage doc below # # [DockerNamespace usage](DockerUsage.md ':include') @@ -823,75 +869,6 @@ Log::logDebug() { 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 log message to file # @arg $1 message:String the message to display Log::logWarning() { @@ -978,6 +955,116 @@ 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 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}}" + + local repo + repo="$(Github::extractRepoFromGithubUrl "${downloadReleaseUrl}")" + local releasesUrl="https://api.github.com/repos/${repo}/releases/latest" + + # 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 build image and push it to 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} @@ -1247,6 +1334,119 @@ Array::contains() { return 1 } +# @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 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 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 remove ansi codes from input or files given as argument # @arg $@ files:String[] the files to filter # @exitcode * if one of the filter command fails @@ -1284,6 +1484,87 @@ Log::logSkipped() { 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 intermediate callback that is used by Github::upgradeRelease +# or Github::installRelease +# if installCallback is not set, it allows to: +# - copy the downloaded file to the right target file +# - and set the execution bit +# else +# installCallback is called with newSoftware, targetFile, version arguments +# fi +# @warning do not use this function as callback for Github::upgradeRelease or Github::installRelease, as it would result to an infinite loop +# @arg $1 newSoftware:String the downloaded software file +# @arg $2 targetFile:String where we want to copy the file +# @arg $3 version:String the version that has been downloaded +# @arg $4 installCallback:Function (optional) the callback to call with 3 first arguments +# @env SUDO String allows to use custom sudo prefix command +# @exitcode * on failure +# @see Github::upgradeRelease +# @see Github::installRelease +# @internal +Github::defaultInstall() { + local newSoftware="$1" + local targetFile="$2" + local version="$3" + local installCallback=$4 + # shellcheck disable=SC2086 + if ! ${SUDO:-} test -d "${targetFile%/*}"; then + ${SUDO:-} mkdir -p "${targetFile%/*}" + fi + if [[ "$(type -t "${installCallback}")" = "function" ]]; then + ${installCallback} "${newSoftware}" "${targetFile}" "${version}" + else + ${SUDO:-} mv "${newSoftware}" "${targetFile}" + ${SUDO:-} chmod +x "${targetFile}" + hash -r + ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" + fi +} + # @description ensure command git is available # @exitcode 1 if git command not available # @stderr diagnostics information is displayed @@ -1291,6 +1572,64 @@ Git::requireGitCommand() { Assert::commandExists git } +# @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 +} + +# @description Retry a command several times depending on parameters +# @arg $1 maxRetries:int $1 max retries +# @arg $2 delay:int between attempt +# @arg $3 message:String to display to describe the attempt +# @arg $@ rest of parameters, the command to run +# @exitcode 0 on success +# @exitcode 1 if max retries count reached +# @exitcode 2 if maxRetries invalid value +Retry::parameterized() { + local maxRetries=$1 + shift || true + local delayBetweenTries=$1 + shift || true + local message="$1" + shift || true + local retriesCount=1 + if [[ "${maxRetries}" -lt 1 ]]; then + Log::displayError "invalid maxRetry value" + return 2 + fi + + while true; do + Log::displayInfo "Attempt ${retriesCount}/${maxRetries}: ${message}" + if "$@"; then + break + elif [[ "${retriesCount}" -lt "${maxRetries}" ]]; then + Log::displayDebug "Command failed. Wait for ${delayBetweenTries} seconds" + ((retriesCount++)) + sleep "${delayBetweenTries}" + else + Log::displayError "The command has failed after ${retriesCount} attempts." + return 1 + fi + done + return 0 +} + +# @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 check if command specified exists or return 1 # with error and message if not # @@ -1313,6 +1652,14 @@ Assert::commandExists() { return 0 } +# @description log message to file +# @arg $1 message:String the message to display +Log::logSuccess() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SUCCESS}" "$1" + fi +} + # FUNCTIONS facade_main_docsh() { @@ -1326,12 +1673,11 @@ Env::requireLoad UI::requireTheme Log::requireLoad Git::requireGitCommand +Linux::requireJqCommand Compiler::Facade::requireCommandBinDir # @require Compiler::Facade::requireCommandBinDir -ShellDoc::installRequirementsIfNeeded - declare -a BASH_FRAMEWORK_ARGV_FILTERED=() copyrightCallback() { @@ -2155,6 +2501,10 @@ run() { PAGES_DIR="${FRAMEWORK_ROOT_DIR}/pages" if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + ShellDoc::installRequirementsIfNeeded + Softwares::installHadolint + Softwares::installShellcheck + # shellcheck disable=SC2034 local -a dockerRunCmd=( "/bash/bin/doc" diff --git a/bin/dockerLint b/bin/dockerLint index 366ad045..febbb7ce 100755 --- a/bin/dockerLint +++ b/bin/dockerLint @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -549,6 +557,7 @@ Version::checkMinimal() { # @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" @@ -572,7 +581,8 @@ Github::upgradeRelease() { "${targetFile}" \ "${releasesUrl}" \ "${downloadReleaseUrl}" \ - "${softVersionArg}" + "${softVersionArg}" \ + "${EXACT_VERSION:-}" } # @description ensure COMMAND_BIN_DIR env var is set @@ -865,6 +875,7 @@ Version::githubApiExtractVersion() { # @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 @@ -876,6 +887,7 @@ Web::upgradeRelease() { 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}" @@ -891,15 +903,22 @@ Web::upgradeRelease() { if [[ -f "${targetFile}" ]]; then currentVersion="$(${softVersionCallback} "${targetFile}" "${softVersionArg}" 2>&1 || true)" fi - if [[ "${currentVersion}" = "${latestVersion}" ]]; then - Log::displayInfo "${targetFile} version ${latestVersion} already installed" + 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 ${latestVersion}" + Log::displayInfo "Installing ${targetFile} with version ${exactVersion}" else - Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${latestVersion}" + Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${exactVersion}" fi - local url="${downloadReleaseUrl//@latestVersion@/${latestVersion}}" Log::displayInfo "Using url ${url}" newSoftware=$(mktemp -p "${TMPDIR:-/tmp}" -t web.newSoftware.XXXX) Retry::default curl \ @@ -909,7 +928,7 @@ Web::upgradeRelease() { --fail \ "${url}" - Github::defaultInstall "${newSoftware}" "${targetFile}" "${latestVersion}" "${installCallback}" + Github::defaultInstall "${newSoftware}" "${targetFile}" "${exactVersion}" "${installCallback}" fi } @@ -943,7 +962,7 @@ UI::requireTheme() { Web::getReleases() { local releaseListUrl="$1" # Get latest release from GitHub api - Retry::default curl \ + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "Retrieving release versions list ..." curl \ -L \ --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ --fail \ @@ -951,12 +970,32 @@ Web::getReleases() { "${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 5 15 "" "$@" + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "" "$@" } # @description intermediate callback that is used by Github::upgradeRelease @@ -993,6 +1032,7 @@ Github::defaultInstall() { ${SUDO:-} chmod +x "${targetFile}" hash -r ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" fi } @@ -1042,6 +1082,26 @@ Retry::parameterized() { return 0 } +# @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 log message to file +# @arg $1 message:String the message to display +Log::logSuccess() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SUCCESS}" "$1" + fi +} + # FUNCTIONS facade_main_dockerLintsh() { diff --git a/bin/findShebangFiles b/bin/findShebangFiles index 8524dc84..4b615b52 100755 --- a/bin/findShebangFiles +++ b/bin/findShebangFiles @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/frameworkLint b/bin/frameworkLint index a2df7d54..c658bc4a 100755 --- a/bin/frameworkLint +++ b/bin/frameworkLint @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/installRequirements b/bin/installRequirements index 8053c656..dd618edf 100755 --- a/bin/installRequirements +++ b/bin/installRequirements @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -547,6 +555,41 @@ Bats::installRequirementsIfNeeded() { fi } +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installHadolint() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/hadolint}" + Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/hadolint/hadolint/releases/download/v@latestVersion@/hadolint-Linux-x86_64" +} + +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installShellcheck() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck}" + # shellcheck disable=SC2317 + install() { + local file="$1" + local targetFile="$2" + local version="$3" + local tempDir + tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" + ( + cd "${tempDir}" || exit 1 + tar -xJvf "${file}" >&2 + mv "shellcheck-v${version}/shellcheck" "${targetFile}" + chmod +x "${targetFile}" + rm -f "${file}" || true + ) + } + INSTALL_CALLBACK=install Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" +} + # @description ensure COMMAND_BIN_DIR env var is set # and PATH correctly prepared # @noargs @@ -773,6 +816,47 @@ Git::shallowClone() { ) } +# @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}}" + + local repo + repo="$(Github::extractRepoFromGithubUrl "${downloadReleaseUrl}")" + local releasesUrl="https://api.github.com/repos/${repo}/releases/latest" + + # 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 prepend directories to the PATH environment variable # @arg $@ args:String[] list of directories to prepend # @set PATH update PATH with the directories prepended @@ -811,6 +895,288 @@ File::elapsedTimeSinceLastModification() { echo -n "${diff}" } +# @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 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 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 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 intermediate callback that is used by Github::upgradeRelease +# or Github::installRelease +# if installCallback is not set, it allows to: +# - copy the downloaded file to the right target file +# - and set the execution bit +# else +# installCallback is called with newSoftware, targetFile, version arguments +# fi +# @warning do not use this function as callback for Github::upgradeRelease or Github::installRelease, as it would result to an infinite loop +# @arg $1 newSoftware:String the downloaded software file +# @arg $2 targetFile:String where we want to copy the file +# @arg $3 version:String the version that has been downloaded +# @arg $4 installCallback:Function (optional) the callback to call with 3 first arguments +# @env SUDO String allows to use custom sudo prefix command +# @exitcode * on failure +# @see Github::upgradeRelease +# @see Github::installRelease +# @internal +Github::defaultInstall() { + local newSoftware="$1" + local targetFile="$2" + local version="$3" + local installCallback=$4 + # shellcheck disable=SC2086 + if ! ${SUDO:-} test -d "${targetFile%/*}"; then + ${SUDO:-} mkdir -p "${targetFile%/*}" + fi + if [[ "$(type -t "${installCallback}")" = "function" ]]; then + ${installCallback} "${newSoftware}" "${targetFile}" "${version}" + else + ${SUDO:-} mv "${newSoftware}" "${targetFile}" + ${SUDO:-} chmod +x "${targetFile}" + hash -r + ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" + 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 +} + +# @description Retry a command several times depending on parameters +# @arg $1 maxRetries:int $1 max retries +# @arg $2 delay:int between attempt +# @arg $3 message:String to display to describe the attempt +# @arg $@ rest of parameters, the command to run +# @exitcode 0 on success +# @exitcode 1 if max retries count reached +# @exitcode 2 if maxRetries invalid value +Retry::parameterized() { + local maxRetries=$1 + shift || true + local delayBetweenTries=$1 + shift || true + local message="$1" + shift || true + local retriesCount=1 + if [[ "${maxRetries}" -lt 1 ]]; then + Log::displayError "invalid maxRetry value" + return 2 + fi + + while true; do + Log::displayInfo "Attempt ${retriesCount}/${maxRetries}: ${message}" + if "$@"; then + break + elif [[ "${retriesCount}" -lt "${maxRetries}" ]]; then + Log::displayDebug "Command failed. Wait for ${delayBetweenTries} seconds" + ((retriesCount++)) + sleep "${delayBetweenTries}" + else + Log::displayError "The command has failed after ${retriesCount} attempts." + return 1 + fi + done + return 0 +} + +# @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 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 log message to file +# @arg $1 message:String the message to display +Log::logSuccess() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SUCCESS}" "$1" + fi +} + # FUNCTIONS facade_main_installRequirementssh() { @@ -823,6 +1189,7 @@ FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" Env::requireLoad UI::requireTheme Log::requireLoad +Linux::requireJqCommand Compiler::Facade::requireCommandBinDir Linux::requireExecutedAsUser @@ -1465,6 +1832,8 @@ installRequirementsCommand parse "${BASH_FRAMEWORK_ARGV[@]}" run() { mkdir -p "${FRAMEWORK_ROOT_DIR}/vendor" || true Bats::installRequirementsIfNeeded "${FRAMEWORK_ROOT_DIR}" + Softwares::installHadolint + Softwares::installShellcheck } if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then diff --git a/bin/megalinter b/bin/megalinter index af1c4204..4e210a31 100755 --- a/bin/megalinter +++ b/bin/megalinter @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -743,8 +751,10 @@ Log::rotate() { # @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 5 15 "" "$@" + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "" "$@" } # @description extract version number from github api diff --git a/bin/plantuml b/bin/plantuml index 4bf7d70d..8380f316 100755 --- a/bin/plantuml +++ b/bin/plantuml @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/runBuildContainer b/bin/runBuildContainer index 3be0c6bc..c8daf2d5 100755 --- a/bin/runBuildContainer +++ b/bin/runBuildContainer @@ -265,7 +265,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/bin/shellcheckLint b/bin/shellcheckLint index 0ee2b0c1..bd635d4b 100755 --- a/bin/shellcheckLint +++ b/bin/shellcheckLint @@ -199,43 +199,29 @@ Version::checkMinimal() { } -# @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 -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}}" - - local repo - repo="$(Github::extractRepoFromGithubUrl "${downloadReleaseUrl}")" - local releasesUrl="https://api.github.com/repos/${repo}/releases/latest" - +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installShellcheck() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck}" # shellcheck disable=SC2317 - extractVersion() { - Version::githubApiExtractVersion | "${parseGithubVersionCallback}" + install() { + local file="$1" + local targetFile="$2" + local version="$3" + local tempDir + tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" + ( + cd "${tempDir}" || exit 1 + tar -xJvf "${file}" >&2 + mv "shellcheck-v${version}/shellcheck" "${targetFile}" + chmod +x "${targetFile}" + rm -f "${file}" || true + ) } - FILTER_LAST_VERSION_CALLBACK=${FILTER_LAST_VERSION_CALLBACK:-extractVersion} \ - SOFT_VERSION_CALLBACK="${softVersionCallback}" \ - Web::upgradeRelease \ + INSTALL_CALLBACK=install Github::upgradeRelease \ "${targetFile}" \ - "${releasesUrl}" \ - "${downloadReleaseUrl}" \ - "${softVersionArg}" + "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" } # @description Display message using warning color (yellow) @@ -346,7 +332,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 @@ -781,93 +775,45 @@ Version::compare() { return 0 } -# @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 +# @description upgrade given binary to latest github release using retry # -# releasesUrl argument : the placeholder @latestVersion@ will be replaced by the latest release version +# 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 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 $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 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 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 -Web::upgradeRelease() { +# @env EXACT_VERSION if provided, retrieve exact version instead of the latest +Github::upgradeRelease() { local targetFile="$1" - local releasesUrl="$2" - local downloadReleaseUrl="$3" - local softVersionArg="${4:---version}" - # 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 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}}" - local currentVersion="not existing" - if [[ -f "${targetFile}" ]]; then - currentVersion="$(${softVersionCallback} "${targetFile}" "${softVersionArg}" 2>&1 || true)" - fi - if [[ "${currentVersion}" = "${latestVersion}" ]]; then - Log::displayInfo "${targetFile} version ${latestVersion} already installed" - else - if [[ -z "${currentVersion}" ]]; then - Log::displayInfo "Installing ${targetFile} with version ${latestVersion}" - else - Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${latestVersion}" - fi - local url="${downloadReleaseUrl//@latestVersion@/${latestVersion}}" - 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}" + local repo + repo="$(Github::extractRepoFromGithubUrl "${downloadReleaseUrl}")" + local releasesUrl="https://api.github.com/repos/${repo}/releases/latest" - Github::defaultInstall "${newSoftware}" "${targetFile}" "${latestVersion}" "${installCallback}" - fi + # 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 log message to file @@ -979,6 +925,104 @@ UI::requireTheme() { fi } +# @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 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 @@ -986,7 +1030,7 @@ UI::requireTheme() { Web::getReleases() { local releaseListUrl="$1" # Get latest release from GitHub api - Retry::default curl \ + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "Retrieving release versions list ..." curl \ -L \ --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ --fail \ @@ -994,12 +1038,32 @@ Web::getReleases() { "${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 5 15 "" "$@" + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "" "$@" } # @description intermediate callback that is used by Github::upgradeRelease @@ -1036,6 +1100,7 @@ Github::defaultInstall() { ${SUDO:-} chmod +x "${targetFile}" hash -r ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" fi } @@ -1085,6 +1150,26 @@ Retry::parameterized() { return 0 } +# @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 log message to file +# @arg $1 message:String the message to display +Log::logSuccess() { + if ((BASH_FRAMEWORK_LOG_LEVEL >= __LEVEL_INFO)); then + Log::logMessage "${2:-SUCCESS}" "$1" + fi +} + # FUNCTIONS facade_main_shellcheckLintsh() { @@ -1096,30 +1181,15 @@ FRAMEWORK_VENDOR_BIN_DIR="${FRAMEWORK_ROOT_DIR}/vendor/bin" # REQUIRES Env::requireLoad UI::requireTheme -Linux::requireJqCommand Log::requireLoad +Linux::requireJqCommand Compiler::Facade::requireCommandBinDir # @require Compiler::Facade::requireCommandBinDir # check if command in PATH is already the minimal version needed if ! Version::checkMinimal "${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck" "--version" "${MIN_SHELLCHECK_VERSION}" >/dev/null 2>&1; then - install() { - local file="$1" - local targetFile="$2" - local version="$3" - local tempDir - tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" - ( - cd "${tempDir}" || exit 1 - tar -xJvf "${file}" >&2 - mv "shellcheck-v${version}/shellcheck" "${targetFile}" - chmod +x "${targetFile}" - ) - } - INSTALL_CALLBACK=install Github::upgradeRelease \ - "${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck" \ - "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" + Softwares::installShellcheck fi declare -a BASH_FRAMEWORK_ARGV_FILTERED=() diff --git a/bin/test b/bin/test index 06cbc195..020dc430 100755 --- a/bin/test +++ b/bin/test @@ -317,7 +317,15 @@ UI::theme() { # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 diff --git a/commit-msg-template.md b/commit-msg-template.md index b3a275c7..c8194000 100644 --- a/commit-msg-template.md +++ b/commit-msg-template.md @@ -4,18 +4,18 @@ Short 3 lines summary tag: 1.1.5 -# Breaking changes +### Breaking changes -# Bug fixes +### Bug fixes -# Compiler changes +### Compiler changes -# Binaries changes +### Binaries changes -# Updated Bash framework functions +### Updated Bash framework functions -# New Bash framework functions +### New Bash framework functions -# Documentation +### Documentation -# Validation/Tooling +### Validation/Tooling diff --git a/kics.config b/kics.config new file mode 100644 index 00000000..7484fca7 --- /dev/null +++ b/kics.config @@ -0,0 +1,2 @@ +--- +exclude-paths: "vendor/**" diff --git a/package.json b/package.json index 53381f58..2468fa80 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "cspell": "cspell.config.yml", + "cspell": "cspell.yaml", "devDependencies": { "eslint-config-prettier": "^9.1.0", "eslint-plugin-json": "^3.1.0" diff --git a/src/Github/defaultInstall.sh b/src/Github/defaultInstall.sh index 6b912540..cf95ab01 100755 --- a/src/Github/defaultInstall.sh +++ b/src/Github/defaultInstall.sh @@ -34,5 +34,6 @@ Github::defaultInstall() { ${SUDO:-} chmod +x "${targetFile}" hash -r ${SUDO:-} rm -f "${newSoftware}" || true + Log::displaySuccess "Version ${version} installed in ${targetFile}" fi } diff --git a/src/Github/upgradeRelease.sh b/src/Github/upgradeRelease.sh index 87c50b0f..8e6ca305 100755 --- a/src/Github/upgradeRelease.sh +++ b/src/Github/upgradeRelease.sh @@ -13,6 +13,7 @@ # @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" @@ -36,5 +37,6 @@ Github::upgradeRelease() { "${targetFile}" \ "${releasesUrl}" \ "${downloadReleaseUrl}" \ - "${softVersionArg}" + "${softVersionArg}" \ + "${EXACT_VERSION:-}" } diff --git a/src/Retry/default.sh b/src/Retry/default.sh index a50e9180..fc676098 100755 --- a/src/Retry/default.sh +++ b/src/Retry/default.sh @@ -4,6 +4,8 @@ # @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 5 15 "" "$@" + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "" "$@" } diff --git a/src/ShellDoc/installRequirementsIfNeeded.sh b/src/ShellDoc/installRequirementsIfNeeded.sh index 42c06bdb..0be97ae9 100755 --- a/src/ShellDoc/installRequirementsIfNeeded.sh +++ b/src/ShellDoc/installRequirementsIfNeeded.sh @@ -14,6 +14,9 @@ BASH_FRAMEWORK_SHDOC_CHECK_TIMEOUT=86400 # 1 day # @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 [[ "$( Cache::getFileContentIfNotExpired \ "${BASH_FRAMEWORK_SHDOC_INSTALLED}" \ diff --git a/src/Softwares/installHadolint.sh b/src/Softwares/installHadolint.sh new file mode 100755 index 00000000..38b05172 --- /dev/null +++ b/src/Softwares/installHadolint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installHadolint() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/hadolint}" + Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/hadolint/hadolint/releases/download/v@latestVersion@/hadolint-Linux-x86_64" +} diff --git a/src/Softwares/installShellcheck.sh b/src/Softwares/installShellcheck.sh new file mode 100755 index 00000000..db9c1875 --- /dev/null +++ b/src/Softwares/installShellcheck.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# @description install hadolint if necessary +# @arg $1 targetFile:String +# @feature Github::upgradeRelease +Softwares::installShellcheck() { + local targetFile="${1:-${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck}" + # shellcheck disable=SC2317 + install() { + local file="$1" + local targetFile="$2" + local version="$3" + local tempDir + tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" + ( + cd "${tempDir}" || exit 1 + tar -xJvf "${file}" >&2 + mv "shellcheck-v${version}/shellcheck" "${targetFile}" + chmod +x "${targetFile}" + rm -f "${file}" || true + ) + } + INSTALL_CALLBACK=install Github::upgradeRelease \ + "${targetFile}" \ + "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" +} diff --git a/src/UI/drawLine.sh b/src/UI/drawLine.sh index a9e01fc1..b8fb36a7 100755 --- a/src/UI/drawLine.sh +++ b/src/UI/drawLine.sh @@ -4,5 +4,13 @@ # @arg $1 character:String character to use as separator (default value #) UI::drawLine() { local character="${1:-#}" - printf -- "${character}%.0s" $(seq "${COLUMNS:-$([[ -t 0 ]] && tput cols || echo '80')}") + 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 } diff --git a/src/UI/drawLineWithMsg.sh b/src/UI/drawLineWithMsg.sh index a5fd4b42..22515b19 100755 --- a/src/UI/drawLineWithMsg.sh +++ b/src/UI/drawLineWithMsg.sh @@ -15,10 +15,8 @@ UI::drawLineWithMsg() { # compute screen width local -i width=${COLUMNS:-0} - if ((width == 0)); then - if [[ -t 0 ]]; then - width=$(tput cols) - fi + if ((width == 0)) && [[ -t 1 ]]; then + width=$(tput cols) fi if ((width == 0)); then width=80 @@ -38,4 +36,5 @@ UI::drawLineWithMsg() { printf -- "${character}%.0s" $(seq "${leftWidth}") echo -n " ${msg} " printf -- "${character}%.0s" $(seq "${rightWidth}") + echo } diff --git a/src/Web/getReleases.bats b/src/Web/getReleases.bats index 2702c1b5..89b070c3 100755 --- a/src/Web/getReleases.bats +++ b/src/Web/getReleases.bats @@ -17,7 +17,8 @@ function teardown() { function Web::getReleases::curlFailure { #@test stub curl '-L --connect-timeout 5 --fail --silent invalidUrl : exit 1' - Retry::default() { + Retry::parameterized() { + shift 3 "$@" } run Web::getReleases "invalidUrl" 2>&1 @@ -27,7 +28,8 @@ function Web::getReleases::curlFailure { #@test function Web::getReleases::curlSuccess { #@test stub curl '-L --connect-timeout 5 --fail --silent validUrl : echo versions' - Retry::default() { + Retry::parameterized() { + shift 3 "$@" } run Web::getReleases "validUrl" 2>&1 diff --git a/src/Web/getReleases.sh b/src/Web/getReleases.sh index 90ddf9fb..760a36f0 100755 --- a/src/Web/getReleases.sh +++ b/src/Web/getReleases.sh @@ -7,7 +7,7 @@ Web::getReleases() { local releaseListUrl="$1" # Get latest release from GitHub api - Retry::default curl \ + Retry::parameterized "${RETRY_MAX_RETRY:-5}" "${RETRY_DELAY_BETWEEN_RETRIES:-15}" "Retrieving release versions list ..." curl \ -L \ --connect-timeout "${CURL_CONNECT_TIMEOUT:-5}" \ --fail \ diff --git a/src/Web/upgradeRelease.bats b/src/Web/upgradeRelease.bats index 5f9ed734..ffdd2a3f 100755 --- a/src/Web/upgradeRelease.bats +++ b/src/Web/upgradeRelease.bats @@ -23,12 +23,19 @@ function Web::upgradeRelease::success { #@test Retry::default() { "$@" } + Retry::parameterized() { + shift 3 + "$@" + } filterLatestNonBetaVersion() { jq -r '.[].files[].version' } getGoVersion() { echo "1.22.1" } + Github::isReleaseVersionExist() { + return 0 + } Github::defaultInstall() { echo "install $2 $3" } diff --git a/src/Web/upgradeRelease.sh b/src/Web/upgradeRelease.sh index ca454ac0..43fef5a3 100755 --- a/src/Web/upgradeRelease.sh +++ b/src/Web/upgradeRelease.sh @@ -7,6 +7,7 @@ # @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 @@ -18,6 +19,7 @@ Web::upgradeRelease() { 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}" @@ -33,15 +35,22 @@ Web::upgradeRelease() { if [[ -f "${targetFile}" ]]; then currentVersion="$(${softVersionCallback} "${targetFile}" "${softVersionArg}" 2>&1 || true)" fi - if [[ "${currentVersion}" = "${latestVersion}" ]]; then - Log::displayInfo "${targetFile} version ${latestVersion} already installed" + 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 ${latestVersion}" + Log::displayInfo "Installing ${targetFile} with version ${exactVersion}" else - Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${latestVersion}" + Log::displayInfo "Upgrading ${targetFile} from version ${currentVersion} to ${exactVersion}" fi - local url="${downloadReleaseUrl//@latestVersion@/${latestVersion}}" Log::displayInfo "Using url ${url}" newSoftware=$(mktemp -p "${TMPDIR:-/tmp}" -t web.newSoftware.XXXX) Retry::default curl \ @@ -51,6 +60,6 @@ Web::upgradeRelease() { --fail \ "${url}" - Github::defaultInstall "${newSoftware}" "${targetFile}" "${latestVersion}" "${installCallback}" + Github::defaultInstall "${newSoftware}" "${targetFile}" "${exactVersion}" "${installCallback}" fi } diff --git a/src/_binaries/buildBinFiles.sh b/src/_binaries/buildBinFiles.sh index ab791ca0..875da5b8 100755 --- a/src/_binaries/buildBinFiles.sh +++ b/src/_binaries/buildBinFiles.sh @@ -89,6 +89,7 @@ runContainer() { --rm -w /bash -v "$(pwd):/bash" + -v "${FRAMEWORK_ROOT_DIR}:/bash/vendor/bash-tools-framework" --entrypoint /usr/local/bin/bash ) # shellcheck disable=SC2154 diff --git a/src/_binaries/doc.sh b/src/_binaries/doc.sh index f306a0ba..6bbc8ce2 100755 --- a/src/_binaries/doc.sh +++ b/src/_binaries/doc.sh @@ -3,8 +3,6 @@ # VAR_RELATIVE_FRAMEWORK_DIR_TO_CURRENT_DIR=.. # FACADE -ShellDoc::installRequirementsIfNeeded - .INCLUDE "$(dynamicTemplateDir _binaries/options/command.doc.tpl)" docCommand parse "${BASH_FRAMEWORK_ARGV[@]}" @@ -13,6 +11,10 @@ run() { PAGES_DIR="${FRAMEWORK_ROOT_DIR}/pages" if [[ "${IN_BASH_DOCKER:-}" != "You're in docker" ]]; then + ShellDoc::installRequirementsIfNeeded + Softwares::installHadolint + Softwares::installShellcheck + # shellcheck disable=SC2034 local -a dockerRunCmd=( "/bash/bin/doc" diff --git a/src/_binaries/installRequirements.sh b/src/_binaries/installRequirements.sh index 3638f88a..fd4ca0e1 100755 --- a/src/_binaries/installRequirements.sh +++ b/src/_binaries/installRequirements.sh @@ -13,6 +13,8 @@ declare optionBashFrameworkConfig="${FRAMEWORK_ROOT_DIR}/.framework-config" run() { mkdir -p "${FRAMEWORK_ROOT_DIR}/vendor" || true Bats::installRequirementsIfNeeded "${FRAMEWORK_ROOT_DIR}" + Softwares::installHadolint + Softwares::installShellcheck } if [[ "${BASH_FRAMEWORK_QUIET_MODE:-0}" = "1" ]]; then diff --git a/src/_binaries/shellcheckLint.sh b/src/_binaries/shellcheckLint.sh index 518483af..81a7325d 100755 --- a/src/_binaries/shellcheckLint.sh +++ b/src/_binaries/shellcheckLint.sh @@ -5,22 +5,7 @@ # check if command in PATH is already the minimal version needed if ! Version::checkMinimal "${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck" "--version" "${MIN_SHELLCHECK_VERSION}" >/dev/null 2>&1; then - install() { - local file="$1" - local targetFile="$2" - local version="$3" - local tempDir - tempDir="$(mktemp -d -p "${TMPDIR:-/tmp}" -t bash-framework-shellcheck-$$-XXXXXX)" - ( - cd "${tempDir}" || exit 1 - tar -xJvf "${file}" >&2 - mv "shellcheck-v${version}/shellcheck" "${targetFile}" - chmod +x "${targetFile}" - ) - } - INSTALL_CALLBACK=install Github::upgradeRelease \ - "${FRAMEWORK_VENDOR_BIN_DIR}/shellcheck" \ - "https://github.com/koalaman/shellcheck/releases/download/v@latestVersion@/shellcheck-v@latestVersion@.linux.x86_64.tar.xz" + Softwares::installShellcheck fi .INCLUDE "$(dynamicTemplateDir _binaries/options/command.shellcheckLint.tpl)" diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..588045e0 --- /dev/null +++ b/test.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +REAL_SCRIPT_FILE="$(readlink -e "$(realpath "${BASH_SOURCE[0]}")")" +CURRENT_DIR="${REAL_SCRIPT_FILE%/*}" + +set -o errexit +set -o pipefail + +declare image="$1" +shift || true + +if [[ -z "${image}" || "${image}" = "" ]]; then + echo "try : ./test.sh scrasnups/build:bash-tools-alpine-5.0 -r src -j 30" + echo "or : ./test.sh scrasnups/build:bash-tools-ubuntu-5.3 -r src -j 30" + echo "display bats help : ./test.sh scrasnups/build:bash-tools-ubuntu-5.3 --help" + exit 0 +fi + +# build docker image +if [[ "${CI_MODE:-0}" = "1" ]] || ! docker inspect --type=image "${image}" &>/dev/null; then + docker pull "${image}" +fi + +# run docker image +declare -a localDockerRunArgs=( + --rm + -e KEEP_TEMP_FILES="${KEEP_TEMP_FILES:-0}" + -e BATS_FIX_TEST="${BATS_FIX_TEST:-0}" + -e USER_ID="${USER_ID:-1000}" + -e GROUP_ID="${GROUP_ID:-1000}" + --user "www-data:www-data" + -w /bash + -v "${CURRENT_DIR}:/bash" + --entrypoint /usr/local/bin/bash +) +# shellcheck disable=SC2154 +if [[ "${CI_MODE:-0}" = "0" ]]; then + localDockerRunArgs+=(-v "/tmp:/tmp") + localDockerRunArgs+=(-it) +fi + +set -x +docker run \ + "${localDockerRunArgs[@]}" \ + "${image}" \ + /bash/vendor/bats/bin/bats \ + "$@" diff --git a/trivy.yaml b/trivy.yaml new file mode 100644 index 00000000..d9f30ab7 --- /dev/null +++ b/trivy.yaml @@ -0,0 +1,5 @@ +scan: + # Same as '--skip-dirs' + # Default is empty + skip-dirs: + - vendor/ diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 00000000..1287e9bd --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1,2 @@ +** +!.gitignore