diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c1de1..76efebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Functions - `longestCommonPrefix`: Longest leading substring shared by every string in a list. `([str] -- str)` + - `setenv`: Set an environment variable by name, taking the value then the name. + Use when the name is not known statically; otherwise prefer `$NAME!`. `(str str -- )` - `stdinIsTerminal`, `stdoutIsTerminal`, and `stderrIsTerminal`: Report whether the current effective standard stream is connected to a terminal or Windows console. Regular files, pipes, captures, and non-file streams return false. diff --git a/doc/functions.inc.html b/doc/functions.inc.html index 546d6e5..3e8c9a4 100644 --- a/doc/functions.inc.html +++ b/doc/functions.inc.html @@ -30,6 +30,7 @@
unsetenv(str -- )dup(a -- a a)swap(a b -- b a)setenv built-in, which takes the value then the name.
-"LOG_LEVEL" unsetenv+"LOG_LEVEL" unsetenv +"info" "LOG_LEVEL" setenv
diff --git a/mshell/BuiltInList.go b/mshell/BuiltInList.go
index ca48227..56cca1e 100644
--- a/mshell/BuiltInList.go
+++ b/mshell/BuiltInList.go
@@ -162,6 +162,7 @@ var BuiltInList = map[string]struct{}{
"set": {},
"setAt": {},
"setd": {},
+ "setenv": {},
"sha256sum": {},
"skip": {},
"sleep": {},
diff --git a/mshell/Evaluator.go b/mshell/Evaluator.go
index 1adcb79..c05a8a5 100644
--- a/mshell/Evaluator.go
+++ b/mshell/Evaluator.go
@@ -7216,6 +7216,36 @@ func (state *EvalState) evaluateToken(t Token, stack *MShellStack, context Execu
}
stack.Push(MShellPath{path})
+ } else if t.Lexeme == "setenv" {
+ obj1, err := stack.Pop()
+ if err != nil {
+ return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'setenv' operation on an empty stack.\n", t.Line, t.Column))
+ }
+
+ varName, err := obj1.CastString()
+ if err != nil {
+ return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot use a %s as an environment variable name.\n", t.Line, t.Column, obj1.TypeName()))
+ }
+
+ obj2, err := stack.Pop()
+ if err != nil {
+ return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'setenv' operation on a stack with less than two items.\n", t.Line, t.Column))
+ }
+
+ varValue, err := obj2.CastString()
+ if err != nil {
+ return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot use a %s as an environment variable value.\n", t.Line, t.Column, obj2.TypeName()))
+ }
+
+ err = os.Setenv(varName, varValue)
+ if err != nil {
+ return state.FailWithMessage(fmt.Sprintf("%d:%d: Could not set the environment variable '%s' to '%s'.\n", t.Line, t.Column, varName, varValue))
+ }
+
+ // If it was the PATH, refresh all the binaries
+ if varName == "PATH" {
+ context.Pbm.Update()
+ }
} else if t.Lexeme == "unsetenv" {
obj1, err := stack.Pop()
if err != nil {
diff --git a/mshell/TypeBuiltins.go b/mshell/TypeBuiltins.go
index 51607fe..671796c 100644
--- a/mshell/TypeBuiltins.go
+++ b/mshell/TypeBuiltins.go
@@ -449,6 +449,8 @@ func builtinSigsByName(arena *TypeArena, names *NameTable) map[NameId][]QuoteSig
r.reg(name, "(str | path -- )")
}
r.reg("cd", "(str | path -- )")
+ // setenv : set an environment variable, value then name
+ r.reg("setenv", "(str str -- )")
// unsetenv : remove an environment variable by name
r.reg("unsetenv", "(str -- )")
// cdh / cdp : interactive directory history / pop navigation
diff --git a/tests/success/setenv.msh b/tests/success/setenv.msh
new file mode 100644
index 0000000..cbd4785
--- /dev/null
+++ b/tests/success/setenv.msh
@@ -0,0 +1,9 @@
+# Set an environment variable with a dynamic name, value then name.
+"hello" "MSH_SETENV_TEST" setenv
+$MSH_SETENV_TEST? str wl
+$MSH_SETENV_TEST wl
+# Overwriting an existing variable.
+"world" "MSH_SETENV_TEST" setenv
+$MSH_SETENV_TEST wl
+"MSH_SETENV_TEST" unsetenv
+$MSH_SETENV_TEST? str wl
diff --git a/tests/success/setenv.msh.stdout b/tests/success/setenv.msh.stdout
new file mode 100644
index 0000000..c2602b9
--- /dev/null
+++ b/tests/success/setenv.msh.stdout
@@ -0,0 +1,4 @@
+true
+hello
+world
+false