complete/complete.go

346 lines
8.8 KiB
Go

package complete
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/posener/complete/v2/install"
"github.com/posener/complete/v2/internal/arg"
"github.com/posener/complete/v2/internal/tokener"
)
// Completer is an interface that a command line should implement in order to get bash completion.
type Completer interface {
// SubCmdList should return the list of all sub commands of the current command.
SubCmdList() []string
// SubCmdGet should return a sub command of the current command for the given sub command name.
SubCmdGet(cmd string) Completer
// FlagList should return a list of all the flag names of the current command. The flag names
// should not have the dash prefix.
FlagList() []string
// FlagGet should return completion options for a given flag. It is invoked with the flag name
// without the dash prefix. The flag is not promised to be in the command flags. In that case,
// this method should return a nil predictor.
FlagGet(flag string) Predictor
// ArgsGet should return predictor for positional arguments of the command line.
ArgsGet() Predictor
}
// Predictor can predict completion options.
type Predictor interface {
// Predict returns prediction options for a given prefix. The prefix is what currently is typed
// as a hint for what to return, but the returned values can have any prefix. The returned
// values will be filtered by the prefix when needed regardless. The prefix may be empty which
// means that no value was typed.
Predict(prefix string) []string
}
// PredictFunc is a function that implements the Predictor interface.
type PredictFunc func(prefix string) []string
func (p PredictFunc) Predict(prefix string) []string {
if p == nil {
return nil
}
return p(prefix)
}
var (
getEnv = os.Getenv
exit = os.Exit
)
// Complete the command line arguments for the given command in the case that the program
// was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also
// `os.Exit()`. The program name should be provided for installation purposes.
func Complete(name string, cmd Completer) {
var (
line = getEnv("COMP_LINE")
point = getEnv("COMP_POINT")
doInstall = getEnv("COMP_INSTALL") == "1"
doUninstall = getEnv("COMP_UNINSTALL") == "1"
yes = getEnv("COMP_YES") == "1"
)
var (
out io.Writer = os.Stdout
in io.Reader = os.Stdin
)
if doInstall || doUninstall {
install.Run(name, doUninstall, yes, out, in)
exit(0)
return
}
if line == "" {
return
}
i, err := strconv.Atoi(point)
if err != nil {
panic("COMP_POINT env should be integer, got: " + point)
}
if i > len(line) {
i = len(line)
}
// Parse the command line up to the completion point.
args := arg.Parse(line[:i])
// The first word is the current command name.
args = args[1:]
// Run the completion algorithm.
options, err := completer{Completer: cmd, args: args}.complete()
if err != nil {
fmt.Fprintln(out, "\n"+err.Error())
} else {
for _, option := range options {
fmt.Fprintln(out, option)
}
}
exit(0)
}
type completer struct {
Completer
args []arg.Arg
stack []Completer
}
// compete command with given before and after text.
// if the command has sub commands: try to complete only sub commands or help flags. Otherwise
// complete flags and positional arguments.
func (c completer) complete() ([]string, error) {
reset:
arg := arg.Arg{}
if len(c.args) > 0 {
arg = c.args[0]
}
switch {
case len(c.SubCmdList()) == 0:
// No sub commands, parse flags and positional arguments.
return c.suggestLeafCommandOptions(), nil
// case !arg.Completed && arg.IsFlag():
// Suggest help flags for command
// return []string{helpFlag(arg.Text)}, nil
case !arg.Completed:
// Currently typing a sub command.
return c.suggestSubCommands(arg.Text), nil
case c.SubCmdGet(arg.Text) != nil:
// Sub command completed, look into that sub command completion.
// Set the complete command to the requested sub command, and the before text to all the text
// after the command name and rerun the complete algorithm with the new sub command.
c.stack = append([]Completer{c.Completer}, c.stack...)
c.Completer = c.SubCmdGet(arg.Text)
c.args = c.args[1:]
goto reset
default:
// Sub command is unknown...
return nil, fmt.Errorf("unknown subcommand: %s", arg.Text)
}
}
func (c completer) suggestSubCommands(prefix string) []string {
if len(prefix) > 0 && prefix[0] == '-' {
help, _ := helpFlag(prefix)
return []string{help}
}
subs := c.SubCmdList()
return suggest("", prefix, func(prefix string) []string {
var options []string
for _, sub := range subs {
if strings.HasPrefix(sub, prefix) {
options = append(options, sub)
}
}
return options
})
}
func (c completer) suggestLeafCommandOptions() (options []string) {
arg, before := arg.Arg{}, arg.Arg{}
if len(c.args) > 0 {
arg = c.args[len(c.args)-1]
}
if len(c.args) > 1 {
before = c.args[len(c.args)-2]
}
if !arg.Completed {
// Complete value being typed.
if arg.HasValue {
// Complete value of current flag.
if arg.HasFlag {
return c.suggestFlagValue(arg.Flag, arg.Value)
}
// Complete value of flag in a previous argument.
if before.HasFlag && !before.HasValue {
return c.suggestFlagValue(before.Flag, arg.Value)
}
}
// A value with no flag. Suggest positional argument.
if !arg.HasValue {
options = c.suggestFlag(arg.Dashes, arg.Flag)
}
if !arg.HasFlag {
options = append(options, c.suggestArgsValue(arg.Value)...)
}
// Suggest flag according to prefix.
return options
}
// Has a value that was already completed. Suggest all flags and positional arguments.
if arg.HasValue {
options = c.suggestFlag(arg.Dashes, "")
if !arg.HasFlag {
options = append(options, c.suggestArgsValue("")...)
}
return options
}
// A flag without a value. Suggest a value or suggest any flag.
options = c.suggestFlagValue(arg.Flag, "")
if len(options) > 0 {
return options
}
return c.suggestFlag("", "")
}
func (c completer) suggestFlag(dashes, prefix string) []string {
if dashes == "" {
dashes = "-"
}
return suggest(dashes, prefix, func(prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
// Suggest all flags with the given prefix.
for _, name := range cmd.FlagList() {
if strings.HasPrefix(name, prefix) {
options = append(options, dashes+name)
}
}
})
return options
})
}
func (c completer) suggestFlagValue(flagName, prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
if len(options) == 0 {
if p := cmd.FlagGet(flagName); p != nil {
options = p.Predict(prefix)
}
}
})
return filterByPrefix(prefix, options...)
}
func (c completer) suggestArgsValue(prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
if len(options) == 0 {
if p := cmd.ArgsGet(); p != nil {
options = p.Predict(prefix)
}
}
})
return filterByPrefix(prefix, options...)
}
func (c completer) iterateStack(f func(Completer)) {
for _, cmd := range append([]Completer{c.Completer}, c.stack...) {
f(cmd)
}
}
func suggest(dashes, prefix string, collect func(prefix string) []string) []string {
options := collect(prefix)
help, helpMatched := helpFlag(dashes + prefix)
// In case that something matched:
if len(options) > 0 {
if strings.HasPrefix(help, dashes+prefix) {
options = append(options, help)
}
return options
}
if helpMatched {
return []string{help}
}
// Nothing matched.
options = collect("")
help, _ = helpFlag(dashes)
return append(options, help)
}
func filterByPrefix(prefix string, options ...string) []string {
var filtered []string
for _, option := range options {
if fixed, ok := hasPrefix(option, prefix); ok {
filtered = append(filtered, fixed)
}
}
if len(filtered) > 0 {
return filtered
}
return options
}
// hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return
// s in the form of the given prefix.
func hasPrefix(s, prefix string) (string, bool) {
var (
token tokener.Tokener
si, pi int
)
for ; pi < len(prefix); pi++ {
token.Visit(prefix[pi])
lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'')
if lastQuote {
continue
}
if si == len(s) {
break
}
if s[si] == ' ' && !token.Quoted() && token.Escaped() {
s = s[:si] + "\\" + s[si:]
}
if s[si] != prefix[pi] {
return "", false
}
si++
}
if pi < len(prefix) {
return "", false
}
for ; si < len(s); si++ {
token.Visit(s[si])
}
return token.Closed(), true
}
// helpFlag returns either "-h", "-help" or "--help".
func helpFlag(prefix string) (string, bool) {
if prefix == "" || prefix == "-" || prefix == "-h" {
return "-h", true
}
if strings.HasPrefix("--help", prefix) {
return "--help", true
}
if strings.HasPrefix(prefix, "--") {
return "--help", false
}
return "-help", false
}