diff --git a/README.md b/README.md index 32347d1..9cd2191 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ Please check the [CONTRIBUTING.md][contributing] documentation if you intend to - for yes/no, add param to choose the default option. - Replace awk with bash. - Provide an alternative bash function if diff is not found. +- Allow to separate commands from options/arguments with `--`. [releases]: https://github.com/jcaillon/valet/releases [latest-release]: https://github.com/jcaillon/valet/releases/latest diff --git a/tests.d/1000-core-functions/00.tests.sh b/tests.d/1000-core-functions/00.tests.sh index dbaaf72..82685ab 100644 --- a/tests.d/1000-core-functions/00.tests.sh +++ b/tests.d/1000-core-functions/00.tests.sh @@ -15,58 +15,60 @@ There were 2 new lines before this." string::wrapText "${shortText}" 30 && echo "${LAST_RETURNED_VALUE}" endTest "Wrapping text at column 30 with no padding" 0 - echo "→ string::wrapText \"\${shortText}\" 90 4 false" echo "------------------------------------------------------------------------------------------" string::wrapText "${shortText}" 90 4 false && echo "${LAST_RETURNED_VALUE}" endTest "Wrapping text at column 90 with padding of 4 on new lines" 0 - echo "→ string::wrapText \"\${shortText}\" 90 2 true" echo "------------------------------------------------------------------------------------------" string::wrapText "${shortText}" 90 2 true && echo "${LAST_RETURNED_VALUE}" endTest "Wrapping text at column 90 with padding of 2 on all lines" 0 } -function testString::fuzzyMatch() { - local lines="l1 this is a word -l2 very unbelievable -l2 unbelievable -l3 self mock1 -l4 self mock2 -l5 ublievable" +function testArray::fuzzyFilter() { + lines=("this is a word" + "very unbelievable" + "unbelievable" + "self mock1" + "self mock2" + "ublievable") - echo "lines=\"${lines}\"" + declare -p lines echo - echo "→ string::fuzzyMatch evle \"\${lines}\"" - string::fuzzyMatch "evle" "${lines}" && echo "${LAST_RETURNED_VALUE}" + echo "→ array::fuzzyFilter evle lines" + array::fuzzyFilter "evle" lines + declare -p LAST_RETURNED_ARRAY_VALUE LAST_RETURNED_ARRAY_VALUE2 LAST_RETURNED_ARRAY_VALUE3 echo - echo "→ string::fuzzyMatch sh2 \"\${lines}\"" - string::fuzzyMatch "sc2" "${lines}" && echo "${LAST_RETURNED_VALUE}" + echo "→ array::fuzzyFilter SC2 lines" + array::fuzzyFilter "SC2" lines + declare -p LAST_RETURNED_ARRAY_VALUE LAST_RETURNED_ARRAY_VALUE2 LAST_RETURNED_ARRAY_VALUE3 echo - echo "# should prioritize lower index of u" - echo "→ string::fuzzyMatch u \"\${lines}\"" - string::fuzzyMatch "u" "${lines}" && echo "${LAST_RETURNED_VALUE}" + echo "→ array::fuzzyFilter u lines" + array::fuzzyFilter "u" lines + declare -p LAST_RETURNED_ARRAY_VALUE LAST_RETURNED_ARRAY_VALUE2 LAST_RETURNED_ARRAY_VALUE3 echo - echo "# should be the first equal match" - echo "→ string::fuzzyMatch self \"\${lines}\"" - string::fuzzyMatch "self" "${lines}" && echo "${LAST_RETURNED_VALUE}" + echo "→ array::fuzzyFilter seLf lines" + array::fuzzyFilter "seLf" lines + declare -p LAST_RETURNED_ARRAY_VALUE LAST_RETURNED_ARRAY_VALUE2 LAST_RETURNED_ARRAY_VALUE3 echo - echo "# should prioritize lower distance between letters" - echo "→ string::fuzzyMatch lubl \"\${lines}\"" - string::fuzzyMatch "lubl" "${lines}" && echo "${LAST_RETURNED_VALUE}" + echo "→ array::fuzzyFilter nomatch lines" + array::fuzzyFilter "nomatch" lines + declare -p LAST_RETURNED_ARRAY_VALUE LAST_RETURNED_ARRAY_VALUE2 LAST_RETURNED_ARRAY_VALUE3 + + unset lines - endTest "Testing string::fuzzyMatch" 0 + endTest "Testing array::fuzzyFilter" 0 } function main() { testString::wrapText - testString::fuzzyMatch + testArray::fuzzyFilter } -main \ No newline at end of file +main diff --git a/tests.d/1000-core-functions/results.approved.md b/tests.d/1000-core-functions/results.approved.md index eba3588..7920fb4 100644 --- a/tests.d/1000-core-functions/results.approved.md +++ b/tests.d/1000-core-functions/results.approved.md @@ -98,36 +98,38 @@ Exit code: `0` There were 2 new lines before this. ``` -### Testing string::fuzzyMatch +### Testing array::fuzzyFilter Exit code: `0` **Standard** output: ```plaintext -lines="l1 this is a word -l2 very unbelievable -l2 unbelievable -l3 self mock1 -l4 self mock2 -l5 ublievable" - -→ string::fuzzyMatch evle "${lines}" -l2 very unbelievable - -→ string::fuzzyMatch sh2 "${lines}" -l4 self mock2 - -# should prioritize lower index of u -→ string::fuzzyMatch u "${lines}" -l2 unbelievable - -# should be the first equal match -→ string::fuzzyMatch self "${lines}" -l3 self mock1 - -# should prioritize lower distance between letters -→ string::fuzzyMatch lubl "${lines}" -l5 ublievable +declare -a lines=([0]="this is a word" [1]="very unbelievable" [2]="unbelievable" [3]="self mock1" [4]="self mock2" [5]="ublievable") + +→ array::fuzzyFilter evle lines +declare -a LAST_RETURNED_ARRAY_VALUE=([0]="very unbelievable" [1]="unbelievable" [2]="ublievable") +declare -a LAST_RETURNED_ARRAY_VALUE2=([0]="1" [1]="3" [2]="4") +declare -a LAST_RETURNED_ARRAY_VALUE3=([0]="1" [1]="1" [2]="1") + +→ array::fuzzyFilter SC2 lines +declare -a LAST_RETURNED_ARRAY_VALUE=([0]="self mock2") +declare -a LAST_RETURNED_ARRAY_VALUE2=([0]="0") +declare -a LAST_RETURNED_ARRAY_VALUE3=([0]="2") + +→ array::fuzzyFilter u lines +declare -a LAST_RETURNED_ARRAY_VALUE=([0]="very unbelievable" [1]="unbelievable" [2]="ublievable") +declare -a LAST_RETURNED_ARRAY_VALUE2=([0]="5" [1]="0" [2]="0") +declare -a LAST_RETURNED_ARRAY_VALUE3=([0]="5" [1]="0" [2]="0") + +→ array::fuzzyFilter seLf lines +declare -a LAST_RETURNED_ARRAY_VALUE=([0]="self mock1" [1]="self mock2") +declare -a LAST_RETURNED_ARRAY_VALUE2=([0]="0" [1]="0") +declare -a LAST_RETURNED_ARRAY_VALUE3=([0]="1" [1]="1") + +→ array::fuzzyFilter nomatch lines +declare -a LAST_RETURNED_ARRAY_VALUE=() +declare -a LAST_RETURNED_ARRAY_VALUE2=() +declare -a LAST_RETURNED_ARRAY_VALUE3=() ``` diff --git a/tests.d/1001-main-functions/99.tests.sh b/tests.d/1001-main-functions/99.tests.sh index 5ea9194..4888775 100644 --- a/tests.d/1001-main-functions/99.tests.sh +++ b/tests.d/1001-main-functions/99.tests.sh @@ -48,15 +48,15 @@ function testGetMaxPossibleCommandLevel() { function testFuzzyFindOption() { echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'de'" - main::fuzzyFindOption "--opt1 --derp2 --allo3" "de" && echo "${LAST_RETURNED_VALUE}" + main::fuzzyFindOption de --opt1 --derp2 --allo3 && echo "${LAST_RETURNED_VALUE}" echo echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' '-a'" - main::fuzzyFindOption "--opt1 --derp2 --allo3" "-a" && echo "${LAST_RETURNED_VALUE}" + main::fuzzyFindOption -a --opt1 --derp2 --allo3 && echo "${LAST_RETURNED_VALUE}" echo echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'thing'" - main::fuzzyFindOption "--opt1 --derp2 --allo3" "thing" && echo "${LAST_RETURNED_VALUE}" + main::fuzzyFindOption thing --opt1 --derp2 --allo3 && echo "${LAST_RETURNED_VALUE}" endTest "Testing main::fuzzyFindOption" 0 } diff --git a/tests.d/1001-main-functions/results.approved.md b/tests.d/1001-main-functions/results.approved.md index 5e0b94d..c3abd03 100644 --- a/tests.d/1001-main-functions/results.approved.md +++ b/tests.d/1001-main-functions/results.approved.md @@ -204,16 +204,15 @@ selfBuild self build → main::fuzzyMatchCommandtoFunctionName 'sf' 'nop' 'other' 'stuff' 'dont care' -_menu -1 -self + +0 + ``` **Error** output: ```log INFO Fuzzy matching the command ⌜se bu⌝ to ⌜self build⌝. -INFO Fuzzy matching the command ⌜sf⌝ to ⌜self⌝. ``` ### Testing main::getMaxPossibleCommandLevel diff --git a/tests.d/1002-lib-kurl/00.kurl.sh b/tests.d/1002-lib-kurl/00.kurl.sh index 590b49a..9e18bf9 100644 --- a/tests.d/1002-lib-kurl/00.kurl.sh +++ b/tests.d/1002-lib-kurl/00.kurl.sh @@ -35,13 +35,14 @@ function testKurl::toFile() { kurl::toFile false '' "${tmpFile}" --code 400 --error https://hello.com/bla --otherOpt && exitCode=0 || exitCode=$? echoOutputKurlToFile ${exitCode} "${tmpFile}" endTest "Testing kurl::toFile, testing debug mode https code 400" ${exitCode} + log::setLevel info echo "→ kurl::toFile false '' \"\${tmpFile}\" --code 200 http://hello.com" log::setLevel debug kurl::toFile false '' "${tmpFile}" --code 200 http://hello.com && exitCode=0 || exitCode=$? echoOutputKurlToFile ${exitCode} "${tmpFile}" endTest "Testing kurl::toFile, testing debug mode http code 200" ${exitCode} - + log::setLevel info } function echoOutputKurlToFile() { @@ -70,7 +71,9 @@ function testKurl::toVar() { endTest "Testing kurl, with no content http code 200" ${exitCode} echo "→ kurl::toVar false '' --code 500 http://hello.com" + export GLOBAL_ERROR_DISPLAYED=1 (kurl::toVar true '' --code 500 http://hello.com) && exitCode=0 || exitCode=$? + unset GLOBAL_ERROR_DISPLAYED endTest "Testing kurl, with no content http code 500, fails" ${exitCode} unset NO_CURL_CONTENT @@ -81,7 +84,7 @@ function testKurl::toVar() { kurl::toVar false '' --code 400 http://hello.com && exitCode=0 || exitCode=$? echoOutputKurlToVar ${exitCode} endTest "Testing kurl, debug mode, with content http code 400" ${exitCode} - + log::setLevel info } function echoOutputKurlToVar() { diff --git a/tests.d/1002-lib-kurl/results.approved.md b/tests.d/1002-lib-kurl/results.approved.md index 52d1959..8f1bf9e 100644 --- a/tests.d/1002-lib-kurl/results.approved.md +++ b/tests.d/1002-lib-kurl/results.approved.md @@ -168,26 +168,6 @@ stderr: ⌉ ``` -**Error** output: - -```log -DEBUG Executing the command ⌜curl⌝. -Fail if it fails: ⌜false⌝ -Acceptable error codes: ⌜0⌝ -Standard stream from file: ⌜false⌝ -Standard stream: ⌜⌝ -Extra parameters: ⌜--silent --show-error --location --write-out %{http_code} --output /tmp/valet-work --code 200 http://hello.com⌝ -DEBUG The command ⌜curl⌝ originally ended with exit code ⌜0⌝. -The error code ⌜0⌝ is acceptable and has been reset to 0. -Standard output: -⌜200⌝ -Error output: -⌜▶ called curl --silent --show-error --location --write-out %{http_code} --output /tmp/valet-work --code 200 http://hello.com -⌝ -DEBUG The curl command for url ⌜http://hello.com⌝ ended with exit code ⌜0⌝, the http return code was ⌜200⌝. -DEBUG The http return code ⌜200⌝ is acceptable and exit code has been reset to 0 from ⌜0⌝. -``` - ### Testing kurl, with no content http code 500, fails Exit code: `1` @@ -201,39 +181,10 @@ Exit code: `1` **Error** output: ```log -DEBUG Executing the command ⌜curl⌝. -Fail if it fails: ⌜false⌝ -Acceptable error codes: ⌜0⌝ -Standard stream from file: ⌜false⌝ -Standard stream: ⌜⌝ -Extra parameters: ⌜--silent --show-error --location --write-out %{http_code} --output /tmp/valet-work --code 500 http://hello.com⌝ -DEBUG The command ⌜curl⌝ originally ended with exit code ⌜0⌝. -The error code ⌜0⌝ is acceptable and has been reset to 0. -Standard output: -⌜500⌝ -Error output: -⌜▶ called curl --silent --show-error --location --write-out %{http_code} --output /tmp/valet-work --code 500 http://hello.com -⌝ -DEBUG The curl command for url ⌜http://hello.com⌝ ended with exit code ⌜0⌝, the http return code was ⌜500⌝. ERROR The http return code ⌜500⌝ is not acceptable for url ⌜http://hello.com⌝. Error output: ⌜▶ called curl --silent --show-error --location --write-out %{http_code} --output /tmp/valet-work --code 500 http://hello.com ⌝ -stack: -├─ In function core::fail() $GLOBAL_VALET_HOME/valet.d/core:296 -├─ In function kurl::toFile() $GLOBAL_VALET_HOME/valet.d/lib-kurl:55 -├─ In function kurl::toVar() $GLOBAL_VALET_HOME/valet.d/lib-kurl:92 -├─ In function testKurl::toVar() $GLOBAL_VALET_HOME/tests.d/1002-lib-kurl/00.kurl.sh:73 -├─ In function main() $GLOBAL_VALET_HOME/tests.d/1002-lib-kurl/00.kurl.sh:101 -├─ In function source() $GLOBAL_VALET_HOME/tests.d/1002-lib-kurl/00.kurl.sh:131 -├─ In function source() $GLOBAL_VALET_HOME/valet.d/core:449 -├─ In function runTest() valet.d/commands.d/self-test-utils:241 -├─ In function runTestSuites() valet.d/commands.d/self-test-utils:195 -├─ In function runCoreTests() valet.d/commands.d/self-test-utils:107 -├─ In function selfTest() valet.d/commands.d/self-test.sh:110 -├─ In function main::runFunction() $GLOBAL_VALET_HOME/valet.d/main:589 -├─ In function main::parseMainArguments() $GLOBAL_VALET_HOME/valet.d/main:541 -└─ In function main() $GLOBAL_VALET_HOME/valet:99 ``` ### Testing kurl, debug mode, with content http code 400 diff --git a/tests.d/1005-lib-io/01.invoke.sh b/tests.d/1005-lib-io/01.invoke.sh index 0060624..263ad54 100644 --- a/tests.d/1005-lib-io/01.invoke.sh +++ b/tests.d/1005-lib-io/01.invoke.sh @@ -58,7 +58,8 @@ function testIo::invoke() { local -i exitCode echo "→ io::invoke fakeexec2 --error" - (io::invoke fakeexec2 --error) && exitCode=0 || exitCode=$? + (io::invoke fakeexec2 --error 2> "${_TEST_TEMP_FILE}") && exitCode=0 || exitCode=$? + echoFileWithLineNumberSubstitution "${_TEST_TEMP_FILE}" 1>&2 endTest "Testing io::invoke, should fail" ${exitCode} echo "→ io::invoke fakeexec2 --option argument1 argument2" diff --git a/tests.d/1005-lib-io/results.approved.md b/tests.d/1005-lib-io/results.approved.md index 82ec773..4b23d54 100644 --- a/tests.d/1005-lib-io/results.approved.md +++ b/tests.d/1005-lib-io/results.approved.md @@ -342,21 +342,21 @@ Error output: returning 1 from fakeexec2 ⌝ stack: -├─ In function core::fail() $GLOBAL_VALET_HOME/valet.d/core:296 -├─ In function io::invoke5() $GLOBAL_VALET_HOME/valet.d/lib-io:120 -├─ In function io::invoke5var() $GLOBAL_VALET_HOME/valet.d/lib-io:178 -├─ In function io::invoke() $GLOBAL_VALET_HOME/valet.d/lib-io:226 -├─ In function testIo::invoke() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:61 -├─ In function main() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:115 -├─ In function source() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:120 -├─ In function source() $GLOBAL_VALET_HOME/valet.d/core:449 -├─ In function runTest() valet.d/commands.d/self-test-utils:241 -├─ In function runTestSuites() valet.d/commands.d/self-test-utils:195 -├─ In function runCoreTests() valet.d/commands.d/self-test-utils:107 -├─ In function selfTest() valet.d/commands.d/self-test.sh:110 -├─ In function main::runFunction() $GLOBAL_VALET_HOME/valet.d/main:589 -├─ In function main::parseMainArguments() $GLOBAL_VALET_HOME/valet.d/main:541 -└─ In function main() $GLOBAL_VALET_HOME/valet:99 +├─ In function core::fail() $GLOBAL_VALET_HOME/valet.d/core:XXX +├─ In function io::invoke5() $GLOBAL_VALET_HOME/valet.d/lib-io:XXX +├─ In function io::invoke5var() $GLOBAL_VALET_HOME/valet.d/lib-io:XXX +├─ In function io::invoke() $GLOBAL_VALET_HOME/valet.d/lib-io:XXX +├─ In function testIo::invoke() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:XXX +├─ In function main() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:XXX +├─ In function source() $GLOBAL_VALET_HOME/tests.d/1005-lib-io/01.invoke.sh:XXX +├─ In function source() $GLOBAL_VALET_HOME/valet.d/core:XXX +├─ In function runTest() valet.d/commands.d/self-test-utils:XXX +├─ In function runTestSuites() valet.d/commands.d/self-test-utils:XXX +├─ In function runCoreTests() valet.d/commands.d/self-test-utils:XXX +├─ In function selfTest() valet.d/commands.d/self-test.sh:XXX +├─ In function main::runFunction() $GLOBAL_VALET_HOME/valet.d/main:XXX +├─ In function main::parseMainArguments() $GLOBAL_VALET_HOME/valet.d/main:XXX +└─ In function main() $GLOBAL_VALET_HOME/valet:XXX ``` ### Testing io::invoke, output to var diff --git a/tests.d/1300-valet-cli/.before-test b/tests.d/1300-valet-cli/.before-test index 3cb0358..6060dfe 100644 --- a/tests.d/1300-valet-cli/.before-test +++ b/tests.d/1300-valet-cli/.before-test @@ -46,19 +46,4 @@ function echoTempFileWithTimeStampSubstitution() { line="${line//????-??-??/YYYY:MM:DD}" echo "${line}" done < "${file}" -} - -function echoTempFileWithLineNumberSubstitution() { - local file="${_TEST_TEMP_FILE}" - local line - local IFS=$'\n' - while read -rd $'\n' line; do - if [[ ${line} =~ :[0-9]{1,}$ ]]; then - line="${line/%:?/:XXX}" - line="${line/%:??/:XXX}" - line="${line/%:???/:XXX}" - line="${line/%:????/:XXX}" - fi - echo "${line}" - done <"${file}" } \ No newline at end of file diff --git a/tests.d/1300-valet-cli/01.command-help.sh b/tests.d/1300-valet-cli/01.command-help.sh index dce6337..1e21853 100644 --- a/tests.d/1300-valet-cli/01.command-help.sh +++ b/tests.d/1300-valet-cli/01.command-help.sh @@ -9,8 +9,8 @@ function testHelp() { endTest "Testing help for the self mock2 command" $? # Testing to fuzzy find an help - echo "→ valet hel s h" - ("${GLOBAL_VALET_HOME}/valet" hel s h) + echo "→ valet hel sel mo3" + ("${GLOBAL_VALET_HOME}/valet" hel sel mo3) endTest "Testing to fuzzy find an help" $? # testing help options diff --git a/tests.d/1300-valet-cli/03.event-handlers.sh b/tests.d/1300-valet-cli/03.event-handlers.sh index 84b3c8a..8d82453 100644 --- a/tests.d/1300-valet-cli/03.event-handlers.sh +++ b/tests.d/1300-valet-cli/03.event-handlers.sh @@ -6,13 +6,13 @@ function testEventHandlers() { # testing error handling (a statement returns != 0) echo "→ valet self mock1 error" ("${GLOBAL_VALET_HOME}/valet" self mock1 error 2> "${_TEST_TEMP_FILE}") - echoTempFileWithLineNumberSubstitution 1>&2 + echoFileWithLineNumberSubstitution "${_TEST_TEMP_FILE}" 1>&2 endTest "Testing error handling" $? # testing exit code (exit 5) and custom exit function echo "→ valet self mock1 exit" ("${GLOBAL_VALET_HOME}/valet" self mock1 exit 2> "${_TEST_TEMP_FILE}") - echoTempFileWithLineNumberSubstitution 1>&2 + echoFileWithLineNumberSubstitution "${_TEST_TEMP_FILE}" 1>&2 endTest "Testing exit message and custom onExit function" $? # testing the fail function @@ -23,7 +23,7 @@ function testEventHandlers() { # testing the unknown command handler echo "→ valet self mock1 unknown-command" ("${GLOBAL_VALET_HOME}/valet" self mock1 unknown-command 2> "${_TEST_TEMP_FILE}") - echoTempFileWithLineNumberSubstitution 1>&2 + echoFileWithLineNumberSubstitution "${_TEST_TEMP_FILE}" 1>&2 endTest "Testing unknown command handling" $? # testing kill diff --git a/tests.d/1300-valet-cli/results.approved.md b/tests.d/1300-valet-cli/results.approved.md index 07909a7..038a0c9 100644 --- a/tests.d/1300-valet-cli/results.approved.md +++ b/tests.d/1300-valet-cli/results.approved.md @@ -54,50 +54,30 @@ Exit code: `0` **Standard** output: ```plaintext -→ valet hel s h +→ valet hel sel mo3 ABOUT - Show a menu with sub commands for the current command. + Before starting this command, valet will check if sudo is available. + + If so, it will require the user to enter the sudo password and use sudo inside the command + USAGE - valet self [options] [command] + valet self mock3 [options] OPTIONS -h, --help Display the help for this command. -COMMANDS - - self build - Re-build the menu of valet from your commands. - self config - Open the configuration file of Valet with your default editor. - self download-binaries - Download the required binaries for valet. - self mock1 - A command that only for testing valet core functions. - self mock2 - A command that only for testing valet core functions. - self mock3 - A command that only for testing valet core functions. - self release - Release a new version of valet. - self setup - The command run after the installation of Valet to setup the tool. - self test - Test your valet custom commands. - self update - Update valet using the latest release on GitHub. - ``` **Error** output: ```log INFO Fuzzy matching the command ⌜hel⌝ to ⌜help⌝. -INFO Fuzzy matching the command ⌜s⌝ to ⌜self⌝. +INFO Fuzzy matching the command ⌜sel mo3⌝ to ⌜self mock3⌝. ``` ### Testing help with columns 48 @@ -656,7 +636,7 @@ Exit code: `1` **Error** output: ```log -ERROR Unknown option ⌜-prof⌝ (did you mean ⌜--profiling⌝?)). +ERROR Unknown option ⌜-prof⌝ (did you mean ⌜--profiling⌝?). ``` ### Testing temp files/directories creation, cleaning and custom cleanUp diff --git a/valet.d/commands.d/help.sh b/valet.d/commands.d/help.sh index 7987088..f0f89be 100644 --- a/valet.d/commands.d/help.sh +++ b/valet.d/commands.d/help.sh @@ -62,7 +62,7 @@ function showCommandHelp() { functionName="${LAST_RETURNED_VALUE:-}" exactCommand="${LAST_RETURNED_VALUE3:-}" if [[ -z "${functionName}" ]]; then - core::fail "Could not show the help because the command ⌜${commands[*]}⌝ does not exist." + core::fail "Could not show the help because the command ⌜${commands[*]}⌝ does not exist or is ambiguous." fi if [[ ${functionName} == "_menu" ]]; then diff --git a/valet.d/commands.d/self-build.sh b/valet.d/commands.d/self-build.sh index 3cc7cb4..4cfaf27 100644 --- a/valet.d/commands.d/self-build.sh +++ b/valet.d/commands.d/self-build.sh @@ -83,7 +83,13 @@ function selfBuild() { shift outputFile="${1}" ;; - -*) core::fail "Unknown option ⌜${1}⌝." ;; + -*) + if [[ -v CMD_OPTS_selfBuild ]]; then + main::fuzzyFindOption "${1}" ${CMD_OPTS_selfBuild[*]} + else + LAST_RETURNED_VALUE="" + fi + core::fail "Unknown option ⌜${1}⌝${LAST_RETURNED_VALUE:-}." ;; *) core::fail "This command takes no arguments." ;; esac shift diff --git a/valet.d/commands.d/self-test-utils b/valet.d/commands.d/self-test-utils index 8eacf64..d82e8c5 100644 --- a/valet.d/commands.d/self-test-utils +++ b/valet.d/commands.d/self-test-utils @@ -89,6 +89,21 @@ function setTempFilesNumber() { TEMPORARY_DIRECTORY_NUMBER=${1} } +function echoFileWithLineNumberSubstitution() { + local file="${1}" + local line + local IFS=$'\n' + while read -rd $'\n' line; do + if [[ ${line} =~ :[0-9]{1,4}$ ]]; then + line="${line/%:[[:digit:]]/:XXX}" + line="${line/%:[[:digit:]][[:digit:]]/:XXX}" + line="${line/%:[[:digit:]][[:digit:]][[:digit:]]/:XXX}" + line="${line/%:[[:digit:]][[:digit:]][[:digit:]][[:digit:]]/:XXX}" + fi + echo "${line}" + done <"${file}" +} + #=============================================================== # >>> Internal tests functions #=============================================================== diff --git a/valet.d/core b/valet.d/core index 90bfa76..d3b17c4 100644 --- a/valet.d/core +++ b/valet.d/core @@ -93,12 +93,12 @@ function io::createTempDirectory() { function io::cleanupTempFiles() { if [[ -d "${GLOBAL_TEMPORARY_DIRECTORY}" ]]; then log::debug "Deleting temporary directory." - rm -Rf "${GLOBAL_TEMPORARY_DIRECTORY}" 1>&2 2>/dev/null + rm -Rf "${GLOBAL_TEMPORARY_DIRECTORY}" 1>/dev/null unset TEMPORARY_FILE_NUMBER TEMPORARY_DIRECTORY_NUMBER fi local _file for _file in "${GLOBAL_TEMPORARY_IN_MEM_PREFIX}${BASHPID}.valet"*; do - rm -f "${GLOBAL_TEMPORARY_IN_MEM_PREFIX}${BASHPID}.valet"* 1>&2 2>/dev/null + rm -f "${GLOBAL_TEMPORARY_IN_MEM_PREFIX}${BASHPID}.valet-"* 1>/dev/null break done } @@ -118,7 +118,10 @@ esac # Usage: # system::exportTerminalSize function system::exportTerminalSize() { - shopt -s checkwinsize; (:;:) + shopt -s checkwinsize + # the following subshell is required to correctly compute the columns and lines + # the bash manual says it these are computed after the execution of an external command + (:;:) GLOBAL_COLUMNS="${COLUMNS:-180}" GLOBAL_LINES="${LINES:-30}" shopt -u checkwinsize @@ -682,41 +685,42 @@ function string::wrapSentence() { LAST_RETURNED_VALUE="${wrappedText}" } -# Allows to fuzzy match a line against a given pattern. -# Returns the best match from all lines. -# Or an empty string if no match is found. +# Allows to fuzzy match an array against a given pattern. +# Returns an array containing only the lines matching the pattern. +# # $1: the pattern to match -# $2: the text (multiple lines) to match against +# $2: the initial array name +# +# Returns: +# an array containing only the lines matching the pattern in the global variable LAST_RETURNED_ARRAY_VALUE +# an array of the same size that contains the start index of the match in the global variable LAST_RETURNED_ARRAY_VALUE2 +# an array of the same size that contains the distance of the match in the global variable LAST_RETURNED_ARRAY_VALUE3 # # Usage: -# string::fuzzyMatch "pattern" "line1\nline2\nline3" && local bestMatch="${LAST_RETURNED_VALUE}" +# array::fuzzyMatch "pattern" "myarray" && local bestMatch="${LAST_RETURNED_VALUE}" # # Note: -# This function is written in pure bash and is faster than the fzf command. # All characters in the pattern must be found in the same order in the matched line. -# The function is case sensitive. -# We prioritize the lines that start with the pattern. -# Then we prioritize the lines that have the less distance between characters in the pattern. -function string::fuzzyMatch() { - local pattern text - pattern="${1}" - text="${2}" - - local bestMatch - local -i patternLength lineLength bestIndex bestDistance +# The function is case insensitive. +# This function does not sort the results, it only filters them. +function array::fuzzyFilter() { + local pattern="${1}" + local -n array="${2}" + + local -a matches=() + local -a indexes=() + local -a distances=() + + local -i patternLength lineLength patternLength="${#pattern}" - bestIndex=999999 - bestDistance=999999 - local IFS=$'\n' + # make all match case insensitive + shopt -s nocasematch + local line patternChar lineChar local -i lineCharIndex patternCharIndex lastLineCharIndex distance patternFirstCharIndex - for line in ${text}; do - # shortcut, exact match - if [[ ${line} == "${pattern}" ]]; then - LAST_RETURNED_VALUE="${line}" - return 0 - fi + + for line in "${array[@]}"; do lineLength="${#line}" # for each character in the pattern @@ -759,18 +763,18 @@ function string::fuzzyMatch() { # if we found all the characters in the pattern if [[ patternCharIndex -ge patternLength ]]; then - - # we found the pattern at a lower index or we found the pattern at the same index but with less distance - if [[ patternFirstCharIndex -lt bestIndex || (patternFirstCharIndex -eq bestIndex && distance -lt bestDistance) ]]; then - bestIndex="${patternFirstCharIndex}" - bestDistance="${distance}" - bestMatch="${line}" - fi + matches+=("${line}") + indexes+=("${patternFirstCharIndex}") + distances+=("${distance}") fi done - LAST_RETURNED_VALUE="${bestMatch:-}" + shopt -u nocasematch + + LAST_RETURNED_ARRAY_VALUE=("${matches[@]}") + LAST_RETURNED_ARRAY_VALUE2=("${indexes[@]}") + LAST_RETURNED_ARRAY_VALUE3=("${distances[@]}") } #=============================================================== diff --git a/valet.d/lib-ansi-codes b/valet.d/lib-ansi-codes index be69d7e..85e13b0 100644 --- a/valet.d/lib-ansi-codes +++ b/valet.d/lib-ansi-codes @@ -184,6 +184,6 @@ AC__CHANGE_CURSOR_TO_PROMPT=$'\e[1 q'$'\e[?16;0;80;c' AC__CHANGE_CURSOR_TO_NORMAL=$'\e[0 q'$'\e[?0;c' # change line wrapping option -AC__ENABLE_LINE_WRAPPING=$'\e[?7h' AC__DISABLE_LINE_WRAPPING=$'\e[?7l' +AC__ENABLE_LINE_WRAPPING=$'\e[?7h' diff --git a/valet.d/lib-fsfs b/valet.d/lib-fsfs index 1c7f4e4..c3726bf 100644 --- a/valet.d/lib-fsfs +++ b/valet.d/lib-fsfs @@ -24,6 +24,7 @@ _RIGHT_PANE_TITLE="Details" # arrays of current items and their details (if any and if already computed) declare -a _ITEMS declare -a _ITEMS_DETAILS +declare -a FSFS_FETCHED_DETAILS_INDEXES # width of the left pane when the right pane is on declare -i _LEFT_PANE_WIDTH @@ -83,6 +84,7 @@ function fsfs::menu() { _SELECTED_ITEM_INDEX=0 _LEFT_PANE_START_INDEX=0 _RIGHT_PANE_START_INDEX=0 + FSFS_FETCHED_DETAILS_INDEXES=() _CLOSE_INTERACTIVE_SESSION=false _SEARCH_STRING="" @@ -108,16 +110,15 @@ function fsfs::menu() { # main loop while true; do - read -t 0.1 -srn 1 && key "${REPLY}" + read -t 0.05 -srn 1 && key "${REPLY}" # break if fd 1 is closed or does not refer to a terminal. if [[ ! -t 1 || ${_CLOSE_INTERACTIVE_SESSION} == "true" ]]; then break; fi - echo "Resized to ${GLOBAL_LINES}x${GLOBAL_COLUMNS}." >&2 # # redraw the screen if the terminal was resized - # if [[ ${FSFS_REDRAW_REQUIRED:-false} == "true" ]]; then - # _drawScreen full - # fi + if [[ ${FSFS_REDRAW_REQUIRED:-false} == "true" ]]; then + _drawScreen full + fi done # restore the initial traps @@ -126,10 +127,17 @@ function fsfs::menu() { } function onResize() { + # export terminal size does not work if + exec 2>&4 4>&- system::exportTerminalSize + exec 4>&2 2>"${FSFS_TEMPORARY_ERROR_FILE}" FSFS_REDRAW_REQUIRED=true } +function fuuuuck() { + return 0 +} + function _drawScreen() { local drawMode="${1:-full}" @@ -138,7 +146,7 @@ function _drawScreen() { if [[ ${drawMode} == "full" ]]; then # compute the potential width of the right panel _RIGHT_PANE_WIDTH=$((GLOBAL_COLUMNS * 9 / 20)) - _ITEMS_DETAILS=() + FSFS_FETCHED_DETAILS_INDEXES=() _drawStaticScreen @@ -147,27 +155,37 @@ function _drawScreen() { # get the details for the current item; this will tell us if we need a right pane # or not - local details - if [[ -n ${_ITEMS_DETAILS[${_SELECTED_ITEM_INDEX}]:-} ]]; then - details="${_ITEMS_DETAILS[${_SELECTED_ITEM_INDEX}]}" - elif [[ -n ${_ITEM_SELECTED_CALLBACK} ]]; then + local hasDetails=${FSFS_FETCHED_DETAILS_INDEXES[${_SELECTED_ITEM_INDEX}]:-unknown} + if [[ ${hasDetails} == "unknown" && -n ${_ITEM_SELECTED_CALLBACK:-} ]]; then "${_ITEM_SELECTED_CALLBACK}" "${_ITEMS[${_SELECTED_ITEM_INDEX}]}" "${_SELECTED_ITEM_INDEX}" "$((_RIGHT_PANE_WIDTH - 4))" - details="${LAST_RETURNED_VALUE}" - _ITEMS_DETAILS[_SELECTED_ITEM_INDEX]="${details}" + _ITEMS_DETAILS[_SELECTED_ITEM_INDEX]="${LAST_RETURNED_VALUE}" + if [[ -n ${LAST_RETURNED_VALUE} ]]; then + hasDetails=true + else + hasDetails=false + fi + FSFS_FETCHED_DETAILS_INDEXES[_SELECTED_ITEM_INDEX]="${hasDetails}" fi # if we have details, we need to draw the right pane and # adjust the left pane width - if [[ -n ${details} ]]; then + if [[ ${hasDetails} == "true" ]]; then _LEFT_PANE_WIDTH=$((GLOBAL_COLUMNS - _RIGHT_PANE_WIDTH)) - # _drawRightPane "${details}" else _LEFT_PANE_WIDTH=${GLOBAL_COLUMNS} fi + + if [[ ${drawMode} == *"right"* && ${hasDetails} == "true" ]]; then + _drawRightPane + fi + + if [[ ${drawMode} == *"left"* ]]; then + _drawLeftPane + fi } function _drawRightPane() { - local details="${1}" + local details="${_ITEMS_DETAILS[_SELECTED_ITEM_INDEX]}" local IFS # draw the title @@ -184,9 +202,14 @@ function _drawRightPane() { # draw the details local -i currentLine=1 + local -i currentIndex=0 local line while IFS= read -r line; do - printf "%s%s%s%s\n" "${AC__CURSOR_MOVE__}$((_RIGHT_PANE_FIRST_LINE + currentLine));$((_LEFT_PANE_WIDTH + 1))${__AC__TO}" "│ " "${line}" "${AC__CURSOR_MOVE__}$((GLOBAL_COLUMNS))${__AC__COLUMN}│" + currentIndex+=1 + if ((currentIndex < _RIGHT_PANE_START_INDEX + 1)); then + continue + fi + printf "%s%s%s%s\n" "${AC__CURSOR_MOVE__}$((_RIGHT_PANE_FIRST_LINE + currentLine));$((_LEFT_PANE_WIDTH + 1))${__AC__TO}${AC__ERASE_CHARS_RIGHT}" "│ " "${line}" "${AC__CURSOR_MOVE__}$((GLOBAL_COLUMNS))${__AC__COLUMN}│" currentLine+=1 if ((currentLine >= _RIGHT_LINES - 1)); then break @@ -200,13 +223,16 @@ function _drawRightPane() { helpWidth=0 help="" fi - local helpPadding=$(((_RIGHT_PANE_WIDTH - helpWidth) / 2 - 3)) printf '%s%s' \ "${AC__CURSOR_MOVE__}$((GLOBAL_LINES));$((_LEFT_PANE_WIDTH + 1))${__AC__TO}" \ - "└─${AC__REPEAT__}${helpPadding}${__AC__LAST_CHAR}${help}─${AC__REPEAT__}$((_RIGHT_PANE_WIDTH - helpWidth - helpPadding - 2 - 2))${__AC__LAST_CHAR}┘" + "└─${AC__REPEAT__}$((_RIGHT_PANE_WIDTH - helpWidth - 4))${__AC__LAST_CHAR}${help}─┘" } +function _drawLeftPane() { + return 0 +} + function _drawStaticScreen() { local IFS @@ -248,7 +274,7 @@ function _setupTerminal() { stty -echo &>/dev/null || true fi # switch to the alternate screen, hide the cursor and clear the screen - printf '%s' "${AC__ENABLE_ALTERNATE_BUFFER_SCREEN}${AC__CURSOR_HIDE}${AC__ERASE_SCREEN}" + printf '%s' "${AC__ENABLE_ALTERNATE_BUFFER_SCREEN}${AC__DISABLE_LINE_WRAPPING}${AC__CURSOR_HIDE}${AC__ERASE_SCREEN}" # in full screen mode, we don't see the error messages so we capture them somewhere else FSFS_TEMPORARY_ERROR_FILE="${GLOBAL_TEMPORARY_IN_MEM_PREFIX}${BASHPID}.valet-interactive.err" @@ -258,7 +284,7 @@ function _setupTerminal() { function _resetTerminal() { # restore the terminal state - printf '%s' "${AC__CURSOR_SHOW}${AC__DISABLE_ALTERNATE_BUFFER_SCREEN}" + printf '%s' "${AC__ENABLE_LINE_WRAPPING}${AC__CURSOR_SHOW}${AC__DISABLE_ALTERNATE_BUFFER_SCREEN}" # restore the key echoing if command -v stty &>/dev/null; then stty echo &>/dev/null || true @@ -296,15 +322,16 @@ function key() { local sk2 printf -v sk2 "%q" "${keyPressed}" - writeToStatusBar "Special key pressed: ⌜${sk2}⌝." fi case ${keyPressed} in $'\e[C' | $'\eOC' | "") - writeToStatusBar right or enter + _RIGHT_PANE_START_INDEX=$((_RIGHT_PANE_START_INDEX + 1)) + _drawRightPane ;; $'\e[D' | $'\eOD' | $'\177' | $'\b') - writeToStatusBar left or backspace + _RIGHT_PANE_START_INDEX=$((_RIGHT_PANE_START_INDEX - 1)) + _drawRightPane ;; $'\e[B' | $'\eOB') writeToStatusBar down diff --git a/valet.d/main b/valet.d/main index ded1f17..e797c72 100644 --- a/valet.d/main +++ b/valet.d/main @@ -353,7 +353,7 @@ function main::getHelpText() { # description string::wrapText "${description}" "${helpWidth}" 2 "true" - description="${LAST_RETURNED_VALUE}" + local wrappedDescription="${LAST_RETURNED_VALUE}" # usage local usage @@ -415,7 +415,7 @@ function main::getHelpText() { local output="${cTitle}ABOUT${cDefault} -${description} +${wrappedDescription} ${cTitle}USAGE${cDefault} @@ -507,11 +507,9 @@ function main::parseMainArguments() { exit 0 ;; -*) - local fuzzyOption options - options="${CMD_OPTIONS_NAME_this[*]%<*}" - options="${options//,/}" - main::fuzzyFindOption "${options}" "${1}" && fuzzyOption="${LAST_RETURNED_VALUE:-}" - core::fail "Unknown option ⌜${1}⌝${fuzzyOption})." + # shellcheck disable=SC2048 disable=SC2086 + main::fuzzyFindOption "${1}" ${CMD_OPTS_this[*]} + core::fail "Unknown option ⌜${1}⌝${LAST_RETURNED_VALUE:-}." ;; *) commands+=("${1}") @@ -899,8 +897,13 @@ function main::fuzzyMatchCommandtoFunctionName() { fi # case where the command is not exact, we try to fuzzy match it - string::fuzzyMatch "${command}" "${CMD_ALL_COMMANDS}" && exactCommand="${LAST_RETURNED_VALUE}" - if [[ -n "${exactCommand}" ]]; then + array::fuzzyFilter "${command}" CMD_ALL_COMMANDS_ARRAY + if (( ${#LAST_RETURNED_ARRAY_VALUE[@]} > 1 )); then + local IFS=$'\n' + log::debug "Ambiguous matching for the command ⌜${command}⌝:"$'\n'"${LAST_RETURNED_ARRAY_VALUE[*]}" + unset IFS + elif (( ${#LAST_RETURNED_ARRAY_VALUE[@]} == 1 )); then + exactCommand="${LAST_RETURNED_ARRAY_VALUE[0]}" main::getFunctionNameFromCommand "${exactCommand}" && functionName="${LAST_RETURNED_VALUE}" log::info "Fuzzy matching the command ⌜${command}⌝ to ⌜${exactCommand}⌝." break @@ -1042,8 +1045,9 @@ function main::parseFunctionArguments() { else # if we didn't match any option, flag it as unknown option and add it to the leftOver - main::fuzzyFindOption "${options[*]}" "${1}" && fuzzyOption="${LAST_RETURNED_VALUE:-}" - outputErrors+=("Unknown option ⌜${1}⌝${fuzzyOption:-}.") + # shellcheck disable=SC2048 disable=SC2086 + main::fuzzyFindOption "${1}" ${options[*]} + outputErrors+=("Unknown option ⌜${1}⌝${LAST_RETURNED_VALUE:-}.") fi else @@ -1150,21 +1154,27 @@ function main::parseFunctionArguments() { # Tries to help the user by suggesting a fix for an unknown option # we receive the function options and the unknown option string +# +# $1: the user string to match +# $2+: options to match against +# # Usage: -# main::fuzzyFindOption "option1 option2 option3" "opt1" && fuzzyOption="${LAST_RETURNED_VALUE}" +# main::fuzzyFindOption opt1 option1 option2 option3 && fuzzyOption="${LAST_RETURNED_VALUE}" function main::fuzzyFindOption() { - local options unknownOption suggestedOption - options="${1}" - unknownOption="${2}" + local unknownOption suggestedOption + unknownOption="${1}" + shift + _OPTIONS_TO_MATCH=("$@") # split to get one possible option per line - string::fuzzyMatch "${unknownOption}" "${options// /$'\n'}" && suggestedOption="${LAST_RETURNED_VALUE}" - suggestedOption="${suggestedOption%%$'\n'*}" - if [[ -n "${suggestedOption}" ]]; then - suggestedOption=" (did you mean ⌜${suggestedOption}⌝?)" + array::fuzzyFilter "${unknownOption}" _OPTIONS_TO_MATCH + if (( ${#LAST_RETURNED_ARRAY_VALUE[@]} == 1 )); then + suggestedOption=" (did you mean ⌜${LAST_RETURNED_ARRAY_VALUE[0]}⌝?)" + elif (( ${#LAST_RETURNED_ARRAY_VALUE[@]} > 1 )); then + suggestedOption=" (did you mean one of ⌜${LAST_RETURNED_ARRAY_VALUE[*]}⌝?)" fi - LAST_RETURNED_VALUE="${suggestedOption}" + LAST_RETURNED_VALUE="${suggestedOption:-}" } # Parse the arguments and options of a function. diff --git a/valet.d/version b/valet.d/version index b066637..0a0e6ba 100644 --- a/valet.d/version +++ b/valet.d/version @@ -1 +1 @@ -0.6.42 \ No newline at end of file +0.6.78 \ No newline at end of file