From 9325b89f07d941d4eed70078a0bad7a43594230d Mon Sep 17 00:00:00 2001 From: Andrei Eternal Date: Wed, 28 Jan 2026 15:31:06 -0800 Subject: [PATCH] Basic handling of env variable snapshots that contain newlines and quotes --- codex-rs/core/src/shell_snapshot.rs | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index 78581186c7..a5cdefe83b 100644 --- a/codex-rs/core/src/shell_snapshot.rs +++ b/codex-rs/core/src/shell_snapshot.rs @@ -235,6 +235,17 @@ export_lines=$(export -p | awk ' name=line sub(/^(export|declare -x|typeset -x) /, "", name) sub(/=.*/, "", name) + tmp=line + gsub(/\\"/, "", tmp) + quote_count=gsub(/"/, "", tmp) + if ((quote_count % 2) == 1 && line !~ /\\$/) { + next + } + if (line ~ /\\$/) { + while (line ~ /\\$/ && (getline continuation) > 0) { + line = line "\n" continuation + } + } if (name ~ /^(EXCLUDED_EXPORTS)$/) { next } @@ -279,6 +290,17 @@ export_lines=$(export -p | awk ' name=line sub(/^(export|declare -x|typeset -x) /, "", name) sub(/=.*/, "", name) + tmp=line + gsub(/\\"/, "", tmp) + quote_count=gsub(/"/, "", tmp) + if ((quote_count % 2) == 1 && line !~ /\\$/) { + next + } + if (line ~ /\\$/) { + while (line ~ /\\$/ && (getline continuation) > 0) { + line = line "\n" continuation + } + } if (name ~ /^(EXCLUDED_EXPORTS)$/) { next } @@ -336,6 +358,17 @@ if export -p >/dev/null 2>&1; then name=line sub(/^(export|declare -x|typeset -x) /, "", name) sub(/=.*/, "", name) + tmp=line + gsub(/\\"/, "", tmp) + quote_count=gsub(/"/, "", tmp) + if ((quote_count % 2) == 1 && line !~ /\\$/) { + next + } + if (line ~ /\\$/) { + while (line ~ /\\$/ && (getline continuation) > 0) { + line = line "\n" continuation + } + } if (name ~ /^(EXCLUDED_EXPORTS)$/) { next } @@ -528,6 +561,38 @@ mod tests { Ok(()) } + #[cfg(unix)] + #[test] + fn bash_snapshot_handles_multiline_exports() -> Result<()> { + let output = Command::new("/bin/bash") + .arg("-c") + .arg(bash_snapshot_script()) + .env_clear() + .env("HOME", "/nonexistent") + .env("PATH", "/usr/bin:/bin") + .env("BASH_ENV", "/dev/null") + .env("CODEX_MULTILINE_EXPORT", "line-1\nline-2") + .output()?; + + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let snapshot = strip_snapshot_preamble(&stdout)?; + + let dir = tempdir()?; + let path = dir.path().join("snapshot.sh"); + std::fs::write(&path, snapshot)?; + + let syntax_check = Command::new("/bin/bash").arg("-n").arg(&path).output()?; + assert!( + syntax_check.status.success(), + "snapshot should be valid bash: {}", + String::from_utf8_lossy(&syntax_check.stderr) + ); + + Ok(()) + } + #[cfg(unix)] #[tokio::test] async fn try_new_creates_and_deletes_snapshot_file() -> Result<()> {