Repository: JanDeDobbeleer/oh-my-posh
Pull request #1604 (2022-01-18 12:52:46)

template cache
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

- feat: cache template environment data
- fix(template): support surrounding variables with braces


<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/environment.go b/src/environment.go
index e18e2fde..cb4ee4ea 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -90,7 +90,6 @@ type wifiInfo struct {
 
 type Environment interface {
 	getenv(key string) string
-	environ() map[string]string
 	getcwd() string
 	homeDir() string
 	hasFiles(pattern string) bool
@@ -128,6 +127,7 @@ type Environment interface {
 	convertToLinuxPath(path string) string
 	convertToWindowsPath(path string) string
 	getWifiNetwork() (*wifiInfo, error)
+	templateCache() *templateCache
 }
 
 type commandCache struct {
@@ -155,13 +155,13 @@ const (
 )
 
 type environment struct {
-	args         *args
-	cwd          string
-	cmdCache     *commandCache
-	fileCache    *fileCache
-	environCache map[string]string
-	logBuilder   strings.Builder
-	debug        bool
+	args       *args
+	cwd        string
+	cmdCache   *commandCache
+	fileCache  *fileCache
+	tmplCache  *templateCache
+	logBuilder strings.Builder
+	debug      bool
 }
 
 func (env *environment) init(args *args) {
@@ -176,18 +176,6 @@ func (env *environment) init(args *args) {
 	}
 	env.fileCache = &fileCache{}
 	env.fileCache.init(env.getCachePath())
-	env.environCache = make(map[string]string)
-	const separator = "="
-	values := os.Environ()
-	for value := range values {
-		splitted := strings.Split(values[value], separator)
-		if len(splitted) != 2 {
-			continue
-		}
-		key := splitted[0]
-		val := splitted[1:]
-		env.environCache[key] = strings.Join(val, separator)
-	}
 }
 
 func (env *environment) resolveConfigPath() {
@@ -236,11 +224,6 @@ func (env *environment) getenv(key string) string {
 	return val
 }
 
-func (env *environment) environ() map[string]string {
-	defer env.trace(time.Now(), "environ")
-	return env.environCache
-}
-
 func (env *environment) getcwd() string {
 	defer env.trace(time.Now(), "getcwd")
 	if env.cwd != "" {
@@ -563,6 +546,42 @@ func (env *environment) logs() string {
 	return env.logBuilder.String()
 }
 
+func (env *environment) templateCache() *templateCache {
+	defer env.trace(time.Now(), "templateCache")
+	if env.tmplCache != nil {
+		return env.tmplCache
+	}
+	tmplCache := &templateCache{
+		Root:  env.isRunningAsRoot(),
+		Shell: env.getShellName(),
+		Code:  env.lastErrorCode(),
+	}
+	tmplCache.Env = make(map[string]string)
+	const separator = "="
+	values := os.Environ()
+	for value := range values {
+		splitted := strings.Split(values[value], separator)
+		if len(splitted) != 2 {
+			continue
+		}
+		key := splitted[0]
+		val := splitted[1:]
+		tmplCache.Env[key] = strings.Join(val, separator)
+	}
+	pwd := env.getcwd()
+	pwd = strings.Replace(pwd, env.homeDir(), "~", 1)
+	tmplCache.PWD = pwd
+	tmplCache.Folder = base(pwd, env)
+	tmplCache.UserName = env.getCurrentUser()
+	if host, err := env.getHostName(); err == nil {
+		tmplCache.HostName = host
+	}
+	goos := env.getRuntimeGOOS()
+	tmplCache.OS = goos
+	env.tmplCache = tmplCache
+	return env.tmplCache
+}
+
 func cleanHostName(hostName string) string {
 	garbage := []string{
 		".lan",
diff --git a/src/environment_unix.go b/src/environment_unix.go
index b05325b1..d2660e74 100644
--- a/src/environment_unix.go
+++ b/src/environment_unix.go
@@ -53,6 +53,9 @@ func (env *environment) getTerminalWidth() (int, error) {
 }
 
 func (env *environment) getPlatform() string {
+	if wsl := env.getenv("WSL_DISTRO_NAME"); len(wsl) != 0 {
+		return strings.ToLower(wsl)
+	}
 	p, _, _, _ := host.PlatformInformation()
 	return p
 }
diff --git a/src/main.go b/src/main.go
index bb7a4ed3..e4f5519c 100644
--- a/src/main.go
+++ b/src/main.go
@@ -336,18 +336,9 @@ func getConsoleBackgroundColor(env Environment, backgroundColorTemplate string)
 	if len(backgroundColorTemplate) == 0 {
 		return backgroundColorTemplate
 	}
-	context := struct {
-		Env map[string]string
-	}{
-		Env: map[string]string{},
-	}
-	matches := findAllNamedRegexMatch(templateEnvRegex, backgroundColorTemplate)
-	for _, match := range matches {
-		context.Env[match["ENV"]] = env.getenv(match["ENV"])
-	}
 	template := &textTemplate{
 		Template: backgroundColorTemplate,
-		Context:  context,
+		Context:  nil,
 		Env:      env,
 	}
 	text, err := template.render()
diff --git a/src/segment_os.go b/src/segment_os.go
index d60c96ad..19d0186e 100644
--- a/src/segment_os.go
+++ b/src/segment_os.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"fmt"
-	"strings"
 )
 
 type osInfo struct {
@@ -79,17 +78,14 @@ func (n *osInfo) string() string {
 		n.os = darwinPlatform
 		return n.props.getString(MacOS, "\uF179")
 	case linuxPlatform:
-		wsl := n.env.getenv("WSL_DISTRO_NAME")
-		p := n.env.getPlatform()
-		if len(wsl) == 0 {
-			n.os = p
-			return n.getDistroName(p, "")
+		n.os = n.env.getPlatform()
+		if !n.env.isWsl() {
+			return n.getDistroName(n.os, "")
 		}
-		n.os = strings.ToLower(wsl)
 		return fmt.Sprintf("%s%s%s",
 			n.props.getString(WSL, "WSL"),
 			n.props.getString(WSLSeparator, " - "),
-			n.getDistroName(p, wsl))
+			n.getDistroName(n.os, n.os))
 	default:
 		n.os = goos
 		return goos
diff --git a/src/template.go b/src/template.go
index 05b3fe21..e74e02e6 100644
--- a/src/template.go
+++ b/src/template.go
@@ -12,9 +12,6 @@ const (
 	// Errors to show when the template handling fails
 	invalidTemplate   = "invalid template text"
 	incorrectTemplate = "unable to create text based on template"
-	// nostruct          = "unable to create map from non-struct type"
-
-	templateEnvRegex = `\.Env\.(?P<ENV>[^ \.}]*)`
 )
 
 type textTemplate struct {
@@ -26,6 +23,13 @@ type textTemplate struct {
 type Data interface{}
 
 type Context struct {
+	templateCache
+
+	// Simple container to hold ANY object
+	Data
+}
+
+type templateCache struct {
 	Root     bool
 	PWD      string
 	Folder   string
@@ -35,37 +39,14 @@ type Context struct {
 	Code     int
 	Env      map[string]string
 	OS       string
-
-	// Simple container to hold ANY object
-	Data
 }
 
 func (c *Context) init(t *textTemplate) {
 	c.Data = t.Context
-	if t.Env == nil {
+	if cache := t.Env.templateCache(); cache != nil {
+		c.templateCache = *cache
 		return
 	}
-	c.Root = t.Env.isRunningAsRoot()
-	pwd := t.Env.getcwd()
-	pwd = strings.Replace(pwd, t.Env.homeDir(), "~", 1)
-	c.PWD = pwd
-	c.Folder = base(c.PWD, t.Env)
-	c.Shell = t.Env.getShellName()
-	c.UserName = t.Env.getCurrentUser()
-	if host, err := t.Env.getHostName(); err == nil {
-		c.HostName = host
-	}
-	c.Code = t.Env.lastErrorCode()
-	c.Env = t.Env.environ()
-	goos := t.Env.getRuntimeGOOS()
-	if goos == linuxPlatform {
-		wsl := t.Env.getenv("WSL_DISTRO_NAME")
-		goos = t.Env.getPlatform()
-		if len(wsl) != 0 {
-			goos = strings.ToLower(wsl)
-		}
-	}
-	c.OS = goos
 }
 
 func (t *textTemplate) render() (string, error) {
@@ -105,7 +86,7 @@ func (t *textTemplate) cleanTemplate() {
 		return splitted[0], true
 	}
 	knownVariables := []string{"Root", "PWD", "Folder", "Shell", "UserName", "HostName", "Env", "Data", "Code", "OS"}
-	matches := findAllNamedRegexMatch(`(?: |{)(?P<var>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
+	matches := findAllNamedRegexMatch(`(?: |{|\()(?P<var>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
 	for _, match := range matches {
 		if variable, OK := unknownVariable(match["var"], &knownVariables); OK {
 			pattern := fmt.Sprintf(`\.%s\b`, variable)


Test patch:
diff --git a/src/console_title_test.go b/src/console_title_test.go
index 34708d28..711d94db 100644
--- a/src/console_title_test.go
+++ b/src/console_title_test.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -57,11 +56,17 @@ func TestGetConsoleTitle(t *testing.T) {
 		env.On("getcwd").Return(tc.Cwd)
 		env.On("homeDir").Return("/usr/home")
 		env.On("getPathSeperator").Return(tc.PathSeperator)
-		env.On("isRunningAsRoot").Return(tc.Root)
-		env.On("getShellName").Return(tc.ShellName)
-		env.On("getenv", "USERDOMAIN").Return("MyCompany")
-		env.On("getCurrentUser").Return("MyUser")
-		env.On("getHostName").Return("MyHost", nil)
+		env.On("templateCache").Return(&templateCache{
+			Env: map[string]string{
+				"USERDOMAIN": "MyCompany",
+			},
+			Shell:    tc.ShellName,
+			UserName: "MyUser",
+			Root:     tc.Root,
+			HostName: "MyHost",
+			PWD:      tc.Cwd,
+			Folder:   base(tc.Cwd, env),
+		})
 		env.onTemplate()
 		ansi := &ansiUtils{}
 		ansi.init(tc.ShellName)
@@ -112,12 +117,15 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
 		env := new(MockedEnvironment)
 		env.On("getcwd").Return(tc.Cwd)
 		env.On("homeDir").Return("/usr/home")
-		env.On("getPathSeperator").Return(tc.PathSeperator)
-		env.On("isRunningAsRoot").Return(tc.Root)
-		env.On("getShellName").Return(tc.ShellName)
-		env.On("getenv", "USERDOMAIN").Return("MyCompany")
-		env.On("getCurrentUser").Return("MyUser")
-		env.On("getHostName").Return("", fmt.Errorf("I have a bad feeling about this"))
+		env.On("templateCache").Return(&templateCache{
+			Env: map[string]string{
+				"USERDOMAIN": "MyCompany",
+			},
+			Shell:    tc.ShellName,
+			UserName: "MyUser",
+			Root:     tc.Root,
+			HostName: "",
+		})
 		env.onTemplate()
 		ansi := &ansiUtils{}
 		ansi.init(tc.ShellName)
diff --git a/src/main_test.go b/src/main_test.go
index ae4e6f28..89b6264e 100644
--- a/src/main_test.go
+++ b/src/main_test.go
@@ -18,7 +18,11 @@ func TestConsoleBackgroundColorTemplate(t *testing.T) {
 
 	for _, tc := range cases {
 		env := new(MockedEnvironment)
-		env.On("getenv", "TERM_PROGRAM").Return(tc.Term)
+		env.On("templateCache").Return(&templateCache{
+			Env: map[string]string{
+				"TERM_PROGRAM": tc.Term,
+			},
+		})
 		env.onTemplate()
 		color := getConsoleBackgroundColor(env, "{{ if eq \"vscode\" .Env.TERM_PROGRAM }}#123456{{end}}")
 		assert.Equal(t, tc.Expected, color, tc.Case)
diff --git a/src/segment_battery_test.go b/src/segment_battery_test.go
index 35c14db7..68e131e5 100644
--- a/src/segment_battery_test.go
+++ b/src/segment_battery_test.go
@@ -70,6 +70,8 @@ func TestGetBatteryColors(t *testing.T) {
 		},
 	}
 	for _, tc := range cases {
+		env := new(MockedEnvironment)
+		env.onTemplate()
 		batt := &batt{
 			Percentage: tc.Percentage,
 		}
@@ -78,6 +80,7 @@ func TestGetBatteryColors(t *testing.T) {
 		}
 		segment := &Segment{
 			writer: batt,
+			env:    env,
 		}
 		segment.Foreground = tc.DefaultColor
 		segment.ForegroundTemplates = tc.Templates
diff --git a/src/segment_exit_test.go b/src/segment_exit_test.go
index b1b3aa56..f8e39249 100644
--- a/src/segment_exit_test.go
+++ b/src/segment_exit_test.go
@@ -77,6 +77,9 @@ func TestExitWriterTemplateString(t *testing.T) {
 	for _, tc := range cases {
 		env := new(MockedEnvironment)
 		env.On("lastErrorCode").Return(tc.ExitCode)
+		env.On("templateCache").Return(&templateCache{
+			Code: tc.ExitCode,
+		})
 		env.onTemplate()
 		props := properties{
 			SegmentTemplate: tc.Template,
diff --git a/src/segment_git_test.go b/src/segment_git_test.go
index d98a06cd..532dbeb8 100644
--- a/src/segment_git_test.go
+++ b/src/segment_git_test.go
@@ -738,6 +738,9 @@ func TestGitTemplateString(t *testing.T) {
 		props := properties{
 			FetchStatus: true,
 		}
+		env := new(MockedEnvironment)
+		env.onTemplate()
+		tc.Git.env = env
 		tc.Git.props = props
 		assert.Equal(t, tc.Expected, tc.Git.templateString(tc.Template), tc.Case)
 	}
diff --git a/src/segment_os_test.go b/src/segment_os_test.go
index d074e946..a2292da0 100644
--- a/src/segment_os_test.go
+++ b/src/segment_os_test.go
@@ -11,7 +11,7 @@ func TestOSInfo(t *testing.T) {
 		Case              string
 		ExpectedString    string
 		GOOS              string
-		WSLDistro         string
+		IsWSL             bool
 		Platform          string
 		DisplayDistroName bool
 	}{
@@ -19,14 +19,14 @@ func TestOSInfo(t *testing.T) {
 			Case:           "WSL debian - icon",
 			ExpectedString: "WSL at \uf306",
 			GOOS:           "linux",
-			WSLDistro:      "debian",
+			IsWSL:          true,
 			Platform:       "debian",
 		},
 		{
 			Case:              "WSL debian - name",
-			ExpectedString:    "WSL at burps",
+			ExpectedString:    "WSL at debian",
 			GOOS:              "linux",
-			WSLDistro:         "burps",
+			IsWSL:             true,
 			Platform:          "debian",
 			DisplayDistroName: true,
 		},
@@ -62,7 +62,7 @@ func TestOSInfo(t *testing.T) {
 	for _, tc := range cases {
 		env := new(MockedEnvironment)
 		env.On("getRuntimeGOOS").Return(tc.GOOS)
-		env.On("getenv", "WSL_DISTRO_NAME").Return(tc.WSLDistro)
+		env.On("isWsl").Return(tc.IsWSL)
 		env.On("getPlatform").Return(tc.Platform)
 		osInfo := &osInfo{
 			env: env,
@@ -75,9 +75,7 @@ func TestOSInfo(t *testing.T) {
 			},
 		}
 		assert.Equal(t, tc.ExpectedString, osInfo.string(), tc.Case)
-		if tc.WSLDistro != "" {
-			assert.Equal(t, tc.WSLDistro, osInfo.os, tc.Case)
-		} else if tc.Platform != "" {
+		if tc.Platform != "" {
 			assert.Equal(t, tc.Platform, osInfo.os, tc.Case)
 		} else {
 			assert.Equal(t, tc.GOOS, osInfo.os, tc.Case)
diff --git a/src/segment_path_test.go b/src/segment_path_test.go
index 0ede23d9..7ce962d6 100644
--- a/src/segment_path_test.go
+++ b/src/segment_path_test.go
@@ -19,11 +19,6 @@ func (env *MockedEnvironment) getenv(key string) string {
 	return args.String(0)
 }
 
-func (env *MockedEnvironment) environ() map[string]string {
-	args := env.Called()
-	return args.Get(0).(map[string]string)
-}
-
 func (env *MockedEnvironment) getcwd() string {
 	args := env.Called()
 	return args.String(0)
@@ -208,38 +203,20 @@ func (env *MockedEnvironment) getWifiNetwork() (*wifiInfo, error) {
 	return args.Get(0).(*wifiInfo), args.Error(1)
 }
 
+func (env *MockedEnvironment) templateCache() *templateCache {
+	args := env.Called()
+	return args.Get(0).(*templateCache)
+}
+
 func (env *MockedEnvironment) onTemplate() {
-	patchMethodIfNotSpecified := func(method string, returnArguments ...interface{}) {
-		for _, call := range env.Mock.ExpectedCalls {
-			if call.Method == method {
-				return
-			}
+	for _, call := range env.Mock.ExpectedCalls {
+		if call.Method == "templateCache" {
+			return
 		}
-		env.On(method).Return(returnArguments...)
-	}
-	patchEnvVars := func() map[string]string {
-		keyValueArray := make(map[string]string)
-		for _, call := range env.Mock.ExpectedCalls {
-			if call.Method == "getenv" {
-				keyValueArray[call.Arguments.String(0)] = call.ReturnArguments.String(0)
-			}
-		}
-		return keyValueArray
-	}
-	patchMethodIfNotSpecified("isRunningAsRoot", false)
-	patchMethodIfNotSpecified("getcwd", "/usr/home/dev/my-app")
-	patchMethodIfNotSpecified("homeDir", "/usr/home/dev")
-	patchMethodIfNotSpecified("getPathSeperator", "/")
-	patchMethodIfNotSpecified("getShellName", "pwsh")
-	patchMethodIfNotSpecified("getCurrentUser", "dev")
-	patchMethodIfNotSpecified("getHostName", "laptop", nil)
-	patchMethodIfNotSpecified("lastErrorCode", 0)
-	patchMethodIfNotSpecified("getRuntimeGOOS", darwinPlatform)
-	if env.getRuntimeGOOS() == linuxPlatform {
-		env.On("getenv", "WSL_DISTRO_NAME").Return("ubuntu")
-		env.On("getPlatform").Return("ubuntu")
 	}
-	patchMethodIfNotSpecified("environ", patchEnvVars())
+	env.On("templateCache").Return(&templateCache{
+		Env: make(map[string]string),
+	})
 }
 
 const (
diff --git a/src/segment_plastic_test.go b/src/segment_plastic_test.go
index 465bb0c3..df51d4b4 100644
--- a/src/segment_plastic_test.go
+++ b/src/segment_plastic_test.go
@@ -328,6 +328,9 @@ func TestPlasticTemplateString(t *testing.T) {
 			FetchStatus: true,
 		}
 		tc.Plastic.props = props
+		env := new(MockedEnvironment)
+		env.onTemplate()
+		tc.Plastic.env = env
 		assert.Equal(t, tc.Expected, tc.Plastic.templateString(tc.Template), tc.Case)
 	}
 }
diff --git a/src/segment_session_test.go b/src/segment_session_test.go
index 24945ea2..acff040f 100644
--- a/src/segment_session_test.go
+++ b/src/segment_session_test.go
@@ -105,9 +105,17 @@ func TestSessionSegmentTemplate(t *testing.T) {
 		}
 		env.On("getenv", "SSH_CONNECTION").Return(SSHSession)
 		env.On("getenv", "SSH_CLIENT").Return(SSHSession)
-		env.On("isRunningAsRoot").Return(tc.Root)
 		env.On("getenv", defaultUserEnvVar).Return(tc.DefaultUserName)
-		env.onTemplate()
+		env.On("templateCache").Return(&templateCache{
+			UserName: tc.UserName,
+			HostName: tc.ComputerName,
+			Env: map[string]string{
+				"SSH_CONNECTION":  SSHSession,
+				"SSH_CLIENT":      SSHSession,
+				defaultUserEnvVar: tc.DefaultUserName,
+			},
+			Root: tc.Root,
+		})
 		session := &session{
 			env: env,
 			props: properties{
diff --git a/src/segment_test.go b/src/segment_test.go
index 69c7efb0..025011e5 100644
--- a/src/segment_test.go
+++ b/src/segment_test.go
@@ -190,11 +190,14 @@ func TestGetColors(t *testing.T) {
 		},
 	}
 	for _, tc := range cases {
+		env := new(MockedEnvironment)
+		env.onTemplate()
 		segment := &Segment{
 			writer: &aws{
 				Profile: tc.Profile,
 				Region:  tc.Region,
 			},
+			env: env,
 		}
 		if tc.Background {
 			segment.Background = tc.DefaultColor
diff --git a/src/segment_text_test.go b/src/segment_text_test.go
index 09d3804e..8671cf59 100644
--- a/src/segment_text_test.go
+++ b/src/segment_text_test.go
@@ -24,16 +24,18 @@ func TestTextSegment(t *testing.T) {
 
 	for _, tc := range cases {
 		env := new(MockedEnvironment)
-		env.On("getcwd").Return("/usr/home/posh")
-		env.On("homeDir").Return("/usr/home")
 		env.On("getPathSeperator").Return("/")
-		env.On("isRunningAsRoot").Return(true)
-		env.On("getShellName").Return("terminal")
-		env.On("getenv", "HELLO").Return("hello")
-		env.On("getenv", "WORLD").Return("")
-		env.On("getCurrentUser").Return("Posh")
-		env.On("getHostName").Return("MyHost", nil)
-		env.onTemplate()
+		env.On("templateCache").Return(&templateCache{
+			UserName: "Posh",
+			Env: map[string]string{
+				"HELLO": "hello",
+				"WORLD": "",
+			},
+			HostName: "MyHost",
+			Shell:    "terminal",
+			Root:     true,
+			Folder:   base("/usr/home/posh", env),
+		})
 		txt := &text{
 			env: env,
 			props: properties{
diff --git a/src/template_test.go b/src/template_test.go
index c62fd465..5be86c93 100644
--- a/src/template_test.go
+++ b/src/template_test.go
@@ -121,10 +121,9 @@ func TestRenderTemplateEnvVar(t *testing.T) {
 	}
 	for _, tc := range cases {
 		env := &MockedEnvironment{}
-		for name, value := range tc.Env {
-			env.On("getenv", name).Return(value)
-		}
-		env.onTemplate()
+		env.On("templateCache").Return(&templateCache{
+			Env: tc.Env,
+		})
 		template := &textTemplate{
 			Template: tc.Template,
 			Context:  tc.Context,
@@ -175,6 +174,11 @@ func TestCleanTemplate(t *testing.T) {
 			Expected: "{{ .Env.HELLO }} {{ .Data.World }} {{ .Data.World }}",
 			Template: "{{ .Env.HELLO }} {{ .World }} {{ .World }}",
 		},
+		{
+			Case:     "Braces",
+			Expected: "{{ if or (.Data.Working.Changed) (.Data.Staging.Changed) }}#FF9248{{ end }}",
+			Template: "{{ if or (.Working.Changed) (.Staging.Changed) }}#FF9248{{ end }}",
+		},
 	}
 	for _, tc := range cases {
 		template := &textTemplate{