From f1f13ce392d1ab4776057b799a856ef2479537f9 Mon Sep 17 00:00:00 2001 From: Julien Caillon Date: Tue, 25 Jun 2024 20:06:51 +0200 Subject: [PATCH] :sparkles: fuzzy finding on options --- docs/content/docs/800.roadmap/_index.md | 3 +- .../02.arguments-parser.sh | 22 ++- tests.d/1001-main-functions/99.tests.sh | 27 +++- .../1001-main-functions/results.approved.md | 126 +++++++++++++--- tests.d/1102-self-build/results.approved.md | 14 +- tests.d/1300-valet-cli/results.approved.md | 27 ++-- tests.d/1301-profiler/results.approved.md | 76 +++++----- valet.d/commands.d/self-build.sh | 4 +- valet.d/commands.d/self-mock.sh | 7 + valet.d/main | 142 ++++++++++++------ valet.d/version | 2 +- 11 files changed, 315 insertions(+), 135 deletions(-) diff --git a/docs/content/docs/800.roadmap/_index.md b/docs/content/docs/800.roadmap/_index.md index e0246ec..3866ed6 100644 --- a/docs/content/docs/800.roadmap/_index.md +++ b/docs/content/docs/800.roadmap/_index.md @@ -8,8 +8,6 @@ url: /docs/roadmap This page lists the features that I would like to implement in Valet. They come in addition to new features described in the [issues][valet-issues]. -- We can have fuzzy matching on options too; just make sure it is not ambiguous. -- Allow to regroup single letter options (e.g. -fsSL). - Add full support for interactive mode. - For dropdown with a set list of options, we can verify that the input value is one of the expected value. - Generate an autocompletion script for bash and zsh. @@ -28,6 +26,7 @@ This page lists the features that I would like to implement in Valet. They come - add valet in brew - Improve the self install script / check for updates by comparing the version number / suggest the user to git pull the repositories existing under .valet.d. Also add snippets and all functions... - For argument and option autocompletion, accept any multiline string that will be eval and that should set RETURNED_ARRAY with the list of possible completion. +- Allow to regroup single letter options (e.g. -fsSL). [valet-issues]: https://github.com/jcaillon/valet/issues diff --git a/tests.d/1001-main-functions/02.arguments-parser.sh b/tests.d/1001-main-functions/02.arguments-parser.sh index a7dc05b..ec2cc8c 100644 --- a/tests.d/1001-main-functions/02.arguments-parser.sh +++ b/tests.d/1001-main-functions/02.arguments-parser.sh @@ -2,40 +2,56 @@ function testMain::parseFunctionArguments() { + # missing argument echo "→ main::parseFunctionArguments selfMock2" main::parseFunctionArguments selfMock2 && echo "${RETURNED_VALUE}" echo + # ok echo "→ main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1 more1 more2" main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1 more1 more2 && echo "${RETURNED_VALUE}" echo + # missing argument echo "→ main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1" main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1 && echo "${RETURNED_VALUE}" echo + # unknown options echo "→ main::parseFunctionArguments selfMock2 -unknown -what optionValue2 arg" main::parseFunctionArguments selfMock2 -unknown -what optionValue2 arg && echo "${RETURNED_VALUE}" echo + # ok with the option at the end echo "→ main::parseFunctionArguments selfMock2 arg more1 more2 -o" main::parseFunctionArguments selfMock2 arg more1 more2 -o && echo "${RETURNED_VALUE}" echo + # fuzzy match the option -this echo "→ main::parseFunctionArguments selfMock2 -this arg more1" main::parseFunctionArguments selfMock2 -this arg more1 && echo "${RETURNED_VALUE}" - echo + + # ok, --option1 is interpreted as the value for --this-is-option2 echo "→ main::parseFunctionArguments selfMock2 --this-is-option2 --option1 arg more1" main::parseFunctionArguments selfMock2 --this-is-option2 --option1 arg more1 && echo "${RETURNED_VALUE}" - echo + + # ok only args echo "→ main::parseFunctionArguments selfMock4 arg1 arg2" main::parseFunctionArguments selfMock4 arg1 arg2 && echo "${RETURNED_VALUE}" - echo + + # ok with -- to separate options from args echo "→ main::parseFunctionArguments selfMock2 -- --arg1-- --arg2--" main::parseFunctionArguments selfMock2 -- --arg1-- --arg2-- && echo "${RETURNED_VALUE}" + echo + echo + + # ambiguous fuzzy match + echo "→ main::parseFunctionArguments selfMock2 arg1 arg2 --th" + main::parseFunctionArguments selfMock2 arg1 arg2 --th && echo "${RETURNED_VALUE}" + echo test::endTest "Testing main::parseFunctionArguments" 0 } diff --git a/tests.d/1001-main-functions/99.tests.sh b/tests.d/1001-main-functions/99.tests.sh index f3d2888..f076626 100644 --- a/tests.d/1001-main-functions/99.tests.sh +++ b/tests.d/1001-main-functions/99.tests.sh @@ -51,16 +51,31 @@ function testGetMaxPossibleCommandLevel() { function testFuzzyFindOption() { - echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'de'" - main::fuzzyFindOption de --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" + # single match, strict mode is enabled + echo "→ VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption de --opt1 --derp2 --allo3" + VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption de --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" && echo "${RETURNED_VALUE2}" + unset VALET_CONFIG_STRICT_MATCHING + + # single match, strict mode is disabled + echo + echo "→ main::fuzzyFindOption de --opt1 --derp2 --allo3" + main::fuzzyFindOption de --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" && echo "${RETURNED_VALUE2}" + + # multiple matches, strict mode is enabled + echo + echo "→ VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption -a --opt1 --derp2 --allo3" + VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption -p --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" && echo "${RETURNED_VALUE2}" + + # multiple matches, strict mode is disabled echo - echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' '-a'" - main::fuzzyFindOption -a --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" + echo "→ main::fuzzyFindOption -a --opt1 --derp2 --allo3" + main::fuzzyFindOption -p --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" && echo "${RETURNED_VALUE2}" + # no match echo - echo "→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'thing'" - main::fuzzyFindOption thing --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" + echo "→ main::fuzzyFindOption thing --opt1 --derp2 --allo3" + main::fuzzyFindOption thing --opt1 --derp2 --allo3 && echo "${RETURNED_VALUE}" && echo "${RETURNED_VALUE2}" test::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 44c4de5..5d03fb6 100644 --- a/tests.d/1001-main-functions/results.approved.md +++ b/tests.d/1001-main-functions/results.approved.md @@ -106,10 +106,12 @@ Exit code: `0` ```plaintext → main::parseFunctionArguments selfMock2 -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more option1="" thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="Expecting ⌜2⌝ argument(s) but got ⌜0⌝. Use ⌜valet self mock2 --help⌝ to get help. @@ -120,8 +122,10 @@ more=( ) → main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1 more1 more2 -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="" option1="true" @@ -133,8 +137,10 @@ more=( ) → main::parseFunctionArguments selfMock2 -o -2 optionValue2 arg1 -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="Expecting ⌜2⌝ argument(s) but got ⌜1⌝. Use ⌜valet self mock2 --help⌝ to get help. @@ -148,23 +154,39 @@ more=( ) → main::parseFunctionArguments selfMock2 -unknown -what optionValue2 arg -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more option1="" thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" help="" -parsingErrors="Unknown option ⌜-unknown⌝. -Unknown option ⌜-what⌝. -Use ⌜valet self mock2 --help⌝ to get help." -firstArg="optionValue2" +parsingErrors="Unknown option ⌜-unknown⌝, valid options are: +-o +--option1 +-2 +--this-is-option2 +-3 +--flag3 +-4 +--with-default +-h +--help +Expecting ⌜2⌝ argument(s) but got ⌜1⌝. +Use ⌜valet self mock2 --help⌝ to get help. + +Usage: +valet [global options] self mock2 [options] [--] " +withDefault="optionValue2" +firstArg="arg" more=( -"arg" ) → main::parseFunctionArguments selfMock2 arg more1 more2 -o -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="" firstArg="arg" @@ -175,22 +197,28 @@ more=( ) → main::parseFunctionArguments selfMock2 -this arg more1 -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more option1="" -thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" -parsingErrors="Unknown option ⌜-this⌝ (did you mean ⌜--this-is-option2⌝?). -Use ⌜valet self mock2 --help⌝ to get help." -firstArg="arg" +parsingErrors="Expecting ⌜2⌝ argument(s) but got ⌜1⌝. +Use ⌜valet self mock2 --help⌝ to get help. + +Usage: +valet [global options] self mock2 [options] [--] " +thisIsOption2="arg" +firstArg="more1" more=( -"more1" ) → main::parseFunctionArguments selfMock2 --this-is-option2 --option1 arg more1 -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more option1="" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="" thisIsOption2="--option1" @@ -208,16 +236,44 @@ secondArg="arg2" → main::parseFunctionArguments selfMock2 -- --arg1-- --arg2-- -local parsingErrors option1 thisIsOption2 help firstArg +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg local -a more option1="" thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" help="" parsingErrors="" firstArg="--arg1--" more=( "--arg2--" ) + + +→ main::parseFunctionArguments selfMock2 arg1 arg2 --th +local parsingErrors option1 thisIsOption2 flag3 withDefault help firstArg +local -a more +option1="" +thisIsOption2="${VALET_THIS_IS_OPTION2:-}" +flag3="${VALET_FLAG3:-}" +withDefault="${VALET_WITH_DEFAULT:-"cool"}" +help="" +parsingErrors="Found multiple matches for the option ⌜--th⌝, please be more specific: +CHI-CDECHI-CDECHItCDECHIhCDEis-is-option2 +CHI-CDECHI-CDEwiCHItCDECHIhCDE-default +Use ⌜valet self mock2 --help⌝ to get help." +firstArg="arg1" +more=( +"arg2" +) + +``` + +**Error** output: + +```log +INFO Fuzzy matching the option ⌜-what⌝ to ⌜--with-default⌝. +INFO Fuzzy matching the option ⌜-this⌝ to ⌜--this-is-option2⌝. ``` ## Test script 99.tests @@ -298,13 +354,37 @@ Exit code: `0` **Standard** output: ```plaintext -→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'de' - (did you mean ⌜--derp2⌝?) +→ VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption de --opt1 --derp2 --allo3 +Unknown option ⌜de⌝, did you mean ⌜--derp2⌝? + -→ main::fuzzyFindOption '--opt1 --derp2 --allo3' '-a' - (did you mean ⌜--allo3⌝?) +→ main::fuzzyFindOption de --opt1 --derp2 --allo3 -→ main::fuzzyFindOption '--opt1 --derp2 --allo3' 'thing' +--derp2 +→ VALET_CONFIG_STRICT_MATCHING=true main::fuzzyFindOption -a --opt1 --derp2 --allo3 +Unknown option ⌜-p⌝, valid matches are: +CHI-CDE-oCHIpCDEt1 +CHI-CDE-derCHIpCDE2 + + +→ main::fuzzyFindOption -a --opt1 --derp2 --allo3 +Found multiple matches for the option ⌜-p⌝, please be more specific: +CHI-CDE-oCHIpCDEt1 +CHI-CDE-derCHIpCDE2 + + +→ main::fuzzyFindOption thing --opt1 --derp2 --allo3 +Unknown option ⌜thing⌝, valid options are: +--opt1 +--derp2 +--allo3 + +``` + +**Error** output: + +```log +INFO Fuzzy matching the option ⌜de⌝ to ⌜--derp2⌝. ``` diff --git a/tests.d/1102-self-build/results.approved.md b/tests.d/1102-self-build/results.approved.md index 30788e3..7de21ad 100644 --- a/tests.d/1102-self-build/results.approved.md +++ b/tests.d/1102-self-build/results.approved.md @@ -117,7 +117,7 @@ CMD_OPTIONS_DESCRIPTION_selfBuild=0 $'Specify the directory in which to look for CMD_OPTIONS_DESCRIPTION_selfConfig=0 $'Create the configuration file if it does not exist but do not open it.\nThis option can be set by exporting the variable VALET_NO_EDIT=\'true\'.' 1 $'Override of the configuration file even if it already exists.\nUnless the option --export-current-values is used, the existing values will be reset.\nThis option can be set by exporting the variable VALET_OVERRIDE=\'true\'.' 2 $'When writing the configuration file, export the current values of the variables.\n\nThis option can be set by exporting the variable VALET_EXPORT_CURRENT_VALUES=\'true\'.' 3 "Display the help for this command." CMD_OPTIONS_DESCRIPTION_selfExport=0 $'Export all the libraries.\n\nThis option can be set by exporting the variable VALET_EXPORT_ALL=\'true\'.' 1 "Display the help for this command." CMD_OPTIONS_DESCRIPTION_selfMock1=0 "Display the help for this command." -CMD_OPTIONS_DESCRIPTION_selfMock2=0 "First option." 1 $'An option with a value.\nThis option can be set by exporting the variable VALET_THIS_IS_OPTION2=\'\'.' 2 "Display the help for this command." +CMD_OPTIONS_DESCRIPTION_selfMock2=0 "First option." 1 $'An option with a value.\nThis option can be set by exporting the variable VALET_THIS_IS_OPTION2=\'\'.' 2 $'Third option.\nThis option can be set by exporting the variable VALET_FLAG3=\'true\'.' 3 $'An option with a default value.\nThis option can be set by exporting the variable VALET_WITH_DEFAULT=\'\'.' 4 "Display the help for this command." CMD_OPTIONS_DESCRIPTION_selfMock3=0 "Display the help for this command." CMD_OPTIONS_DESCRIPTION_selfMock4=0 "Display the help for this command." CMD_OPTIONS_DESCRIPTION_selfRelease=0 $'The token necessary to create the release on GitHub and upload artifacts.\nThis option can be set by exporting the variable VALET_GITHUB_RELEASE_TOKEN=\'\'.' 1 $'The semver level to bump the version.\n\nCan be either: major or minor. Defaults to minor.\nThis option can be set by exporting the variable VALET_BUMP_LEVEL=\'\'.' 2 $'Do not perform the release, just show what would be done.\nThis option can be set by exporting the variable VALET_DRY_RUN=\'true\'.' 3 $'Do no create the release, just upload the artifacts to the latest release.\n\nThis option can be set by exporting the variable VALET_UPLOAD_ARTIFACTS_ONLY=\'true\'.' 4 "Display the help for this command." @@ -131,7 +131,7 @@ CMD_OPTIONS_NAME_selfBuild=0 "-d, --user-directory " 1 "-o, --output " 2 "-h, --help" +CMD_OPTIONS_NAME_selfMock2=0 "-o, --option1" 1 "-2, --this-is-option2 " 2 "-3, --flag3" 3 "-4, --with-default " 4 "-h, --help" CMD_OPTIONS_NAME_selfMock3=0 "-h, --help" CMD_OPTIONS_NAME_selfMock4=0 "-h, --help" CMD_OPTIONS_NAME_selfRelease=0 "-t, --github-release-token " 1 "-b, --bump-level " 2 "--dry-run" 3 "--upload-artifacts-only" 4 "-h, --help" @@ -144,7 +144,7 @@ CMD_OPTS_DEFAULT_selfBuild=0 "" 1 "" 2 "" CMD_OPTS_DEFAULT_selfConfig=0 "" 1 "" 2 "" 3 "" CMD_OPTS_DEFAULT_selfExport=0 "" 1 "" CMD_OPTS_DEFAULT_selfMock1=0 "" -CMD_OPTS_DEFAULT_selfMock2=0 "" 1 "" 2 "" +CMD_OPTS_DEFAULT_selfMock2=0 "" 1 "" 2 "" 3 "cool" 4 "" CMD_OPTS_DEFAULT_selfMock3=0 "" CMD_OPTS_DEFAULT_selfMock4=0 "" CMD_OPTS_DEFAULT_selfRelease=0 "" 1 "" 2 "" 3 "" 4 "" @@ -157,7 +157,7 @@ CMD_OPTS_HAS_VALUE_selfBuild=0 "true" 1 "true" 2 "false" CMD_OPTS_HAS_VALUE_selfConfig=0 "false" 1 "false" 2 "false" 3 "false" CMD_OPTS_HAS_VALUE_selfExport=0 "false" 1 "false" CMD_OPTS_HAS_VALUE_selfMock1=0 "false" -CMD_OPTS_HAS_VALUE_selfMock2=0 "false" 1 "true" 2 "false" +CMD_OPTS_HAS_VALUE_selfMock2=0 "false" 1 "true" 2 "false" 3 "true" 4 "false" CMD_OPTS_HAS_VALUE_selfMock3=0 "false" CMD_OPTS_HAS_VALUE_selfMock4=0 "false" CMD_OPTS_HAS_VALUE_selfRelease=0 "true" 1 "true" 2 "false" 3 "false" 4 "false" @@ -170,7 +170,7 @@ CMD_OPTS_NAME_SC_selfBuild=0 "VALET_USER_DIRECTORY" 1 "VALET_OUTPUT" 2 "" CMD_OPTS_NAME_SC_selfConfig=0 "VALET_NO_EDIT" 1 "VALET_OVERRIDE" 2 "VALET_EXPORT_CURRENT_VALUES" 3 "" CMD_OPTS_NAME_SC_selfExport=0 "VALET_EXPORT_ALL" 1 "" CMD_OPTS_NAME_SC_selfMock1=0 "" -CMD_OPTS_NAME_SC_selfMock2=0 "" 1 "VALET_THIS_IS_OPTION2" 2 "" +CMD_OPTS_NAME_SC_selfMock2=0 "" 1 "VALET_THIS_IS_OPTION2" 2 "VALET_FLAG3" 3 "VALET_WITH_DEFAULT" 4 "" CMD_OPTS_NAME_SC_selfMock3=0 "" CMD_OPTS_NAME_SC_selfMock4=0 "" CMD_OPTS_NAME_SC_selfRelease=0 "VALET_GITHUB_RELEASE_TOKEN" 1 "VALET_BUMP_LEVEL" 2 "VALET_DRY_RUN" 3 "VALET_UPLOAD_ARTIFACTS_ONLY" 4 "" @@ -184,7 +184,7 @@ CMD_OPTS_NAME_selfBuild=0 "userDirectory" 1 "output" 2 "help" CMD_OPTS_NAME_selfConfig=0 "noEdit" 1 "override" 2 "exportCurrentValues" 3 "help" CMD_OPTS_NAME_selfExport=0 "exportAll" 1 "help" CMD_OPTS_NAME_selfMock1=0 "help" -CMD_OPTS_NAME_selfMock2=0 "option1" 1 "thisIsOption2" 2 "help" +CMD_OPTS_NAME_selfMock2=0 "option1" 1 "thisIsOption2" 2 "flag3" 3 "withDefault" 4 "help" CMD_OPTS_NAME_selfMock3=0 "help" CMD_OPTS_NAME_selfMock4=0 "help" CMD_OPTS_NAME_selfRelease=0 "githubReleaseToken" 1 "bumpLevel" 2 "dryRun" 3 "uploadArtifactsOnly" 4 "help" @@ -198,7 +198,7 @@ CMD_OPTS_selfBuild=0 "-d --user-directory" 1 "-o --output" 2 "-h --help" CMD_OPTS_selfConfig=0 "--no-edit" 1 "--override" 2 "--export-current-values" 3 "-h --help" CMD_OPTS_selfExport=0 "-a --export-all" 1 "-h --help" CMD_OPTS_selfMock1=0 "-h --help" -CMD_OPTS_selfMock2=0 "-o --option1" 1 "-2 --this-is-option2" 2 "-h --help" +CMD_OPTS_selfMock2=0 "-o --option1" 1 "-2 --this-is-option2" 2 "-3 --flag3" 3 "-4 --with-default" 4 "-h --help" CMD_OPTS_selfMock3=0 "-h --help" CMD_OPTS_selfMock4=0 "-h --help" CMD_OPTS_selfRelease=0 "-t --github-release-token" 1 "-b --bump-level" 2 "--dry-run" 3 "--upload-artifacts-only" 4 "-h --help" diff --git a/tests.d/1300-valet-cli/results.approved.md b/tests.d/1300-valet-cli/results.approved.md index f59fc2a..2d4146c 100644 --- a/tests.d/1300-valet-cli/results.approved.md +++ b/tests.d/1300-valet-cli/results.approved.md @@ -255,8 +255,14 @@ Failed as expected. **Error** output: ```log -ERROR Unknown option ⌜--unknown⌝. -Unknown option ⌜-colo⌝ (did you mean ⌜--no-colors⌝?). +INFO Fuzzy matching the option ⌜-colo⌝ to ⌜--no-colors⌝. +ERROR Unknown option ⌜--unknown⌝, valid options are: +-n +--no-colors +-c +--columns +-h +--help Use ⌜valet help --help⌝ to get help. ``` @@ -340,7 +346,9 @@ Failed as expected. ```log ERROR Expecting 1 argument(s), got extra argument ⌜nonNeededArg1⌝. -Unknown option ⌜-derp⌝. +Unknown option ⌜-derp⌝, valid options are: +-h +--help Expecting 1 argument(s), got extra argument ⌜anotherArg⌝. Use ⌜valet self mock1 --help⌝ to get help. @@ -947,7 +955,8 @@ Failed as expected. **Error** output: ```log -ERROR Unknown option ⌜-prof⌝ (did you mean ⌜--profiling⌝?). +INFO Fuzzy matching the option ⌜-prof⌝ to ⌜--profiling⌝. +ERROR ``` ### Testing temp files/directories creation, cleaning and custom cleanUp @@ -1111,11 +1120,9 @@ Failed as expected. **Error** output: ```log -$GLOBAL_VALET_HOME/valet.d/main: line 1009: command: unbound variable -EXIT Exiting with code 1, stack: -├─ in main::parseFunctionArguments() at $GLOBAL_VALET_HOME/valet.d/main:1 -├─ in main::runMenuWithSubCommands() at $GLOBAL_VALET_HOME/valet.d/main:517 -├─ in main::parseMainArguments() at $GLOBAL_VALET_HOME/valet.d/main:425 -└─ in main() at $GLOBAL_VALET_HOME/valet:104 +ERROR Unknown option ⌜--unknown⌝, valid options are: +-h +--help +Use ⌜valet self --help⌝ to get help. ``` diff --git a/tests.d/1301-profiler/results.approved.md b/tests.d/1301-profiler/results.approved.md index e2f0993..6a434b3 100644 --- a/tests.d/1301-profiler/results.approved.md +++ b/tests.d/1301-profiler/results.approved.md @@ -20,9 +20,9 @@ That's it! (D=function depth, I=level of indirection, S=subshell level, timer=elapsed time in seconds, delta=delta between the last command in seconds, caller source:line=the source file and line number of the caller of the function, function=the name of the function in which the command is executed, command=the executed command) D I S timer delta source:line function → command -00 00 00 0.0XXX 0.0XXX self-mock.sh:169 selfMock2() → local -a more -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → core::parseArguments arg1 arg2 -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → eval 'local parsingErrors option1 thisIsOption2 help firstArg +00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → local -a more +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → core::parseArguments arg1 arg2 +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → eval 'local parsingErrors option1 thisIsOption2 help firstArg local -a more option1="" thisIsOption2="${VALET_THIS_IS_OPTION2:-}" @@ -32,22 +32,22 @@ D I S timer delta source:line function more=( "arg2" )' -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → local parsingErrors option1 thisIsOption2 help firstArg -00 00 00 0.0XXX 0.0XXX self-mock.sh:171 selfMock2() → local -a more -00 00 00 0.0XXX 0.0XXX self-mock.sh:172 selfMock2() → option1= -00 00 00 0.0XXX 0.0XXX self-mock.sh:173 selfMock2() → thisIsOption2= -00 00 00 0.0XXX 0.0XXX self-mock.sh:174 selfMock2() → help= -00 00 00 0.0XXX 0.0XXX self-mock.sh:175 selfMock2() → parsingErrors= -00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → firstArg=arg1 -00 00 00 0.0XXX 0.0XXX self-mock.sh:179 selfMock2() → more=("arg2") -00 00 00 0.0XXX 0.0XXX self-mock.sh:171 selfMock2() → core::checkParseResults '' '' -00 00 00 0.0XXX 0.0XXX self-mock.sh:173 selfMock2() → log::info 'First argument: arg1.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:174 selfMock2() → log::info 'Option 1: .' -00 00 00 0.0XXX 0.0XXX self-mock.sh:175 selfMock2() → log::info 'Option 2: .' -00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → log::info 'More: arg2.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → aSubFunctionInselfMock2 -00 00 00 0.0XXX 0.0XXX self-mock.sh:185 aSubFunctionInselfMock2() → log::debug 'This is a sub function.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → printf '%s\n' 'That'\''s it!' +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → local parsingErrors option1 thisIsOption2 help firstArg +00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → local -a more +00 00 00 0.0XXX 0.0XXX self-mock.sh:179 selfMock2() → option1= +00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → thisIsOption2= +00 00 00 0.0XXX 0.0XXX self-mock.sh:181 selfMock2() → help= +00 00 00 0.0XXX 0.0XXX self-mock.sh:182 selfMock2() → parsingErrors= +00 00 00 0.0XXX 0.0XXX self-mock.sh:183 selfMock2() → firstArg=arg1 +00 00 00 0.0XXX 0.0XXX self-mock.sh:186 selfMock2() → more=("arg2") +00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → core::checkParseResults '' '' +00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → log::info 'First argument: arg1.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:181 selfMock2() → log::info 'Option 1: .' +00 00 00 0.0XXX 0.0XXX self-mock.sh:182 selfMock2() → log::info 'Option 2: .' +00 00 00 0.0XXX 0.0XXX self-mock.sh:183 selfMock2() → log::info 'More: arg2.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:185 selfMock2() → aSubFunctionInselfMock2 +00 00 00 0.0XXX 0.0XXX self-mock.sh:192 aSubFunctionInselfMock2() → log::debug 'This is a sub function.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:187 selfMock2() → printf '%s\n' 'That'\''s it!' ``` **Error** output: @@ -76,9 +76,9 @@ That's it! (D=function depth, I=level of indirection, S=subshell level, timer=elapsed time in seconds, delta=delta between the last command in seconds, caller source:line=the source file and line number of the caller of the function, function=the name of the function in which the command is executed, command=the executed command) D I S timer delta source:line function → command -00 00 00 0.0XXX 0.0XXX self-mock.sh:169 selfMock2() → local -a more -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → core::parseArguments arg1 arg2 -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → eval 'local parsingErrors option1 thisIsOption2 help firstArg +00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → local -a more +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → core::parseArguments arg1 arg2 +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → eval 'local parsingErrors option1 thisIsOption2 help firstArg local -a more option1="" thisIsOption2="${VALET_THIS_IS_OPTION2:-}" @@ -88,22 +88,22 @@ D I S timer delta source:line function more=( "arg2" )' -00 00 00 0.0XXX 0.0XXX self-mock.sh:170 selfMock2() → local parsingErrors option1 thisIsOption2 help firstArg -00 00 00 0.0XXX 0.0XXX self-mock.sh:171 selfMock2() → local -a more -00 00 00 0.0XXX 0.0XXX self-mock.sh:172 selfMock2() → option1= -00 00 00 0.0XXX 0.0XXX self-mock.sh:173 selfMock2() → thisIsOption2= -00 00 00 0.0XXX 0.0XXX self-mock.sh:174 selfMock2() → help= -00 00 00 0.0XXX 0.0XXX self-mock.sh:175 selfMock2() → parsingErrors= -00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → firstArg=arg1 -00 00 00 0.0XXX 0.0XXX self-mock.sh:179 selfMock2() → more=("arg2") -00 00 00 0.0XXX 0.0XXX self-mock.sh:171 selfMock2() → core::checkParseResults '' '' -00 00 00 0.0XXX 0.0XXX self-mock.sh:173 selfMock2() → log::info 'First argument: arg1.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:174 selfMock2() → log::info 'Option 1: .' -00 00 00 0.0XXX 0.0XXX self-mock.sh:175 selfMock2() → log::info 'Option 2: .' -00 00 00 0.0XXX 0.0XXX self-mock.sh:176 selfMock2() → log::info 'More: arg2.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → aSubFunctionInselfMock2 -00 00 00 0.0XXX 0.0XXX self-mock.sh:185 aSubFunctionInselfMock2() → log::debug 'This is a sub function.' -00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → printf '%s\n' 'That'\''s it!' +00 00 00 0.0XXX 0.0XXX self-mock.sh:177 selfMock2() → local parsingErrors option1 thisIsOption2 help firstArg +00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → local -a more +00 00 00 0.0XXX 0.0XXX self-mock.sh:179 selfMock2() → option1= +00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → thisIsOption2= +00 00 00 0.0XXX 0.0XXX self-mock.sh:181 selfMock2() → help= +00 00 00 0.0XXX 0.0XXX self-mock.sh:182 selfMock2() → parsingErrors= +00 00 00 0.0XXX 0.0XXX self-mock.sh:183 selfMock2() → firstArg=arg1 +00 00 00 0.0XXX 0.0XXX self-mock.sh:186 selfMock2() → more=("arg2") +00 00 00 0.0XXX 0.0XXX self-mock.sh:178 selfMock2() → core::checkParseResults '' '' +00 00 00 0.0XXX 0.0XXX self-mock.sh:180 selfMock2() → log::info 'First argument: arg1.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:181 selfMock2() → log::info 'Option 1: .' +00 00 00 0.0XXX 0.0XXX self-mock.sh:182 selfMock2() → log::info 'Option 2: .' +00 00 00 0.0XXX 0.0XXX self-mock.sh:183 selfMock2() → log::info 'More: arg2.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:185 selfMock2() → aSubFunctionInselfMock2 +00 00 00 0.0XXX 0.0XXX self-mock.sh:192 aSubFunctionInselfMock2() → log::debug 'This is a sub function.' +00 00 00 0.0XXX 0.0XXX self-mock.sh:187 selfMock2() → printf '%s\n' 'That'\''s it!' ``` **Error** output: diff --git a/valet.d/commands.d/self-build.sh b/valet.d/commands.d/self-build.sh index 6995e13..674925f 100644 --- a/valet.d/commands.d/self-build.sh +++ b/valet.d/commands.d/self-build.sh @@ -89,9 +89,9 @@ function selfBuild() { # shellcheck disable=SC2086 main::fuzzyFindOption "${1}" ${CMD_OPTS_selfBuild[*]} else - RETURNED_VALUE="" + RETURNED_VALUE="Unknown option ⌜${1}⌝" fi - core::fail "Unknown option ⌜${1}⌝${RETURNED_VALUE:-}." ;; + core::fail "${RETURNED_VALUE}" ;; *) core::fail "This command takes no arguments." ;; esac shift diff --git a/valet.d/commands.d/self-mock.sh b/valet.d/commands.d/self-mock.sh index 8be5b87..66911e0 100644 --- a/valet.d/commands.d/self-mock.sh +++ b/valet.d/commands.d/self-mock.sh @@ -160,6 +160,13 @@ options: - name: -2, --this-is-option2 description: |- An option with a value. +- name: -3, --flag3 + description: |- + Third option. +- name: -4, --with-default + description: |- + An option with a default value. + default: cool examples: - name: self mock2 -o -2 value1 arg1 more1 more2 description: |- diff --git a/valet.d/main b/valet.d/main index e9ce451..4889040 100644 --- a/valet.d/main +++ b/valet.d/main @@ -388,7 +388,7 @@ function main::parseMainArguments() { -*) # shellcheck disable=SC2048 disable=SC2086 main::fuzzyFindOption "${1}" ${CMD_OPTS_this[*]} - core::fail "Unknown option ⌜${1}⌝${RETURNED_VALUE:-}." + core::fail "${RETURNED_VALUE}" ;; *) commands+=("${1}") @@ -513,6 +513,10 @@ function main::runMenuWithSubCommands() { local command="${1}" shift + # export the command name for the menu + # shellcheck disable=SC2034 + CMD_COMMAND__menu="${command}" + local parsedArguments main::parseFunctionArguments "_menu" "$@" parsedArguments="${RETURNED_VALUE}" @@ -794,7 +798,7 @@ function main::fuzzyMatchCommandtoFunctionNameOrFail() { break fi - if [[ ${VALET_CONFIG_STRICT_MATCHING:-false} == "true" ]]; then + if [[ ${VALET_CONFIG_STRICT_MATCHING:-} == "true" ]]; then continue fi @@ -802,10 +806,10 @@ function main::fuzzyMatchCommandtoFunctionNameOrFail() { array::fuzzyFilter "${command}" CMD_ALL_COMMANDS_ARRAY if (( ${#RETURNED_ARRAY[@]} > 1)); then # case of ambiguous command, show the list of possible commands - local IFS=$'\n' # shellcheck disable=SC1091 source array array::fuzzyFilterSort "${command}" RETURNED_ARRAY "${VALET_CONFIG_COLOR_HIGHLIGHT:-$'\e'"[95m"}" "${VALET_CONFIG_COLOR_DEFAULT:-$'\e'"[0m"}" + local IFS=$'\n' core::fail "Found multiple matches for the command ⌜${command}⌝, please be more specific:"$'\n'"${RETURNED_ARRAY[*]}" elif (( ${#RETURNED_ARRAY[@]} == 1 )); then # case of a single match, we can match the function immediately @@ -818,7 +822,7 @@ function main::fuzzyMatchCommandtoFunctionNameOrFail() { if [[ -z "${functionName:-}" ]]; then local messageDetails - if [[ ${VALET_CONFIG_STRICT_MATCHING:-false} == "true" ]]; then + if [[ ${VALET_CONFIG_STRICT_MATCHING:-} == "true" ]]; then messageDetails="an exact command" else messageDetails="a matching command" @@ -901,6 +905,7 @@ function main::parseFunctionArguments() { else totalNbOptions=0; fi + # shellcheck disable=SC2034 local -n optionsHasValue="CMD_OPTS_HAS_VALUE_${functionName}" local -n optionsName="CMD_OPTS_NAME_${functionName}" local -n optionsNameSc="CMD_OPTS_NAME_SC_${functionName}" @@ -919,54 +924,79 @@ function main::parseFunctionArguments() { local badArguments=false + # we define reusable pieces of code that will be used to parse the options + # shellcheck disable=SC2016 + local optionFinder=' + # we are matching an option + # try to match the argument with one of the option name + if [[ totalNbOptions -gt 0 ]]; then + for optionIndex in "${!options[@]}"; do + for option in ${options[${optionIndex}]}; do + if [[ ${option} == "${1}" ]]; then + matchedIndex="${optionIndex}" + break 2 + fi + done + done + fi + + if [[ matchedIndex -ne -1 ]]; then + # its a match! + optionValue="${optionsHasValue[${matchedIndex}]:-}" + optionName="${optionsName[${matchedIndex}]}" + matchedOptionsIndex+=("${matchedIndex}") + fi + ' + + # shellcheck disable=SC2016 + local registerOption=' + if [[ ${optionValue} == "true" ]]; then + shift + if [[ $# -eq 0 ]]; then + outputErrors+=("Missing value for option ⌜${optionName}⌝.") + else + outputSetLine+=("${optionName}=\"${1//\"/\\\"}\"") + fi + else + outputSetLine+=("${optionName}=\"true\"") + fi + ' + # parse each arguments local -i optionIndex matchedIndex - local option startedArguments optionValue optionName argumentName + # shellcheck disable=SC2034 + local optionValue + local option startedArguments optionName argumentName startedArguments="false" while [[ $# -gt 0 ]]; do if [[ ${1} == "-"* && ${startedArguments} != "true" ]]; then + # we are matching an option, try to find the option index matchedIndex=-1 - - # we are matching an option - # try to match the argument with one of the option name - if [[ totalNbOptions -gt 0 ]]; then - for optionIndex in "${!options[@]}"; do - for option in ${options[${optionIndex}]}; do - if [[ ${option} == "${1}" ]]; then - matchedIndex="${optionIndex}" - break 2 - fi - done - done - fi + eval "${optionFinder}" if [[ matchedIndex -ne -1 ]]; then - # it's a match! - optionValue="${optionsHasValue[${matchedIndex}]:-}" - optionName="${optionsName[${matchedIndex}]}" - matchedOptionsIndex+=("${matchedIndex}") - - if [[ ${optionValue} == "true" ]]; then - shift - if [[ $# -eq 0 ]]; then - outputErrors+=("Missing value for option ⌜${optionName}⌝.") - else - outputSetLine+=("${optionName}=\"${1//\"/\\\"}\"") - fi - else - outputSetLine+=("${optionName}=\"true\"") - fi + # it's a match! register the option + eval "${registerOption}" elif [[ ${1} == "--" ]]; then # if we have -- we stop parsing options startedArguments="true" else - # if we didn't match any option, flag it as unknown option and add it to the leftOver + # if we didn't match any known option, try to fuzzy find it # shellcheck disable=SC2048 disable=SC2086 main::fuzzyFindOption "${1}" ${options[*]} - outputErrors+=("Unknown option ⌜${1}⌝${RETURNED_VALUE:-}.") + + if [[ -n "${RETURNED_VALUE2}" ]]; then + # we found a single match! + matchedIndex=-1 + eval "${optionFinder//"\${1}"/"${RETURNED_VALUE2}"}" + eval "${registerOption}" + else + # we found multiple matches or no match + outputErrors+=("${RETURNED_VALUE}") + fi fi else @@ -1043,6 +1073,7 @@ function main::parseFunctionArguments() { fi done if [[ ${optionMatched} == "false" ]]; then + # shellcheck disable=SC2034 optionName="${optionsName[${optionIndex}]}" optionNameSc="${optionsNameSc[${optionIndex}]:-}" optionDefault="${optionsDefault[${optionIndex}]:-}" @@ -1087,29 +1118,54 @@ function main::parseFunctionArguments() { RETURNED_VALUE="${outputString}" } -# Tries to help the user by suggesting a fix for an unknown option -# we receive the function options and the unknown option string +# Tries to find a match for an inexact option. +# If the strict matching is enabled, we will only suggest a fix (if we find a match). +# If strict matching is disabled and we have found a single match, we can return it. # -# $1: the user string to match -# $2+: options to match against +# - $1: the user string to match +# - $2+: options to match against # -# Usage: -# main::fuzzyFindOption opt1 option1 option2 option3 && fuzzyOption="${RETURNED_VALUE}" +# Returns: +# +# - `RETURNED_VALUE`: An error message with the suggested option(s). +# - `RETURNED_VALUE2`: The single matched option that we can use. +# +# ```bash +# main::fuzzyFindOption opt1 option1 option2 option3 && fuzzyOption="${RETURNED_VALUE}" +# ``` function main::fuzzyFindOption() { local unknownOption suggestedOption unknownOption="${1}" shift _OPTIONS_TO_MATCH=("$@") + local singleMatchedOption + local IFS=$'\n' + # split to get one possible option per line array::fuzzyFilter "${unknownOption}" _OPTIONS_TO_MATCH if (( ${#RETURNED_ARRAY[@]} == 1 )); then - suggestedOption=" (did you mean ⌜${RETURNED_ARRAY[0]}⌝?)" + if [[ ${VALET_CONFIG_STRICT_MATCHING:-} == "true" ]]; then + suggestedOption="Unknown option ⌜${unknownOption}⌝, did you mean ⌜${RETURNED_ARRAY[0]}⌝?" + else + log::info "Fuzzy matching the option ⌜${unknownOption}⌝ to ⌜${RETURNED_ARRAY[0]}⌝." + singleMatchedOption="${RETURNED_ARRAY[0]}" + fi elif (( ${#RETURNED_ARRAY[@]} > 1 )); then - suggestedOption=" (did you mean one of ⌜${RETURNED_ARRAY[*]}⌝?)" + # shellcheck disable=SC1091 + source array + array::fuzzyFilterSort "${unknownOption}" _OPTIONS_TO_MATCH "${VALET_CONFIG_COLOR_HIGHLIGHT:-$'\e'"[95m"}" "${VALET_CONFIG_COLOR_DEFAULT:-$'\e'"[0m"}" + if [[ ${VALET_CONFIG_STRICT_MATCHING:-} == "true" ]]; then + suggestedOption="Unknown option ⌜${unknownOption}⌝, valid matches are:"$'\n'"${RETURNED_ARRAY[*]}" + else + suggestedOption="Found multiple matches for the option ⌜${unknownOption}⌝, please be more specific:"$'\n'"${RETURNED_ARRAY[*]}" + fi + else + suggestedOption="Unknown option ⌜${unknownOption}⌝, valid options are:"$'\n'"${_OPTIONS_TO_MATCH[*]}" fi RETURNED_VALUE="${suggestedOption:-}" + RETURNED_VALUE2="${singleMatchedOption:-}" } # Parse the arguments and options of a function. diff --git a/valet.d/version b/valet.d/version index 7651e24..511073a 100644 --- a/valet.d/version +++ b/valet.d/version @@ -1 +1 @@ -0.18.229 \ No newline at end of file +0.18.243 \ No newline at end of file