Skip to content

Commit a3e780c

Browse files
lavocattbrusdev
authored andcommitted
[#16] add ability to unset env variables
The users could already export variables to make them available to subsequent chunks, now they can also unset variables to cleanup the env if necessary.
1 parent 25f611c commit a3e780c

3 files changed

Lines changed: 166 additions & 23 deletions

File tree

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ go test ./...
131131
? github.com/arkmq-org/markdown-runner/runnercontext [no test files]
132132
? github.com/arkmq-org/markdown-runner/view [no test files]
133133
ok github.com/arkmq-org/markdown-runner 0.005s
134-
ok github.com/arkmq-org/markdown-runner/chunk 0.112s
134+
ok github.com/arkmq-org/markdown-runner/chunk (cached)
135135
ok github.com/arkmq-org/markdown-runner/config (cached)
136136
ok github.com/arkmq-org/markdown-runner/parser (cached)
137-
ok github.com/arkmq-org/markdown-runner/runner 0.013s
138-
ok github.com/arkmq-org/markdown-runner/stage 0.110s
137+
ok github.com/arkmq-org/markdown-runner/runner (cached)
138+
ok github.com/arkmq-org/markdown-runner/stage (cached)
139139
```
140140

141141
Integration tests can be run with:
@@ -632,7 +632,8 @@ tool that this a disposable code fence that can be overridden in the future.
632632
Every chunk is started with the environment of the parent process that started
633633
it. If as chunk whom runtime is bash is executed, all the variables it adds to
634634
its own env via `export` will get added to the environment of subsequent
635-
chunks.
635+
chunks. Similarly, variables that are `unset` in a bash chunk will be removed
636+
from the environment of subsequent chunks.
636637
637638
The following diagram illustrates this flow:
638639
@@ -737,19 +738,33 @@ export TEST="some value"
737738
````
738739
739740
The chunk below is able to perform some comparisons with value of
740-
`SOME_VARIABLE`
741+
`SOME_VARIABLE`. It's also possible to unset this variable from the env for
742+
subsequent chunks.
741743
742744
````markdown
743-
```bash {"stage":"test2", "runtime":"bash", "label": "Verify environment variable"}
745+
```bash {"stage":"test2", "runtime":"bash", "label": "Verify exported variable"}
744746
if [ "$SOME_VARIABLE" == "This is some content" ]; then
745747
echo "same string"
748+
unset SOME_VARIABLE
746749
fi
747750
```
748751
```shell markdown_runner
749752
same string
750753
```
751754
````
752755
756+
757+
````markdown
758+
```bash {"stage":"test2", "runtime":"bash", "label": "Verify unset variable"}
759+
if [ ! -v SOME_VARIABLE ]; then
760+
echo "SOME_VARIABLE is truly unset."
761+
fi
762+
```
763+
```shell markdown_runner
764+
SOME_VARIABLE is truly unset.
765+
```
766+
````
767+
753768
#### Executing in parallel
754769
755770
Parallelism can be important for some workflows, when for instance two processes

chunk/command.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11-
"os"
1211
"os/exec"
1312
"strings"
1413

@@ -155,15 +154,14 @@ func (command *RunningCommand) Wait() error {
155154
}
156155
command.Ctx.RView.StopCommand(command.id, true, command.CmdPrettyName)
157156

158-
// During a bash runtime the user might want to export new variables.
157+
// During a bash runtime the user might want to export new variables or unset existing ones.
159158
// Our job here is to recover them to build the new environment for the next chunk
160159
if command.IsBash {
161160
stoudtLines := strings.Split(command.Stdout, "\n")
162-
// reinitialize the env
163-
command.Ctx.Cfg.Env = []string{}
164-
command.Ctx.Cfg.Env = append(command.Ctx.Cfg.Env, os.Environ()...)
165161
var newLines []string
166162
extractVariables := false
163+
var bashEnvVars []string
164+
167165
for _, line := range stoudtLines {
168166
// the environment gets separated by the output from a special string
169167
if line == "### ENV ###" {
@@ -173,13 +171,17 @@ func (command *RunningCommand) Wait() error {
173171
if extractVariables {
174172
parts := strings.Split(line, "=")
175173
if len(parts) > 1 {
176-
command.Ctx.Cfg.Env = append(command.Ctx.Cfg.Env, line)
174+
bashEnvVars = append(bashEnvVars, line)
177175
}
178176
} else {
179177
// if not it's output we want to keep for the user
180178
newLines = append(newLines, line)
181179
}
182180
}
181+
182+
// This includes all environment variables that should be available to subsequent chunks.
183+
command.Ctx.Cfg.Env = bashEnvVars
184+
183185
if len(newLines) > 0 {
184186
command.Stdout = strings.Join(newLines, "\n")
185187
command.Stdout = command.Stdout + "\n"

chunk/command_test.go

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,153 @@ func TestRunningCommand(t *testing.T) {
7676
assert.Error(t, err, "Expected an error when executing a command without a spinner initialized")
7777
})
7878

79-
t.Run("bash env extraction", func(t *testing.T) {
79+
t.Run("bash env export preserves original environment", func(t *testing.T) {
8080
tmpDirs := make(map[string]string)
81-
cfg := &config.Config{MinutesToTimeout: 1}
81+
82+
// Set up initial environment with some original variables
83+
originalEnv := []string{
84+
"ORIGINAL_VAR=original_value",
85+
"PATH=/usr/bin:/bin",
86+
"HOME=/home/test",
87+
"WORKING_DIR=/test/dir",
88+
}
89+
90+
cfg := &config.Config{MinutesToTimeout: 1, Env: originalEnv}
8291
ui := view.NewView("mock")
92+
93+
// Export a new variable using the real bash chunk approach
8394
chunk := ExecutableChunk{
8495
Runtime: "bash",
96+
Content: []string{"export NEW_VAR=new_value"},
8597
Context: &runnercontext.Context{
8698
Cfg: cfg,
8799
RView: ui,
88100
},
89101
}
90-
cmdStr := "bash -c 'echo \"### ENV ###\" && echo \"TEST_ENV_VAR=test_value\"'"
91-
command, err := chunk.AddCommandToExecute(cmdStr, tmpDirs)
92-
assert.NoError(t, err, "Expected no error when adding command to execute")
93-
command.IsBash = true
94-
err = command.Execute()
95-
assert.NoError(t, err, "Expected no error when executing bash command")
96-
found := slices.ContainsFunc(command.Ctx.Cfg.Env, func(env string) bool {
97-
return env == "TEST_ENV_VAR=test_value"
102+
err := chunk.PrepareForExecution(tmpDirs)
103+
assert.NoError(t, err, "Expected no error when preparing chunk for execution")
104+
105+
err = chunk.ExecuteSequential()
106+
assert.NoError(t, err, "Expected no error when executing bash chunk")
107+
108+
// Verify original environment variables are preserved
109+
foundOriginal := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
110+
return env == "ORIGINAL_VAR=original_value"
111+
})
112+
assert.True(t, foundOriginal, "Expected ORIGINAL_VAR to be preserved")
113+
114+
foundPath := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
115+
return strings.HasPrefix(env, "PATH=")
116+
})
117+
assert.True(t, foundPath, "Expected PATH to be preserved")
118+
119+
foundHome := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
120+
return env == "HOME=/home/test"
121+
})
122+
assert.True(t, foundHome, "Expected HOME to be preserved")
123+
124+
foundWorkingDir := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
125+
return env == "WORKING_DIR=/test/dir"
126+
})
127+
assert.True(t, foundWorkingDir, "Expected WORKING_DIR to be preserved")
128+
129+
// Verify new variable was added
130+
foundNew := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
131+
return strings.HasPrefix(env, "NEW_VAR=")
132+
})
133+
assert.True(t, foundNew, "Expected NEW_VAR to be added")
134+
})
135+
136+
t.Run("bash env unset removes variables selectively", func(t *testing.T) {
137+
tmpDirs := make(map[string]string)
138+
139+
// Set up environment with variables to test unset behavior
140+
initialEnv := []string{
141+
"KEEP_VAR=keep_this",
142+
"REMOVE_VAR=remove_this",
143+
"PATH=/usr/bin:/bin",
144+
}
145+
146+
cfg := &config.Config{MinutesToTimeout: 1, Env: initialEnv}
147+
ui := view.NewView("mock")
148+
149+
// Unset one variable while keeping others
150+
chunk := ExecutableChunk{
151+
Runtime: "bash",
152+
Content: []string{"unset REMOVE_VAR"},
153+
Context: &runnercontext.Context{
154+
Cfg: cfg,
155+
RView: ui,
156+
},
157+
}
158+
err := chunk.PrepareForExecution(tmpDirs)
159+
assert.NoError(t, err, "Expected no error when preparing chunk for execution")
160+
161+
err = chunk.ExecuteSequential()
162+
assert.NoError(t, err, "Expected no error when executing bash chunk")
163+
164+
// Verify the targeted variable was unset
165+
foundRemoved := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
166+
return strings.HasPrefix(env, "REMOVE_VAR=")
167+
})
168+
assert.False(t, foundRemoved, "Expected REMOVE_VAR to be removed after unset")
169+
170+
// Verify other variables are still preserved
171+
foundKeep := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
172+
return env == "KEEP_VAR=keep_this"
173+
})
174+
assert.True(t, foundKeep, "Expected KEEP_VAR to be preserved after unset")
175+
176+
foundPath := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
177+
return strings.HasPrefix(env, "PATH=")
178+
})
179+
assert.True(t, foundPath, "Expected PATH to be preserved after unset")
180+
})
181+
182+
t.Run("bash env unset critical variables", func(t *testing.T) {
183+
tmpDirs := make(map[string]string)
184+
185+
// Set up environment with critical variables
186+
initialEnv := []string{
187+
"HOME=/home/test",
188+
"PATH=/usr/bin:/bin",
189+
"KEEP_VAR=keep_this",
190+
}
191+
192+
cfg := &config.Config{MinutesToTimeout: 1, Env: initialEnv}
193+
ui := view.NewView("mock")
194+
195+
// Unset HOME to verify critical environment variables can be unset
196+
chunk := ExecutableChunk{
197+
Runtime: "bash",
198+
Content: []string{"unset HOME"},
199+
Context: &runnercontext.Context{
200+
Cfg: cfg,
201+
RView: ui,
202+
},
203+
}
204+
err := chunk.PrepareForExecution(tmpDirs)
205+
assert.NoError(t, err, "Expected no error when preparing chunk for execution")
206+
207+
err = chunk.ExecuteSequential()
208+
assert.NoError(t, err, "Expected no error when executing bash chunk")
209+
210+
// Verify HOME was unset
211+
foundHome := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
212+
return strings.HasPrefix(env, "HOME=")
213+
})
214+
assert.False(t, foundHome, "Expected HOME to be removed after unset")
215+
216+
// Verify other variables are still preserved
217+
foundKeep := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
218+
return env == "KEEP_VAR=keep_this"
219+
})
220+
assert.True(t, foundKeep, "Expected KEEP_VAR to be preserved after unsetting HOME")
221+
222+
foundPath := slices.ContainsFunc(chunk.Context.Cfg.Env, func(env string) bool {
223+
return strings.HasPrefix(env, "PATH=")
98224
})
99-
assert.True(t, found, "Expected to find TEST_ENV_VAR in the environment variables")
225+
assert.True(t, foundPath, "Expected PATH to be preserved after unsetting HOME")
100226
})
101227

102228
t.Run("kill", func(t *testing.T) {

0 commit comments

Comments
 (0)