package cliargs import ( "fmt" "strings" "testing" ) // Result is the parsed command line: flags by name plus remaining positional args. type Result struct { Flags map[string][]string Args []string } // Has reports whether a flag was provided at least once. func (r Result) Has(name string) bool { _, ok := r.Flags[name] return ok } // Value returns the last value for a flag, or fallback when it was not provided. func (r Result) Value(name, fallback string) string { values := r.Flags[name] if len(values) == 0 { return fallback } return values[len(values)-1] } // Values returns all values provided for a flag. func (r Result) Values(name string) []string { return append([]string(nil), r.Flags[name]...) } // Parse parses a small, predictable flag syntax: // // --name value // --name=value // --flag // -n value // -n=value // // A bare "--" stops flag parsing. func Parse(argv []string) (Result, error) { out := Result{Flags: make(map[string][]string)} for i := 0; i < len(argv); i++ { arg := argv[i] if arg == "--" { out.Args = append(out.Args, argv[i+1:]...) break } if arg == "" || arg == "-" || !strings.HasPrefix(arg, "-") { out.Args = append(out.Args, arg) continue } name, value, hasValue, err := splitFlag(arg) if err != nil { return Result{}, err } if !hasValue && i+1 < len(argv) && argv[i+1] != "" && !strings.HasPrefix(argv[i+1], "-") { i++ value = argv[i] hasValue = true } if !hasValue { value = "true" } out.Flags[name] = append(out.Flags[name], value) } return out, nil } func splitFlag(arg string) (name, value string, hasValue bool, err error) { raw := strings.TrimLeft(arg, "-") if raw == "" { return "", "", false, fmt.Errorf("invalid flag %q", arg) } if before, after, ok := strings.Cut(raw, "="); ok { if before == "" { return "", "", false, fmt.Errorf("invalid flag %q", arg) } return before, after, true, nil } return raw, "", false, nil } func TestParseMixedFlagsAndArgs(t *testing.T) { got, err := Parse([]string{"--name=ada", "-v", "--tag", "one", "input.txt"}) if err != nil { t.Fatal(err) } if got.Value("name", "") != "ada" { t.Fatalf("name = %q", got.Value("name", "")) } if !got.Has("v") || got.Value("v", "") != "true" { t.Fatalf("v flag not parsed as boolean: %#v", got.Flags["v"]) } if got.Value("tag", "") != "one" { t.Fatalf("tag = %q", got.Value("tag", "")) } if len(got.Args) != 1 || got.Args[0] != "input.txt" { t.Fatalf("args = %#v", got.Args) } } func TestParseStopsAtDoubleDash(t *testing.T) { got, err := Parse([]string{"--dry-run", "--", "--literal", "file"}) if err != nil { t.Fatal(err) } if !got.Has("dry-run") { t.Fatal("missing dry-run flag") } if strings.Join(got.Args, ",") != "--literal,file" { t.Fatalf("args = %#v", got.Args) } }