From 703dd6ebc30f7c6f5a5c02e07a307e0e34d9c2c2 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Sat, 6 May 2017 22:06:49 +0300 Subject: [PATCH 1/2] 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()) } } From 404634e843081e7010260bd95006b84d6c40a8fd Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Sat, 6 May 2017 22:16:39 +0300 Subject: [PATCH 2/2] 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.