Repository: alecthomas/kong
Pull request #67 (2020-03-12 23:11:29)

move completion into model
This moves completion into the kong model instead of building a separate model just for completion.



Patch:
diff --git a/completers.go b/completers.go
index 99549fe..8206511 100644
--- a/completers.go
+++ b/completers.go
@@ -1,216 +1,129 @@
 package kong
 
 import (
-	"fmt"
 	"strings"
-
-	"github.com/posener/complete"
+	"unicode"
 )
 
-// Completer implements a completion method, in which given command
-// line arguments returns a list of options it completes.
+// Completers contains custom Completers used to generate completion options.
 //
-// See https://github.com/posener/complete for details.
-type Completer = complete.Predictor
+// They can be used with an annotation like `completer='myCustomCompleter'` where "myCustomCompleter" is a
+// key in Completers.
+type Completers map[string]Completer
+
+// Apply completers to Kong as a configuration option.
+func (c Completers) Apply(k *Kong) error {
+	k.completers = c
+	return nil
+}
 
-// CompleterArgs describes command line arguments used by a Completer to predict options.
+// Completer implements a completion method, in which given command
+// line arguments returns a list of options it completes.
 //
 // See https://github.com/posener/complete for details.
-type CompleterArgs = complete.Args
-
-// A CompleterFunc is a function that implements the Completer interface.
-type CompleterFunc func(CompleterArgs) []string
-
-// Predict completion results.
-func (p CompleterFunc) Predict(args CompleterArgs) []string {
-	if p == nil {
-		return nil
-	}
-	return p(args)
+type Completer interface {
+	Options(CompleterArgs) []string
 }
 
-// CompleteNothing returns a nil Completer that indicates no prediction is to be made.
-func CompleteNothing() Completer { return complete.PredictNothing }
+// CompleterArgs describes command line arguments used by a Completer.
+type CompleterArgs []string
 
-// CompleteAnything returns a Completer that expects something, but nothing particular, such as a number or arbitrary name.
-func CompleteAnything() Completer { return complete.PredictAnything }
-
-// CompleteSet returns a Completer that predicts provided options
-func CompleteSet(options ...string) Completer { return complete.PredictSet(options...) }
-
-// CompleteOr unions two predicate functions, so that the result predicate returns the union of their predication
-func CompleteOr(completers ...Completer) Completer { return complete.PredictOr(completers...) }
-
-// CompleteDirs 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 CompleteDirs(pattern string) Completer { return complete.PredictDirs(pattern) }
-
-// CompleteFiles 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 CompleteFiles(pattern string) Completer { return complete.PredictFiles(pattern) }
-
-func tagCompleter(tag *Tag, completers map[string]Completer) (Completer, error) {
-	if tag == nil || tag.Completer == "" {
-		if tag != nil && tag.Type != "" {
-			switch tag.Type {
-			case "path":
-				return CompleteOr(CompleteFiles("*"), CompleteDirs("*")), nil
-
-			case "existingfile":
-				return CompleteFiles("*"), nil
-
-			case "existingdir":
-				return CompleteDirs("*"), nil
-			}
-		}
-		return nil, nil
-	}
-	if completers == nil {
-		completers = map[string]Completer{}
-	}
-	completerName := tag.Completer
-	completer, ok := completers[completerName]
-	if !ok {
-		return nil, fmt.Errorf("no completer with name %q", completerName)
+// All lists of all arguments in command line (not including the command itself)
+func (a CompleterArgs) All() []string {
+	if len(a) == 0 {
+		return []string{}
 	}
-	return completer, nil
+	return a[1:]
 }
 
-func valueCompleter(value *Value, completers map[string]Completer) (Completer, error) {
-	if value == nil {
-		return nil, nil
+// 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.
+func (a CompleterArgs) Completed() []string {
+	all := a.All()
+	if len(all) > 0 {
+		return all[:len(all)-1]
 	}
-	completer, err := tagCompleter(value.Tag, completers)
-	if err != nil {
-		return nil, err
-	}
-	if completer != nil {
-		return completer, nil
-	}
-	switch {
-	case value.IsBool():
-		return CompleteNothing(), nil
-
-	case value.Enum != "":
-		enumVals := make([]string, 0, len(value.EnumMap()))
-		for enumVal := range value.EnumMap() {
-			enumVals = append(enumVals, enumVal)
-		}
-		return CompleteSet(enumVals...), nil
+	return []string{}
+}
 
-	default:
-		return CompleteAnything(), nil
+// 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.
+func (a CompleterArgs) Last() string {
+	if len(a) == 0 {
+		return ""
 	}
+	return a[len(a)-1]
 }
 
-func positionalCompleters(args []*Positional, completers map[string]Completer) ([]Completer, error) {
-	res := make([]Completer, len(args))
-	var err error
-	for i, arg := range args {
-		res[i], err = valueCompleter(arg, completers)
-		if err != nil {
-			return nil, err
-		}
+// 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.
+func (a CompleterArgs) LastCompleted() string {
+	comp := a.Completed()
+	if len(comp) > 0 {
+		return comp[len(comp)-1]
 	}
-	return res, nil
+	return ""
 }
 
-func flagCompleter(flag *Flag, completers map[string]Completer) (Completer, error) {
-	return valueCompleter(flag.Value, completers)
-}
+func newCompleterArgs(line string) CompleterArgs {
+	var args CompleterArgs = strings.Fields(line)
 
-// positionalCompleter is a completer for positional arguments
-type positionalCompleter struct {
-	Completers []Completer
-	Flags      []*Flag
-}
+	if len(args) == 0 {
+		return args
+	}
 
-// Predict implements complete.Predict
-func (p *positionalCompleter) Predict(args CompleterArgs) []string {
-	completer := p.completer(args)
-	if completer == nil {
-		return []string{}
+	// Add empty field if the last field was completed.
+	if unicode.IsSpace(rune(line[len(line)-1])) {
+		args = append(args, "")
 	}
-	return completer.Predict(args)
+
+	// if the last arg is in the form a=b, split it
+	lastArgParts := strings.Split(args.Last(), "=")
+	return append(args[:len(args)-1], lastArgParts...)
 }
 
-func (p *positionalCompleter) completer(args CompleterArgs) Completer {
-	position := p.completerIndex(args)
-	if position < 0 || position > len(p.Completers)-1 {
+// A CompleterFunc is a function that implements the Completer interface.
+type CompleterFunc func(CompleterArgs) []string
+
+// Options completion results.
+func (p CompleterFunc) Options(args CompleterArgs) []string {
+	if p == nil {
 		return nil
 	}
-	return p.Completers[position]
+	return p(args)
 }
 
-// completerIndex returns the index in completers to use. Returns -1 if no completer should be used.
-func (p *positionalCompleter) completerIndex(args CompleterArgs) int {
-	idx := 0
-	for i := 0; i < len(args.Completed); i++ {
-		if !p.nonCompleterPos(args, i) {
-			idx++
-		}
-	}
-	return idx
+// CompleteNothing returns a nil Completer that indicates no completion is to be made.
+func CompleteNothing() Completer {
+	return nil
 }
 
-// nonCompleterPos returns true if the value at this position is either a flag or a flag's argument
-func (p *positionalCompleter) nonCompleterPos(args CompleterArgs, 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
+// CompleteAnything returns a Completer that expects something, but nothing particular, such as a number or arbitrary name.
+func CompleteAnything() CompleterFunc {
+	return func(_ CompleterArgs) []string {
+		return nil
 	}
-	prev := args.All[pos-1]
-	return p.nextValueIsFlagArg(prev)
 }
 
-// valIsFlag returns true if the value matches a flag from the configuration
-func (p *positionalCompleter) 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
-		}
+// CompleteSet returns a Completer that returns the provided options
+func CompleteSet(options ...string) CompleterFunc {
+	return func(_ CompleterArgs) []string {
+		return options
 	}
-	return false
 }
 
-// nextValueIsFlagArg returns true if the value matches an ArgFlag and doesn't contain an equal sign.
-func (p *positionalCompleter) 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
+// CompleteOr unions two predicate functions, so that the result predicate returns the union of their predication
+func CompleteOr(completers ...Completer) Completer {
+	return CompleterFunc(func(args CompleterArgs) []string {
+		result := make([]string, 0, len(completers))
+		for _, c := range completers {
+			if c != nil {
+				result = append(result, c.Options(args)...)
+			}
 		}
-	}
-	return false
+		return result
+	})
 }
diff --git a/completion.go b/completion.go
deleted file mode 100644
index b3273f5..0000000
--- a/completion.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package kong
-
-import (
-	"github.com/posener/complete"
-)
-
-// Completers contains custom Completers used to generate completion options.
-//
-// They can be used with an annotation like `completer='myCustomCompleter'` where "myCustomCompleter" is a
-// key in Completers.
-type Completers map[string]Completer
-
-// Apply completers to Kong as a configuration option.
-func (c Completers) Apply(k *Kong) error {
-	k.completers = c
-	return nil
-}
-
-func defaultCompleter(ctx *Context) (bool, error) {
-	cmd, err := nodeCompleteCommand(ctx.Model.Node, ctx.completers)
-	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, completers map[string]Completer) (*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, completers)
-		if err != nil {
-			return nil, err
-		}
-		if childCmd != nil {
-			cmd.Sub[child.Name] = *childCmd
-		}
-	}
-
-	var err error
-	cmd.GlobalFlags, err = nodeGlobalFlags(node, completers)
-	if err != nil {
-		return nil, err
-	}
-
-	pps, err := positionalCompleters(node.Positional, completers)
-	if err != nil {
-		return nil, err
-	}
-	cmd.Args = &positionalCompleter{
-		Completers: pps,
-		Flags:      node.Flags,
-	}
-
-	return &cmd, nil
-}
-
-func nodeGlobalFlags(node *Node, completers map[string]Completer) (map[string]Completer, error) {
-	if node == nil || node.Flags == nil {
-		return map[string]Completer{}, nil
-	}
-	globalFlags := make(map[string]Completer, len(node.Flags)*2)
-	for _, flag := range node.Flags {
-		if flag == nil {
-			continue
-		}
-		completer, err := flagCompleter(flag, completers)
-		if err != nil {
-			return nil, err
-		}
-		globalFlags["--"+flag.Name] = completer
-		if flag.Short == 0 {
-			continue
-		}
-		globalFlags["-"+string(flag.Short)] = completer
-	}
-	return globalFlags, nil
-}
diff --git a/file_completer.go b/file_completer.go
new file mode 100644
index 0000000..0c2c6ad
--- /dev/null
+++ b/file_completer.go
@@ -0,0 +1,166 @@
+package kong
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+// CompleteFilesSet is like CompleteSet but for files. It applies formatPathOption() before matching.
+//  see formatPathOption() for more details.
+func CompleteFilesSet(files []string) CompleterFunc {
+	return func(args CompleterArgs) []string {
+		return filterFilesByPrefix(args.Last(), files)
+	}
+}
+
+// CompleteDirs 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 CompleteDirs() CompleterFunc {
+	return func(args CompleterArgs) []string {
+		return recurseDirectoryMatches(args.Last(), 1, subdirOptions)
+	}
+}
+
+// CompleteFiles 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 CompleteFiles(pattern string) CompleterFunc {
+	return func(args CompleterArgs) []string {
+		return recurseDirectoryMatches(args.Last(), -1, func(prefix string) []string {
+			//return fileOptions(prefix, pattern)
+			if strings.HasSuffix(prefix, "/..") {
+				return nil
+			}
+			dir := matcherBaseDirectory(prefix)
+			results := subdirOptions(prefix)
+
+			files, err := filepath.Glob(filepath.Join(dir, pattern))
+			if err != nil {
+				return results
+			}
+
+			for _, file := range files {
+				if !isExistingDir(file) {
+					results = append(results, file)
+				}
+			}
+			return filterFilesByPrefix(prefix, results)
+		})
+	}
+}
+
+// recurseDirectoryMatches runs fn() on its output as long as the output is a single directory and it hasn't reached
+//  maxDepth. When maxDepth is negative, there is no recursion limit.
+func recurseDirectoryMatches(prefix string, maxDepth int, fn func(string) []string) []string {
+	var options []string
+	for {
+		options = fn(prefix)
+		if len(options) != 1 || !isExistingDir(options[0]) || options[0] == prefix || maxDepth == 0 {
+			break
+		}
+		maxDepth--
+		prefix = options[0]
+	}
+	return options
+}
+
+// isExistingDir returns true if path points to a directory that exists.
+// always returns false when os.Stat returns an error
+func isExistingDir(path string) bool {
+	info, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+	return info.IsDir()
+}
+
+// matcherBaseDirectory returns the directory for file matchers to use for a given prefix.
+//  If the prefix or its parent is a directory, that is returned.  Otherwise it returns "./"
+func matcherBaseDirectory(prefix string) string {
+	switch {
+	case isExistingDir(prefix):
+		return formatPathOption(prefix, prefix)
+	case isExistingDir(filepath.Dir(prefix)):
+		return formatPathOption(prefix, filepath.Dir(prefix))
+	default:
+		return filepath.FromSlash("./")
+	}
+}
+
+// subdirOptions returns the directories that match prefix
+func subdirOptions(prefix string) (subDirs []string) {
+	dir := matcherBaseDirectory(prefix)
+	contents, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return nil
+	}
+	subDirs = make([]string, 0, len(contents))
+	for _, info := range contents {
+		if info.IsDir() {
+			subDirs = append(subDirs, filepath.Join(dir, info.Name()))
+		}
+	}
+	return filterFilesByPrefix(prefix, append(subDirs, dir))
+}
+
+// filterFilesByPrefix returns the members of files that with the given prefix
+func filterFilesByPrefix(prefix string, files []string) []string {
+	const dotSlash = "." + string(filepath.Separator)
+	options := make([]string, 0, len(files))
+	for _, file := range files {
+		file = formatPathOption(prefix, file)
+
+		// for matching purposes, "." is equivalent to "./"
+		matchPrefix := prefix
+		if matchPrefix == "." {
+			matchPrefix = dotSlash
+		}
+
+		// strip "./" from the front of both strings before doing the prefix match
+		matchPrefix = strings.TrimPrefix(matchPrefix, dotSlash)
+		if strings.HasPrefix(strings.TrimPrefix(file, dotSlash), matchPrefix) {
+			options = append(options, file)
+		}
+	}
+	return options
+}
+
+// formatPathOption returns path in the form needed for a path completion
+//  - when base is an absolute path, the returned value is an absolute path
+//  - when base starts with ".", the returned value starts with "./"
+//  - when base is a relative path, the retuned value is a relative path to the current working directory (not base)
+//  - when path points to a directory, the returned value ends with "/"
+func formatPathOption(base string, path string) string {
+	// if base is absolute, return path as absolute
+	if filepath.IsAbs(base) {
+		abs, err := filepath.Abs(path)
+		if err != nil {
+			return path
+		}
+		return appendSeparatorToDirectory(abs)
+	}
+
+	rel, err := filepath.Rel("", path)
+	if err != nil {
+		return path
+	}
+
+	// the result should start with "./" when base starts with .
+	if rel != "." && strings.HasPrefix(base, ".") {
+		rel = filepath.FromSlash("./") + rel
+	}
+
+	return appendSeparatorToDirectory(rel)
+}
+
+// appendSeparatorToDirectory adds "/" to the end of path if it isn't already present and path points to a directory
+func appendSeparatorToDirectory(path string) string {
+	if strings.HasSuffix(path, string(filepath.Separator)) || !isExistingDir(path) {
+		return path
+	}
+	return path + string(filepath.Separator)
+}
diff --git a/go.mod b/go.mod
index 6a9ebba..77d740a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/alecthomas/kong
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/pkg/errors v0.8.1
-	github.com/posener/complete v1.2.3
 	github.com/riywo/loginshell v0.0.0-20190610082906-2ed199a032f6
 	github.com/stretchr/testify v1.4.0
 )
diff --git a/go.sum b/go.sum
index ddb840b..6252947 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,10 @@
 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/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
-github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/riywo/loginshell v0.0.0-20190610082906-2ed199a032f6 h1:0QWE8TiOGSB+korydW5z4hPQ5QBVqLos+M2ta4pHaY0=
 github.com/riywo/loginshell v0.0.0-20190610082906-2ed199a032f6/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/kong.go b/kong.go
index 41aa425..5fd682b 100644
--- a/kong.go
+++ b/kong.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"path/filepath"
 	"reflect"
+	"strconv"
 	"strings"
 )
 
@@ -362,13 +363,32 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
 }
 
 func (k *Kong) runCompletion(ctx *Context) error {
-	ran, err := defaultCompleter(ctx)
+	const envLine = "COMP_LINE"
+	const envPoint = "COMP_POINT"
+
+	line := os.Getenv(envLine)
+	if line == "" {
+		return nil
+	}
+	point, err := strconv.Atoi(os.Getenv(envPoint))
+	if err == nil && point < len(line) {
+		line = line[:point]
+	}
+	a := newCompleterArgs(line)
+	opts := &completionOptions{}
+	_, err = ctx.Model.Node.runCompletion(ctx, a, opts)
 	if err != nil {
 		return err
 	}
-	if ran {
-		k.Exit(0)
+	for _, option := range opts.slice() {
+		if strings.HasPrefix(option, a.Last()) {
+			_, err := fmt.Fprintln(k.Stdout, option)
+			if err != nil {
+				return err
+			}
+		}
 	}
+	k.Exit(0)
 	return nil
 }
 
diff --git a/model.go b/model.go
index 8023dce..1da78c4 100644
--- a/model.go
+++ b/model.go
@@ -5,6 +5,7 @@ import (
 	"math"
 	"os"
 	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -202,6 +203,181 @@ func (n *Node) Path() (out string) {
 	return strings.TrimSpace(out)
 }
 
+type completionOptions map[string]struct{}
+
+func (c completionOptions) add(option ...string) {
+	for _, opt := range option {
+		c[opt] = struct{}{}
+	}
+}
+
+func (c completionOptions) slice() []string {
+	options := make([]string, 0, len(c))
+	for s := range c {
+		options = append(options, s)
+	}
+	sort.Strings(options)
+	return options
+}
+
+func (n *Node) subCommandCompletion(ctx *Context, a CompleterArgs, opts *completionOptions) (found, stop bool, err error) {
+	var subCommand *Node
+	completedArgs := a.Completed()
+	for i := 0; i < len(completedArgs); i++ {
+		i := i
+		for _, sub := range n.Children {
+			if sub.Name == completedArgs[i] && sub.Type == CommandNode {
+				subCommand = sub
+				found = true
+				break
+			}
+		}
+		if !found {
+			continue
+		}
+		// when there's a subCommand, run the subCommand's completion with the remaining args
+		if i < len(a) {
+			a = a[i+1:]
+		} else {
+			a = a[len(a):]
+		}
+		stop, err = subCommand.runCompletion(ctx, a, opts)
+		if err != nil {
+			return false, false, err
+		}
+		return found, stop, nil
+	}
+	return found, stop, nil
+}
+
+func (n *Node) runCompletion(ctx *Context, a CompleterArgs, opts *completionOptions) (bool, error) {
+	foundSub, _, err := n.subCommandCompletion(ctx, a, opts)
+	if err != nil {
+		return false, err
+	}
+
+	if foundSub {
+		return false, nil
+	}
+
+	stop, err := n.flagCompletion(ctx, a, opts)
+	if err != nil || stop {
+		return stop, err
+	}
+
+	err = n.positionalCompletion(ctx, a, opts)
+	if err != nil {
+		return false, err
+	}
+
+	for _, child := range n.Children {
+		if child.Type == CommandNode {
+			opts.add(child.Name)
+		}
+	}
+
+	return false, nil
+}
+
+func (n *Node) positionalCompletion(ctx *Context, a CompleterArgs, opts *completionOptions) error {
+	lookupFlag := func(str string) *Flag {
+		if !strings.HasPrefix(str, "-") {
+			return nil
+		}
+		for _, flags := range n.AllFlags(true) {
+			for _, flag := range flags {
+				if flag == nil {
+					continue
+				}
+				if str == "--"+flag.Name {
+					return flag
+				}
+				if flag.Short == 0 {
+					continue
+				}
+				if strings.HasPrefix(str, "-"+string(flag.Short)) {
+					return flag
+				}
+			}
+		}
+		return nil
+	}
+
+	var position int
+	for i := 0; i < len(a.Completed()); i++ {
+		allArgs := a.All()
+		if i > len(allArgs)-1 {
+			position++
+			continue
+		}
+		val := allArgs[i]
+		if lookupFlag(val) != nil {
+			continue
+		}
+		if i == 0 {
+			position++
+			continue
+		}
+		prev := allArgs[i-1]
+		flag := lookupFlag(prev)
+		if flag != nil && !flag.IsBool() {
+			continue
+		}
+		position++
+	}
+
+	var completer Completer
+	var err error
+	if position < len(n.Positional) {
+		completer, err = n.Positional[position].completer(ctx)
+	} else if len(n.Positional) > 0 && n.Positional[len(n.Positional)-1].IsCumulative() {
+		completer, err = n.Positional[len(n.Positional)-1].completer(ctx)
+	}
+	if err != nil || completer == nil {
+		return err
+	}
+
+	opts.add(completer.Options(a)...)
+	return nil
+}
+
+func (n *Node) flagCompletion(ctx *Context, a CompleterArgs, opts *completionOptions) (bool, error) {
+	if n.Flags == nil {
+		return false, nil
+	}
+	completers := make(map[string]Completer, len(n.Flags)*2)
+	for _, flags := range n.AllFlags(true) {
+		for _, flag := range flags {
+			if flag == nil {
+				continue
+			}
+			completer, err := flag.completer(ctx)
+			if err != nil {
+				return false, err
+			}
+			completers["--"+flag.Name] = completer
+			if flag.Short == 0 {
+				continue
+			}
+			completers["-"+string(flag.Short)] = completer
+		}
+	}
+
+	completer, ok := completers[a.LastCompleted()]
+	if ok && completer != nil {
+		opts.add(completer.Options(a)...)
+		// stop further completions when a flag is being completed
+		return true, nil
+	}
+
+	if strings.HasPrefix(a.Last(), "-") {
+		for flag := range completers {
+			opts.add(flag)
+		}
+	}
+	return false, nil
+}
+
 // A Value is either a flag or a variable positional argument.
 type Value struct {
 	Flag         *Flag // Nil if positional argument.
@@ -344,6 +520,33 @@ func (v *Value) Reset() error {
 
 func (*Value) node() {}
 
+func (v *Value) completer(ctx *Context) (Completer, error) {
+	if v == nil {
+		return nil, nil
+	}
+	completer, err := v.Tag.completer(ctx)
+	if err != nil {
+		return nil, err
+	}
+	if completer != nil {
+		return completer, nil
+	}
+	switch {
+	case v.IsBool():
+		return CompleteNothing(), nil
+
+	case v.Enum != "":
+		enumVals := make([]string, 0, len(v.EnumMap()))
+		for enumVal := range v.EnumMap() {
+			enumVals = append(enumVals, enumVal)
+		}
+		return CompleteSet(enumVals...), nil
+
+	default:
+		return CompleteAnything(), nil
+	}
+}
+
 // A Positional represents a non-branching command-line positional argument.
 type Positional = Value
 
diff --git a/tag.go b/tag.go
index b365d35..8fa2c9f 100644
--- a/tag.go
+++ b/tag.go
@@ -234,3 +234,31 @@ func (t *Tag) GetRune(k string) (rune, error) {
 	}
 	return r, nil
 }
+
+func (t *Tag) completer(ctx *Context) (Completer, error) {
+	if t == nil || t.Completer == "" {
+		if t != nil && t.Type != "" {
+			switch t.Type {
+			case "path":
+				return CompleteOr(CompleteFiles("*"), CompleteDirs()), nil
+
+			case "existingfile":
+				return CompleteFiles("*"), nil
+
+			case "existingdir":
+				return CompleteDirs(), nil
+			}
+		}
+		return nil, nil
+	}
+	completers := ctx.completers
+	if completers == nil {
+		completers = map[string]Completer{}
+	}
+	completerName := t.Completer
+	completer, ok := completers[completerName]
+	if !ok {
+		return nil, fmt.Errorf("no completer with name %q", completerName)
+	}
+	return completer, nil
+}


Test patch:
diff --git a/completers_test.go b/completers_test.go
index fd5e953..ce3ef0f 100644
--- a/completers_test.go
+++ b/completers_test.go
@@ -1,143 +1,257 @@
 package kong
 
 import (
+	"bytes"
+	"os"
+	"sort"
+	"strconv"
 	"strings"
 	"testing"
-	"unicode"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func TestPositionalCompleter_position(t *testing.T) {
-	posCompleter := &positionalCompleter{
-		Flags: []*Flag{
-			{
-				Value: &Value{
-					Name:   "mybool",
-					Mapper: boolMapper{},
-				},
-				Short: 'b',
-			},
-			{
-				Value: &Value{
-					Name:   "mybool2",
-					Mapper: boolMapper{},
-				},
-				Short: 'c',
-			},
-			{
-				Value: &Value{
-					Name: "myarg",
-				},
-				Short: 'a',
-			},
-		},
+func setLineAndPoint(t *testing.T, line string, point *int) func() {
+	pVal := len(line)
+	if point != nil {
+		pVal = *point
 	}
-
-	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 := posCompleter.completerIndex(newArgs("foo " + args))
-			assert.Equal(t, want, got)
-		})
+	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 TestPositionalCompleter_Predict(t *testing.T) {
-	completer1 := CompleteSet("1")
-	completer2 := CompleteSet("2")
-	posCompleter := &positionalCompleter{
-		Completers: []Completer{completer1, completer2},
+func Test_runCompletion(t *testing.T) {
+	type embed struct {
+		Lion string
 	}
 
-	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 := posCompleter.Predict(newArgs("app " + args))
-
-			assert.Equal(t, want, got)
-		})
+	completers := Completers{
+		"things":      CompleteSet("thing1", "thing2"),
+		"otherthings": CompleteSet("otherthing1", "otherthing2"),
 	}
-}
 
-// The code below is taken from https://github.com/posener/complete/blob/f6dd29e97e24f8cb51a8d4050781ce2b238776a4/args.go
-// to assist in tests.
-
-func newArgs(line string) CompleterArgs {
-	var (
-		all       []string
-		completed []string
-	)
-	parts := splitFields(line)
-	if len(parts) > 0 {
-		all = parts[1:]
-		completed = removeLast(parts[1:])
-	}
-	return CompleterArgs{
-		All:           all,
-		Completed:     completed,
-		Last:          last(parts),
-		LastCompleted: last(completed),
+	var cli struct {
+		Foo struct {
+			Embedded embed  `kong:"embed"`
+			Bar      string `kong:"completer=things"`
+			Baz      bool
+			Rabbit   struct {
+			} `kong:"cmd"`
+			Duck struct {
+			} `kong:"cmd"`
+		} `kong:"cmd"`
+		Bar struct {
+			Tiger   string `kong:"arg,completer=things"`
+			Bear    string `kong:"arg,completer=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"`
+		Baz struct {
+			RepeatingArg  []string `kong:"arg,completer=things"`
+			RepeatingFlag []int    `kong:"short=r,enum='11,12,13,20'"`
+		} `kong:"cmd"`
 	}
-}
-
-// 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, "")
+	type completeTest struct {
+		want  []string
+		line  string
+		point *int
 	}
 
-	// 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
+	lenPtr := func(val string) *int {
+		v := len(val)
+		return &v
 	}
-	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]
+	tests := []completeTest{
+		{
+			line: "myApp ",
+			want: []string{"bar", "baz", "foo"},
+		},
+		{
+			line: "myApp baz -",
+			want: []string{"--help", "--repeating-flag", "-r"},
+		},
+		{
+			line: "myApp baz -r",
+			want: []string{"-r"},
+		},
+		{
+			line: "myApp baz -r ",
+			want: []string{"11", "12", "13", "20"},
+		},
+		{
+			line: "myApp baz -r 11 -r 1",
+			want: []string{"11", "12", "13"},
+		},
+		{
+			line: "myApp baz -r 11 -r 12 ",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp baz thing1 thing2 -r 11 -r 12 -r ",
+			want: []string{"11", "12", "13", "20"},
+		},
+		{
+			line: "myApp baz thing1 ",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp foo",
+			want: []string{"foo"},
+		},
+		{
+			line: "myApp foo ",
+			want: []string{"duck", "rabbit"},
+		},
+		{
+			line: "myApp foo r",
+			want: []string{"rabbit"},
+		},
+		{
+			line: "myApp -",
+			want: []string{"--help"},
+		},
+		{
+			line: "myApp foo -",
+			want: []string{"--bar", "--baz", "--help", "--lion"},
+		},
+		{
+			line: "myApp foo --lion ",
+			want: []string{},
+		},
+		{
+			line: "myApp foo --baz ",
+			want: []string{"duck", "rabbit"},
+		},
+		{
+			line: "myApp foo --baz -",
+			want: []string{"--bar", "--baz", "--help", "--lion"},
+		},
+		{
+			line: "myApp foo --bar ",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp bar ",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp bar thing",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp bar thing1 ",
+			want: []string{"otherthing1", "otherthing2"},
+		},
+		{
+			line: "myApp bar --omg ",
+			want: []string{"gizzles", "my", "oh"},
+		},
+		{
+			line: "myApp bar -",
+			want: []string{"--boofl", "--help", "--number", "--omg", "-b", "-n"},
+		},
+		{
+			line: "myApp bar -b ",
+			want: []string{"thing1", "thing2"},
+		},
+		{
+			line: "myApp bar -b thing1 -",
+			want: []string{"--boofl", "--help", "--number", "--omg", "-b", "-n"},
+		},
+		{
+			line: "myApp bar -b thing1 --omg ",
+			want: []string{"gizzles", "my", "oh"},
+		},
+		{
+			line: "myApp bar -b thing1 --omg gizzles ",
+			want: []string{"otherthing1", "otherthing2"},
+		},
+		{
+			line: "myApp bar -b thing1 --omg gi",
+			want: []string{"gizzles"},
+		},
+		{
+			line:  "myApp bar -b thing1 --omg gi",
+			want:  []string{"thing1", "thing2"},
+			point: lenPtr("myApp bar -b th"),
+		},
+		{
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			want:  []string{"thing1", "thing2"},
+			point: lenPtr("myApp bar -b th"),
+		},
+		{
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			want:  []string{"thing1"},
+			point: lenPtr("myApp bar -b thing1"),
+		},
+		{
+			line:  "myApp bar -b thing1 --omg gizzles ",
+			want:  []string{"otherthing1", "otherthing2"},
+			point: lenPtr("myApp bar -b thing1 "),
+		},
+		{
+			line: "myApp bar --number ",
+			want: []string{"1", "2", "3"},
+		},
+		{
+			line: "myApp bar --number=",
+			want: []string{"1", "2", "3"},
+		},
 	}
-	return a
-}
 
-func last(args []string) string {
-	if len(args) == 0 {
-		return ""
+	for _, td := range tests {
+		td := td
+		t.Run(td.line, func(t *testing.T) {
+			var stdOut, stdErr bytes.Buffer
+			var exited bool
+			p, err := New(&cli,
+				Writers(&stdOut, &stdErr),
+				Exit(func(i int) {
+					exited = assert.Equal(t, 0, i)
+				}),
+				Name("test"),
+				completers,
+			)
+			require.NoError(t, err)
+			cleanup := setLineAndPoint(t, td.line, td.point)
+			defer cleanup()
+			_, err = p.Parse([]string{})
+			require.Error(t, err)
+			require.IsType(t, &ParseError{}, err)
+			require.True(t, exited)
+			require.Equal(t, "", stdErr.String())
+			gotLines := strings.Split(stdOut.String(), "\n")
+			sort.Strings(gotLines)
+			gotOpts := []string{}
+			for _, l := range gotLines {
+				if l != "" {
+					gotOpts = append(gotOpts, l)
+				}
+			}
+			require.Equal(t, td.want, gotOpts)
+		})
 	}
-	return args[len(args)-1]
 }
diff --git a/completion_test.go b/completion_test.go
deleted file mode 100644
index fbfc7b9..0000000
--- a/completion_test.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package kong_test
-
-import (
-	"bytes"
-	"os"
-	"sort"
-	"strconv"
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"github.com/alecthomas/kong"
-)
-
-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
-	}
-
-	completers := kong.Completers{
-		"things":      kong.CompleteSet("thing1", "thing2"),
-		"otherthings": kong.CompleteSet("otherthing1", "otherthing2"),
-	}
-
-	var cli struct {
-		Foo struct {
-			Embedded embed  `kong:"embed"`
-			Bar      string `kong:"completer=things"`
-			Baz      bool
-			Rabbit   struct {
-			} `kong:"cmd"`
-			Duck struct {
-			} `kong:"cmd"`
-		} `kong:"cmd"`
-		Bar struct {
-			Tiger   string `kong:"arg,completer=things"`
-			Bear    string `kong:"arg,completer=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{
-		{
-			line: "myApp ",
-			want: []string{"bar", "foo"},
-		},
-		{
-			line: "myApp foo",
-			want: []string{"foo"},
-		},
-		{
-			line: "myApp foo ",
-			want: []string{"duck", "rabbit"},
-		},
-		{
-			line: "myApp foo r",
-			want: []string{"rabbit"},
-		},
-		{
-			line: "myApp foo -",
-			want: []string{"--bar", "--baz", "--help", "--lion"},
-		},
-		{
-			line: "myApp foo --lion ",
-			want: []string{},
-		},
-		{
-			line: "myApp foo --baz ",
-			want: []string{"duck", "rabbit"},
-		},
-		{
-			line: "myApp foo --baz -",
-			want: []string{"--bar", "--baz", "--help", "--lion"},
-		},
-		{
-			line: "myApp foo --bar ",
-			want: []string{"thing1", "thing2"},
-		},
-		{
-			line: "myApp bar ",
-			want: []string{"thing1", "thing2"},
-		},
-		{
-			line: "myApp bar thing",
-			want: []string{"thing1", "thing2"},
-		},
-		{
-			line: "myApp bar thing1 ",
-			want: []string{"otherthing1", "otherthing2"},
-		},
-		{
-			line: "myApp bar --omg ",
-			want: []string{"gizzles", "my", "oh"},
-		},
-		{
-			line: "myApp bar -",
-			want: []string{"--boofl", "--help", "--number", "--omg", "-b", "-n"},
-		},
-		{
-			line: "myApp bar -b ",
-			want: []string{"thing1", "thing2"},
-		},
-		{
-			line: "myApp bar -b thing1 -",
-			want: []string{"--boofl", "--help", "--number", "--omg", "-b", "-n"},
-		},
-		{
-			line: "myApp bar -b thing1 --omg ",
-			want: []string{"gizzles", "my", "oh"},
-		},
-		{
-			line: "myApp bar -b thing1 --omg gizzles ",
-			want: []string{"otherthing1", "otherthing2"},
-		},
-		{
-			line: "myApp bar -b thing1 --omg gizzles ",
-			want: []string{"otherthing1", "otherthing2"},
-		},
-		{
-			line: "myApp bar -b thing1 --omg gi",
-			want: []string{"gizzles"},
-		},
-		{
-			line:  "myApp bar -b thing1 --omg gi",
-			want:  []string{"thing1", "thing2"},
-			point: lenPtr("myApp bar -b th"),
-		},
-		{
-			line:  "myApp bar -b thing1 --omg gizzles ",
-			want:  []string{"thing1", "thing2"},
-			point: lenPtr("myApp bar -b th"),
-		},
-		{
-			line:  "myApp bar -b thing1 --omg gizzles ",
-			want:  []string{"thing1"},
-			point: lenPtr("myApp bar -b thing1"),
-		},
-		{
-			line:  "myApp bar -b thing1 --omg gizzles ",
-			want:  []string{"otherthing1", "otherthing2"},
-			point: lenPtr("myApp bar -b thing1 "),
-		},
-		{
-			line: "myApp bar --number ",
-			want: []string{"1", "2", "3"},
-		},
-		{
-			line: "myApp bar --number=",
-			want: []string{"1", "2", "3"},
-		},
-	}
-
-	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)
-				}),
-				completers,
-			)
-			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")
-			sort.Strings(gotLines)
-			gotOpts := []string{}
-			for _, l := range gotLines {
-				if l != "" {
-					gotOpts = append(gotOpts, l)
-				}
-			}
-			require.Equal(t, td.want, gotOpts)
-		})
-	}
-}
diff --git a/file_completer_test.go b/file_completer_test.go
new file mode 100644
index 0000000..6960bce
--- /dev/null
+++ b/file_completer_test.go
@@ -0,0 +1,258 @@
+package kong
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"sort"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func setupTestFilesDir(t *testing.T) (teardown func()) {
+	t.Helper()
+	var err error
+	tmpDir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	files := []string{
+		"dir/foo",
+		"dir/bar",
+		"outer/inner/readme.md",
+		".dot.txt",
+		"a.txt",
+		"b.txt",
+		"c.txt",
+		"readme.md",
+	}
+	for _, file := range files {
+		file = filepath.Join(tmpDir, filepath.FromSlash(file))
+		require.NoError(t, os.MkdirAll(filepath.Dir(file), 0700))
+		require.NoError(t, ioutil.WriteFile(file, nil, 0600))
+	}
+	wd, err := os.Getwd()
+	require.NoError(t, err)
+	require.NoError(t, os.Chdir(tmpDir))
+	return func() {
+		require.NoError(t, os.Chdir(wd))
+		require.NoError(t, os.RemoveAll(tmpDir))
+	}
+}
+
+func Test_formatPathOption(t *testing.T) {
+	teardown := setupTestFilesDir(t)
+	defer teardown()
+
+	wd, err := os.Getwd()
+	require.NoError(t, err)
+
+	for _, td := range []struct {
+		base string
+		path string
+		want string
+	}{
+		{
+			base: ".",
+			path: "dir",
+			want: "./dir/",
+		},
+		{
+			base: "",
+			path: "dir",
+			want: "dir/",
+		},
+		{
+			base: "read",
+			path: "readme.md",
+			want: "readme.md",
+		},
+		{
+			base: "/",
+			path: "readme.md",
+			want: filepath.Join(wd, "readme.md"),
+		},
+		{
+			base: wd,
+			path: "readme.md",
+			want: filepath.Join(wd, "readme.md"),
+		},
+		{
+			base: "/",
+			path: "dir",
+			want: filepath.Join(wd, "dir") + string(filepath.Separator),
+		},
+	} {
+		td := td
+		t.Run(fmt.Sprintf("(%q, %q)", td.base, td.path), func(t *testing.T) {
+			got := formatPathOption(td.base, td.path)
+			require.Equal(t, td.want, got)
+		})
+	}
+}
+
+func TestCompleteFilesSet(t *testing.T) {
+	set := CompleteFilesSet([]string{
+		"foo/bar", "baz", "foo/qux", ".", "./one", "./flood",
+	})
+
+	for _, td := range []struct {
+		input string
+		want  []string
+	}{
+		{
+			input: "fl",
+			want:  []string{"flood"},
+		},
+		{
+			input: "f",
+			want:  []string{"flood", "foo/bar", "foo/qux"},
+		},
+		{
+			input: "./f",
+			want:  []string{"./flood", "./foo/bar", "./foo/qux"},
+		},
+		{
+			input: "foo/",
+			want:  []string{"foo/bar", "foo/qux"},
+		},
+		{
+			input: "./foo/",
+			want:  []string{"./foo/bar", "./foo/qux"},
+		},
+		{
+			input: "",
+			want:  []string{"./", "baz", "flood", "foo/bar", "foo/qux", "one"},
+		},
+		{
+			input: ".",
+			want:  []string{"./", "./baz", "./flood", "./foo/bar", "./foo/qux", "./one"},
+		},
+		{
+			input: "./",
+			want:  []string{"./", "./baz", "./flood", "./foo/bar", "./foo/qux", "./one"},
+		},
+		{
+			input: "./q",
+			want:  []string{},
+		},
+		{
+			input: "q",
+			want:  []string{},
+		},
+		{
+			input: "foo/q",
+			want:  []string{"foo/qux"},
+		},
+		{
+			input: "./foo/q",
+			want:  []string{"./foo/qux"},
+		},
+	} {
+		td := td
+		t.Run(fmt.Sprintf("input:%q", td.input), func(t *testing.T) {
+			input := filepath.FromSlash(td.input)
+			got := set.Options(newCompleterArgs(input))
+			sort.Strings(got)
+			sort.Strings(td.want)
+			require.Equal(t, td.want, got)
+		})
+	}
+}
+
+func TestCompleteDirs(t *testing.T) {
+	teardown := setupTestFilesDir(t)
+	defer teardown()
+	for pattern, args := range map[string]map[string][]string{
+		"*": {
+			"di":     {"dir/"},
+			"dir":    {"dir/"},
+			"dir/":   {"dir/"},
+			"./di":   {"./dir/"},
+			"./dir":  {"./dir/"},
+			"./dir/": {"./dir/"},
+			"":       {"./", "dir/", "outer/"},
+			".":      {"./", "./dir/", "./outer/"},
+			"./":     {"./", "./dir/", "./outer/"},
+		},
+		"*.md": {
+			"ou":       {"outer/", "outer/inner/"},
+			"outer":    {"outer/", "outer/inner/"},
+			"outer/":   {"outer/", "outer/inner/"},
+			"./ou":     []string{"./outer/", "./outer/inner/"},
+			"./outer":  []string{"./outer/", "./outer/inner/"},
+			"./outer/": []string{"./outer/", "./outer/inner/"},
+		},
+		"dir": {
+			"di":     {"dir/"},
+			"dir":    {"dir/"},
+			"dir/":   {"dir/"},
+			"./di":   {"./dir/"},
+			"./dir":  {"./dir/"},
+			"./dir/": {"./dir/"},
+		},
+	} {
+		pattern := pattern
+		args := args
+		t.Run(fmt.Sprintf("pattern:%q", pattern), func(t *testing.T) {
+			completer := CompleteDirs()
+			for arg, want := range args {
+				arg := arg
+				want := want
+				t.Run(fmt.Sprintf("arg:%q", arg), func(t *testing.T) {
+					got := completer.Options(newCompleterArgs(arg))
+					sort.Strings(got)
+					sort.Strings(want)
+					require.Equal(t, want, got)
+				})
+			}
+		})
+	}
+}
+
+func TestCompleteFiles(t *testing.T) {
+	teardown := setupTestFilesDir(t)
+	defer teardown()
+	for pattern, args := range map[string]map[string][]string{
+		"*.txt": {
+			"":       {"./", "dir/", "outer/", "a.txt", "b.txt", "c.txt", ".dot.txt"},
+			"./dir/": []string{"./dir/"},
+		},
+		"*": {
+			"./dir/f":   []string{"./dir/foo"},
+			"./dir/foo": []string{"./dir/foo"},
+			"dir":       []string{"dir/", "dir/foo", "dir/bar"},
+			"di":        []string{"dir/", "dir/foo", "dir/bar"},
+			"dir/":      []string{"dir/", "dir/foo", "dir/bar"},
+			"./dir":     []string{"./dir/", "./dir/foo", "./dir/bar"},
+			"./dir/":    []string{"./dir/", "./dir/foo", "./dir/bar"},
+			"./di":      []string{"./dir/", "./dir/foo", "./dir/bar"},
+		},
+		"*.md": {
+			"":        []string{"./", "dir/", "outer/", "readme.md"},
+			".":       []string{"./", "./dir/", "./outer/", "./readme.md"},
+			"./":      []string{"./", "./dir/", "./outer/", "./readme.md"},
+			"outer/i": []string{"outer/inner/", "outer/inner/readme.md"},
+		},
+		"foo": {
+			"./dir/": []string{"./dir/", "./dir/foo"},
+			"./d":    []string{"./dir/", "./dir/foo"},
+		},
+	} {
+		pattern := pattern
+		args := args
+		t.Run(fmt.Sprintf("pattern:%q", pattern), func(t *testing.T) {
+			completer := CompleteFiles(pattern)
+			for arg, want := range args {
+				arg := arg
+				want := want
+				t.Run(fmt.Sprintf("arg:%q", arg), func(t *testing.T) {
+					got := completer.Options(newCompleterArgs(arg))
+					sort.Strings(got)
+					sort.Strings(want)
+					require.Equal(t, want, got)
+				})
+			}
+		})
+	}
+}