github.com/posener/complete/v2@v2.1.0/complete.go (about)

     1  package complete
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/posener/complete/v2/install"
    11  	"github.com/posener/complete/v2/internal/arg"
    12  	"github.com/posener/complete/v2/internal/tokener"
    13  )
    14  
    15  // Completer is an interface that a command line should implement in order to get bash completion.
    16  type Completer interface {
    17  	// SubCmdList should return the list of all sub commands of the current command.
    18  	SubCmdList() []string
    19  	// SubCmdGet should return a sub command of the current command for the given sub command name.
    20  	SubCmdGet(cmd string) Completer
    21  	// FlagList should return a list of all the flag names of the current command. The flag names
    22  	// should not have the dash prefix.
    23  	FlagList() []string
    24  	// FlagGet should return completion options for a given flag. It is invoked with the flag name
    25  	// without the dash prefix. The flag is not promised to be in the command flags. In that case,
    26  	// this method should return a nil predictor.
    27  	FlagGet(flag string) Predictor
    28  	// ArgsGet should return predictor for positional arguments of the command line.
    29  	ArgsGet() Predictor
    30  }
    31  
    32  // Predictor can predict completion options.
    33  type Predictor interface {
    34  	// Predict returns prediction options for a given prefix. The prefix is what currently is typed
    35  	// as a hint for what to return, but the returned values can have any prefix. The returned
    36  	// values will be filtered by the prefix when needed regardless. The prefix may be empty which
    37  	// means that no value was typed.
    38  	Predict(prefix string) []string
    39  }
    40  
    41  // PredictFunc is a function that implements the Predictor interface.
    42  type PredictFunc func(prefix string) []string
    43  
    44  func (p PredictFunc) Predict(prefix string) []string {
    45  	if p == nil {
    46  		return nil
    47  	}
    48  	return p(prefix)
    49  }
    50  
    51  var (
    52  	getEnv = os.Getenv
    53  	exit   = os.Exit
    54  )
    55  
    56  // Complete the command line arguments for the given command in the case that the program
    57  // was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also
    58  // `os.Exit()`. The program name should be provided for installation purposes.
    59  func Complete(name string, cmd Completer) {
    60  	var (
    61  		line        = getEnv("COMP_LINE")
    62  		point       = getEnv("COMP_POINT")
    63  		doInstall   = getEnv("COMP_INSTALL") == "1"
    64  		doUninstall = getEnv("COMP_UNINSTALL") == "1"
    65  		yes         = getEnv("COMP_YES") == "1"
    66  	)
    67  	var (
    68  		out io.Writer = os.Stdout
    69  		in  io.Reader = os.Stdin
    70  	)
    71  	if doInstall || doUninstall {
    72  		install.Run(name, doUninstall, yes, out, in)
    73  		exit(0)
    74  		return
    75  	}
    76  	if line == "" {
    77  		return
    78  	}
    79  	i, err := strconv.Atoi(point)
    80  	if err != nil {
    81  		panic("COMP_POINT env should be integer, got: " + point)
    82  	}
    83  	if i > len(line) {
    84  		i = len(line)
    85  	}
    86  
    87  	// Parse the command line up to the completion point.
    88  	args := arg.Parse(line[:i])
    89  
    90  	// The first word is the current command name.
    91  	args = args[1:]
    92  
    93  	// Run the completion algorithm.
    94  	options, err := completer{Completer: cmd, args: args}.complete()
    95  	if err != nil {
    96  		fmt.Fprintln(out, "\n"+err.Error())
    97  	} else {
    98  		for _, option := range options {
    99  			fmt.Fprintln(out, option)
   100  		}
   101  	}
   102  	exit(0)
   103  }
   104  
   105  type completer struct {
   106  	Completer
   107  	args  []arg.Arg
   108  	stack []Completer
   109  }
   110  
   111  // compete command with given before and after text.
   112  // if the command has sub commands: try to complete only sub commands or help flags. Otherwise
   113  // complete flags and positional arguments.
   114  func (c completer) complete() ([]string, error) {
   115  reset:
   116  	arg := arg.Arg{}
   117  	if len(c.args) > 0 {
   118  		arg = c.args[0]
   119  	}
   120  	switch {
   121  	case len(c.SubCmdList()) == 0:
   122  		// No sub commands, parse flags and positional arguments.
   123  		return c.suggestLeafCommandOptions(), nil
   124  
   125  	// case !arg.Completed && arg.IsFlag():
   126  	// Suggest help flags for command
   127  	// return []string{helpFlag(arg.Text)}, nil
   128  
   129  	case !arg.Completed:
   130  		// Currently typing a sub command.
   131  		return c.suggestSubCommands(arg.Text), nil
   132  
   133  	case c.SubCmdGet(arg.Text) != nil:
   134  		// Sub command completed, look into that sub command completion.
   135  		// Set the complete command to the requested sub command, and the before text to all the text
   136  		// after the command name and rerun the complete algorithm with the new sub command.
   137  		c.stack = append([]Completer{c.Completer}, c.stack...)
   138  		c.Completer = c.SubCmdGet(arg.Text)
   139  		c.args = c.args[1:]
   140  		goto reset
   141  
   142  	default:
   143  
   144  		// Sub command is unknown...
   145  		return nil, fmt.Errorf("unknown subcommand: %s", arg.Text)
   146  	}
   147  }
   148  
   149  func (c completer) suggestSubCommands(prefix string) []string {
   150  	if len(prefix) > 0 && prefix[0] == '-' {
   151  		help, _ := helpFlag(prefix)
   152  		return []string{help}
   153  	}
   154  	subs := c.SubCmdList()
   155  	return suggest("", prefix, func(prefix string) []string {
   156  		var options []string
   157  		for _, sub := range subs {
   158  			if strings.HasPrefix(sub, prefix) {
   159  				options = append(options, sub)
   160  			}
   161  		}
   162  		return options
   163  	})
   164  }
   165  
   166  func (c completer) suggestLeafCommandOptions() (options []string) {
   167  	arg, before := arg.Arg{}, arg.Arg{}
   168  	if len(c.args) > 0 {
   169  		arg = c.args[len(c.args)-1]
   170  	}
   171  	if len(c.args) > 1 {
   172  		before = c.args[len(c.args)-2]
   173  	}
   174  
   175  	if !arg.Completed {
   176  		// Complete value being typed.
   177  		if arg.HasValue {
   178  			// Complete value of current flag.
   179  			if arg.HasFlag {
   180  				return c.suggestFlagValue(arg.Flag, arg.Value)
   181  			}
   182  			// Complete value of flag in a previous argument.
   183  			if before.HasFlag && !before.HasValue {
   184  				return c.suggestFlagValue(before.Flag, arg.Value)
   185  			}
   186  		}
   187  
   188  		// A value with no flag. Suggest positional argument.
   189  		if !arg.HasValue {
   190  			options = c.suggestFlag(arg.Dashes, arg.Flag)
   191  		}
   192  		if !arg.HasFlag {
   193  			options = append(options, c.suggestArgsValue(arg.Value)...)
   194  		}
   195  		// Suggest flag according to prefix.
   196  		return options
   197  	}
   198  
   199  	// Has a value that was already completed. Suggest all flags and positional arguments.
   200  	if arg.HasValue {
   201  		options = c.suggestFlag(arg.Dashes, "")
   202  		if !arg.HasFlag {
   203  			options = append(options, c.suggestArgsValue("")...)
   204  		}
   205  		return options
   206  	}
   207  	// A flag without a value. Suggest a value or suggest any flag.
   208  	options = c.suggestFlagValue(arg.Flag, "")
   209  	if len(options) > 0 {
   210  		return options
   211  	}
   212  	return c.suggestFlag("", "")
   213  }
   214  
   215  func (c completer) suggestFlag(dashes, prefix string) []string {
   216  	if dashes == "" {
   217  		dashes = "-"
   218  	}
   219  	return suggest(dashes, prefix, func(prefix string) []string {
   220  		var options []string
   221  		c.iterateStack(func(cmd Completer) {
   222  			// Suggest all flags with the given prefix.
   223  			for _, name := range cmd.FlagList() {
   224  				if strings.HasPrefix(name, prefix) {
   225  					options = append(options, dashes+name)
   226  				}
   227  			}
   228  		})
   229  		return options
   230  	})
   231  }
   232  
   233  func (c completer) suggestFlagValue(flagName, prefix string) []string {
   234  	var options []string
   235  	c.iterateStack(func(cmd Completer) {
   236  		if len(options) == 0 {
   237  			if p := cmd.FlagGet(flagName); p != nil {
   238  				options = p.Predict(prefix)
   239  			}
   240  		}
   241  	})
   242  	return filterByPrefix(prefix, options...)
   243  }
   244  
   245  func (c completer) suggestArgsValue(prefix string) []string {
   246  	var options []string
   247  	c.iterateStack(func(cmd Completer) {
   248  		if len(options) == 0 {
   249  			if p := cmd.ArgsGet(); p != nil {
   250  				options = p.Predict(prefix)
   251  			}
   252  		}
   253  	})
   254  	return filterByPrefix(prefix, options...)
   255  }
   256  
   257  func (c completer) iterateStack(f func(Completer)) {
   258  	for _, cmd := range append([]Completer{c.Completer}, c.stack...) {
   259  		f(cmd)
   260  	}
   261  }
   262  
   263  func suggest(dashes, prefix string, collect func(prefix string) []string) []string {
   264  	options := collect(prefix)
   265  	help, helpMatched := helpFlag(dashes + prefix)
   266  	// In case that something matched:
   267  	if len(options) > 0 {
   268  		if strings.HasPrefix(help, dashes+prefix) {
   269  			options = append(options, help)
   270  		}
   271  		return options
   272  	}
   273  
   274  	if helpMatched {
   275  		return []string{help}
   276  	}
   277  
   278  	// Nothing matched.
   279  	options = collect("")
   280  	help, _ = helpFlag(dashes)
   281  	return append(options, help)
   282  }
   283  
   284  func filterByPrefix(prefix string, options ...string) []string {
   285  	var filtered []string
   286  	for _, option := range options {
   287  		if fixed, ok := hasPrefix(option, prefix); ok {
   288  			filtered = append(filtered, fixed)
   289  		}
   290  	}
   291  	if len(filtered) > 0 {
   292  		return filtered
   293  	}
   294  	return options
   295  }
   296  
   297  // hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return
   298  // s in the form of the given prefix.
   299  func hasPrefix(s, prefix string) (string, bool) {
   300  	var (
   301  		token  tokener.Tokener
   302  		si, pi int
   303  	)
   304  	for ; pi < len(prefix); pi++ {
   305  		token.Visit(prefix[pi])
   306  		lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'')
   307  		if lastQuote {
   308  			continue
   309  		}
   310  		if si == len(s) {
   311  			break
   312  		}
   313  		if s[si] == ' ' && !token.Quoted() && token.Escaped() {
   314  			s = s[:si] + "\\" + s[si:]
   315  		}
   316  		if s[si] != prefix[pi] {
   317  			return "", false
   318  		}
   319  		si++
   320  	}
   321  
   322  	if pi < len(prefix) {
   323  		return "", false
   324  	}
   325  
   326  	for ; si < len(s); si++ {
   327  		token.Visit(s[si])
   328  	}
   329  
   330  	return token.Closed(), true
   331  }
   332  
   333  // helpFlag returns either "-h", "-help" or "--help".
   334  func helpFlag(prefix string) (string, bool) {
   335  	if prefix == "" || prefix == "-" || prefix == "-h" {
   336  		return "-h", true
   337  	}
   338  	if strings.HasPrefix("--help", prefix) {
   339  		return "--help", true
   340  	}
   341  	if strings.HasPrefix(prefix, "--") {
   342  		return "--help", false
   343  	}
   344  	return "-help", false
   345  }