Repository: spf13/viper (go)

Issue:
Error ``Unmarshal`` map when key contains dot
Test code:

```
package main

import (
	"bytes"
	"fmt"
	"github.com/spf13/viper"
)

type Config struct {
	Foo struct {
		Bar map[string]int `mapstructure:"bar"`
	} `mapstructure:"foo"`
}

var content = []byte(`
foo:
  bar:
    "x": 1
    "y": 2
    "z.z": 3
`)

func main() {
	var c1, c2 Config

	viper.SetConfigType("yaml")
	viper.ReadConfig(bytes.NewReader(content))

	viper.Unmarshal(&c1)
	fmt.Println("c1:", c1)

	viper.UnmarshalKey("foo", &c2.Foo)
	fmt.Println("c2:", c2)
}
```

Output:
```
c1: {{map[x:1 y:2]}}
c2: {{map[x:1 y:2 z.z:3]}}
```

As you can see, when the map key contains dot, ``UnmarshalKey`` works correctly but ``Unmarshal`` doesn't.

Pull request:
Unmarshal keys containing dots
Fixes #324 

Includes fix #669

- Replaced the use of `strings.Split` with a method that takes a fully qualified key and returns a list of valid map keys which may include dots in them.

- Reformatted test objects to improve readability and Git change diffing.

- Added key containing dot to yaml test.


Patch:
diff --git a/viper.go b/viper.go
index 5133de7..491799c 100644
--- a/viper.go
+++ b/viper.go
@@ -1789,6 +1789,24 @@ outer:
 	return shadow
 }
 
