mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-14 02:57:52 +08:00
e01e5dd2f5
- 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.
298 lines
9.0 KiB
Go
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
|
|
}
|