package cliargs import ( "fmt" "strings" "testing" ) type Parser struct { flags map[string]*Flag args []string } type Flag struct { Name string Short string Help string HasValue bool Default string Value string Set bool } // New creates a parser for a command's argument list, usually os.Args[1:]. func New(args []string) *Parser { return &Parser{ flags: make(map[string]*Flag), args: args, } } // Bool registers a boolean flag. It is false unless present. func (p *Parser) Bool(name, short, help string) *Flag { return p.add(name, short, help, false, "false") } // String registers a string flag with a default value. func (p *Parser) String(name, short, help, def string) *Flag { return p.add(name, short, help, true, def) } func (p *Parser) add(name, short, help string, hasValue bool, def string) *Flag { flag := &Flag{ Name: name, Short: short, Help: help, HasValue: hasValue, Default: def, Value: def, } p.flags["--"+name] = flag if short != "" { p.flags["-"+short] = flag } return flag } // Parse reads flags and returns positional arguments. func (p *Parser) Parse() ([]string, error) { var positional []string for i := 0; i < len(p.args); i++ { arg := p.args[i] if arg == "--" { positional = append(positional, p.args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { positional = append(positional, arg) continue } key, value, hasInlineValue := strings.Cut(arg, "=") flag, ok := p.flags[key] if !ok { return nil, fmt.Errorf("unknown flag %q", key) } flag.Set = true if flag.HasValue { if hasInlineValue { flag.Value = value continue } if i+1 >= len(p.args) { return nil, fmt.Errorf("missing value for %q", key) } i++ flag.Value = p.args[i] continue } if hasInlineValue { return nil, fmt.Errorf("flag %q does not take a value", key) } flag.Value = "true" } return positional, nil } // Usage returns a compact help string. func (p *Parser) Usage(command string) string { var b strings.Builder fmt.Fprintf(&b, "Usage: %s [flags] [args]\n\nFlags:\n", command) seen := map[*Flag]bool{} for _, flag := range p.flags { if seen[flag] { continue } seen[flag] = true names := "--" + flag.Name if flag.Short != "" { names = "-" + flag.Short + ", " + names } if flag.HasValue { names += " " } fmt.Fprintf(&b, " %-22s %s", names, flag.Help) if flag.Default != "" && flag.Default != "false" { fmt.Fprintf(&b, " (default: %s)", flag.Default) } b.WriteByte('\n') } return b.String() } func TestParseFlagsAndPositionals(t *testing.T) { p := New([]string{"-v", "--name=demo", "input.txt", "--", "--literal"}) verbose := p.Bool("verbose", "v", "enable verbose output") name := p.String("name", "n", "set name", "default") args, err := p.Parse() if err != nil { t.Fatal(err) } if verbose.Value != "true" || !verbose.Set { t.Fatalf("verbose = %q, set = %v", verbose.Value, verbose.Set) } if name.Value != "demo" { t.Fatalf("name = %q", name.Value) } if got := strings.Join(args, ","); got != "input.txt,--literal" { t.Fatalf("args = %q", got) } } func TestParseErrors(t *testing.T) { p := New([]string{"--name"}) p.String("name", "n", "set name", "") if _, err := p.Parse(); err == nil { t.Fatal("expected missing value error") } p = New([]string{"--verbose=true"}) p.Bool("verbose", "v", "enable verbose output") if _, err := p.Parse(); err == nil { t.Fatal("expected boolean value error") } }