elvish/pkg/getopt/getopt.go
Qi Xiao e01e5dd2f5 pkg/getopt: Revamp the API and implementation.
- Don't require creating a Getopt object in the API.

- Add a new Parse function, and rename the existing method to Complete.

- Add an Unknown field to Option to indicate unknown options.

- Rewrite the tests.

- Numerous stylistic changes.
2021-12-31 14:36:01 +00:00

298 lines
9.0 KiB
Go

// Package getopt implements a command-line argument parser.
//
// It tries to cover all common styles of option syntaxes, and provides context
// information when given a partial input. It is mainly useful for writing
// completion engines and wrapper programs.
//
// If you are looking for an option parser for your go program, consider using
// the flag package in the standard library instead.
package getopt
//go:generate stringer -type=Config,Arity,ContextType -output=string.go
import (
"fmt"
"strings"
"src.elv.sh/pkg/diag"
)
// Config configurates the parsing behavior.
type Config uint
const (
// Stop parsing options after "--".
StopAfterDoubleDash Config = 1 << iota
// Stop parsing options before the first non-option argument.
StopBeforeFirstNonOption
// Allow long options to start with "-", and disallow short options.
// Replicates the behavior of getopt_long_only and the flag package.
LongOnly
// Config to replicate the behavior of GNU's getopt_long.
GNU = StopAfterDoubleDash
// Config to replicate the behavior of BSD's getopt_long.
BSD = StopAfterDoubleDash | StopBeforeFirstNonOption
)
// Tests whether a configuration has all specified flags set.
func (c Config) has(bits Config) bool { return c&bits == bits }
// OptionSpec is a command-line option.
type OptionSpec struct {
// Short option. Set to 0 for long-only.
Short rune
// Long option. Set to "" for short-only.
Long string
// Whether the option takes an argument, and whether it is required.
Arity Arity
}
// Arity indicates whether an option takes an argument, and whether it is
// required.
type Arity uint
const (
// The option takes no argument.
NoArgument Arity = iota
// The option requires an argument. The argument can come either directly
// after a short option (-oarg), after a long option followed by an equal
// sign (--long=arg), or as a separate argument after the option (-o arg,
// --long arg).
RequiredArgument
// The option takes an optional argument. The argument can come either
// directly after a short option (-oarg) or after a long option followed by
// an equal sign (--long=arg).
OptionalArgument
)
// Option represents a parsed option.
type Option struct {
Spec *OptionSpec
Unknown bool
Long bool
Argument string
}
// Context describes the context of the last argument.
type Context struct {
// The nature of the context.
Type ContextType
// Current option, with a likely incomplete Argument. Non-nil when Type is
// OptionArgument.
Option *Option
// Current partial long option name or argument. Non-empty when Type is
// LongOption or Argument.
Text string
}
// ContextType encodes how the last argument can be completed.
type ContextType uint
const (
// OptionOrArgument indicates that the last element may be either a new
// option or a new argument. Returned when it is an empty string.
OptionOrArgument ContextType = iota
// AnyOption indicates that the last element must be new option, short or
// long. Returned when it is "-".
AnyOption
// LongOption indicates that the last element is a long option (but not its
// argument). The partial name of the long option is stored in Context.Text.
LongOption
// ChainShortOption indicates that a new short option may be chained.
// Returned when the last element consists of a chain of options that take
// no arguments.
ChainShortOption
// OptionArgument indicates that the last element list must be an argument
// to an option. The option in question is stored in Context.Option.
OptionArgument
// Argument indicates that the last element is a non-option argument. The
// partial argument is stored in Context.Text.
Argument
)
// Parse parses an argument list. It returns the parsed options, the non-option
// arguments, and any error.
func Parse(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, error) {
opts, nonOptArgs, opt, _ := parse(args, specs, cfg)
var err error
if opt != nil {
err = fmt.Errorf("missing argument for %s", optionPart(opt))
}
for _, opt := range opts {
if opt.Unknown {
err = diag.Errors(err, fmt.Errorf("unknown option %s", optionPart(opt)))
}
}
return opts, nonOptArgs, err
}
func optionPart(opt *Option) string {
if opt.Long {
return "--" + opt.Spec.Long
}
return "-" + string(opt.Spec.Short)
}
// Complete parses an argument list for completion. It returns the parsed
// options, the non-option arguments, and the context of the last argument. It
// tolerates unknown options, assuming that they take optional arguments.
func Complete(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, Context) {
opts, nonOptArgs, opt, stopOpt := parse(args[:len(args)-1], specs, cfg)
arg := args[len(args)-1]
var ctx Context
switch {
case opt != nil:
opt.Argument = arg
ctx = Context{Type: OptionArgument, Option: opt}
case stopOpt:
ctx = Context{Type: Argument, Text: arg}
case arg == "":
ctx = Context{Type: OptionOrArgument}
case arg == "-":
ctx = Context{Type: AnyOption}
case strings.HasPrefix(arg, "--"):
if !strings.ContainsRune(arg, '=') {
ctx = Context{Type: LongOption, Text: arg[2:]}
} else {
newopt, _ := parseLong(arg[2:], specs)
ctx = Context{Type: OptionArgument, Option: newopt}
}
case strings.HasPrefix(arg, "-"):
if cfg.has(LongOnly) {
if !strings.ContainsRune(arg, '=') {
ctx = Context{Type: LongOption, Text: arg[1:]}
} else {
newopt, _ := parseLong(arg[1:], specs)
ctx = Context{Type: OptionArgument, Option: newopt}
}
} else {
newopts, _ := parseShort(arg[1:], specs)
if newopts[len(newopts)-1].Spec.Arity == NoArgument {
opts = append(opts, newopts...)
ctx = Context{Type: ChainShortOption}
} else {
opts = append(opts, newopts[:len(newopts)-1]...)
ctx = Context{Type: OptionArgument, Option: newopts[len(newopts)-1]}
}
}
default:
ctx = Context{Type: Argument, Text: arg}
}
return opts, nonOptArgs, ctx
}
func parse(args []string, spec []*OptionSpec, cfg Config) ([]*Option, []string, *Option, bool) {
var (
opts []*Option
nonOptArgs []string
// Non-nil only when the last argument was an option with required
// argument, but the argument has not been seen.
opt *Option
// Whether option parsing has been stopped. The condition is controlled
// by the StopAfterDoubleDash and StopBeforeFirstNonOption bits in cfg.
stopOpt bool
)
for _, arg := range args {
switch {
case opt != nil:
opt.Argument = arg
opts = append(opts, opt)
opt = nil
case stopOpt:
nonOptArgs = append(nonOptArgs, arg)
case cfg.has(StopAfterDoubleDash) && arg == "--":
stopOpt = true
case strings.HasPrefix(arg, "--") && arg != "--":
newopt, needArg := parseLong(arg[2:], spec)
if needArg {
opt = newopt
} else {
opts = append(opts, newopt)
}
case strings.HasPrefix(arg, "-") && arg != "--" && arg != "-":
if cfg.has(LongOnly) {
newopt, needArg := parseLong(arg[1:], spec)
if needArg {
opt = newopt
} else {
opts = append(opts, newopt)
}
} else {
newopts, needArg := parseShort(arg[1:], spec)
if needArg {
opts = append(opts, newopts[:len(newopts)-1]...)
opt = newopts[len(newopts)-1]
} else {
opts = append(opts, newopts...)
}
}
default:
nonOptArgs = append(nonOptArgs, arg)
if cfg.has(StopBeforeFirstNonOption) {
stopOpt = true
}
}
}
return opts, nonOptArgs, opt, stopOpt
}
// Parses short options, without the leading dash. Returns the parsed options
// and whether an argument is still to be seen.
func parseShort(s string, specs []*OptionSpec) ([]*Option, bool) {
var opts []*Option
var needArg bool
for i, r := range s {
opt := findShort(r, specs)
if opt != nil {
if opt.Arity == NoArgument {
opts = append(opts, &Option{Spec: opt})
continue
} else {
parsed := &Option{Spec: opt, Argument: s[i+len(string(r)):]}
opts = append(opts, parsed)
needArg = parsed.Argument == "" && opt.Arity == RequiredArgument
break
}
}
// Unknown option, treat as taking an optional argument
parsed := &Option{
Spec: &OptionSpec{r, "", OptionalArgument}, Unknown: true,
Argument: s[i+len(string(r)):]}
opts = append(opts, parsed)
break
}
return opts, needArg
}
func findShort(r rune, specs []*OptionSpec) *OptionSpec {
for _, opt := range specs {
if r == opt.Short {
return opt
}
}
return nil
}
// Parses a long option, without the leading dashes. Returns the parsed option
// and whether an argument is still to be seen.
func parseLong(s string, specs []*OptionSpec) (*Option, bool) {
eq := strings.IndexRune(s, '=')
for _, opt := range specs {
if s == opt.Long {
return &Option{Spec: opt, Long: true}, opt.Arity == RequiredArgument
} else if eq != -1 && s[:eq] == opt.Long {
return &Option{Spec: opt, Long: true, Argument: s[eq+1:]}, false
}
}
// Unknown option, treat as taking an optional argument
if eq == -1 {
return &Option{
Spec: &OptionSpec{0, s, OptionalArgument}, Unknown: true, Long: true}, false
}
return &Option{
Spec: &OptionSpec{0, s[:eq], OptionalArgument}, Unknown: true,
Long: true, Argument: s[eq+1:]}, false
}