Skip to content

Commit e770ee9

Browse files
authored
Merge pull request #1390 from urfave/saschagrunert-suggestions
Add suggestions support (#977)
2 parents 97a222b + f3cf764 commit e770ee9

File tree

13 files changed

+335
-11
lines changed

13 files changed

+335
-11
lines changed

.github/workflows/cli.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: vet
3838
run: go run internal/build/build.go vet
3939

40-
- name: test with tags
40+
- name: test with urfave_cli_no_docs tag
4141
run: go run internal/build/build.go -tags urfave_cli_no_docs test
4242

4343
- name: test
@@ -47,7 +47,7 @@ jobs:
4747
run: go run internal/build/build.go check-binary-size
4848

4949
- name: check-binary-size with tags (informational only)
50-
run: go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size || true
50+
run: go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size
5151

5252
- name: Upload coverage to Codecov
5353
if: success() && matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ You can use the following build tags:
6363

6464
When set, this removes `ToMarkdown` and `ToMan` methods, so your application
6565
won't be able to call those. This reduces the resulting binary size by about
66-
300-400 KB (measured using Go 1.18.1 on Linux/amd64), due to less dependencies.
66+
300-400 KB (measured using Go 1.18.1 on Linux/amd64), due to fewer dependencies.
6767

6868
### GOPATH
6969

app.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type App struct {
9494
// single-character bool arguments into one
9595
// i.e. foobar -o -v -> foobar -ov
9696
UseShortOptionHandling bool
97+
// Enable suggestions for commands and flags
98+
Suggest bool
9799

98100
didSetup bool
99101
}
@@ -264,6 +266,11 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
264266
return err
265267
}
266268
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
269+
if a.Suggest {
270+
if suggestion, err := a.suggestFlagFromError(err, ""); err == nil {
271+
fmt.Fprintf(a.Writer, suggestion)
272+
}
273+
}
267274
_ = ShowAppHelp(cCtx)
268275
return err
269276
}
@@ -383,6 +390,11 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
383390
return err
384391
}
385392
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
393+
if a.Suggest {
394+
if suggestion, err := a.suggestFlagFromError(err, cCtx.Command.Name); err == nil {
395+
fmt.Fprintf(a.Writer, suggestion)
396+
}
397+
}
386398
_ = ShowSubcommandHelp(cCtx)
387399
return err
388400
}

