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 }