diff --git a/Cargo.lock b/Cargo.lock index fb070409e..5ad490188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "tempfile", "test-case", "tikv-jemallocator", + "unix_mode", ] [[package]] @@ -875,6 +876,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unix_mode" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55eedc365f81a3c32aea49baf23fa965e3cd85bcc28fb8045708c7388d124ef" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index c85026188..a1e7eae6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ normpath = "1.1.1" crossbeam-channel = "0.5.15" clap_complete = {version = "4.6.0", optional = true} faccess = "0.2.4" +jiff = "0.2.14" +unix_mode = "0.1.4" jiff = "0.2.18" [dependencies.clap] diff --git a/src/cli.rs b/src/cli.rs index 29f23035d..23e7e62d9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -206,10 +206,9 @@ pub struct Opts { #[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)] relative_path: (), - /// Use a detailed listing format like 'ls -l'. This is basically an alias - /// for '--exec-batch ls -l' with some additional 'ls' options. This can be - /// used to see more metadata, to show symlink targets and to achieve a - /// deterministic sort order. + /// Use a detailed listing format like 'ls -l'. This can be used to see + /// more metadata, to show symlink targets and to achieve a deterministic + /// sort order. #[arg( long, short = 'l', diff --git a/src/config.rs b/src/config.rs index a027812a4..3a3564949 100644 --- a/src/config.rs +++ b/src/config.rs @@ -131,6 +131,8 @@ pub struct Config { /// Whether or not to use hyperlinks on paths pub hyperlink: bool, + /// Whether or not to show a long listing format with file metadata + pub list_details: bool, /// Names that should stop traversal down their parent. (e.g. https://bford.info/cachedir/). pub ignore_contain: Vec, } diff --git a/src/main.rs b/src/main.rs index 8b7bd936c..e5c955f49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -242,7 +242,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result false, HyperlinkWhen::Auto => colored_output, }; - let command = extract_command(&mut opts, colored_output)?; + let command = extract_command(&mut opts)?; let has_command = command.is_some(); let full_path_base = if opts.full_path { @@ -335,6 +335,12 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result Result> { + opts.exec.command.take().map(Ok).transpose() ignore_contain: opts.ignore_contain, }) } diff --git a/src/output.rs b/src/output.rs index 91e9f0216..3c21a460d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,7 +1,14 @@ use std::borrow::Cow; use std::io::{self, Write}; +use std::time::SystemTime; +#[cfg(unix)] +use std::os::unix::fs::{MetadataExt, PermissionsExt}; + +use jiff::{Timestamp, tz::TimeZone}; use lscolors::{Indicator, LsColors, Style}; +#[cfg(unix)] +use nix::unistd::{Gid, Group, Uid, User}; use crate::config::Config; use crate::dir_entry::DirEntry; @@ -22,7 +29,9 @@ pub fn print_entry(stdout: &mut W, entry: &DirEntry, config: &Config) has_hyperlink = true; } - if let Some(ref format) = config.format { + if config.list_details { + print_entry_details(stdout, entry, config, &config.ls_colors)?; + } else if let Some(ref format) = config.format { print_entry_format(stdout, entry, config, format)?; } else if let Some(ref ls_colors) = config.ls_colors { print_entry_colorized(stdout, entry, config, ls_colors)?; @@ -173,3 +182,95 @@ fn print_entry_uncolorized( print_trailing_slash(stdout, entry, config, None) } } + +fn format_size(size: u64) -> String { + if size < 1024 { + return format!("{} B", size); + } + let units = ["K", "M", "G", "T", "P", "E"]; + let mut size = size as f64; + let mut unit_idx = 0; + + size /= 1024.0; + + while size >= 1024.0 && unit_idx < units.len() - 1 { + size /= 1024.0; + unit_idx += 1; + } + + if size < 10.0 { + format!("{:.1} {}", size, units[unit_idx]) + } else { + format!("{:.0} {}", size, units[unit_idx]) + } +} + +fn print_entry_details( + stdout: &mut W, + entry: &DirEntry, + config: &Config, + ls_colors: &Option, +) -> io::Result<()> { + let metadata = entry.metadata(); + + #[cfg(unix)] + let mode = metadata.map(|m| m.permissions().mode()).unwrap_or(0); + #[cfg(not(unix))] + let mode = 0; + + let perms = unix_mode::to_string(mode); + + #[cfg(unix)] + let nlink = metadata.map(|m| m.nlink()).unwrap_or(1); + #[cfg(not(unix))] + let nlink = 1; + + #[cfg(unix)] + let (user, group) = { + let uid = metadata.map(|m| m.uid()).unwrap_or(0); + let gid = metadata.map(|m| m.gid()).unwrap_or(0); + let user = User::from_uid(Uid::from_raw(uid)) + .ok() + .flatten() + .map(|u| u.name) + .unwrap_or_else(|| uid.to_string()); + let group = Group::from_gid(Gid::from_raw(gid)) + .ok() + .flatten() + .map(|g| g.name) + .unwrap_or_else(|| gid.to_string()); + (user, group) + }; + #[cfg(not(unix))] + let (user, group) = ("".to_string(), "".to_string()); + + let size = metadata.map(|m| m.len()).unwrap_or(0); + let size_str = format_size(size); + + let time = metadata + .and_then(|m| m.modified().ok()) + .unwrap_or(SystemTime::UNIX_EPOCH); + let timestamp = Timestamp::try_from(time).unwrap_or(Timestamp::UNIX_EPOCH); + let zoned = timestamp.to_zoned(TimeZone::system()); + let date_str = zoned.strftime("%b %d %H:%M").to_string(); + + write!( + stdout, + "{} {:>3} {:>8} {:>8} {:>8} {} ", + perms, nlink, user, group, size_str, date_str + )?; + + if let Some(ls_colors) = ls_colors { + print_entry_colorized(stdout, entry, config, ls_colors)?; + } else { + print_entry_uncolorized(stdout, entry, config)?; + } + + if entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false) + && let Ok(target) = std::fs::read_link(entry.path()) + { + write!(stdout, " -> {}", target.to_string_lossy())?; + } + + Ok(()) +} diff --git a/tests/tests.rs b/tests/tests.rs index c125d3a59..040e1bfb4 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2525,6 +2525,18 @@ fn test_list_details() { te.assert_success_and_get_output(".", &["--list-details"]); } +#[test] +fn test_list_details_format() { + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + create_file_with_size(te.test_root().join("size_test_100"), 100); + + let output = te.assert_success_and_get_output(".", &["--list-details", "size_test_100"]); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout.contains("100 B")); + assert!(stdout.trim().ends_with("size_test_100")); +} + #[test] fn test_single_and_multithreaded_execution() { let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);