Repository: spf13/viper (Go)

Issue:
StringToString pflag values are not handled correctly by viper
I bind a StringToString pflag like so
```go
RootCmd.PersistentFlags().StringToStringP(key, conf.alias, defaultValue, conf.usage)
viper.BindPFlag(key, RootCmd.PersistentFlags().Lookup(key))
```

Then retrieve the value from my viper with
```go
smap := viper.GetStringMapString(key)
```
The string map returned is an empty map, which seems to be the default value returned on error by spf13/cast.

I expected to get a populated string map back.




Discussion:
This issue seems related to what was seen in https://github.com/spf13/viper/issues/200
It looks like viper is fixed, can someone update viper?
https://github.com/spf13/viper/issues/200
Created a PR, https://github.com/sensu/sensu-go/pull/2867
I made a demo of this issue: https://github.com/kd7lxl/viper-demo
it seems that in v1.6.2 this issue is fixed
I tried it @danmx and still doesn't work.  Using `filters = viper.GetStringMapString("filter")` with `--filter name=test` outputs `"DEBUG filterArgs = {map[]}"`. 
@danmx @amir20 I've submitted a pr with a fix for this issue. Hopefully someone will have a look at some point.
@terev here you have how the input works:
`key_a=value_1,key_b=value_2`
https://github.com/spf13/pflag/blob/d929dcb/string_to_string_test.go#L143-L144

@amir20 did you add this to your code:
```go
serverCmd.Flags().Int("filter", map[string]string{}, "desc filter")
if err := viper.BindPFlag("filter", serverCmd.Flags().Lookup("filter")); err != nil {
    return err
}
```
https://github.com/spf13/viper#working-with-flags

The issue still persists in v1.6.2.

This:
```go
f := pflag.NewFlagSet("", pflag.ContinueOnError)
f.StringToString("filter", map[string]string{}, "desc filter")
_ = viper.BindPFlags(f)
...
filters := viper.GetStringMapString("filter")
fmt.Println(filters)
```
prints `map[]`.

While using just `GetString`:
```go
filters := viper.GetString("filter")
```
prints `[a=0]`.
@matevz is correct here. This is still broken. 

@danmx Not sure why you have an `Int` for pflags. You need to use `StringToString` to reproduce this. Can you try @matevz code? I get `map[]` too. Maybe you are doing something different. 
@amir20 that was a poor copy-paste of code snippets from my tool.

I wrote a simple cli just to test it and you're right also now I know that I have a bug in my tool 🤦‍♂

here's the code if someone what's to quickly reproduce it:
```go
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var (
	cfg     *viper.Viper
	rootCmd = &cobra.Command{
		PreRunE: func(cmd *cobra.Command, args []string) error {
			if err := cfg.BindPFlag("test1", cmd.Flags().Lookup("test1")); err != nil {
				return err
			}
			if err := cfg.BindPFlag("test2", cmd.Flags().Lookup("test2")); err != nil {
				return err
			}
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			a := cfg.GetStringMapString("test1")
			fmt.Printf("GetStringMapString - T: %T, v: %v, s: %s\n", a, a, a)
			b := cfg.GetStringMap("test1")
			fmt.Printf("GetStringMap - T: %T, v: %v, s: %s\n", b, b, b)
			c := cfg.Get("test1")
			fmt.Printf("Get - T: %T, v: %v, s: %s\n", c, c, c)
			arg2 := cfg.GetString("test2")
			fmt.Printf("GetString - T: %T, v: %v, s: %s\n", arg2, arg2, arg2)
			return nil
		},
	}
)

func init() {
	cfg = viper.GetViper()
	rootCmd.Flags().StringToString("test1", map[string]string{"default_key": "default_value"}, "test description")
	rootCmd.Flags().String("test2", "default_string", "test description")
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1) //nolint:gomnd
	}
}
```

```console
$ go build -o main

$ ./main --test1 a=b,c=d
GetStringMapString - T: map[string]string, v: map[], s: map[]
GetStringMap - T: map[string]interface {}, v: map[], s: map[]
Get - T: string, v: [c=d,a=b], s: [c=d,a=b]
GetString - T: string, v: default_string, s: default_string
```
@amir20 @danmx if you wouldn't mind could you try my fork? i linked the pr above
```console
$ ./main --test1 a=b,c=d
GetStringMapString - T: map[string]string, v: map[a:b c:d], s: map[a:b c:d]
GetStringMap - T: map[string]interface {}, v: map[], s: map[]
Get - T: map[string]string, v: map[a:b c:d], s: map[a:b c:d]
GetString - T: string, v: default_string, s: default_string
```
@terev it seems to work only for `GetStringMapString`
@danmx to me that output looks like Get also worked
@terev you're right 😂 I only glimpsed over `GetStringMap` (which is still broken) and `GetStringMapString`
@amir20 @danmx pushed some changes https://github.com/spf13/viper/pull/874. I was able to get GetStringMap working, 
```
GetStringMapString - T: map[string]string, v: map[a:b c:d], s: map[a:b c:d]
GetStringMap - T: map[string]interface {}, v: map[a:b c:d], s: map[a:b c:d]
Get - T: map[string]interface {}, v: map[a:b c:d], s: map[a:b c:d]
GetString - T: string, v: default_string, s: default_string
```
This _still_ doesn't work. :( @sagikazarmark I tried testing with `EXAMPLE_FOO=bar=test go run main.go` and it still fails. 

