package cliargs import ( "fmt" "strings" ) // Parser parses simple command-line flags and positional arguments. type Parser struct { flags map[string]*Flag } // Flag describes a supported command-line flag. type Flag struct { Name string Short string Default string Value string Bool bool Help string Seen bool } // Result is the output of parsing an argument list. type Result struct { Flags map[string]string Booleans map[string]bool Positionals []string } // New creates an empty parser. func New() *Parser { return &Parser{flags: make(map[string]*Flag)} } // String registers a string flag. func (p *Parser) String(name, short, def, help string) { p.add(&Flag{Name: name, Short: short, Default: def, Value: def, Help: help}) } // Bool registers a boolean flag. func (p *Parser) Bool(name, short, help string) { p.add(&Flag{Name: name, Short: short, Bool: true, Help: help}) } func (p *Parser) add(f *Flag) { p.flags["--"+f.Name] = f if f.Short != "" { p.flags["-"+f.Short] = f } } // Parse parses args without the executable name. func (p *Parser) Parse(args []string) (Result, error) { out := Result{ Flags: make(map[string]string), Booleans: make(map[string]bool), } for _, f := range p.flags { if f.Bool { out.Booleans[f.Name] = false } else { out.Flags[f.Name] = f.Default } } for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { out.Positionals = append(out.Positionals, args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { out.Positionals = append(out.Positionals, arg) continue } key, val, hasInlineValue := strings.Cut(arg, "=") flag, ok := p.flags[key] if !ok { return out, fmt.Errorf("unknown flag %q", key) } flag.Seen = true if flag.Bool { if hasInlineValue { switch val { case "true", "1", "yes", "on": out.Booleans[flag.Name] = true case "false", "0", "no", "off": out.Booleans[flag.Name] = false default: return out, fmt.Errorf("invalid boolean value for %q: %q", key, val) } } else { out.Booleans[flag.Name] = true } continue } if hasInlineValue { out.Flags[flag.Name] = val continue } if i+1 >= len(args) { return out, fmt.Errorf("missing value for %q", key) } i++ out.Flags[flag.Name] = args[i] } return out, nil } // Help returns a compact help listing for registered flags. func (p *Parser) Help() string { var b strings.Builder for _, f := range p.flags { if strings.HasPrefix(f.Name, "-") { continue } name := "--" + f.Name if f.Short != "" { name += ", -" + f.Short } if !f.Bool { name += " " } if f.Help != "" { name += "\t" + f.Help } b.WriteString(name + "\n") } return b.String() } func testParseLongAndShortFlags() error { p := New() p.String("name", "n", "guest", "name to greet") p.Bool("verbose", "v", "enable verbose output") got, err := p.Parse([]string{"--name=ada", "-v", "file.txt"}) if err != nil { return err } if got.Flags["name"] != "ada" { return fmt.Errorf("name = %q, want %q", got.Flags["name"], "ada") } if !got.Booleans["verbose"] { return fmt.Errorf("verbose = false, want true") } if len(got.Positionals) != 1 || got.Positionals[0] != "file.txt" { return fmt.Errorf("positionals = %#v, want []string{%q}", got.Positionals, "file.txt") } return nil } func testMissingValueFails() error { p := New() p.String("config", "c", "", "config path") _, err := p.Parse([]string{"--config"}) if err == nil { return fmt.Errorf("expected missing value error") } return nil }