Skip to content

Commit

Permalink
feat: export environment variable as array
Browse files Browse the repository at this point in the history
kiliantyler authored and JanDeDobbeleer committed Apr 21, 2024

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent 59e6b7a commit 0c3b2c7
Showing 9 changed files with 307 additions and 7 deletions.
8 changes: 8 additions & 0 deletions src/shell/env.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ type Env struct {
Delimiter Template `yaml:"delimiter"`
If If `yaml:"if"`
Persist bool `yaml:"persist"`
Type EnvType `yaml:"type"`

template string
parsed bool
@@ -155,3 +156,10 @@ func filterEmpty[S ~[]E, E string](s S) S {
}
return cleaned
}

type EnvType string

const (
String EnvType = "string"
Array EnvType = "array"
)
54 changes: 51 additions & 3 deletions src/shell/env_test.go
Original file line number Diff line number Diff line change
@@ -8,62 +8,110 @@ import (
)

func TestEnvironmentVariable(t *testing.T) {
env := &Env{Name: "HELLO", Value: "world"}
envs := map[EnvType]Env{
String: {Name: "HELLO", Value: "world"},
Array: {Name: "ARRAY", Value: "hello array world", Type: "array"},
}
cases := []struct {
Case string
Env Env
Shell string
Expected string
}{
{
Case: "PWSH",
Shell: PWSH,
Env: envs[String],
Expected: `$env:HELLO = "world"`,
},
{
Case: "PWSH Array",
Shell: PWSH,
Env: envs[Array],
Expected: `$env:ARRAY = @("hello","array","world")`,
},
{
Case: "CMD",
Shell: CMD,
Env: envs[String],
Expected: `os.setenv("HELLO", "world")`,
},
{
Case: "FISH",
Shell: FISH,
Env: envs[String],
Expected: "set --global HELLO world",
},
{
Case: "FISH Array",
Shell: FISH,
Env: envs[Array],
Expected: "set --global ARRAY hello array world",
},
{
Case: "NU",
Shell: NU,
Env: envs[String],
Expected: ` $env.HELLO = "world"`,
},
{
Case: "NU Array",
Shell: NU,
Env: envs[Array],
Expected: ` $env.ARRAY = ["hello" "array" "world"]`,
},
{
Case: "TCSH",
Shell: TCSH,
Env: envs[String],
Expected: `setenv HELLO "world";`,
},
{
Case: "XONSH",
Shell: XONSH,
Env: envs[String],
Expected: `$HELLO = "world"`,
},
{
Case: "XONSH Array",
Shell: XONSH,
Env: envs[Array],
Expected: `$ARRAY = ["hello","array","world"]`,
},
{
Case: "ZSH",
Shell: ZSH,
Env: envs[String],
Expected: `export HELLO="world"`,
},
{
Case: "ZSH Array",
Shell: ZSH,
Env: envs[Array],
Expected: `export ARRAY=("hello" "array" "world")`,
},
{
Case: "BASH",
Shell: BASH,
Env: envs[String],
Expected: `export HELLO="world"`,
},
{
Case: "BASH Array",
Shell: BASH,
Env: envs[Array],
Expected: `export ARRAY=("hello" "array" "world")`,
},
{
Case: "Unknown",
Shell: "unknown",
},
}

for _, tc := range cases {
env.template = ""
tc.Env.template = ""
context.Current = &context.Runtime{Shell: tc.Shell}
assert.Equal(t, tc.Expected, env.string(), tc.Case)
assert.Equal(t, tc.Expected, tc.Env.string(), tc.Case)
}
}

10 changes: 9 additions & 1 deletion src/shell/nu.go
Original file line number Diff line number Diff line change
@@ -33,7 +33,15 @@ func (e *Echo) nu() *Echo {
}

func (e *Env) nu() *Env {
e.template = ` $env.{{ .Name }} = {{ formatString .Value }}`
switch e.Type {
case Array:
e.template = ` $env.{{ .Name }} = [{{ formatArray .Value }}]`
case String:
fallthrough
default:
e.template = ` $env.{{ .Name }} = {{ formatString .Value }}`
}

return e
}

