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 @@

Built-ins str str -- ) unsetenv Remove an environment variable by name. Unsetting a variable that does not exist is not an error. (str -- ) dup Duplicate the top stack item. (a -- a a) swap Swap the top two stack items. (a b -- b a) diff --git a/doc/mshell.md b/doc/mshell.md index 78ad384..64a8247 100644 --- a/doc/mshell.md +++ b/doc/mshell.md @@ -327,6 +327,10 @@ An environment variable can be removed with the `unsetenv` built-in, which takes the variable name as a string. Unsetting a variable that does not exist is not an error. +When the variable name is not known statically, +an environment variable can be set with the `setenv` built-in, +which takes the value then the name as strings. + ```mshell $HOME cd @@ -338,6 +342,9 @@ $HOME cd # Removing an environment variable "MSHELL_VAR" unsetenv + +# Setting with a dynamic name, value then name +"Hello, World!" "MSHELL_VAR" setenv ``` ## Indexing @@ -1057,6 +1064,7 @@ end wl # Output: 11 - `defs`: Print available definitions at the current location (--) - `env`: Write all environment variables to stderr in sorted order (--) - `completionDefs`: Push a dictionary of completion definitions. Keys are command names, values are lists of quotations. `( -- dict)` +- `setenv`: Set an environment variable by name, value then name. Use when the name is not known statically; otherwise prefer `$NAME!`. `(str str -- )` - `unsetenv`: Remove an environment variable by name. Unsetting a variable that does not exist is not an error. `(str -- )` - `dup`: Duplicate (a -- a a) - `swap`: Swap (a b -- b a) diff --git a/doc/variables.inc.html b/doc/variables.inc.html index 405bdae..4884929 100644 --- a/doc/variables.inc.html +++ b/doc/variables.inc.html @@ -57,10 +57,12 @@

Environment Variables unsetenv built-in, which takes the name as a string. Unsetting a variable that does not exist is not an error. +When the variable name is not known statically, set one with the 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