After reading and using debugger, I see that the code eventually gets to https://github.com/spf13/cast/blob/master/cast.go#L108-L110.

```
func ToStringMapString(i interface{}) map[string]string {
	v, _ := ToStringMapStringE(i)
	return v
}
```

`ToStringMapStringE` returns an error because it cannot cast `bar=test` using JSON. :/ 

I can create a new bug or reopen this. 

@amir20 This issue (and the fix) is for pflag values, not env vars. So I think it should definitely be a new feature request.
Interesting. That's confusing. This whole time I thought `viper.GetStringMapString(key)` is going to fix both env and pFalgs use case. I'd argue that as a developer it shouldn't be a different feature to know the source. 

Regardless, not a big deal. I'll create a new bug. 



Patch:
diff --git a/viper.go b/viper.go
index 7b12b36e9..65641f665 100644
--- a/viper.go
+++ b/viper.go
@@ -1083,6 +1083,8 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
 			s = strings.TrimSuffix(s, "]")
 			res, _ := readAsCSV(s)
 			return cast.ToIntSlice(res)
+		case "stringToString":
+			return stringToStringConv(flag.ValueString())
 		default:
 			return flag.ValueString()
 		}
@@ -1158,6 +1160,8 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
 				s = strings.TrimSuffix(s, "]")
 				res, _ := readAsCSV(s)
 				return cast.ToIntSlice(res)
+			case "stringToString":
+				return stringToStringConv(flag.ValueString())
 			default:
 				return flag.ValueString()
 			}
@@ -1177,6 +1181,30 @@ func readAsCSV(val string) ([]string, error) {
 	return csvReader.Read()
 }
 
+// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/master/string_to_string.go#L79
+// alterations are: errors are swallowed, map[string]interface{} is returned in order to enable cast.ToStringMap
+func stringToStringConv(val string) interface{} {
+	val = strings.Trim(val, "[]")
+	// An empty string would cause an empty map
+	if len(val) == 0 {
+		return map[string]interface{}{}
+	}
+	r := csv.NewReader(strings.NewReader(val))
+	ss, err := r.Read()
+	if err != nil {
+		return nil
+	}
+	out := make(map[string]interface{}, len(ss))
+	for _, pair := range ss {
+		kv := strings.SplitN(pair, "=", 2)
+		if len(kv) != 2 {
+			return nil
+		}
+		out[kv[0]] = kv[1]
+	}
+	return out
+}
+
 // IsSet checks to see if the key has been set in any of the data locations.
 // IsSet is case-insensitive for a key.
 func IsSet(key string) bool { return v.IsSet(key) }


Test patch:
diff --git a/viper_test.go b/viper_test.go
index b8ceccba5..fe942de29 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -970,6 +970,53 @@ func TestBindPFlag(t *testing.T) {
 	assert.Equal(t, "testing_mutate", Get("testvalue"))
 }
 
+func TestBindPFlagStringToString(t *testing.T) {
+	tests := []struct {
+		Expected map[string]string
+		Value    string
+	}{
+		{map[string]string{}, ""},
+		{map[string]string{"yo": "hi"}, "yo=hi"},
+		{map[string]string{"yo": "hi", "oh": "hi=there"}, "yo=hi,oh=hi=there"},
+		{map[string]string{"yo": ""}, "yo="},
+		{map[string]string{"yo": "", "oh": "hi=there"}, "yo=,oh=hi=there"},
+	}
+
+	v := New() // create independent Viper object
+	defaultVal := map[string]string{}
+	v.SetDefault("stringtostring", defaultVal)
+
+	for _, testValue := range tests {
+		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
+		flagSet.StringToString("stringtostring", testValue.Expected, "test")
+
+		for _, changed := range []bool{true, false} {
+			flagSet.VisitAll(func(f *pflag.Flag) {
+				f.Value.Set(testValue.Value)
+				f.Changed = changed
+			})
+
+			err := v.BindPFlags(flagSet)
+			if err != nil {
+				t.Fatalf("error binding flag set, %v", err)
+			}
+
+			type TestMap struct {
+				StringToString map[string]string
+			}
+			val := &TestMap{}
+			if err := v.Unmarshal(val); err != nil {
+				t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err)
+			}
+			if changed {
+				assert.Equal(t, testValue.Expected, val.StringToString)
+			} else {
+				assert.Equal(t, defaultVal, val.StringToString)
+			}
+		}
+	}
+}
+
 func TestBoundCaseSensitivity(t *testing.T) {
 	assert.Equal(t, "brown", Get("eyes"))