10 changes: 9 additions & 1 deletion src/shell/pwsh.go
Original file line number Diff line number Diff line change
@@ -55,7 +55,15 @@ Write-Host $message`
}

func (e *Env) pwsh() *Env {
e.template = `$env:{{ .Name }} = {{ formatString .Value }}`
switch e.Type {
case Array:
e.template = `$env:{{ .Name }} = @({{ formatArray .Value "," }})`
case String:
fallthrough
default:
e.template = `$env:{{ .Name }} = {{ formatString .Value }}`
}

return e
}

44 changes: 44 additions & 0 deletions src/shell/template.go
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ func funcMap() template.FuncMap {
"isPwshOption": isPwshOption,
"isPwshScope": isPwshScope,
"formatString": formatString,
"formatArray": formatArray,
"escapeString": escapeString,
"env": os.Getenv,
"match": match,
@@ -68,6 +69,49 @@ func formatString(variable interface{}) interface{} {
}
}

func splitString(variable interface{}) interface{} {
switch variable := variable.(type) {
case string:
variable = strings.TrimSpace(variable)
if len(variable) == 0 {
return []string{variable}
}

if strings.Contains(variable, "\n") {
return strings.Split(variable, "\n")
}

return strings.Fields(variable)
case Template:
return splitString(variable.String())
default:
return variable
}
}

func formatArray(variable interface{}, delim ...string) interface{} {
delimiter := " "
if len(delim) > 0 {
delimiter = delim[0]
}

switch variable := variable.(type) {
case string:
split := splitString(variable).([]string)
array := []string{}

for _, value := range split {
array = append(array, formatString(value).(string))
}

return strings.Join(array, delimiter)
case Template:
return formatArray(variable.String())
default:
return variable
}
}

func escapeString(variable interface{}) interface{} {
clean := func(v string) string {
v = strings.ReplaceAll(v, `\`, `\\`)
167 changes: 167 additions & 0 deletions src/shell/template_test.go
Original file line number Diff line number Diff line change
@@ -35,3 +35,170 @@ func TestFormatString(t *testing.T) {
assert.Equal(t, tc.Expected, got, tc.Case)
}
}

// This tests both formatArray and splitString
func TestFormatArray(t *testing.T) {
text := `{{ formatArray .Value }}`
textDelim := `{{ formatArray .Value .Delim }}`
cases := []struct {
Case string
Value interface{}
Expected string
Delim string
}{
{
Case: "string",
Value: "hello",
Expected: `"hello"`,
},
{
Case: "Multiple Strings",
Value: "hello world, I am a long string",
Expected: `"hello" "world," "I" "am" "a" "long" "string"`,
},
{
Case: "Multiline String",
Value: `hello
world
I
am
a
multiline
string`,
Expected: `"hello" "world" "I" "am" "a" "multiline" "string"`,
},
{
Case: "Single Line Starts with newline",
Value: `
hello world I am a long string`,
Expected: `"hello" "world" "I" "am" "a" "long" "string"`,
},
{
Case: "Single line with delimiter",
Value: `hello world I am a long string`,
Delim: ",",
Expected: `"hello","world","I","am","a","long","string"`,
},
{
Case: "Multiline with delimiter",
Value: `hello
I
am
a
mutliline
string`,
Delim: ";",
Expected: `"hello";"I";"am";"a";"mutliline";"string"`,
},
{
Case: "bool",
Value: true,
Expected: `true`,
},
{
Case: "int",
Value: 32,
Expected: `32`,
},
}

for _, tc := range cases {
got := ""
if tc.Delim == "" {
got, _ = parse(text, tc)
} else {
got, _ = parse(textDelim, tc)
}
assert.Equal(t, tc.Expected, got, tc.Case)
}
}

func TestEscapeString(t *testing.T) {
text := `{{ escapeString .Value}}`
cases := []struct {
Case string
Value interface{}
Expected string
}{
{
Case: "string",
Value: `hello`,
Expected: `hello`,
},
{
Case: "stringWithQuotes",
Value: `hello "world"`,
Expected: `hello \"world\"`,
},
{
Case: "stringWithBackslashes",
Value: `hello \world`,
Expected: `hello \\world`,
},
{
Case: "template",
Value: Template(`hello "world"`),
Expected: `hello \"world\"`,
},
}

for _, tc := range cases {
got, _ := parse(text, tc)
assert.Equal(t, tc.Expected, got, tc.Case)
}
}

func TestMatch(t *testing.T) {
text := `{{ match .Variable "hello" "world"}}`
cases := []struct {
Case string
Variable string
Expected string
}{
{
Case: "match",
Variable: "hello",
Expected: `true`,
},
{
Case: "match",
Variable: "world",
Expected: `true`,
},
{
Case: "noMatch",
Variable: "goodbye",
Expected: `false`,
},
}

for _, tc := range cases {
got, _ := parse(text, tc)
assert.Equal(t, tc.Expected, got, tc.Case)
}
}

func TestHasCommand(t *testing.T) {
text := `{{ hasCommand .Command}}`
cases := []struct {
Case string
Command string
Expected string
}{
{
Case: "hasCommand",
Command: "go",
Expected: `true`,
},
{
Case: "noCommand",
Command: "notACommand",
Expected: `false`,
},
}

for _, tc := range cases {
got, _ := parse(text, tc)
assert.Equal(t, tc.Expected, got, tc.Case)
}
}
10 changes: 9 additions & 1 deletion src/shell/xonsh.go
Original file line number Diff line number Diff line change
@@ -32,7 +32,15 @@ print(message)`
}

func (e *Env) xonsh() *Env {
e.template = `${{ .Name }} = {{ formatString .Value }}`
switch e.Type {
case Array:
e.template = `${{ .Name }} = [{{ formatArray .Value "," }}]`
case String:
fallthrough
default:
e.template = `${{ .Name }} = {{ formatString .Value }}`
}

return e
}

10 changes: 9 additions & 1 deletion src/shell/zsh.go
Original file line number Diff line number Diff line change
@@ -30,7 +30,15 @@ func (e *Echo) zsh() *Echo {
}

func (e *Env) zsh() *Env {
e.template = `export {{ .Name }}={{ formatString .Value }}`
switch e.Type {
case Array:
e.template = `export {{ .Name }}=({{ formatArray .Value }})`
case String:
fallthrough
default:
e.template = `export {{ .Name }}={{ formatString .Value }}`
}

return e
}

1 change: 1 addition & 0 deletions website/docs/setup/env.mdx
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ env:
| `delimiter` | `string` | if you want to join an array of string values (separated by newlines), supports [templating][templates] |
| `if` | `string` | golang [template][go-text-template] conditional statement, see [if][if] |
| `persist` | `boolean` | if you want to persist the environment variable into the registry for the current user (Windows only) |
| `type` | `string` | type to export to, possible values are `string` (default) and `array` |

[templates]: templates.mdx
[go-text-template]: https://golang.org/pkg/text/template/

0 comments on commit 0c3b2c7

Please sign in to comment.