Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/devbox/envvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,14 @@ func exportify(w io.Writer, vars map[string]string) string {
switch r {
// Special characters inside double quotes:
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
case '$', '`', '"', '\\', '\n':
//
// Note: a literal newline is NOT escaped. Inside double quotes
// a bare newline is preserved verbatim, whereas a backslash
// immediately followed by a newline is a line continuation that
// the shell deletes. Escaping it would join a multi-line value's
// adjacent lines and corrupt values like a multi-command
// PROMPT_COMMAND (see jetify-com/devbox#2814).
case '$', '`', '"', '\\':
strb.WriteRune('\\')
}
strb.WriteRune(r)
Expand Down
21 changes: 21 additions & 0 deletions internal/devbox/envvars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ func TestExportifySkipsInvalidNames(t *testing.T) {
}
}

// TestExportifyPreservesNewlines ensures that multi-line values (e.g. a
// PROMPT_COMMAND made of several commands separated by newlines) are emitted so
// that the shell preserves the newlines. A backslash before a newline would be a
// line continuation that the shell deletes, joining adjacent lines and corrupting
// the value (see jetify-com/devbox#2814).
func TestExportifyPreservesNewlines(t *testing.T) {
value := "__bp_precmd_invoke_cmd\ndbus-send ... >/dev/null 2>&1\n__bp_interactive_mode"
got := exportify(io.Discard, map[string]string{"PROMPT_COMMAND": value})

// The emitted value must contain a bare newline, not an escaped one. If the
// newline were escaped, "2>&1\" and "__bp_interactive_mode" would collapse
// into "2>&1__bp_interactive_mode" when the shell sourced the export.
if strings.Contains(got, "\\\n") {
t.Errorf("newline should not be backslash-escaped, got:\n%q", got)
}
want := "export PROMPT_COMMAND=\"" + value + "\";"
if !strings.Contains(got, want) {
t.Errorf("expected exported value to preserve newlines.\ngot:\n%q\nwant to contain:\n%q", got, want)
}
}

func TestExportifyNushellSkipsInvalidNames(t *testing.T) {
got := exportifyNushell(io.Discard, map[string]string{
"GOOD": "value",
Expand Down
Loading