From f6b9981a336cca8b9e122fc82ef27ece8a1ab915 Mon Sep 17 00:00:00 2001 From: Manish Tiwari <69393822+manish-9245@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:30:46 +0530 Subject: [PATCH] fix: support StringMap values from map value sources (#1634) Map value source Lookup stringified map values with Go-syntax fmt.Sprintf("%+v"), e.g. map[env:prod], which MapBase.Set rejects with missing separator "=". Serialize map values through the same NewStringMap(...).Serialize() path used for flag values so they round-trip, while preserving scalar and slice formatting. Fixes #1634. --- value_source.go | 19 +++++++++- value_source_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/value_source.go b/value_source.go index 2dcee75870..1ce1267f41 100644 --- a/value_source.go +++ b/value_source.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "reflect" "strings" ) @@ -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) +} diff --git a/value_source_test.go b/value_source_test.go index 3aca91b824..29ce455fd8 100644 --- a/value_source_test.go +++ b/value_source_test.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "math/rand" "os" @@ -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{