package argparser import ( "fmt" "strings" "testing" ) type Parser struct { flags map[string]Flag } type Flag struct { Name string Short string Help string HasValue bool Default string } type Result struct { Flags map[string]string Args []string } func NewParser() *Parser { return &Parser{flags: make(map[string]Flag)} } // AddFlag registers a long option, with an optional short alias. func (p *Parser) AddFlag(name, short, help string, hasValue bool, defaultValue string) { flag := Flag{ Name: name, Short: short, Help: help, HasValue: hasValue, Default: defaultValue, } p.flags["--"+name] = flag if short != "" { p.flags["-"+short] = flag } } // Parse reads argv-style tokens and returns parsed flags plus positional args. func (p *Parser) Parse(args []string) (Result, error) { result := Result{ Flags: make(map[string]string), Args: make([]string, 0), } seen := make(map[string]bool) for _, flag := range p.flags { if flag.Default != "" && !seen[flag.Name] { result.Flags[flag.Name] = flag.Default seen[flag.Name] = true } } for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { result.Args = append(result.Args, args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { result.Args = append(result.Args, arg) continue } key, inlineValue, hasInlineValue := strings.Cut(arg, "=") flag, ok := p.flags[key] if !ok { return result, fmt.Errorf("unknown flag: %s", key) } if flag.HasValue { if hasInlineValue { result.Flags[flag.Name] = inlineValue continue } if i+1 >= len(args) { return result, fmt.Errorf("missing value for flag: %s", key) } i++ result.Flags[flag.Name] = args[i] continue } if hasInlineValue { return result, fmt.Errorf("flag does not accept value: %s", key) } result.Flags[flag.Name] = "true" } return result, nil } func TestParseFlagsAndArgs(t *testing.T) { parser := NewParser() parser.AddFlag("verbose", "v", "enable verbose output", false, "") parser.AddFlag("output", "o", "output file", true, "stdout") got, err := parser.Parse([]string{"-v", "--output=app.log", "input.txt"}) if err != nil { t.Fatal(err) } if got.Flags["verbose"] != "true" { t.Fatalf("verbose = %q, want true", got.Flags["verbose"]) } if got.Flags["output"] != "app.log" { t.Fatalf("output = %q, want app.log", got.Flags["output"]) } if len(got.Args) != 1 || got.Args[0] != "input.txt" { t.Fatalf("args = %#v, want [input.txt]", got.Args) } } func TestParseStopsAtDoubleDash(t *testing.T) { parser := NewParser() parser.AddFlag("name", "n", "name value", true, "") got, err := parser.Parse([]string{"--name", "build", "--", "--not-a-flag"}) if err != nil { t.Fatal(err) } if got.Flags["name"] != "build" { t.Fatalf("name = %q, want build", got.Flags["name"]) } if len(got.Args) != 1 || got.Args[0] != "--not-a-flag" { t.Fatalf("args = %#v, want [--not-a-flag]", got.Args) } }