diff --git a/fs/config/config_list.go b/fs/config/config_list.go new file mode 100644 index 000000000..34495105b --- /dev/null +++ b/fs/config/config_list.go @@ -0,0 +1,94 @@ +package config + +import ( + "bytes" + "encoding/csv" + "fmt" +) + +// CommaSepList is a comma separated config value +// It uses the encoding/csv rules for quoting and escaping +type CommaSepList []string + +// SpaceSepList is a space separated config value +// It uses the encoding/csv rules for quoting and escaping +type SpaceSepList []string + +type genericList []string + +func (l CommaSepList) String() string { + return genericList(l).string(',') +} + +// Set the List entries +func (l *CommaSepList) Set(s string) error { + return (*genericList)(l).set(',', []byte(s)) +} + +// Type of the value +func (CommaSepList) Type() string { + return "[]string" +} + +// Scan implements the fmt.Scanner interface +func (l *CommaSepList) Scan(s fmt.ScanState, ch rune) error { + return (*genericList)(l).scan(',', s, ch) +} + +func (l SpaceSepList) String() string { + return genericList(l).string(' ') +} + +// Set the List entries +func (l *SpaceSepList) Set(s string) error { + return (*genericList)(l).set(' ', []byte(s)) +} + +// Type of the value +func (SpaceSepList) Type() string { + return "[]string" +} + +// Scan implements the fmt.Scanner interface +func (l *SpaceSepList) Scan(s fmt.ScanState, ch rune) error { + return (*genericList)(l).scan(' ', s, ch) +} + +func (gl genericList) string(sep rune) string { + var buf bytes.Buffer + w := csv.NewWriter(&buf) + w.Comma = sep + err := w.Write(gl) + if err != nil { + // can only happen if w.Comma is invalid + panic(err) + } + w.Flush() + return string(bytes.TrimSpace(buf.Bytes())) +} + +func (gl *genericList) set(sep rune, b []byte) error { + if len(b) == 0 { + *gl = nil + return nil + } + r := csv.NewReader(bytes.NewReader(b)) + r.Comma = sep + + record, err := r.Read() + switch _err := err.(type) { + case nil: + *gl = record + case *csv.ParseError: + err = _err.Err // remove line numbers from the error message + } + return err +} + +func (gl *genericList) scan(sep rune, s fmt.ScanState, ch rune) error { + token, err := s.Token(true, func(rune) bool { return true }) + if err != nil { + return err + } + return gl.set(sep, bytes.TrimSpace(token)) +} diff --git a/fs/config/config_list_test.go b/fs/config/config_list_test.go new file mode 100644 index 000000000..461ca5656 --- /dev/null +++ b/fs/config/config_list_test.go @@ -0,0 +1,87 @@ +package config + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func must(err error) { + if err != nil { + panic(err) + } +} + +func ExampleSpaceSepList() { + for _, s := range []string{ + `remotea:test/dir remoteb:`, + `"remotea:test/space dir" remoteb:`, + `"remotea:test/quote""dir" remoteb:`, + } { + var l SpaceSepList + must(l.Set(s)) + fmt.Printf("%#v\n", l) + } + // Output: + // config.SpaceSepList{"remotea:test/dir", "remoteb:"} + // config.SpaceSepList{"remotea:test/space dir", "remoteb:"} + // config.SpaceSepList{"remotea:test/quote\"dir", "remoteb:"} +} + +func ExampleCommaSepList() { + for _, s := range []string{ + `remotea:test/dir,remoteb:`, + `"remotea:test/space dir",remoteb:`, + `"remotea:test/quote""dir",remoteb:`, + } { + var l CommaSepList + must(l.Set(s)) + fmt.Printf("%#v\n", l) + } + // Output: + // config.CommaSepList{"remotea:test/dir", "remoteb:"} + // config.CommaSepList{"remotea:test/space dir", "remoteb:"} + // config.CommaSepList{"remotea:test/quote\"dir", "remoteb:"} +} + +func TestSpaceSepListSet(t *testing.T) { + type tc struct { + in string + out SpaceSepList + err string + } + tests := []tc{ + {``, nil, ""}, + {`\`, SpaceSepList{`\`}, ""}, + {`\\`, SpaceSepList{`\\`}, ""}, + {`potato`, SpaceSepList{`potato`}, ""}, + {`po\tato`, SpaceSepList{`po\tato`}, ""}, + {`potato\`, SpaceSepList{`potato\`}, ""}, + {`'potato`, SpaceSepList{`'potato`}, ""}, + {`pot'ato`, SpaceSepList{`pot'ato`}, ""}, + {`potato'`, SpaceSepList{`potato'`}, ""}, + {`"potato"`, SpaceSepList{`potato`}, ""}, + {`'potato'`, SpaceSepList{`'potato'`}, ""}, + {`potato apple`, SpaceSepList{`potato`, `apple`}, ""}, + {`potato\ apple`, SpaceSepList{`potato\`, `apple`}, ""}, + {`"potato apple"`, SpaceSepList{`potato apple`}, ""}, + {`"potato'apple"`, SpaceSepList{`potato'apple`}, ""}, + {`"potato''apple"`, SpaceSepList{`potato''apple`}, ""}, + {`"potato' 'apple"`, SpaceSepList{`potato' 'apple`}, ""}, + {`potato="apple"`, nil, `bare " in non-quoted-field`}, + {`apple "potato`, nil, "extraneous"}, + {`apple pot"ato`, nil, "bare \" in non-quoted-field"}, + {`potato"`, nil, "bare \" in non-quoted-field"}, + } + for _, tc := range tests { + var l SpaceSepList + err := l.Set(tc.in) + if tc.err == "" { + require.NoErrorf(t, err, "input: %q", tc.in) + } else { + require.Containsf(t, err.Error(), tc.err, "input: %q", tc.in) + } + require.Equalf(t, tc.out, l, "input: %q", tc.in) + } +}