From 9de57bdcf5246827e9b1a57c905203e2edf6edf4 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Wed, 10 May 2017 07:28:43 +0300 Subject: [PATCH] Enable completion and executable be the same command Fixes #6 --- cmd/cmd.go | 163 ++++++++++++++++++++------------ run.go => complete.go | 31 ++++-- run_test.go => complete_test.go | 0 example/self/main.go | 51 ++++++++++ gocomplete/complete.go | 2 +- match/match_test.go | 150 ++++++++++++++--------------- readme.md | 9 +- 7 files changed, 257 insertions(+), 149 deletions(-) rename run.go => complete.go (72%) rename run_test.go => complete_test.go (100%) create mode 100644 example/self/main.go diff --git a/cmd/cmd.go b/cmd/cmd.go index d0b341c..f796ec8 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -11,81 +11,122 @@ import ( "github.com/posener/complete/cmd/install" ) -// Run is used when running complete in command line mode. -// this is used when the complete is not completing words, but to -// install it or uninstall it. -func Run(cmd string) { - c := parseFlags(cmd) - err := c.validate() - if err != nil { - os.Stderr.WriteString(err.Error() + "\n") - os.Exit(1) - } - if !c.yes && !prompt(c.action(), cmd) { - fmt.Println("Cancelling...") - os.Exit(2) - } - fmt.Println(c.action() + "ing...") - if c.install { - err = install.Install(cmd) - } else { - err = install.Uninstall(cmd) - } - if err != nil { - fmt.Printf("%s failed! %s\n", c.action(), err) - os.Exit(3) - } - fmt.Println("Done!") -} +// CLI for command line +type CLI struct { + Name string -// prompt use for approval -func prompt(action, cmd string) bool { - fmt.Printf("%s completion for %s? ", action, cmd) - var answer string - fmt.Scanln(&answer) - - switch strings.ToLower(answer) { - case "y", "yes": - return true - default: - return false - } -} - -// config for command line -type config struct { install bool uninstall bool yes bool } -// create a config from command line arguments -func parseFlags(cmd string) config { - var c config - flag.BoolVar(&c.install, "install", false, - fmt.Sprintf("Install completion for %s command", cmd)) - flag.BoolVar(&c.uninstall, "uninstall", false, - fmt.Sprintf("Uninstall completion for %s command", cmd)) - flag.BoolVar(&c.yes, "y", false, "Don't prompt user for typing 'yes'") +const ( + defaultInstallName = "install" + defaultUninstallName = "uninstall" +) + +// Run is used when running complete in command line mode. +// this is used when the complete is not completing words, but to +// install it or uninstall it. +func (f *CLI) Run() bool { + + // add flags and parse them in case they were not added and parsed + // by the main program + f.AddFlags(nil, "", "") flag.Parse() - return c + + err := f.validate() + if err != nil { + os.Stderr.WriteString(err.Error() + "\n") + os.Exit(1) + } + + switch { + case f.install: + f.prompt() + err = install.Install(f.Name) + case f.uninstall: + f.prompt() + err = install.Uninstall(f.Name) + default: + // non of the action flags matched, + // returning false should make the real program execute + return false + } + + if err != nil { + fmt.Printf("%s failed! %s\n", f.action(), err) + os.Exit(3) + } + fmt.Println("Done!") + return true } -// validate the config -func (c config) validate() error { - if c.install && c.uninstall { - return errors.New("Install and uninstall are exclusive") +// prompt use for approval +// exit if approval was not given +func (f *CLI) prompt() { + defer fmt.Println(f.action() + "ing...") + if f.yes { + return } - if !c.install && !c.uninstall { - return errors.New("Must specify -install or -uninstall") + fmt.Printf("%s completion for %s? ", f.action(), f.Name) + var answer string + fmt.Scanln(&answer) + + switch strings.ToLower(answer) { + case "y", "yes": + return + default: + fmt.Println("Cancelling...") + os.Exit(1) + } +} + +// AddFlags adds the CLI flags to the flag set. +// If flags is nil, the default command line flags will be taken. +// Pass non-empty strings as installName and uninstallName to override the default +// flag names. +func (f *CLI) AddFlags(flags *flag.FlagSet, installName, uninstallName string) { + if flags == nil { + flags = flag.CommandLine + } + + if installName == "" { + installName = defaultInstallName + } + if uninstallName == "" { + uninstallName = defaultUninstallName + } + + if flags.Lookup(installName) == nil { + flags.BoolVar(&f.install, installName, false, + fmt.Sprintf("Install completion for %s command", f.Name)) + } + if flags.Lookup(uninstallName) == nil { + flags.BoolVar(&f.uninstall, uninstallName, false, + fmt.Sprintf("Uninstall completion for %s command", f.Name)) + } + if flags.Lookup("y") == nil { + flags.BoolVar(&f.yes, "y", false, "Don't prompt user for typing 'yes'") + } +} + +// validate the CLI +func (f *CLI) validate() error { + if f.install && f.uninstall { + return errors.New("Install and uninstall are mutually exclusive") } return nil } -// action name according to the config values. -func (c config) action() string { - if c.install { +// action name according to the CLI values. +func (f *CLI) action() string { + switch { + case f.install: return "Install" + case f.uninstall: + return "Uninstall" + default: + return "unknown" } - return "Uninstall" } diff --git a/run.go b/complete.go similarity index 72% rename from run.go rename to complete.go index 5d9706f..925c9a2 100644 --- a/run.go +++ b/complete.go @@ -18,23 +18,42 @@ const ( envDebug = "COMP_DEBUG" ) -// Run get a command, get the typed arguments from environment -// variable, and print out the complete options +// Complete structs define completion for a command with CLI options +type Complete struct { + Command Command + cmd.CLI +} + +// New creates a new complete command. // name is the name of command we want to auto complete. // IMPORTANT: it must be the same name - if the auto complete // completes the 'go' command, name must be equal to "go". -func Run(name string, c Command) { +// command is the struct of the command completion. +func New(name string, command Command) *Complete { + return &Complete{ + Command: command, + CLI: cmd.CLI{Name: name}, + } +} + +// Run get a command, get the typed arguments from environment +// variable, and print out the complete options +// returns success if the completion ran or if the cli matched +// any of the given flags, false otherwise +func (c *Complete) Run() bool { args, ok := getLine() if !ok { - cmd.Run(name) - return + // make sure flags parsed, + // in case they were not added in the main program + return c.CLI.Run() } Log("Completing args: %s", args) - options := complete(c, args) + options := complete(c.Command, args) Log("Completion: %s", options) output(options) + return true } // complete get a command an command line arguments and returns diff --git a/run_test.go b/complete_test.go similarity index 100% rename from run_test.go rename to complete_test.go diff --git a/example/self/main.go b/example/self/main.go new file mode 100644 index 0000000..068a0ac --- /dev/null +++ b/example/self/main.go @@ -0,0 +1,51 @@ +// Package self +// a program that complete itself +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/posener/complete" +) + +func main() { + + // add a variable to the program + var name string + flag.StringVar(&name, "name", "", "Give your name") + + // create the complete command + cmp := complete.New( + "self", + complete.Command{Flags: complete.Flags{"name": complete.PredictAnything}}, + ) + + // AddFlags adds the completion flags to the program flags, + // in case of using non-default flag set, it is possible to pass + // it as an argument. + // it is possible to set custom flags name + // so when one will type 'self -h', he will see '-complete' to install the + // completion and -uncomplete to uninstall it. + cmp.AddFlags(nil, "complete", "uncomplete") + + // parse the flags - both the program's flags and the completion flags + flag.Parse() + + // run the completion, in case that the completion was invoked + // and ran as a completion script or handled a flag that passed + // as argument, the Run method will return true, + // in that case, our program have nothing to do and should return. + if cmp.Run() { + return + } + + // if the completion did not do anything, we can run our program logic here. + if name == "" { + fmt.Println("Your name is missing") + os.Exit(1) + } + + fmt.Println("Hi,", name) +} diff --git a/gocomplete/complete.go b/gocomplete/complete.go index 1f94553..ac5f5ed 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -185,5 +185,5 @@ func main() { }, } - complete.Run("go", gogo) + complete.New("go", gogo).Run() } diff --git a/match/match_test.go b/match/match_test.go index f9afd46..ae1ffea 100644 --- a/match/match_test.go +++ b/match/match_test.go @@ -15,100 +15,92 @@ func TestMatch(t *testing.T) { panic(err) } - tests := []struct { - m Matcher + type matcherTest struct { prefix string want bool + } + + tests := []struct { + m Matcher + tests []matcherTest }{ { - m: Prefix("abcd"), - prefix: "", - want: true, + m: Prefix("abcd"), + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: "ab", want: true}, + {prefix: "ac", want: false}, + }, }, { - m: Prefix("abcd"), - prefix: "ab", - want: true, + m: Prefix(""), + tests: []matcherTest{ + {prefix: "ac", want: false}, + {prefix: "", want: true}, + }, }, { - m: Prefix("abcd"), - prefix: "ac", - want: false, + m: File("file.txt"), + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: "f", want: true}, + {prefix: "./f", want: true}, + {prefix: "file.", want: true}, + {prefix: "./file.", want: true}, + {prefix: "file.txt", want: true}, + {prefix: "./file.txt", want: true}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: false}, + {prefix: "/fil", want: false}, + {prefix: "/file.txt2", want: false}, + }, }, { - m: Prefix(""), - prefix: "ac", - want: false, + m: File("./file.txt"), + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: "f", want: true}, + {prefix: "./f", want: true}, + {prefix: "file.", want: true}, + {prefix: "./file.", want: true}, + {prefix: "file.txt", want: true}, + {prefix: "./file.txt", want: true}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: false}, + {prefix: "/fil", want: false}, + {prefix: "/file.txt2", want: false}, + }, }, { - m: Prefix(""), - prefix: "", - want: true, - }, - { - m: File("file.txt"), - prefix: "", - want: true, - }, - { - m: File("./file.txt"), - prefix: "", - want: true, - }, - { - m: File("./file.txt"), - prefix: "f", - want: true, - }, - { - m: File("./file.txt"), - prefix: "file.", - want: true, - }, - { - m: File("./file.txt"), - prefix: "./f", - want: true, - }, - { - m: File("./file.txt"), - prefix: "other.txt", - want: false, - }, - { - m: File("./file.txt"), - prefix: "/file.txt", - want: false, - }, - { - m: File("/file.txt"), - prefix: "file.txt", - want: false, - }, - { - m: File("/file.txt"), - prefix: "./file.txt", - want: false, - }, - { - m: File("/file.txt"), - prefix: "/file.txt", - want: true, - }, - { - m: File("/file.txt"), - prefix: "/fil", - want: true, + m: File("/file.txt"), + tests: []matcherTest{ + {prefix: "", want: false}, + {prefix: "f", want: false}, + {prefix: "./f", want: false}, + {prefix: "file.", want: false}, + {prefix: "./file.", want: false}, + {prefix: "file.txt", want: false}, + {prefix: "./file.txt", want: false}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: true}, + {prefix: "/fil", want: true}, + {prefix: "/file.txt2", want: false}, + }, }, } for _, tt := range tests { - name := tt.m.String() + "/" + tt.prefix - t.Run(name, func(t *testing.T) { - got := tt.m.Match(tt.prefix) - if got != tt.want { - t.Errorf("Failed %s: got = %t, want: %t", name, got, tt.want) - } - }) + for _, ttt := range tt.tests { + name := "matcher:" + tt.m.String() + "/prefix:" + ttt.prefix + t.Run(name, func(t *testing.T) { + got := tt.m.Match(ttt.prefix) + if got != ttt.want { + t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want) + } + }) + } } } diff --git a/readme.md b/readme.md index 81e0864..0010098 100644 --- a/readme.md +++ b/readme.md @@ -85,7 +85,12 @@ func main() { // run the command completion, as part of the main() function. // this triggers the autocompletion when needed. - // name must be exactly as the binary that we want to complete. - complete.Run("run", run) + // name must be exactly as the binary that we want to complete. + complete.New("run", run).Run() } ``` + +## Self completing program + +In case that the program that we want to complete is written in go we +can make it self completing. Here is an [example](./example/self/main.go)