From 44199db79d035c9cd04f4a576b3ba359af7dcc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wenkai=20Yin=28=E5=B0=B9=E6=96=87=E5=BC=80=29?= Date: Tue, 17 May 2022 10:21:35 +0800 Subject: [PATCH] Enhance the map flag to support parsing input value contains entry delimiters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance the map flag to support parsing input value contains entry delimiters Signed-off-by: Wenkai Yin(尹文开) --- changelogs/unreleased/4920-ywk253100 | 1 + pkg/cmd/cli/restore/create.go | 2 +- pkg/cmd/server/server.go | 2 +- pkg/cmd/util/flag/map.go | 36 ++++++++----- pkg/cmd/util/flag/map_test.go | 75 ++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/4920-ywk253100 create mode 100644 pkg/cmd/util/flag/map_test.go diff --git a/changelogs/unreleased/4920-ywk253100 b/changelogs/unreleased/4920-ywk253100 new file mode 100644 index 000000000..0867b6935 --- /dev/null +++ b/changelogs/unreleased/4920-ywk253100 @@ -0,0 +1 @@ +Enhance the map flag to support parsing input value contains entry delimiters \ No newline at end of file diff --git a/pkg/cmd/cli/restore/create.go b/pkg/cmd/cli/restore/create.go index caf94dc51..223414d63 100644 --- a/pkg/cmd/cli/restore/create.go +++ b/pkg/cmd/cli/restore/create.go @@ -98,7 +98,7 @@ func NewCreateOptions() *CreateOptions { return &CreateOptions{ Labels: flag.NewMap(), IncludeNamespaces: flag.NewStringArray("*"), - NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"), + NamespaceMappings: flag.NewMap().WithEntryDelimiter(',').WithKeyValueDelimiter(':'), RestoreVolumes: flag.NewOptionalBool(nil), PreserveNodePorts: flag.NewOptionalBool(nil), IncludeClusterResources: flag.NewOptionalBool(nil), diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 5aaee731c..be59459c1 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -132,7 +132,7 @@ type controllerRunInfo struct { func NewCommand(f client.Factory) *cobra.Command { var ( - volumeSnapshotLocations = flag.NewMap().WithKeyValueDelimiter(":") + volumeSnapshotLocations = flag.NewMap().WithKeyValueDelimiter(':') logLevelFlag = logging.LogLevelFlag(logrus.InfoLevel) config = serverConfig{ pluginDir: "/plugins", diff --git a/pkg/cmd/util/flag/map.go b/pkg/cmd/util/flag/map.go index 4c50b6ee8..1b4a6e21c 100644 --- a/pkg/cmd/util/flag/map.go +++ b/pkg/cmd/util/flag/map.go @@ -17,6 +17,7 @@ limitations under the License. package flag import ( + "encoding/csv" "fmt" "strings" @@ -27,25 +28,25 @@ import ( // map data (i.e. a collection of key-value pairs). type Map struct { data map[string]string - entryDelimiter string - keyValueDelimiter string + entryDelimiter rune + keyValueDelimiter rune } -// NewMap returns a Map using the default delimiters ("=" between keys and -// values, and "," between map entries, e.g. k1=v1,k2=v2) +// NewMap returns a Map using the default delimiters ('=' between keys and +// values, and ',' between map entries, e.g. k1=v1,k2=v2) func NewMap() Map { m := Map{ data: make(map[string]string), } - return m.WithEntryDelimiter(",").WithKeyValueDelimiter("=") + return m.WithEntryDelimiter(',').WithKeyValueDelimiter('=') } // WithEntryDelimiter sets the delimiter to be used between map // entries. // -// For example, in "k1=v1&k2=v2", the entry delimiter is "&" -func (m Map) WithEntryDelimiter(delimiter string) Map { +// For example, in "k1=v1&k2=v2", the entry delimiter is '&' +func (m Map) WithEntryDelimiter(delimiter rune) Map { m.entryDelimiter = delimiter return m } @@ -53,8 +54,8 @@ func (m Map) WithEntryDelimiter(delimiter string) Map { // WithKeyValueDelimiter sets the delimiter to be used between // keys and values. // -// For example, in "k1=v1&k2=v2", the key-value delimiter is "=" -func (m Map) WithKeyValueDelimiter(delimiter string) Map { +// For example, in "k1=v1&k2=v2", the key-value delimiter is '=' +func (m Map) WithKeyValueDelimiter(delimiter rune) Map { m.keyValueDelimiter = delimiter return m } @@ -63,17 +64,26 @@ func (m Map) WithKeyValueDelimiter(delimiter string) Map { func (m *Map) String() string { var a []string for k, v := range m.data { - a = append(a, fmt.Sprintf("%s%s%s", k, m.keyValueDelimiter, v)) + a = append(a, fmt.Sprintf("%s%s%s", k, string(m.keyValueDelimiter), v)) } - return strings.Join(a, m.entryDelimiter) + return strings.Join(a, string(m.entryDelimiter)) } // Set parses the provided string according to the delimiters and // assigns the result to the Map receiver. It returns an error if // the string is not parseable. func (m *Map) Set(s string) error { - for _, part := range strings.Split(s, m.entryDelimiter) { - kvs := strings.SplitN(part, m.keyValueDelimiter, 2) + // use csv.Reader to support parsing input string contains entry delimiters. + // e.g. `"k1=a=b,c=d",k2=v2` will be parsed into two parts: `k1=a=b,c=d` and `k2=v2` + r := csv.NewReader(strings.NewReader(s)) + r.Comma = m.entryDelimiter + parts, err := r.Read() + if err != nil { + return errors.Wrapf(err, "error parsing %q", s) + } + + for _, part := range parts { + kvs := strings.SplitN(part, string(m.keyValueDelimiter), 2) if len(kvs) != 2 { return errors.Errorf("error parsing %q", part) } diff --git a/pkg/cmd/util/flag/map_test.go b/pkg/cmd/util/flag/map_test.go new file mode 100644 index 000000000..621015680 --- /dev/null +++ b/pkg/cmd/util/flag/map_test.go @@ -0,0 +1,75 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flag + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSetOfMap(t *testing.T) { + cases := []struct { + name string + input string + error bool + expected map[string]string + }{ + { + name: "invalid_input_missing_quote", + input: `"k=v`, + error: true, + }, + { + name: "invalid_input_contains_no_key_value_delimiter", + input: `k`, + error: true, + }, + { + name: "valid input", + input: `k1=v1,k2=v2`, + error: false, + expected: map[string]string{ + "k1": "v1", + "k2": "v2", + }, + }, + { + name: "valid input whose value contains entry delimiter", + input: `k1=v1,"k2=a=b,c=d"`, + error: false, + expected: map[string]string{ + "k1": "v1", + "k2": "a=b,c=d", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + m := NewMap() + err := m.Set(c.input) + if c.error { + require.NotNil(t, err) + return + } + assert.EqualValues(t, c.expected, m.data) + }) + } + +}