feat: expose environment info to all templates

This commit is contained in:
Jan De Dobbeleer 2022-01-12 23:39:34 +01:00 committed by Jan De Dobbeleer
parent 5426567d77
commit f512df208a
42 changed files with 223 additions and 111 deletions

18
.vscode/launch.json vendored
View file

@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"name": "Debug Prompt",
"type": "go",
"request": "launch",
"mode": "debug",
@ -13,7 +13,7 @@
]
},
{
"name": "Launch Tooltip",
"name": "Debug Tooltip",
"type": "go",
"request": "launch",
"mode": "debug",
@ -24,6 +24,18 @@
"--shell=pwsh"
]
},
{
"name": "Debug Transient",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/src",
"args": [
"--config=/Users/jan/.jandedobbeleer.omp.json",
"--shell=pwsh",
"--print-transient"
]
},
{
"name": "Launch tests",
"type": "go",
@ -84,7 +96,7 @@
"type": "node",
"request": "attach",
"port": 9229,
"preLaunchTask": "func: host start"
"preDebugTask": "func: host start"
}
]
}

View file

@ -34,7 +34,7 @@ Under the hood, this uses go's [text/template][go-text-template] feature extende
offers a few standard properties to work with.
- `.Root`: `boolean` - is the current user root/admin or not
- `.Path`: `string` - the current working directory
- `.PWD`: `string` - the current working directory
- `.Folder`: `string` - the current working folder
- `.Shell`: `string` - the current shell name
- `.User`: `string` - the current user name
@ -53,9 +53,9 @@ the current working directory is `/usr/home/omp` and the shell is `zsh`.
// when root == false: omp :: zsh
// when root == true: omp :: root :: zsh
"console_title_template": "{{.Folder}}", // outputs: omp
"console_title_template": "{{.Shell}} in {{.Path}}", // outputs: zsh in /usr/home/omp
"console_title_template": "{{.User}}@{{.Host}} {{.Shell}} in {{.Path}}", // outputs: MyUser@MyMachine zsh in /usr/home/omp
"console_title_template": "{{.Env.USERDOMAIN}} {{.Shell}} in {{.Path}}", // outputs: MyCompany zsh in /usr/home/omp
"console_title_template": "{{.Shell}} in {{.PWD}}", // outputs: zsh in /usr/home/omp
"console_title_template": "{{.User}}@{{.Host}} {{.Shell}} in {{.PWD}}", // outputs: MyUser@MyMachine zsh in /usr/home/omp
"console_title_template": "{{.Env.USERDOMAIN}} {{.Shell}} in {{.PWD}}", // outputs: MyCompany zsh in /usr/home/omp
}
```

View file

@ -36,7 +36,7 @@ performance - defaults to `false`
- `.Context`: `string` - the current kubectl context
- `.Namespace`: `string` - the current kubectl context namespace
- `.User`: `string` - the current kubectl context user
- `.UserName`: `string` - the current kubectl context user
- `.Cluster`: `string` - the current kubectl context cluster
## Tips

View file

@ -127,7 +127,6 @@ starts with a symbol or icon.
## Template Properties
- `.PWD`: `string` - the current directory (non styled)
- `.Path`: `string` - the current directory (styled)
- `.StackCount`: `int` - the stack count

View file

@ -44,7 +44,10 @@ func (t *consoleTitle) getTemplateText() string {
Template: t.config.ConsoleTitleTemplate,
Env: t.env,
}
return template.renderPlainContextTemplate(nil)
if text, err := template.render(); err == nil {
return text
}
return ""
}
func (t *consoleTitle) getPwd() string {

View file

@ -22,7 +22,7 @@ func TestGetConsoleTitle(t *testing.T) {
{Style: FullPath, Cwd: "/usr/home/jan", PathSeperator: "/", ShellName: "default", Expected: "\x1b]0;~/jan\a"},
{
Style: Template,
Template: "{{.Env.USERDOMAIN}} :: {{.Path}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Template: "{{.Env.USERDOMAIN}} :: {{.PWD}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Cwd: "C:\\vagrant",
PathSeperator: "\\",
ShellName: "PowerShell",
@ -62,6 +62,7 @@ func TestGetConsoleTitle(t *testing.T) {
env.On("getenv", "USERDOMAIN").Return("MyCompany")
env.On("getCurrentUser", nil).Return("MyUser")
env.On("getHostName", nil).Return("MyHost", nil)
env.onTemplate()
ansi := &ansiUtils{}
ansi.init(tc.ShellName)
ct := &consoleTitle{
@ -117,6 +118,7 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
env.On("getenv", "USERDOMAIN").Return("MyCompany")
env.On("getCurrentUser", nil).Return("MyUser")
env.On("getHostName", nil).Return("", fmt.Errorf("I have a bad feeling about this"))
env.onTemplate()
ansi := &ansiUtils{}
ansi.init(tc.ShellName)
ct := &consoleTitle{

View file

@ -241,7 +241,10 @@ func (e *engine) renderTransientPrompt() string {
Template: promptTemplate,
Env: e.env,
}
prompt := template.renderPlainContextTemplate(nil)
prompt, err := template.render()
if err != nil {
prompt = err.Error()
}
e.writer.setColors(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground)
e.writer.write(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground, prompt)
switch e.env.getShellName() {

View file

@ -19,6 +19,7 @@ func TestConsoleBackgroundColorTemplate(t *testing.T) {
for _, tc := range cases {
env := new(MockedEnvironment)
env.On("getenv", "TERM_PROGRAM").Return(tc.Term)
env.onTemplate()
color := getConsoleBackgroundColor(env, "{{ if eq \"vscode\" .Env.TERM_PROGRAM }}#123456{{end}}")
assert.Equal(t, tc.Expected, color, tc.Case)
}

View file

@ -29,7 +29,7 @@ func TestAngularCliVersionDisplayed(t *testing.T) {
env.On("hasFiles", params.extension).Return(true)
env.On("hasFilesInDir", "/usr/home/dev/my-app/node_modules/@angular/core", "package.json").Return(true)
env.On("getFileContent", "/usr/home/dev/my-app/node_modules/@angular/core/package.json").Return(ta.Version)
env.onTemplate()
props := properties{}
angular := &angular{}
angular.init(props, env)

View file

@ -54,6 +54,7 @@ func TestAWSSegment(t *testing.T) {
env.On("getenv", "AWS_CONFIG_FILE").Return(tc.ConfigFile)
env.On("getFileContent", "/usr/home/.aws/config").Return("")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
DisplayDefault: tc.DisplayDefault,
}

View file

@ -59,7 +59,7 @@ func TestAzSegment(t *testing.T) {
}
env.On("getFileContent", filepath.Join(home, ".azure/azureProfile.json")).Return(azureProfile)
env.On("getFileContent", filepath.Join(home, ".azure/AzureRmContext.json")).Return(azureRmContext)
env.onTemplate()
props := properties{
SegmentTemplate: tc.Template,
}

View file

@ -151,7 +151,7 @@ func TestBrewfatherSegment(t *testing.T) {
env.On("HTTPRequest", BFBatchURL).Return([]byte(tc.BatchJSONResponse), tc.Error)
env.On("HTTPRequest", BFBatchReadingsURL).Return([]byte(tc.BatchReadingsJSONResponse), tc.Error)
env.On("cache", nil).Return(cache)
env.onTemplate()
if tc.Template != "" {
props[SegmentTemplate] = tc.Template
}

View file

@ -181,13 +181,13 @@ const (
func (e *exit) deprecatedString() string {
colorBackground := e.props.getBool(ColorBackground, false)
if e.Code != 0 && !colorBackground {
if e.code != 0 && !colorBackground {
e.props.set(ForegroundOverride, e.props.getColor(ErrorColor, e.props.getColor(ForegroundOverride, "")))
}
if e.Code != 0 && colorBackground {
if e.code != 0 && colorBackground {
e.props.set(BackgroundOverride, e.props.getColor(ErrorColor, e.props.getColor(BackgroundOverride, "")))
}
if e.Code == 0 {
if e.code == 0 {
return e.props.getString(SuccessIcon, "")
}
errorIcon := e.props.getString(ErrorIcon, "")
@ -195,7 +195,7 @@ func (e *exit) deprecatedString() string {
return errorIcon
}
if e.props.getBool(AlwaysNumeric, false) {
return fmt.Sprintf("%s%d", errorIcon, e.Code)
return fmt.Sprintf("%s%d", errorIcon, e.code)
}
return fmt.Sprintf("%s%s", errorIcon, e.Text)
}

View file

@ -447,6 +447,7 @@ func TestBatterySegmentSingle(t *testing.T) {
props[DisplayCharged] = false
}
env.On("getBatteryInfo", nil).Return(tc.Batteries, tc.Error)
env.onTemplate()
b := &batt{
props: props,
env: env,
@ -756,6 +757,7 @@ func TestPythonVirtualEnv(t *testing.T) {
env.On("getPathSeperator", nil).Return("")
env.On("getcwd", nil).Return("/usr/home/project")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
FetchVersion: tc.FetchVersion,
DisplayVirtualEnv: true,

View file

@ -28,6 +28,7 @@ func bootStrapDotnetTest(args *dotnetArgs) *dotnet {
env.On("getPathSeperator", nil).Return("")
env.On("getcwd", nil).Return("/usr/home/project")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
FetchVersion: args.displayVersion,
UnsupportedDotnetVersionIcon: args.unsupportedIcon,

View file

@ -6,7 +6,7 @@ type exit struct {
props Properties
env Environment
Code int
code int
Text string
}
@ -27,7 +27,7 @@ func (e *exit) init(props Properties, env Environment) {
}
func (e *exit) getFormattedText() string {
e.Code = e.env.lastErrorCode()
e.code = e.env.lastErrorCode()
e.Text = e.getMeaningFromExitCode()
segmentTemplate := e.props.getString(SegmentTemplate, "")
if len(segmentTemplate) == 0 {
@ -46,7 +46,7 @@ func (e *exit) getFormattedText() string {
}
func (e *exit) getMeaningFromExitCode() string {
switch e.Code {
switch e.code {
case 1:
return "ERROR"
case 2:
@ -100,6 +100,6 @@ func (e *exit) getMeaningFromExitCode() string {
case 128 + 22:
return "SIGTTOU"
default:
return strconv.Itoa(e.Code)
return strconv.Itoa(e.code)
}
}

View file

@ -59,7 +59,7 @@ func TestGetMeaningFromExitCode(t *testing.T) {
errorMap[7000] = "7000"
for exitcode, want := range errorMap {
e := &exit{}
e.Code = exitcode
e.code = exitcode
assert.Equal(t, want, e.getMeaningFromExitCode())
}
}
@ -77,6 +77,7 @@ func TestExitWriterTemplateString(t *testing.T) {
for _, tc := range cases {
env := new(MockedEnvironment)
env.On("lastErrorCode", nil).Return(tc.ExitCode)
env.onTemplate()
props := properties{
SegmentTemplate: tc.Template,
}

View file

@ -23,6 +23,7 @@ func getMockedLanguageEnv(params *mockedLanguageParams) (*MockedEnvironment, pro
env.On("hasFiles", params.extension).Return(true)
env.On("getcwd", nil).Return("/usr/home/project")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
FetchVersion: true,
}

View file

@ -48,6 +48,7 @@ func TestIpifySegment(t *testing.T) {
}
env.On("HTTPRequest", IPIFYAPIURL).Return([]byte(tc.Response), tc.Error)
env.onTemplate()
if tc.Template != "" {
props[SegmentTemplate] = tc.Template

View file

@ -66,6 +66,7 @@ func TestJava(t *testing.T) {
} else {
env.On("getenv", "JAVA_HOME").Return("")
}
env.onTemplate()
props := properties{
FetchVersion: true,
}

View file

@ -26,7 +26,7 @@ type KubeConfig struct {
type KubeContext struct {
Cluster string `yaml:"cluster"`
User string `yaml:"user"`
UserName string `yaml:"user"`
Namespace string `yaml:"namespace"`
}
@ -139,6 +139,6 @@ func (k *kubectl) setError(message string) {
k.Context = message
}
k.Namespace = message
k.User = message
k.UserName = message
k.Cluster = message
}

View file

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
const testKubectlAllInfoTemplate = "{{.Context}} :: {{.Namespace}} :: {{.User}} :: {{.Cluster}}"
const testKubectlAllInfoTemplate = "{{.Context}} :: {{.Namespace}} :: {{.UserName}} :: {{.Cluster}}"
func TestKubectlSegment(t *testing.T) {
standardTemplate := "{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
@ -24,7 +24,7 @@ func TestKubectlSegment(t *testing.T) {
ParseKubeConfig bool
Context string
Namespace string
User string
UserName string
Cluster string
KubectlErr bool
ExpectedEnabled bool
@ -47,7 +47,7 @@ func TestKubectlSegment(t *testing.T) {
KubectlExists: true,
Context: "aaa",
Namespace: "bbb",
User: "ccc",
UserName: "ccc",
Cluster: "ddd",
ExpectedString: "aaa :: bbb :: ccc :: ddd",
ExpectedEnabled: true,
@ -110,7 +110,7 @@ func TestKubectlSegment(t *testing.T) {
var kubeconfig string
content, err := ioutil.ReadFile("./test/kubectl.yml")
if err == nil {
kubeconfig = fmt.Sprintf(string(content), tc.Cluster, tc.User, tc.Namespace, tc.Context)
kubeconfig = fmt.Sprintf(string(content), tc.Cluster, tc.UserName, tc.Namespace, tc.Context)
}
var kubectlErr error
if tc.KubectlErr {
@ -125,6 +125,7 @@ func TestKubectlSegment(t *testing.T) {
env.On("getFileContent", path).Return(content)
}
env.On("homeDir", nil).Return("testhome")
env.onTemplate()
k := &kubectl{
env: env,

View file

@ -50,6 +50,7 @@ func bootStrapLanguageTest(args *languageArgs) *language {
}
env.On("getcwd", nil).Return(cwd)
env.On("homeDir", nil).Return(home)
env.onTemplate()
if args.properties == nil {
args.properties = properties{}
}

View file

@ -60,6 +60,7 @@ func TestNbgv(t *testing.T) {
env := new(MockedEnvironment)
env.On("hasCommand", "nbgv").Return(tc.HasNbgv)
env.On("runCommand", "nbgv", []string{"get-version", "--format=json"}).Return(tc.Response, tc.Error)
env.onTemplate()
nbgv := &nbgv{
env: env,
props: properties{

View file

@ -141,6 +141,7 @@ func TestNSSegment(t *testing.T) {
env.On("HTTPRequest", FAKEAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
env.On("cache", nil).Return(cache)
env.onTemplate()
if tc.Template != "" {
props[SegmentTemplate] = tc.Template

View file

@ -59,6 +59,7 @@ func TestOWMSegmentSingle(t *testing.T) {
}
env.On("HTTPRequest", OWMAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
env.onTemplate()
if tc.Template != "" {
props[SegmentTemplate] = tc.Template
@ -185,6 +186,7 @@ func TestOWMSegmentIcons(t *testing.T) {
expectedString := fmt.Sprintf("%s (20°C)", tc.ExpectedIconString)
env.On("HTTPRequest", OWMAPIURL).Return([]byte(response), nil)
env.onTemplate()
o := &owm{
props: properties{
@ -208,6 +210,7 @@ func TestOWMSegmentIcons(t *testing.T) {
expectedString := fmt.Sprintf("[%s (20°C)](http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", tc.ExpectedIconString)
env.On("HTTPRequest", OWMAPIURL).Return([]byte(response), nil)
env.onTemplate()
o := &owm{
props: properties{
@ -243,6 +246,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
cache.On("get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key", true)
cache.On("set").Return()
env.On("cache", nil).Return(cache)
env.onTemplate()
assert.Nil(t, o.setStatus())
assert.Equal(t, expectedString, o.string())
@ -269,6 +273,7 @@ func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
cache.On("get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key", true)
cache.On("set").Return()
env.On("cache", nil).Return(cache)
env.onTemplate()
assert.Nil(t, o.setStatus())
assert.Equal(t, expectedString, o.string())

View file

@ -11,7 +11,7 @@ type path struct {
props Properties
env Environment
PWD string
pwd string
Path string
StackCount int
}
@ -58,7 +58,7 @@ func (pt *path) enabled() bool {
}
func (pt *path) string() string {
pt.PWD = pt.env.getcwd()
pt.pwd = pt.env.getcwd()
switch style := pt.props.getString(Style, Agnoster); style {
case Agnoster:
pt.Path = pt.getAgnosterPath()
@ -86,9 +86,9 @@ func (pt *path) string() string {
if pt.props.getBool(EnableHyperlink, false) {
// wsl check
if pt.env.isWsl() {
pt.PWD, _ = pt.env.runCommand("wslpath", "-m", pt.PWD)
pt.pwd, _ = pt.env.runCommand("wslpath", "-m", pt.pwd)
}
pt.Path = fmt.Sprintf("[%s](file://%s)", pt.Path, pt.PWD)
pt.Path = fmt.Sprintf("[%s](file://%s)", pt.Path, pt.pwd)
}
pt.StackCount = pt.env.stackCount()

View file

@ -203,6 +203,25 @@ func (env *MockedEnvironment) getWifiNetwork() (*wifiInfo, error) {
return args.Get(0).(*wifiInfo), args.Error(1)
}
func (env *MockedEnvironment) onTemplate() {
patchMethodIfNotSpecified := func(method string, returnArguments ...interface{}) {
for _, call := range env.Mock.ExpectedCalls {
if call.Method == method {
return
}
}
env.On(method, nil).Return(returnArguments...)
}
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)
}
const (
homeBill = "/home/bill"
homeJan = "/usr/home/jan"
@ -417,6 +436,7 @@ func TestAgnosterPathStyles(t *testing.T) {
PSWD: &tc.Pswd,
}
env.On("getArgs", nil).Return(args)
env.onTemplate()
path := &path{
env: env,
props: properties{
@ -537,6 +557,7 @@ func TestGetFullPath(t *testing.T) {
PSWD: &tc.Pswd,
}
env.On("getArgs", nil).Return(args)
env.onTemplate()
if len(tc.Template) == 0 {
tc.Template = "{{ if gt .StackCount 0 }}{{ .StackCount }} {{ end }}{{ .Path }}"
}

View file

@ -52,6 +52,7 @@ func TestPythonTemplate(t *testing.T) {
env.On("getPathSeperator", nil).Return("")
env.On("getcwd", nil).Return("/usr/home/project")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
FetchVersion: tc.FetchVersion,
SegmentTemplate: tc.Template,

View file

@ -98,6 +98,7 @@ func TestRuby(t *testing.T) {
env.On("hasFiles", "Gemfile").Return(tc.HasGemFile)
env.On("getcwd", nil).Return("/usr/home/project")
env.On("homeDir", nil).Return("/usr/home")
env.onTemplate()
props := properties{
FetchVersion: tc.FetchVersion,
}

View file

@ -107,6 +107,7 @@ func TestSessionSegmentTemplate(t *testing.T) {
env.On("getenv", "SSH_CLIENT").Return(SSHSession)
env.On("isRunningAsRoot", nil).Return(tc.Root)
env.On("getenv", defaultUserEnvVar).Return(tc.DefaultUserName)
env.onTemplate()
session := &session{
env: env,
props: properties{

View file

@ -154,6 +154,7 @@ func TestStravaSegment(t *testing.T) {
env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error)
env.On("HTTPRequest", tokenURL).Return([]byte(tc.TokenResponse), tc.Error)
env.On("cache", nil).Return(cache)
env.onTemplate()
if tc.Template != "" {
props[SegmentTemplate] = tc.Template

View file

@ -34,7 +34,9 @@ func TestSysInfo(t *testing.T) {
}
for _, tc := range cases {
tc.SysInfo.env = new(MockedEnvironment)
env := new(MockedEnvironment)
env.onTemplate()
tc.SysInfo.env = env
tc.SysInfo.props = properties{
Precision: tc.Precision,
}

View file

@ -18,6 +18,7 @@ func bootStrapTerraformTest(args *terraformArgs) *terraform {
env.On("hasFolder", "/.terraform").Return(args.hasTfFolder)
env.On("getcwd", nil).Return("")
env.On("runCommand", "terraform", []string{"workspace", "show"}).Return(args.workspaceName, nil)
env.onTemplate()
k := &terraform{
env: env,
props: properties{},

View file

@ -18,8 +18,11 @@ func (t *text) enabled() bool {
Context: t,
Env: t.env,
}
t.content = template.renderPlainContextTemplate(nil)
return len(t.content) > 0
if text, err := template.render(); err == nil {
t.content = text
return len(t.content) > 0
}
return false
}
func (t *text) string() string {

View file

@ -33,6 +33,7 @@ func TestTextSegment(t *testing.T) {
env.On("getenv", "WORLD").Return("")
env.On("getCurrentUser", nil).Return("Posh")
env.On("getHostName", nil).Return("MyHost", nil)
env.onTemplate()
txt := &text{
env: env,
props: properties{

View file

@ -39,6 +39,7 @@ func TestTimeSegmentTemplate(t *testing.T) {
for _, tc := range cases {
env := new(MockedEnvironment)
env.onTemplate()
tempus := &tempus{
env: env,
props: properties{

View file

@ -77,6 +77,7 @@ func TestWTTrackedTime(t *testing.T) {
cache.On("get", FAKEAPIURL).Return(response, !tc.CacheFoundFail)
cache.On("set", FAKEAPIURL, response, tc.CacheTimeout).Return()
env.On("cache", nil).Return(cache)
env.onTemplate()
w := &wakatime{
props: properties{

View file

@ -45,6 +45,7 @@ func TestWiFiSegment(t *testing.T) {
env.On("getPlatform", nil).Return(windowsPlatform)
env.On("isWsl", nil).Return(false)
env.On("getWifiNetwork", nil).Return(tc.Network, tc.WifiError)
env.onTemplate()
w := &wifi{
env: env,

View file

@ -73,6 +73,7 @@ func TestWinReg(t *testing.T) {
env := new(MockedEnvironment)
env.On("getRuntimeGOOS", nil).Return(windowsPlatform)
env.On("getWindowsRegistryKeyValue", tc.Path).Return(tc.getWRKVOutput, tc.Err)
env.onTemplate()
r := &winreg{
env: env,
props: properties{

View file

@ -3,7 +3,7 @@ package main
import (
"bytes"
"errors"
"reflect"
"fmt"
"strings"
"text/template"
)
@ -12,6 +12,7 @@ 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>[^ \.}]*)`
)
@ -22,40 +23,58 @@ type textTemplate struct {
Env Environment
}
func (t *textTemplate) renderPlainContextTemplate(context map[string]interface{}) string {
if context == nil {
context = make(map[string]interface{})
type Data interface{}
type Context struct {
Root bool
PWD string
Folder string
Shell string
User string
Host string
Code int
Env map[string]string
// Simple container to hold ANY object
Data
}
func (c *Context) init(t *textTemplate) {
c.Data = t.Context
if t.Env == nil {
return
}
context["Root"] = t.Env.isRunningAsRoot()
c.Root = t.Env.isRunningAsRoot()
pwd := t.Env.getcwd()
pwd = strings.Replace(pwd, t.Env.homeDir(), "~", 1)
context["Path"] = pwd
context["Folder"] = base(pwd, t.Env)
context["Shell"] = t.Env.getShellName()
context["User"] = t.Env.getCurrentUser()
context["Host"] = ""
c.PWD = pwd
c.Folder = base(c.PWD, t.Env)
c.Shell = t.Env.getShellName()
c.User = t.Env.getCurrentUser()
if host, err := t.Env.getHostName(); err == nil {
context["Host"] = host
c.Host = host
}
t.Context = context
text, err := t.render()
if err != nil {
return err.Error()
c.Code = t.Env.lastErrorCode()
if strings.Contains(t.Template, ".Env.") {
c.Env = map[string]string{}
matches := findAllNamedRegexMatch(templateEnvRegex, t.Template)
for _, match := range matches {
c.Env[match["ENV"]] = t.Env.getenv(match["ENV"])
}
}
return text
}
func (t *textTemplate) render() (string, error) {
t.cleanTemplate()
tmpl, err := template.New("title").Funcs(funcMap()).Parse(t.Template)
if err != nil {
return "", errors.New(invalidTemplate)
}
if strings.Contains(t.Template, ".Env.") {
t.loadEnvVars()
}
context := &Context{}
context.init(t)
buffer := new(bytes.Buffer)
defer buffer.Reset()
err = tmpl.Execute(buffer, t.Context)
err = tmpl.Execute(buffer, context)
if err != nil {
return "", errors.New(incorrectTemplate)
}
@ -66,56 +85,28 @@ func (t *textTemplate) render() (string, error) {
return text, nil
}
func (t *textTemplate) loadEnvVars() {
context := make(map[string]interface{})
switch v := t.Context.(type) {
case map[string]interface{}:
context = v
default:
// we currently only support structs
if !t.isStruct() {
break
func (t *textTemplate) cleanTemplate() {
unknownVariable := func(variable string, knownVariables *[]string) (string, bool) {
variable = strings.TrimPrefix(variable, ".")
splitted := strings.Split(variable, ".")
if len(splitted) == 0 {
return "", false
}
context = t.structToMap()
for _, b := range *knownVariables {
if b == splitted[0] {
return "", false
}
}
*knownVariables = append(*knownVariables, splitted[0])
return splitted[0], true
}
envVars := map[string]string{}
matches := findAllNamedRegexMatch(templateEnvRegex, t.Template)
knownVariables := []string{"Root", "PWD", "Folder", "Shell", "User", "Host", "Env", "Data", "Code"}
matches := findAllNamedRegexMatch(`(?: |{)(?P<var>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
for _, match := range matches {
envVars[match["ENV"]] = t.Env.getenv(match["ENV"])
}
context["Env"] = envVars
t.Context = context
}
func (t *textTemplate) isStruct() bool {
v := reflect.TypeOf(t.Context)
if v == nil {
return false
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() == reflect.Invalid {
return false
}
return v.Kind() == reflect.Struct
}
func (t *textTemplate) structToMap() map[string]interface{} {
context := make(map[string]interface{})
v := reflect.ValueOf(t.Context)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
strct := v.Type()
for i := 0; i < strct.NumField(); i++ {
sf := strct.Field(i)
if !v.Field(i).CanInterface() {
continue
if variable, OK := unknownVariable(match["var"], &knownVariables); OK {
pattern := fmt.Sprintf(`\.%s\b`, variable)
dataVar := fmt.Sprintf(".Data.%s", variable)
t.Template = replaceAllString(pattern, t.Template, dataVar)
}
name := sf.Name
value := v.Field(i).Interface()
context[name] = value
}
return context
}

View file

@ -69,6 +69,7 @@ func TestRenderTemplate(t *testing.T) {
}
env := &MockedEnvironment{}
env.onTemplate()
for _, tc := range cases {
template := &textTemplate{
Template: tc.Template,
@ -93,6 +94,13 @@ func TestRenderTemplateEnvVar(t *testing.T) {
Env map[string]string
Context interface{}
}{
{
Case: "nil struct with env var",
ShouldError: true,
Template: "{{.Env.HELLO }} world{{ .Text}}",
Context: nil,
Env: map[string]string{"HELLO": "hello"},
},
{
Case: "map with env var",
Expected: "hello world",
@ -100,13 +108,6 @@ func TestRenderTemplateEnvVar(t *testing.T) {
Context: map[string]interface{}{"World": "world"},
Env: map[string]string{"HELLO": "hello"},
},
{
Case: "nil struct with env var",
Expected: "hello world",
Template: "{{.Env.HELLO }} world{{ .Text}}",
Context: nil,
Env: map[string]string{"HELLO": "hello"},
},
{
Case: "struct with env var",
Expected: "hello world posh",
@ -120,6 +121,7 @@ func TestRenderTemplateEnvVar(t *testing.T) {
}
for _, tc := range cases {
env := &MockedEnvironment{}
env.onTemplate()
for name, value := range tc.Env {
env.On("getenv", name).Return(value)
}
@ -136,3 +138,49 @@ func TestRenderTemplateEnvVar(t *testing.T) {
assert.Equal(t, tc.Expected, text, tc.Case)
}
}
func TestCleanTemplate(t *testing.T) {
cases := []struct {
Case string
Expected string
Template string
}{
{
Case: "Variable",
Expected: "{{range $cpu := .Data.CPU}}{{round $cpu.Mhz 2 }} {{end}}",
Template: "{{range $cpu := .CPU}}{{round $cpu.Mhz 2 }} {{end}}",
},
{
Case: "Same prefix",
Expected: "{{ .Env.HELLO }} {{ .Data.World }} {{ .Data.WorldTrend }}",
Template: "{{ .Env.HELLO }} {{ .World }} {{ .WorldTrend }}",
},
{
Case: "Double use of property with different child",
Expected: "{{ .Env.HELLO }} {{ .Data.World.Trend }} {{ .Data.World.Hello }} {{ .Data.World }}",
Template: "{{ .Env.HELLO }} {{ .World.Trend }} {{ .World.Hello }} {{ .World }}",
},
{
Case: "Hello world",
Expected: "{{.Env.HELLO}} {{.Data.World}}",
Template: "{{.Env.HELLO}} {{.World}}",
},
{
Case: "Multiple vars",
Expected: "{{.Env.HELLO}} {{.Data.World}} {{.Data.World}}",
Template: "{{.Env.HELLO}} {{.World}} {{.World}}",
},
{
Case: "Multiple vars with spaces",
Expected: "{{ .Env.HELLO }} {{ .Data.World }} {{ .Data.World }}",
Template: "{{ .Env.HELLO }} {{ .World }} {{ .World }}",
},
}
for _, tc := range cases {
template := &textTemplate{
Template: tc.Template,
}
template.cleanTemplate()
assert.Equal(t, tc.Expected, template.Template, tc.Case)
}
}