Skip to content

Commit

Permalink
Command Suggestion for Incorrect Subcommands
Browse files Browse the repository at this point in the history
Earlier there were no suggestions for shp subcommands but with this patch entering wrong subcommands will give suggestions.
example:- shp build cr

Error: unknown command "cr" for "shp build"

Did you mean this?
	create

Signed-off-by: vinamra28 <vinjain@redhat.com>
  • Loading branch information
vinamra28 committed May 7, 2021
1 parent 16979e9 commit 36f8832
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/shipwright-io/build v0.3.1-0.20210330182238-23d2672f2f61
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c
k8s.io/api v0.19.7
k8s.io/apimachinery v0.19.7
k8s.io/cli-runtime v0.19.7
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tektoncd/pipeline v0.21.0/go.mod h1:GwdfGGt/5VhZL8JvJu8kFz8friKufcJ/TJkJmK6uc0U=
github.com/tektoncd/plumbing v0.0.0-20201021153918-6b7e894737b5/go.mod h1:WTWwsg91xgm+jPOKoyKVK/yRYxnVDlUYeDlypB1lDdQ=
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c h1:HelZ2kAFadG0La9d+4htN4HzQ68Bm2iM9qKMSMES6xg=
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c/go.mod h1:JlzghshsemAMDGZLytTFY8C1JQxQPhnatWqNwUXjggo=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
Expand Down
24 changes: 24 additions & 0 deletions pkg/shp/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/shipwright-io/cli/pkg/shp/cmd/build"
"github.com/shipwright-io/cli/pkg/shp/cmd/buildrun"
"github.com/shipwright-io/cli/pkg/shp/params"
"github.com/shipwright-io/cli/pkg/shp/suggestion"
)

var rootCmd = &cobra.Command{
Expand All @@ -25,5 +26,28 @@ func NewCmdSHP(ioStreams *genericclioptions.IOStreams) *cobra.Command {
rootCmd.AddCommand(build.Command(p, ioStreams))
rootCmd.AddCommand(buildrun.Command(p, ioStreams))

visitCommands(rootCmd, reconfigureCommandWithSubcommand)

return rootCmd
}

func reconfigureCommandWithSubcommand(cmd *cobra.Command) {
if len(cmd.Commands()) == 0 {
return
}

if cmd.Args == nil {
cmd.Args = cobra.ArbitraryArgs
}

if cmd.RunE == nil {
cmd.RunE = suggestion.SubcommandsRequiredWithSuggestions
}
}

func visitCommands(cmd *cobra.Command, f func(*cobra.Command)) {
f(cmd)
for _, child := range cmd.Commands() {
visitCommands(child, f)
}
}
12 changes: 11 additions & 1 deletion pkg/shp/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package cmd

import (
"fmt"
"os"
"testing"

"github.com/onsi/gomega"
"github.com/shipwright-io/cli/test/stub"

"k8s.io/cli-runtime/pkg/genericclioptions"
)
Expand All @@ -15,5 +17,13 @@ func TestCMD_NewCmdSHP(t *testing.T) {
genericOpts := &genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
cmd := NewCmdSHP(genericOpts)

g.Expect(cmd).NotTo(gomega.BeNil())
out, err := stub.ExecuteCommand(cmd, "build", "cr")

if err == nil {
t.Errorf("No errors was defined. Output: %s", out)
}

expected := fmt.Sprintf("unknown command %q for %q\n\nDid you mean this?\n\t%s\n", "cr", "shp build", "create")

g.Expect(err.Error()).To(gomega.Equal(expected))
}
90 changes: 90 additions & 0 deletions pkg/shp/suggestion/suggest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package suggestion

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/texttheater/golang-levenshtein/levenshtein"
)

