package cliargs import ( "fmt" "strings" "testing" ) // Parser stores accepted flags and positional arguments. type Parser struct { flags map[string]*Flag } // Flag describes one command-line flag. type Flag struct { Name string Short string Help string HasValue bool Default string } // Result contains parsed flag values and remaining positional arguments. type Result struct { Values map[string]string Args []string } // New creates an empty argument parser. func New() *Parser { return &Parser{flags: make(map[string]*Flag)} } // AddFlag registers a named flag. Use short without the leading dash. func (p *Parser) AddFlag(name, short, help string, hasValue bool, def string) { f := &Flag{Name: name, Short: short, Help: help, HasValue: hasValue, Default: def} p.flags["--"+name] = f if short != "" { p.flags["-"+short] = f } } // Parse reads argv-style arguments, excluding the program name. func (p *Parser) Parse(args []string) (Result, error) { res := Result{Values: make(map[string]string)} for _, f := range p.flags { if _, ok := res.Values[f.Name]; !ok && f.Default != "" { res.Values[f.Name] = f.Default } } for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { res.Args = append(res.Args, args[i+1:]...) break } if !strings.HasPrefix(arg, "-") || arg == "-" { res.Args = append(res.Args, arg) continue } key, val, hasInlineValue := splitFlag(arg) flag, ok := p.flags[key] if !ok { return res, fmt.Errorf("unknown flag: %s", key) } if flag.HasValue { if hasInlineValue { res.Values[flag.Name] = val continue } if i+1 >= len(args) { return res, fmt.Errorf("missing value for flag: %s", key) } i++ res.Values[flag.Name] = args[i] continue } if hasInlineValue { return res, fmt.Errorf("flag does not accept a value: %s", key) } res.Values[flag.Name] = "true" } return res, nil } func splitFlag(arg string) (key, value string, hasValue bool) { if i := strings.IndexByte(arg, '='); i >= 0 { return arg[:i], arg[i+1:], true } return arg, "", false } func TestParseFlagsAndArgs(t *testing.T) { p := New() p.AddFlag("output", "o", "output path", true, "") p.AddFlag("verbose", "v", "verbose logging", false, "") got, err := p.Parse([]string{"-v", "--output=out.txt", "input.txt"}) if err != nil { t.Fatal(err) } if got.Values["verbose"] != "true" || got.Values["output"] != "out.txt" { t.Fatalf("unexpected flags: %#v", got.Values) } if len(got.Args) != 1 || got.Args[0] != "input.txt" { t.Fatalf("unexpected args: %#v", got.Args) } } func TestParseMissingValue(t *testing.T) { p := New() p.AddFlag("config", "c", "config file", true, "") if _, err := p.Parse([]string{"--config"}); err == nil { t.Fatal("expected missing value error") } }