command.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ func (c *Command) Run(ctx *Context) (err error) {
119119
}
120120
_, _ = fmt.Fprintln(cCtx.App.Writer, "Incorrect Usage:", err.Error())
121121
_, _ = fmt.Fprintln(cCtx.App.Writer)
122+
if ctx.App.Suggest {
123+
if suggestion, err := ctx.App.suggestFlagFromError(err, c.Name); err == nil {
124+
fmt.Fprintf(cCtx.App.Writer, suggestion)
125+
}
126+
}
122127
_ = ShowCommandHelp(cCtx, c.Name)
123128
return err
124129
}
@@ -249,6 +254,7 @@ func (c *Command) startApp(ctx *Context) error {
249254
app.ErrWriter = ctx.App.ErrWriter
250255
app.ExitErrHandler = ctx.App.ExitErrHandler
251256
app.UseShortOptionHandling = ctx.App.UseShortOptionHandling
257+
app.Suggest = ctx.App.Suggest
252258

253259
app.categories = newCommandCategories()
254260
for _, command := range c.Subcommands {

docs/v2/manual.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,13 @@ In this example the flag could be used like this :
14101410

14111411
Side note: quotes may be necessary around the date depending on your layout (if you have spaces for instance)
14121412

1413+
### Suggestions
1414+
1415+
To enable flag and command suggestions, set `app.Suggest = true`. If the suggest
1416+
feature is enabled, then the help output of the corresponding command will
1417+
provide an appropriate suggestion for the provided flag or subcommand if
1418+
available.
1419+
14131420
### Full API Example
14141421

14151422
**Notice**: This is a contrived (functioning) example meant strictly for API

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.18
44

55
require (
66
github.com/BurntSushi/toml v1.1.0
7+
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0
78
github.com/cpuguy83/go-md2man/v2 v2.0.1
89
golang.org/x/text v0.3.7
910
gopkg.in/yaml.v2 v2.4.0

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
22
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3+
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c h1:CucViv7orgFBMkehuFFdkCVF5ERovbkRRyhvaYaHu/k=
4+
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c/go.mod h1:bV/CkX4+ANGDaBwbHkt9kK287al/i9BsB18PRBvyqYo=
5+
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 h1:R/qAiUxFT3mNgQaNqJe0IVznjKRNm23ohAIh9lgtlzc=
6+
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U=
37
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
48
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
9+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
511
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
612
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
13+
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
14+
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
715
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
816
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
917
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=

godoc-current.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ type App struct {
305305
// single-character bool arguments into one
306306
// i.e. foobar -o -v -> foobar -ov
307307
UseShortOptionHandling bool
308+
// Enable suggestions for commands and flags
309+
Suggest bool
308310

309311
// Has unexported fields.
310312
}

help.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import (
1010
"unicode/utf8"
1111
)
1212

13+
const (
14+
helpName = "help"
15+
helpAlias = "h"
16+
)
17+
1318
var helpCommand = &Command{
14-
Name: "help",
15-
Aliases: []string{"h"},
19+
Name: helpName,
20+
Aliases: []string{helpAlias},
1621
Usage: "Shows a list of commands or help for one command",
1722
ArgsUsage: "[command]",
1823
Action: func(cCtx *Context) error {
@@ -27,8 +32,8 @@ var helpCommand = &Command{
2732
}
2833

2934
var helpSubcommand = &Command{
30-
Name: "help",
31-
Aliases: []string{"h"},
35+
Name: helpName,
36+
Aliases: []string{helpAlias},
3237
Usage: "Shows a list of commands or help for one command",
3338
ArgsUsage: "[command]",
3439
Action: func(cCtx *Context) error {
@@ -214,7 +219,13 @@ func ShowCommandHelp(ctx *Context, command string) error {
214219
}
215220

216221
if ctx.App.CommandNotFound == nil {
217-
return Exit(fmt.Sprintf("No help topic for '%v'", command), 3)
222+
errMsg := fmt.Sprintf("No help topic for '%v'", command)
223+
if ctx.App.Suggest {
224+
if suggestion := suggestCommand(ctx.App.Commands, command); suggestion != "" {
225+
errMsg += ". " + suggestion
226+
}
227+
}
228+
return Exit(errMsg, 3)
218229
}
219230

220231
ctx.App.CommandNotFound(ctx, command)

parse.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple
2626
return err
2727
}
2828

29-
errStr := err.Error()
30-
trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -")
31-
if errStr == trimmed {
29+
trimmed, trimErr := flagFromError(err)
30+
if trimErr != nil {
3231
return err
3332
}
3433

@@ -67,6 +66,19 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple
6766
}
6867
}
6968

69+
const providedButNotDefinedErrMsg = "flag provided but not defined: -"
70+
71+
// flagFromError tries to parse a provided flag from an error message. If the
72+
// parsing fials, it returns the input error and an empty string
73+
func flagFromError(err error) (string, error) {
74+
errStr := err.Error()
75+
trimmed := strings.TrimPrefix(errStr, providedButNotDefinedErrMsg)
76+
if errStr == trimmed {
77+
return "", err
78+
}
79+
return trimmed, nil
80+
}
81+
7082
func splitShortOptions(set *flag.FlagSet, arg string) []string {
7183
shortFlagsExist := func(s string) bool {
7284
for _, c := range s[1:] {

suggestions.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/antzucaro/matchr"
7+
)
8+
9+
const didYouMeanTemplate = "Did you mean '%s'?"
10+
11+
func (a *App) suggestFlagFromError(err error, command string) (string, error) {
12+
flag, parseErr := flagFromError(err)
13+
if parseErr != nil {
14+
return "", err
15+
}
16+
17+
flags := a.Flags
18+
if command != "" {
19+
cmd := a.Command(command)
20+
if cmd == nil {
21+
return "", err
22+
}
23+
flags = cmd.Flags
24+
}
25+
26+
suggestion := a.suggestFlag(flags, flag)
27+
if len(suggestion) == 0 {
28+
return "", err
29+
}
30+
31+
return fmt.Sprintf(didYouMeanTemplate+"\n\n", suggestion), nil
32+
}
33+
34+
func (a *App) suggestFlag(flags []Flag, provided string) (suggestion string) {
35+
distance := 0.0
36+
37+
for _, flag := range flags {
38+
flagNames := flag.Names()
39+
if !a.HideHelp {
40+
flagNames = append(flagNames, HelpFlag.Names()...)
41+
}
42+
for _, name := range flagNames {
43+
newDistance := matchr.JaroWinkler(name, provided, true)
44+
if newDistance > distance {
45+
distance = newDistance
46+
suggestion = name
47+
}
48+
}
49+
}
50+
51+
if len(suggestion) == 1 {
52+
suggestion = "-" + suggestion
53+
} else if len(suggestion) > 1 {
54+
suggestion = "--" + suggestion
55+
}
56+
57+
return suggestion
58+
}
59+
60+
// suggestCommand takes a list of commands and a provided string to suggest a
61+
// command name
62+
func suggestCommand(commands []*Command, provided string) (suggestion string) {
63+
distance := 0.0
64+
for _, command := range commands {
65+
for _, name := range append(command.Names(), helpName, helpAlias) {
66+
newDistance := matchr.JaroWinkler(name, provided, true)
67+
if newDistance > distance {
68+
distance = newDistance
69+
suggestion = name
70+
}
71+
}
72+
}
73+
74+
return fmt.Sprintf(didYouMeanTemplate, suggestion)
75+
}

0 commit comments

Comments
 (0)