package cliargs import ( "fmt" "strings" ) // Option describes one accepted CLI flag. type Option struct { Name string Short string HasValue bool Default string Help string } // Result contains parsed flags and positional arguments. type Result struct { Values map[string]string Bools map[string]bool Positionals []string } // Parser is a small, dependency-free argument parser. type Parser struct { options map[string]Option alias map[string]string } // NewParser builds a parser from the provided option definitions. func NewParser(opts []Option) (*Parser, error) { p := &Parser{ options: make(map[string]Option), alias: make(map[string]string), } for _, opt := range opts { if opt.Name == "" { return nil, fmt.Errorf("option name cannot be empty") } if _, exists := p.options[opt.Name]; exists { return nil, fmt.Errorf("duplicate option %q", opt.Name) } p.options[opt.Name] = opt p.alias["--"+opt.Name] = opt.Name if opt.Short != "" { short := "-" + strings.TrimPrefix(opt.Short, "-") if _, exists := p.alias[short]; exists { return nil, fmt.Errorf("duplicate short option %q", short) } p.alias[short] = opt.Name } } return p, nil } // Parse converts command-line arguments into flags and positionals. func (p *Parser) Parse(args []string) (*Result, error) { r := &Result{ Values: make(map[string]string), Bools: make(map[string]bool), } for name, opt := range p.options { if opt.HasValue { r.Values[name] = opt.Default } else { r.Bools[name] = opt.Default == "true" } } for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { r.Positionals = append(r.Positionals, args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { r.Positionals = append(r.Positionals, arg) continue } key, value, hasInlineValue := strings.Cut(arg, "=") name, ok := p.alias[key] if !ok { return nil, fmt.Errorf("unknown option %q", key) } opt := p.options[name] if opt.HasValue { if hasInlineValue { r.Values[name] = value continue } if i+1 >= len(args) { return nil, fmt.Errorf("missing value for %q", key) } i++ r.Values[name] = args[i] continue } if hasInlineValue { return nil, fmt.Errorf("option %q does not take a value", key) } r.Bools[name] = true } return r, nil } // Example-style tests: // // p, _ := NewParser([]Option{ // {Name: "verbose", Short: "v"}, // {Name: "output", Short: "o", HasValue: true, Default: "out.txt"}, // }) // got, _ := p.Parse([]string{"-v", "--output=result.txt", "input.txt"}) // // got.Bools["verbose"] == true // // got.Values["output"] == "result.txt" // // got.Positionals[0] == "input.txt" // // _, err := p.Parse([]string{"--missing"}) // // err != nil