Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions string_to_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/csv"
"fmt"
"io"
"sort"
"strings"
)
Expand Down Expand Up @@ -37,6 +38,9 @@ func (s *stringToStringValue) Set(val string) error {
if err != nil {
return err
}
if _, err := r.Read(); err != io.EOF {
return fmt.Errorf("must be a single CSV record; quote fields containing newlines")
}
}

out := make(map[string]string, len(ss))
Expand Down Expand Up @@ -125,6 +129,16 @@ func (f *FlagSet) GetStringToString(name string) (map[string]string, error) {
//
// --arg a=1,2 -> map[string]string{ "a": "1,2" }
//
// Arguments containing multiple '=' characters are parsed as one CSV record.
// Quote the entire key=value field to preserve commas, quotes, or newlines in
// its value. When invoking a command from a shell, preserve the CSV quotes,
// for example by enclosing the argument in single quotes:
//
// --arg '"a=first,second=value"' -> map[string]string{ "a": "first,second=value" }
//
// Multiple unquoted CSV records in one argument are rejected rather than
// silently discarding mappings after the first record.
//
// Returns a pointer to the map which will be updated upon invocation of [FlagSet.Parse], [Flag.Value.Set], and others.
func (f *FlagSet) StringToString(name string, value map[string]string, usage string) *map[string]string {
p := map[string]string{}
Expand Down
25 changes: 25 additions & 0 deletions string_to_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ func TestStringToString(t *testing.T) {
}
}

func TestStringToStringRejectsExtraCSVRecords(t *testing.T) {
f := NewFlagSet("test", ContinueOnError)
values := f.StringToString("arg", map[string]string{"default": "retained"}, "")
if err := f.Parse([]string{"--arg", "a=first\nb=second"}); err == nil {
t.Fatal("expected an error instead of silently dropping the second record")
}
if !reflect.DeepEqual(*values, map[string]string{"default": "retained"}) {
t.Fatalf("failed parsing changed the default map: %v", *values)
}
if f.Lookup("arg").Changed {
t.Fatal("failed parsing marked the flag as changed")
}
}

func TestStringToStringQuotedNewline(t *testing.T) {
f := NewFlagSet("test", ContinueOnError)
values := f.StringToString("arg", nil, "")
if err := f.Parse([]string{"--arg", "\"a=first\nsecond=value\""}); err != nil {
t.Fatalf("quoted newline should be preserved: %v", err)
}
if !reflect.DeepEqual(*values, map[string]string{"a": "first\nsecond=value"}) {
t.Fatalf("quoted value was not preserved: %v", *values)
}
}

// This test ensures that [FlagSet.GetStringToString] always return the pointers which were given during flag
// initialization.
//
Expand Down