diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6ade9e2e6..a935eef54 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -78,6 +78,8 @@ pub mod options { pub static ONELINE: &str = "1"; pub static LONG: &str = "long"; pub static COLUMNS: &str = "C"; + pub static LONG_NO_OWNER: &str = "g"; + pub static LONG_NO_GROUP: &str = "o"; } pub mod files { pub static ALL: &str = "all"; @@ -96,6 +98,8 @@ pub mod options { pub static HUMAN_READABLE: &str = "human-readable"; pub static SI: &str = "si"; } + pub static AUTHOR: &str = "author"; + pub static NO_GROUP: &str = "no-group"; pub static FORMAT: &str = "format"; pub static SORT: &str = "sort"; pub static TIME: &str = "time"; @@ -161,26 +165,85 @@ struct Config { inode: bool, #[cfg(unix)] color: bool, + long: LongFormat, +} + +// Fields that can be removed or added to the long format +struct LongFormat { + author: bool, + group: bool, + owner: bool, } impl Config { fn from(options: clap::ArgMatches) -> Config { - let format = if let Some(format_) = options.value_of(options::FORMAT) { - match format_ { - "long" | "verbose" => Format::Long, - "single-column" => Format::OneLine, - "columns" => Format::Columns, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --format"), - } + let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) { + ( + match format_ { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + options::FORMAT, + ) } else if options.is_present(options::format::LONG) { - Format::Long - } else if options.is_present(options::format::ONELINE) { - Format::OneLine + (Format::Long, options::format::LONG) } else { - Format::Columns + (Format::Columns, options::format::COLUMNS) }; + // The -o and -g options are tricky. They cannot override with each + // other because it's possible to combine them. For example, the option + // -og should hide both owner and group. Furthermore, they are not + // reset if -l or --format=long is used. So these should just show the + // group: -gl or "-g --format=long". Finally, they are also not reset + // when switching to a different format option inbetween like this: + // -ogCl or "-og --format=vertical --format=long". + // + // -1 has a similar issue: it does nothing if the format is long. This + // actually makes it distinct from the --format=singe-column option, + // which always applies. + // + // The idea here is to not let these options override with the other + // options, but manually check the last index they occur. If this index + // is larger than the index for the other format options, we apply the + // long format. + match options.indices_of(opt).map(|x| x.max().unwrap()) { + None => { + if options.is_present(options::format::LONG_NO_GROUP) + || options.is_present(options::format::LONG_NO_OWNER) + { + format = Format::Long; + } else if options.is_present(options::format::ONELINE) { + format = Format::OneLine; + } + } + Some(mut idx) => { + if let Some(indices) = options.indices_of(options::format::LONG_NO_OWNER) { + let i = indices.max().unwrap(); + if i > idx { + format = Format::Long; + idx = i; + } + } + if let Some(indices) = options.indices_of(options::format::LONG_NO_GROUP) { + let i = indices.max().unwrap(); + if i > idx { + format = Format::Long; + idx = i; + } + } + if let Some(indices) = options.indices_of(options::format::ONELINE) { + let i = indices.max().unwrap(); + if i > idx && format != Format::Long { + format = Format::OneLine; + } + } + } + } + let files = if options.is_present(options::files::ALL) { Files::All } else if options.is_present(options::files::ALMOST_ALL) { @@ -241,6 +304,18 @@ impl Config { SizeFormat::Bytes }; + let long = { + let author = options.is_present(options::AUTHOR); + let group = !options.is_present(options::NO_GROUP) + && !options.is_present(options::format::LONG_NO_GROUP); + let owner = !options.is_present(options::format::LONG_NO_OWNER); + LongFormat { + author, + group, + owner, + } + }; + Config { format, files, @@ -258,6 +333,7 @@ impl Config { color, #[cfg(unix)] inode: options.is_present(options::INODE), + long, } } } @@ -284,7 +360,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .overrides_with_all(&[ options::FORMAT, options::format::COLUMNS, - options::format::ONELINE, options::format::LONG, ]), ) @@ -292,17 +367,40 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::format::COLUMNS) .short(options::format::COLUMNS) .help("Display the files in columns.") - ) - .arg( - Arg::with_name(options::format::ONELINE) - .short(options::format::ONELINE) - .help("List one file per line.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + ]), ) .arg( Arg::with_name(options::format::LONG) .short("l") .long(options::format::LONG) .help("Display detailed information.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::ONELINE, + options::format::LONG, + ]), + ) + // The next three arguments do not override with the other format + // options, see the comment in Config::from for the reason. + .arg( + Arg::with_name(options::format::ONELINE) + .short(options::format::ONELINE) + .help("List one file per line.") + ) + .arg( + Arg::with_name(options::format::LONG_NO_GROUP) + .short(options::format::LONG_NO_GROUP) + .help("Long format without group information. Identical to --format=long with --no-group.") + ) + .arg( + Arg::with_name(options::format::LONG_NO_OWNER) + .short(options::format::LONG_NO_OWNER) + .help("Long format without owner information.") ) // Time arguments @@ -329,8 +427,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("If the long listing format (e.g., -l, -o) is being used, print the status \ change time (the ‘ctime’ in the inode) instead of the modification time. When \ explicitly sorting by time (--sort=time or -t) or when not using a long listing \ - format, sort according to the status change time.", - )) + format, sort according to the status change time.") + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) + ) .arg( Arg::with_name(options::time::ACCESS) .short(options::time::ACCESS) @@ -338,6 +441,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { access time instead of the modification time. When explicitly sorting by time \ (--sort=time or -t) or when not using a long listing format, sort according to the \ access time.") + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) ) // Sort arguments @@ -359,12 +467,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::sort::SIZE) .short(options::sort::SIZE) - .help("Sort by file size, largest first."), + .help("Sort by file size, largest first.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + ]) ) .arg( Arg::with_name(options::sort::TIME) .short(options::sort::TIME) - .help("Sort by modification time (the 'mtime' in the inode), newest first."), + .help("Sort by modification time (the 'mtime' in the inode), newest first.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + ]) ) .arg( Arg::with_name(options::sort::NONE) @@ -372,8 +492,27 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("Do not sort; list the files in whatever order they are stored in the \ directory. This is especially useful when listing very large directories, \ since not doing any sorting can be noticeably faster.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + ]) ) + // Long format options + .arg( + Arg::with_name(options::NO_GROUP) + .long(options::NO_GROUP) + .short("-G") + .help("Do not show group in long format.") + ) + .arg( + Arg::with_name(options::AUTHOR) + .long(options::AUTHOR) + .help("Show author in long format. On the supported platforms, the author \ + always matches the file owner.") + ) // Other Flags .arg( Arg::with_name(options::files::ALL) @@ -699,32 +838,45 @@ fn display_item_long( Ok(md) => md, }; - println!( - "{}{}{} {} {} {} {} {} {}", - get_inode(&md, config), + #[cfg(unix)] + { + if config.inode { + print!("{} ", get_inode(&md)); + } + } + + print!( + "{}{} {}", display_file_type(md.file_type()), display_permissions(&md), pad_left(display_symlink_count(&md), max_links), - display_uname(&md, config), - display_group(&md, config), + ); + + if config.long.owner { + print!(" {}", display_uname(&md, config)); + } + + if config.long.group { + print!(" {}", display_group(&md, config)); + } + + // 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 { + print!(" {}", display_uname(&md, config)); + } + + println!( + " {} {} {}", pad_left(display_file_size(&md, config), max_size), display_date(&md, config), - display_file_name(&item, strip, &md, config).contents + display_file_name(&item, strip, &md, config).contents, ); } #[cfg(unix)] -fn get_inode(metadata: &Metadata, config: &Config) -> String { - if config.inode { - format!("{:8} ", metadata.ino()) - } else { - "".to_string() - } -} - -#[cfg(not(unix))] -fn get_inode(_metadata: &Metadata, _config: &Config) -> String { - "".to_string() +fn get_inode(metadata: &Metadata) -> String { + format!("{:8}", metadata.ino()) } // Currently getpwuid is `linux` target only. If it's broken out into @@ -808,7 +960,7 @@ fn format_prefixed(prefixed: NumberPrefix) -> String { NumberPrefix::Standalone(bytes) => bytes.to_string(), NumberPrefix::Prefixed(prefix, bytes) => { // Remove the "i" from "Ki", "Mi", etc. if present - let prefix_str = prefix.symbol().trim_end_matches("i"); + let prefix_str = prefix.symbol().trim_end_matches('i'); // Check whether we get more than 10 if we round up to the first decimal // because we want do display 9.81 as "9.9", not as "10". @@ -861,10 +1013,6 @@ fn display_file_name( ) -> Cell { let mut name = get_file_name(path, strip); - if config.format == Format::Long { - name = get_inode(metadata, config) + &name; - } - if config.classify { let file_type = metadata.file_type(); if file_type.is_dir() { @@ -922,8 +1070,8 @@ fn display_file_name( config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); - if config.format != Format::Long { - name = get_inode(metadata, config) + &name; + if config.format != Format::Long && config.inode { + name = get_inode(metadata) + " " + &name; } let mut width = UnicodeWidthStr::width(&*name); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index e0063aa1a..ac5e0c8b3 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -124,6 +124,108 @@ fn test_ls_long() { } } +#[test] +fn test_ls_long_formats() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-long-formats")); + + // Regex for three names, so all of author, group and owner + let re_three = Regex::new(r"[xrw-]{9} \d ([-0-9_a-z]+ ){3}0").unwrap(); + + // Regex for two names, either: + // - group and owner + // - author and owner + // - author and group + let re_two = Regex::new(r"[xrw-]{9} \d ([-0-9_a-z]+ ){2}0").unwrap(); + + // Regex for one name: author, group or owner + let re_one = Regex::new(r"[xrw-]{9} \d [-0-9_a-z]+ 0").unwrap(); + + // Regex for no names + let re_zero = Regex::new(r"[xrw-]{9} \d 0").unwrap(); + + let result = scene + .ucmd() + .arg("-l") + .arg("--author") + .arg("test-long-formats") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_three.is_match(&result.stdout)); + + let result = scene + .ucmd() + .arg("-l1") + .arg("--author") + .arg("test-long-formats") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_three.is_match(&result.stdout)); + + for arg in &[ + "-l", // only group and owner + "-g --author", // only author and group + "-o --author", // only author and owner + "-lG --author", // only author and owner + "-l --no-group --author", // only author and owner + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_two.is_match(&result.stdout)); + } + + for arg in &[ + "-g", // only group + "-gl", // only group + "-o", // only owner + "-ol", // only owner + "-oG", // only owner + "-lG", // only owner + "-l --no-group", // only owner + "-gG --author", // only author + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_one.is_match(&result.stdout)); + } + + for arg in &[ + "-og", + "-ogl", + "-lgo", + "-gG", + "-g --no-group", + "-og --no-group", + "-og --format=long", + "-ogCl", + "-og --format=vertical -l", + "-og1", + "-og1l", + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_zero.is_match(&result.stdout)); + } +} + #[test] fn test_ls_oneline() { let scene = TestScenario::new(util_name!()); @@ -426,6 +528,55 @@ fn test_ls_ls_color() { assert_eq!(result.stdout, ""); } +#[cfg(unix)] +#[test] +fn test_ls_inode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_inode"; + at.touch(file); + + let re_short = Regex::new(r" *(\d+) test_inode").unwrap(); + let re_long = Regex::new(r" *(\d+) [xrw-]{10} \d .+ test_inode").unwrap(); + + let result = scene.ucmd().arg("test_inode").arg("-i").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_short.is_match(&result.stdout)); + let inode_short = re_short + .captures(&result.stdout) + .unwrap() + .get(1) + .unwrap() + .as_str(); + + let result = scene.ucmd().arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(!re_short.is_match(&result.stdout)); + assert!(!result.stdout.contains(inode_short)); + + let result = scene.ucmd().arg("-li").arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_long.is_match(&result.stdout)); + let inode_long = re_long + .captures(&result.stdout) + .unwrap() + .get(1) + .unwrap() + .as_str(); + + let result = scene.ucmd().arg("-l").arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(!re_long.is_match(&result.stdout)); + assert!(!result.stdout.contains(inode_long)); + + assert_eq!(inode_short, inode_long) +} + #[cfg(not(any(target_vendor = "apple", target_os = "windows")))] // Truncate not available on mac or win #[test] fn test_ls_human_si() {