diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 10d7cc2da..59901f807 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/ls.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" lazy_static = "1.0.1" number_prefix = "0.4" term_grid = "0.1.5" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index b8defa397..5c4cdaa80 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,6 +13,7 @@ extern crate lazy_static; #[macro_use] extern crate uucore; +use clap::{App, Arg}; use number_prefix::NumberPrefix; use std::cmp::Reverse; #[cfg(unix)] @@ -26,6 +27,10 @@ use std::os::unix::fs::MetadataExt; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::time::Duration; +use std::time::{SystemTime, UNIX_EPOCH}; + use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use time::{strftime, Timespec}; #[cfg(unix)] @@ -33,14 +38,17 @@ use unicode_width::UnicodeWidthStr; #[cfg(unix)] use uucore::libc::{mode_t, S_ISGID, S_ISUID, S_ISVTX, S_IWOTH, S_IXGRP, S_IXOTH, S_IXUSR}; -static NAME: &str = "ls"; -static SUMMARY: &str = ""; -static LONG_HELP: &str = " +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = " By default, ls will list the files and contents of any directories on the command line, expect that it will ignore files and directories whose names start with '.' "; +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} + #[cfg(unix)] static DEFAULT_COLORS: &str = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"; @@ -52,7 +60,7 @@ lazy_static! { let codes = LS_COLORS.split(':'); let mut map = HashMap::new(); for c in codes { - let p: Vec<_> = c.split('=').collect(); + let p: Vec<_> = c.splitn(2, '=').collect(); if p.len() == 2 { map.insert(p[0], p[1]); } @@ -65,107 +73,409 @@ lazy_static! { static ref END_CODE: &'static str = COLOR_MAP.get("ec").unwrap_or(&""); } +pub mod options { + pub mod format { + pub static ONELINE: &str = "1"; + pub static LONG: &str = "long"; + pub static COLUMNS: &str = "C"; + } + pub mod files { + pub static ALL: &str = "all"; + pub static ALMOST_ALL: &str = "almost-all"; + } + pub mod sort { + pub static SIZE: &str = "S"; + pub static TIME: &str = "t"; + pub static NONE: &str = "U"; + } + pub mod time { + pub static ACCESS: &str = "u"; + pub static CHANGE: &str = "c"; + } + pub static FORMAT: &str = "format"; + pub static SORT: &str = "sort"; + pub static TIME: &str = "time"; + pub static IGNORE_BACKUPS: &str = "ignore-backups"; + pub static DIRECTORY: &str = "directory"; + pub static CLASSIFY: &str = "classify"; + pub static HUMAN_READABLE: &str = "human-readable"; + pub static INODE: &str = "inode"; + pub static DEREFERENCE: &str = "dereference"; + pub static NUMERIC_UID_GID: &str = "numeric-uid-gid"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; +} + +#[derive(PartialEq, Eq)] +enum Format { + Columns, + Long, + OneLine, +} + +enum Sort { + None, + Name, + Size, + Time, +} + +enum SizeFormats { + Bytes, + Binary, // Powers of 1024, --human-readable +} + +#[derive(PartialEq, Eq)] +enum Files { + All, + AlmostAll, + Normal, +} + +enum Time { + Modification, + Access, + Change, +} + +struct Config { + format: Format, + files: Files, + sort: Sort, + recursive: bool, + reverse: bool, + dereference: bool, + classify: bool, + ignore_backups: bool, + size_format: SizeFormats, + numeric_uid_gid: bool, + directory: bool, + time: Time, + #[cfg(unix)] + inode: bool, + #[cfg(unix)] + color: 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"), + } + } else if options.is_present(options::format::LONG) { + Format::Long + } else if options.is_present(options::format::ONELINE) { + Format::OneLine + } else if options.is_present(options::format::COLUMNS) { + Format::Columns + } else { + Format::Columns + }; + + let files = if options.is_present(options::files::ALL) { + Files::All + } else if options.is_present(options::files::ALMOST_ALL) { + Files::AlmostAll + } else { + Files::Normal + }; + + let sort = if let Some(field) = options.value_of(options::SORT) { + match field { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --sort"), + } + } else if options.is_present(options::sort::TIME) { + Sort::Time + } else if options.is_present(options::sort::SIZE) { + Sort::Size + } else if options.is_present(options::sort::NONE) { + Sort::None + } else { + Sort::Name + }; + + let time = if let Some(field) = options.value_of(options::TIME) { + match field { + "ctime" | "status" => Time::Change, + "access" | "atime" | "use" => Time::Access, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --time"), + } + } else if options.is_present(options::time::ACCESS) { + Time::Access + } else if options.is_present(options::time::CHANGE) { + Time::Change + } else { + Time::Modification + }; + + #[cfg(unix)] + let color = match options.value_of(options::COLOR) { + None => options.is_present(options::COLOR), + Some(val) => match val { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), + /* "never" | "no" | "none" | */ _ => false, + }, + }; + + let size_format = if options.is_present(options::HUMAN_READABLE) { + SizeFormats::Binary + } else { + SizeFormats::Bytes + }; + + Config { + format, + 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), + time, + #[cfg(unix)] + color, + #[cfg(unix)] + inode: options.is_present(options::INODE), + } + } +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let syntax = format!( - "[OPTION]... DIRECTORY - {0} [OPTION]... [FILE]...", - NAME - ); - let matches = app!(&syntax, SUMMARY, LONG_HELP) - .optflag("1", "", "list one file per line.") - .optflag( - "a", - "all", - "Do not ignore hidden files (files with names that start with '.').", - ) - .optflag( - "A", - "almost-all", - "In a directory, do not ignore all file names that start with '.', only ignore \ - '.' and '..'.", - ) - .optflag("B", "ignore-backups", "Ignore entries which end with ~.") - .optflag( - "c", - "", - "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.", - ) - .optflag( - "d", - "directory", - "Only list the names of directories, rather than listing directory contents. \ - This will not follow symbolic links unless one of `--dereference-command-line \ - (-H)`, `--dereference (-L)`, or `--dereference-command-line-symlink-to-dir` is \ - specified.", - ) - .optflag( - "F", - "classify", - "Append a character to each file name indicating the file type. Also, for \ - regular files that are executable, append '*'. The file type indicators are \ - '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ - '>' for doors, and nothing for regular files.", - ) - .optflag( - "h", - "human-readable", - "Print human readable file sizes (e.g. 1K 234M 56G).", - ) - .optflag("i", "inode", "print the index number of each file") - .optflag( - "L", - "dereference", - "When showing file information for a symbolic link, show information for the \ - file the link references rather than the link itself.", - ) - .optflag("l", "long", "Display detailed information.") - .optflag("n", "numeric-uid-gid", "-l with numeric UIDs and GIDs.") - .optflag( - "r", - "reverse", - "Reverse whatever the sorting method is--e.g., list files in reverse \ - alphabetical order, youngest first, smallest first, or whatever.", - ) - .optflag( - "R", - "recursive", - "List the contents of all directories recursively.", - ) - .optflag("S", "", "Sort by file size, largest first.") - .optflag( - "t", - "", - "Sort by modification time (the 'mtime' in the inode), newest first.", - ) - .optflag( - "U", - "", - "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.", - ) - .optflagopt( - "", - "color", - "Color output based on file type.", - "always|auto|never", - ) - .parse(args); + let usage = get_usage(); - list(matches) + let app = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + + // Format arguments + .arg( + Arg::with_name(options::FORMAT) + .long(options::FORMAT) + .help("Set the display format.") + .takes_value(true) + .possible_values(&["long", "verbose", "single-column", "columns"]) + .hide_possible_values(true) + .require_equals(true) + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::ONELINE, + options::format::LONG, + ]), + ) + .arg( + 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.") + ) + .arg( + Arg::with_name(options::format::LONG) + .short("l") + .long(options::format::LONG) + .help("Display detailed information.") + ) + + // Time arguments + .arg( + Arg::with_name(options::TIME) + .long(options::TIME) + .help("Show time in :\n\ + \taccess time (-u): atime, access, use;\n\ + \tchange time (-t): ctime, status.") + .value_name("field") + .takes_value(true) + .possible_values(&["atime", "access", "use", "ctime", "status"]) + .hide_possible_values(true) + .require_equals(true) + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) + ) + .arg( + Arg::with_name(options::time::CHANGE) + .short(options::time::CHANGE) + .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.", + )) + .arg( + Arg::with_name(options::time::ACCESS) + .short(options::time::ACCESS) + .help("If the long listing format (e.g., -l, -o) is being used, print the status \ + 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.") + ) + + // Sort arguments + .arg( + Arg::with_name(options::SORT) + .long(options::SORT) + .help("Sort by : name, none (-U), time (-t) or size (-S)") + .value_name("field") + .takes_value(true) + .possible_values(&["name", "none", "time", "size"]) + .require_equals(true) + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + ]) + ) + .arg( + Arg::with_name(options::sort::SIZE) + .short(options::sort::SIZE) + .help("Sort by file size, largest first."), + ) + .arg( + Arg::with_name(options::sort::TIME) + .short(options::sort::TIME) + .help("Sort by modification time (the 'mtime' in the inode), newest first."), + ) + .arg( + Arg::with_name(options::sort::NONE) + .short(options::sort::NONE) + .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.") + ) + + // Other Flags + .arg( + Arg::with_name(options::files::ALL) + .short("a") + .long(options::files::ALL) + .help("Do not ignore hidden files (files with names that start with '.')."), + ) + .arg( + Arg::with_name(options::files::ALMOST_ALL) + .short("A") + .long(options::files::ALMOST_ALL) + .help( + "In a directory, do not ignore all file names that start with '.', only ignore \ + '.' and '..'.", + ), + ) + .arg( + Arg::with_name(options::IGNORE_BACKUPS) + .short("B") + .long(options::IGNORE_BACKUPS) + .help("Ignore entries which end with ~."), + ) + .arg( + Arg::with_name(options::DIRECTORY) + .short("d") + .long(options::DIRECTORY) + .help( + "Only list the names of directories, rather than listing directory contents. \ + This will not follow symbolic links unless one of `--dereference-command-line \ + (-H)`, `--dereference (-L)`, or `--dereference-command-line-symlink-to-dir` is \ + specified.", + ), + ) + .arg( + Arg::with_name(options::CLASSIFY) + .short("F") + .long(options::CLASSIFY) + .help("Append a character to each file name indicating the file type. Also, for \ + regular files that are executable, append '*'. The file type indicators are \ + '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ + '>' for doors, and nothing for regular files.", + )) + .arg( + Arg::with_name(options::HUMAN_READABLE) + .short("h") + .long(options::HUMAN_READABLE) + .help("Print human readable file sizes (e.g. 1K 234M 56G)."), + ) + .arg( + Arg::with_name(options::INODE) + .short("i") + .long(options::INODE) + .help("print the index number of each file"), + ) + .arg( + Arg::with_name(options::DEREFERENCE) + .short("L") + .long(options::DEREFERENCE) + .help( + "When showing file information for a symbolic link, show information for the \ + file the link references rather than the link itself.", + ), + ) + .arg( + Arg::with_name(options::NUMERIC_UID_GID) + .short("n") + .long(options::NUMERIC_UID_GID) + .help("-l with numeric UIDs and GIDs."), + ) + .arg( + Arg::with_name(options::REVERSE) + .short("r") + .long(options::REVERSE) + .help("Reverse whatever the sorting method is--e.g., list files in reverse \ + alphabetical order, youngest first, smallest first, or whatever.", + )) + .arg( + Arg::with_name(options::RECURSIVE) + .short("R") + .long(options::RECURSIVE) + .help("List the contents of all directories recursively."), + ) + .arg( + Arg::with_name(options::COLOR) + .long(options::COLOR) + .help("Color output based on file type.") + .takes_value(true) + .require_equals(true) + .min_values(0), + ) + + // Positional arguments + .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)); + + let matches = app.get_matches_from(args); + + 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(options: getopts::Matches) -> i32 { - let locs: Vec = if options.free.is_empty() { - vec![String::from(".")] - } else { - options.free.to_vec() - }; +fn list(locs: Vec, config: Config) -> i32 { + let number_of_locs = locs.len(); let mut files = Vec::::new(); let mut dirs = Vec::::new(); @@ -181,9 +491,9 @@ fn list(options: getopts::Matches) -> i32 { } let mut dir = false; - if p.is_dir() && !options.opt_present("d") { + if p.is_dir() && !config.directory { dir = true; - if options.opt_present("l") && !(options.opt_present("L")) { + if config.format == Format::Long && !config.dereference { if let Ok(md) = p.symlink_metadata() { if md.file_type().is_symlink() && !p.ends_with("/") { dir = false; @@ -197,15 +507,15 @@ fn list(options: getopts::Matches) -> 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 options.free.len() > 1 { + if number_of_locs > 1 { println!("\n{}:", dir.to_string_lossy()); } - enter_directory(&dir, &options); + enter_directory(&dir, &config); } if has_failed { 1 @@ -214,126 +524,90 @@ fn list(options: getopts::Matches) -> i32 { } } -#[cfg(any(unix, target_os = "redox"))] -fn sort_entries(entries: &mut Vec, options: &getopts::Matches) { - let mut reverse = options.opt_present("r"); - if options.opt_present("t") { - if options.opt_present("c") { - 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.opt_present("S") { - entries.sort_by_key(|k| get_metadata(k, options).map(|md| md.size()).unwrap_or(0)); - reverse = !reverse; - } else if !options.opt_present("U") { - entries.sort(); +fn sort_entries(entries: &mut Vec, config: &Config) { + match config.sort { + Sort::Time => entries.sort_by_key(|k| { + Reverse( + get_metadata(k, config) + .ok() + .and_then(|md| get_system_time(&md, config)) + .unwrap_or(UNIX_EPOCH), + ) + }), + Sort::Size => entries + .sort_by_key(|k| Reverse(get_metadata(k, config).map(|md| md.len()).unwrap_or(0))), + // The default sort in GNU ls is case insensitive + Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()), + Sort::None => {} } - if reverse { + if config.reverse { entries.reverse(); } } #[cfg(windows)] -fn is_hidden(file_path: &DirEntry) -> std::io::Result { - let metadata = fs::metadata(file_path.path())?; +fn is_hidden(file_path: &DirEntry) -> bool { + let metadata = fs::metadata(file_path.path()).unwrap(); let attr = metadata.file_attributes(); - Ok(((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.')) + ((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('.')) +fn is_hidden(file_path: &DirEntry) -> bool { + file_path.file_name().to_string_lossy().starts_with('.') } -#[cfg(windows)] -fn sort_entries(entries: &mut Vec, options: &getopts::Matches) { - let mut reverse = options.opt_present("r"); - if options.opt_present("t") { - 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.opt_present("S") { - entries.sort_by_key(|k| { - get_metadata(k, options) - .map(|md| md.file_size()) - .unwrap_or(0) - }); - reverse = !reverse; - } else if !options.opt_present("U") { - entries.sort(); - } - - if reverse { - entries.reverse(); - } -} - -fn should_display(entry: &DirEntry, options: &getopts::Matches) -> bool { +fn should_display(entry: &DirEntry, config: &Config) -> bool { let ffi_name = entry.file_name(); let name = ffi_name.to_string_lossy(); - if !options.opt_present("a") && !options.opt_present("A") && is_hidden(entry).unwrap() { + if config.files == Files::Normal && is_hidden(entry) { return false; } - if options.opt_present("B") && name.ends_with('~') { + if config.ignore_backups && name.ends_with('~') { return false; } true } -fn enter_directory(dir: &PathBuf, options: &getopts::Matches) { +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.opt_present("a") { + 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.opt_present("R") { + 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: &getopts::Matches) -> std::io::Result { - if options.opt_present("L") { +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: &getopts::Matches) -> (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) @@ -341,37 +615,31 @@ fn display_dir_entry_size(entry: &PathBuf, options: &getopts::Matches) -> (usize } fn pad_left(string: String, count: usize) -> String { - if count > string.len() { - let pad = count - string.len(); - let pad = String::from_utf8(vec![b' '; pad]).unwrap(); - format!("{}{}", pad, string) - } else { - string - } + format!("{:>width$}", string, width = count) } -fn display_items(items: &[PathBuf], strip: Option<&Path>, options: &getopts::Matches) { - if options.opt_present("long") || options.opt_present("numeric-uid-gid") { +fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config) { + if config.format == Format::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.opt_present("1") { + if config.format != Format::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)), } }); @@ -395,9 +663,9 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, options: &getopts::Mat // 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); } } } @@ -410,9 +678,9 @@ fn display_item_long( strip: Option<&Path>, max_links: usize, max_size: usize, - options: &getopts::Matches, + 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); @@ -423,21 +691,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: &getopts::Matches) -> String { - if options.opt_present("inode") { +fn get_inode(metadata: &Metadata, config: &Config) -> String { + if config.inode { format!("{:8} ", metadata.ino()) } else { "".to_string() @@ -445,7 +713,7 @@ fn get_inode(metadata: &Metadata, options: &getopts::Matches) -> String { } #[cfg(not(unix))] -fn get_inode(_metadata: &Metadata, _options: &getopts::Matches) -> String { +fn get_inode(_metadata: &Metadata, _config: &Config) -> String { "".to_string() } @@ -455,8 +723,8 @@ fn get_inode(_metadata: &Metadata, _options: &getopts::Matches) -> String { use uucore::entries; #[cfg(unix)] -fn display_uname(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("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()) @@ -464,8 +732,8 @@ fn display_uname(metadata: &Metadata, options: &getopts::Matches) -> String { } #[cfg(unix)] -fn display_group(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("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()) @@ -473,55 +741,63 @@ fn display_group(metadata: &Metadata, options: &getopts::Matches) -> String { } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_uname(metadata: &Metadata, _options: &getopts::Matches) -> String { +fn display_uname(_metadata: &Metadata, _config: &Config) -> String { "somebody".to_string() } #[cfg(not(unix))] #[allow(unused_variables)] -fn display_group(metadata: &Metadata, _options: &getopts::Matches) -> String { +fn display_group(_metadata: &Metadata, _config: &Config) -> String { "somegroup".to_string() } +// The implementations for get_time are separated because some options, such +// as ctime will not be available #[cfg(unix)] -fn display_date(metadata: &Metadata, options: &getopts::Matches) -> String { - let secs = if options.opt_present("c") { - metadata.ctime() - } else { - metadata.mtime() - }; - 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: &getopts::Matches) -> String { - if let Ok(mtime) = metadata.modified() { - let time = time::at(Timespec::new( - mtime - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - 0, - )); - strftime("%F %R", &time).unwrap() - } else { - "???".to_string() +fn get_system_time(md: &Metadata, config: &Config) -> Option { + match config.time { + Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)), + Time::Modification => md.modified().ok(), + Time::Access => md.accessed().ok(), } } -fn display_file_size(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("human-readable") { - match NumberPrefix::decimal(metadata.len() as f64) { +#[cfg(not(unix))] +fn get_system_time(md: &Metadata, config: &Config) -> Option { + match config.time { + Time::Modification => md.modified().ok(), + Time::Access => md.accessed().ok(), + _ => None, + } +} + +fn get_time(md: &Metadata, config: &Config) -> Option { + let duration = get_system_time(md, config)? + .duration_since(UNIX_EPOCH) + .ok()?; + let secs = duration.as_secs() as i64; + let nsec = duration.subsec_nanos() as i32; + Some(time::at(Timespec::new(secs, nsec))) +} + +fn display_date(metadata: &Metadata, config: &Config) -> String { + match get_time(metadata, config) { + Some(time) => strftime("%F %R", &time).unwrap(), + None => "???".into(), + } +} + +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. + 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(), } } @@ -551,15 +827,15 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &getopts::Matches, + config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); - if !options.opt_present("long") { - name = get_inode(metadata, options) + &name; + if config.format == Format::Long { + name = get_inode(metadata, config) + &name; } - if options.opt_present("classify") { + if config.classify { let file_type = metadata.file_type(); if file_type.is_dir() { name.push('/'); @@ -568,7 +844,7 @@ fn display_file_name( } } - if options.opt_present("long") && metadata.file_type().is_symlink() { + if config.format == Format::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(); @@ -613,26 +889,17 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &getopts::Matches, + config: &Config, ) -> Cell { let mut name = get_file_name(path, strip); - if !options.opt_present("long") { - name = get_inode(metadata, options) + &name; + if config.format != Format::Long { + name = get_inode(metadata, config) + &name; } let mut width = UnicodeWidthStr::width(&*name); - let color = match options.opt_str("color") { - None => atty::is(atty::Stream::Stdout), - Some(val) => match val.as_ref() { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - let classify = options.opt_present("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() { @@ -682,10 +949,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; @@ -693,7 +960,7 @@ fn display_file_name( } } - if options.opt_present("long") && metadata.file_type().is_symlink() { + if config.format == Format::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" }; @@ -710,8 +977,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") diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index a1c3d7569..d01aaffd9 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -57,6 +57,36 @@ fn test_ls_a() { assert!(!result.stdout.contains("..")); } +#[test] +fn test_ls_columns() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-columns-1")); + at.touch(&at.plus_as_string("test-columns-2")); + + // Columns is the default + let result = scene.ucmd().run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-columns-1\ntest-columns-2\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-columns-1 test-columns-2\n"); + + for option in &["-C", "--format=columns"] { + let result = scene.ucmd().arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-columns-1\ntest-columns-2\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-columns-1 test-columns-2\n"); + } +} + #[test] fn test_ls_long() { #[cfg(not(windows))] @@ -71,16 +101,20 @@ fn test_ls_long() { } } - let (at, mut ucmd) = at_and_ucmd!(); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; at.touch(&at.plus_as_string("test-long")); - let result = ucmd.arg("-l").arg("test-long").succeeds(); - println!("stderr = {:?}", result.stderr); - println!("stdout = {:?}", result.stdout); - #[cfg(not(windows))] - assert!(result.stdout.contains("-rw-rw-r--")); - #[cfg(windows)] - assert!(result.stdout.contains("---------- 1 somebody somegroup")); + for arg in &["-l", "--long", "--format=long", "--format=verbose"] { + let result = scene.ucmd().arg(arg).arg("test-long").succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + #[cfg(not(windows))] + assert!(result.stdout.contains("-rw-rw-r--")); + + #[cfg(windows)] + assert!(result.stdout.contains("---------- 1 somebody somegroup")); + } #[cfg(not(windows))] { @@ -90,6 +124,24 @@ fn test_ls_long() { } } +#[test] +fn test_ls_oneline() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-oneline-1")); + at.touch(&at.plus_as_string("test-oneline-2")); + + // Bit of a weird situation: in the tests oneline and columns have the same output, + // except on Windows. + for option in &["-1", "--format=single-column"] { + let result = scene.ucmd().arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!(result.stdout, "test-oneline-1\ntest-oneline-2\n"); + } +} + #[test] fn test_ls_deref() { let scene = TestScenario::new(util_name!()); @@ -166,27 +218,54 @@ fn test_ls_order_size() { } #[test] -fn test_ls_order_creation() { +fn test_ls_long_ctime() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-long-ctime-1"); + let result = scene.ucmd().arg("-lc").succeeds(); + + // Should show the time on Unix, but question marks on windows. + #[cfg(unix)] + assert!(result.stdout.contains(":")); + #[cfg(not(unix))] + assert!(result.stdout.contains("???")); +} + +#[test] +fn test_ls_order_time() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; at.touch("test-1"); at.append("test-1", "1"); - sleep(Duration::from_millis(500)); + sleep(Duration::from_millis(100)); at.touch("test-2"); at.append("test-2", "22"); - sleep(Duration::from_millis(500)); + sleep(Duration::from_millis(100)); at.touch("test-3"); at.append("test-3", "333"); - sleep(Duration::from_millis(500)); + sleep(Duration::from_millis(100)); at.touch("test-4"); at.append("test-4", "4444"); + sleep(Duration::from_millis(100)); + + // Read test-3, only changing access time + at.read("test-3"); + + // Set permissions of test-2, only changing ctime + std::fs::set_permissions( + at.plus_as_string("test-2"), + at.metadata("test-2").permissions(), + ) + .unwrap(); let result = scene.ucmd().arg("-al").run(); println!("stderr = {:?}", result.stderr); println!("stdout = {:?}", result.stdout); assert!(result.success); + // ctime was changed at write, so the order is 4 3 2 1 let result = scene.ucmd().arg("-t").run(); println!("stderr = {:?}", result.stderr); println!("stdout = {:?}", result.stdout); @@ -196,7 +275,7 @@ fn test_ls_order_creation() { #[cfg(windows)] assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); - let result = scene.ucmd().arg("-t").arg("-r").run(); + let result = scene.ucmd().arg("-tr").run(); println!("stderr = {:?}", result.stderr); println!("stdout = {:?}", result.stdout); assert!(result.success); @@ -204,6 +283,31 @@ fn test_ls_order_creation() { assert_eq!(result.stdout, "test-1\ntest-2\ntest-3\ntest-4\n"); #[cfg(windows)] assert_eq!(result.stdout, "test-1 test-2 test-3 test-4\n"); + + // 3 was accessed last in the read + // So the order should be 2 3 4 1 + let result = scene.ucmd().arg("-tu").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-3\ntest-4\ntest-2\ntest-1\n"); + + // Access time does not seem to be set on Windows on read call + // so the order is 4 3 2 1 + #[cfg(windows)] + assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); + + // test-2 had the last ctime change when the permissions were set + // So the order should be 2 4 3 1 + #[cfg(unix)] + { + let result = scene.ucmd().arg("-tc").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!(result.stdout, "test-2\ntest-4\ntest-3\ntest-1\n"); + } } #[test] @@ -270,19 +374,56 @@ fn test_ls_recursive() { assert!(result.stdout.contains("a\\b:\nb")); } +#[cfg(unix)] #[test] fn test_ls_ls_color() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; at.mkdir("a"); + at.mkdir("a/nested_dir"); at.mkdir("z"); - at.touch(&at.plus_as_string("a/a")); - scene.ucmd().arg("--color").succeeds(); - scene.ucmd().arg("--color=always").succeeds(); - scene.ucmd().arg("--color=never").succeeds(); - scene.ucmd().arg("--color").arg("a").succeeds(); - scene.ucmd().arg("--color=always").arg("a/a").succeeds(); - scene.ucmd().arg("--color=never").arg("z").succeeds(); + at.touch(&at.plus_as_string("a/nested_file")); + at.touch("test-color"); + + let a_with_colors = "\x1b[01;34ma\x1b[0m"; + let z_with_colors = "\x1b[01;34mz\x1b[0m"; + let nested_dir_with_colors = "\x1b[01;34mnested_dir\x1b[0m"; + + // Color is disabled by default + let result = scene.ucmd().succeeds(); + assert!(!result.stdout.contains(a_with_colors)); + assert!(!result.stdout.contains(z_with_colors)); + + // Color should be enabled + let result = scene.ucmd().arg("--color").succeeds(); + assert!(result.stdout.contains(a_with_colors)); + assert!(result.stdout.contains(z_with_colors)); + + // Color should be enabled + let result = scene.ucmd().arg("--color=always").succeeds(); + assert!(result.stdout.contains(a_with_colors)); + assert!(result.stdout.contains(z_with_colors)); + + // Color should be disabled + let result = scene.ucmd().arg("--color=never").succeeds(); + assert!(!result.stdout.contains(a_with_colors)); + assert!(!result.stdout.contains(z_with_colors)); + + // Nested dir should be shown and colored + let result = scene.ucmd().arg("--color").arg("a").succeeds(); + assert!(result.stdout.contains(nested_dir_with_colors)); + + // Color has no effect + let result = scene + .ucmd() + .arg("--color=always") + .arg("a/nested_file") + .succeeds(); + assert!(result.stdout.contains("a/nested_file\n")); + + // No output + let result = scene.ucmd().arg("--color=never").arg("z").succeeds(); + assert_eq!(result.stdout, ""); } #[cfg(not(any(target_vendor = "apple", target_os = "windows")))] // Truncate not available on mac or win