package main import ( "fmt" "os" "strings" "testing" ) type OptionKind int const ( StringOption OptionKind = iota BoolOption ) type Option struct { Name string Short string Kind OptionKind Default string Help string } type Result struct { Values map[string]string Provided map[string]bool Positionals []string } type Parser struct { options map[string]Option alias map[string]string } // NewParser builds a parser from a small set of known options. func NewParser(options ...Option) *Parser { p := &Parser{ options: make(map[string]Option), alias: make(map[string]string), } for _, opt := range options { p.options[opt.Name] = opt if opt.Short != "" { p.alias[opt.Short] = opt.Name } } return p } // Parse accepts --name value, --name=value, -n value, boolean flags, and --. func (p *Parser) Parse(args []string) (Result, error) { result := Result{ Values: make(map[string]string), Provided: make(map[string]bool), } for name, opt := range p.options { if opt.Default != "" { result.Values[name] = opt.Default } } for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { result.Positionals = append(result.Positionals, args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { result.Positionals = append(result.Positionals, arg) continue } name, value, hasValue, err := splitFlag(arg) if err != nil { return result, err } if canonical, ok := p.alias[name]; ok { name = canonical } opt, ok := p.options[name] if !ok { return result, fmt.Errorf("unknown option %q", name) } switch opt.Kind { case BoolOption: if !hasValue { value = "true" } case StringOption: if !hasValue { i++ if i >= len(args) { return result, fmt.Errorf("missing value for option %q", name) } value = args[i] } default: return result, fmt.Errorf("unsupported option kind for %q", name) } result.Values[name] = value result.Provided[name] = true } return result, nil } func splitFlag(arg string) (name, value string, hasValue bool, err error) { arg = strings.TrimLeft(arg, "-") if arg == "" { return "", "", false, fmt.Errorf("empty option") } name, value, hasValue = strings.Cut(arg, "=") if name == "" { return "", "", false, fmt.Errorf("empty option name") } return name, value, hasValue, nil } func main() { parser := NewParser( Option{Name: "config", Short: "c", Kind: StringOption, Help: "path to config file"}, Option{Name: "verbose", Short: "v", Kind: BoolOption, Help: "enable verbose output"}, ) result, err := parser.Parse(os.Args[1:]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(2) } fmt.Printf("%+v\n", result) } func TestParserHandlesLongShortAndPositionals(t *testing.T) { parser := NewParser( Option{Name: "name", Short: "n", Kind: StringOption}, Option{Name: "verbose", Short: "v", Kind: BoolOption}, ) got, err := parser.Parse([]string{"--name=build", "-v", "src"}) if err != nil { t.Fatal(err) } if got.Values["name"] != "build" { t.Fatalf("name = %q, want build", got.Values["name"]) } if got.Values["verbose"] != "true" { t.Fatalf("verbose = %q, want true", got.Values["verbose"]) } if len(got.Positionals) != 1 || got.Positionals[0] != "src" { t.Fatalf("positionals = %#v, want [src]", got.Positionals) } } func TestParserRejectsMissingStringValue(t *testing.T) { parser := NewParser(Option{Name: "config", Short: "c", Kind: StringOption}) if _, err := parser.Parse([]string{"-c"}); err == nil { t.Fatal("expected missing value error") } }