diff options
| author | Eyal Posener <[email protected]> | 2017-05-10 20:14:34 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2017-05-10 20:14:34 +0300 |
| commit | 1c743d8c0b8235ea2dbf0856987f8bd5b77a0042 (patch) | |
| tree | 139eb01e2305d3d43800095bc5db3fecd407a15d | |
| parent | 5db452a63f1b8ff0319f08986a4a04324647738f (diff) | |
| parent | 9de57bdcf5246827e9b1a57c905203e2edf6edf4 (diff) | |
Merge pull request #10 from posener/alongside
Enable completion and executable be the same command
| -rw-r--r-- | cmd/cmd.go | 129 | ||||
| -rw-r--r-- | complete.go (renamed from run.go) | 31 | ||||
| -rw-r--r-- | complete_test.go (renamed from run_test.go) | 0 | ||||
| -rw-r--r-- | example/self/main.go | 51 | ||||
| -rw-r--r-- | gocomplete/complete.go | 2 | ||||
| -rw-r--r-- | match/match_test.go | 150 | ||||
| -rw-r--r-- | readme.md | 9 |
7 files changed, 240 insertions, 132 deletions
@@ -11,81 +11,122 @@ import ( "github.com/posener/complete/cmd/install" ) +// CLI for command line +type CLI struct { + Name string + + install bool + uninstall bool + yes bool +} + +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 Run(cmd string) { - c := parseFlags(cmd) - err := c.validate() +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() + + err := f.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) + + 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", c.action(), err) + fmt.Printf("%s failed! %s\n", f.action(), err) os.Exit(3) } fmt.Println("Done!") + return true } // prompt use for approval -func prompt(action, cmd string) bool { - fmt.Printf("%s completion for %s? ", action, cmd) +// exit if approval was not given +func (f *CLI) prompt() { + defer fmt.Println(f.action() + "ing...") + if f.yes { + return + } + fmt.Printf("%s completion for %s? ", f.action(), f.Name) var answer string fmt.Scanln(&answer) switch strings.ToLower(answer) { case "y", "yes": - return true + return default: - return false + fmt.Println("Cancelling...") + os.Exit(1) } } -// config for command line -type config struct { - install bool - uninstall bool - yes bool -} +// 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 + } -// 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'") - flag.Parse() - return c -} + if installName == "" { + installName = defaultInstallName + } + if uninstallName == "" { + uninstallName = defaultUninstallName + } -// validate the config -func (c config) validate() error { - if c.install && c.uninstall { - return errors.New("Install and uninstall are exclusive") + 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 !c.install && !c.uninstall { - return errors.New("Must specify -install or -uninstall") + 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" } @@ -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 index 147a361..147a361 100644 --- a/run_test.go +++ b/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"), - prefix: "ab", - want: true, - }, - { - m: Prefix("abcd"), - prefix: "ac", - want: false, - }, - { - m: Prefix(""), - prefix: "ac", - 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: Prefix("abcd"), + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: "ab", want: true}, + {prefix: "ac", want: false}, + }, }, { - m: File("/file.txt"), - prefix: "file.txt", - want: false, + m: Prefix(""), + tests: []matcherTest{ + {prefix: "ac", want: false}, + {prefix: "", want: true}, + }, }, { - m: File("/file.txt"), - prefix: "./file.txt", - 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: File("/file.txt"), - prefix: "/file.txt", - want: true, + 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: 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) + } + }) + } } } @@ -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) |
