Skip to content

Commit

Permalink
Merge branch 'main' into fix/autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
bartekpacia committed May 1, 2024
2 parents 0b61790 + 38003d1 commit 9835d74
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 41 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ jobs:
- if: matrix.go == '1.20.x' && matrix.os == 'ubuntu-latest'
run: make v3diff
- if: success() && matrix.go == '1.20.x' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true

test-docs:
name: test-docs
Expand Down
2 changes: 1 addition & 1 deletion docs/v3/examples/bash-completions.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ The default shell completion flag (`--generate-bash-completion`) is defined as

<!-- {
"args": ["&#45;&#45;generate&#45;shell&#45;completion"],
"output": "wat\nhelp\nh"
"output": "wat\nhelp\n"
} -->
```go
package main
Expand Down
7 changes: 0 additions & 7 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,8 @@ func ExampleCommand_Run_shellComplete_bash_withShortFlag() {
_ = cmd.Run(context.Background(), os.Args)
// Output:
// --other
// -o
// --xyz
// -x
// --help
// -h
}

func ExampleCommand_Run_shellComplete_bash_withLongFlag() {
Expand Down Expand Up @@ -376,10 +373,8 @@ func ExampleCommand_Run_shellComplete_bash() {
_ = cmd.Run(context.Background(), os.Args)
// Output:
// describeit
// d
// next
// help
// h
}

func ExampleCommand_Run_shellComplete_zsh() {
Expand Down Expand Up @@ -415,10 +410,8 @@ func ExampleCommand_Run_shellComplete_zsh() {
_ = cmd.Run(context.Background(), os.Args)
// Output:
// describeit:use it to see a description
// d:use it to see a description
// next:next example
// help:Shows a list of commands or help for one command
// h:Shows a list of commands or help for one command
}

func ExampleCommand_Run_sliceValues() {
Expand Down
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ module github.com/urfave/cli/v3

go 1.18

require (
github.com/stretchr/testify v1.8.4
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
)
require github.com/stretchr/testify v1.8.4

require (
github.com/BurntSushi/toml v1.3.2 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli-altsrc/v3 v3.0.0-alpha2 h1:j4SaBpPB8++L0c0KuTnz/Yus3UQoWJ54hQjhIMW8rCM=
github.com/urfave/cli-altsrc/v3 v3.0.0-alpha2/go.mod h1:Q79oyIY/z4jtzIrKEK6MUeWC7/szGr46x4QdOaOAIWc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
42 changes: 19 additions & 23 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,9 @@ func printCommandSuggestions(commands []*Command, writer io.Writer) {
continue
}
if strings.HasSuffix(os.Getenv("SHELL"), "zsh") {
for _, name := range command.Names() {
_, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
}
_, _ = fmt.Fprintf(writer, "%s:%s\n", command.Name, command.Usage)
} else {
for _, name := range command.Names() {
_, _ = fmt.Fprintf(writer, "%s\n", name)
}
_, _ = fmt.Fprintf(writer, "%s\n", command.Name)
}
}
}
Expand Down Expand Up @@ -195,23 +191,23 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden {
continue
}
for _, name := range flag.Names() {
name = strings.TrimSpace(name)
// this will get total count utf8 letters in flag name
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2 // reuse this count to generate single - or -- in flag completion
}
// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
// skip flag completion for short flags example -v or -x
if strings.HasPrefix(lastArg, "--") && count == 1 {
continue
}
// match if last argument matches this flag and it is not repeated
if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) {
flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
fmt.Fprintln(writer, flagCompletion)
}

name := strings.TrimSpace(flag.Names()[0])
// this will get total count utf8 letters in flag name
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2 // reuse this count to generate single - or -- in flag completion
}
// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
// skip flag completion for short flags example -v or -x
if strings.HasPrefix(lastArg, "--") && count == 1 {
continue
}
// match if last argument matches this flag and it is not repeated
if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) {
flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
fmt.Fprintln(writer, flagCompletion)

}
}
}
Expand Down
83 changes: 80 additions & 3 deletions suggestions.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cli

import (
"github.com/xrash/smetrics"
"math"
)

const suggestDidYouMeanTemplate = "Did you mean %q?"
Expand All @@ -16,13 +16,90 @@ type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string

type SuggestCommandFunc func(commands []*Command, provided string) string

// jaroDistance is the measure of similarity between two strings. It returns a
// value between 0 and 1, where 1 indicates identical strings and 0 indicates
// completely different strings.
//
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro.go.
func jaroDistance(a, b string) float64 {
if len(a) == 0 && len(b) == 0 {
return 1
}
if len(a) == 0 || len(b) == 0 {
return 0
}

lenA := float64(len(a))
lenB := float64(len(b))
hashA := make([]bool, len(a))
hashB := make([]bool, len(b))
maxDistance := int(math.Max(0, math.Floor(math.Max(lenA, lenB)/2.0)-1))

var matches float64
for i := 0; i < len(a); i++ {
start := int(math.Max(0, float64(i-maxDistance)))
end := int(math.Min(lenB-1, float64(i+maxDistance)))

for j := start; j <= end; j++ {
if hashB[j] {
continue
}
if a[i] == b[j] {
hashA[i] = true
hashB[j] = true
matches++
break
}
}
}
if matches == 0 {
return 0
}

var transpositions float64
var j int
for i := 0; i < len(a); i++ {
if !hashA[i] {
continue
}
for !hashB[j] {
j++
}
if a[i] != b[j] {
transpositions++
}
j++
}

transpositions /= 2
return ((matches / lenA) + (matches / lenB) + ((matches - transpositions) / matches)) / 3.0
}

// jaroWinkler is more accurate when strings have a common prefix up to a
// defined maximum length.
//
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro-winkler.go.
func jaroWinkler(a, b string) float64 {
// magic values are from https://github.com/xrash/smetrics/blob/039620a656736e6ad994090895784a7af15e0b80/jaro-winkler.go#L8
const (
boostThreshold = 0.7
prefixSize = 4
)
return smetrics.JaroWinkler(a, b, boostThreshold, prefixSize)
jaroDist := jaroDistance(a, b)
if jaroDist <= boostThreshold {
return jaroDist
}

prefix := int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b)))))

var prefixMatch float64
for i := 0; i < prefix; i++ {
if a[i] == b[i] {
prefixMatch++
} else {
break
}
}
return jaroDist + 0.1*prefixMatch*(1.0-jaroDist)
}

func suggestFlag(flags []Flag, provided string, hideHelp bool) string {
Expand Down
27 changes: 27 additions & 0 deletions suggestions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ import (
"github.com/stretchr/testify/assert"
)

func TestJaroWinkler(t *testing.T) {
// Given
for _, testCase := range []struct {
a, b string
expected float64
}{
{"", "", 1},
{"a", "", 0},
{"", "a", 0},
{"a", "a", 1},
{"a", "b", 0},
{"aa", "aa", 1},
{"aa", "bb", 0},
{"aaa", "aaa", 1},
{"aa", "ab", 0.6666666666666666},
{"aa", "ba", 0.6666666666666666},
{"ba", "aa", 0.6666666666666666},
{"ab", "aa", 0.6666666666666666},
} {
// When
res := jaroWinkler(testCase.a, testCase.b)

// Then
assert.Equal(t, testCase.expected, res)
}
}

func TestSuggestFlag(t *testing.T) {
// Given
app := buildExtendedTestCommand()
Expand Down
1 change: 1 addition & 0 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var authorsTemplate = `{{with $length := len .Authors}}{{if ne 1 $length}}S{{end
var visibleCommandTemplate = `{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}}
{{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}`
var visibleCommandCategoryTemplate = `{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{template "visibleCommandTemplate" .}}{{end}}{{end}}`
var visibleFlagCategoryTemplate = `{{range .VisibleFlagCategories}}
Expand Down

0 comments on commit 9835d74

Please sign in to comment.