// SubcommandsRequiredWithSuggestions will ensure we have a subcommand provided by the user and augments it with
// suggestion for commands, alias and help on root command.
func SubcommandsRequiredWithSuggestions(cmd *cobra.Command, args []string) error {
requireMsg := "unknown command %q for %q"
typedName := ""
// This will be triggered if cobra didn't find any subcommands.
// Find some suggestions.
var suggestions []string

if len(args) != 0 && !cmd.DisableSuggestions {
typedName += args[0]
if cmd.SuggestionsMinimumDistance <= 0 {
cmd.SuggestionsMinimumDistance = 2
}
// subcommand suggestions
suggestions = cmd.SuggestionsFor(args[0])

// subcommand alias suggestions (with distance, not exact)
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() {
continue
}

candidate := suggestsByPrefixOrLd(typedName, c.Name(), cmd.SuggestionsMinimumDistance)
if candidate == "" {
continue
}
_, found := Find(suggestions, candidate)
if !found {
suggestions = append(suggestions, candidate)
}
}

// help for root command
if !cmd.HasParent() {
candidate := suggestsByPrefixOrLd(typedName, "help", cmd.SuggestionsMinimumDistance)
if candidate != "" {
suggestions = append(suggestions, candidate)
}
}
}

var suggestionsMsg string
if len(suggestions) > 0 {
suggestionsMsg += "\nDid you mean this?\n"
for _, s := range suggestions {
suggestionsMsg += fmt.Sprintf("\t%v\n", s)
}
}

if suggestionsMsg != "" {
requireMsg = fmt.Sprintf("%s\n%s", requireMsg, suggestionsMsg)
return fmt.Errorf(requireMsg, typedName, cmd.CommandPath())
}

return cmd.Help()
}

// suggestsByPrefixOrLd suggests a command by levenshtein distance or by prefix.
// It returns an empty string if nothing was found
func suggestsByPrefixOrLd(typedName, candidate string, minDistance int) string {
levenshteinVariable := levenshtein.DistanceForStrings([]rune(typedName), []rune(candidate), levenshtein.DefaultOptions)
suggestByLevenshtein := levenshteinVariable <= minDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(candidate), strings.ToLower(typedName))
if !suggestByLevenshtein && !suggestByPrefix {
return ""
}
return candidate
}

// Find takes a slice and looks for an element in it. If found it will
// return it's key, otherwise it will return -1 and a bool of false.
func Find(slice []string, val string) (int, bool) {
for i, item := range slice {
if item == val {
return i, true
}
}
return -1, false
}
25 changes: 25 additions & 0 deletions pkg/shp/suggestion/suggest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package suggestion

import (
"fmt"
"os"
"testing"

"github.com/onsi/gomega"
"github.com/shipwright-io/cli/pkg/shp/cmd/build"

"k8s.io/cli-runtime/pkg/genericclioptions"
)

func TestSuggestion(t *testing.T) {
g := gomega.NewGomegaWithT(t)

genericOpts := &genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
cmd := build.Command(nil, genericOpts)

err := SubcommandsRequiredWithSuggestions(cmd, []string{"cr"})

expected := fmt.Sprintf("unknown command %q for %q\n\nDid you mean this?\n\t%s\n", "cr", "build", "create")

g.Expect(err.Error()).To(gomega.Equal(expected))
}
27 changes: 27 additions & 0 deletions test/stub/cobra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package stub

import (
"bytes"

"github.com/spf13/cobra"
)

// ExecuteCommand executes the root command passing the args and returns
// the output as a string and error
func ExecuteCommand(root *cobra.Command, args ...string) (string, error) {
_, output, err := ExecuteCommandC(root, args...)
return output, err
}

// ExecuteCommandC executes the root command passing the args and returns
// the root command, output as a string and error if any
func ExecuteCommandC(c *cobra.Command, args ...string) (*cobra.Command, string, error) {
buf := new(bytes.Buffer)
c.SetOutput(buf)
c.SetArgs(args)
c.SilenceUsage = true

root, err := c.ExecuteC()

return root, buf.String(), err
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 36f8832

Please sign in to comment.