ls: implement --dired

* Support ls --dired

* stat-failed.sh: update of the test - we have a small difference

* ls --dired: address some of the comments

* fix warnings

* use unwrap()

* Improve test

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

* Simplify test

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

* Remove a word from the spell ignore

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

* remove duplication of the spell ignore

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

* rustfmt

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
This commit is contained in:
Sylvestre Ledru 2023-09-20 08:17:46 +02:00 committed by GitHub
parent bc8e3818a4
commit 9b4d2c6bc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 510 additions and 52 deletions

178
src/uu/ls/src/dired.rs Normal file
View file

@ -0,0 +1,178 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore dired subdired
use crate::Config;
use std::fmt;
use std::io::{BufWriter, Stdout, Write};
use uucore::error::UResult;
#[derive(Debug, Clone)]
pub struct BytePosition {
pub start: usize,
pub end: usize,
}
/// Represents the output structure for DIRED, containing positions for both DIRED and SUBDIRED.
#[derive(Debug, Clone, Default)]
pub struct DiredOutput {
pub dired_positions: Vec<BytePosition>,
pub subdired_positions: Vec<BytePosition>,
pub just_printed_total: bool,
}
impl fmt::Display for BytePosition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.start, self.end)
}
}
// When --dired is used, all lines starts with 2 spaces
static DIRED_TRAILING_OFFSET: usize = 2;
/// Calculates the byte positions for DIRED
pub fn calculate_dired_byte_positions(
output_display_len: usize,
dfn_len: usize,
dired_positions: &[BytePosition],
) -> (usize, usize) {
let offset_from_previous_line = if let Some(last_position) = dired_positions.last() {
last_position.end + 1
} else {
0
};
let start = output_display_len + offset_from_previous_line;
let end = start + dfn_len;
(start, end)
}
pub fn indent(out: &mut BufWriter<Stdout>) -> UResult<()> {
write!(out, " ")?;
Ok(())
}
pub fn calculate_offset_and_push(dired: &mut DiredOutput, path_len: usize) {
let offset = if dired.subdired_positions.is_empty() {
DIRED_TRAILING_OFFSET
} else {
dired.subdired_positions[dired.subdired_positions.len() - 1].start + DIRED_TRAILING_OFFSET
};
dired.subdired_positions.push(BytePosition {
start: offset,
end: path_len + offset,
});
}
/// Prints the dired output based on the given configuration and dired structure.
pub fn print_dired_output(
config: &Config,
dired: &DiredOutput,
out: &mut BufWriter<Stdout>,
) -> UResult<()> {
out.flush()?;
if config.recursive {
print_positions("//SUBDIRED//", &dired.subdired_positions);
} else if !dired.just_printed_total {
print_positions("//DIRED//", &dired.dired_positions);
}
println!("//DIRED-OPTIONS// --quoting-style={}", config.quoting_style);
Ok(())
}
/// Helper function to print positions with a given prefix.
fn print_positions(prefix: &str, positions: &Vec<BytePosition>) {
print!("{}", prefix);
for c in positions {
print!(" {}", c);
}
println!();
}
pub fn add_total(total_len: usize, dired: &mut DiredOutput) {
dired.just_printed_total = true;
dired.dired_positions.push(BytePosition {
start: 0,
// the 2 is from the trailing spaces
// the 1 is from the line ending (\n)
end: total_len + DIRED_TRAILING_OFFSET - 1,
});
}
/// Calculates byte positions and updates the dired structure.
pub fn calculate_and_update_positions(
output_display_len: usize,
dfn_len: usize,
dired: &mut DiredOutput,
) {
let offset = dired
.dired_positions
.last()
.map_or(DIRED_TRAILING_OFFSET, |last_position| {
last_position.start + DIRED_TRAILING_OFFSET
});
let start = output_display_len + offset + DIRED_TRAILING_OFFSET;
let end = start + dfn_len;
update_positions(start, end, dired, true);
}
/// Updates the dired positions based on the given start and end positions.
/// update when it is the first element in the list (to manage "total X"
/// insert when it isn't the about total
pub fn update_positions(start: usize, end: usize, dired: &mut DiredOutput, adjust: bool) {
if dired.just_printed_total {
if let Some(last_position) = dired.dired_positions.last_mut() {
*last_position = BytePosition {
start: if adjust {
start + last_position.end
} else {
start
},
end: if adjust { end + last_position.end } else { end },
};
dired.just_printed_total = false;
}
} else {
dired.dired_positions.push(BytePosition { start, end });
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_dired_byte_positions() {
let output_display = "sample_output".to_string();
let dfn = "sample_file".to_string();
let dired_positions = vec![BytePosition { start: 5, end: 10 }];
let (start, end) =
calculate_dired_byte_positions(output_display.len(), dfn.len(), &dired_positions);
assert_eq!(start, 24);
assert_eq!(end, 35);
}
#[test]
fn test_dired_update_positions() {
let mut dired = DiredOutput {
dired_positions: vec![BytePosition { start: 5, end: 10 }],
subdired_positions: vec![],
just_printed_total: true,
};
// Test with adjust = true
update_positions(15, 20, &mut dired, true);
let last_position = dired.dired_positions.last().unwrap();
assert_eq!(last_position.start, 25); // 15 + 10 (end of the previous position)
assert_eq!(last_position.end, 30); // 20 + 10 (end of the previous position)
// Test with adjust = false
update_positions(30, 35, &mut dired, false);
let last_position = dired.dired_positions.last().unwrap();
assert_eq!(last_position.start, 30);
assert_eq!(last_position.end, 35);
}
}

View file

@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize dired
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf tabsize dired subdired
use clap::{
builder::{NonEmptyStringValueParser, ValueParser},
@ -61,7 +61,8 @@ use uucore::{
version_cmp::version_cmp,
};
use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning};
mod dired;
use dired::DiredOutput;
#[cfg(not(feature = "selinux"))]
static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)";
#[cfg(feature = "selinux")]
@ -167,6 +168,7 @@ enum LsError {
IOError(std::io::Error),
IOErrorContext(std::io::Error, PathBuf, bool),
BlockSizeParseError(String),
ConflictingArgumentDired(),
AlreadyListedError(PathBuf),
TimeStyleParseError(String, Vec<String>),
}
@ -179,6 +181,7 @@ impl UError for LsError {
Self::IOErrorContext(_, _, false) => 1,
Self::IOErrorContext(_, _, true) => 2,
Self::BlockSizeParseError(_) => 1,
Self::ConflictingArgumentDired() => 1,
Self::AlreadyListedError(_) => 2,
Self::TimeStyleParseError(_, _) => 1,
}
@ -193,6 +196,10 @@ impl Display for LsError {
Self::BlockSizeParseError(s) => {
write!(f, "invalid --block-size argument {}", s.quote())
}
Self::ConflictingArgumentDired() => {
write!(f, "--dired requires --format=long")
}
Self::TimeStyleParseError(s, possible_time_styles) => {
write!(
f,
@ -406,6 +413,7 @@ pub struct Config {
selinux_supported: bool,
group_directories_first: bool,
line_ending: LineEnding,
dired: bool,
}
// Fields that can be removed or added to the long format
@ -610,6 +618,8 @@ fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> Quot
QuotingStyle::C {
quotes: quoting_style::Quotes::Double,
}
} else if options.get_flag(options::DIRED) {
QuotingStyle::Literal { show_control }
} else {
// TODO: use environment variable if available
QuotingStyle::Shell {
@ -954,6 +964,11 @@ impl Config {
None
};
let dired = options.get_flag(options::DIRED);
if dired && format != Format::Long {
return Err(Box::new(LsError::ConflictingArgumentDired()));
}
let dereference = if options.get_flag(options::dereference::ALL) {
Dereference::All
} else if options.get_flag(options::dereference::ARGS) {
@ -1003,6 +1018,7 @@ impl Config {
},
group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST),
line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)),
dired,
})
}
}
@ -1135,7 +1151,7 @@ pub fn uu_app() -> Command {
Arg::new(options::DIRED)
.long(options::DIRED)
.short('D')
.hide(true)
.help("generate output designed for Emacs' dired (Directory Editor) mode")
.action(ArgAction::SetTrue),
)
// The next four arguments do not override with the other format
@ -1844,6 +1860,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new();
let mut out = BufWriter::new(stdout());
let mut dired = DiredOutput::default();
let initial_locs_len = locs.len();
for loc in locs {
@ -1877,7 +1894,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
sort_entries(&mut files, config, &mut out);
sort_entries(&mut dirs, config, &mut out);
display_items(&files, config, &mut out)?;
display_items(&files, config, &mut out, &mut dired)?;
for (pos, path_data) in dirs.iter().enumerate() {
// Do read_dir call here to match GNU semantics by printing
@ -1899,7 +1916,13 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
// Print dir heading - name... 'total' comes after error display
if initial_locs_len > 1 || config.recursive {
if pos.eq(&0usize) && files.is_empty() {
if config.dired {
dired::indent(&mut out)?;
}
writeln!(out, "{}:", path_data.p_buf.display())?;
if config.dired {
dired::calculate_offset_and_push(&mut dired, path_data.display_name.len());
}
} else {
writeln!(out, "\n{}:", path_data.p_buf.display())?;
}
@ -1909,9 +1932,18 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
&path_data.p_buf,
path_data.must_dereference,
)?);
enter_directory(path_data, read_dir, config, &mut out, &mut listed_ancestors)?;
enter_directory(
path_data,
read_dir,
config,
&mut out,
&mut listed_ancestors,
&mut dired,
)?;
}
if config.dired {
dired::print_dired_output(config, &dired, &mut out)?;
}
Ok(())
}
@ -2022,6 +2054,7 @@ fn enter_directory(
config: &Config,
out: &mut BufWriter<Stdout>,
listed_ancestors: &mut HashSet<FileInformation>,
dired: &mut DiredOutput,
) -> UResult<()> {
// Create vec of entries with initial dot files
let mut entries: Vec<PathData> = if config.files == Files::All {
@ -2067,10 +2100,14 @@ fn enter_directory(
// Print total after any error display
if config.format == Format::Long || config.alloc_size {
display_total(&entries, config, out)?;
let total = return_total(&entries, config, out)?;
write!(out, "{}", total.as_str())?;
if config.dired {
dired::add_total(total.len(), dired);
}
}
display_items(&entries, config, out)?;
display_items(&entries, config, out, dired)?;
if config.recursive {
for e in entries
@ -2095,7 +2132,7 @@ fn enter_directory(
.insert(FileInformation::from_path(&e.p_buf, e.must_dereference)?)
{
writeln!(out, "\n{}:", e.p_buf.display())?;
enter_directory(e, rd, config, out, listed_ancestors)?;
enter_directory(e, rd, config, out, listed_ancestors, dired)?;
listed_ancestors
.remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?);
} else {
@ -2154,7 +2191,11 @@ fn pad_right(string: &str, count: usize) -> String {
format!("{string:<count$}")
}
fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) -> UResult<()> {
fn return_total(
items: &[PathData],
config: &Config,
out: &mut BufWriter<Stdout>,
) -> UResult<String> {
let mut total_size = 0;
for item in items {
total_size += item
@ -2162,13 +2203,14 @@ fn display_total(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
.as_ref()
.map_or(0, |md| get_block_size(md, config));
}
write!(
out,
if config.dired {
dired::indent(out)?;
}
Ok(format!(
"total {}{}",
display_size(total_size, config),
config.line_ending
)?;
Ok(())
))
}
fn display_additional_leading_info(
@ -2207,7 +2249,12 @@ fn display_additional_leading_info(
}
#[allow(clippy::cognitive_complexity)]
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) -> UResult<()> {
fn display_items(
items: &[PathData],
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
) -> UResult<()> {
// `-Z`, `--context`:
// Display the SELinux security context or '?' if none is found. When used with the `-l`
// option, print the security context to the left of the size column.
@ -2220,6 +2267,7 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
if config.inode || config.alloc_size {
let more_info =
display_additional_leading_info(item, &padding_collection, config, out)?;
write!(out, "{more_info}")?;
}
#[cfg(not(unix))]
@ -2228,7 +2276,7 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
display_additional_leading_info(item, &padding_collection, config, out)?;
write!(out, "{more_info}")?;
}
display_item_long(item, &padding_collection, config, out)?;
display_item_long(item, &padding_collection, config, out, dired)?;
}
} else {
let mut longest_context_len = 1;
@ -2402,10 +2450,15 @@ fn display_item_long(
padding: &PaddingCollection,
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
) -> UResult<()> {
let mut output_display: String = String::new();
if config.dired {
output_display += " ";
}
if let Some(md) = item.md(out) {
write!(
out,
output_display,
"{}{} {}",
display_permissions(md, true),
if item.security_context.len() > 1 {
@ -2416,49 +2469,54 @@ fn display_item_long(
""
},
pad_left(&display_symlink_count(md), padding.link_count)
)?;
)
.unwrap();
if config.long.owner {
write!(
out,
output_display,
" {}",
pad_right(&display_uname(md, config), padding.uname)
)?;
)
.unwrap();
}
if config.long.group {
write!(
out,
output_display,
" {}",
pad_right(&display_group(md, config), padding.group)
)?;
)
.unwrap();
}
if config.context {
write!(
out,
output_display,
" {}",
pad_right(&item.security_context, padding.context)
)?;
)
.unwrap();
}
// Author is only different from owner on GNU/Hurd, so we reuse
// the owner, since GNU/Hurd is not currently supported by Rust.
if config.long.author {
write!(
out,
output_display,
" {}",
pad_right(&display_uname(md, config), padding.uname)
)?;
)
.unwrap();
}
match display_len_or_rdev(md, config) {
SizeOrDeviceId::Size(size) => {
write!(out, " {}", pad_left(&size, padding.size))?;
write!(output_display, " {}", pad_left(&size, padding.size)).unwrap();
}
SizeOrDeviceId::Device(major, minor) => {
write!(
out,
output_display,
" {}, {}",
pad_left(
&major,
@ -2478,19 +2536,23 @@ fn display_item_long(
#[cfg(unix)]
padding.minor,
),
)?;
)
.unwrap();
}
};
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
write!(output_display, " {} ", display_date(md, config)).unwrap();
write!(
out,
" {} {}{}",
display_date(md, config),
displayed_file,
config.line_ending
)?;
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
if config.dired {
let (start, end) = dired::calculate_dired_byte_positions(
output_display.len(),
displayed_file.len(),
&dired.dired_positions,
);
dired::update_positions(start, end, dired, false);
}
write!(output_display, "{}{}", displayed_file, config.line_ending).unwrap();
} else {
#[cfg(unix)]
let leading_char = {
@ -2526,7 +2588,7 @@ fn display_item_long(
};
write!(
out,
output_display,
"{}{} {}",
format_args!("{leading_char}?????????"),
if item.security_context.len() > 1 {
@ -2537,41 +2599,53 @@ fn display_item_long(
""
},
pad_left("?", padding.link_count)
)?;
)
.unwrap();
if config.long.owner {
write!(out, " {}", pad_right("?", padding.uname))?;
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
}
if config.long.group {
write!(out, " {}", pad_right("?", padding.group))?;
write!(output_display, " {}", pad_right("?", padding.group)).unwrap();
}
if config.context {
write!(
out,
output_display,
" {}",
pad_right(&item.security_context, padding.context)
)?;
)
.unwrap();
}
// Author is only different from owner on GNU/Hurd, so we reuse
// the owner, since GNU/Hurd is not currently supported by Rust.
if config.long.author {
write!(out, " {}", pad_right("?", padding.uname))?;
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
}
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
let date_len = 12;
writeln!(
out,
" {} {} {}",
write!(
output_display,
" {} {} ",
pad_left("?", padding.size),
pad_left("?", date_len),
displayed_file,
)?;
)
.unwrap();
if config.dired {
dired::calculate_and_update_positions(
output_display.len(),
displayed_file.trim().len(),
dired,
);
}
write!(output_display, "{}{}", displayed_file, config.line_ending).unwrap();
}
write!(out, "{}", output_display)?;
Ok(())
}

View file

@ -4,12 +4,14 @@
// file that was distributed with this source code.
use std::char::from_digit;
use std::ffi::OsStr;
use std::fmt;
// These are characters with special meaning in the shell (e.g. bash).
// The first const contains characters that only have a special meaning when they appear at the beginning of a name.
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#'];
const SPECIAL_SHELL_CHARS: &str = "`$&*()|[]{};\\'\"<>?! ";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuotingStyle {
Shell {
escape: bool,
@ -24,7 +26,7 @@ pub enum QuotingStyle {
},
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Quotes {
None,
Single,
@ -316,6 +318,42 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String {
}
}
impl fmt::Display for QuotingStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Shell {
escape,
always_quote,
show_control,
} => {
let mut style = "shell".to_string();
if escape {
style.push_str("-escape");
}
if always_quote {
style.push_str("-always-quote");
}
if show_control {
style.push_str("-show-control");
}
f.write_str(&style)
}
Self::C { .. } => f.write_str("C"),
Self::Literal { .. } => f.write_str("literal"),
}
}
}
impl fmt::Display for Quotes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::None => f.write_str("None"),
Self::Single => f.write_str("Single"),
Self::Double => f.write_str("Double"),
}
}
}
#[cfg(test)]
mod tests {
use crate::quoting_style::{escape_name, Quotes, QuotingStyle};
@ -732,4 +770,45 @@ mod tests {
],
);
}
#[test]
fn test_quoting_style_display() {
let style = QuotingStyle::Shell {
escape: true,
always_quote: false,
show_control: false,
};
assert_eq!(format!("{}", style), "shell-escape");
let style = QuotingStyle::Shell {
escape: false,
always_quote: true,
show_control: false,
};
assert_eq!(format!("{}", style), "shell-always-quote");
let style = QuotingStyle::Shell {
escape: false,
always_quote: false,
show_control: true,
};
assert_eq!(format!("{}", style), "shell-show-control");
let style = QuotingStyle::C {
quotes: Quotes::Double,
};
assert_eq!(format!("{}", style), "C");
let style = QuotingStyle::Literal {
show_control: false,
};
assert_eq!(format!("{}", style), "literal");
}
#[test]
fn test_quotes_display() {
assert_eq!(format!("{}", Quotes::None), "None");
assert_eq!(format!("{}", Quotes::Single), "Single");
assert_eq!(format!("{}", Quotes::Double), "Double");
}
}

