@@ -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+
180233fn 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