Skip to content
Closed
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
19 changes: 18 additions & 1 deletion value_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"fmt"
"os"
"reflect"
"strings"
)

Expand Down Expand Up @@ -253,6 +254,22 @@ func (mvs *mapValueSource) Lookup() (string, bool) {
if v, ok := mvs.ms.Lookup(mvs.key); !ok {
return "", false
} else {
return fmt.Sprintf("%+v", v), true
return mapValueSourceString(v), true
}
}

// mapValueSourceString converts a looked-up value to the string form
// consumed by flag values. Map values are serialized through the same
// path as StringMap Serialize so they round-trip via MapBase.Set,
// while scalars and slices keep their historical formatting.
func mapValueSourceString(v any) string {
if val := reflect.ValueOf(v); val.IsValid() && val.Kind() == reflect.Map {
stringMap := make(map[string]string, val.Len())
iter := val.MapRange()
for iter.Next() {
stringMap[fmt.Sprintf("%v", iter.Key().Interface())] = fmt.Sprintf("%v", iter.Value().Interface())
}
return NewStringMap(stringMap).Serialize()
}
return fmt.Sprintf("%+v", v)
}
82 changes: 82 additions & 0 deletions value_source_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"fmt"
"math/rand"
"os"
Expand Down Expand Up @@ -335,6 +336,87 @@ func TestMapValueSource(t *testing.T) {
}
}

func TestMapValueSourceStringMap(t *testing.T) {
// Regression test for https://github.com/urfave/cli/issues/1634:
// map-sourced values must round-trip through StringMap parsing
// instead of Go-syntax "%+v" which MapBase.Set rejects.
tests := []struct {
name string
m map[any]any
key string
want map[string]string
found bool
}{
{
name: "map any any",
m: map[any]any{
"labels": map[any]any{"env": "prod", "team": "platform"},
},
key: "labels",
want: map[string]string{"env": "prod", "team": "platform"},
found: true,
},
{
name: "map string any",
m: map[any]any{
"labels": map[string]any{"env": "prod", "team": "platform"},
},
key: "labels",
want: map[string]string{"env": "prod", "team": "platform"},
found: true,
},
{
name: "map string string",
m: map[any]any{
"labels": map[string]string{"env": "prod", "team": "platform"},
},
key: "labels",
want: map[string]string{"env": "prod", "team": "platform"},
found: true,
},
{
name: "missing key",
m: map[any]any{
"labels": map[any]any{"env": "prod"},
},
key: "other",
found: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ms := NewMapSource("test", test.m)
m := NewMapValueSource(test.key, ms)
val, ok := m.Lookup()
if !test.found {
assert.False(t, ok)
return
}
require.True(t, ok)
sm := NewStringMap(nil)
require.NoError(t, sm.Set(val))
assert.Equal(t, test.want, sm.Value())
})
}
}

func TestStringMapFlagFromMapSource(t *testing.T) {
ms := NewMapSource("test", map[any]any{
"labels": map[any]any{"env": "prod", "team": "platform"},
})
cmd := &Command{
Flags: []Flag{
&StringMapFlag{Name: "labels", Sources: NewValueSourceChain(NewMapValueSource("labels", ms))},
},
Action: func(_ context.Context, cmd *Command) error {
assert.Equal(t, map[string]string{"env": "prod", "team": "platform"}, cmd.StringMap("labels"))
return nil
},
}
require.NoError(t, cmd.Run(buildTestContext(t), []string{"app"}))
}

func TestMapValueSourceStringer(t *testing.T) {
m := map[any]any{
"foo": map[any]any{
Expand Down
Loading