Repository: alecthomas/kong
Pull request #64 (2020-02-25 00:58:30)

add support for shell completion
This adds hooks for supporting shell completion but doesn't contain any actual shell completion code.  I don't believe there are any breaking changes in this PR and any changes to existing behavior will be a result of modifying `kong.CompletionOptions`

I haven't made any documentation changes yet.  I want to get some feedback on this before putting in that work.

`kong.CompletionOptions` / `kong.ConfigureCompletion`<sup>[1](#foot1)</sup> are the only API additions.  

### The options

#### Completer

Completer is where most of the action is.  This is the function that actually provides the completions.  If you want to implement it with for a basic case, you could set it to `kongplete.Complete` <sup>[2](#foot2)</sup>. For custom options it would be something like :

```golang
func(ctx *kong.Context) (bool, error) {
	return kongplete.Complete(ctx,
		kongplete.WithPredictor("mypredictor", complete.PredictSet("foo", "bar"), 
	)
}
```

The completer is responsible for determining whether this is a completion run or not.  Normally that will be by checking for the presence of the environment variable `COMP_LINE`.  I didn't want to move that into kong because that would make kong responsible for any differences between shells.

The completer is also responsible for writing the completion options to stdout.  This should be done by writing to `ctx.Stdout`. Another option would have been to make the Complete interface return a list of options for kong to write. I don't have a stong opinion on this one. I slightly prefer leaving it up to the completer because that allows more flexibility.

#### CompletionInstaller

CompletionInstaller provides the functions to install completion to the shell (edit .bash_profile, zsh or whatnot).  The functions are pretty straight forward.  They should do whatever configuration is necessary and return nil, or an error depending on success.  They are implemented similar to help, so the program is terminated immediately after they run.  By default their flags are `--install-shell-completion` and `--uninstall-shell-completion`

#### All the rest

The rest of the options control the appearance of the install and uninstall flags in help.  They are self-explanatory (I hope)

### Finally

As mentioned above, this is a bare-bones PR.  It could be merged as-is, with documentation referring to kongplete for implementation.  Another option would be to move kongplete into the kong repo.  A third option would be providing a Completer in the kong repo that pulls in kongplete.  I think the best usability is the middle option (moving kongplete to the kong repo).  However that would require importing `github.com/posener/complete`.  I see that kong currently has only one external import, and I suspect that is an intentional decision.

I'm looking forward to feedback.

---

<a name="foot1">1</a>: I'm not sure how `kong.ConfigureCompletion` adds value.  I was copying from `kong.HelpOptions` and added the equivalent to `kong.ConfigureHelp` for completeness. 

<a name="foot2">2</a>: I haven't actually updated kongplete to match this interface yet, so these examples are aspirational.


alecthomas: > I think the best usability is the middle option (moving kongplete to the kong repo).

Yes, agreed, I think this is the best option. I'd prefer to have comprehensive completion included in Kong itself without users having to think about it too much.

>  However that would require importing github.com/posener/complete. I see that kong currently has only one external import, and I suspect that is an intentional decision.

This is not specifically intentional, it just happened to be the case :)
WillAbides: Thanks for the feedback.  I will continue by making the configuration changes you requested and moving kongplete into this PR.

Once kongplete is part of kong, I think it makes sense to merge kongplete's Options into kong's. We can get rid of all of the kongplete options aside from `WithPredictors` (which should probably be renamed to `Predictors` to fit with kong's options). The rest of the options primarily existed to test kongplete separately from kong.

WillAbides: doh. I pushed 122ad6c9eb0d9a2910c3fec0bac8983a9744d10c too soon.  That won't work.  Correction coming.
WillAbides: As of d14caa7316e161ffeeb254c3b33a25aadda16c51, setting up `install-completion` and `uninstall-completion` will look like this:  

```golang
var myCli struct {
	DoIt   struct{} `kong:"cmd"`
	Config struct {
		InstallCompletion   kong.InstallCompletion   `kong:"cmd,help='Install shell completion.'"`
		UninstallCompletion kong.UninstallCompletion `kong:"cmd,help='Uninstall shell completion.'"`
	}
}
```

I like it better as a command than the flag that I had before because you can control where it shows up (in the case above, under a config sub-command).

WillAbides: The lions' share is done.  Documentation needs to be updates, and there's probably some cleanup to do.
WillAbides: @alecthomas I addressed your comments from the most recent review. Next I'm going to work on README.md to make sure this is something that can conveyed simply.

One thing I don't like about the interface is the use of the word "predictor". It works in `posener/complete` because the whole package is dealing with completions and it is obvious what would be predicted. In kong, the meaning is less obvious. "completion predictor" would put it in context, but that's getting a bit long.  Any suggestions are appreciated here.

Patch:
diff --git a/completion.go b/completion.go
new file mode 100644
index 0000000..3d62d94
--- /dev/null
+++ b/completion.go
@@ -0,0 +1,177 @@
+package kong
+
+import (
+	"github.com/posener/complete"
+	"github.com/posener/complete/cmd/install"
+)
+
+// CompletionOptions options for shell completion.
+type CompletionOptions struct {
+	// RunCompletion determines whether to respond to shell completion requests. default: false
+	// When RunCompletion is false, all other options in CompletionOptions are ignored.
+	RunCompletion bool
+
+	// Completer is the function to run for shell completions. If nil, a default completer based on github.com/posener/complete
+	// will be run.
+	Completer Completer
+
+	// CompletionInstaller determines the functions to run when running InstallCompletion and UninstallCompletion.
+	// By default, Install and Uninstall functions from github.com/posener/complete/cmd/install are used.
+	CompletionInstaller CompletionInstaller
+
+	// Predictors contains custom Predictors used to generate completion options.
+	// They can be used with an annotation like `predictor='myCustomPredictor'` where "myCustomPredictor" is a
+	// key in Predictors.
+	Predictors map[string]Predictor
+}
+
+// CompletionInstaller contains functions to install completions to a shell.
+type CompletionInstaller interface {
+	Install(ctx *Context) error
+	Uninstall(ctx *Context) error
+}
+
+// NewCompletionInstaller returns a CompletionInstaller with the given install and uninstall funcs
+// leaving either nil will cause it to defer to the default installer/uninstaller
+func NewCompletionInstaller(install, uninstall func(*Context) error) CompletionInstaller {
+	return &completionInstaller{
+		install:   install,
+		uninstall: uninstall,
+	}
+}
+
+// completionInstaller is an implementation of CompletionInstaller
+type completionInstaller struct {
+	install   func(ctx *Context) error
+	uninstall func(ctx *Context) error
+}
+
+func (c *completionInstaller) Install(ctx *Context) error {
+	if c.install != nil {
+		return c.install(ctx)
+	}
+	return defaultCompletionInstall(ctx)
+}
+
+func defaultCompletionInstall(ctx *Context) error {
+	return install.Install(ctx.Model.Name)
+}
+
+func (c *completionInstaller) Uninstall(ctx *Context) error {
+	if c.uninstall != nil {
+		return c.uninstall(ctx)
+	}
+	return defaultCompletionUninstall(ctx)
+}
+
+func defaultCompletionUninstall(ctx *Context) error {
+	return install.Uninstall(ctx.Model.Name)
+}
+
+// InstallCompletion will install completion to your shell
+type InstallCompletion struct{}
+
+//Run runs install
+func (c *InstallCompletion) Run(ctx *Context) error {
+	inst := ctx.Kong.completionOptions.CompletionInstaller
+	if inst == nil {
+		inst = NewCompletionInstaller(nil, nil)
+	}
+	return inst.Install(ctx)
+}
+
+// UninstallCompletion will uninstall completion from your shell (reverses InstallCompletion)
+type UninstallCompletion struct{}
+
+//Run runs uninstall
+func (c *UninstallCompletion) Run(ctx *Context) error {
+	inst := ctx.Kong.completionOptions.CompletionInstaller
+	if inst == nil {
+		inst = NewCompletionInstaller(nil, nil)
+	}
+	return inst.Uninstall(ctx)
+}
+
+// Completer is a function to run shell completions. Returns true if this was a completion run. Kong will exit 0
+// immediately when it returns true.
+type Completer func(*Context) (bool, error)
+
+// Apply options to Kong as a configuration option.
+func (c CompletionOptions) Apply(k *Kong) error {
+	k.completionOptions = c
+	return nil
+}
+
+func defaultCompleter(ctx *Context) (bool, error) {
+	cmd, err := nodeCompleteCommand(ctx.Model.Node, ctx.completionOptions.Predictors)
+	if err != nil {
+		return false, err
+	}
+	cmp := complete.New(ctx.Kong.Model.Name, *cmd)
+	cmp.Out = ctx.Kong.Stdout
+	return cmp.Complete(), nil
+}
+
+// nodeCompleteCommand recursively builds a *complete.Command for a node and all of its dear children.
+func nodeCompleteCommand(node *Node, predictors map[string]Predictor) (*complete.Command, error) {
+	if node == nil {
+		return nil, nil
+	}
+
+	cmd := complete.Command{
+		Sub:         complete.Commands{},
+		GlobalFlags: complete.Flags{},
+	}
+	for _, child := range node.Children {
+		if child == nil {
+			continue
+		}
+		childCmd, err := nodeCompleteCommand(child, predictors)
+		if err != nil {
+			return nil, err
+		}
+		if childCmd != nil {
+			cmd.Sub[child.Name] = *childCmd
+		}
+	}
+
+	var err error
+	cmd.GlobalFlags, err = nodeGlobalFlags(node, predictors)
+	if err != nil {
+		return nil, err
+	}
+
+	pps, err := positionalPredictors(node.Positional, predictors)
+	if err != nil {
+		return nil, err
+	}
+	cmd.Args = newCompletePredictor(&positionalPredictor{
+		Predictors: pps,
+		Flags:      node.Flags,
+	})
+
+	return &cmd, nil
+}
+
+func nodeGlobalFlags(node *Node, predictors map[string]Predictor) (map[string]complete.Predictor, error) {
+	if node == nil || node.Flags == nil {
+		return map[string]complete.Predictor{}, nil
+	}
+	globalFlags := make(map[string]complete.Predictor, len(node.Flags)*2)
+	for _, flag := range node.Flags {
+		if flag == nil {
+			continue
+		}
+		predictor, err := flagPredictor(flag, predictors)
+		if err != nil {
+			return nil, err
+		}
+		cp := newCompletePredictor(predictor)
+		globalFlags["--"+flag.Name] = cp
+		if flag.Short == 0 {
+			continue
+		}
+		globalFlags["-"+string(flag.Short)] = cp
+	}
+	return globalFlags, nil
+}
diff --git a/go.mod b/go.mod
index 8bbc12d..8c1af84 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/alecthomas/kong
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/pkg/errors v0.8.1
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/stretchr/testify v1.2.2
+	github.com/posener/complete v1.2.3
+	github.com/stretchr/testify v1.4.0
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index c935d68..bc0b797 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
+github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/kong.go b/kong.go
index 3a61567..997daee 100644
--- a/kong.go
+++ b/kong.go
@@ -55,6 +55,8 @@ type Kong struct {
 	helpFlag      *Flag
 	vars          Vars
 
+	completionOptions CompletionOptions
+
 	// Set temporarily by Options. These are applied after build().
 	postBuildOptions []Option
 }
@@ -196,6 +198,10 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
 	if err != nil {
 		return nil, err
 	}
+	err = k.runCompletion(ctx)
+	if err != nil {
+		return nil, err
+	}
 	if ctx.Error != nil {
 		return nil, &ParseError{error: ctx.Error, Context: ctx}
 	}
@@ -355,6 +361,25 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
 	return k.loader(r)
 }
 
+func (k *Kong) runCompletion(ctx *Context) error {
+	options := k.completionOptions
+	if !options.RunCompletion {
+		return nil
+	}
+	completer := options.Completer
+	if completer == nil {
+		completer = defaultCompleter
+	}
+	ran, err := completer(ctx)
+	if err != nil {
+		return err
+	}
+	if ran {
+		k.Exit(0)
+	}
+	return nil
+}
+
 func catch(err *error) {
 	msg := recover()
 	if test, ok := msg.(Error); ok {
diff --git a/options.go b/options.go
index 1464d71..17fddbf 100644
--- a/options.go
+++ b/options.go
@@ -202,6 +202,14 @@ func ConfigureHelp(options HelpOptions) Option {
 	})
 }
 
+// ConfigureCompletion sets options for shell completion.
+func ConfigureCompletion(options CompletionOptions) Option {
+	return OptionFunc(func(k *Kong) error {
+		k.completionOptions = options
+		return nil
+	})
+}
+
 // UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
 func UsageOnError() Option {
 	return OptionFunc(func(k *Kong) error {
diff --git a/predictors.go b/predictors.go
new file mode 100644
index 0000000..860535a
--- /dev/null
+++ b/predictors.go
@@ -0,0 +1,240 @@
+package kong
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/posener/complete"
+)
+
+// Predictor implements a predict method, in which given command line arguments returns a list of options it predicts.
+// Taken from github.com/posener/complete
+type Predictor interface {
+	Predict(PredictorArgs) []string
+}
+
+// PredictorArgs describes command line arguments used by a Predictor to predict options.
+// Taken from Args in github.com/posener/complete
+type PredictorArgs struct {
+	// All lists of all arguments in command line (not including the command itself)
+	All []string
+	// Completed lists of all completed arguments in command line,
+	// If the last one is still being typed - no space after it,
+	// it won't appear in this list of arguments.
+	Completed []string
+	// Last argument in command line, the one being typed, if the last
+	// character in the command line is a space, this argument will be empty,
+	// otherwise this would be the last word.
+	Last string
+	// LastCompleted is the last argument that was fully typed.
+	// If the last character in the command line is space, this would be the
+	// last word, otherwise, it would be the word before that.
+	LastCompleted string
+}
+
+// NewPredictor returns a Predictor that runs the provided function.
+func NewPredictor(fn func(PredictorArgs) []string) Predictor {
+	return predictorFunc(fn)
+}
+
+type predictorFunc func(PredictorArgs) []string
+
+func (p predictorFunc) Predict(args PredictorArgs) []string {
+	if p == nil {
+		return nil
+	}
+	return p(args)
+}
+
+// PredictNothing returns a nil Predictor that indicates no prediction is to be made.
+func PredictNothing() Predictor {
+	return nil
+}
+
+// PredictAnything returns a Predictor that expects something, but nothing particular, such as a number or arbitrary name.
+func PredictAnything() Predictor {
+	return NewPredictor(func(PredictorArgs) []string {
+		return nil
+	})
+}
+
+//PredictSet returns a Predictor that predicts provided options
+func PredictSet(options ...string) Predictor {
+	return predictorFunc(func(args PredictorArgs) []string {
+		return options
+	})
+}
+
+// newCompletePredictor returns a completePredictor or nil.
+// this is needed because nil predictor's have special meaning to complete.Complete
+func newCompletePredictor(predictor Predictor) complete.Predictor {
+	if predictor == nil {
+		return nil
+	}
+	return &completePredictor{
+		predictor: predictor,
+	}
+}
+
+// completePredictor wraps a Predictor to make it a complete.Predictor
+type completePredictor struct {
+	predictor Predictor
+}
+
+func (c *completePredictor) Predict(args complete.Args) []string {
+	return c.predictor.Predict(PredictorArgs{
+		All:           args.All,
+		Completed:     args.Completed,
+		Last:          args.Last,
+		LastCompleted: args.LastCompleted,
+	})
+}
+
+func tagPredictor(tag *Tag, predictors map[string]Predictor) (Predictor, error) {
+	if tag == nil || tag.Predictor == "" {
+		return nil, nil
+	}
+	if predictors == nil {
+		predictors = map[string]Predictor{}
+	}
+	predictorName := tag.Predictor
+	predictor, ok := predictors[predictorName]
+	if !ok {
+		return nil, fmt.Errorf("no predictor with name %q", predictorName)
+	}
+	return predictor, nil
+}
+
+func valuePredictor(value *Value, predictors map[string]Predictor) (Predictor, error) {
+	if value == nil {
+		return nil, nil
+	}
+	predictor, err := tagPredictor(value.Tag, predictors)
+	if err != nil {
+		return nil, err
+	}
+	if predictor != nil {
+		return predictor, nil
+	}
+	switch {
+	case value.IsBool():
+		return PredictNothing(), nil
+	case value.Enum != "":
+		enumVals := make([]string, 0, len(value.EnumMap()))
+		for enumVal := range value.EnumMap() {
+			enumVals = append(enumVals, enumVal)
+		}
+		return PredictSet(enumVals...), nil
+	default:
+		return PredictAnything(), nil
+	}
+}
+
+func positionalPredictors(args []*Positional, predictors map[string]Predictor) ([]Predictor, error) {
+	res := make([]Predictor, len(args))
+	var err error
+	for i, arg := range args {
+		res[i], err = valuePredictor(arg, predictors)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func flagPredictor(flag *Flag, predictors map[string]Predictor) (Predictor, error) {
+	return valuePredictor(flag.Value, predictors)
+}
+
+//positionalPredictor is a predictor for positional arguments
+type positionalPredictor struct {
+	Predictors []Predictor
+	Flags      []*Flag
+}
+
+//Predict implements complete.Predict
+func (p *positionalPredictor) Predict(args PredictorArgs) []string {
+	predictor := p.predictor(args)
+	if predictor == nil {
+		return []string{}
+	}
+	return predictor.Predict(args)
+}
+
+func (p *positionalPredictor) predictor(args PredictorArgs) Predictor {
+	position := p.predictorIndex(args)
+	if position < 0 || position > len(p.Predictors)-1 {
+		return nil
+	}
+	return p.Predictors[position]
+}
+
+//predictorIndex returns the index in predictors to use. Returns -1 if no predictor should be used.
+func (p *positionalPredictor) predictorIndex(args PredictorArgs) int {
+	idx := 0
+	for i := 0; i < len(args.Completed); i++ {
+		if !p.nonPredictorPos(args, i) {
+			idx++
+		}
+	}
+	return idx
+}
+
+//nonPredictorPos returns true if the value at this position is either a flag or a flag's argument
+func (p *positionalPredictor) nonPredictorPos(args PredictorArgs, pos int) bool {
+	if pos < 0 || pos > len(args.All)-1 {
+		return false
+	}
+	val := args.All[pos]
+	if p.valIsFlag(val) {
+		return true
+	}
+	if pos == 0 {
+		return false
+	}
+	prev := args.All[pos-1]
+	return p.nextValueIsFlagArg(prev)
+}
+
+//valIsFlag returns true if the value matches a flag from the configuration
+func (p *positionalPredictor) valIsFlag(val string) bool {
+	val = strings.Split(val, "=")[0]
+
+	for _, flag := range p.Flags {
+		if flag == nil {
+			continue
+		}
+		if val == "--"+flag.Name {
+			return true
+		}
+		if flag.Short == 0 {
+			continue
+		}
+		if strings.HasPrefix(val, "-"+string(flag.Short)) {
+			return true
+		}
+	}
+	return false
+}
+
+//nextValueIsFlagArg returns true if the value matches an ArgFlag and doesn't contain an equal sign.
+func (p *positionalPredictor) nextValueIsFlagArg(val string) bool {
+	if strings.Contains(val, "=") {
+		return false
+	}
+	for _, flag := range p.Flags {
+		if flag.IsBool() {
+			continue
+		}
+		if val == "--"+flag.Name {
+			return true
+		}
+		if flag.Short == 0 {
+			continue
+		}
+		if strings.HasPrefix(val, "-"+string(flag.Short)) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/tag.go b/tag.go
index 88380fe..8b5adef 100644
--- a/tag.go
+++ b/tag.go
@@ -32,6 +32,7 @@ type Tag struct {
 	Vars        Vars
 	Prefix      string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
 	Embed       bool
+	Predictor   string
 
 	// Storage for all tag keys for arbitrary lookups.
 	items map[string][]string
@@ -157,6 +158,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
 	t.Xor = t.Get("xor")
 	t.Prefix = t.Get("prefix")
 	t.Embed = t.Has("embed")
+	t.Predictor = t.Get("predictor")
 	if t.Sep == 0 {
 		if t.Get("sep") == "none" {
 			t.Sep = -1


Test patch:
diff --git a/completion_test.go b/completion_test.go
new file mode 100644
index 0000000..d278c7a
--- /dev/null
+++ b/completion_test.go
@@ -0,0 +1,390 @@
+package kong_test
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/alecthomas/kong"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func setLineAndPoint(t *testing.T, line string, point *int) func() {
+	pVal := len(line)
+	if point != nil {
+		pVal = *point
+	}
+	const (
+		envLine  = "COMP_LINE"
+		envPoint = "COMP_POINT"
+	)
+	t.Helper()
+	origLine, hasOrigLine := os.LookupEnv(envLine)
+	origPoint, hasOrigPoint := os.LookupEnv(envPoint)
+	require.NoError(t, os.Setenv(envLine, line))
+	require.NoError(t, os.Setenv(envPoint, strconv.Itoa(pVal)))
+	return func() {
+		t.Helper()
+		require.NoError(t, os.Unsetenv(envLine))
+		require.NoError(t, os.Unsetenv(envPoint))
+		if hasOrigLine {
+			require.NoError(t, os.Setenv(envLine, origLine))
+		}
+		if hasOrigPoint {
+			require.NoError(t, os.Setenv(envPoint, origPoint))
+		}
+	}
+}
+
+func TestComplete(t *testing.T) {
+	type embed struct {
+		Lion string
+	}
+
+	predictors := map[string]kong.Predictor{
+		"things":      kong.PredictSet("thing1", "thing2"),
+		"otherthings": kong.PredictSet("otherthing1", "otherthing2"),
+	}
+
+	var cli struct {
+		Foo struct {
+			Embedded embed  `kong:"embed"`
+			Bar      string `kong:"predictor=things"`
+			Baz      bool
+			Rabbit   struct {
+			} `kong:"cmd"`
+			Duck struct {
+			} `kong:"cmd"`
+		} `kong:"cmd"`
+		Bar struct {
+			Tiger   string `kong:"arg,predictor=things"`
+			Bear    string `kong:"arg,predictor=otherthings"`
+			OMG     string `kong:"enum='oh,my,gizzles'"`
+			Number  int    `kong:"short=n,enum='1,2,3'"`
+			BooFlag bool   `kong:"name=boofl,short=b"`
+		} `kong:"cmd"`
+	}
+
+	type completeTest struct {
+		want  []string
+		line  string
+		point *int
+	}
+
+	lenPtr := func(val string) *int {
+		v := len(val)
+		return &v
+	}
+
+	tests := []completeTest{
+		{
+			want: []string{"foo", "bar"},
+			line: "myApp ",
+		},
+		{
+			want: []string{"foo"},
+			line: "myApp foo",
+		},
+		{
+			want: []string{"rabbit", "duck"},
+			line: "myApp foo ",
+		},
+		{
+			want: []string{"rabbit"},
+			line: "myApp foo r",
+		},
+		{
+			want: []string{"--bar", "--baz", "--lion", "--help"},
+			line: "myApp foo -",
+		},
+		{
+			want: []string{},
+			line: "myApp foo --lion ",
+		},
+		{
+			want: []string{"rabbit", "duck"},
+			line: "myApp foo --baz ",
+		},
+		{
+			want: []string{"--bar", "--baz", "--lion", "--help"},
+			line: "myApp foo --baz -",
+		},
+		{
+			want: []string{"thing1", "thing2"},
+			line: "myApp foo --bar ",
+		},
+		{
+			want: []string{"thing1", "thing2"},
+			line: "myApp bar ",
+		},
+		{
+			want: []string{"thing1", "thing2"},
+			line: "myApp bar thing",
+		},
+		{
+			want: []string{"otherthing1", "otherthing2"},
+			line: "myApp bar thing1 ",
+		},
+		{
+			want: []string{"oh", "my", "gizzles"},
+			line: "myApp bar --omg ",
+		},
+		{
+			want: []string{"-n", "--number", "--omg", "--help", "--boofl", "-b"},
+			line: "myApp bar -",
+		},
+		{
+			want: []string{"thing1", "thing2"},
+			line: "myApp bar -b ",
+		},
+		{
+			want: []string{"-n", "--number", "--omg", "--help", "--boofl", "-b"},
+			line: "myApp bar -b thing1 -",
+		},
+		{
+			want: []string{"oh", "my", "gizzles"},
+			line: "myApp bar -b thing1 --omg ",
+		},
+		{
+			want: []string{"otherthing1", "otherthing2"},
+			line: "myApp bar -b thing1 --omg gizzles ",
+		},
+		{
+			want: []string{"otherthing1", "otherthing2"},
+			line: "myApp bar -b thing1 --omg gizzles ",
+		},
+		{
+			want: []string{"gizzles"},
+			line: "myApp bar -b thing1 --omg gi",
+		},
+		{
+			want:  []string{"thing1", "thing2"},
+			line:  "myApp bar -b thing1 --omg gi",
+			point: lenPtr("myApp bar -b th"),
+		},
+		{
+			want:  []string{"thing1", "thing2"},
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			point: lenPtr("myApp bar -b th"),
+		},
+		{
+			want:  []string{"thing1"},
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			point: lenPtr("myApp bar -b thing1"),
+		},
+		{
+			want:  []string{"otherthing1", "otherthing2"},
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			point: lenPtr("myApp bar -b thing1 "),
+		},
+	}
+
+	for _, td := range tests {
+		td := td
+		t.Run(td.line, func(t *testing.T) {
+			var stdOut, stdErr bytes.Buffer
+			var exited bool
+			p := mustNew(t, &cli,
+				kong.Writers(&stdOut, &stdErr),
+				kong.Exit(func(i int) {
+					exited = assert.Equal(t, 0, i)
+				}),
+				kong.CompletionOptions{
+					RunCompletion: true,
+					Predictors:    predictors,
+				},
+			)
+			cleanup := setLineAndPoint(t, td.line, td.point)
+			defer cleanup()
+			_, err := p.Parse([]string{})
+			require.Error(t, err)
+			require.IsType(t, &kong.ParseError{}, err)
+			require.True(t, exited)
+			require.Equal(t, "", stdErr.String())
+			gotLines := strings.Split(stdOut.String(), "\n")
+			gotOpts := []string{}
+			for _, l := range gotLines {
+				if l != "" {
+					gotOpts = append(gotOpts, l)
+				}
+			}
+			require.ElementsMatch(t, td.want, gotOpts)
+		})
+	}
+}
+
+func TestRunCompletion(t *testing.T) {
+	t.Run("completer returns false, nil", func(t *testing.T) {
+		var ran bool
+		w := &strings.Builder{}
+		p := mustNew(t, &struct{}{},
+			kong.Writers(w, w),
+			kong.CompletionOptions{
+				RunCompletion: true,
+				Completer: func(ctx *kong.Context) (b bool, err error) {
+					ran = true
+					return false, nil
+				},
+			},
+		)
+		_, err := p.Parse([]string{})
+		require.NoError(t, err)
+		require.True(t, ran)
+	})
+
+	t.Run("completer returns true, nil", func(t *testing.T) {
+		var ran, exited bool
+		output := "foo"
+		w := &strings.Builder{}
+		p := mustNew(t, &struct{}{},
+			kong.Writers(w, w),
+			kong.Exit(func(i int) {
+				exited = assert.Equal(t, 0, i)
+			}),
+			kong.CompletionOptions{
+				RunCompletion: true,
+				Completer: func(ctx *kong.Context) (bool, error) {
+					_, err := ctx.Stdout.Write([]byte(output))
+					require.NoError(t, err)
+					ran = true
+					return true, nil
+				},
+			},
+		)
+		_, err := p.Parse([]string{})
+		require.NoError(t, err)
+		require.True(t, ran)
+		require.True(t, exited)
+		require.Equal(t, output, w.String())
+	})
+
+	t.Run("completer returns true, error", func(t *testing.T) {
+		var ran bool
+		errVal := fmt.Errorf("boo")
+		w := &strings.Builder{}
+		p := mustNew(t, &struct{}{},
+			kong.Writers(w, w),
+			kong.CompletionOptions{
+				RunCompletion: true,
+				Completer: func(ctx *kong.Context) (b bool, err error) {
+					ran = true
+					return true, errVal
+				},
+			},
+		)
+		_, err := p.Parse([]string{})
+		require.EqualError(t, err, errVal.Error())
+		require.True(t, ran)
+	})
+
+	t.Run("RunCompletion = false", func(t *testing.T) {
+		w := &strings.Builder{}
+		p := mustNew(t, &struct{}{},
+			kong.Writers(w, w),
+			kong.CompletionOptions{
+				Completer: func(ctx *kong.Context) (b bool, err error) {
+					require.Fail(t, "completer should not be called")
+					return true, nil
+				},
+			},
+		)
+		_, err := p.Parse([]string{})
+		require.NoError(t, err)
+	})
+}
+
+// nolint: dupl
+func TestInstallCompletion(t *testing.T) {
+	t.Run("success", func(t *testing.T) {
+		var ran bool
+		var cli struct {
+			InstallCompletion kong.InstallCompletion `kong:"cmd"`
+		}
+		installer := kong.NewCompletionInstaller(func(ctx *kong.Context) error {
+			ran = true
+			return nil
+		}, nil)
+		p := mustNew(t, &cli,
+			kong.CompletionOptions{
+				CompletionInstaller: installer,
+			},
+		)
+		k, err := p.Parse([]string{"install-completion"})
+		require.NoError(t, err)
+		err = k.Run()
+		require.NoError(t, err)
+		require.True(t, ran)
+	})
+
+	t.Run("error", func(t *testing.T) {
+		var ran bool
+		errVal := fmt.Errorf("boo")
+		var cli struct {
+			InstallCompletion kong.InstallCompletion `kong:"cmd"`
+		}
+		installer := kong.NewCompletionInstaller(func(ctx *kong.Context) error {
+			ran = true
+			return errVal
+		}, nil)
+		p := mustNew(t, &cli,
+			kong.CompletionOptions{
+				CompletionInstaller: installer,
+			},
+		)
+		k, err := p.Parse([]string{"install-completion"})
+		require.NoError(t, err)
+		err = k.Run()
+		assert.EqualError(t, err, errVal.Error())
+		require.True(t, ran)
+	})
+}
+
+// nolint: dupl
+func TestUninstallCompletion(t *testing.T) {
+	t.Run("success", func(t *testing.T) {
+		var ran bool
+		var cli struct {
+			UninstallCompletion kong.UninstallCompletion `kong:"cmd"`
+		}
+		installer := kong.NewCompletionInstaller(nil, func(ctx *kong.Context) error {
+			ran = true
+			return nil
+		})
+		p := mustNew(t, &cli,
+			kong.CompletionOptions{
+				CompletionInstaller: installer,
+			},
+		)
+		k, err := p.Parse([]string{"uninstall-completion"})
+		require.NoError(t, err)
+		err = k.Run()
+		require.NoError(t, err)
+		require.True(t, ran)
+	})
+
+	t.Run("error", func(t *testing.T) {
+		var ran bool
+		errVal := fmt.Errorf("boo")
+		var cli struct {
+			UninstallCompletion kong.UninstallCompletion `kong:"cmd"`
+		}
+		installer := kong.NewCompletionInstaller(nil, func(ctx *kong.Context) error {
+			ran = true
+			return errVal
+		})
+		p := mustNew(t, &cli,
+			kong.CompletionOptions{
+				CompletionInstaller: installer,
+			},
+		)
+		k, err := p.Parse([]string{"uninstall-completion"})
+		require.NoError(t, err)
+		err = k.Run()
+		assert.EqualError(t, err, errVal.Error())
+		require.True(t, ran)
+	})
+}
diff --git a/predictors_test.go b/predictors_test.go
new file mode 100644
index 0000000..0f61480
--- /dev/null
+++ b/predictors_test.go
@@ -0,0 +1,143 @@
+package kong
+
+import (
+	"strings"
+	"testing"
+	"unicode"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPositionalPredictor_position(t *testing.T) {
+	posPredictor := &positionalPredictor{
+		Flags: []*Flag{
+			{
+				Value: &Value{
+					Name:   "mybool",
+					Mapper: boolMapper{},
+				},
+				Short: 'b',
+			},
+			{
+				Value: &Value{
+					Name:   "mybool2",
+					Mapper: boolMapper{},
+				},
+				Short: 'c',
+			},
+			{
+				Value: &Value{
+					Name: "myarg",
+				},
+				Short: 'a',
+			},
+		},
+	}
+
+	for args, want := range map[string]int{
+		``:                 0,
+		`foo`:              0,
+		`foo `:             1,
+		`-b foo `:          1,
+		`-bc foo `:         1,
+		`-bd foo `:         1,
+		`-a foo `:          0,
+		`-a=omg foo `:      1,
+		`--myarg omg foo `: 1,
+		`--myarg=omg foo `: 1,
+		`foo bar`:          1,
+		`foo bar `:         2,
+	} {
+		args := args
+		want := want
+		t.Run(args, func(t *testing.T) {
+			got := posPredictor.predictorIndex(newArgs("foo " + args))
+			assert.Equal(t, want, got)
+		})
+	}
+}
+
+func TestPositionalPredictor_Predict(t *testing.T) {
+	predictor1 := PredictSet("1")
+	predictor2 := PredictSet("2")
+	posPredictor := &positionalPredictor{
+		Predictors: []Predictor{predictor1, predictor2},
+	}
+
+	for args, want := range map[string][]string{
+		``:         {"1"},
+		`foo`:      {"1"},
+		`foo `:     {"2"},
+		`foo bar`:  {"2"},
+		`foo bar `: {},
+	} {
+		args := args
+		want := want
+		t.Run(args, func(t *testing.T) {
+			got := posPredictor.Predict(newArgs("app " + args))
+
+			assert.Equal(t, want, got)
+		})
+	}
+}
+
+// The code below is taken from https://github.com/posener/complete/blob/f6dd29e97e24f8cb51a8d4050781ce2b238776a4/args.go
+// to assist in tests.
+
+func newArgs(line string) PredictorArgs {
+	var (
+		all       []string
+		completed []string
+	)
+	parts := splitFields(line)
+	if len(parts) > 0 {
+		all = parts[1:]
+		completed = removeLast(parts[1:])
+	}
+	return PredictorArgs{
+		All:           all,
+		Completed:     completed,
+		Last:          last(parts),
+		LastCompleted: last(completed),
+	}
+}
+
+// splitFields returns a list of fields from the given command line.
+// If the last character is space, it appends an empty field in the end
+// indicating that the field before it was completed.
+// If the last field is of the form "a=b", it splits it to two fields: "a", "b",
+// So it can be completed.
+func splitFields(line string) []string {
+	parts := strings.Fields(line)
+
+	// Add empty field if the last field was completed.
+	if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) {
+		parts = append(parts, "")
+	}
+
+	// Treat the last field if it is of the form "a=b"
+	parts = splitLastEqual(parts)
+	return parts
+}
+
+func splitLastEqual(line []string) []string {
+	if len(line) == 0 {
+		return line
+	}
+	parts := strings.Split(line[len(line)-1], "=")
+	return append(line[:len(line)-1], parts...)
+}
+
+func removeLast(a []string) []string {
+	if len(a) > 0 {
+		return a[:len(a)-1]
+	}
+	return a
+}
+
+func last(args []string) string {
+	if len(args) == 0 {
+		return ""
+	}
+	return args[len(args)-1]
+}