View file

@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired
#[cfg(any(unix, feature = "feat_selinux"))]
use crate::common::util::expected_result;
@ -3522,3 +3522,126 @@ fn test_ls_perm_io_errors() {
.code_is(1)
.stderr_contains("Permission denied");
}
#[test]
fn test_ls_dired_incompatible() {
let scene = TestScenario::new(util_name!());
scene
.ucmd()
.arg("--dired")
.fails()
.code_is(1)
.stderr_contains("--dired requires --format=long");
}
#[test]
fn test_ls_dired_recursive() {
let scene = TestScenario::new(util_name!());
scene
.ucmd()
.arg("--dired")
.arg("-l")
.arg("-R")
.succeeds()
.stdout_does_not_contain("//DIRED//")
.stdout_contains(" total 0")
.stdout_contains("//SUBDIRED// 2 3")
.stdout_contains("//DIRED-OPTIONS// --quoting-style");
}
#[test]
fn test_ls_dired_simple() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
scene
.ucmd()
.arg("--dired")
.arg("-l")
.succeeds()
.stdout_contains(" total 0");
at.mkdir("d");
at.touch("d/a1");
let mut cmd = scene.ucmd();
cmd.arg("--dired").arg("-l").arg("d");
let result = cmd.succeeds();
result.stdout_contains(" total 0");
println!(" result.stdout = {:#?}", result.stdout_str());
let dired_line = result
.stdout_str()
.lines()
.find(|&line| line.starts_with("//DIRED//"))
.unwrap();
let positions: Vec<usize> = dired_line
.split_whitespace()
.skip(1)
.map(|s| s.parse().unwrap())
.collect();
assert_eq!(positions.len(), 2);
let start_pos = positions[0];
let end_pos = positions[1];
// Extract the filename using the positions
let filename =
String::from_utf8(result.stdout_str().as_bytes()[start_pos..end_pos].to_vec()).unwrap();
assert_eq!(filename, "a1");
}
#[test]
fn test_ls_dired_complex() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("d");
at.mkdir("d/d");
at.touch("d/a1");
at.touch("d/a22");
at.touch("d/a333");
at.touch("d/a4444");
let mut cmd = scene.ucmd();
cmd.arg("--dired").arg("-l").arg("d");
let result = cmd.succeeds();
// Number of blocks
#[cfg(target_os = "linux")]
result.stdout_contains(" total 4");
let output = result.stdout_str().to_string();
println!("Output:\n{}", output);
let dired_line = output
.lines()
.find(|&line| line.starts_with("//DIRED//"))
.unwrap();
let positions: Vec<usize> = dired_line
.split_whitespace()
.skip(1)
.map(|s| s.parse().unwrap())
.collect();
println!("{:?}", positions);
println!("Parsed byte positions: {:?}", positions);
assert_eq!(positions.len() % 2, 0); // Ensure there's an even number of positions
let filenames: Vec<String> = positions
.chunks(2)
.map(|chunk| {
let start_pos = chunk[0];
let end_pos = chunk[1];
let filename = String::from_utf8(output.as_bytes()[start_pos..=end_pos].to_vec())
.unwrap()
.trim()
.to_string();
println!("Extracted filename: {}", filename);
filename
})
.collect();
println!("Extracted filenames: {:?}", filenames);
assert_eq!(filenames, vec!["a1", "a22", "a333", "a4444", "d"]);
}

View file

@ -3,7 +3,7 @@
#
# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'release'
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR vdir rcexp xpart
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR vdir rcexp xpart dired
set -e
@ -261,6 +261,10 @@ sed -i -e "s/Try 'mv --help' for more information/For more information, try '--h
# disable these test cases
sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl
# with ls --dired, in case of error, we have a slightly different error position
sed -i -e "s|44 45|47 48|" tests/ls/stat-failed.sh
sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold <SIZE>' but none was supplied/" -e "/Try 'du --help' for more information./d" tests/du/threshold.sh
# disable two kind of tests: