diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index a2c6f3481cd..c78afd3c85c 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -119,6 +119,7 @@ pub mod options { pub static IGNORE_BACKUPS: &str = "ignore-backups"; pub static DIRECTORY: &str = "directory"; pub static INODE: &str = "inode"; + pub static BLOCKS: &str = "blocks"; pub static REVERSE: &str = "reverse"; pub static RECURSIVE: &str = "recursive"; pub static COLOR: &str = "color"; @@ -230,6 +231,7 @@ struct Config { time: Time, #[cfg(unix)] inode: bool, + blocks: bool, color: Option, long: LongFormat, width: Option, @@ -571,6 +573,7 @@ impl Config { color, #[cfg(unix)] inode: options.is_present(options::INODE), + blocks: options.is_present(options::BLOCKS), long, width, quoting_style, @@ -1038,6 +1041,12 @@ only ignore '.' and '..'.", .long(options::INODE) .help("print the index number of each file"), ) + .arg( + Arg::with_name(options::BLOCKS) + .short("s") + .long(options::BLOCKS) + .help("print the number of file system blocks of each file"), + ) .arg( Arg::with_name(options::REVERSE) .short("r") @@ -1377,42 +1386,59 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { } } -fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) { - if let Some(md) = entry.md() { - ( - display_symlink_count(md).len(), - display_size_or_rdev(md, config).len(), - ) - } else { - (0, 0) +fn get_display_size(md: &Metadata) -> u64 { + #[cfg(windows)] + { + if md.is_dir() { + return 0; + } + md.len() as u64 + } + + #[cfg(unix)] + { + md.blocks() as u64 } } -fn pad_left(string: String, count: usize) -> String { +// Get maximum string sizes for: +// - number of links of each items +// - size +// - blocks +fn get_max_sizes(items: &[PathData], config: &Config) -> (usize, usize, usize, u64) { + let (mut max_links, mut max_width, mut max_blocks) = (1, 1, 1); + let mut total_size = 0; + + for item in items { + if let Some(md) = item.md() { + max_links = display_symlink_count(md).len().max(max_links); + max_width = display_size_or_rdev(md, config).len().max(max_width); + max_blocks = get_display_size(md).to_string().len().max(max_blocks); + total_size += get_block_size(md, config); + } + } + + (max_links, max_width, max_blocks, total_size) +} + +fn pad_left(string: impl Display, count: usize) -> String { format!("{:>width$}", string, width = count) } fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter) { + let (max_links, max_width, max_blocks, total_size) = get_max_sizes(items, config); if config.format == Format::Long { - let (mut max_links, mut max_width) = (1, 1); - let mut total_size = 0; - - for item in items { - let (links, width) = display_dir_entry_size(item, config); - max_links = links.max(max_links); - max_width = width.max(max_width); - total_size += item.md().map_or(0, |md| get_block_size(md, config)); - } - if total_size > 0 { let _ = writeln!(out, "total {}", display_size(total_size, config)); } for item in items { - display_item_long(item, max_links, max_width, config, out); + display_item_long(item, max_links, max_width, max_blocks, config, out); } } else { - let names = items.iter().filter_map(|i| display_file_name(i, config)); + let names = items + .iter() + .filter_map(|i| display_file_name(i, config, max_blocks)); match (&config.format, config.width) { (Format::Columns, Some(width)) => { @@ -1507,6 +1533,7 @@ fn display_item_long( item: &PathData, max_links: usize, max_size: usize, + max_blocks: usize, config: &Config, out: &mut BufWriter, ) { @@ -1525,6 +1552,14 @@ fn display_item_long( } } + if config.blocks { + let _ = write!( + out, + "{} ", + pad_left(get_display_size(md).to_string(), max_blocks) + ); + } + let _ = write!( out, "{} {}", @@ -1554,7 +1589,7 @@ fn display_item_long( // unwrap is fine because it fails when metadata is not available // but we already know that it is because it's checked at the // start of the function. - display_file_name(item, config).unwrap().contents, + display_file_name(item, config, 1).unwrap().contents, ); } @@ -1765,7 +1800,7 @@ fn classify_file(path: &PathData) -> Option { } } -fn display_file_name(path: &PathData, config: &Config) -> Option { +fn display_file_name(path: &PathData, config: &Config, max_blocks: usize) -> Option { let mut name = escape_name(&path.display_name, &config.quoting_style); #[cfg(unix)] @@ -1815,6 +1850,18 @@ fn display_file_name(path: &PathData, config: &Config) -> Option { } } + if config.format != Format::Long && config.blocks { + width += max_blocks + 1; + if let Some(md) = path.md() { + name.insert_str( + 0, + &format!("{} ", pad_left(get_display_size(md), max_blocks)), + ); + } else { + name.insert_str(0, &format!("{} ", pad_left(String::from(""), max_blocks))); + } + } + if config.format == Format::Long && path.file_type()?.is_symlink() { if let Ok(target) = path.p_buf.read_link() { name.push_str(" -> "); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 44d14c30416..32ab099f4e8 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -2046,3 +2046,58 @@ fn test_ls_dangling_symlinks() { .succeeds() // this should fail, though at the moment, ls lacks a way to propagate errors encountered during display .stdout_contains(if cfg!(windows) { "dangle" } else { "? dangle" }); } + +#[test] +fn test_ls_block_size_display_short() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir-test"); + at.touch("file-1"); + at.append("file-1", &"x".repeat(1000)); + at.touch("file-2"); + at.append("file-2", &"x".repeat(100000)); + + let result = scene.ucmd().arg("-s").arg("-w=0").succeeds(); + + // Block sizes vary by OS. Only values for the following OS'es are checked + #[cfg(target_os = "macos")] + result.stdout_only(" 0 dir-test\n 8 file-1\n200 file-2\n"); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + result.stdout_only(" 8 dir-test\n 8 file-1\n200 file-2\n"); + #[cfg(target_os = "windows")] + result.stdout_only(" 0 dir-test\n 1000 file-1\n100000 file-2\n"); +} + +#[test] +fn test_ls_block_size_display_long() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir-test"); + at.touch("file-1"); + at.append("file-1", &"x".repeat(1000)); + at.touch("file-2"); + at.append("file-2", &"x".repeat(100000)); + + let result = scene.ucmd().arg("-ls").succeeds(); + // Block sizes vary by OS. Only values for the following OS'es are checked + #[cfg(target_os = "macos")] + { + result.stdout_contains("\n 0 "); + result.stdout_contains("\n 8 "); // for file-1 + result.stdout_contains("\n200 "); // for file- + } + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + result.stdout_contains("\n 8 "); // for directory + result.stdout_contains("\n 8 "); // for file-1 + result.stdout_contains("\n200 "); // for file-2 + } + #[cfg(target_os = "windows")] + { + result.stdout_contains("\n 0 "); + result.stdout_contains("\n 1000 "); // for file-1 + result.stdout_contains("\n100000 "); // for file-2 + } +}