-
-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implements #207
- Loading branch information
Showing
8 changed files
with
294 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package shell | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"github.com/version-fox/vfox/internal/env" | ||
"path/filepath" | ||
) | ||
|
||
type nushell struct{} | ||
|
||
var Nushell = nushell{} | ||
|
||
const nushellConfig = env.Newline + | ||
"# vfox configuration" + env.Newline + | ||
"export-env {" + env.Newline + | ||
" def --env updateVfoxEnvironment [] {" + env.Newline + | ||
" let envData = (^'{{.SelfPath}}' env -s nushell | from json)" + env.Newline + | ||
" load-env $envData.envsToSet" + env.Newline + | ||
" hide-env ...$envData.envsToUnset" + env.Newline + | ||
" }" + env.Newline + | ||
" $env.config = ($env.config | upsert hooks.pre_prompt {" + env.Newline + | ||
" let currentValue = ($env.config | get -i hooks.pre_prompt)" + env.Newline + | ||
" if $currentValue == null {" + env.Newline + | ||
" [{updateVfoxEnvironment}]" + env.Newline + | ||
" } else {" + env.Newline + | ||
" $currentValue | append {updateVfoxEnvironment}" + env.Newline + | ||
" }" + env.Newline + | ||
" })" + env.Newline + | ||
" $env.__VFOX_SHELL = 'nushell'" + env.Newline + | ||
" $env.__VFOX_PID = $nu.pid" + env.Newline + | ||
" ^'{{.SelfPath}}' env --cleanup | ignore" + env.Newline + | ||
" updateVfoxEnvironment" + env.Newline + | ||
"}" + env.Newline | ||
|
||
// Activate implements shell.Activate by returning a script to be placed in the Nushell configuration file. This script | ||
// does the following: | ||
// | ||
// 1. Sets up a [pre_prompt hook] to update the environment variables when needed. | ||
// 2. Initializes the __VFOX_SHELL and __VFOX_PID environment variables. | ||
// 3. Runs the vfox cleanup task. | ||
// 4. Updates the environment variables. | ||
// | ||
// [pre_prompt hook]: https://www.nushell.sh/book/hooks.html | ||
func (n nushell) Activate() (string, error) { | ||
return nushellConfig, nil | ||
} | ||
|
||
// nushellExportData is used to create a JSON representation of the environment variables to be set and unset. | ||
type nushellExportData struct { | ||
EnvsToSet map[string]any `json:"envsToSet"` | ||
EnvsToUnset []string `json:"envsToUnset"` | ||
} | ||
|
||
// Export implements shell.Export by creating a JSON representation of the environment variables to be set and unset. | ||
// Nushell can then convert this JSON string to a [record] using the [from json] command, so it can load and unload the | ||
// environment variables using the [load-env] and [hide-env] commands. | ||
// | ||
// This approach is required for Nushell because it does not support eval-like functionality. For more background | ||
// information on this, see the article [How Nushell Code Gets Run]. | ||
// | ||
// [record]: https://www.nushell.sh/lang-guide/chapters/types/basic_types/record.html | ||
// [from json]: https://www.nushell.sh/commands/docs/from_json.html | ||
// [load-env]: https://www.nushell.sh/commands/docs/load-env.html | ||
// [hide-env]: https://www.nushell.sh/commands/docs/hide-env.html | ||
// [How Nushell Code Gets Run]: https://www.nushell.sh/book/how_nushell_code_gets_run.html | ||
func (n nushell) Export(envs env.Vars) string { | ||
exportData := nushellExportData{ | ||
EnvsToSet: make(map[string]any), | ||
EnvsToUnset: make([]string, 0), | ||
} | ||
|
||
for key, value := range envs { | ||
if key == "PATH" { // Convert from string to list. | ||
if value == nil { | ||
value = new(string) | ||
} | ||
pathEntries := filepath.SplitList(*value) | ||
exportData.EnvsToSet[key] = pathEntries | ||
} else { | ||
if value == nil { | ||
exportData.EnvsToUnset = append(exportData.EnvsToUnset, key) | ||
} else { | ||
exportData.EnvsToSet[key] = *value | ||
} | ||
} | ||
} | ||
|
||
exportJson, err := json.Marshal(exportData) | ||
if err != nil { | ||
fmt.Printf("Failed to marshal export data: %s\n", err) | ||
return "" | ||
} | ||
|
||
return string(exportJson) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package shell | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"github.com/version-fox/vfox/internal/env" | ||
"os" | ||
"reflect" | ||
"runtime" | ||
"slices" | ||
"testing" | ||
"text/template" | ||
) | ||
|
||
func TestActivate(t *testing.T) { | ||
var newline string | ||
if runtime.GOOS == "windows" { | ||
newline = "\r\n" | ||
} else { | ||
newline = "\n" | ||
} | ||
selfPath := "/path/to/vfox" | ||
want := newline + | ||
"# vfox configuration" + newline + | ||
"export-env {" + newline + | ||
" def --env updateVfoxEnvironment [] {" + newline + | ||
" let envData = (^'" + selfPath + "' env -s nushell | from json)" + newline + | ||
" load-env $envData.envsToSet" + newline + | ||
" hide-env ...$envData.envsToUnset" + newline + | ||
" }" + newline + | ||
" $env.config = ($env.config | upsert hooks.pre_prompt {" + newline + | ||
" let currentValue = ($env.config | get -i hooks.pre_prompt)" + newline + | ||
" if $currentValue == null {" + newline + | ||
" [{updateVfoxEnvironment}]" + newline + | ||
" } else {" + newline + | ||
" $currentValue | append {updateVfoxEnvironment}" + newline + | ||
" }" + newline + | ||
" })" + newline + | ||
" $env.__VFOX_SHELL = 'nushell'" + newline + | ||
" $env.__VFOX_PID = $nu.pid" + newline + | ||
" ^'" + selfPath + "' env --cleanup | ignore" + newline + | ||
" updateVfoxEnvironment" + newline + | ||
"}" + newline | ||
|
||
n := nushell{} | ||
gotTemplate, err := n.Activate() | ||
|
||
if err != nil { | ||
t.Errorf("Unexpected error: %v", err) | ||
return | ||
} | ||
|
||
parsedTemplate, err := template.New("activate").Parse(gotTemplate) | ||
if err != nil { | ||
t.Errorf("Unexpected error parsing template: %v", err) | ||
return | ||
} | ||
|
||
var buffer bytes.Buffer | ||
err = parsedTemplate.Execute(&buffer, struct{ SelfPath string }{selfPath}) | ||
if err != nil { | ||
t.Errorf("Unexpected error executing template: %v", err) | ||
return | ||
} | ||
|
||
got := buffer.String() | ||
if got != want { | ||
t.Errorf("Output mismatch:\n\ngot=\n%v\n\nwant=\n%v", got, want) | ||
} | ||
} | ||
|
||
func TestExport(t *testing.T) { | ||
sep := string(os.PathListSeparator) | ||
|
||
tests := []struct { | ||
name string | ||
envs env.Vars | ||
want nushellExportData | ||
}{ | ||
{ | ||
"Empty", | ||
env.Vars{}, | ||
nushellExportData{ | ||
EnvsToSet: make(map[string]any), | ||
EnvsToUnset: make([]string, 0)}, | ||
}, | ||
{ | ||
"SingleEnv", | ||
env.Vars{"FOO": newString("bar")}, | ||
nushellExportData{ | ||
EnvsToSet: map[string]any{"FOO": "bar"}, | ||
EnvsToUnset: make([]string, 0), | ||
}, | ||
}, | ||
{ | ||
"MultipleEnvs", | ||
env.Vars{"FOO": newString("bar"), "BAZ": newString("qux")}, | ||
nushellExportData{ | ||
EnvsToSet: map[string]any{"FOO": "bar", "BAZ": "qux"}, | ||
EnvsToUnset: make([]string, 0), | ||
}, | ||
}, | ||
{ | ||
"UnsetEnv", | ||
env.Vars{"FOO": nil}, | ||
nushellExportData{ | ||
EnvsToSet: make(map[string]any), | ||
EnvsToUnset: []string{"FOO"}, | ||
}, | ||
}, | ||
{ | ||
"MixedEnvs", | ||
env.Vars{"FOO": newString("bar"), "BAZ": nil}, | ||
nushellExportData{ | ||
EnvsToSet: map[string]any{"FOO": "bar"}, | ||
EnvsToUnset: []string{"BAZ"}, | ||
}, | ||
}, | ||
{ | ||
"MultipleUnsetEnvs", | ||
env.Vars{"FOO": nil, "BAZ": nil}, | ||
nushellExportData{ | ||
EnvsToSet: make(map[string]any), | ||
EnvsToUnset: []string{"FOO", "BAZ"}, | ||
}, | ||
}, | ||
{ | ||
"PathEnv", | ||
env.Vars{"PATH": newString("/path1" + sep + "/path2")}, | ||
nushellExportData{ | ||
EnvsToSet: map[string]any{"PATH": []any{"/path1", "/path2"}}, | ||
EnvsToUnset: make([]string, 0), | ||
}, | ||
}, | ||
{ | ||
"PathAndOtherEnv", | ||
env.Vars{ | ||
"PATH": newString("/path1" + sep + "/path2" + sep + "/path3"), | ||
"FOO": newString("bar"), | ||
"BAZ": nil, | ||
}, | ||
nushellExportData{ | ||
EnvsToSet: map[string]any{"PATH": []any{"/path1", "/path2", "/path3"}, "FOO": "bar"}, | ||
EnvsToUnset: []string{"BAZ"}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
runExportTest(t, test.envs, test.want) | ||
}) | ||
} | ||
} | ||
|
||
func runExportTest(t *testing.T, envs env.Vars, want nushellExportData) { | ||
n := nushell{} | ||
got := n.Export(envs) | ||
var gotData nushellExportData | ||
err := json.Unmarshal([]byte(got), &gotData) | ||
if err != nil { | ||
t.Errorf("%s: error unmarshaling export data - %v", t.Name(), err) | ||
return | ||
} | ||
|
||
slices.Sort(want.EnvsToUnset) | ||
slices.Sort(gotData.EnvsToUnset) | ||
if !reflect.DeepEqual(gotData, want) { | ||
t.Errorf("%s: export data mismatch - want %v, got %v", t.Name(), want, gotData) | ||
} | ||
} | ||
|
||
func newString(s string) *string { | ||
return &s | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters