From bb58e7295a398c69aea847e7b77df3e3f7769b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Sat, 13 Jan 2024 01:27:00 +0100 Subject: [PATCH] Simplified Array::wrap2 algorithm and performance improvement performance improvement using: - echo instead of string concatenation - string substitution instead of calling sed on each element - echo -e removed the need to do a loop on each character to parse ansi code and the need of Filters::removeAnsiCodes --- .github/workflows/lint-test.yml | 7 +- .markdown-link-check.json | 5 +- .mega-linter.yml | 3 +- .pre-commit-config.yaml | 9 +- .vscode/bats.code-snippets | 2 +- .vscode/shdoc.code-snippets | 2 +- README.md | 12 +- bin/awkLint | 194 +++++++--------- bin/buildBinFiles | 194 +++++++--------- bin/buildPushDockerImage | 194 +++++++--------- bin/definitionLint | 194 +++++++--------- bin/doc | 207 ++++++++---------- bin/dockerLint | 194 +++++++--------- bin/findShebangFiles | 194 +++++++--------- bin/frameworkLint | 194 +++++++--------- bin/megalinter | 194 +++++++--------- bin/plantuml | 194 +++++++--------- bin/runBuildContainer | 194 +++++++--------- bin/shellcheckLint | 194 +++++++--------- bin/test | 194 +++++++--------- manualTests/Array::wrap.sh | 42 ++-- manualTests/Array::wrap2Perf.sh | 47 ++++ .../array_wrap2_argFunction.expected.result | 6 +- ...ptyLinesWithForcedNewLines.expected.result | 2 +- .../array_wrap2_indent3.expected.result | 5 +- .../array_wrap2_multilineArg.expected.result | 28 +-- ...lShouldGenerateJustOneEcho.expected.result | 13 ++ src/Array/wrap2.bats | 93 +++++--- src/Array/wrap2.sh | 179 +++++++-------- src/Options/_bats.sh | 2 +- src/Options/generateCommand.bats | 6 +- .../generateCommand.case7.expected.help | 25 ++- .../testsData/generateCommand.case7.sh | 2 +- 33 files changed, 1409 insertions(+), 1616 deletions(-) create mode 100755 manualTests/Array::wrap2Perf.sh create mode 100644 src/Array/testsData/nlShouldGenerateJustOneEcho.expected.result diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 214a8d68..967e63d7 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -11,6 +11,9 @@ on: # yamllint disable-line rule:truthy jobs: build: runs-on: ubuntu-22.04 + permissions: + # needed by ouzi-dev/commit-status-updater@v2 + statuses: write strategy: fail-fast: true matrix: @@ -111,7 +114,7 @@ jobs: - name: Archive results if: matrix.runPrecommitTests && always() continue-on-error: true - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linter-reports path: | @@ -151,7 +154,7 @@ jobs: - name: Upload Test Results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results ${{matrix.vendor}} ${{matrix.bashTarVersion}} path: logs/report.xml diff --git a/.markdown-link-check.json b/.markdown-link-check.json index 2b107efe..75b21201 100644 --- a/.markdown-link-check.json +++ b/.markdown-link-check.json @@ -6,7 +6,10 @@ "pattern": "^http://localhost:3000/" }, { - "pattern": "^https://www.cyberciti.biz/.*$" + "pattern": "^https://www.cyberciti.biz/" + }, + { + "pattern": "^https://tinyurl.com/" }, { "pattern": "^#.*" diff --git a/.mega-linter.yml b/.mega-linter.yml index 173b2072..c031702e 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -71,7 +71,8 @@ EDITORCONFIG_EDITORCONFIG_CHECKER_FILTER_REGEX_EXCLUDE: | bin/bash-tpl| ^doc/guides/Options/generate.*\.md$| ^pages/Commands.md - ^.*-megalinter_file_names_cspell.txt + ^.*-megalinter_file_names_cspell.txt| + testsData/.*\.result$ ) GIT_GIT_DIFF_PRE_COMMANDS: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91993c2b..6b23bce8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,8 @@ repos: exclude: | (?x)( ^doc/images/.*\.svg$| - ^doc/guides/Options/generate.*.md$ + ^doc/guides/Options/generate.*.md$| + testsData/.*\.result$ ) - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -49,7 +50,11 @@ repos: rev: v4.0.0-alpha.8 hooks: - id: prettier - exclude: ^doc/guides/Options/generate.*.md$ + exclude: | + (?x)( + ^doc/guides/Options/generate.*.md$| + ^\.vscode/.*\.code-snippets$ + ) - repo: https://github.com/fchastanet/bash-tools-framework rev: 2.1.0 diff --git a/.vscode/bats.code-snippets b/.vscode/bats.code-snippets index 4f59c93f..b2ded7df 100644 --- a/.vscode/bats.code-snippets +++ b/.vscode/bats.code-snippets @@ -15,6 +15,6 @@ "[[ \"\\${status}\" = \"0\" ]]", "run cat \"\\${BATS_TEST_TMPDIR}/result\"" ], - "description": "bats get status and output" + "description": "bats get status and output", } } diff --git a/.vscode/shdoc.code-snippets b/.vscode/shdoc.code-snippets index e2f33cdf..b2c86758 100644 --- a/.vscode/shdoc.code-snippets +++ b/.vscode/shdoc.code-snippets @@ -24,6 +24,6 @@ "# @require Git::requireGitCommand", "# @feature Retry::default, sudo" ], - "description": "bash function generic shdoc" + "description": "bash function generic shdoc", } } diff --git a/README.md b/README.md index 23e5ae3d..605422f1 100644 --- a/README.md +++ b/README.md @@ -389,10 +389,18 @@ bin/compile src/_binaries/testsData/bin/embed.sh --template-dir src --bin-dir bi echo $? # prints 127 # try to get more logs -KEEP_TEMP_FILES=1 BASH_FRAMEWORK_DISPLAY_LEVEL=4 bin/compile src/_binaries/testsData/bin/embed.sh --template-dir src --bin-dir bin --root-dir $PWD --src-dir src/_binaries/testsData/src +KEEP_TEMP_FILES=1 BASH_FRAMEWORK_DISPLAY_LEVEL=4 bin/compile \ + src/_binaries/testsData/bin/embed.sh \ + --template-dir src \ + --bin-dir bin \ + --root-dir "${PWD}" \ + --src-dir src/_binaries/testsData/src # try to use strace -docker run --rm -it -w /bash -v "$(pwd):/bash" --entrypoint="" build:bash-tools-alpine-4.4-user bash +docker run --rm -it \ + -w /bash -v "$(pwd):/bash" \ + --entrypoint="" \ + build:bash-tools-alpine-4.4-user bash apk update apk add strace ``` diff --git a/bin/awkLint b/bin/awkLint index 85f3425b..68e21b9d 100755 --- a/bin/awkLint +++ b/bin/awkLint @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -523,19 +510,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/buildBinFiles b/bin/buildBinFiles index 06af60d6..e3aa4979 100755 --- a/bin/buildBinFiles +++ b/bin/buildBinFiles @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -523,19 +510,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/buildPushDockerImage b/bin/buildPushDockerImage index e628daa2..fd91904a 100755 --- a/bin/buildPushDockerImage +++ b/bin/buildPushDockerImage @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description checkout usage doc below @@ -584,19 +571,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/definitionLint b/bin/definitionLint index cafbcd2f..c0dd78d9 100755 --- a/bin/definitionLint +++ b/bin/definitionLint @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -704,19 +691,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description Display message using success color (bg green/fg white) # @arg $1 message:String the message to display Log::displaySuccess() { diff --git a/bin/doc b/bin/doc index d91a7fe3..bac04786 100755 --- a/bin/doc +++ b/bin/doc @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description checkout usage doc below @@ -886,19 +873,6 @@ File::replaceTokenByInput() { ) } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description 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 @@ -1146,6 +1120,19 @@ File::elapsedTimeSinceLastModification() { echo -n "${diff}" } +# @description remove ansi codes from input or files given as argument +# @arg $@ files:String[] the files to filter +# @exitcode * if one of the filter command fails +# @stdin you can use stdin as alternative to files argument +# @stdout the filtered content +# @see https://en.wikipedia.org/wiki/ANSI_escape_code +# shellcheck disable=SC2120 +Filters::removeAnsiCodes() { + # cspell:disable + sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" + # cspell:enable +} + # @description pull git directory only if no change has been detected # @arg $1 dir:String the git directory to pull # @exitcode 0 on successful pulling diff --git a/bin/dockerLint b/bin/dockerLint index 44ff284c..47a36ee2 100755 --- a/bin/dockerLint +++ b/bin/dockerLint @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -666,19 +653,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description intermediate callback that is used by Github::upgradeRelease # or Github::installRelease # if installCallback is not set, it allows to: diff --git a/bin/findShebangFiles b/bin/findShebangFiles index 8bc6782d..e764691e 100755 --- a/bin/findShebangFiles +++ b/bin/findShebangFiles @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure that file begin with a bash shebang @@ -565,19 +552,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/frameworkLint b/bin/frameworkLint index 46fc49b8..39d4a062 100755 --- a/bin/frameworkLint +++ b/bin/frameworkLint @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure that file begin with a bash shebang @@ -654,19 +641,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/megalinter b/bin/megalinter index 69496b18..79e5c391 100755 --- a/bin/megalinter +++ b/bin/megalinter @@ -117,10 +117,12 @@ Array::join() { # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -142,117 +144,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -610,19 +597,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/plantuml b/bin/plantuml index b78baaec..28b87721 100755 --- a/bin/plantuml +++ b/bin/plantuml @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure env files are loaded @@ -573,19 +560,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/runBuildContainer b/bin/runBuildContainer index b451141d..7afd92e6 100755 --- a/bin/runBuildContainer +++ b/bin/runBuildContainer @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description checkout usage doc below @@ -683,19 +670,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description log message to file # @arg $1 message:String the message to display Log::logDebug() { diff --git a/bin/shellcheckLint b/bin/shellcheckLint index aa200dd1..0da66fca 100755 --- a/bin/shellcheckLint +++ b/bin/shellcheckLint @@ -101,10 +101,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -126,117 +128,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } # @description ensure that file begin with a bash shebang @@ -718,19 +705,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description intermediate callback that is used by Github::upgradeRelease # or Github::installRelease # if installCallback is not set, it allows to: diff --git a/bin/test b/bin/test index 1bde5341..cc4fb6a8 100755 --- a/bin/test +++ b/bin/test @@ -100,10 +100,12 @@ trap cleanOnExit EXIT HUP QUIT ABRT TERM # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -125,117 +127,102 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" - - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + ((argLength = ${#arg})) || true + + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } BASH_FRAMEWORK_BATS_DEPENDENCIES_CHECK_TIMEOUT=86400 # 1 day @@ -754,19 +741,6 @@ Env::pathPrepend() { done } -# @description remove ansi codes from input or files given as argument -# @arg $@ files:String[] the files to filter -# @exitcode * if one of the filter command fails -# @stdin you can use stdin as alternative to files argument -# @stdout the filtered content -# @see https://en.wikipedia.org/wiki/ANSI_escape_code -# shellcheck disable=SC2120 -Filters::removeAnsiCodes() { - # cspell:disable - sed -E 's/\x1b\[[0-9;]*[mGKHF]//g' "$@" - # cspell:enable -} - # @description shallow clone a repository at specific commit sha, tag or branch # or update repo if already exists # diff --git a/manualTests/Array::wrap.sh b/manualTests/Array::wrap.sh index 17ab3090..b8be72d3 100755 --- a/manualTests/Array::wrap.sh +++ b/manualTests/Array::wrap.sh @@ -46,25 +46,29 @@ UI::theme "default" # echo "if first arg is not a profile" # } -declare -a helpArray=( - $'\n Common Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Log in to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information' -) +# declare -a helpArray=( +# $'\n Common Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Log in to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information' +# ) -echo "-------------" -for ((j = 0; j < 1; j++)); do - Array::wrap2 " " 20 4 "${helpArray[@]}" -done -echo "-------------" +# echo "-------------" +# for ((j = 0; j < 1; j++)); do +# Array::wrap2 " " 20 4 "${helpArray[@]}" +# done +# echo "-------------" -echo "@@@@@@@@@@@@@ using fold" -fold -s -w 20 - <<<"${helpArray[@]}" -time ( - for ((j = 0; j < 100; j++)); do - fold -w 20 - <<<"${helpArray[@]}" &>/dev/null - done -) -echo "@@@@@@@@@@@@@" +# echo "@@@@@@@@@@@@@ using fold" +# fold -s -w 20 - <<<"${helpArray[@]}" +# time ( +# for ((j = 0; j < 100; j++)); do +# fold -w 20 - <<<"${helpArray[@]}" &>/dev/null +# done +# ) +# echo "@@@@@@@@@@@@@" -echo "############# using fmt" -fmt --width=20 --goal=20 -s -t -p ' ' - <<<"${helpArray[@]}" | pr -T --indent=4 -echo "#############" +# echo "############# using fmt" +# fmt --width=20 --goal=20 -s -t -p ' ' - <<<"${helpArray[@]}" | pr -T --indent=4 +# echo "#############" + +declare -a helpArray=($' pull Download an image from a registry\n push Upload an image to a registry\r\n images List images\r\n login Log in to a registry\r\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information') + +Array::wrap2 " " 76 4 "${helpArray[@]}" diff --git a/manualTests/Array::wrap2Perf.sh b/manualTests/Array::wrap2Perf.sh new file mode 100755 index 00000000..e5e6bbbc --- /dev/null +++ b/manualTests/Array::wrap2Perf.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +rootDir="$(cd "$(readlink -e "${BASH_SOURCE[0]%/*}")/.." && pwd -P)" +export FRAMEWORK_ROOT_DIR="${rootDir}" +export _COMPILE_ROOT_DIR="${rootDir}" + +srcDir="$(cd "${rootDir}/src" && pwd -P)" + +# shellcheck source=src/Assert/tty.sh +source "${srcDir}/Assert/tty.sh" +# shellcheck source=src/Filters/removeAnsiCodes.sh +source "${srcDir}/Filters/removeAnsiCodes.sh" +# shellcheck source=src/Array/wrap.sh +source "${srcDir}/Array/wrap.sh" +# shellcheck source=src/Array/wrap2.sh +source "${srcDir}/Array/wrap2.sh" +# shellcheck source=/src/Log/__all.sh +source "${srcDir}/Log/__all.sh" +# shellcheck source=/src/UI/theme.sh +source "${srcDir}/UI/theme.sh" + +#set -x +set -o errexit +set -o pipefail +export TMPDIR="/tmp" + +UI::theme "default" + +time ( + for ((c = 1; c <= 100; c++)); do + Array::wrap2 ' ' 76 4 $' +${__HELP_TITLE_COLOR}Common Commands:${__RESET_COLOR} +run Create and run a new container from an image +exec Execute a command in a running container +ps List containers +build Build an image from a Dockerfile +pull Download an image from a registry +push Upload an image to a registry\r +images List images\r +login Log in to a registry\r +logout Log out from a registry +search Search Docker Hub for images +version Show the Docker version information +info Display system-wide information +' + done +) diff --git a/src/Array/testsData/array_wrap2_argFunction.expected.result b/src/Array/testsData/array_wrap2_argFunction.expected.result index 1cc4d442..5a0d0cf7 100644 --- a/src/Array/testsData/array_wrap2_argFunction.expected.result +++ b/src/Array/testsData/array_wrap2_argFunction.expected.result @@ -1,4 +1,4 @@ -container should be the name of a profile from profile list,check containers - list below. +containers should be the name of list from list, check container list below. + If not provided, it will load the container specified in default configu - ration. + ration. \ No newline at end of file diff --git a/src/Array/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result b/src/Array/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result index 837ea9ee..fa8e758f 100644 --- a/src/Array/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result +++ b/src/Array/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result @@ -1,3 +1,3 @@ line1 - line2 + line2 \ No newline at end of file diff --git a/src/Array/testsData/array_wrap2_indent3.expected.result b/src/Array/testsData/array_wrap2_indent3.expected.result index 99da2eb1..280f3409 100644 --- a/src/Array/testsData/array_wrap2_indent3.expected.result +++ b/src/Array/testsData/array_wrap2_indent3.expected.result @@ -1,5 +1,6 @@ Description: lint awk files + + Lint all files with .awk extension in specified folder. -Filters out eventual .history folder -Result in checkstyle format. +Filters out eventual .history folder Result in checkstyle format. \ No newline at end of file diff --git a/src/Array/testsData/array_wrap2_multilineArg.expected.result b/src/Array/testsData/array_wrap2_multilineArg.expected.result index 41f0191c..e6314c5d 100644 --- a/src/Array/testsData/array_wrap2_multilineArg.expected.result +++ b/src/Array/testsData/array_wrap2_multilineArg.expected.result @@ -1,13 +1,15 @@ - Common Commands: - run Create and run a new container from an image - exec Execute a command in a running container - ps List containers - build Build an image from a Dockerfile - pull Download an image from a registry - push Upload an image to a registry - images List images - login Log in to a registry - logout Log out from a registry - search Search Docker Hub for images - version Show the Docker version information - info Display system-wide information + + + Common Commands: + run Create and run a new container from an image + exec Execute a command in a running container + ps List containers + build Build an image from a Dockerfile + pull Download an image from a registry + push Upload an image to a registry + images List images + login Log in to a registry + logout Log out from a registry + search Search Docker Hub for images + version Show the Docker version information + info Display system-wide information \ No newline at end of file diff --git a/src/Array/testsData/nlShouldGenerateJustOneEcho.expected.result b/src/Array/testsData/nlShouldGenerateJustOneEcho.expected.result new file mode 100644 index 00000000..b4b4ee65 --- /dev/null +++ b/src/Array/testsData/nlShouldGenerateJustOneEcho.expected.result @@ -0,0 +1,13 @@ + + ${__HELP_TITLE_COLOR}Common Commands:${__RESET_COLOR} + run Create and run a new container from an image + exec Execute a command in a running container + ps List containers build Build an image from a Dockerfile + pull Download an image from a registry + push Upload an image to a registry + images List images + login Log in to a registry + logout Log out from a registry + search Search Docker Hub for images + version Show the Docker version information + info Display system-wide information diff --git a/src/Array/wrap2.bats b/src/Array/wrap2.bats index 03e2485c..f87366cc 100755 --- a/src/Array/wrap2.bats +++ b/src/Array/wrap2.bats @@ -41,9 +41,9 @@ function Array::wrap2::OneArrayElement::noWrap { #@test function Array::wrap2::OneArrayElement::wrap { #@test run Array::wrap2 ":" 80 0 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ac elit id massa condimentum finibus." assert_success - assert_lines_count 2 assert_line --index 0 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ac elit id ma" assert_line --index 1 "ssa condimentum finibus." + assert_lines_count 2 } function Array::wrap2::4ArrayElement::wrap40 { #@test @@ -54,11 +54,11 @@ function Array::wrap2::4ArrayElement::wrap40 { #@test "condimentum finibus." assert_success - assert_lines_count 4 assert_line --index 0 "Lorem ipsum dolor sit amet," assert_line --index 1 "consectetur adipiscing elit." assert_line --index 2 "Curabitur ac elit id massa" assert_line --index 3 "condimentum finibus." + assert_lines_count 4 } function Array::wrap2::4ArrayElement::wrap20 { #@test @@ -68,14 +68,13 @@ function Array::wrap2::4ArrayElement::wrap20 { #@test "Curabitur ac elit id massa" \ "condimentum finibus." assert_success - assert_lines_count 7 assert_line --index 0 "Lorem ipsum dolor si" - assert_line --index 1 "t amet," - assert_line --index 2 "consectetur adipisci" - assert_line --index 3 "ng elit." - assert_line --index 4 "Curabitur ac elit id" - assert_line --index 5 "massa" - assert_line --index 6 "condimentum finibus." + assert_line --index 1 "t amet,:consectetur" + assert_line --index 2 "adipiscing elit.:Cur" + assert_line --index 3 "abitur ac elit id ma" + assert_line --index 4 "ssa:condimentum fini" + assert_line --index 5 "bus." + assert_lines_count 6 } function Array::wrap2::realExample::noIndent { #@test @@ -89,11 +88,11 @@ function Array::wrap2::realExample::noIndent { #@test "[--copyright ]" \ "[--help-template ]" assert_success - assert_lines_count 4 assert_line --index 0 "Usage: Options::generateCommand --help " assert_line --index 1 "[--command-name ] [--version ]" assert_line --index 2 "[--author ] [--License ]" assert_line --index 3 "[--copyright ] [--help-template ]" + assert_lines_count 4 } function Array::wrap2::realExample::indent2 { #@test @@ -107,30 +106,30 @@ function Array::wrap2::realExample::indent2 { #@test "[--copyright ]" \ "[--help-template ]" assert_success - assert_lines_count 4 assert_line --index 0 "Usage: Options::generateCommand --help " assert_line --index 1 " [--command-name ] [--version ]" assert_line --index 2 " [--author ] [--License ]" assert_line --index 3 " [--copyright ] [--help-template ]" + assert_lines_count 4 } function Array::wrap2::realExample::indent3 { #@test - Array::wrap2 " " 80 0 "\e[32mDescription:\e[0m" "lint awk files + Array::wrap2 " " 80 0 "\e[32mDescription:\e[0m" $'lint awk files \n Lint all files with .awk extension in specified folder. Filters out eventual .history folder -Result in checkstyle format." >"${BATS_TEST_TMPDIR}/result" - diff -u "${BATS_TEST_TMPDIR}/result" <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_indent3.expected.result") >&3 +Result in checkstyle format.' >"${BATS_TEST_TMPDIR}/result" + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_indent3.expected.result") "${BATS_TEST_TMPDIR}/result" >&3 } function Array::wrap2::realExample::indent4 { #@test run Array::wrap2 " " 80 0 "USAGE: awkLint" "[--display-level ]" \ "[--help|-h]" "[--log-level ]" "[--no-color]" "[--quiet|-q]" \ "[--verbose|-v]" "[--version]" - assert_lines_count 2 assert_line --index 0 "USAGE: awkLint [--display-level ] [--help|-h] [--log-level ]" assert_line --index 1 "[--no-color] [--quiet|-q] [--verbose|-v] [--version]" + assert_lines_count 2 } function Array::wrap2::help { #@test @@ -140,41 +139,85 @@ function Array::wrap2::help { #@test function Array::wrap2::emptyArgShouldCreateNewLines { #@test run Array::wrap2 " " 80 2 "line1" "" "line2" - assert_lines_count 2 assert_line --index 0 "line1" assert_line --index 1 " line2" + assert_lines_count 2 } function Array::wrap2::argWithForcedNewLines { #@test Array::wrap2 " " 80 2 "line1" $'\n' "line2" >"${BATS_TEST_TMPDIR}/result" - diff -u "${BATS_TEST_TMPDIR}/result" <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result") >&3 + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_emptyLinesWithForcedNewLines.expected.result") "${BATS_TEST_TMPDIR}/result" >&3 } function Array::wrap2::argFunction { #@test help() { - echo "container should be the name of a profile from profile list," - echo "check containers list below." $'\n' + echo "containers should be the name of list from list," + echo "check container list below." $'\n' echo "If not provided, it will load the container specified in default configuration." $'\n' } Array::wrap2 ' ' 76 4 "$(help)" >"${BATS_TEST_TMPDIR}/result2" - diff -u "${BATS_TEST_TMPDIR}/result2" <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_argFunction.expected.result") >&3 + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_argFunction.expected.result") "${BATS_TEST_TMPDIR}/result2" >&3 } -function Array::wrap2::multilineArg { #@test - local -a helpArray=($'\n Common Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Log in to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information') +function Array::wrap2::multilineArgSingleString { #@test + local -a helpArray=($'\n\n Common Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\r\n images List images\r\n login Log in to a registry\r\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information') Array::wrap2 " " 76 4 "${helpArray[@]}" >"${BATS_TEST_TMPDIR}/result3" diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_multilineArg.expected.result") "${BATS_TEST_TMPDIR}/result3" >&3 +} + +function Array::wrap2::multilineArgSingleStringWithEcho { #@test + local -a helpArray=($'\n\n Common Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\r\n images List images\r\n login Log in to a registry\r\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information') + + echo -en "$(Array::wrap2 " " 76 4 "${helpArray[@]}")" >"${BATS_TEST_TMPDIR}/result4" + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_multilineArg.expected.result") "${BATS_TEST_TMPDIR}/result4" >&3 +} + +function Array::wrap2::multilineArgSingleStringFunction { #@test + help() { + echo $'\n\n Common Commands:' + echo ' run Create and run a new container from an image' + echo ' exec Execute a command in a running container' + echo ' ps List containers' + echo ' build Build an image from a Dockerfile' + echo ' pull Download an image from a registry' + echo $' push Upload an image to a registry\r' + echo $' images List images\r' + echo $' login Log in to a registry\r' + echo $' logout Log out from a registry\r' + echo ' search Search Docker Hub for images' + echo ' version Show the Docker version information' + echo ' info Display system-wide information' + } - echo -e "$(Array::wrap2 " " 76 4 "${helpArray[@]}")" >"${BATS_TEST_TMPDIR}/result4" - diff -u "${BATS_TEST_TMPDIR}/result4" <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_multilineArg.expected.result") >&3 + Array::wrap2 ' ' 76 4 "$(help)" >"${BATS_TEST_TMPDIR}/result2" + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/array_wrap2_multilineArg.expected.result") "${BATS_TEST_TMPDIR}/result2" >&3 } function Array::wrap2::OneArrayElement::wrapAvoidsSpaceOnNewline { #@test run Array::wrap2 ":" 10 0 "Lorem ipsu dolor sit" assert_success - assert_lines_count 2 assert_line --index 0 "Lorem ipsu" assert_line --index 1 "dolor sit" + assert_lines_count 2 +} + +function Array::wrap2::nlShouldGenerateJustOneEcho { #@test + Array::wrap2 ' ' 76 4 $' +${__HELP_TITLE_COLOR}Common Commands:${__RESET_COLOR} +run Create and run a new container from an image +exec Execute a command in a running container +ps List containers +build Build an image from a Dockerfile +pull Download an image from a registry +push Upload an image to a registry\r +images List images\r +login Log in to a registry\r +logout Log out from a registry +search Search Docker Hub for images +version Show the Docker version information +info Display system-wide information +' >"${BATS_TEST_TMPDIR}/result2" + diff -u <(cat "${BATS_TEST_DIRNAME}/testsData/nlShouldGenerateJustOneEcho.expected.result") "${BATS_TEST_TMPDIR}/result2" >&3 } diff --git a/src/Array/wrap2.sh b/src/Array/wrap2.sh index 9333e058..0ea1f4e8 100755 --- a/src/Array/wrap2.sh +++ b/src/Array/wrap2.sh @@ -3,10 +3,12 @@ # @description concat each element of an array with a separator # but wrapping text when line length is more than provided argument # The algorithm will try not to cut the array element if it can. -# if an arg can be placed on current line it will be, +# - if an arg can be placed on current line it will be, # otherwise current line is printed and arg is added to the new # current line -# empty arg is interpreted as a new line. +# - Empty arg is interpreted as a new line. +# - Add \r to arg in order to force break line and avoid following +# arg to be concatenated with current arg. # # @arg $1 glue:String # @arg $2 maxLineLength:int @@ -28,115 +30,100 @@ Array::wrap2() { return 0 fi - eatNextSpaces() { - if [[ "${currentChar}" != [[:space:]] ]]; then - ((i--)) || true - return 0 - fi - for (( ; i < textLength; i++)); do - if [[ "${text:${i}:1}" != [[:space:]] ]]; then - ((i--)) || true - break - fi - done - } printCurrentLine() { - echo -e "${currentLine}" | sed -E -e 's/[[:blank:]]*$//' + if ((isNewline == 0)) || ((previousLineEmpty == 1)); then + echo + fi ((isNewline = 1)) - currentLine="${indentStr}" + echo -en "${indentStr}" ((currentLineLength = indentNextLine)) || true } - nextLine() { - printCurrentLine - eatNextSpaces - } - local currentLine currentChar ansiCode - local -i isAnsiCode currentLineLength=0 isNewline=1 textLength=0 - local text="" - local arg="" - while (($# > 0)); do - arg="$1" - shift || true - if ((${#arg} == 0)) || [[ "${arg}" = $'\n' ]]; then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - printCurrentLine - fi - echo - continue - fi - local textFirstLine="${arg%%$'\n'*}" - text="$(echo "${arg}" | sed -E '1d')" - ((textLength = ${#text})) || true - local textFirstLineNoAnsi - textFirstLineNoAnsi="$(echo "${textFirstLine}" | Filters::removeAnsiCodes)" - local -i textFirstLineNoAnsiLength=0 - ((textFirstLineNoAnsiLength = ${#textFirstLineNoAnsi})) || true - - if ((isNewline == 0)); then - glueLength="${#glue}" + appendToCurrentLine() { + local text="$1" + local -i length=$2 + ((currentLineLength += length)) || true + ((isNewline = 0)) || true + if [[ "${text: -1}" = $'\r' ]]; then + text="${text:0:-1}" + echo -en "${text%%+([[:blank:]])}" + printCurrentLine else - glueLength="0" + echo -en "${text%%+([[:blank:]])}" fi - if ((currentLineLength + textFirstLineNoAnsiLength + glueLength > maxLineLength)); then - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty + } + + ( + local currentLine + local -i currentLineLength=0 isNewline=1 argLength=0 + local -a additionalLines + local -i previousLineEmpty=0 + local arg="" + + while (($# > 0)); do + arg="$1" + shift || true + + # replace tab by 2 spaces + arg="${arg//$'\t'/ }" + # remove trailing spaces + arg="${arg%[[:blank:]]}" + if [[ "${arg}" = $'\n' || -z "${arg}" ]]; then printCurrentLine + ((previousLineEmpty = 1)) + continue + else + if ((previousLineEmpty == 1)); then + printCurrentLine + fi + ((previousLineEmpty = 0)) || true fi - # restore current arg without considering first line - text="${arg}" - ((textLength = ${#text})) || true - else - if ((isNewline == 0)); then - # isNewline = 0 means currentLine is not empty - currentLine+="${glue}" - ((currentLineLength += glueLength)) || true + # convert eol to args + mapfile -t additionalLines <<<"${arg}" + if ((${#additionalLines[@]} > 1)); then + set -- "${additionalLines[@]}" "$@" + continue fi - currentLine+="${textFirstLine}" - isNewline="0" - ((currentLineLength += ${#textFirstLine})) || true - fi - for ((i = 0; i < textLength; i++)); do - currentChar="${text:${i}:1}" + ((argLength = ${#arg})) || true - if [[ "${currentChar}" = $'\r' ]]; then - # ignore - true - elif [[ "${currentChar}" = "\x1b" ]]; then - isAnsiCode=1 - ansiCode+="${currentChar}" - elif ((isAnsiCode == 1)); then - ansiCode+="${currentChar}" - if [[ "${currentChar}" =~ [mGKHF] ]]; then - isAnsiCode=0 - echo -e "${ansiCode}" - elif [[ "${currentChar}" = $'\n' ]]; then - # invalid ansi code, ignore it - isAnsiCode=0 - ansiCode="" + # empty arg + if ((argLength == 0)); then + if ((isNewline == 0)); then + # isNewline = 0 means currentLine is not empty + printCurrentLine fi + continue + fi + + if ((isNewline == 0)); then + glueLength="${#glue}" else - # non ansi code - if [[ "${currentChar}" = $'\n' ]] || ((currentLineLength == maxLineLength)); then - nextLine - elif [[ "${currentChar}" = "\t" ]]; then - if ((currentLineLength + 2 <= maxLineLength)); then - currentLine+=" " - ((isNewline = 0)) || true - ((currentLineLength = currentLineLength + 2)) - else - nextLine - fi + glueLength="0" + fi + if ((currentLineLength + argLength + glueLength > maxLineLength)); then + if ((argLength + glueLength > maxLineLength)); then + # arg is too long to even fit on one line + # we have to split the arg on current and next line + local -i remainingLineLength + ((remainingLineLength = maxLineLength - currentLineLength - glueLength)) + appendToCurrentLine "${glue:0:${glueLength}}${arg:0:${remainingLineLength}}" "$((glueLength + remainingLineLength))" + printCurrentLine + arg="${arg:${remainingLineLength}}" + # remove leading spaces + arg="${arg##[[:blank:]]}" + + set -- "${arg}" "$@" else - currentLine+="${currentChar}" - ((isNewline = 0)) || true - ((++currentLineLength)) + # the arg can fit on next line + printCurrentLine + appendToCurrentLine "${arg}" "${argLength}" fi + else + appendToCurrentLine "${glue:0:${glueLength}}${arg}" "$((glueLength + argLength))" fi done - done - if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then - printCurrentLine - fi + if [[ "${currentLine}" != "" ]] && [[ ! "${currentLine}" =~ ^[\ \t]+$ ]]; then + printCurrentLine + fi + ) | sed -E -e 's/[[:blank:]]+$//' } diff --git a/src/Options/_bats.sh b/src/Options/_bats.sh index e784bd25..12c87ab8 100755 --- a/src/Options/_bats.sh +++ b/src/Options/_bats.sh @@ -46,7 +46,7 @@ checkCommandResult() { tmpFileResultAnsi="$(mktemp -p "${BATS_TEST_TMPDIR}" -t resultAnsi-$$-XXXXXX)" echo -e "${output}" >"${tmpFileResultAnsi}" - diff -u <(Filters::removeAnsiCodes "${tmpFileResultAnsi}") <(Filters::removeAnsiCodes <"${BATS_TEST_DIRNAME}/testsData/${expectedResult}") >&3 || { + diff -u <(Filters::removeAnsiCodes <"${BATS_TEST_DIRNAME}/testsData/${expectedResult}") <(Filters::removeAnsiCodes "${tmpFileResultAnsi}") >&3 || { echo -e "${output}" >&3 if [[ "${BATS_FIX_TEST}" = "1" ]]; then echo "writing output to ${BATS_TEST_DIRNAME}/testsData/${expectedResult}" >&3 diff --git a/src/Options/generateCommand.bats b/src/Options/generateCommand.bats index fcdd1544..9edaa728 100755 --- a/src/Options/generateCommand.bats +++ b/src/Options/generateCommand.bats @@ -524,9 +524,9 @@ function Options::generateCommand::case7::init { #@test ps List containers build Build an image from a Dockerfile pull Download an image from a registry - push Upload an image to a registry - images List images - login Log in to a registry + push Upload an image to a registry\r + images List images\r + login Log in to a registry\r logout Log out from a registry search Search Docker Hub for images version Show the Docker version information diff --git a/src/Options/testsData/generateCommand.case7.expected.help b/src/Options/testsData/generateCommand.case7.expected.help index 06be14ac..5ec72fcc 100644 --- a/src/Options/testsData/generateCommand.case7.expected.help +++ b/src/Options/testsData/generateCommand.case7.expected.help @@ -5,19 +5,20 @@ ARGUMENTS: subCommand {single} (mandatory) + Common Commands: - run Create and run a new container from an image - exec Execute a command in a running container - ps List containers - build Build an image from a Dockerfile - pull Download an image from a registry - push Upload an image to a registry - images List images - login Log in to a registry - logout Log out from a registry - search Search Docker Hub for images - version Show the Docker version information - info Display system-wide information + run Create and run a new container from an image + exec Execute a command in a running container + ps List containers + build Build an image from a Dockerfile + pull Download an image from a registry + push Upload an image to a registry + images List images + login Log in to a registry + logout Log out from a registry + search Search Docker Hub for images + version Show the Docker version information + info Display system-wide information OPTIONS: --help, -h {single} diff --git a/src/Options/testsData/generateCommand.case7.sh b/src/Options/testsData/generateCommand.case7.sh index e2b56003..1b3ea9ed 100755 --- a/src/Options/testsData/generateCommand.case7.sh +++ b/src/Options/testsData/generateCommand.case7.sh @@ -83,7 +83,7 @@ Options::command() { echo -e " ${__HELP_OPTION_COLOR}subCommand${__HELP_NORMAL} {single} (mandatory)" local -a helpArray # shellcheck disable=SC2054 - helpArray=($'\n \e[1;37mCommon Commands:\e[0m\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Log in to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n ') + helpArray=($'\n \e[1;37mCommon Commands:\e[0m\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\r\n images List images\r\n login Log in to a registry\r\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n ') echo -e " $(Array::wrap2 " " 76 4 "${helpArray[@]}")" echo echo -e "${__HELP_TITLE_COLOR}OPTIONS:${__RESET_COLOR}"