github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/getopt/getopt.go (about)

     1  // Package getopt implements a command-line argument parser.
     2  //
     3  // It tries to cover all common styles of option syntaxes, and provides context
     4  // information when given a partial input. It is mainly useful for writing
     5  // completion engines and wrapper programs.
     6  //
     7  // If you are looking for an option parser for your go program, consider using
     8  // the flag package in the standard library instead.
     9  package getopt
    10  
    11  //go:generate stringer -type=Config,Arity,ContextType -output=string.go
    12  
    13  import (
    14  	"fmt"
    15  	"strings"
    16  
    17  	"github.com/markusbkk/elvish/pkg/diag"
    18  )
    19  
    20  // Config configurates the parsing behavior.
    21  type Config uint
    22  
    23  const (
    24  	// Stop parsing options after "--".
    25  	StopAfterDoubleDash Config = 1 << iota
    26  	// Stop parsing options before the first non-option argument.
    27  	StopBeforeFirstNonOption
    28  	// Allow long options to start with "-", and disallow short options.
    29  	// Replicates the behavior of getopt_long_only and the flag package.
    30  	LongOnly
    31  
    32  	// Config to replicate the behavior of GNU's getopt_long.
    33  	GNU = StopAfterDoubleDash
    34  	// Config to replicate the behavior of BSD's getopt_long.
    35  	BSD = StopAfterDoubleDash | StopBeforeFirstNonOption
    36  )
    37  
    38  // Tests whether a configuration has all specified flags set.
    39  func (c Config) has(bits Config) bool { return c&bits == bits }
    40  
    41  // OptionSpec is a command-line option.
    42  type OptionSpec struct {
    43  	// Short option. Set to 0 for long-only.
    44  	Short rune
    45  	// Long option. Set to "" for short-only.
    46  	Long string
    47  	// Whether the option takes an argument, and whether it is required.
    48  	Arity Arity
    49  }
    50  
    51  // Arity indicates whether an option takes an argument, and whether it is
    52  // required.
    53  type Arity uint
    54  
    55  const (
    56  	// The option takes no argument.
    57  	NoArgument Arity = iota
    58  	// The option requires an argument. The argument can come either directly
    59  	// after a short option (-oarg), after a long option followed by an equal
    60  	// sign (--long=arg), or as a separate argument after the option (-o arg,
    61  	// --long arg).
    62  	RequiredArgument
    63  	// The option takes an optional argument. The argument can come either
    64  	// directly after a short option (-oarg) or after a long option followed by
    65  	// an equal sign (--long=arg).
    66  	OptionalArgument
    67  )
    68  
    69  // Option represents a parsed option.
    70  type Option struct {
    71  	Spec     *OptionSpec
    72  	Unknown  bool
    73  	Long     bool
    74  	Argument string
    75  }
    76  
    77  // Context describes the context of the last argument.
    78  type Context struct {
    79  	// The nature of the context.
    80  	Type ContextType
    81  	// Current option, with a likely incomplete Argument. Non-nil when Type is
    82  	// OptionArgument.
    83  	Option *Option
    84  	// Current partial long option name or argument. Non-empty when Type is
    85  	// LongOption or Argument.
    86  	Text string
    87  }
    88  
    89  // ContextType encodes how the last argument can be completed.
    90  type ContextType uint
    91  
    92  const (
    93  	// OptionOrArgument indicates that the last element may be either a new
    94  	// option or a new argument. Returned when it is an empty string.
    95  	OptionOrArgument ContextType = iota
    96  	// AnyOption indicates that the last element must be new option, short or
    97  	// long. Returned when it is "-".
    98  	AnyOption
    99  	// LongOption indicates that the last element is a long option (but not its
   100  	// argument). The partial name of the long option is stored in Context.Text.
   101  	LongOption
   102  	// ChainShortOption indicates that a new short option may be chained.
   103  	// Returned when the last element consists of a chain of options that take
   104  	// no arguments.
   105  	ChainShortOption
   106  	// OptionArgument indicates that the last element list must be an argument
   107  	// to an option. The option in question is stored in Context.Option.
   108  	OptionArgument
   109  	// Argument indicates that the last element is a non-option argument. The
   110  	// partial argument is stored in Context.Text.
   111  	Argument
   112  )
   113  
   114  // Parse parses an argument list. It returns the parsed options, the non-option
   115  // arguments, and any error.
   116  func Parse(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, error) {
   117  	opts, nonOptArgs, opt, _ := parse(args, specs, cfg)
   118  	var err error
   119  	if opt != nil {
   120  		err = fmt.Errorf("missing argument for %s", optionPart(opt))
   121  	}
   122  	for _, opt := range opts {
   123  		if opt.Unknown {
   124  			err = diag.Errors(err, fmt.Errorf("unknown option %s", optionPart(opt)))
   125  		}
   126  	}
   127  	return opts, nonOptArgs, err
   128  }
   129  
   130  func optionPart(opt *Option) string {
   131  	if opt.Long {
   132  		return "--" + opt.Spec.Long
   133  	}
   134  	return "-" + string(opt.Spec.Short)
   135  }
   136  
   137  // Complete parses an argument list for completion. It returns the parsed
   138  // options, the non-option arguments, and the context of the last argument. It
   139  // tolerates unknown options, assuming that they take optional arguments.
   140  func Complete(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, Context) {
   141  	opts, nonOptArgs, opt, stopOpt := parse(args[:len(args)-1], specs, cfg)
   142  
   143  	arg := args[len(args)-1]
   144  	var ctx Context
   145  	switch {
   146  	case opt != nil:
   147  		opt.Argument = arg
   148  		ctx = Context{Type: OptionArgument, Option: opt}
   149  	case stopOpt:
   150  		ctx = Context{Type: Argument, Text: arg}
   151  	case arg == "":
   152  		ctx = Context{Type: OptionOrArgument}
   153  	case arg == "-":
   154  		ctx = Context{Type: AnyOption}
   155  	case strings.HasPrefix(arg, "--"):
   156  		if !strings.ContainsRune(arg, '=') {
   157  			ctx = Context{Type: LongOption, Text: arg[2:]}
   158  		} else {
   159  			newopt, _ := parseLong(arg[2:], specs)
   160  			ctx = Context{Type: OptionArgument, Option: newopt}
   161  		}
   162  	case strings.HasPrefix(arg, "-"):
   163  		if cfg.has(LongOnly) {
   164  			if !strings.ContainsRune(arg, '=') {
   165  				ctx = Context{Type: LongOption, Text: arg[1:]}
   166  			} else {
   167  				newopt, _ := parseLong(arg[1:], specs)
   168  				ctx = Context{Type: OptionArgument, Option: newopt}
   169  			}
   170  		} else {
   171  			newopts, _ := parseShort(arg[1:], specs)
   172  			if newopts[len(newopts)-1].Spec.Arity == NoArgument {
   173  				opts = append(opts, newopts...)
   174  				ctx = Context{Type: ChainShortOption}
   175  			} else {
   176  				opts = append(opts, newopts[:len(newopts)-1]...)
   177  				ctx = Context{Type: OptionArgument, Option: newopts[len(newopts)-1]}
   178  			}
   179  		}
   180  	default:
   181  		ctx = Context{Type: Argument, Text: arg}
   182  	}
   183  	return opts, nonOptArgs, ctx
   184  }
   185  
   186  func parse(args []string, spec []*OptionSpec, cfg Config) ([]*Option, []string, *Option, bool) {
   187  	var (
   188  		opts       []*Option
   189  		nonOptArgs []string
   190  		// Non-nil only when the last argument was an option with required
   191  		// argument, but the argument has not been seen.
   192  		opt *Option
   193  		// Whether option parsing has been stopped. The condition is controlled
   194  		// by the StopAfterDoubleDash and StopBeforeFirstNonOption bits in cfg.
   195  		stopOpt bool
   196  	)
   197  	for _, arg := range args {
   198  		switch {
   199  		case opt != nil:
   200  			opt.Argument = arg
   201  			opts = append(opts, opt)
   202  			opt = nil
   203  		case stopOpt:
   204  			nonOptArgs = append(nonOptArgs, arg)
   205  		case cfg.has(StopAfterDoubleDash) && arg == "--":
   206  			stopOpt = true
   207  		case strings.HasPrefix(arg, "--") && arg != "--":
   208  			newopt, needArg := parseLong(arg[2:], spec)
   209  			if needArg {
   210  				opt = newopt
   211  			} else {
   212  				opts = append(opts, newopt)
   213  			}
   214  		case strings.HasPrefix(arg, "-") && arg != "--" && arg != "-":
   215  			if cfg.has(LongOnly) {
   216  				newopt, needArg := parseLong(arg[1:], spec)
   217  				if needArg {
   218  					opt = newopt
   219  				} else {
   220  					opts = append(opts, newopt)
   221  				}
   222  			} else {
   223  				newopts, needArg := parseShort(arg[1:], spec)
   224  				if needArg {
   225  					opts = append(opts, newopts[:len(newopts)-1]...)
   226  					opt = newopts[len(newopts)-1]
   227  				} else {
   228  					opts = append(opts, newopts...)
   229  				}
   230  			}
   231  		default:
   232  			nonOptArgs = append(nonOptArgs, arg)
   233  			if cfg.has(StopBeforeFirstNonOption) {
   234  				stopOpt = true
   235  			}
   236  		}
   237  	}
   238  	return opts, nonOptArgs, opt, stopOpt
   239  }
   240  
   241  // Parses short options, without the leading dash. Returns the parsed options
   242  // and whether an argument is still to be seen.
   243  func parseShort(s string, specs []*OptionSpec) ([]*Option, bool) {
   244  	var opts []*Option
   245  	var needArg bool
   246  	for i, r := range s {
   247  		opt := findShort(r, specs)
   248  		if opt != nil {
   249  			if opt.Arity == NoArgument {
   250  				opts = append(opts, &Option{Spec: opt})
   251  				continue
   252  			} else {
   253  				parsed := &Option{Spec: opt, Argument: s[i+len(string(r)):]}
   254  				opts = append(opts, parsed)
   255  				needArg = parsed.Argument == "" && opt.Arity == RequiredArgument
   256  				break
   257  			}
   258  		}
   259  		// Unknown option, treat as taking an optional argument
   260  		parsed := &Option{
   261  			Spec: &OptionSpec{r, "", OptionalArgument}, Unknown: true,
   262  			Argument: s[i+len(string(r)):]}
   263  		opts = append(opts, parsed)
   264  		break
   265  	}
   266  	return opts, needArg
   267  }
   268  
   269  func findShort(r rune, specs []*OptionSpec) *OptionSpec {
   270  	for _, opt := range specs {
   271  		if r == opt.Short {
   272  			return opt
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  // Parses a long option, without the leading dashes. Returns the parsed option
   279  // and whether an argument is still to be seen.
   280  func parseLong(s string, specs []*OptionSpec) (*Option, bool) {
   281  	eq := strings.IndexRune(s, '=')
   282  	for _, opt := range specs {
   283  		if s == opt.Long {
   284  			return &Option{Spec: opt, Long: true}, opt.Arity == RequiredArgument
   285  		} else if eq != -1 && s[:eq] == opt.Long {
   286  			return &Option{Spec: opt, Long: true, Argument: s[eq+1:]}, false
   287  		}
   288  	}
   289  	// Unknown option, treat as taking an optional argument
   290  	if eq == -1 {
   291  		return &Option{
   292  			Spec: &OptionSpec{0, s, OptionalArgument}, Unknown: true, Long: true}, false
   293  	}
   294  	return &Option{
   295  		Spec: &OptionSpec{0, s[:eq], OptionalArgument}, Unknown: true,
   296  		Long: true, Argument: s[eq+1:]}, false
   297  }