+// Converts a fully qualified map key into a list of relative
+// map keys, allowing for keys to contain the delimiter themselves
+func keyComponents(v *Viper, key string) []string {
+	var result []string
+	components := strings.Split(key, v.keyDelim)
+	for index := 0; index < len(components); index++ {
+		potentialKey := strings.Join(components[0:index], v.keyDelim)
+		if v.Get(potentialKey) != nil {
+			result = append(result, potentialKey)
+		}
+	}
+	result = append(result, key)
+	for i := len(result) - 1; i > 0; i-- {
+		result[i] = strings.Replace(result[i], result[i-1]+v.keyDelim, "", 1)
+	}
+	return result
+}
+
 // AllSettings merges all settings and returns them as a map[string]interface{}.
 func AllSettings() map[string]interface{} { return v.AllSettings() }
 func (v *Viper) AllSettings() map[string]interface{} {
@@ -1801,7 +1819,7 @@ func (v *Viper) AllSettings() map[string]interface{} {
 			// check just in case anything changes
 			continue
 		}
-		path := strings.Split(k, v.keyDelim)
+		path := keyComponents(v, k)
 		lastKey := strings.ToLower(path[len(path)-1])
 		deepestMap := deepSearch(m, path[0:len(path)-1])
 		// set innermost value


Test patch:
diff --git a/viper_test.go b/viper_test.go
index 592551c..3c15ed2 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -13,6 +13,7 @@ import (
 	"os"
 	"os/exec"
 	"path"
+	"path/filepath"
 	"reflect"
 	"runtime"
 	"sort"
@@ -45,6 +46,10 @@ clothing:
 age: 35
 eyes : brown
 beard: true
+emails:
+  steve@hacker.com:
+    created: 01/02/03
+    active: true
 `)
 
 var yamlExampleWithExtras = []byte(`Existing: true
@@ -207,11 +212,16 @@ func initHcl() {
 func initDirs(t *testing.T) (string, string, func()) {
 
 	var (
-		testDirs = []string{`a a`, `b`, `c\c`, `D_`}
+		testDirs = []string{`a a`, `b`, `C_`}
 		config   = `improbable`
 	)
 
+	if runtime.GOOS != "windows" {
+		testDirs = append(testDirs, `d\d`)
+	}
+
 	root, err := ioutil.TempDir("", "")
+	require.NoError(t, err, "Failed to create temporary directory")
 
 	cleanup := true
 	defer func() {
@@ -224,7 +234,7 @@ func initDirs(t *testing.T) (string, string, func()) {
 	assert.Nil(t, err)
 
 	err = os.Chdir(root)
-	assert.Nil(t, err)
+	require.Nil(t, err)
 
 	for _, dir := range testDirs {
 		err = os.Mkdir(dir, 0750)
@@ -415,7 +425,10 @@ func TestEmptyEnv(t *testing.T) {
 	BindEnv("type") // Empty environment variable
 	BindEnv("name") // Bound, but not set environment variable
 
-	os.Clearenv()
+	os.Unsetenv("type")
+	os.Unsetenv("TYPE")
+	os.Unsetenv("name")
+	os.Unsetenv("NAME")
 
 	os.Setenv("TYPE", "")
 
@@ -431,7 +444,10 @@ func TestEmptyEnv_Allowed(t *testing.T) {
 	BindEnv("type") // Empty environment variable
 	BindEnv("name") // Bound, but not set environment variable
 
-	os.Clearenv()
+	os.Unsetenv("type")
+	os.Unsetenv("TYPE")
+	os.Unsetenv("name")
+	os.Unsetenv("NAME")
 
 	os.Setenv("TYPE", "")
 
@@ -491,11 +507,98 @@ func TestSetEnvKeyReplacer(t *testing.T) {
 func TestAllKeys(t *testing.T) {
 	initConfigs()
 
-	ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos",
-		"title_dotenv", "type_dotenv", "name_dotenv",
+	ks := sort.StringSlice{
+		"title",
+		"newkey",
+		"owner.organization",
+		"owner.dob",
+		"owner.bio",
+		"name",
+		"beard",
+		"ppu",
+		"batters.batter",
+		"hobbies",
+		"clothing.jacket",
+		"clothing.trousers",
+		"clothing.pants.size",
+		"age",
+		"hacker",
+		"id",
+		"type",
+		"eyes",
+		"p_id",
+		"p_ppu",
+		"p_batters.batter.type",
+		"p_type",
+		"p_name",
+		"foos",
+		"title_dotenv",
+		"type_dotenv",
+		"name_dotenv",
+		"emails.steve@hacker.com.active",
+		"emails.steve@hacker.com.created",
 	}
 	dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
-	all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}, "title_dotenv": "DotEnv Example", "type_dotenv": "donut", "name_dotenv": "Cake"}
+	all := map[string]interface{}{
+		"owner": map[string]interface{}{
+			"organization": "MongoDB",
+			"bio":          "MongoDB Chief Developer Advocate & Hacker at Large",
+			"dob":          dob,
+		},
+		"title": "TOML Example",
+		"ppu":   0.55,
+		"emails": map[string]interface{}{
+			"steve@hacker.com": map[string]interface{}{
+				"active":  true,
+				"created": "01/02/03",
+			},
+		},
+		"eyes": "brown",
+		"clothing": map[string]interface{}{
+			"trousers": "denim",
+			"jacket":   "leather",
+			"pants":    map[string]interface{}{"size": "large"},
+		},
+		"id": "0001",
+		"batters": map[string]interface{}{
+			"batter": []interface{}{
+				map[string]interface{}{"type": "Regular"},
+				map[string]interface{}{"type": "Chocolate"},
+				map[string]interface{}{"type": "Blueberry"},
+				map[string]interface{}{"type": "Devil's Food"},
+			},
+		},
+		"hacker": true,
+		"beard":  true,
+		"hobbies": []interface{}{
+			"skateboarding",
+			"snowboarding",
+			"go",
+		},
+		"age":    35,
+		"type":   "donut",
+		"newkey": "remote",
+		"name":   "Cake",
+		"p_id":   "0001",
+		"p_ppu":  "0.55",
+		"p_name": "Cake",
+		"p_batters": map[string]interface{}{
+			"batter": map[string]interface{}{"type": "Regular"},
+		},
+		"p_type": "donut",
+		"foos": []map[string]interface{}{
+			{
+				"foo": []map[string]interface{}{
+					{"key": 1},
+					{"key": 2},
+					{"key": 3},
+					{"key": 4}},
+			},
+		},
+		"title_dotenv": "DotEnv Example",
+		"type_dotenv":  "donut",
+		"name_dotenv":  "Cake",
+	}
 
 	allkeys := sort.StringSlice(AllKeys())
 	allkeys.Sort()
@@ -960,7 +1063,7 @@ func TestDirsSearch(t *testing.T) {
 	err = v.ReadInConfig()
 	assert.Nil(t, err)
 
-	assert.Equal(t, `value is `+path.Base(v.configPaths[0]), v.GetString(`key`))
+	assert.Equal(t, `value is `+filepath.Base(v.configPaths[0]), v.GetString(`key`))
 }
 
 func TestWrongDirsSearchNotFound(t *testing.T) {
@@ -1213,6 +1316,10 @@ clothing:
   pants:
     size: large
   trousers: denim
+emails:
+  steve@hacker.com:
+    active: true
+    created: 01/02/03
 eyes: brown
 hacker: true
 hobbies: