From c97f2eeadb9524ff0a5ef8fc66e4152b804ad135 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 28 May 2026 18:14:37 -0700 Subject: [PATCH] fix(core): preserve zsh tied path exports with combined flags Co-authored-by: Codex noreply@openai.com --- codex-rs/core/src/shell_snapshot.rs | 7 ++- codex-rs/core/src/shell_snapshot_tests.rs | 77 ++++++++++++----------- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index 0f83d0b45f..55a69c61b0 100644 --- a/codex-rs/core/src/shell_snapshot.rs +++ b/codex-rs/core/src/shell_snapshot.rs @@ -342,9 +342,10 @@ export_lines=$(export -p | awk ' line=$0 name=line sub(/^(export|declare -x|typeset -x) /, "", name) - if (name ~ /^-T [A-Za-z_][A-Za-z0-9_]* [A-Za-z_][A-Za-z0-9_]*=/) { - # Zsh prints tied parameters like PATH/path as `export -T NAME tied=(...)`. - sub(/^-T /, "", name) + if (name ~ /^-[A-Za-z]*T[A-Za-z]* [A-Za-z_][A-Za-z0-9_]* [A-Za-z_][A-Za-z0-9_]*=/) { + # Zsh prints tied parameters like PATH/path as `export -T NAME tied=(...)`; + # the option group may include flags such as `-UT`. + sub(/^-[A-Za-z]*T[A-Za-z]* /, "", name) sub(/ .*/, "", name) } else { sub(/=.*/, "", name) diff --git a/codex-rs/core/src/shell_snapshot_tests.rs b/codex-rs/core/src/shell_snapshot_tests.rs index 859e505011..d098e73e9c 100644 --- a/codex-rs/core/src/shell_snapshot_tests.rs +++ b/codex-rs/core/src/shell_snapshot_tests.rs @@ -189,47 +189,52 @@ fn bash_snapshot_preserves_multiline_exports() -> Result<()> { #[cfg(target_os = "macos")] #[test] -fn zsh_snapshot_preserves_tied_path_export() -> Result<()> { - let dir = tempdir()?; - let tool_dir = dir.path().join("toolbin"); - let expected_path = format!("{}:/usr/bin:/bin", tool_dir.display()); - std::fs::write( - dir.path().join(".zshrc"), - format!("export PATH='{expected_path}'\n"), - )?; +fn zsh_snapshot_preserves_tied_path_exports() -> Result<()> { + for (zshrc_prefix, expected_export) in [ + ("", "export -T PATH path="), + ("typeset -U PATH path\n", "export -UT PATH path="), + ] { + let dir = tempdir()?; + let tool_dir = dir.path().join("toolbin"); + let expected_path = format!("{}:/usr/bin:/bin", tool_dir.display()); + std::fs::write( + dir.path().join(".zshrc"), + format!("{zshrc_prefix}export PATH='{expected_path}'\n"), + )?; - let output = Command::new("/bin/zsh") - .arg("-fc") - .arg(zsh_snapshot_script()) - .env("ZDOTDIR", dir.path()) - .output()?; - assert!(output.status.success()); + let output = Command::new("/bin/zsh") + .arg("-fc") + .arg(zsh_snapshot_script()) + .env("ZDOTDIR", dir.path()) + .output()?; + assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - stdout.contains("export -T PATH path="), - "snapshot should include zsh tied PATH export; stdout={stdout:?}" - ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains(expected_export), + "snapshot should include {expected_export:?}; stdout={stdout:?}" + ); - let snapshot_path = dir.path().join("snapshot.sh"); - std::fs::write(&snapshot_path, stdout.as_bytes())?; + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write(&snapshot_path, stdout.as_bytes())?; - let validate = Command::new("/bin/zsh") - .arg("-fc") - .arg("PATH=/should/not/survive; . \"$1\"; print -r -- \"$PATH\"") - .arg("zsh") - .arg(&snapshot_path) - .output()?; - assert!( - validate.status.success(), - "snapshot validation failed: {}", - String::from_utf8_lossy(&validate.stderr) - ); + let validate = Command::new("/bin/zsh") + .arg("-fc") + .arg("PATH=/should/not/survive; . \"$1\"; print -r -- \"$PATH\"") + .arg("zsh") + .arg(&snapshot_path) + .output()?; + assert!( + validate.status.success(), + "snapshot validation failed: {}", + String::from_utf8_lossy(&validate.stderr) + ); - assert_eq!( - String::from_utf8_lossy(&validate.stdout).trim(), - expected_path - ); + assert_eq!( + String::from_utf8_lossy(&validate.stdout).trim(), + expected_path + ); + } Ok(()) }