From d8c15d7b2348de1f8568c3f5cdad899701535565 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Jan 2026 15:46:26 +0000 Subject: [PATCH 1/5] feat: add 'Did you mean?' suggestions for unknown flags Fixes #205 When users provide an unknown flag (e.g., --if-exist instead of --if-exists), the error now includes a helpful suggestion pointing to the most similar valid flag. Implementation details: - Added Levenshtein distance algorithm for string similarity matching - Extract valid flag names from struct tags using reflection - Wrap unknown flag errors with hints suggesting closest matches - Only suggest flags within reasonable edit distance to avoid unrelated suggestions The suggestion mechanism uses the existing hint package to display helpful messages like "Did you mean '--if-exists'?" when typos occur. This improves the developer experience, especially for newcomers who may make minor typos in flag names. --- util/flagutil/parse.go | 152 +++++++++++++++++++++++++++++++++ util/flagutil/parse_test.go | 166 ++++++++++++++++++++++++++++++++++++ 2 files changed, 318 insertions(+) diff --git a/util/flagutil/parse.go b/util/flagutil/parse.go index 24243d5b32..27c3f260d1 100644 --- a/util/flagutil/parse.go +++ b/util/flagutil/parse.go @@ -3,10 +3,12 @@ package flagutil import ( "context" "os" + "reflect" "strings" "github.com/EarthBuild/earthbuild/ast/commandflag" "github.com/EarthBuild/earthbuild/ast/spec" + "github.com/EarthBuild/earthbuild/util/hint" "github.com/EarthBuild/earthbuild/util/stringutil" "github.com/pkg/errors" @@ -14,6 +16,154 @@ import ( "github.com/urfave/cli/v2" ) +// levenshteinDistance calculates the edit distance between two strings. +func levenshteinDistance(s1, s2 string) int { + if len(s1) == 0 { + return len(s2) + } + if len(s2) == 0 { + return len(s1) + } + + // Create a 2D slice for dynamic programming + d := make([][]int, len(s1)+1) + for i := range d { + d[i] = make([]int, len(s2)+1) + d[i][0] = i + } + for j := range d[0] { + d[0][j] = j + } + + for i := 1; i <= len(s1); i++ { + for j := 1; j <= len(s2); j++ { + cost := 1 + if s1[i-1] == s2[j-1] { + cost = 0 + } + d[i][j] = min( + d[i-1][j]+1, // deletion + d[i][j-1]+1, // insertion + d[i-1][j-1]+cost, // substitution + ) + } + } + return d[len(s1)][len(s2)] +} + +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} + +// extractFlagNames extracts all long flag names from a struct using reflection. +func extractFlagNames(data any) []string { + var flagNames []string + if data == nil { + return flagNames + } + + v := reflect.ValueOf(data) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return flagNames + } + + t := v.Type() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + longTag := field.Tag.Get("long") + if longTag != "" { + flagNames = append(flagNames, longTag) + } + } + return flagNames +} + +// findClosestFlag finds the most similar flag name to the given unknown flag. +// Returns the suggested flag and whether a good suggestion was found. +func findClosestFlag(unknownFlag string, validFlags []string) (string, bool) { + if len(validFlags) == 0 { + return "", false + } + + // Remove leading dashes from the unknown flag for comparison + unknownFlag = strings.TrimLeft(unknownFlag, "-") + + bestMatch := "" + bestDistance := -1 + + for _, validFlag := range validFlags { + distance := levenshteinDistance(unknownFlag, validFlag) + if bestDistance == -1 || distance < bestDistance { + bestDistance = distance + bestMatch = validFlag + } + } + + // Only suggest if the distance is reasonable (less than half the length of the unknown flag) + // This prevents suggesting completely unrelated flags + maxDistance := len(unknownFlag) / 2 + if maxDistance < 2 { + maxDistance = 2 // Allow at least 2 character difference for short flags + } + + if bestDistance <= maxDistance { + return bestMatch, true + } + return "", false +} + +// suggestFlagIfUnknown checks if the error is about an unknown flag and adds a suggestion if possible. +func suggestFlagIfUnknown(err error, data any) error { + if err == nil { + return nil + } + + errMsg := err.Error() + + // Check if this is an "unknown flag" error + // The go-flags library returns errors like "unknown flag `flag-name'" + if !strings.Contains(errMsg, "unknown flag") { + return err + } + + // Extract the unknown flag name from the error message + // Pattern: "unknown flag `flag-name'" + startIdx := strings.Index(errMsg, "`") + endIdx := strings.LastIndex(errMsg, "'") + if startIdx == -1 || endIdx == -1 || startIdx >= endIdx { + return err + } + + unknownFlag := errMsg[startIdx+1 : endIdx] + + // Get all valid flag names from the struct + validFlags := extractFlagNames(data) + if len(validFlags) == 0 { + return err + } + + // Find the closest matching flag + suggestion, found := findClosestFlag(unknownFlag, validFlags) + if !found { + return err + } + + // Wrap the error with a helpful hint + return hint.Wrapf(err, "Did you mean '--%s'?", suggestion) +} + // ArgumentModFunc accepts a flagName which corresponds to the long flag name, and a pointer // to a flag value. The pointer is nil if no flag was given. // the function returns a new pointer set to nil if one wants to pretend as if no value was given, @@ -81,6 +231,8 @@ func ParseArgsWithValueModifierAndOptions( if parserOptions&flags.PrintErrors != flags.None { p.WriteHelp(os.Stderr) } + // Try to provide helpful suggestions for unknown flags + err = suggestFlagIfUnknown(err, data) return nil, err } if modFuncErr != nil { diff --git a/util/flagutil/parse_test.go b/util/flagutil/parse_test.go index c9b29c657f..46be4c8062 100644 --- a/util/flagutil/parse_test.go +++ b/util/flagutil/parse_test.go @@ -1,9 +1,11 @@ package flagutil import ( + "errors" "reflect" "testing" + "github.com/EarthBuild/earthbuild/util/hint" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" @@ -127,3 +129,167 @@ func TestNegativeParseParams(t *testing.T) { assert.Error(t, err) } } + +func TestLevenshteinDistance(t *testing.T) { + t.Parallel() + + tests := []struct { + s1 string + s2 string + expected int + }{ + {"if-exist", "if-exists", 1}, + {"keep-ts", "keep-own", 4}, + {"force", "from", 3}, + {"", "test", 4}, + {"test", "", 4}, + {"same", "same", 0}, + } + + for _, tt := range tests { + t.Run(tt.s1+"_"+tt.s2, func(t *testing.T) { + t.Parallel() + + result := levenshteinDistance(tt.s1, tt.s2) + if result != tt.expected { + t.Errorf("levenshteinDistance(%q, %q) = %d; want %d", tt.s1, tt.s2, result, tt.expected) + } + }) + } +} + +func TestExtractFlagNames(t *testing.T) { + t.Parallel() + + type TestOpts struct { + KeepTs bool `long:"keep-ts"` + KeepOwn bool `long:"keep-own"` + IfExists bool `long:"if-exists"` + Force bool `long:"force"` + NoTag bool // no long tag, should be ignored + } + + opts := &TestOpts{} + flags := extractFlagNames(opts) + + expected := []string{"keep-ts", "keep-own", "if-exists", "force"} + if len(flags) != len(expected) { + t.Errorf("extractFlagNames returned %d flags; want %d", len(flags), len(expected)) + } + + // Check that all expected flags are present + flagMap := make(map[string]bool) + for _, f := range flags { + flagMap[f] = true + } + for _, exp := range expected { + if !flagMap[exp] { + t.Errorf("extractFlagNames missing expected flag: %s", exp) + } + } +} + +func TestFindClosestFlag(t *testing.T) { + t.Parallel() + + validFlags := []string{"keep-ts", "keep-own", "if-exists", "symlink-no-follow", "force"} + + tests := []struct { + unknownFlag string + expectedMatch string + shouldFind bool + description string + }{ + {"if-exist", "if-exists", true, "missing final 's'"}, + {"--if-exist", "if-exists", true, "with leading dashes"}, + {"keep-t", "keep-ts", true, "shortened version"}, + {"forc", "force", true, "missing final 'e'"}, + {"completely-different", "", false, "no close match"}, + {"xyz", "", false, "very short and different"}, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + t.Parallel() + + match, found := findClosestFlag(tt.unknownFlag, validFlags) + if found != tt.shouldFind { + t.Errorf("findClosestFlag(%q) found=%v; want %v (%s)", tt.unknownFlag, found, tt.shouldFind, tt.description) + } + if found && match != tt.expectedMatch { + t.Errorf("findClosestFlag(%q) = %q; want %q (%s)", tt.unknownFlag, match, tt.expectedMatch, tt.description) + } + }) + } +} + +func TestSuggestFlagIfUnknown(t *testing.T) { + t.Parallel() + + type TestOpts struct { + KeepTs bool `long:"keep-ts"` + KeepOwn bool `long:"keep-own"` + IfExists bool `long:"if-exists"` + Force bool `long:"force"` + } + + opts := &TestOpts{} + + tests := []struct { + inputError error + shouldHaveHint bool + expectedHint string + description string + }{ + { + errors.New("unknown flag `if-exist'"), + true, + "Did you mean '--if-exists'?", + "typo in if-exists flag", + }, + { + errors.New("unknown flag `keep-t'"), + true, + "Did you mean '--keep-ts'?", + "shortened keep-ts flag", + }, + { + errors.New("some other error"), + false, + "", + "non-flag error should pass through", + }, + { + errors.New("unknown flag `completely-wrong-flag'"), + false, + "", + "flag too different to suggest", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + t.Parallel() + + result := suggestFlagIfUnknown(tt.inputError, opts) + + // Check if the result is a hint.Error + hintErr, isHintErr := result.(*hint.Error) + + if tt.shouldHaveHint { + if !isHintErr { + t.Errorf("%s: expected hint error, got regular error: %v", tt.description, result) + return + } + hintText := hintErr.Hint() + if hintText != tt.expectedHint+"\n" { + t.Errorf("%s: hint = %q; want %q", tt.description, hintText, tt.expectedHint+"\n") + } + } else { + if isHintErr { + t.Errorf("%s: expected regular error, got hint error: %v", tt.description, result) + } + } + }) + } +} From 121ecfaa946d93eddb7596f01298ea5bc70ed2fe Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Sat, 10 Jan 2026 16:13:33 +0000 Subject: [PATCH 2/5] fix: simplify --- .claude/settings.local.json | 4 +- util/flagutil/parse.go | 80 ++++++++++++++----------------------- util/flagutil/parse_test.go | 2 +- 3 files changed, 33 insertions(+), 53 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d18abe0cb3..e05e6a8a57 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,9 @@ "Bash(git commit:*)", "Bash(gh pr view:*)", "Bash(grep:*)", - "Bash(earthly +earthly-linux-amd64:*)" + "Bash(earthly +earthly-linux-amd64:*)", + "Bash(go version:*)", + "Bash(go vet:*)" ] } } diff --git a/util/flagutil/parse.go b/util/flagutil/parse.go index 27c3f260d1..3175ec04d9 100644 --- a/util/flagutil/parse.go +++ b/util/flagutil/parse.go @@ -2,6 +2,7 @@ package flagutil import ( "context" + "math" "os" "reflect" "strings" @@ -51,24 +52,10 @@ func levenshteinDistance(s1, s2 string) int { return d[len(s1)][len(s2)] } -func min(a, b, c int) int { - if a < b { - if a < c { - return a - } - return c - } - if b < c { - return b - } - return c -} - // extractFlagNames extracts all long flag names from a struct using reflection. func extractFlagNames(data any) []string { - var flagNames []string if data == nil { - return flagNames + return nil } v := reflect.ValueOf(data) @@ -76,14 +63,13 @@ func extractFlagNames(data any) []string { v = v.Elem() } if v.Kind() != reflect.Struct { - return flagNames + return nil } t := v.Type() - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - longTag := field.Tag.Get("long") - if longTag != "" { + var flagNames []string + for i := range t.NumField() { + if longTag := t.Field(i).Tag.Get("long"); longTag != "" { flagNames = append(flagNames, longTag) } } @@ -101,23 +87,19 @@ func findClosestFlag(unknownFlag string, validFlags []string) (string, bool) { unknownFlag = strings.TrimLeft(unknownFlag, "-") bestMatch := "" - bestDistance := -1 + bestDistance := math.MaxInt for _, validFlag := range validFlags { - distance := levenshteinDistance(unknownFlag, validFlag) - if bestDistance == -1 || distance < bestDistance { + if distance := levenshteinDistance(unknownFlag, validFlag); distance < bestDistance { bestDistance = distance bestMatch = validFlag } } - // Only suggest if the distance is reasonable (less than half the length of the unknown flag) - // This prevents suggesting completely unrelated flags - maxDistance := len(unknownFlag) / 2 - if maxDistance < 2 { - maxDistance = 2 // Allow at least 2 character difference for short flags - } - + // Only suggest if the distance is reasonable (less than half the length of the unknown flag). + // This prevents suggesting completely unrelated flags. + // Allow at least 2 character difference for short flags. + maxDistance := max(len(unknownFlag)/2, 2) if bestDistance <= maxDistance { return bestMatch, true } @@ -130,38 +112,34 @@ func suggestFlagIfUnknown(err error, data any) error { return nil } - errMsg := err.Error() - - // Check if this is an "unknown flag" error - // The go-flags library returns errors like "unknown flag `flag-name'" - if !strings.Contains(errMsg, "unknown flag") { + unknownFlag, ok := extractUnknownFlagFromError(err) + if !ok { return err } - // Extract the unknown flag name from the error message - // Pattern: "unknown flag `flag-name'" - startIdx := strings.Index(errMsg, "`") - endIdx := strings.LastIndex(errMsg, "'") - if startIdx == -1 || endIdx == -1 || startIdx >= endIdx { + suggestion, found := findClosestFlag(unknownFlag, extractFlagNames(data)) + if !found { return err } - unknownFlag := errMsg[startIdx+1 : endIdx] + return hint.Wrapf(err, "Did you mean '--%s'?", suggestion) +} - // Get all valid flag names from the struct - validFlags := extractFlagNames(data) - if len(validFlags) == 0 { - return err +// extractUnknownFlagFromError extracts the flag name from an "unknown flag" error. +// The go-flags library returns errors like "unknown flag `flag-name'". +func extractUnknownFlagFromError(err error) (string, bool) { + errMsg := err.Error() + if !strings.Contains(errMsg, "unknown flag") { + return "", false } - // Find the closest matching flag - suggestion, found := findClosestFlag(unknownFlag, validFlags) - if !found { - return err + startIdx := strings.Index(errMsg, "`") + endIdx := strings.LastIndex(errMsg, "'") + if startIdx == -1 || endIdx == -1 || startIdx >= endIdx { + return "", false } - // Wrap the error with a helpful hint - return hint.Wrapf(err, "Did you mean '--%s'?", suggestion) + return errMsg[startIdx+1 : endIdx], true } // ArgumentModFunc accepts a flagName which corresponds to the long flag name, and a pointer diff --git a/util/flagutil/parse_test.go b/util/flagutil/parse_test.go index 46be4c8062..e6eb178482 100644 --- a/util/flagutil/parse_test.go +++ b/util/flagutil/parse_test.go @@ -139,7 +139,7 @@ func TestLevenshteinDistance(t *testing.T) { expected int }{ {"if-exist", "if-exists", 1}, - {"keep-ts", "keep-own", 4}, + {"keep-ts", "keep-own", 3}, {"force", "from", 3}, {"", "test", 4}, {"test", "", 4}, From 8c7bd58489f24c5e1f8003b39931cb3c3c2f1e1b Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Sat, 10 Jan 2026 16:13:57 +0000 Subject: [PATCH 3/5] fix: optimise algorythm --- util/flagutil/parse.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/util/flagutil/parse.go b/util/flagutil/parse.go index 3175ec04d9..4632e25d5a 100644 --- a/util/flagutil/parse.go +++ b/util/flagutil/parse.go @@ -18,6 +18,7 @@ import ( ) // levenshteinDistance calculates the edit distance between two strings. +// Uses O(min(m,n)) space by only keeping two rows of the DP matrix. func levenshteinDistance(s1, s2 string) int { if len(s1) == 0 { return len(s2) @@ -26,30 +27,36 @@ func levenshteinDistance(s1, s2 string) int { return len(s1) } - // Create a 2D slice for dynamic programming - d := make([][]int, len(s1)+1) - for i := range d { - d[i] = make([]int, len(s2)+1) - d[i][0] = i + // Ensure s2 is the shorter string to minimize space usage + if len(s1) < len(s2) { + s1, s2 = s2, s1 } - for j := range d[0] { - d[0][j] = j + + // Only need two rows: previous and current + prev := make([]int, len(s2)+1) + curr := make([]int, len(s2)+1) + + // Initialize first row + for j := range prev { + prev[j] = j } for i := 1; i <= len(s1); i++ { + curr[0] = i for j := 1; j <= len(s2); j++ { cost := 1 if s1[i-1] == s2[j-1] { cost = 0 } - d[i][j] = min( - d[i-1][j]+1, // deletion - d[i][j-1]+1, // insertion - d[i-1][j-1]+cost, // substitution + curr[j] = min( + prev[j]+1, // deletion + curr[j-1]+1, // insertion + prev[j-1]+cost, // substitution ) } + prev, curr = curr, prev } - return d[len(s1)][len(s2)] + return prev[len(s2)] } // extractFlagNames extracts all long flag names from a struct using reflection. From e8ecc1e1bd97d7cfa4b73c407e8a05e9c45b6317 Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Sat, 10 Jan 2026 16:35:21 +0000 Subject: [PATCH 4/5] fix: tidy parsing --- util/flagutil/parse.go | 17 ++++++++++------- util/flagutil/parse_test.go | 7 ++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/util/flagutil/parse.go b/util/flagutil/parse.go index 4632e25d5a..decfded017 100644 --- a/util/flagutil/parse.go +++ b/util/flagutil/parse.go @@ -5,6 +5,7 @@ import ( "math" "os" "reflect" + "regexp" "strings" "github.com/EarthBuild/earthbuild/ast/commandflag" @@ -132,21 +133,23 @@ func suggestFlagIfUnknown(err error, data any) error { return hint.Wrapf(err, "Did you mean '--%s'?", suggestion) } +// unknownFlagRegexp matches the flag name in go-flags error messages like "unknown flag `flag-name'". +var unknownFlagRegexp = regexp.MustCompile("`([^']+)'") + // extractUnknownFlagFromError extracts the flag name from an "unknown flag" error. -// The go-flags library returns errors like "unknown flag `flag-name'". +// Uses type assertion to check for the specific error type from go-flags library. func extractUnknownFlagFromError(err error) (string, bool) { - errMsg := err.Error() - if !strings.Contains(errMsg, "unknown flag") { + var flagErr *flags.Error + if !errors.As(err, &flagErr) || flagErr.Type != flags.ErrUnknownFlag { return "", false } - startIdx := strings.Index(errMsg, "`") - endIdx := strings.LastIndex(errMsg, "'") - if startIdx == -1 || endIdx == -1 || startIdx >= endIdx { + matches := unknownFlagRegexp.FindStringSubmatch(flagErr.Message) + if len(matches) < 2 { return "", false } - return errMsg[startIdx+1 : endIdx], true + return matches[1], true } // ArgumentModFunc accepts a flagName which corresponds to the long flag name, and a pointer diff --git a/util/flagutil/parse_test.go b/util/flagutil/parse_test.go index e6eb178482..893c5be3d0 100644 --- a/util/flagutil/parse_test.go +++ b/util/flagutil/parse_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/EarthBuild/earthbuild/util/hint" + "github.com/jessevdk/go-flags" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" @@ -242,13 +243,13 @@ func TestSuggestFlagIfUnknown(t *testing.T) { description string }{ { - errors.New("unknown flag `if-exist'"), + &flags.Error{Type: flags.ErrUnknownFlag, Message: "unknown flag `if-exist'"}, true, "Did you mean '--if-exists'?", "typo in if-exists flag", }, { - errors.New("unknown flag `keep-t'"), + &flags.Error{Type: flags.ErrUnknownFlag, Message: "unknown flag `keep-t'"}, true, "Did you mean '--keep-ts'?", "shortened keep-ts flag", @@ -260,7 +261,7 @@ func TestSuggestFlagIfUnknown(t *testing.T) { "non-flag error should pass through", }, { - errors.New("unknown flag `completely-wrong-flag'"), + &flags.Error{Type: flags.ErrUnknownFlag, Message: "unknown flag `completely-wrong-flag'"}, false, "", "flag too different to suggest", From a2a25cb6477540a0ada62ab11b5a841e3acdf5cb Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Sat, 10 Jan 2026 16:43:54 +0000 Subject: [PATCH 5/5] fix: use existing lib --- util/flagutil/parse.go | 45 ++----------------------------------- util/flagutil/parse_test.go | 28 ----------------------- 2 files changed, 2 insertions(+), 71 deletions(-) diff --git a/util/flagutil/parse.go b/util/flagutil/parse.go index decfded017..8b6f830639 100644 --- a/util/flagutil/parse.go +++ b/util/flagutil/parse.go @@ -12,54 +12,13 @@ import ( "github.com/EarthBuild/earthbuild/ast/spec" "github.com/EarthBuild/earthbuild/util/hint" "github.com/EarthBuild/earthbuild/util/stringutil" + "github.com/agext/levenshtein" "github.com/pkg/errors" "github.com/jessevdk/go-flags" "github.com/urfave/cli/v2" ) -// levenshteinDistance calculates the edit distance between two strings. -// Uses O(min(m,n)) space by only keeping two rows of the DP matrix. -func levenshteinDistance(s1, s2 string) int { - if len(s1) == 0 { - return len(s2) - } - if len(s2) == 0 { - return len(s1) - } - - // Ensure s2 is the shorter string to minimize space usage - if len(s1) < len(s2) { - s1, s2 = s2, s1 - } - - // Only need two rows: previous and current - prev := make([]int, len(s2)+1) - curr := make([]int, len(s2)+1) - - // Initialize first row - for j := range prev { - prev[j] = j - } - - for i := 1; i <= len(s1); i++ { - curr[0] = i - for j := 1; j <= len(s2); j++ { - cost := 1 - if s1[i-1] == s2[j-1] { - cost = 0 - } - curr[j] = min( - prev[j]+1, // deletion - curr[j-1]+1, // insertion - prev[j-1]+cost, // substitution - ) - } - prev, curr = curr, prev - } - return prev[len(s2)] -} - // extractFlagNames extracts all long flag names from a struct using reflection. func extractFlagNames(data any) []string { if data == nil { @@ -98,7 +57,7 @@ func findClosestFlag(unknownFlag string, validFlags []string) (string, bool) { bestDistance := math.MaxInt for _, validFlag := range validFlags { - if distance := levenshteinDistance(unknownFlag, validFlag); distance < bestDistance { + if distance := levenshtein.Distance(unknownFlag, validFlag, nil); distance < bestDistance { bestDistance = distance bestMatch = validFlag } diff --git a/util/flagutil/parse_test.go b/util/flagutil/parse_test.go index 893c5be3d0..322f329a13 100644 --- a/util/flagutil/parse_test.go +++ b/util/flagutil/parse_test.go @@ -131,34 +131,6 @@ func TestNegativeParseParams(t *testing.T) { } } -func TestLevenshteinDistance(t *testing.T) { - t.Parallel() - - tests := []struct { - s1 string - s2 string - expected int - }{ - {"if-exist", "if-exists", 1}, - {"keep-ts", "keep-own", 3}, - {"force", "from", 3}, - {"", "test", 4}, - {"test", "", 4}, - {"same", "same", 0}, - } - - for _, tt := range tests { - t.Run(tt.s1+"_"+tt.s2, func(t *testing.T) { - t.Parallel() - - result := levenshteinDistance(tt.s1, tt.s2) - if result != tt.expected { - t.Errorf("levenshteinDistance(%q, %q) = %d; want %d", tt.s1, tt.s2, result, tt.expected) - } - }) - } -} - func TestExtractFlagNames(t *testing.T) { t.Parallel()