From 1c94ca4e78ed7a453e7eede44434f358677315c6 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Tue, 9 Jun 2026 14:44:12 -0700 Subject: [PATCH] fix(shell): quote shellrc source guard so ZDOTDIR with spaces works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `[ -f ]` guard around sourcing the user's shellrc was unquoted. When ZDOTDIR (and thus the path) contains a space — e.g. a terminal integration setting ".../Application Support/..." — the path split into multiple words, `[` failed with "too many arguments", and the user's real shellrc never got sourced. Quote the path in both the zsh and bash branches. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/devbox/shell_test.go | 60 +++++++++++++++++++ internal/devbox/shellrc.tmpl | 4 +- .../testdata/shellrc/basic/shellrc.golden | 2 +- .../shellrc/zsh_zdotdir/shellrc.golden | 2 +- 4 files changed, 64 insertions(+), 4 deletions(-) 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"