From 7c8e8b2d4cce76b7c3bdddd11a1dcc4b683d6e4e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 14 Mar 2021 12:22:32 +0100 Subject: [PATCH] ls: refactor arguments into a config struct --- src/uu/ls/src/ls.rs | 419 ++++++++++++++++++++++++++------------------ 1 file changed, 253 insertions(+), 166 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index ec17ec2b3..d61a45702 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -42,10 +42,7 @@ static ABOUT: &str = " "; fn get_usage() -> String { - format!( - "{0} [OPTION]... [FILE]...", - executable!() - ) + format!("{0} [OPTION]... [FILE]...", executable!()) } #[cfg(unix)] @@ -73,11 +70,10 @@ lazy_static! { } pub mod options { - pub static ONE: &str = "1"; + pub static ONELINE: &str = "1"; pub static ALL: &str = "all"; pub static ALMOST_ALL: &str = "almost-all"; pub static IGNORE_BACKUPS: &str = "ignore-backups"; - pub static COLUMNS: &str = "c"; pub static DIRECTORY: &str = "directory"; pub static CLASSIFY: &str = "classify"; pub static HUMAN_READABLE: &str = "human-readable"; @@ -90,10 +86,128 @@ pub mod options { pub static SORT_SIZE: &str = "S"; pub static SORT_TIME: &str = "t"; pub static SORT_NONE: &str = "U"; + pub static SORT_CTIME: &str = "c"; pub static COLOR: &str = "color"; pub static PATHS: &str = "paths"; } +#[derive(PartialEq, Eq)] +enum DisplayOptions { + Columns, + Long, + OneLine, +} + +enum Sort { + None, + Name, + Size, + Time, + CTime, +} + +enum SizeFormats { + Bytes, + Binary, // Powers of 1024, --human-readable +} + +#[derive(PartialEq, Eq)] +enum Files { + All, + AlmostAll, + Normal, +} + +struct Config { + display: DisplayOptions, + files: Files, + sort: Sort, + + recursive: bool, + reverse: bool, + dereference: bool, + classify: bool, + ignore_backups: bool, + size_format: SizeFormats, + numeric_uid_gid: bool, + directory: bool, + #[cfg(unix)] + inode: bool, + #[cfg(unix)] + color: bool, +} + +impl Config { + fn from(options: clap::ArgMatches) -> Config { + let display = if options.is_present(options::LONG) { + DisplayOptions::Long + } else if options.is_present(options::ONELINE) { + DisplayOptions::OneLine + } else { + DisplayOptions::Columns + }; + + let files = if options.is_present(options::ALL) { + Files::All + } else if options.is_present(options::ALMOST_ALL) { + Files::AlmostAll + } else { + Files::Normal + }; + + let sort = if options.is_present(options::SORT_TIME) { + Sort::Time + } else if options.is_present(options::SORT_CTIME) { + Sort::CTime + } else if options.is_present(options::SORT_SIZE) { + Sort::Size + } else if options.is_present(options::SORT_NONE) { + Sort::None + } else { + Sort::Name + }; + + #[cfg(unix)] + let color = if options.is_present(options::COLOR) { + match options.value_of(options::COLOR) { + None => atty::is(atty::Stream::Stdout), + Some(val) => match val { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), + /* "never" | "no" | "none" | */ _ => false, + }, + } + } else { + false + }; + + let size_format = if options.is_present(options::HUMAN_READABLE) { + SizeFormats::Binary + } else { + SizeFormats::Bytes + }; + + Config { + display, + files, + sort, + + recursive: options.is_present(options::RECURSIVE), + reverse: options.is_present(options::REVERSE), + dereference: options.is_present(options::DEREFERENCE), + classify: options.is_present(options::CLASSIFY), + ignore_backups: options.is_present(options::IGNORE_BACKUPS), + size_format, + numeric_uid_gid: options.is_present(options::NUMERIC_UID_GID), + directory: options.is_present(options::DIRECTORY), + #[cfg(unix)] + color, + #[cfg(unix)] + inode: options.is_present(options::INODE), + } + } +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); @@ -104,8 +218,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .about(ABOUT) .usage(&usage[..]) .arg( - Arg::with_name(options::ONE) - .short(options::ONE) + Arg::with_name(options ::ONELINE) + .short(options ::ONELINE) .help("list one file per line."), ) .arg( @@ -130,8 +244,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("Ignore entries which end with ~."), ) .arg( - Arg::with_name(options::COLUMNS) - .short(options::COLUMNS) + Arg::with_name(options::SORT_CTIME) + .short(options::SORT_CTIME) .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 \ @@ -231,15 +345,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)) .get_matches_from(args); - list(matches) -} - -fn list(options: clap::ArgMatches) -> i32 { - let locs: Vec = options + let locs = matches .values_of(options::PATHS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_else(|| vec![String::from(".")]); + list(locs, Config::from(matches)) +} + +fn list(locs: Vec, config: Config) -> i32 { let number_of_locs = locs.len(); let mut files = Vec::::new(); @@ -256,9 +370,9 @@ fn list(options: clap::ArgMatches) -> i32 { } let mut dir = false; - if p.is_dir() && !options.is_present(options::DIRECTORY) { + if p.is_dir() && !config.directory { dir = true; - if options.is_present(options::LONG) && !options.is_present(options::DEREFERENCE) { + if config.display == DisplayOptions::Long && !config.dereference { if let Ok(md) = p.symlink_metadata() { if md.file_type().is_symlink() && !p.ends_with("/") { dir = false; @@ -272,15 +386,15 @@ fn list(options: clap::ArgMatches) -> i32 { files.push(p); } } - sort_entries(&mut files, &options); - display_items(&files, None, &options); + sort_entries(&mut files, &config); + display_items(&files, None, &config); - sort_entries(&mut dirs, &options); + sort_entries(&mut dirs, &config); for dir in dirs { if number_of_locs > 1 { println!("\n{}:", dir.to_string_lossy()); } - enter_directory(&dir, &options); + enter_directory(&dir, &config); } if has_failed { 1 @@ -290,128 +404,118 @@ fn list(options: clap::ArgMatches) -> i32 { } #[cfg(any(unix, target_os = "redox"))] -fn sort_entries(entries: &mut Vec, options: &clap::ArgMatches) { - let mut reverse = options.is_present(options::REVERSE); - if options.is_present(options::SORT_TIME) { - if options.is_present(options::COLUMNS) { - entries.sort_by_key(|k| { - Reverse(get_metadata(k, options).map(|md| md.ctime()).unwrap_or(0)) - }); - } else { - entries.sort_by_key(|k| { - // Newest first - Reverse( - get_metadata(k, options) - .and_then(|md| md.modified()) - .unwrap_or(std::time::UNIX_EPOCH), - ) - }); - } - } else if options.is_present(options::SORT_SIZE) { - entries.sort_by_key(|k| get_metadata(k, options).map(|md| md.size()).unwrap_or(0)); - reverse = !reverse; - } else if !options.is_present(options::SORT_NONE) { - entries.sort(); - } - - if reverse { - entries.reverse(); - } -} - -#[cfg(windows)] -fn is_hidden(file_path: &DirEntry) -> std::io::Result { - let metadata = fs::metadata(file_path.path())?; - let attr = metadata.file_attributes(); - Ok(((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.')) -} - -#[cfg(unix)] -fn is_hidden(file_path: &DirEntry) -> std::io::Result { - Ok(file_path.file_name().to_string_lossy().starts_with('.')) -} - -#[cfg(windows)] -fn sort_entries(entries: &mut Vec, options: &clap::ArgMatches) { - let mut reverse = options.is_present(options::REVERSE); - if options.is_present(options::SORT_TIME) { - entries.sort_by_key(|k| { - // Newest first +fn sort_entries(entries: &mut Vec, config: &Config) { + match config.sort { + Sort::CTime => entries + .sort_by_key(|k| Reverse(get_metadata(k, config).map(|md| md.ctime()).unwrap_or(0))), + Sort::Time => entries.sort_by_key(|k| { Reverse( - get_metadata(k, options) + get_metadata(k, config) .and_then(|md| md.modified()) .unwrap_or(std::time::UNIX_EPOCH), ) - }); - } else if options.is_present(options::SORT_SIZE) { - entries.sort_by_key(|k| { - get_metadata(k, options) - .map(|md| md.file_size()) - .unwrap_or(0) - }); - reverse = !reverse; - } else if !options.is_present(options::SORT_NONE) { - entries.sort(); + }), + Sort::Size => entries + .sort_by_key(|k| Reverse(get_metadata(k, config).map(|md| md.size()).unwrap_or(0))), + Sort::Name => entries.sort(), + Sort::None => {} } - if reverse { + if config.reverse { entries.reverse(); } } -fn should_display(entry: &DirEntry, options: &clap::ArgMatches) -> bool { +#[cfg(windows)] +fn is_hidden(file_path: &DirEntry) -> bool { + let metadata = fs::metadata(file_path.path()).unwrap(); + let attr = metadata.file_attributes(); + ((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.') +} + +#[cfg(unix)] +fn is_hidden(file_path: &DirEntry) -> bool { + file_path.file_name().to_string_lossy().starts_with('.') +} + +#[cfg(windows)] +fn sort_entries(entries: &mut Vec, config: &Config) { + match config.sort { + Sort::CTime | Sort::Time => entries.sort_by_key(|k| { + // Newest first + Reverse( + get_metadata(k, config) + .and_then(|md| md.modified()) + .unwrap_or(std::time::UNIX_EPOCH), + ) + }), + Sort::Size => entries.sort_by_key(|k| { + // Largest first + Reverse( + get_metadata(k, config) + .map(|md| md.file_size()) + .unwrap_or(0), + ) + }), + Sort::Name => entries.sort(), + Sort::None => {}, + } + + if config.reverse { + entries.reverse(); + } +} + +fn should_display(entry: &DirEntry, config: &Config) -> bool { let ffi_name = entry.file_name(); let name = ffi_name.to_string_lossy(); - if !options.is_present(options::ALL) - && !options.is_present(options::ALMOST_ALL) - && is_hidden(entry).unwrap() - { + if config.files == Files::Normal && is_hidden(entry) { return false; } - if options.is_present(options::IGNORE_BACKUPS) && name.ends_with('~') { + if config.ignore_backups && name.ends_with('~') { return false; } true } -fn enter_directory(dir: &PathBuf, options: &clap::ArgMatches) { +fn enter_directory(dir: &PathBuf, config: &Config) { let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect)); - entries.retain(|e| should_display(e, options)); + entries.retain(|e| should_display(e, config)); let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect(); - sort_entries(&mut entries, options); + sort_entries(&mut entries, config); - if options.is_present(options::ALL) { + if config.files == Files::All { let mut display_entries = entries.clone(); display_entries.insert(0, dir.join("..")); display_entries.insert(0, dir.join(".")); - display_items(&display_entries, Some(dir), options); + display_items(&display_entries, Some(dir), config); } else { - display_items(&entries, Some(dir), options); + display_items(&entries, Some(dir), config); } - if options.is_present(options::RECURSIVE) { + if config.recursive { for e in entries.iter().filter(|p| p.is_dir()) { println!("\n{}:", e.to_string_lossy()); - enter_directory(&e, options); + enter_directory(&e, config); } } } -fn get_metadata(entry: &PathBuf, options: &clap::ArgMatches) -> std::io::Result { - if options.is_present(options::DEREFERENCE) { +fn get_metadata(entry: &PathBuf, config: &Config) -> std::io::Result { + if config.dereference { entry.metadata().or_else(|_| entry.symlink_metadata()) } else { entry.symlink_metadata() } } -fn display_dir_entry_size(entry: &PathBuf, options: &clap::ArgMatches) -> (usize, usize) { - if let Ok(md) = get_metadata(entry, options) { +fn display_dir_entry_size(entry: &PathBuf, config: &Config) -> (usize, usize) { + if let Ok(md) = get_metadata(entry, config) { ( display_symlink_count(&md).len(), - display_file_size(&md, options).len(), + display_file_size(&md, config).len(), ) } else { (0, 0) @@ -422,28 +526,28 @@ fn pad_left(string: String, count: usize) -> String { format!("{:>width$}", string, width = count) } -fn display_items(items: &[PathBuf], strip: Option<&Path>, options: &clap::ArgMatches) { - if options.is_present(options::LONG) || options.is_present(options::NUMERIC_UID_GID) { +fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config) { + if config.display == DisplayOptions::Long || config.numeric_uid_gid { let (mut max_links, mut max_size) = (1, 1); for item in items { - let (links, size) = display_dir_entry_size(item, options); + let (links, size) = display_dir_entry_size(item, config); max_links = links.max(max_links); max_size = size.max(max_size); } for item in items { - display_item_long(item, strip, max_links, max_size, options); + display_item_long(item, strip, max_links, max_size, config); } } else { - if !options.is_present(options::ONE) { + if config.display != DisplayOptions::OneLine { let names = items.iter().filter_map(|i| { - let md = get_metadata(i, options); + let md = get_metadata(i, config); match md { Err(e) => { let filename = get_file_name(i, strip); show_error!("'{}': {}", filename, e); None } - Ok(md) => Some(display_file_name(&i, strip, &md, options)), + Ok(md) => Some(display_file_name(&i, strip, &md, config)), } }); @@ -467,9 +571,9 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, options: &clap::ArgMat // Couldn't display a grid, either because we don't know // the terminal width or because fit_into_width failed for i in items { - let md = get_metadata(i, options); + let md = get_metadata(i, config); if let Ok(md) = md { - println!("{}", display_file_name(&i, strip, &md, options).contents); + println!("{}", display_file_name(&i, strip, &md, config).contents); } } } @@ -482,9 +586,9 @@ fn display_item_long( strip: Option<&Path>, max_links: usize, max_size: usize, - options: &clap::ArgMatches, + config: &Config, ) { - let md = match get_metadata(item, options) { + let md = match get_metadata(item, config) { Err(e) => { let filename = get_file_name(&item, strip); show_error!("{}: {}", filename, e); @@ -495,21 +599,21 @@ fn display_item_long( println!( "{}{}{} {} {} {} {} {} {}", - get_inode(&md, options), + get_inode(&md, config), display_file_type(md.file_type()), display_permissions(&md), pad_left(display_symlink_count(&md), max_links), - display_uname(&md, options), - display_group(&md, options), - pad_left(display_file_size(&md, options), max_size), - display_date(&md, options), - display_file_name(&item, strip, &md, options).contents + display_uname(&md, config), + display_group(&md, config), + pad_left(display_file_size(&md, config), max_size), + display_date(&md, config), + display_file_name(&item, strip, &md, config).contents ); } #[cfg(unix)] -fn get_inode(metadata: &Metadata, options: &clap::ArgMatches) -> String { - if options.is_present(options::INODE) { +fn get_inode(metadata: &Metadata, config: &Config) -> String { + if config.inode { format!("{:8} ", metadata.ino()) } else { "".to_string() @@ -517,7 +621,7 @@ fn get_inode(metadata: &Metadata, options: &clap::ArgMatches) -> String { } #[cfg(not(unix))] -fn get_inode(_metadata: &Metadata, _options: &clap::ArgMatches) -> String { +fn get_inode(_metadata: &Metadata, _config: &Config) -> String { "".to_string() } @@ -527,8 +631,8 @@ fn get_inode(_metadata: &Metadata, _options: &clap::ArgMatches) -> String { use uucore::entries; #[cfg(unix)] -fn display_uname(metadata: &Metadata, options: &clap::ArgMatches) -> String { - if options.is_present(options::NUMERIC_UID_GID) { +fn display_uname(metadata: &Metadata, config: &Config) -> String { + if config.numeric_uid_gid { metadata.uid().to_string() } else { entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string()) @@ -536,8 +640,8 @@ fn display_uname(metadata: &Metadata, options: &clap::ArgMatches) -> String { } #[cfg(unix)] -fn display_group(metadata: &Metadata, options: &clap::ArgMatches) -> String { - if options.is_present(options::NUMERIC_UID_GID) { +fn display_group(metadata: &Metadata, config: &Config) -> String { + if config.numeric_uid_gid { metadata.gid().to_string() } else { entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string()) @@ -545,31 +649,29 @@ fn display_group(metadata: &Metadata, options: &clap::ArgMatches) -> String { } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_uname(metadata: &Metadata, _options: &clap::ArgMatches) -> String { +fn display_uname(_metadata: &Metadata, _config: &Config) -> String { "somebody".to_string() } #[cfg(not(unix))] #[allow(unused_variables)] -fn display_group(metadata: &Metadata, _options: &clap::ArgMatches) -> String { +fn display_group(_metadata: &Metadata, _config: &Config) -> String { "somegroup".to_string() } #[cfg(unix)] -fn display_date(metadata: &Metadata, options: &clap::ArgMatches) -> String { - let secs = if options.is_present(options::COLUMNS) { - metadata.ctime() - } else { - metadata.mtime() +fn display_date(metadata: &Metadata, config: &Config) -> String { + let secs = match config.sort { + Sort::CTime => metadata.ctime(), + Sort::Time => metadata.mtime(), + _ => 0, }; let time = time::at(Timespec::new(secs, 0)); strftime("%F %R", &time).unwrap() } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_date(metadata: &Metadata, options: &clap::ArgMatches) -> String { +fn display_date(metadata: &Metadata, _config: &Config) -> String { if let Ok(mtime) = metadata.modified() { let time = time::at(Timespec::new( mtime @@ -584,18 +686,17 @@ fn display_date(metadata: &Metadata, options: &clap::ArgMatches) -> String { } } -fn display_file_size(metadata: &Metadata, options: &clap::ArgMatches) -> String { +fn display_file_size(metadata: &Metadata, config: &Config) -> String { // NOTE: The human-readable behaviour deviates from the GNU ls. // The GNU ls uses binary prefixes by default. - if options.is_present(options::HUMAN_READABLE) { - match NumberPrefix::decimal(metadata.len() as f64) { + match config.size_format { + SizeFormats::Binary => match NumberPrefix::decimal(metadata.len() as f64) { NumberPrefix::Standalone(bytes) => bytes.to_string(), NumberPrefix::Prefixed(prefix, bytes) => { format!("{:.2}{}", bytes, prefix).to_uppercase() } - } - } else { - metadata.len().to_string() + }, + SizeFormats::Bytes => metadata.len().to_string(), } } @@ -625,15 +726,15 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &clap::ArgMatches, + config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); - if !options.is_present(options::LONG) { - name = get_inode(metadata, options) + &name; + if config.display == DisplayOptions::Long { + name = get_inode(metadata, config) + &name; } - if options.is_present(options::CLASSIFY) { + if config.classify { let file_type = metadata.file_type(); if file_type.is_dir() { name.push('/'); @@ -642,7 +743,7 @@ fn display_file_name( } } - if options.is_present(options::LONG) && metadata.file_type().is_symlink() { + if config.display == DisplayOptions::Long && metadata.file_type().is_symlink() { if let Ok(target) = path.read_link() { // We don't bother updating width here because it's not used for long listings let target_name = target.to_string_lossy().to_string(); @@ -687,30 +788,17 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &clap::ArgMatches, + config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); - if !options.is_present(options::LONG) { - name = get_inode(metadata, options) + &name; + if config.display != DisplayOptions::Long { + name = get_inode(metadata, config) + &name; } let mut width = UnicodeWidthStr::width(&*name); - let color = if options.is_present(options::COLOR) { - match options.value_of(options::COLOR) { - None => atty::is(atty::Stream::Stdout), - Some(val) => match val { - "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), - /* "never" | "no" | "none" | */ _ => false, - }, - } - } else { - false - }; - let classify = options.is_present(options::CLASSIFY); let ext; - if color || classify { + if config.color || config.classify { let file_type = metadata.file_type(); let (code, sym) = if file_type.is_dir() { @@ -760,10 +848,10 @@ fn display_file_name( ("", None) }; - if color { + if config.color { name = color_name(name, code); } - if classify { + if config.classify { if let Some(s) = sym { name.push(s); width += 1; @@ -771,7 +859,7 @@ fn display_file_name( } } - if options.is_present(options::LONG) && metadata.file_type().is_symlink() { + if config.display == DisplayOptions::Long && metadata.file_type().is_symlink() { if let Ok(target) = path.read_link() { // We don't bother updating width here because it's not used for long listings let code = if target.exists() { "fi" } else { "mi" }; @@ -788,8 +876,7 @@ fn display_file_name( } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_symlink_count(metadata: &Metadata) -> String { +fn display_symlink_count(_metadata: &Metadata) -> String { // Currently not sure of how to get this on Windows, so I'm punting. // Git Bash looks like it may do the same thing. String::from("1")