Skip to content

Commit 7e7b16c

Browse files
authored
Escape env vars in shellenv the same way as in shellrc (#731)
## Summary Before, running `eval $(devbox shellenv)` would fail for me, because my `PS1` var wasn't being escaped properly. Now it is and we reuse the function. ## How was it tested? Ran `eval $(devbox shellenv)`
1 parent bc7b7e1 commit 7e7b16c

File tree

4 files changed

+70
-50
lines changed

4 files changed

+70
-50
lines changed

internal/impl/devbox.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,7 @@ func (d *Devbox) PrintEnv() (string, error) {
233233
return "", err
234234
}
235235

236-
script := ""
237-
for k, v := range envs {
238-
// %q is for escaping quotes in env variables that
239-
// have quotes in them e.g., shellHook
240-
script += fmt.Sprintf("export %s=%q\n", k, v)
241-
}
242-
243-
return script, nil
236+
return exportify(envs), nil
244237
}
245238

246239
func (d *Devbox) Info(pkg string, markdown bool) error {

internal/impl/envvars.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package impl
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
)
8+
9+
func mapToPairs(m map[string]string) []string {
10+
pairs := []string{}
11+
for k, v := range m {
12+
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
13+
}
14+
return pairs
15+
}
16+
17+
func pairsToMap(pairs []string) map[string]string {
18+
vars := map[string]string{}
19+
for _, p := range pairs {
20+
k, v, ok := strings.Cut(p, "=")
21+
if !ok {
22+
continue
23+
}
24+
vars[k] = v
25+
}
26+
return vars
27+
}
28+
29+
// exportify takes an array of strings of the form VAR=VAL and returns a bash script
30+
// that exports all the vars after properly escaping them.
31+
func exportify(vars map[string]string) string {
32+
keys := make([]string, 0, len(vars))
33+
for k := range vars {
34+
keys = append(keys, k)
35+
}
36+
sort.Strings(keys)
37+
38+
strb := strings.Builder{}
39+
for _, k := range keys {
40+
strb.WriteString("export ")
41+
strb.WriteString(k)
42+
strb.WriteString(`="`)
43+
for _, r := range vars[k] {
44+
switch r {
45+
// Special characters inside double quotes:
46+
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
47+
case '$', '`', '"', '\\', '\n':
48+
strb.WriteRune('\\')
49+
}
50+
strb.WriteRune(r)
51+
}
52+
strb.WriteString("\"\n")
53+
}
54+
return strings.TrimSpace(strb.String())
55+
}

internal/impl/shell.go

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"os"
1313
"os/exec"
1414
"path/filepath"
15-
"sort"
1615
"strings"
1716
"text/template"
1817

