Skip to content

Commit 8a24ce2

Browse files
authored
Merge pull request #2325 from fengjikui/fix/escaped-space-tee-path
fix(tee): quote recovery hint paths with spaces
2 parents a8b9eb3 + 5de188b commit 8a24ce2

1 file changed

Lines changed: 120 additions & 2 deletions

File tree

src/core/tee.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,61 @@ fn display_path(path: &std::path::Path) -> String {
177177
path.display().to_string()
178178
}
179179

180+
fn needs_shell_quoting(path: &str) -> bool {
181+
path.chars().any(|c| {
182+
c.is_whitespace()
183+
|| matches!(
184+
c,
185+
'\'' | '"'
186+
| '\\'
187+
| '$'
188+
| '`'
189+
| '!'
190+
| '#'
191+
| '&'
192+
| '('
193+
| ')'
194+
| ';'
195+
| '<'
196+
| '>'
197+
| '?'
198+
| '['
199+
| ']'
200+
| '{'
201+
| '}'
202+
| '|'
203+
| '*'
204+
)
205+
})
206+
}
207+
208+
fn escape_double_quoted_path(path: &str) -> String {
209+
let mut escaped = String::with_capacity(path.len());
210+
for c in path.chars() {
211+
if matches!(c, '\\' | '"' | '$' | '`') {
212+
escaped.push('\\');
213+
}
214+
escaped.push(c);
215+
}
216+
escaped
217+
}
218+
219+
fn display_shell_path(path: &std::path::Path) -> String {
220+
let display = display_path(path);
221+
if !needs_shell_quoting(&display) {
222+
return display;
223+
}
224+
225+
if let Some(relative) = display.strip_prefix("~/") {
226+
let relative = relative.replace(std::path::MAIN_SEPARATOR, "/");
227+
return format!("\"$HOME/{}\"", escape_double_quoted_path(&relative));
228+
}
229+
230+
format!("\"{}\"", escape_double_quoted_path(&display))
231+
}
232+
180233
fn format_hint(path: &std::path::Path) -> String {
181-
format!("[full output: {}]", display_path(path))
234+
format!("[full output: {}]", display_shell_path(path))
182235
}
183236

184237
/// Convenience: tee + format hint in one call.
@@ -231,7 +284,7 @@ pub fn force_tee_tail_hint(
231284
Some(format!(
232285
"[see remaining: tail -n +{} {}]",
233286
line_offset,
234-
display_path(&path)
287+
display_shell_path(&path)
235288
))
236289
}
237290

@@ -447,6 +500,71 @@ mod tests {
447500
assert!(hint.contains("123_cargo_test.log"));
448501
}
449502

503+
#[test]
504+
fn test_display_shell_path_preserves_simple_paths() {
505+
let path = PathBuf::from("/tmp/rtk/tee/123_cargo_test.log");
506+
assert_eq!(display_shell_path(&path), "/tmp/rtk/tee/123_cargo_test.log");
507+
}
508+
509+
#[test]
510+
fn test_display_shell_path_quotes_paths_with_spaces() {
511+
let path = PathBuf::from("/tmp/rtk/Application Support/123_go_test.log");
512+
assert_eq!(
513+
display_shell_path(&path),
514+
"\"/tmp/rtk/Application Support/123_go_test.log\""
515+
);
516+
}
517+
518+
#[test]
519+
fn test_display_shell_path_quotes_backslashes() {
520+
let path = PathBuf::from(r"/tmp/rtk/tee/path\segment.log");
521+
assert_eq!(
522+
display_shell_path(&path),
523+
r#""/tmp/rtk/tee/path\\segment.log""#
524+
);
525+
}
526+
527+
#[test]
528+
fn test_display_shell_path_uses_home_var_for_home_paths_with_spaces() {
529+
let Some(home) = dirs::home_dir() else {
530+
return;
531+
};
532+
let path = home
533+
.join("Library")
534+
.join("Application Support")
535+
.join("rtk")
536+
.join("tee")
537+
.join("123_go_test.log");
538+
539+
assert_eq!(
540+
display_shell_path(&path),
541+
"\"$HOME/Library/Application Support/rtk/tee/123_go_test.log\""
542+
);
543+
}
544+
545+
#[test]
546+
fn test_format_hint_avoids_backslash_escaped_whitespace() {
547+
let Some(home) = dirs::home_dir() else {
548+
return;
549+
};
550+
let path = home
551+
.join("Library")
552+
.join("Application Support")
553+
.join("rtk")
554+
.join("tee")
555+
.join("123_go_test.log");
556+
let hint = format_hint(&path);
557+
558+
assert_eq!(
559+
hint,
560+
"[full output: \"$HOME/Library/Application Support/rtk/tee/123_go_test.log\"]"
561+
);
562+
assert!(
563+
!hint.contains("\\ "),
564+
"hint should not encourage backslash-escaped whitespace"
565+
);
566+
}
567+
450568
#[test]
451569
fn test_tee_config_default() {
452570
let config = TeeConfig::default();

0 commit comments

Comments
 (0)