From 703dd6ebc30f7c6f5a5c02e07a307e0e34d9c2c2 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Sat, 6 May 2017 22:06:49 +0300 Subject: [PATCH] improve docs --- cmd.go => cmd/cmd.go | 15 ++++-- {install => cmd/install}/home.go | 1 - {install => cmd/install}/install.go | 0 {install => cmd/install}/root.go | 0 command.go | 50 ++++++++++++++----- example_test.go | 46 ++++++++++++++++++ gocomplete/complete.go | 5 +- gocomplete/tests.go | 6 +-- option.go => match.go | 22 ++++++--- predicate.go | 75 +++++++++++++++++------------ readme.md | 65 ++++++++++++++++++++++++- run.go | 11 ++++- 12 files changed, 231 insertions(+), 65 deletions(-) rename cmd.go => cmd/cmd.go (79%) rename {install => cmd/install}/home.go (99%) rename {install => cmd/install}/install.go (100%) rename {install => cmd/install}/root.go (100%) create mode 100644 example_test.go rename option.go => match.go (50%) diff --git a/cmd.go b/cmd/cmd.go similarity index 79% rename from cmd.go rename to cmd/cmd.go index a9024b2..8149aac 100644 --- a/cmd.go +++ b/cmd/cmd.go @@ -1,4 +1,5 @@ -package complete +// Package cmd used for command line options for the complete tool +package cmd import ( "errors" @@ -7,10 +8,13 @@ import ( "os" "strings" - "github.com/posener/complete/install" + "github.com/posener/complete/cmd/install" ) -func runCommandLine(cmd string) { +// 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 { @@ -34,6 +38,7 @@ func runCommandLine(cmd string) { 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 @@ -47,6 +52,7 @@ func prompt(action, cmd string) bool { } } +// config for command line type config struct { install bool uninstall bool @@ -54,6 +60,7 @@ type config struct { yes bool } +// create a config from command line arguments func parseFlags(cmd string) config { var c config flag.BoolVar(&c.install, "install", false, @@ -69,6 +76,7 @@ func parseFlags(cmd string) config { return c } +// validate the config func (c config) validate() error { if c.install && c.uninstall { return errors.New("Install and uninstall are exclusive") @@ -79,6 +87,7 @@ func (c config) validate() error { return nil } +// action name according to the config values. func (c config) action() string { if c.install { return "Install" diff --git a/install/home.go b/cmd/install/home.go similarity index 99% rename from install/home.go rename to cmd/install/home.go index 825bdb7..2694e96 100644 --- a/install/home.go +++ b/cmd/install/home.go @@ -97,7 +97,6 @@ func isInFile(name string, lookFor string) bool { } prefix = prefix[:0] } - return false } func uninstallToTemp(bashRCFileName, completeCmd string) (string, error) { diff --git a/install/install.go b/cmd/install/install.go similarity index 100% rename from install/install.go rename to cmd/install/install.go diff --git a/install/root.go b/cmd/install/root.go similarity index 100% rename from install/root.go rename to cmd/install/root.go diff --git a/command.go b/command.go index eac9dde..b658af3 100644 --- a/command.go +++ b/command.go @@ -1,19 +1,40 @@ package complete +// 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 + + // 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 + + // 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 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 -type Command struct { - Name string - Sub Commands - Flags Flags - Args 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/option.go b/match.go similarity index 50% rename from option.go rename to match.go index 3915091..7593d65 100644 --- a/option.go +++ b/match.go @@ -5,28 +5,34 @@ import ( "strings" ) -type Option interface { +// 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 - Matches(prefix string) bool + Match(prefix string) bool } -type Arg string +// MatchPrefix is a simple Matcher, if the word is it's prefix, there is a match +type MatchPrefix string -func (a Arg) String() string { +func (a MatchPrefix) String() string { return string(a) } -func (a Arg) Matches(prefix string) bool { +func (a MatchPrefix) Match(prefix string) bool { return strings.HasPrefix(string(a), prefix) } -type ArgFileName string +// 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 ArgFileName) String() string { +func (a MatchFileName) String() string { return string(a) } -func (a ArgFileName) Matches(prefix string) bool { +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) diff --git a/predicate.go b/predicate.go index f975e27..a6746be 100644 --- a/predicate.go +++ b/predicate.go @@ -6,46 +6,71 @@ 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 { +// 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) []Matcher { + dir := dirFromLast(last) + files, err := filepath.Glob(filepath.Join(dir, pattern)) + if err != nil { + Log("failed glob operation with pattern '%s': %s", pattern, err) + } + if !filepath.IsAbs(pattern) { + filesToRel(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() { @@ -56,23 +81,11 @@ func dirsAt(path string) []Option { if !filepath.IsAbs(path) { filesToRel(dirs) } - return filesToOptions(dirs) -} - -func PredictFiles(pattern string) Predicate { - return func(last string) []Option { - dir := dirFromLast(last) - files, err := filepath.Glob(filepath.Join(dir, pattern)) - if err != nil { - Log("failed glob operation with pattern '%s': %s", pattern, err) - } - if !filepath.IsAbs(pattern) { - filesToRel(files) - } - return filesToOptions(files) - } + 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()) } }