@@ -58,7 +57,7 @@ type DevboxShell struct {
5857
binPath string
5958
projectDir string // path to where devbox.json config resides
6059
pkgConfigDir string
61-
env []string
60+
env map[string]string
6261
userShellrcPath string
6362
pluginInitHook string
6463

@@ -218,15 +217,7 @@ func WithHistoryFile(historyFile string) ShellOption {
218217
// via wrapper scripts.
219218
func WithEnvVariables(envVariables map[string]string) ShellOption {
220219
return func(s *DevboxShell) {
221-
keys := make([]string, 0, len(envVariables))
222-
for k := range envVariables {
223-
keys = append(keys, k)
224-
}
225-
sort.Strings(keys)
226-
227-
for _, k := range keys {
228-
s.env = append(s.env, fmt.Sprintf("%s=%s", k, envVariables[k]))
229-
}
220+
s.env = envVariables
230221
}
231222
}
232223

@@ -277,9 +268,13 @@ func (s *DevboxShell) Run() error {
277268
// Link other files that affect the shell settings and environments.
278269
s.linkShellStartupFiles(filepath.Dir(shellrc))
279270
extraEnv, extraArgs := s.shellRCOverrides(shellrc)
271+
env := s.env
272+
for k, v := range extraEnv {
273+
env[k] = v
274+
}
280275

281276
cmd = exec.Command(s.binPath)
282-
cmd.Env = append(s.env, extraEnv...)
277+
cmd.Env = mapToPairs(env)
283278
cmd.Args = append(cmd.Args, extraArgs...)
284279
cmd.Stdin = os.Stdin
285280
cmd.Stdout = os.Stdout
@@ -309,16 +304,16 @@ func (s *DevboxShell) Run() error {
309304
return errors.WithStack(err)
310305
}
311306

312-
func (s *DevboxShell) shellRCOverrides(shellrc string) (extraEnv []string, extraArgs []string) {
307+
func (s *DevboxShell) shellRCOverrides(shellrc string) (extraEnv map[string]string, extraArgs []string) {
313308
// Shells have different ways of overriding the shellrc, so we need to
314309
// look at the name to know which env vars or args to set when launching the shell.
315310
switch s.name {
316311
case shBash:
317312
extraArgs = []string{"--rcfile", shellescape.Quote(shellrc)}
318313
case shZsh:
319-
extraEnv = []string{fmt.Sprintf(`ZDOTDIR=%s`, shellescape.Quote(filepath.Dir(shellrc)))}
314+
extraEnv = map[string]string{"ZDOTDIR": shellescape.Quote(filepath.Dir(shellrc))}
320315
case shKsh, shPosix:
321-
extraEnv = []string{fmt.Sprintf(`ENV=%s`, shellescape.Quote(shellrc))}
316+
extraEnv = map[string]string{"ENV": shellescape.Quote(shellrc)}
322317
case shFish:
323318
extraArgs = []string{"-C", ". " + shellrc}
324319
}
@@ -369,29 +364,6 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
369364
tmpl = fishrcTmpl
370365
}
371366

372-
exportEnv := ""
373-
strb := strings.Builder{}
374-
for _, kv := range s.env {
375-
k, v, ok := strings.Cut(kv, "=")
376-
if !ok {
377-
continue
378-
}
379-
strb.WriteString("export ")
380-
strb.WriteString(k)
381-
strb.WriteString(`="`)
382-
for _, r := range v {
383-
switch r {
384-
// Special characters inside double quotes:
385-
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
386-
case '$', '`', '"', '\\', '\n':
387-
strb.WriteRune('\\')
388-
}
389-
strb.WriteRune(r)
390-
}
391-
strb.WriteString("\"\n")
392-
}
393-
exportEnv = strings.TrimSpace(strb.String())
394-
395367
err = tmpl.Execute(shellrcf, struct {
396368
ProjectDir string
397369
OriginalInit string
@@ -413,7 +385,7 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
413385
ScriptCommand: strings.TrimSpace(s.ScriptCommand),
414386
ShellStartTime: s.shellStartTime,
415387
HistoryFile: strings.TrimSpace(s.historyFile),
416-
ExportEnv: exportEnv,
388+
ExportEnv: exportify(s.env),
417389
})
418390
if err != nil {
419391
return "", fmt.Errorf("execute shellrc template: %v", err)

internal/impl/shell_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ func TestWriteDevboxShellrcWithUnifiedEnv(t *testing.T) {
3333
}
3434

3535
func testWriteDevboxShellrc(t *testing.T, testdirs []string) {
36-
// Load up all the necessary data from each testdata/shellrc directory
36+
// Load up all the necessary data from each internal/nix/testdata/shellrc directory
3737
// into a slice of tests cases.
3838
tests := make([]struct {
3939
name string
40-
env []string
40+
env map[string]string
4141
hook string
4242
shellrcPath string
4343
goldShellrcPath string
@@ -48,7 +48,7 @@ func testWriteDevboxShellrc(t *testing.T, testdirs []string) {
4848
test := &tests[i]
4949
test.name = filepath.Base(path)
5050
if b, err := os.ReadFile(filepath.Join(path, "env")); err == nil {
51-
test.env = strings.Split(string(b), "\n")
51+
test.env = pairsToMap(strings.Split(string(b), "\n"))
5252
}
5353
if b, err := os.ReadFile(filepath.Join(path, "hook")); err == nil {
5454
test.hook = string(b)

0 commit comments

Comments
 (0)