diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 4d17562b61..fb0b366cf8 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -162,11 +162,13 @@ enum SizeOrDeviceId { /// dir1: <- This as well /// file11 /// ``` +/// Returns the number of bytes the rendered name occupies, which `--dired` +/// needs to place the header in `//SUBDIRED//`. pub fn show_dir_name( path_data: &PathData, out: &mut BufWriter, config: &Config, -) -> std::io::Result<()> { +) -> std::io::Result { let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); let name = if config.hyperlink && !config.dired { @@ -176,7 +178,8 @@ pub fn show_dir_name( }; write_os_str(out, &name)?; - write!(out, ":") + write!(out, ":")?; + Ok(name.len()) } fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3b8eec3fd3..45a5d29e13 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1078,29 +1078,23 @@ impl LsOutput for TextOutput<'_> { config: &Config, is_first: bool, ) -> UResult<()> { - if is_first { - if config.dired { - dired::indent(&mut self.state.out)?; - } - show_dir_name(path_data, &mut self.state.out, config)?; - writeln!(self.state.out)?; - if config.dired { - let dir_len = path_data.path().as_os_str().len(); - dired::calculate_subdired(&mut self.dired, dir_len); - dired::add_dir_name(&mut self.dired, dir_len); - } - } else { + if !is_first { writeln!(self.state.out)?; if config.dired { self.dired.line_offset += 1; // account for the blank line before recursive directory headings self.dired.padding = 0; - dired::indent(&mut self.state.out)?; - let dir_name_size = path_data.path().as_os_str().len(); - dired::calculate_subdired(&mut self.dired, dir_name_size); - dired::add_dir_name(&mut self.dired, dir_name_size); } - show_dir_name(path_data, &mut self.state.out, config)?; - writeln!(self.state.out)?; + } + if config.dired { + dired::indent(&mut self.state.out)?; + } + let name_len = show_dir_name(path_data, &mut self.state.out, config)?; + writeln!(self.state.out)?; + if config.dired { + // The header is rendered with the quoting style in force, so the + // offsets must follow the rendered name, not the raw path. + dired::calculate_subdired(&mut self.dired, name_len); + dired::add_dir_name(&mut self.dired, name_len); } Ok(()) } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 817d1b4052..0f6d74f7d0 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -5665,6 +5665,31 @@ fn test_ls_dired_order_format() { .stdout_contains("//DIRED//"); } +#[test] +fn test_ls_dired_offsets_follow_quoted_dir_headers() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("a b"); + at.touch("a b/x"); + at.mkdir("it's"); + at.touch("it's/y"); + + // Quoting lengthens the directory headers; both offset lists must follow + // the rendered header rather than the raw path. + let result = scene + .ucmd() + .args(&[ + "--dired", + "-R", + "--quoting-style=shell-escape", + "a b", + "it's", + ]) + .succeeds(); + assert_eq!(dired_names(result.stdout_str()), ["x", "y"]); + assert_eq!(subdired_names(result.stdout_str()), ["'a b'", "\"it's\""]); +} + #[test] fn test_ls_dired_and_zero_are_incompatible() { let scene = TestScenario::new(util_name!()); @@ -5917,10 +5942,19 @@ fn test_ls_dired_symlink_name_only() { /// Extracts the file names delimited by the //DIRED// byte offsets. fn dired_names(output: &str) -> Vec { + names_at_offsets(output, "//DIRED//") +} + +/// Extracts the directory headers delimited by the //SUBDIRED// byte offsets. +fn subdired_names(output: &str) -> Vec { + names_at_offsets(output, "//SUBDIRED//") +} + +fn names_at_offsets(output: &str, tag: &str) -> Vec { let dired_line = output .lines() - .find(|&line| line.starts_with("//DIRED//")) - .unwrap(); + .find(|&line| line.starts_with(tag)) + .unwrap_or_else(|| panic!("no {tag} line in the output")); let positions: Vec = dired_line .split_whitespace() .skip(1)