From 703dd6ebc30f7c6f5a5c02e07a307e0e34d9c2c2 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Sat, 6 May 2017 22:06:49 +0300 Subject: improve docs --- cmd.go | 87 ---------------------------- cmd/cmd.go | 96 +++++++++++++++++++++++++++++++ cmd/install/home.go | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/install/install.go | 43 ++++++++++++++ cmd/install/root.go | 29 ++++++++++ command.go | 48 ++++++++++++---- example_test.go | 46 +++++++++++++++ gocomplete/complete.go | 5 +- gocomplete/tests.go | 6 +- install/home.go | 153 ------------------------------------------------- install/install.go | 43 -------------- install/root.go | 29 ---------- match.go | 48 ++++++++++++++++ option.go | 42 -------------- predicate.go | 75 ++++++++++++++---------- readme.md | 65 ++++++++++++++++++++- run.go | 11 +++- 17 files changed, 572 insertions(+), 406 deletions(-) delete mode 100644 cmd.go create mode 100644 cmd/cmd.go create mode 100644 cmd/install/home.go create mode 100644 cmd/install/install.go create mode 100644 cmd/install/root.go create mode 100644 example_test.go delete mode 100644 install/home.go delete mode 100644 install/install.go delete mode 100644 install/root.go create mode 100644 match.go delete mode 100644 option.go diff --git a/cmd.go b/cmd.go deleted file mode 100644 index a9024b2..0000000 --- a/cmd.go +++ /dev/null @@ -1,87 +0,0 @@ -package complete - -import ( - "errors" - "flag" - "fmt" - "os" - "strings" - - "github.com/posener/complete/install" -) - -func runCommandLine(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, c.root) - } else { - err = install.Uninstall(cmd, c.root) - } - if err != nil { - fmt.Printf("%s failed! %s\n", c.action(), err) - os.Exit(3) - } - fmt.Println("Done!") -} - -func prompt(action, cmd string) bool { - fmt.Printf("%s bash completion for %s? ", action, cmd) - var answer string - fmt.Scanln(&answer) - - switch strings.ToLower(answer) { - case "y", "yes": - return true - default: - return false - } -} - -type config struct { - install bool - uninstall bool - root bool - yes bool -} - -func parseFlags(cmd string) config { - var c config - flag.BoolVar(&c.install, "install", false, - fmt.Sprintf("Install bash completion for %s command", cmd)) - flag.BoolVar(&c.uninstall, "uninstall", false, - fmt.Sprintf("Uninstall bash completion for %s command", cmd)) - flag.BoolVar(&c.root, "root", false, - "(Un)Install as root:\n"+ - " (Un)Install at /etc/bash_completion.d/ (user should have write permissions to that directory).\n"+ - " If not set, a complete command will be added(removed) to ~/.bashrc") - flag.BoolVar(&c.yes, "y", false, "Don't prompt user for typing 'yes'") - flag.Parse() - return c -} - -func (c config) validate() error { - if c.install && c.uninstall { - return errors.New("Install and uninstall are exclusive") - } - if !c.install && !c.uninstall { - return errors.New("Must specify -install or -uninstall") - } - return nil -} - -func (c config) action() string { - if c.install { - return "Install" - } - return "Uninstall" -} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..8149aac --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,96 @@ +// Package cmd used for command line options for the complete tool +package cmd + +import ( + "errors" + "flag" + "fmt" + "os" + "strings" + + "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, c.root) + } else { + err = install.Uninstall(cmd, c.root) + } + if err != nil { + fmt.Printf("%s failed! %s\n", c.action(), err) + os.Exit(3) + } + fmt.Println("Done!") +} + +// prompt use for approval +func prompt(action, cmd string) bool { + fmt.Printf("%s bash 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 + root 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 bash completion for %s command", cmd)) + flag.BoolVar(&c.uninstall, "uninstall", false, + fmt.Sprintf("Uninstall bash completion for %s command", cmd)) + flag.BoolVar(&c.root, "root", false, + "(Un)Install as root:\n"+ + " (Un)Install at /etc/bash_completion.d/ (user should have write permissions to that directory).\n"+ + " If not set, a complete command will be added(removed) to ~/.bashrc") + flag.BoolVar(&c.yes, "y", false, "Don't prompt user for typing 'yes'") + flag.Parse() + return c +} + +// validate the config +func (c config) validate() error { + if c.install && c.uninstall { + return errors.New("Install and uninstall are exclusive") + } + if !c.install && !c.uninstall { + return errors.New("Must specify -install or -uninstall") + } + return nil +} + +// action name according to the config values. +func (c config) action() string { + if c.install { + return "Install" + } + return "Uninstall" +} diff --git a/cmd/install/home.go b/cmd/install/home.go new file mode 100644 index 0000000..2694e96 --- /dev/null +++ b/cmd/install/home.go @@ -0,0 +1,152 @@ +package install + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/user" + "path/filepath" +) + +type home struct{} + +func (home) Install(cmd, bin string) error { + bashRCFileName, err := bashRCFileName() + if err != nil { + return err + } + completeCmd := completeCmd(cmd, bin) + if isInFile(bashRCFileName, completeCmd) { + return errors.New("Already installed in ~/.bashrc") + } + + bashRC, err := os.OpenFile(bashRCFileName, os.O_RDWR|os.O_APPEND, 0) + if err != nil { + return err + } + defer bashRC.Close() + _, err = bashRC.WriteString(fmt.Sprintf("\n%s\n", completeCmd)) + return err +} + +func (home) Uninstall(cmd, bin string) error { + bashRC, err := bashRCFileName() + if err != nil { + return err + } + backup := bashRC + ".bck" + err = copyFile(bashRC, backup) + if err != nil { + return err + } + completeCmd := completeCmd(cmd, bin) + if !isInFile(bashRC, completeCmd) { + return errors.New("Does not installed in ~/.bashrc") + } + temp, err := uninstallToTemp(bashRC, completeCmd) + if err != nil { + return err + } + + err = copyFile(temp, bashRC) + if err != nil { + return err + } + + return os.Remove(backup) +} + +func completeCmd(cmd, bin string) string { + return fmt.Sprintf("complete -C %s %s", bin, cmd) +} + +func bashRCFileName() (string, error) { + u, err := user.Current() + if err != nil { + return "", err + } + return filepath.Join(u.HomeDir, ".bashrc"), nil +} + +func isInFile(name string, lookFor string) bool { + f, err := os.Open(name) + if err != nil { + return false + } + defer f.Close() + r := bufio.NewReader(f) + prefix := []byte{} + for { + line, isPrefix, err := r.ReadLine() + if err == io.EOF { + return false + } + if err != nil { + return false + } + if isPrefix { + prefix = append(prefix, line...) + continue + } + line = append(prefix, line...) + if string(line) == lookFor { + return true + } + prefix = prefix[:0] + } +} + +func uninstallToTemp(bashRCFileName, completeCmd string) (string, error) { + rf, err := os.Open(bashRCFileName) + if err != nil { + return "", err + } + defer rf.Close() + wf, err := ioutil.TempFile("/tmp", "bashrc-") + if err != nil { + return "", err + } + defer wf.Close() + + r := bufio.NewReader(rf) + prefix := []byte{} + for { + line, isPrefix, err := r.ReadLine() + if err == io.EOF { + break + } + if err != nil { + return "", err + } + if isPrefix { + prefix = append(prefix, line...) + continue + } + line = append(prefix, line...) + str := string(line) + if str == completeCmd { + continue + } + wf.WriteString(str + "\n") + prefix = prefix[:0] + } + return wf.Name(), nil +} + +func copyFile(src string, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + return err +} diff --git a/cmd/install/install.go b/cmd/install/install.go new file mode 100644 index 0000000..cef11f0 --- /dev/null +++ b/cmd/install/install.go @@ -0,0 +1,43 @@ +package install + +import ( + "os" + "path/filepath" +) + +type installer interface { + Install(cmd, bin string) error + Uninstall(cmd, bin string) error +} + +func Install(cmd string, asRoot bool) error { + bin, err := getBinaryPath() + if err != nil { + return err + } + return getInstaller(asRoot).Install(cmd, bin) +} + +func Uninstall(cmd string, asRoot bool) error { + bin, err := getBinaryPath() + if err != nil { + return err + } + return getInstaller(asRoot).Uninstall(cmd, bin) +} + +func getInstaller(asRoot bool) installer { + if asRoot { + return root{} + } else { + return home{} + } +} + +func getBinaryPath() (string, error) { + bin, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Abs(bin) +} diff --git a/cmd/install/root.go b/cmd/install/root.go new file mode 100644 index 0000000..66e23b3 --- /dev/null +++ b/cmd/install/root.go @@ -0,0 +1,29 @@ +package install + +import "os" + +type root struct{} + +func (r root) Install(cmd string, bin string) error { + completeLink := getBashCompletionDLink(cmd) + err := r.Uninstall(cmd, bin) + if err != nil { + return err + } + return os.Symlink(bin, completeLink) +} + +func (root) Uninstall(cmd string, bin string) error { + completeLink := getBashCompletionDLink(cmd) + if _, err := os.Stat(completeLink); err == nil { + err := os.Remove(completeLink) + if err != nil { + return err + } + } + return nil +} + +func getBashCompletionDLink(cmd string) string { + return "/etc/bash_completion.d/" + cmd +} diff --git a/command.go b/command.go index eac9dde..b658af3 100644 --- a/command.go +++ b/command.go @@ -1,19 +1,40 @@ package complete -type Commands map[string]Command +// Command represents a command line +// It holds the data that enables auto completion of a given typed command line +// Command can also be a sub command. +type Command struct { + // Name is the name of command, + // IMPORTANT: For root command - it must be the same name as the program + // that the auto complete completes. So if the auto complete + // completes the 'go' command, Name must be equal to "go". + // It is optional for sub commands. + Name string -type Flags map[string]Predicate + // Sub is map of sub commands of the current command + // The key refer to the sub command name, and the value is it's + // Command descriptive struct. + Sub Commands -type Command struct { - Name string - Sub Commands + // Flags is a map of flags that the command accepts. + // The key is the flag name, and the value is it's prediction options. Flags Flags - Args Predicate + + // Args are extra arguments that the command accepts, those who are + // given without any flag before. + Args Predicate } +// Commands is the type of Sub member, it maps a command name to a command struct +type Commands map[string]Command + +// Flags is the type Flags of the Flags member, it maps a flag name to the flag +// prediction options. +type Flags map[string]Predicate + // options returns all available complete options for the given command // args are all except the last command line arguments relevant to the command -func (c *Command) options(args []string) (options []Option, only bool) { +func (c *Command) options(args []string) (options []Matcher, only bool) { // remove the first argument, which is the command name args = args[1:] @@ -37,7 +58,7 @@ func (c *Command) options(args []string) (options []Option, only bool) { // add global available complete options for flag := range c.Flags { - options = append(options, Arg(flag)) + options = append(options, MatchPrefix(flag)) } // add additional expected argument of the command @@ -46,7 +67,9 @@ func (c *Command) options(args []string) (options []Option, only bool) { return } -func (c *Command) searchSub(args []string) (sub string, all []Option, only bool) { +// searchSub searches recursively within sub commands if the sub command appear +// in the on of the arguments. +func (c *Command) searchSub(args []string) (sub string, all []Matcher, only bool) { for i, arg := range args { if cmd, ok := c.Sub[arg]; ok { sub = arg @@ -57,10 +80,11 @@ func (c *Command) searchSub(args []string) (sub string, all []Option, only bool) return "", nil, false } -func (c *Command) subCommands() []Option { - subs := make([]Option, 0, len(c.Sub)) +// suvCommands returns a list of matchers according to the sub command names +func (c *Command) subCommands() []Matcher { + subs := make([]Matcher, 0, len(c.Sub)) for sub := range c.Sub { - subs = append(subs, Arg(sub)) + subs = append(subs, MatchPrefix(sub)) } return subs } diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..35664f3 --- /dev/null +++ b/example_test.go @@ -0,0 +1,46 @@ +package complete_test + +import "github.com/posener/complete" + +func main() { + + // create a Command object, that represents the command we want + // to complete. + run := complete.Command{ + + // Name must be exactly as the binary that we want to complete + Name: "run", + + // Sub defines a list of sub commands of the program, + // this is recursive, since every command is of type command also. + Sub: complete.Commands{ + + // add a build sub command + "build": complete.Command{ + + // define flags of the build sub command + Flags: complete.Flags{ + // build sub command has a flag '-fast', which + // does not expects anything after it. + "-fast": complete.PredictNothing, + }, + }, + }, + + // define flags of the 'run' main command + Flags: complete.Flags{ + + // a flag '-h' which does not expects anything after it + "-h": complete.PredictNothing, + + // a flag -o, which expects a file ending with .out after + // it, the tab completion will auto complete for files matching + // the given pattern. + "-o": complete.PredictFiles("*.out"), + }, + } + + // run the command completion, as part of the main() function. + // this triggers the autocompletion when needed. + complete.Run(run) +} diff --git a/gocomplete/complete.go b/gocomplete/complete.go index 94586db..bdeecd1 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -1,3 +1,4 @@ +// Package main is complete tool for the go command line package main import ( @@ -7,7 +8,7 @@ import ( var ( predictEllipsis = complete.PredictSet("./...") - goFilesOrPackages = complete.PredictFiles("**.go"). + goFilesOrPackages = complete.PredictFiles("*.go"). Or(complete.PredictDirs). Or(predictEllipsis) ) @@ -15,7 +16,7 @@ var ( func main() { build := complete.Command{ Flags: complete.Flags{ - "-o": complete.PredictFiles("**"), + "-o": complete.PredictFiles("*"), "-i": complete.PredictNothing, "-a": complete.PredictNothing, diff --git a/gocomplete/tests.go b/gocomplete/tests.go index 60218a5..3a6a185 100644 --- a/gocomplete/tests.go +++ b/gocomplete/tests.go @@ -12,11 +12,11 @@ import ( ) func predictTest(testType string) complete.Predicate { - return func(last string) []complete.Option { + return func(last string) []complete.Matcher { tests := testNames(testType) - options := make([]complete.Option, len(tests)) + options := make([]complete.Matcher, len(tests)) for i := range tests { - options[i] = complete.Arg(tests[i]) + options[i] = complete.MatchPrefix(tests[i]) } return options } diff --git a/install/home.go b/install/home.go deleted file mode 100644 index 825bdb7..0000000 --- a/install/home.go +++ /dev/null @@ -1,153 +0,0 @@ -package install - -import ( - "bufio" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/user" - "path/filepath" -) - -type home struct{} - -func (home) Install(cmd, bin string) error { - bashRCFileName, err := bashRCFileName() - if err != nil { - return err - } - completeCmd := completeCmd(cmd, bin) - if isInFile(bashRCFileName, completeCmd) { - return errors.New("Already installed in ~/.bashrc") - } - - bashRC, err := os.OpenFile(bashRCFileName, os.O_RDWR|os.O_APPEND, 0) - if err != nil { - return err - } - defer bashRC.Close() - _, err = bashRC.WriteString(fmt.Sprintf("\n%s\n", completeCmd)) - return err -} - -func (home) Uninstall(cmd, bin string) error { - bashRC, err := bashRCFileName() - if err != nil { - return err - } - backup := bashRC + ".bck" - err = copyFile(bashRC, backup) - if err != nil { - return err - } - completeCmd := completeCmd(cmd, bin) - if !isInFile(bashRC, completeCmd) { - return errors.New("Does not installed in ~/.bashrc") - } - temp, err := uninstallToTemp(bashRC, completeCmd) - if err != nil { - return err - } - - err = copyFile(temp, bashRC) - if err != nil { - return err - } - - return os.Remove(backup) -} - -func completeCmd(cmd, bin string) string { - return fmt.Sprintf("complete -C %s %s", bin, cmd) -} - -func bashRCFileName() (string, error) { - u, err := user.Current() - if err != nil { - return "", err - } - return filepath.Join(u.HomeDir, ".bashrc"), nil -} - -func isInFile(name string, lookFor string) bool { - f, err := os.Open(name) - if err != nil { - return false - } - defer f.Close() - r := bufio.NewReader(f) - prefix := []byte{} - for { - line, isPrefix, err := r.ReadLine() - if err == io.EOF { - return false - } - if err != nil { - return false - } - if isPrefix { - prefix = append(prefix, line...) - continue - } - line = append(prefix, line...) - if string(line) == lookFor { - return true - } - prefix = prefix[:0] - } - return false -} - -func uninstallToTemp(bashRCFileName, completeCmd string) (string, error) { - rf, err := os.Open(bashRCFileName) - if err != nil { - return "", err - } - defer rf.Close() - wf, err := ioutil.TempFile("/tmp", "bashrc-") - if err != nil { - return "", err - } - defer wf.Close() - - r := bufio.NewReader(rf) - prefix := []byte{} - for { - line, isPrefix, err := r.ReadLine() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - if isPrefix { - prefix = append(prefix, line...) - continue - } - line = append(prefix, line...) - str := string(line) - if str == completeCmd { - continue - } - wf.WriteString(str + "\n") - prefix = prefix[:0] - } - return wf.Name(), nil -} - -func copyFile(src string, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - _, err = io.Copy(out, in) - return err -} diff --git a/install/install.go b/install/install.go deleted file mode 100644 index cef11f0..0000000 --- a/install/install.go +++ /dev/null @@ -1,43 +0,0 @@ -package install - -import ( - "os" - "path/filepath" -) - -type installer interface { - Install(cmd, bin string) error - Uninstall(cmd, bin string) error -} - -func Install(cmd string, asRoot bool) error { - bin, err := getBinaryPath() - if err != nil { - return err - } - return getInstaller(asRoot).Install(cmd, bin) -} - -func Uninstall(cmd string, asRoot bool) error { - bin, err := getBinaryPath() - if err != nil { - return err - } - return getInstaller(asRoot).Uninstall(cmd, bin) -} - -func getInstaller(asRoot bool) installer { - if asRoot { - return root{} - } else { - return home{} - } -} - -func getBinaryPath() (string, error) { - bin, err := os.Executable() - if err != nil { - return "", err - } - return filepath.Abs(bin) -} diff --git a/install/root.go b/install/root.go deleted file mode 100644 index 66e23b3..0000000 --- a/install/root.go +++ /dev/null @@ -1,29 +0,0 @@ -package install - -import "os" - -type root struct{} - -func (r root) Install(cmd string, bin string) error { - completeLink := getBashCompletionDLink(cmd) - err := r.Uninstall(cmd, bin) - if err != nil { - return err - } - return os.Symlink(bin, completeLink) -} - -func (root) Uninstall(cmd string, bin string) error { - completeLink := getBashCompletionDLink(cmd) - if _, err := os.Stat(completeLink); err == nil { - err := os.Remove(completeLink) - if err != nil { - return err - } - } - return nil -} - -func getBashCompletionDLink(cmd string) string { - return "/etc/bash_completion.d/" + cmd -} diff --git a/match.go b/match.go new file mode 100644 index 0000000..7593d65 --- /dev/null +++ b/match.go @@ -0,0 +1,48 @@ +package complete + +import ( + "path/filepath" + "strings" +) + +// Matcher matches itself to a string +// it is used for comparing a given argument to the last typed +// word, and see if it is a possible auto complete option. +type Matcher interface { + String() string + Match(prefix string) bool +} + +// MatchPrefix is a simple Matcher, if the word is it's prefix, there is a match +type MatchPrefix string + +func (a MatchPrefix) String() string { + return string(a) +} + +func (a MatchPrefix) Match(prefix string) bool { + return strings.HasPrefix(string(a), prefix) +} + +// MatchFileName is a file name Matcher, if the last word can prefix the +// MatchFileName path, there is a possible match +type MatchFileName string + +func (a MatchFileName) String() string { + return string(a) +} + +func (a MatchFileName) Match(prefix string) bool { + full, err := filepath.Abs(string(a)) + if err != nil { + Log("failed getting abs path of %s: %s", a, err) + } + prefixFull, err := filepath.Abs(prefix) + if err != nil { + Log("failed getting abs path of %s: %s", prefix, err) + } + + // if the file has the prefix as prefix, + // but we don't want to show too many files, so, if it is in a deeper directory - omit it. + return strings.HasPrefix(full, prefixFull) && (full == prefixFull || !strings.Contains(full[len(prefixFull)+1:], "/")) +} diff --git a/option.go b/option.go deleted file mode 100644 index 3915091..0000000 --- a/option.go +++ /dev/null @@ -1,42 +0,0 @@ -package complete - -import ( - "path/filepath" - "strings" -) - -type Option interface { - String() string - Matches(prefix string) bool -} - -type Arg string - -func (a Arg) String() string { - return string(a) -} - -func (a Arg) Matches(prefix string) bool { - return strings.HasPrefix(string(a), prefix) -} - -type ArgFileName string - -func (a ArgFileName) String() string { - return string(a) -} - -func (a ArgFileName) Matches(prefix string) bool { - full, err := filepath.Abs(string(a)) - if err != nil { - Log("failed getting abs path of %s: %s", a, err) - } - prefixFull, err := filepath.Abs(prefix) - if err != nil { - Log("failed getting abs path of %s: %s", prefix, err) - } - - // if the file has the prefix as prefix, - // but we don't want to show too many files, so, if it is in a deeper directory - omit it. - return strings.HasPrefix(full, prefixFull) && (full == prefixFull || !strings.Contains(full[len(prefixFull)+1:], "/")) -} diff --git a/predicate.go b/predicate.go index f975e27..a6746be 100644 --- a/predicate.go +++ b/predicate.go @@ -6,61 +6,58 @@ import ( ) // Predicate determines what terms can follow a command or a flag -type Predicate func(last string) []Option +// It is used for auto completion, given last - the last word in the already +// in the command line, what words can complete it. +type Predicate func(last string) []Matcher -// Or unions two predicate struct, so that the result predicate +// Or unions two predicate functions, so that the result predicate // returns the union of their predication func (p Predicate) Or(other Predicate) Predicate { if p == nil || other == nil { return nil } - return func(last string) []Option { return append(p.predict(last), other.predict(last)...) } + return func(last string) []Matcher { return append(p.predict(last), other.predict(last)...) } } -func (p Predicate) predict(last string) []Option { +func (p Predicate) predict(last string) []Matcher { if p == nil { return nil } return p(last) } -var ( - PredictNothing Predicate = nil -) +// PredictNothing does not expect anything after. +var PredictNothing Predicate = nil -func PredictAnything(last string) []Option { return nil } +// PredictNothing expects something, but nothing particular, such as a number +// or arbitrary name. +func PredictAnything(last string) []Matcher { return nil } +// PredictSet expects specific set of terms, given in the options argument. func PredictSet(options ...string) Predicate { - return func(last string) []Option { - ret := make([]Option, len(options)) + return func(last string) []Matcher { + ret := make([]Matcher, len(options)) for i := range options { - ret[i] = Arg(options[i]) + ret[i] = MatchPrefix(options[i]) } return ret } } -func PredictDirs(last string) (options []Option) { +// PredictDirs will search for directories in the given started to be typed +// path, if no path was started to be typed, it will complete to directories +// in the current working directory. +func PredictDirs(last string) (options []Matcher) { dir := dirFromLast(last) return dirsAt(dir) } -func dirsAt(path string) []Option { - dirs := []string{} - filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - dirs = append(dirs, path) - } - return nil - }) - if !filepath.IsAbs(path) { - filesToRel(dirs) - } - return filesToOptions(dirs) -} - +// PredictFiles will search for files matching the given pattern in the started to +// be typed path, if no path was started to be typed, it will complete to files that +// match the pattern in the current working directory. +// To match any file, use "*" as pattern. To match go files use "*.go", and so on. func PredictFiles(pattern string) Predicate { - return func(last string) []Option { + return func(last string) []Matcher { dir := dirFromLast(last) files, err := filepath.Glob(filepath.Join(dir, pattern)) if err != nil { @@ -69,10 +66,26 @@ func PredictFiles(pattern string) Predicate { if !filepath.IsAbs(pattern) { filesToRel(files) } - return filesToOptions(files) + return filesToMatchers(files) + } +} + +func dirsAt(path string) []Matcher { + dirs := []string{} + filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + dirs = append(dirs, path) + } + return nil + }) + if !filepath.IsAbs(path) { + filesToRel(dirs) } + return filesToMatchers(dirs) } +// filesToRel, change list of files to their names in the relative +// to current directory form. func filesToRel(files []string) { wd, err := os.Getwd() if err != nil { @@ -95,10 +108,10 @@ func filesToRel(files []string) { return } -func filesToOptions(files []string) []Option { - options := make([]Option, len(files)) +func filesToMatchers(files []string) []Matcher { + options := make([]Matcher, len(files)) for i, f := range files { - options[i] = ArgFileName(f) + options[i] = MatchFileName(f) } return options } diff --git a/readme.md b/readme.md index 8dc640b..62ce6af 100644 --- a/readme.md +++ b/readme.md @@ -2,8 +2,8 @@ [![Build Status](https://travis-ci.org/posener/complete.svg?branch=master)](https://travis-ci.org/posener/complete) [![codecov](https://codecov.io/gh/posener/complete/branch/master/graph/badge.svg)](https://codecov.io/gh/posener/complete) - -WIP +[![GoDoc](https://godoc.org/github.com/posener/complete?status.svg)](http://godoc.org/github.com/posener/complete) +[![Go Report Card](https://goreportcard.com/badge/github.com/posener/complete)](https://goreportcard.com/report/github.com/posener/complete) A tool for bash writing bash completion in go. @@ -30,3 +30,64 @@ gocomplete -install ``` gocomplete -uninstall ``` + +## Usage + +Assuming you have program called `run` and you want to have bash completion +for it, meaning, if you type `run` then space, then press the `Tab` key, +the shell will suggest relevant complete options. + +In that case, we will create a program called `runcomplete`, a go program, +with a `func main()` and so, that will make the completion of the `run` +program. Once the `runcomplete` will be in a binary form, we could +`runcomplete -install` and that will add to our shell all the bash completion +options for `run`. + +So here it is: + +```go +import "github.com/posener/complete" + +func main() { + + // create a Command object, that represents the command we want + // to complete. + run := complete.Command{ + + // Name must be exactly as the binary that we want to complete + Name: "run", + + // Sub defines a list of sub commands of the program, + // this is recursive, since every command is of type command also. + Sub: complete.Commands{ + + // add a build sub command + "build": complete.Command { + + // define flags of the build sub command + Flags: complete.Flags{ + // build sub command has a flag '-fast', which + // does not expects anything after it. + "-fast": complete.PredictNothing, + }, + }, + }, + + // define flags of the 'run' main command + Flags: complete.Flags{ + + // a flag '-h' which does not expects anything after it + "-h": complete.PredictNothing, + + // a flag -o, which expects a file ending with .out after + // it, the tab completion will auto complete for files matching + // the given pattern. + "-o": complete.PredictFiles("*.out"), + }, + } + + // run the command completion, as part of the main() function. + // this triggers the autocompletion when needed. + complete.Run(run) +} +``` diff --git a/run.go b/run.go index bd9f662..90d0df4 100644 --- a/run.go +++ b/run.go @@ -1,9 +1,16 @@ +// Package complete provides a tool for bash writing bash completion in go. +// +// Writing bash completion scripts is a hard work. This package provides an easy way +// to create bash completion scripts for any command, and also an easy way to install/uninstall +// the completion of the command. package complete import ( "fmt" "os" "strings" + + "github.com/posener/complete/cmd" ) const ( @@ -16,7 +23,7 @@ const ( func Run(c Command) { args, ok := getLine() if !ok { - runCommandLine(c.Name) + cmd.Run(c.Name) return } Log("Completing args: %s", args) @@ -35,7 +42,7 @@ func complete(c Command, args []string) (matching []string) { // choose only matching options l := last(args) for _, option := range options { - if option.Matches(l) { + if option.Match(l) { matching = append(matching, option.String()) } } -- cgit v1.2.3