diff --git a/internal/devbox/shell_test.go b/internal/devbox/shell_test.go index 952286bfacb..ee361a14564 100644 --- a/internal/devbox/shell_test.go +++ b/internal/devbox/shell_test.go @@ -8,6 +8,7 @@ import ( "flag" "io/fs" "os" + "os/exec" "path/filepath" "regexp" "strings" @@ -487,3 +488,62 @@ func TestWriteDevboxShellrcWithZDOTDIR(t *testing.T) { t.Error("Expected shellrc to source the custom .zshrc file") } } + +// TestWriteDevboxShellrcZDOTDIRWithSpaces guards against a regression where the +// `[ -f ]` guard around sourcing the user's shellrc was unquoted. When +// ZDOTDIR contains spaces (e.g. ".../Application Support/..."), the unquoted +// path expands to multiple words and `[` fails with "too many arguments", so +// the user's real shellrc never gets sourced. +func TestWriteDevboxShellrcZDOTDIRWithSpaces(t *testing.T) { + // A ZDOTDIR with a space, like the one some terminal integrations inject. + zdotdir := filepath.Join(t.TempDir(), "Application Support", "zsh") + if err := os.MkdirAll(zdotdir, 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("ZDOTDIR", zdotdir) + + zshrcPath := filepath.Join(zdotdir, ".zshrc") + if err := os.WriteFile(zshrcPath, []byte("# user zshrc"), 0o644); err != nil { + t.Fatalf("Failed to create test .zshrc: %v", err) + } + + shell := initShellBinaryFields("/usr/bin/zsh") + shell.devbox = &Devbox{projectDir: "/test/project"} + shell.projectDir = "/test/project" + + shellrcPath, err := shell.writeDevboxShellrc() + if err != nil { + t.Fatalf("Failed to write devbox shellrc: %v", err) + } + content, err := os.ReadFile(shellrcPath) + if err != nil { + t.Fatalf("Failed to read generated shellrc: %v", err) + } + contentStr := string(content) + + // The guard must quote the path so the space doesn't split into words. + wantGuard := `if [ -f "` + zshrcPath + `" ]; then` + if !strings.Contains(contentStr, wantGuard) { + t.Errorf("expected quoted guard %q in generated shellrc:\n%s", wantGuard, contentStr) + } + + // Run the actual generated guard line through /bin/sh to prove it doesn't + // error with "too many arguments" on a space-containing path. + var guardLine string + for _, line := range strings.Split(contentStr, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "if [ -f ") { + guardLine = strings.TrimSpace(line) + " echo sourced; fi" + break + } + } + if guardLine == "" { + t.Fatalf("could not find guard line in generated shellrc:\n%s", contentStr) + } + out, err := exec.Command("/bin/sh", "-c", guardLine).CombinedOutput() + if err != nil { + t.Fatalf("guard line failed to execute (%v): %s\nline: %s", err, out, guardLine) + } + if strings.TrimSpace(string(out)) != "sourced" { + t.Errorf("guard line did not take the true branch; output: %q\nline: %s", out, guardLine) + } +} diff --git a/internal/devbox/shellrc.tmpl b/internal/devbox/shellrc.tmpl index 037012a22c1..1ff790d496b 100644 --- a/internal/devbox/shellrc.tmpl +++ b/internal/devbox/shellrc.tmpl @@ -20,14 +20,14 @@ content readable. {{- if .OriginalInitPath -}} {{- if eq .ShellName "zsh" -}} -if [ -f {{ .OriginalInitPath }} ]; then +if [ -f "{{ .OriginalInitPath }}" ]; then local DEVBOX_ZDOTDIR="$ZDOTDIR" export ZDOTDIR="{{dirPath .OriginalInitPath}}" . "{{ .OriginalInitPath }}" export ZDOTDIR="$DEVBOX_ZDOTDIR" fi {{ else -}} -if [ -f {{ .OriginalInitPath }} ]; then +if [ -f "{{ .OriginalInitPath }}" ]; then . "{{ .OriginalInitPath }}" fi {{ end -}} diff --git a/internal/devbox/testdata/shellrc/basic/shellrc.golden b/internal/devbox/testdata/shellrc/basic/shellrc.golden index 45a901c3a5e..b22fbfb2a71 100644 --- a/internal/devbox/testdata/shellrc/basic/shellrc.golden +++ b/internal/devbox/testdata/shellrc/basic/shellrc.golden @@ -1,4 +1,4 @@ -if [ -f testdata/shellrc/basic/shellrc ]; then +if [ -f "testdata/shellrc/basic/shellrc" ]; then . "testdata/shellrc/basic/shellrc" fi # Begin Devbox Post-init Hook diff --git a/internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc.golden b/internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc.golden index 19194369a52..bb23c48724c 100644 --- a/internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc.golden +++ b/internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc.golden @@ -1,4 +1,4 @@ -if [ -f testdata/shellrc/zsh_zdotdir/shellrc ]; then +if [ -f "testdata/shellrc/zsh_zdotdir/shellrc" ]; then local DEVBOX_ZDOTDIR="$ZDOTDIR" export ZDOTDIR="testdata/shellrc/zsh_zdotdir" . "testdata/shellrc/zsh_zdotdir/shellrc"