Skip to content

Commit 633f755

Browse files
authored
impl: set SHELL environment variable (#1512)
We were assuming that most shells set `SHELL` upon startup, but this is only true for bash. Usually `SHELL` is only set by `login`, so we need to set it ourselves when launching the user's shell. Also remove the `keyEqualsValue` which was only being used by the VSCode integration. This function escapes environment variable values for a shell, but we were setting the `code` process's environment with them directly. This was mangling the values by adding quotes and backslashes. Fixes #1454.
1 parent 56dcac5 commit 633f755

File tree

5 files changed

+35
-65
lines changed

5 files changed

+35
-65
lines changed

internal/boxcli/integrate.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"bytes"
88
"encoding/json"
99
"os/exec"
10+
"slices"
11+
"strings"
1012

1113
"github.com/spf13/cobra"
1214
"github.com/zealic/go2node"
@@ -54,7 +56,6 @@ type parentMessage struct {
5456
}
5557

5658
func runIntegrateVSCodeCmd(cmd *cobra.Command) error {
57-
5859
// Setup process communication with node as parent
5960
channel, err := go2node.RunAsNodeChild()
6061
if err != nil {
@@ -85,6 +86,14 @@ func runIntegrateVSCodeCmd(cmd *cobra.Command) error {
8586
if err != nil {
8687
return err
8788
}
89+
envVars = slices.DeleteFunc(envVars, func(s string) bool {
90+
k, _, ok := strings.Cut(s, "=")
91+
// DEVBOX_OG_PATH_<hash> being set causes devbox global shellenv to overwrite the
92+
// PATH after VSCode opens and resets it to global shellenv. This causes the VSCode
93+
// terminal to not be able to find devbox packages after the reopen in devbox
94+
// environment action is called.
95+
return ok && (strings.HasPrefix(k, "DEVBOX_OG_PATH") || k == "HOME" || k == "NODE_CHANNEL_FD")
96+
})
8897

8998
// Send message to parent process to terminate
9099
err = channel.Write(&go2node.NodeMessage{

internal/impl/devbox.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ type Devbox struct {
7171
var legacyPackagesWarningHasBeenShown = false
7272

7373
func Open(opts *devopt.Opts) (*Devbox, error) {
74-
7574
projectDir, err := findProjectDir(opts.Dir)
7675
if err != nil {
7776
return nil, err
@@ -318,7 +317,7 @@ func (d *Devbox) EnvVars(ctx context.Context) ([]string, error) {
318317
if err != nil {
319318
return nil, err
320319
}
321-
return keyEqualsValue(envs), nil
320+
return mapToPairs(envs), nil
322321
}
323322

324323
func (d *Devbox) ShellEnvHash(ctx context.Context) (string, error) {
@@ -684,7 +683,6 @@ func (d *Devbox) StartProcessManager(
684683
background bool,
685684
processComposeFileOrDir string,
686685
) error {
687-
688686
if !d.IsEnvEnabled() {
689687
args := []string{"services", "up"}
690688
args = append(args, requestedServices...)

internal/impl/envvars.go

Lines changed: 22 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,31 @@
44
package impl
55

66
import (
7-
"fmt"
87
"os"
9-
"sort"
8+
"slices"
109
"strings"
1110
)
1211

1312
const devboxSetPrefix = "__DEVBOX_SET_"
1413

14+
// mapToPairs creates a slice of environment variable "key=value" pairs from a
15+
// map.
1516
func mapToPairs(m map[string]string) []string {
16-
pairs := []string{}
17+
pairs := make([]string, len(m))
18+
i := 0
1719
for k, v := range m {
18-
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
20+
pairs[i] = k + "=" + v
21+
i++
1922
}
23+
slices.Sort(pairs) // for reproducibility
2024
return pairs
2125
}
2226

27+
// pairsToMap creates a map from a slice of "key=value" environment variable
28+
// pairs. Note that maps are not ordered, which can affect the final variable
29+
// values when pairs contains duplicate keys.
2330
func pairsToMap(pairs []string) map[string]string {
24-
vars := map[string]string{}
31+
vars := make(map[string]string, len(pairs))
2532
for _, p := range pairs {
2633
k, v, ok := strings.Cut(p, "=")
2734
if !ok {
@@ -32,14 +39,19 @@ func pairsToMap(pairs []string) map[string]string {
3239
return vars
3340
}
3441

35-
// exportify takes a map of [string]string and returns a single string
36-
// of the form export KEY="VAL"; and escapes all the vals from special characters.
42+
// exportify formats vars as a line-separated string of shell export statements.
43+
// Each line is of the form `export key="value";` with any special characters in
44+
// value escaped. This means that the shell will always interpret values as
45+
// literal strings; no variable expansion or command substitution will take
46+
// place.
3747
func exportify(vars map[string]string) string {
38-
keys := make([]string, 0, len(vars))
48+
keys := make([]string, len(vars))
49+
i := 0
3950
for k := range vars {
40-
keys = append(keys, k)
51+
keys[i] = k
52+
i++
4153
}
42-
sort.Strings(keys)
54+
slices.Sort(keys) // for reproducibility
4355

4456
strb := strings.Builder{}
4557
for _, k := range keys {
@@ -60,56 +72,6 @@ func exportify(vars map[string]string) string {
6072
return strings.TrimSpace(strb.String())
6173
}
6274

63-
// exportify takes a map of [string]string and returns an array of string
64-
// of the form KEY="VAL" and escapes all the vals from special characters.
65-
func keyEqualsValue(vars map[string]string) []string {
66-
keys := make([]string, 0, len(vars))
67-
for k := range vars {
68-
keys = append(keys, k)
69-
}
70-
keyValues := make([]string, 0, len(vars))
71-
sort.Strings(keys)
72-
73-
for _, k := range keys {
74-
if isApproved(k) {
75-
strb := strings.Builder{}
76-
strb.WriteString(k)
77-
strb.WriteString(`=`)
78-
for _, r := range vars[k] {
79-
switch r {
80-
// Special characters inside double quotes:
81-
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
82-
case '$', '`', '"', '\\', '\n':
83-
strb.WriteRune('\\')
84-
}
85-
strb.WriteRune(r)
86-
}
87-
keyValues = append(keyValues, strb.String())
88-
}
89-
}
90-
return keyValues
91-
}
92-
93-
func isApproved(key string) bool {
94-
// list to keys
95-
// should find the corrupt key
96-
troublingEnvKeys := []string{
97-
"HOME",
98-
"NODE_CHANNEL_FD",
99-
}
100-
approved := true
101-
for _, ak := range troublingEnvKeys {
102-
// DEVBOX_OG_PATH_<hash> being set causes devbox global shellenv or overwrite
103-
// the PATH after vscode opens and resets it to global shellenv
104-
// This causes vscode terminal to not be able to find devbox packages
105-
// after reopen in devbox environment action is called
106-
if key == ak || strings.HasPrefix(key, "DEVBOX_OG_PATH") {
107-
approved = false
108-
}
109-
}
110-
return approved
111-
}
112-
11375
// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
11476
// but only if the key was not previously set by devbox
11577
// Caveat, this won't mark the values as set by devbox automatically. Instead,

internal/impl/shell.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ func (s *DevboxShell) Run() error {
244244
for k, v := range extraEnv {
245245
env[k] = v
246246
}
247+
env["SHELL"] = s.binPath
247248

248249
cmd = exec.Command(s.binPath)
249250
cmd.Env = mapToPairs(env)

internal/impl/shell_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func testWriteDevboxShellrc(t *testing.T, testdirs []string) {
8383
// set, and then proceed normally. The test should
8484
// always pass in this case.
8585
if *update {
86-
err = os.WriteFile(test.goldShellrcPath, gotShellrc, 0666)
86+
err = os.WriteFile(test.goldShellrcPath, gotShellrc, 0o666)
8787
if err != nil {
8888
t.Error("Error updating golden files:", err)
8989
}

0 commit comments

Comments
 (0)