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 }