github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/elvish/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 programm, consider using
     8  // the flag package in the standard library instead.
     9  package getopt
    10  
    11  //go:generate stringer -type=Config,HasArg,ContextType -output=string.go
    12  
    13  import "strings"
    14  
    15  // Getopt specifies the syntax of command-line arguments.
    16  type Getopt struct {
    17  	Options []*Option
    18  	Config  Config
    19  }
    20  
    21  // Config configurates the parsing behavior.
    22  type Config uint
    23  
    24  const (
    25  	// DoubleDashTerminatesOptions indicates that all elements after an argument
    26  	// "--" are treated as arguments.
    27  	DoubleDashTerminatesOptions Config = 1 << iota
    28  	// FirstArgTerminatesOptions indicates that all elements after the first
    29  	// argument are treated as arguments.
    30  	FirstArgTerminatesOptions
    31  	// LongOnly indicates that long options may be started by either one or two
    32  	// dashes, and short options are not allowed. Should replicate the behavior
    33  	// of getopt_long_only and the
    34  	// flag package of the Go standard library.
    35  	LongOnly
    36  	// GNUGetoptLong is a configuration that should replicate the behavior of
    37  	// GNU getopt_long.
    38  	GNUGetoptLong = DoubleDashTerminatesOptions
    39  	// POSIXGetopt is a configuration that should replicate the behavior of
    40  	// POSIX getopt.
    41  	POSIXGetopt = DoubleDashTerminatesOptions | FirstArgTerminatesOptions
    42  )
    43  
    44  // HasAll tests whether a configuration has all specified flags set.
    45  func (conf Config) HasAll(flags Config) bool {
    46  	return (conf & flags) == flags
    47  }
    48  
    49  // Option is a command-line option.
    50  type Option struct {
    51  	// Short option. Set to 0 for long-only.
    52  	Short rune
    53  	// Long option. Set to "" for short-only.
    54  	Long string
    55  	// Whether the option takes an argument, and whether it is required.
    56  	HasArg HasArg
    57  }
    58  
    59  // HasArg indicates whether an option takes an argument, and whether it is
    60  // required.
    61  type HasArg uint
    62  
    63  const (
    64  	// NoArgument indicates that an option takes no argument.
    65  	NoArgument HasArg = iota
    66  	// RequiredArgument indicates that an option must take an argument. The
    67  	// argument can come either directly after a short option (-oarg), after a
    68  	// long option followed by an equal sign (--long=arg), or as a subsequent
    69  	// argument after the option (-o arg, --long arg).
    70  	RequiredArgument
    71  	// OptionalArgument indicates that an option takes an optional argument.
    72  	// The argument can come either directly after a short option (-oarg) or
    73  	// after a long option followed by an equal sign (--long=arg).
    74  	OptionalArgument
    75  )
    76  
    77  // ParsedOption represents a parsed option.
    78  type ParsedOption struct {
    79  	Option   *Option
    80  	Long     bool
    81  	Argument string
    82  }
    83  
    84  // Context indicates what may come after the supplied argument list.
    85  type Context struct {
    86  	// The nature of the context.
    87  	Type ContextType
    88  	// Current option, with a likely incomplete Argument. Non-nil when Type is
    89  	// OptionArgument.
    90  	Option *ParsedOption
    91  	// Current partial long option name or argument. Non-empty when Type is
    92  	// LongOption or Argument.
    93  	Text string
    94  }
    95  
    96  // ContextType encodes what may be appended to the last element of the argument
    97  // list.
    98  type ContextType uint
    99  
   100  const (
   101  	// NewOptionOrArgument indicates that the last element may be either a new
   102  	// option or a new argument. Returned when it is an empty string.
   103  	NewOptionOrArgument ContextType = iota
   104  	// NewOption indicates that the last element must be new option, short or
   105  	// long. Returned when it is "-".
   106  	NewOption
   107  	// NewLongOption indicates that the last element must be a new long option.
   108  	// Returned when it is "--".
   109  	NewLongOption
   110  	// LongOption indicates that the last element is a long option, but not its
   111  	// argument. The partial name of the long option is stored in Context.Text.
   112  	LongOption
   113  	// ChainShortOption indicates that a new short option may be chained.
   114  	// Returned when the last element consists of a chain of options that take
   115  	// no arguments.
   116  	ChainShortOption
   117  	// OptionArgument indicates that the last element list must be an argument
   118  	// to an option. The option in question is stored in Context.Option.
   119  	OptionArgument
   120  	// Argument indicates that the last element is an argument. The partial
   121  	// argument is stored in Context.Text.
   122  	Argument
   123  )
   124  
   125  func (g *Getopt) findShort(r rune) *Option {
   126  	for _, opt := range g.Options {
   127  		if r == opt.Short {
   128  			return opt
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  // parseShort parse short options, without the leading dash. It returns the
   135  // parsed options and whether an argument is still to be seen.
   136  func (g *Getopt) parseShort(s string) ([]*ParsedOption, bool) {
   137  	var opts []*ParsedOption
   138  	var needArg bool
   139  	for i, r := range s {
   140  		opt := g.findShort(r)
   141  		if opt != nil {
   142  			if opt.HasArg == NoArgument {
   143  				opts = append(opts, &ParsedOption{opt, false, ""})
   144  				continue
   145  			} else {
   146  				parsed := &ParsedOption{opt, false, s[i+len(string(r)):]}
   147  				opts = append(opts, parsed)
   148  				needArg = parsed.Argument == "" && opt.HasArg == RequiredArgument
   149  				break
   150  			}
   151  		}
   152  		// Unknown option, treat as taking an optional argument
   153  		parsed := &ParsedOption{
   154  			&Option{r, "", OptionalArgument}, false, s[i+len(string(r)):]}
   155  		opts = append(opts, parsed)
   156  		break
   157  	}
   158  	return opts, needArg
   159  }
   160  
   161  // parseLong parse a long option, without the leading dashes. It returns the
   162  // parsed option and whether an argument is still to be seen.
   163  func (g *Getopt) parseLong(s string) (*ParsedOption, bool) {
   164  	eq := strings.IndexRune(s, '=')
   165  	for _, opt := range g.Options {
   166  		if s == opt.Long {
   167  			return &ParsedOption{opt, true, ""}, opt.HasArg == RequiredArgument
   168  		} else if eq != -1 && s[:eq] == opt.Long {
   169  			return &ParsedOption{opt, true, s[eq+1:]}, false
   170  		}
   171  	}
   172  	// Unknown option, treat as taking an optional argument
   173  	if eq == -1 {
   174  		return &ParsedOption{&Option{0, s, OptionalArgument}, true, ""}, false
   175  	}
   176  	return &ParsedOption{&Option{0, s[:eq], OptionalArgument}, true, s[eq+1:]}, false
   177  }
   178  
   179  // Parse parses an argument list.
   180  func (g *Getopt) Parse(elems []string) ([]*ParsedOption, []string, *Context) {
   181  	var (
   182  		opts []*ParsedOption
   183  		args []string
   184  		// Non-nil only when the last element was an option with required
   185  		// argument, but the argument has not been seen.
   186  		opt *ParsedOption
   187  		// True if an option terminator has been seen. The criteria of option
   188  		// terminators is determined by the configuration.
   189  		noopt bool
   190  	)
   191  	var elem string
   192  	hasPrefix := func(p string) bool { return strings.HasPrefix(elem, p) }
   193  	for _, elem = range elems[:len(elems)-1] {
   194  		if opt != nil {
   195  			opt.Argument = elem
   196  			opts = append(opts, opt)
   197  			opt = nil
   198  		} else if noopt {
   199  			args = append(args, elem)
   200  		} else if g.Config.HasAll(DoubleDashTerminatesOptions) && elem == "--" {
   201  			noopt = true
   202  		} else if hasPrefix("--") {
   203  			newopt, needArg := g.parseLong(elem[2:])
   204  			if needArg {
   205  				opt = newopt
   206  			} else {
   207  				opts = append(opts, newopt)
   208  			}
   209  		} else if hasPrefix("-") {
   210  			if g.Config.HasAll(LongOnly) {
   211  				newopt, needArg := g.parseLong(elem[1:])
   212  				if needArg {
   213  					opt = newopt
   214  				} else {
   215  					opts = append(opts, newopt)
   216  				}
   217  			} else {
   218  				newopts, needArg := g.parseShort(elem[1:])
   219  				if needArg {
   220  					opts = append(opts, newopts[:len(newopts)-1]...)
   221  					opt = newopts[len(newopts)-1]
   222  				} else {
   223  					opts = append(opts, newopts...)
   224  				}
   225  			}
   226  		} else {
   227  			args = append(args, elem)
   228  			if g.Config.HasAll(FirstArgTerminatesOptions) {
   229  				noopt = true
   230  			}
   231  		}
   232  	}
   233  	elem = elems[len(elems)-1]
   234  	ctx := &Context{}
   235  	if opt != nil {
   236  		opt.Argument = elem
   237  		ctx.Type, ctx.Option = OptionArgument, opt
   238  	} else if noopt {
   239  		ctx.Type, ctx.Text = Argument, elem
   240  	} else if elem == "" {
   241  		ctx.Type = NewOptionOrArgument
   242  	} else if elem == "-" {
   243  		ctx.Type = NewOption
   244  	} else if elem == "--" {
   245  		ctx.Type = NewLongOption
   246  	} else if hasPrefix("--") {
   247  		if strings.IndexRune(elem, '=') == -1 {
   248  			ctx.Type, ctx.Text = LongOption, elem[2:]
   249  		} else {
   250  			newopt, _ := g.parseLong(elem[2:])
   251  			ctx.Type, ctx.Option = OptionArgument, newopt
   252  		}
   253  	} else if hasPrefix("-") {
   254  		if g.Config.HasAll(LongOnly) {
   255  			if strings.IndexRune(elem, '=') == -1 {
   256  				ctx.Type, ctx.Text = LongOption, elem[1:]
   257  			} else {
   258  				newopt, _ := g.parseLong(elem[1:])
   259  				ctx.Type, ctx.Option = OptionArgument, newopt
   260  			}
   261  		} else {
   262  			newopts, _ := g.parseShort(elem[1:])
   263  			if newopts[len(newopts)-1].Option.HasArg == NoArgument {
   264  				opts = append(opts, newopts...)
   265  				ctx.Type = ChainShortOption
   266  			} else {
   267  				opts = append(opts, newopts[:len(newopts)-1]...)
   268  				ctx.Type, ctx.Option = OptionArgument, newopts[len(newopts)-1]
   269  			}
   270  		}
   271  	} else {
   272  		ctx.Type, ctx.Text = Argument, elem
   273  	}
   274  	return opts, args, ctx
   275  }