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 From 404634e843081e7010260bd95006b84d6c40a8fd Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Sat, 6 May 2017 22:16:39 +0300 Subject: Add licence --- LICENSE.txt | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..298f0e2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -- cgit v1.2.3