mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(terminal): respect user shell for provider terminals (#4140)
* fix(terminal): detect user default shell instead of hardcoded bash All macOS/Linux terminal launch functions in misc.rs hardcoded `bash` as the execution shell, causing zsh users' .zshrc to not load. Now detect the user's default shell via $SHELL (macOS fallback /bin/zsh, Linux fallback /bin/bash) and use it consistently across shebang, AppleScript, spawn args, and interactive shell flags. - Add get_user_shell(), get_shell_name(), interactive_flags_for_shell() - Replace 10 hardcoded bash locations with dynamic shell detection - zsh gets --norcs --no-globalrcs, bash keeps --norc --noprofile - run_tool_lifecycle_silently unchanged (bash-specific syntax) - Fixes #1546, Related #2385 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(terminal): use POSIX sh for script execution, user shell for interactive session Address code review issues: 1. Replace shell-specific `read -n 1 -s`/`read -k 1 -s` with POSIX `read -r _` (press Enter to close) — ensures portability across bash/zsh/fish/dash 2. Unify AppleScript to use `sh` consistently (not mixing shell/shell_name) 3. Protect fish users: scripts always interpreted by POSIX sh, not fish (fish doesn't support trap/exec). exec line switches to user's shell. 4. Use build_exec_line to avoid trailing space when interactive_flags is empty All terminal launchers now execute scripts via `sh`. Script shebangs are `#!/bin/sh`. The exec line at script end switches to the user's detected shell with appropriate clean-start flags. Terminal launcher functions no longer need shell/shell_name parameters — simplified signatures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: validate user shell for terminal launch * fix: load user shell config after terminal command * test: align terminal shell exec expectations * refactor(terminal): remove dead code, quote Warp path, unify shebangs - Remove `interactive_flags_for_shell` (always returned "") and `get_shell_name` (no longer needed after removing flags); simplify `build_exec_line` to single-arg form - Quote the Warp launcher script path with `shell_single_quote` for consistency with the main launcher scripts - Unify all script shebangs to `#!/usr/bin/env sh` for better portability (e.g. NixOS where /bin/sh may not exist) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(terminal): fix linux shell exec helper call * fix(terminal): run provider command through user shell * fix(terminal): validate user shell executable * fix(terminal): avoid interactive provider shell job * fix(terminal): harden user shell launcher Load zsh provider commands through the user's login and interactive environment, preserve launch cwd handling, and replace Terminal/iTerm launcher shells so the final user shell is restored cleanly. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+292
-52
@@ -1069,6 +1069,111 @@ fn default_flag_for_shell(shell: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_user_shell() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"/bin/zsh"
|
||||
} else {
|
||||
"/bin/bash"
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_user_shell_path(shell: &str) -> bool {
|
||||
if shell.is_empty()
|
||||
|| !shell.starts_with('/')
|
||||
|| !is_valid_shell(shell)
|
||||
|| shell.chars().any(char::is_control)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let path = std::path::Path::new(shell);
|
||||
path.is_file() && is_executable_file(path)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_executable_file(path: &std::path::Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
path.metadata()
|
||||
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn is_executable_file(path: &std::path::Path) -> bool {
|
||||
path.is_file()
|
||||
}
|
||||
|
||||
/// 获取用户默认 shell 的完整路径;异常或被污染的 SHELL 回退到平台默认值。
|
||||
fn get_user_shell() -> String {
|
||||
std::env::var("SHELL")
|
||||
.ok()
|
||||
.filter(|shell| valid_user_shell_path(shell))
|
||||
.unwrap_or_else(|| fallback_user_shell().to_string())
|
||||
}
|
||||
|
||||
/// 构建 exec 行:引号保护 shell 路径,交还用户 shell 让其按默认规则加载 rc 配置。
|
||||
fn build_exec_line(shell: &str, cwd: Option<&Path>) -> String {
|
||||
let quoted_shell = shell_single_quote(shell);
|
||||
|
||||
match shell.rsplit('/').next().unwrap_or(shell) {
|
||||
"zsh" => cwd
|
||||
.map(|dir| {
|
||||
let command = format!(
|
||||
"cd {} || exit 1; exec {} -i",
|
||||
shell_single_quote(&dir.to_string_lossy()),
|
||||
quoted_shell
|
||||
);
|
||||
format!("exec {} -lc {}", quoted_shell, shell_single_quote(&command))
|
||||
})
|
||||
.unwrap_or_else(|| format!("exec {quoted_shell} -l")),
|
||||
_ => format!("exec {quoted_shell}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 provider 命令行:通过用户 shell 的交互模式执行,确保 GUI 启动的终端也加载用户 PATH。
|
||||
fn build_provider_command_line(shell: &str, config_path: &str, cwd: Option<&Path>) -> String {
|
||||
let claude_command = format!("claude --settings {}", shell_single_quote(config_path));
|
||||
let command = cwd
|
||||
.map(|dir| {
|
||||
format!(
|
||||
"cd {} && {}",
|
||||
shell_single_quote(&dir.to_string_lossy()),
|
||||
claude_command
|
||||
)
|
||||
})
|
||||
.unwrap_or(claude_command);
|
||||
|
||||
format!(
|
||||
"{} {} {}",
|
||||
shell_single_quote(shell),
|
||||
provider_command_flag_for_shell(shell),
|
||||
shell_single_quote(&command)
|
||||
)
|
||||
}
|
||||
|
||||
fn provider_command_flag_for_shell(shell: &str) -> &'static str {
|
||||
match shell.rsplit('/').next().unwrap_or(shell) {
|
||||
"dash" | "sh" => "-c",
|
||||
"zsh" => "-lic",
|
||||
_ => "-ic",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_final_shell_cd_command(shell: &str, cwd: Option<&Path>) -> String {
|
||||
if matches!(shell.rsplit('/').next().unwrap_or(shell), "zsh") {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
cwd.map(|dir| {
|
||||
format!(
|
||||
"cd {} || exit 1\n",
|
||||
shell_single_quote(&dir.to_string_lossy())
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn try_get_version_wsl(
|
||||
tool: &str,
|
||||
@@ -2642,24 +2747,31 @@ fn launch_macos_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> R
|
||||
let preferred = crate::settings::get_preferred_terminal();
|
||||
let terminal = preferred.as_deref().unwrap_or("terminal");
|
||||
|
||||
let shell = get_user_shell();
|
||||
let exec_line = build_exec_line(&shell, cwd);
|
||||
let final_cd_command = build_final_shell_cd_command(&shell, cwd);
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||
let config_path = config_file.to_string_lossy();
|
||||
let cd_command = build_shell_cd_command(cwd);
|
||||
let provider_command = build_provider_command_line(&shell, &config_path, cwd);
|
||||
|
||||
// Write the shell script to a temp file
|
||||
// 脚本使用 POSIX sh 语法确保可移植性,exec 行切换到用户交互式 shell
|
||||
let script_content = format!(
|
||||
r#"#!/bin/bash
|
||||
r#"#!/usr/bin/env sh
|
||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||
{cd_command}
|
||||
echo "Using provider-specific claude config:"
|
||||
echo "{config_path}"
|
||||
claude --settings "{config_path}"
|
||||
exec bash --norc --noprofile
|
||||
{provider_command}
|
||||
{final_cd_command}
|
||||
{exec_line}
|
||||
"#,
|
||||
config_path = config_path,
|
||||
script_file = script_file.display(),
|
||||
cd_command = cd_command,
|
||||
provider_command = provider_command,
|
||||
final_cd_command = final_cd_command,
|
||||
exec_line = exec_line,
|
||||
);
|
||||
|
||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||
@@ -2678,7 +2790,7 @@ exec bash --norc --noprofile
|
||||
"ghostty" => launch_macos_ghostty(&script_file),
|
||||
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
|
||||
"kaku" => launch_macos_open_app("Kaku", &script_file, true),
|
||||
_ => launch_macos_terminal_app(&script_file), // "terminal" or default
|
||||
_ => launch_macos_terminal_app(&script_file),
|
||||
};
|
||||
|
||||
// If preferred terminal fails and it's not the default, try Terminal.app as fallback
|
||||
@@ -2704,7 +2816,16 @@ fn applescript_string_literal(value: &str) -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn applescript_launcher_command(script_file: &std::path::Path) -> String {
|
||||
applescript_string_literal(&format!(
|
||||
"bash {}",
|
||||
"sh {}",
|
||||
shell_single_quote(&script_file.to_string_lossy())
|
||||
))
|
||||
}
|
||||
|
||||
/// Build a launcher command that replaces the terminal-created shell session.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn applescript_exec_launcher_command(script_file: &std::path::Path) -> String {
|
||||
applescript_string_literal(&format!(
|
||||
"exec sh {}",
|
||||
shell_single_quote(&script_file.to_string_lossy())
|
||||
))
|
||||
}
|
||||
@@ -2727,7 +2848,7 @@ tell application "Terminal"
|
||||
activate
|
||||
end if
|
||||
end tell"#,
|
||||
launcher = applescript_launcher_command(script_file)
|
||||
launcher = applescript_exec_launcher_command(script_file)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2795,7 +2916,7 @@ tell application "iTerm"
|
||||
write text launcher_script
|
||||
end tell
|
||||
end tell"#,
|
||||
launcher = applescript_launcher_command(script_file)
|
||||
launcher = applescript_exec_launcher_command(script_file)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2805,12 +2926,12 @@ fn launch_macos_iterm2(script_file: &std::path::Path) -> Result<(), String> {
|
||||
run_terminal_osascript(&build_macos_iterm2_applescript(script_file), "iTerm2")
|
||||
}
|
||||
|
||||
/// Keep the launcher path inside a `bash -c` string.
|
||||
/// Keep the launcher path inside a `sh -c` string.
|
||||
/// A bare `.sh` passed through `open --args` may also be opened as a document.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_macos_dash_c_command(script_file: &std::path::Path) -> String {
|
||||
format!(
|
||||
"exec bash {}",
|
||||
"exec sh {}",
|
||||
shell_single_quote(&script_file.to_string_lossy())
|
||||
)
|
||||
}
|
||||
@@ -2865,8 +2986,8 @@ fn launch_macos_open_app(
|
||||
if use_e_flag {
|
||||
cmd.arg("-e");
|
||||
}
|
||||
// Keep the script path inside `bash -c`; a trailing bare `.sh` can be opened as a document.
|
||||
cmd.arg("bash")
|
||||
// Keep the script path inside `sh -c`; a trailing bare `.sh` can be opened as a document.
|
||||
cmd.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(build_macos_dash_c_command(script_file));
|
||||
|
||||
@@ -2912,9 +3033,9 @@ fn launch_macos_warp(script_file: &std::path::Path) -> Result<(), String> {
|
||||
|
||||
rm -- "$0"
|
||||
|
||||
exec bash {}
|
||||
exec sh {quoted_script}
|
||||
"#,
|
||||
script_file.display(),
|
||||
quoted_script = shell_single_quote(&script_file.to_string_lossy()),
|
||||
)
|
||||
.map_err(|e| format!("Failed to write to temporary script file for Warp: {e}"))?;
|
||||
|
||||
@@ -2946,6 +3067,10 @@ fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> R
|
||||
|
||||
let preferred = crate::settings::get_preferred_terminal();
|
||||
|
||||
let shell = get_user_shell();
|
||||
let exec_line = build_exec_line(&shell, cwd);
|
||||
let final_cd_command = build_final_shell_cd_command(&shell, cwd);
|
||||
|
||||
// Default terminal list with their arguments
|
||||
let default_terminals = [
|
||||
("gnome-terminal", vec!["--"]),
|
||||
@@ -2962,20 +3087,22 @@ fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> R
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||
let config_path = config_file.to_string_lossy();
|
||||
let cd_command = build_shell_cd_command(cwd);
|
||||
let provider_command = build_provider_command_line(&shell, &config_path, cwd);
|
||||
|
||||
let script_content = format!(
|
||||
r#"#!/bin/bash
|
||||
r#"#!/usr/bin/env sh
|
||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||
{cd_command}
|
||||
echo "Using provider-specific claude config:"
|
||||
echo "{config_path}"
|
||||
claude --settings "{config_path}"
|
||||
exec bash --norc --noprofile
|
||||
{provider_command}
|
||||
{final_cd_command}
|
||||
{exec_line}
|
||||
"#,
|
||||
config_path = config_path,
|
||||
script_file = script_file.display(),
|
||||
cd_command = cd_command,
|
||||
provider_command = provider_command,
|
||||
final_cd_command = final_cd_command,
|
||||
exec_line = exec_line,
|
||||
);
|
||||
|
||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||
@@ -3019,7 +3146,7 @@ exec bash --norc --noprofile
|
||||
if terminal_exists {
|
||||
let result = Command::new(terminal)
|
||||
.args(&args)
|
||||
.arg("bash")
|
||||
.arg("sh")
|
||||
.arg(script_file.to_string_lossy().as_ref())
|
||||
.spawn();
|
||||
|
||||
@@ -3106,16 +3233,6 @@ del \"%~f0\" >nul 2>&1
|
||||
result
|
||||
}
|
||||
|
||||
fn build_shell_cd_command(cwd: Option<&Path>) -> String {
|
||||
cwd.map(|dir| {
|
||||
format!(
|
||||
"cd {} || exit 1\n",
|
||||
shell_single_quote(&dir.to_string_lossy())
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn shell_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
||||
}
|
||||
@@ -3182,7 +3299,7 @@ fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), S
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 打开用户首选终端并在其中执行一段可信命令脚本。脚本尾部 `read -n 1` / `pause`
|
||||
/// 打开用户首选终端并在其中执行一段可信命令脚本。脚本尾部 `read -r` / `pause`
|
||||
/// 是刻意设计的——让命令退出后窗口不要瞬间关闭,用户才看得到 `command
|
||||
/// not found` / `ModuleNotFoundError` 这类诊断信息。
|
||||
///
|
||||
@@ -3196,14 +3313,14 @@ pub(crate) fn launch_terminal_running(command_line: &str, label: &str) -> Result
|
||||
let (script_file, script_content) = {
|
||||
let file = temp_dir.join(format!("cc_switch_{}_{}.sh", label, pid));
|
||||
let content = format!(
|
||||
r#"#!/bin/bash
|
||||
r#"#!/usr/bin/env sh
|
||||
trap 'rm -f "{script_path}"' EXIT
|
||||
echo "[cc-switch] Starting: {label}"
|
||||
echo ""
|
||||
{cmd}
|
||||
echo ""
|
||||
echo "[cc-switch] Command exited. Press any key to close."
|
||||
read -n 1 -s
|
||||
echo "[cc-switch] Command exited. Press Enter to close."
|
||||
read -r _
|
||||
"#,
|
||||
script_path = file.display(),
|
||||
label = label,
|
||||
@@ -3299,7 +3416,7 @@ read -n 1 -s
|
||||
if terminal_exists {
|
||||
let spawn_result = Command::new(terminal)
|
||||
.args(&args)
|
||||
.arg("bash")
|
||||
.arg("sh")
|
||||
.arg(script_file.to_string_lossy().as_ref())
|
||||
.spawn();
|
||||
match spawn_result {
|
||||
@@ -3387,6 +3504,126 @@ mod tests {
|
||||
use super::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_test_executable(path: &Path, executable: bool) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mode = if executable { 0o755 } else { 0o644 };
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
|
||||
.expect("fixture permissions should be set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_exec_line() {
|
||||
assert_eq!(build_exec_line("/bin/zsh", None), "exec '/bin/zsh' -l");
|
||||
assert_eq!(build_exec_line("/bin/bash", None), "exec '/bin/bash'");
|
||||
assert_eq!(
|
||||
build_exec_line("/opt/homebrew dir/bin/fish", None),
|
||||
"exec '/opt/homebrew dir/bin/fish'"
|
||||
);
|
||||
assert_eq!(build_exec_line("/bin/sh", None), "exec '/bin/sh'");
|
||||
assert_eq!(
|
||||
build_exec_line("/tmp/shell'quote/zsh", None),
|
||||
"exec '/tmp/shell'\"'\"'quote/zsh' -l"
|
||||
);
|
||||
assert_eq!(
|
||||
build_exec_line("/bin/zsh", Some(Path::new("/tmp/project"))),
|
||||
r#"exec '/bin/zsh' -lc 'cd '"'"'/tmp/project'"'"' || exit 1; exec '"'"'/bin/zsh'"'"' -i'"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_provider_command_line_uses_user_shell_environment() {
|
||||
assert_eq!(
|
||||
build_provider_command_line("/bin/zsh", "/tmp/claude config.json", None),
|
||||
"'/bin/zsh' -lic 'claude --settings '\"'\"'/tmp/claude config.json'\"'\"''"
|
||||
);
|
||||
assert_eq!(
|
||||
build_provider_command_line(
|
||||
"/bin/bash",
|
||||
"/tmp/claude config.json",
|
||||
Some(Path::new("/tmp/project"))
|
||||
),
|
||||
r#"'/bin/bash' -ic 'cd '"'"'/tmp/project'"'"' && claude --settings '"'"'/tmp/claude config.json'"'"''"#
|
||||
);
|
||||
assert_eq!(
|
||||
build_provider_command_line(
|
||||
"/bin/sh",
|
||||
"/tmp/claude config.json",
|
||||
Some(Path::new("/tmp/project O'Brien"))
|
||||
),
|
||||
r#"'/bin/sh' -c 'cd '"'"'/tmp/project O'"'"'"'"'"'"'"'"'Brien'"'"' && claude --settings '"'"'/tmp/claude config.json'"'"''"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_final_shell_cd_command() {
|
||||
assert_eq!(build_final_shell_cd_command("/bin/zsh", None), "");
|
||||
assert_eq!(
|
||||
build_final_shell_cd_command("/bin/zsh", Some(Path::new("/tmp/project"))),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
build_final_shell_cd_command("/bin/bash", Some(Path::new("/tmp/project O'Brien"))),
|
||||
"cd '/tmp/project O'\"'\"'Brien' || exit 1\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_get_user_shell_fallback() {
|
||||
// $SHELL 未设置时应按平台 fallback
|
||||
// 此测试验证 fallback 逻辑,但不验证环境变量值(取决于运行环境)
|
||||
let shell = get_user_shell();
|
||||
// 至少应返回一个合法的绝对路径
|
||||
assert!(valid_user_shell_path(&shell));
|
||||
// basename 应为合法 shell 名
|
||||
let basename = shell.rsplit('/').next().unwrap_or("sh");
|
||||
assert!(["sh", "bash", "zsh", "fish", "dash"].contains(&basename));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_valid_user_shell_path() {
|
||||
let temp = tempfile::tempdir().expect("temp dir should be created");
|
||||
let executable_zsh = temp.path().join("zsh");
|
||||
std::fs::write(&executable_zsh, "#!/usr/bin/env sh\n")
|
||||
.expect("shell fixture should be written");
|
||||
set_test_executable(&executable_zsh, true);
|
||||
|
||||
let executable_fish_dir = temp.path().join("homebrew dir/bin");
|
||||
std::fs::create_dir_all(&executable_fish_dir)
|
||||
.expect("shell fixture directory should be created");
|
||||
let executable_fish = executable_fish_dir.join("fish");
|
||||
std::fs::write(&executable_fish, "#!/usr/bin/env sh\n")
|
||||
.expect("shell fixture should be written");
|
||||
set_test_executable(&executable_fish, true);
|
||||
|
||||
let non_executable_bash = temp.path().join("bash");
|
||||
std::fs::write(&non_executable_bash, "#!/usr/bin/env sh\n")
|
||||
.expect("shell fixture should be written");
|
||||
set_test_executable(&non_executable_bash, false);
|
||||
|
||||
assert!(valid_user_shell_path(&executable_zsh.to_string_lossy()));
|
||||
assert!(valid_user_shell_path(&executable_fish.to_string_lossy()));
|
||||
assert!(!valid_user_shell_path(""));
|
||||
assert!(!valid_user_shell_path("zsh"));
|
||||
assert!(!valid_user_shell_path(
|
||||
&temp.path().join("missing/zsh").to_string_lossy()
|
||||
));
|
||||
assert!(!valid_user_shell_path(
|
||||
&non_executable_bash.to_string_lossy()
|
||||
));
|
||||
assert!(!valid_user_shell_path(
|
||||
&temp.path().join("zsh; rm -rf /").to_string_lossy()
|
||||
));
|
||||
assert!(!valid_user_shell_path(&format!(
|
||||
"{}\n/bin/bash",
|
||||
executable_zsh.to_string_lossy()
|
||||
)));
|
||||
assert!(!valid_user_shell_path("/usr/bin/powershell"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_version() {
|
||||
assert_eq!(extract_version("claude 1.0.20"), "1.0.20");
|
||||
@@ -4850,13 +5087,6 @@ mod tests {
|
||||
assert!(error.contains("目录不存在"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_shell_cd_command_quotes_spaces_and_single_quotes() {
|
||||
let command = build_shell_cd_command(Some(Path::new("/tmp/project O'Brien")));
|
||||
|
||||
assert_eq!(command, "cd '/tmp/project O'\"'\"'Brien' || exit 1\n");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn iterm2_applescript_cold_start_avoids_current_window_before_one_exists() {
|
||||
@@ -4918,6 +5148,10 @@ mod tests {
|
||||
),
|
||||
"already-running branch should use bare do script:\n{script}"
|
||||
);
|
||||
assert!(
|
||||
script.contains(r#"set launcher_script to "exec sh '/tmp/cc_switch_launcher.sh'""#),
|
||||
"Terminal should replace the auto-created shell:\n{script}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Restored windows should not receive the launcher command.
|
||||
@@ -4943,7 +5177,7 @@ mod tests {
|
||||
|
||||
// Warm launches execute through the AppleScript command property, not `open -na ... -e`.
|
||||
assert!(
|
||||
script.contains(r#"set launcher_command to "bash '/tmp/cc_switch_launcher.sh'""#),
|
||||
script.contains(r#"set launcher_command to "sh '/tmp/cc_switch_launcher.sh'""#),
|
||||
"missing launcher_command:\n{script}"
|
||||
);
|
||||
assert!(script.contains("if was_running then"));
|
||||
@@ -4983,11 +5217,11 @@ mod tests {
|
||||
fn dash_c_command_wraps_script_path_inside_quoted_arg() {
|
||||
// The script path must stay inside the `-c` string, not as a bare argv.
|
||||
let s = build_macos_dash_c_command(Path::new("/tmp/cc_switch_launcher_1.sh"));
|
||||
assert_eq!(s, "exec bash '/tmp/cc_switch_launcher_1.sh'");
|
||||
assert_eq!(s, "exec sh '/tmp/cc_switch_launcher_1.sh'");
|
||||
|
||||
// Spaces and single quotes must stay shell-safe too.
|
||||
let s2 = build_macos_dash_c_command(Path::new("/Users/me/it's dir/x.sh"));
|
||||
assert_eq!(s2, r#"exec bash '/Users/me/it'"'"'s dir/x.sh'"#);
|
||||
assert_eq!(s2, r#"exec sh '/Users/me/it'"'"'s dir/x.sh'"#);
|
||||
}
|
||||
|
||||
/// AppleScript launchers need both shell-path quoting and AppleScript string quoting.
|
||||
@@ -4995,20 +5229,26 @@ mod tests {
|
||||
#[test]
|
||||
fn applescript_builders_safely_quote_special_paths() {
|
||||
// First shell-quote the path, then wrap the whole command as an AppleScript string.
|
||||
let expected = r#""bash '/Users/me/it'\"'\"'s dir/x.sh'""#;
|
||||
let expected = r#""sh '/Users/me/it'\"'\"'s dir/x.sh'""#;
|
||||
let p = Path::new("/Users/me/it's dir/x.sh");
|
||||
assert_eq!(applescript_launcher_command(p), expected);
|
||||
assert_eq!(
|
||||
applescript_exec_launcher_command(p),
|
||||
r#""exec sh '/Users/me/it'\"'\"'s dir/x.sh'""#
|
||||
);
|
||||
assert!(
|
||||
build_macos_terminal_applescript(p).contains(expected),
|
||||
build_macos_terminal_applescript(p)
|
||||
.contains(r#""exec sh '/Users/me/it'\"'\"'s dir/x.sh'""#),
|
||||
"Terminal did not quote safely"
|
||||
);
|
||||
assert!(
|
||||
build_macos_iterm2_applescript(p).contains(expected),
|
||||
build_macos_iterm2_applescript(p)
|
||||
.contains(r#""exec sh '/Users/me/it'\"'\"'s dir/x.sh'""#),
|
||||
"iTerm2 did not quote safely"
|
||||
);
|
||||
assert!(
|
||||
build_macos_ghostty_applescript(p).contains(expected),
|
||||
"Ghostty did not quote safely"
|
||||
"Ghostty did not keep the non-exec launcher"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user