Skip to content

Commit

Permalink
feat: export environment variable as array
Browse files Browse the repository at this point in the history
  • Loading branch information
kiliantyler authored and JanDeDobbeleer committed Apr 21, 2024
1 parent 59e6b7a commit 0c3b2c7
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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)
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/shell/nu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
10 changes: 9 additions & 1 deletion src/shell/pwsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
44 changes: 44 additions & 0 deletions src/shell/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func funcMap() template.FuncMap {
"isPwshOption": isPwshOption,
"isPwshScope": isPwshScope,
"formatString": formatString,
"formatArray": formatArray,
"escapeString": escapeString,
"env": os.Getenv,
"match": match,
Expand All @@ -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, `\`, `\\`)
Expand Down
167 changes: 167 additions & 0 deletions src/shell/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading

0 comments on commit 0c3b2c7

Please sign in to comment.