From 7f717c3af326a463c5f75d09eed6de7184f63d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Fri, 1 May 2020 21:15:50 -0700 Subject: [PATCH 01/18] checkpoint --- Cargo.lock | 7 + Cargo.toml | 5 +- src/fs/fields.rs | 1 + src/fs/file.rs | 232 +++++++++++--------- src/fs/filter.rs | 111 +++++----- src/options/filter.rs | 3 + src/options/parser.rs | 350 +++++++++++++++++-------------- src/options/style.rs | 8 +- src/options/view.rs | 8 +- src/output/file_name.rs | 16 ++ src/output/render/mod.rs | 4 + src/output/render/permissions.rs | 1 + src/output/table.rs | 206 ++++++++++++------ src/style/colours.rs | 2 + 14 files changed, 574 insertions(+), 380 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8323b908..c0905661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,7 @@ dependencies = [ "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "os_str_bytes 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -272,6 +273,11 @@ dependencies = [ "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "os_str_bytes" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pad" version = "0.1.5" @@ -506,6 +512,7 @@ dependencies = [ "checksum number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" "checksum openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)" = "53ed5f31d294bdf5f7a4ba0a206c2754b0f60e9a63b7e3076babc5317873c797" "checksum openssl-sys 0.9.48 (registry+https://github.com/rust-lang/crates.io-index)" = "b5ba300217253bcc5dc68bed23d782affa45000193866e025329aa8a7a9f05b8" +"checksum os_str_bytes 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ced912e1439b63a8172b943887c33373f226a4a9cfab95d09c0e3c44851d04d4" "checksum pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9b8de33465981073e32e1d75bb89ade49062bb853e7c97ec2c13439095563a" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" diff --git a/Cargo.toml b/Cargo.toml index b683c860..f023b428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,11 @@ scoped_threadpool = "0.1.9" term_grid = "0.1.7" term_size = "0.3.1" unicode-width = "0.1.5" -users = "0.9.1" zoneinfo_compiled = "0.4.8" +os_str_bytes = "2.2.0" + +[target.'cfg(unix)'.dependencies] +users = "0.9.1" [dependencies.git2] version = "0.9.1" diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 87cebfc9..3444adf0 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -82,6 +82,7 @@ pub struct Permissions { /// little more compressed. pub struct PermissionsPlus { pub file_type: Type, + #[cfg(unix)] pub permissions: Permissions, pub xattrs: bool, } diff --git a/src/fs/file.rs b/src/fs/file.rs index 0d42d548..7e1c016a 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -2,9 +2,10 @@ use std::io::Error as IOError; use std::io::Result as IOResult; -use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt}; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; -use std::time::{UNIX_EPOCH, Duration}; +use std::time::{Duration, UNIX_EPOCH}; use log::{debug, error}; @@ -19,7 +20,6 @@ use crate::fs::fields as f; /// information queried at least once, so it makes sense to do all this at the /// start and hold on to all the information. pub struct File<'dir> { - /// The filename portion of this file’s path, including the extension. /// /// This is used to compare against certain filenames (such as checking if @@ -67,39 +67,61 @@ pub struct File<'dir> { impl<'dir> File<'dir> { pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult> - where PD: Into>, - FN: Into> + where + PD: Into>, + FN: Into>, { let parent_dir = parent_dir.into(); - let name = filename.into().unwrap_or_else(|| File::filename(&path)); - let ext = File::ext(&path); + let name = filename.into().unwrap_or_else(|| File::filename(&path)); + let ext = File::ext(&path); debug!("Statting file {:?}", &path); - let metadata = std::fs::symlink_metadata(&path)?; + let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = false; - Ok(File { path, parent_dir, metadata, ext, name, is_all_all }) + Ok(File { + path, + parent_dir, + metadata, + ext, + name, + is_all_all, + }) } pub fn new_aa_current(parent_dir: &'dir Dir) -> IOResult> { - let path = parent_dir.path.to_path_buf(); - let ext = File::ext(&path); + let path = parent_dir.path.to_path_buf(); + let ext = File::ext(&path); debug!("Statting file {:?}", &path); - let metadata = std::fs::symlink_metadata(&path)?; + let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; - Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: ".".to_string(), is_all_all }) + Ok(File { + path, + parent_dir: Some(parent_dir), + metadata, + ext, + name: ".".to_string(), + is_all_all, + }) } pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> IOResult> { - let ext = File::ext(&path); + let ext = File::ext(&path); debug!("Statting file {:?}", &path); - let metadata = std::fs::symlink_metadata(&path)?; + let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; - Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: "..".to_string(), is_all_all }) + Ok(File { + path, + parent_dir: Some(parent_dir), + metadata, + ext, + name: "..".to_string(), + is_all_all, + }) } /// A file’s name is derived from its string. This needs to handle directories @@ -107,9 +129,12 @@ impl<'dir> File<'dir> { /// use the last component as the name. pub fn filename(path: &Path) -> String { if let Some(back) = path.components().next_back() { - back.as_os_str().to_string_lossy().to_string() - } - else { + let name = back.as_os_str().to_string_lossy().to_string(); + #[cfg(unix)] + return name; + #[cfg(windows)] + return name; + } else { // use the path as fallback error!("Path {:?} has no last component", path); path.display().to_string() @@ -127,7 +152,7 @@ impl<'dir> File<'dir> { fn ext(path: &Path) -> Option { let name = path.file_name().map(|f| f.to_string_lossy().to_string())?; - name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase()) + name.rfind('.').map(|p| name[p + 1..].to_ascii_lowercase()) } /// Whether this file is a directory on the filesystem. @@ -170,6 +195,7 @@ impl<'dir> File<'dir> { /// Whether this file is both a regular file *and* executable for the /// current user. An executable file has a different purpose from an /// executable directory, so they should be highlighted differently. + #[cfg(unix)] pub fn is_executable_file(&self) -> bool { let bit = modes::USER_EXECUTE; self.is_file() && (self.metadata.permissions().mode() & bit) == bit @@ -181,40 +207,40 @@ impl<'dir> File<'dir> { } /// Whether this file is a named pipe on the filesystem. + #[cfg(unix)] pub fn is_pipe(&self) -> bool { self.metadata.file_type().is_fifo() } /// Whether this file is a char device on the filesystem. + #[cfg(unix)] pub fn is_char_device(&self) -> bool { self.metadata.file_type().is_char_device() } /// Whether this file is a block device on the filesystem. + #[cfg(unix)] pub fn is_block_device(&self) -> bool { self.metadata.file_type().is_block_device() } /// Whether this file is a socket on the filesystem. + #[cfg(unix)] pub fn is_socket(&self) -> bool { self.metadata.file_type().is_socket() } - /// Re-prefixes the path pointed to by this file, if it’s a symlink, to /// make it an absolute path that can be accessed from whichever /// directory exa is being run from. fn reorient_target_path(&self, path: &Path) -> PathBuf { if path.is_absolute() { path.to_path_buf() - } - else if let Some(dir) = self.parent_dir { + } else if let Some(dir) = self.parent_dir { dir.join(&*path) - } - else if let Some(parent) = self.path.parent() { + } else if let Some(parent) = self.path.parent() { parent.join(&*path) - } - else { + } else { self.path.join(&*path) } } @@ -230,15 +256,14 @@ impl<'dir> File<'dir> { /// existed. If this file cannot be read at all, returns the error that /// we got when we tried to read it. pub fn link_target(&self) -> FileTarget<'dir> { - // We need to be careful to treat the path actually pointed to by // this file — which could be absolute or relative — to the path // we actually look up and turn into a `File` — which needs to be // absolute to be accessible from any directory. debug!("Reading link {:?}", &self.path); let path = match std::fs::read_link(&self.path) { - Ok(p) => p, - Err(e) => return FileTarget::Err(e), + Ok(p) => p, + Err(e) => return FileTarget::Err(e), }; let absolute_path = self.reorient_target_path(&path); @@ -247,9 +272,16 @@ impl<'dir> File<'dir> { // follow links. match std::fs::metadata(&absolute_path) { Ok(metadata) => { - let ext = File::ext(&path); + let ext = File::ext(&path); let name = File::filename(&path); - FileTarget::Ok(Box::new(File { parent_dir: None, path, ext, metadata, name, is_all_all: false })) + FileTarget::Ok(Box::new(File { + parent_dir: None, + path, + ext, + metadata, + name, + is_all_all: false, + })) } Err(e) => { error!("Error following link {:?}: {:#?}", &path, e); @@ -265,6 +297,7 @@ impl<'dir> File<'dir> { /// is uncommon, while you come across directories and other types /// with multiple links much more often. Thus, it should get highlighted /// more attentively. + #[cfg(unix)] pub fn links(&self) -> f::Links { let count = self.metadata.nlink(); @@ -275,6 +308,7 @@ impl<'dir> File<'dir> { } /// This file's inode. + #[cfg(unix)] pub fn inode(&self) -> f::Inode { f::Inode(self.metadata.ino()) } @@ -282,21 +316,23 @@ impl<'dir> File<'dir> { /// This file's number of filesystem blocks. /// /// (Not the size of each block, which we don't actually report on) + #[cfg(unix)] pub fn blocks(&self) -> f::Blocks { if self.is_file() || self.is_link() { f::Blocks::Some(self.metadata.blocks()) - } - else { + } else { f::Blocks::None } } /// The ID of the user that own this file. + #[cfg(unix)] pub fn user(&self) -> f::User { f::User(self.metadata.uid()) } /// The ID of the group that owns this file. + #[cfg(unix)] pub fn group(&self) -> f::Group { f::Group(self.metadata.gid()) } @@ -311,18 +347,19 @@ impl<'dir> File<'dir> { /// usually just have a file size of zero. pub fn size(&self) -> f::Size { if self.is_directory() { - f::Size::None - } - else if self.is_char_device() || self.is_block_device() { - let dev = self.metadata.rdev(); - f::Size::DeviceIDs(f::DeviceIDs { - major: (dev / 256) as u8, - minor: (dev % 256) as u8, - }) - } - else { - f::Size::Some(self.metadata.len()) + return f::Size::None; + }; + #[cfg(unix)] + { + if self.is_char_device() || self.is_block_device() { + let dev = self.metadata.rdev(); + return f::Size::DeviceIDs(f::DeviceIDs { + major: (dev / 256) as u8, + minor: (dev % 256) as u8, + }); + }; } + return f::Size::Some(self.metadata.len()); } /// This file’s last modified timestamp. @@ -335,8 +372,12 @@ impl<'dir> File<'dir> { } /// This file’s last changed timestamp. + #[cfg(unix)] pub fn changed_time(&self) -> Duration { - Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32) + Duration::new( + self.metadata.ctime() as u64, + self.metadata.ctime_nsec() as u32, + ) } /// This file’s last accessed timestamp. @@ -362,54 +403,59 @@ impl<'dir> File<'dir> { /// This is used a the leftmost character of the permissions column. /// The file type can usually be guessed from the colour of the file, but /// ls puts this character there. + #[cfg(windows)] pub fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File - } - else if self.is_directory() { + } else if self.is_directory() { f::Type::Directory + } else { + f::Type::Special } - else if self.is_pipe() { + } + #[cfg(unix)] + pub fn type_char(&self) -> f::Type { + if self.is_file() { + f::Type::File + } else if self.is_directory() { + f::Type::Directory + } else if self.is_pipe() { f::Type::Pipe - } - else if self.is_link() { + } else if self.is_link() { f::Type::Link - } - else if self.is_char_device() { + } else if self.is_char_device() { f::Type::CharDevice - } - else if self.is_block_device() { + } else if self.is_block_device() { f::Type::BlockDevice - } - else if self.is_socket() { + } else if self.is_socket() { f::Type::Socket - } - else { + } else { f::Type::Special } } /// This file’s permissions, with flags for each bit. + #[cfg(unix)] pub fn permissions(&self) -> f::Permissions { let bits = self.metadata.mode(); - let has_bit = |bit| { bits & bit == bit }; + let has_bit = |bit| bits & bit == bit; f::Permissions { - user_read: has_bit(modes::USER_READ), - user_write: has_bit(modes::USER_WRITE), - user_execute: has_bit(modes::USER_EXECUTE), + user_read: has_bit(modes::USER_READ), + user_write: has_bit(modes::USER_WRITE), + user_execute: has_bit(modes::USER_EXECUTE), - group_read: has_bit(modes::GROUP_READ), - group_write: has_bit(modes::GROUP_WRITE), - group_execute: has_bit(modes::GROUP_EXECUTE), + group_read: has_bit(modes::GROUP_READ), + group_write: has_bit(modes::GROUP_WRITE), + group_execute: has_bit(modes::GROUP_EXECUTE), - other_read: has_bit(modes::OTHER_READ), - other_write: has_bit(modes::OTHER_WRITE), - other_execute: has_bit(modes::OTHER_EXECUTE), + other_read: has_bit(modes::OTHER_READ), + other_write: has_bit(modes::OTHER_WRITE), + other_execute: has_bit(modes::OTHER_EXECUTE), - sticky: has_bit(modes::STICKY), - setgid: has_bit(modes::SETGID), - setuid: has_bit(modes::SETUID), + sticky: has_bit(modes::STICKY), + setgid: has_bit(modes::SETGID), + setuid: has_bit(modes::SETUID), } } @@ -418,8 +464,8 @@ impl<'dir> File<'dir> { /// This will always return `false` if the file has no extension. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool { match self.ext { - Some(ref ext) => choices.contains(&&ext[..]), - None => false, + Some(ref ext) => choices.contains(&&ext[..]), + None => false, } } @@ -430,17 +476,14 @@ impl<'dir> File<'dir> { } } - impl<'a> AsRef> for File<'a> { fn as_ref(&self) -> &File<'a> { self } } - /// The result of following a symlink. pub enum FileTarget<'dir> { - /// The symlink pointed at a file that exists. Ok(Box>), @@ -452,27 +495,25 @@ pub enum FileTarget<'dir> { /// file isn’t a link to begin with, but also if, say, we don’t have /// permission to follow it. Err(IOError), - // Err is its own variant, instead of having the whole thing be inside an // `IOResult`, because being unable to follow a symlink is not a serious // error -- we just display the error message and move on. } impl<'dir> FileTarget<'dir> { - /// Whether this link doesn’t lead to a file, for whatever reason. This /// gets used to determine how to highlight the link in grid views. pub fn is_broken(&self) -> bool { match *self { - FileTarget::Ok(_) => false, - FileTarget::Broken(_) | FileTarget::Err(_) => true, + FileTarget::Ok(_) => false, + FileTarget::Broken(_) | FileTarget::Err(_) => true, } } } - /// More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] +#[cfg(unix)] mod modes { use libc; @@ -480,24 +521,23 @@ mod modes { // The `libc::mode_t` type’s actual type varies, but the value returned // from `metadata.permissions().mode()` is always `u32`. - pub const USER_READ: Mode = libc::S_IRUSR as Mode; - pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; - pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; + pub const USER_READ: Mode = libc::S_IRUSR as Mode; + pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; + pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; - pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; - pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; + pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; + pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; - pub const OTHER_READ: Mode = libc::S_IROTH as Mode; - pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; + pub const OTHER_READ: Mode = libc::S_IROTH as Mode; + pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; - pub const STICKY: Mode = libc::S_ISVTX as Mode; - pub const SETGID: Mode = libc::S_ISGID as Mode; - pub const SETUID: Mode = libc::S_ISUID as Mode; + pub const STICKY: Mode = libc::S_ISVTX as Mode; + pub const SETGID: Mode = libc::S_ISGID as Mode; + pub const SETUID: Mode = libc::S_ISUID as Mode; } - #[cfg(test)] mod ext_test { use super::File; @@ -519,7 +559,6 @@ mod ext_test { } } - #[cfg(test)] mod filename_test { use super::File; @@ -552,6 +591,9 @@ mod filename_test { #[test] fn topmost() { - assert_eq!("/", File::filename(Path::new("/"))) + #[cfg(unix)] + assert_eq!("/", File::filename(Path::new("/"))); + #[cfg(windows)] + assert_eq!("C:\\", File::filename(Path::new("C:\\"))); } } diff --git a/src/fs/filter.rs b/src/fs/filter.rs index 44944c33..894ccbe5 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -2,15 +2,15 @@ use std::cmp::Ordering; use std::iter::FromIterator; +#[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::path::Path; use glob; use natord; -use crate::fs::File; use crate::fs::DotFilter; - +use crate::fs::File; /// The **file filter** processes a list of files before displaying them to /// the user, by removing files they don’t want to see, and putting the list @@ -28,7 +28,6 @@ use crate::fs::DotFilter; /// performing the comparison. #[derive(PartialEq, Debug, Clone)] pub struct FileFilter { - /// Whether directories should be listed first, and other types of file /// second. Some users prefer it like this. pub list_dirs_first: bool, @@ -91,7 +90,6 @@ pub struct FileFilter { pub git_ignore: GitIgnore, } - impl FileFilter { /// Remove every file in the given vector that does *not* pass the /// filter predicate for files found inside a directory. @@ -118,8 +116,9 @@ impl FileFilter { /// Sort the files in the given vector based on the sort field option. pub fn sort_files<'a, F>(&self, files: &mut Vec) - where F: AsRef> { - + where + F: AsRef>, + { files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref())); if self.reverse { @@ -129,18 +128,18 @@ impl FileFilter { if self.list_dirs_first { // This relies on the fact that `sort_by` is *stable*: it will keep // adjacent elements next to each other. - files.sort_by(|a, b| {b.as_ref().points_to_directory() - .cmp(&a.as_ref().points_to_directory()) + files.sort_by(|a, b| { + b.as_ref() + .points_to_directory() + .cmp(&a.as_ref().points_to_directory()) }); } } } - /// User-supplied field to sort by. #[derive(PartialEq, Debug, Copy, Clone)] pub enum SortField { - /// Don’t apply any sorting. This is usually used as an optimisation in /// scripts, where the order doesn’t matter. Unsorted, @@ -156,6 +155,7 @@ pub enum SortField { /// The file’s inode, which usually corresponds to the order in which /// files were created on the filesystem, more or less. + #[cfg(unix)] FileInode, /// The time the file was modified (the “mtime”). @@ -182,6 +182,7 @@ pub enum SortField { /// /// In original Unix, this was, however, meant as creation time. /// https://www.bell-labs.com/usr/dmr/www/cacm.html + #[cfg(unix)] ChangedDate, /// The time the file was created (the "btime" or "birthtime"). @@ -220,7 +221,6 @@ pub enum SortField { /// effects they have. #[derive(PartialEq, Debug, Copy, Clone)] pub enum SortCase { - /// Sort files case-sensitively with uppercase first, with ‘A’ coming /// before ‘a’. ABCabc, @@ -230,7 +230,6 @@ pub enum SortCase { } impl SortField { - /// Compares two files to determine the order they should be listed in, /// depending on the search field. /// @@ -243,42 +242,44 @@ impl SortField { use self::SortCase::{ABCabc, AaBbCc}; match self { - SortField::Unsorted => Ordering::Equal, - - SortField::Name(ABCabc) => natord::compare(&a.name, &b.name), - SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), - - SortField::Size => a.metadata.len().cmp(&b.metadata.len()), - SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), - SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()), - SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), - SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()), - SortField::CreatedDate => a.created_time().cmp(&b.created_time()), - SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a - - SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes - Ordering::Equal => natord::compare(&*a.name, &*b.name), - order => order, + SortField::Unsorted => Ordering::Equal, + + SortField::Name(ABCabc) => natord::compare(&a.name, &b.name), + SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), + + SortField::Size => a.metadata.len().cmp(&b.metadata.len()), + #[cfg(unix)] + SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), + SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()), + SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), + #[cfg(unix)] + SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()), + SortField::CreatedDate => a.created_time().cmp(&b.created_time()), + SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a + + SortField::FileType => match a.type_char().cmp(&b.type_char()) { + // todo: this recomputes + Ordering::Equal => natord::compare(&*a.name, &*b.name), + order => order, }, SortField::Extension(ABCabc) => match a.ext.cmp(&b.ext) { - Ordering::Equal => natord::compare(&*a.name, &*b.name), - order => order, + Ordering::Equal => natord::compare(&*a.name, &*b.name), + order => order, }, SortField::Extension(AaBbCc) => match a.ext.cmp(&b.ext) { - Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name), - order => order, + Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name), + order => order, }, - SortField::NameMixHidden(ABCabc) => natord::compare( - SortField::strip_dot(&a.name), - SortField::strip_dot(&b.name) - ), + SortField::NameMixHidden(ABCabc) => { + natord::compare(SortField::strip_dot(&a.name), SortField::strip_dot(&b.name)) + } SortField::NameMixHidden(AaBbCc) => natord::compare_ignore_case( SortField::strip_dot(&a.name), - SortField::strip_dot(&b.name) - ) + SortField::strip_dot(&b.name), + ), } } @@ -291,7 +292,6 @@ impl SortField { } } - /// The **ignore patterns** are a list of globs that are tested against /// each filename, and if any of them match, that file isn’t displayed. /// This lets a user hide, say, text files by ignoring `*.txt`. @@ -302,23 +302,26 @@ pub struct IgnorePatterns { impl FromIterator for IgnorePatterns { fn from_iter>(iter: I) -> Self { - IgnorePatterns { patterns: iter.into_iter().collect() } + IgnorePatterns { + patterns: iter.into_iter().collect(), + } } } impl IgnorePatterns { - /// Create a new list from the input glob strings, turning the inputs that /// are valid glob patterns into an IgnorePatterns. The inputs that don’t /// parse correctly are returned separately. - pub fn parse_from_iter<'a, I: IntoIterator>(iter: I) -> (Self, Vec) { + pub fn parse_from_iter<'a, I: IntoIterator>( + iter: I, + ) -> (Self, Vec) { let iter = iter.into_iter(); // Almost all glob patterns are valid, so it’s worth pre-allocating // the vector with enough space for all of them. let mut patterns = match iter.size_hint() { - (_, Some(count)) => Vec::with_capacity(count), - _ => Vec::new(), + (_, Some(count)) => Vec::with_capacity(count), + _ => Vec::new(), }; // Similarly, assume there won’t be any errors. @@ -327,7 +330,7 @@ impl IgnorePatterns { for input in iter { match glob::Pattern::new(input) { Ok(pat) => patterns.push(pat), - Err(e) => errors.push(e), + Err(e) => errors.push(e), } } @@ -336,7 +339,9 @@ impl IgnorePatterns { /// Create a new empty set of patterns that matches nothing. pub fn empty() -> IgnorePatterns { - IgnorePatterns { patterns: Vec::new() } + IgnorePatterns { + patterns: Vec::new(), + } } /// Test whether the given file should be hidden from the results. @@ -353,11 +358,9 @@ impl IgnorePatterns { // isn’t probably means it’s in the wrong place } - /// Whether to ignore or display files that are mentioned in `.gitignore` files. #[derive(PartialEq, Debug, Copy, Clone)] pub enum GitIgnore { - /// Ignore files that Git would ignore. This means doing a check for a /// `.gitignore` file, possibly recursively up the filesystem tree. CheckAndIgnore, @@ -373,8 +376,6 @@ pub enum GitIgnore { // > .gitignore, .git/info/exclude and even your global gitignore globs, // > usually found in $XDG_CONFIG_HOME/git/ignore. - - #[cfg(test)] mod test_ignores { use super::*; @@ -388,23 +389,23 @@ mod test_ignores { #[test] fn ignores_a_glob() { - let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]); + let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["*.mp3"]); assert!(fails.is_empty()); assert_eq!(false, pats.is_ignored("nothing")); - assert_eq!(true, pats.is_ignored("test.mp3")); + assert_eq!(true, pats.is_ignored("test.mp3")); } #[test] fn ignores_an_exact_filename() { - let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]); + let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing"]); assert!(fails.is_empty()); - assert_eq!(true, pats.is_ignored("nothing")); + assert_eq!(true, pats.is_ignored("nothing")); assert_eq!(false, pats.is_ignored("test.mp3")); } #[test] fn ignores_both() { - let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]); + let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing", "*.mp3"]); assert!(fails.is_empty()); assert_eq!(true, pats.is_ignored("nothing")); assert_eq!(true, pats.is_ignored("test.mp3")); diff --git a/src/options/filter.rs b/src/options/filter.rs index 2ab644a1..0855ccfb 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -58,9 +58,12 @@ impl SortField { // newest files) get sorted at the top, and files with the most // age (the oldest) at the bottom. "age" | "old" | "oldest" => SortField::ModifiedAge, + #[cfg(unix)] "ch" | "changed" => SortField::ChangedDate, "acc" | "accessed" => SortField::AccessedDate, + #[cfg(unix)] "cr" | "created" => SortField::CreatedDate, + #[cfg(unix)] "inode" => SortField::FileInode, "type" => SortField::FileType, "none" => SortField::Unsorted, diff --git a/src/options/parser.rs b/src/options/parser.rs index f6a219a5..00608ba9 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -27,12 +27,13 @@ //! command-line options, as all the options and their values (such as //! `--sort size`) are guaranteed to just be 8-bit ASCII. - +use std::borrow::Cow; use std::ffi::{OsStr, OsString}; use std::fmt; -use crate::options::Misfire; +use os_str_bytes::{OsStrBytes, OsStringBytes}; +use crate::options::Misfire; /// A **short argument** is a single ASCII character. pub type ShortArg = u8; @@ -61,8 +62,8 @@ pub enum Flag { impl Flag { pub fn matches(&self, arg: &Arg) -> bool { match *self { - Flag::Short(short) => arg.short == Some(short), - Flag::Long(long) => arg.long == long, + Flag::Short(short) => arg.short == Some(short), + Flag::Long(long) => arg.long == long, } } } @@ -71,7 +72,7 @@ impl fmt::Display for Flag { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { Flag::Short(short) => write!(f, "-{}", short as char), - Flag::Long(long) => write!(f, "--{}", long), + Flag::Long(long) => write!(f, "--{}", long), } } } @@ -79,7 +80,6 @@ impl fmt::Display for Flag { /// Whether redundant arguments should be considered a problem. #[derive(PartialEq, Debug, Copy, Clone)] pub enum Strictness { - /// Throw an error when an argument doesn’t do anything, either because /// it requires another argument to be specified, or because two conflict. ComplainAboutRedundantArguments, @@ -93,7 +93,6 @@ pub enum Strictness { /// arguments. #[derive(Copy, Clone, PartialEq, Debug)] pub enum TakesValue { - /// This flag has to be followed by a value. /// If there’s a fixed set of possible values, they can be printed out /// with the error text. @@ -106,11 +105,9 @@ pub enum TakesValue { Optional(Option), } - /// An **argument** can be matched by one of the user’s input strings. #[derive(PartialEq, Debug)] pub struct Arg { - /// The short argument that matches it, if any. pub short: Option, @@ -134,18 +131,21 @@ impl fmt::Display for Arg { } } - /// Literally just several args. #[derive(PartialEq, Debug)] pub struct Args(pub &'static [&'static Arg]); impl Args { - /// Iterates over the given list of command-line arguments and parses /// them into a list of matched flags and free strings. - pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result, ParseError> - where I: IntoIterator { - use std::os::unix::ffi::OsStrExt; + pub fn parse<'args, I>( + &self, + inputs: I, + strictness: Strictness, + ) -> Result, ParseError> + where + I: IntoIterator, + { use self::TakesValue::*; let mut parsing = true; @@ -159,7 +159,7 @@ impl Args { // doesn’t have one in its string so it needs the next one. let mut inputs = inputs.into_iter(); while let Some(arg) = inputs.next() { - let bytes = arg.as_bytes(); + let bytes = arg.to_bytes(); // Stop parsing if one of the arguments is the literal string “--”. // This allows a file named “--arg” to be specified by passing in @@ -167,58 +167,52 @@ impl Args { // doesn’t exist. if !parsing { frees.push(arg) - } - else if arg == "--" { + } else if arg == "--" { parsing = false; } - // If the string starts with *two* dashes then it’s a long argument. else if bytes.starts_with(b"--") { - let long_arg_name = OsStr::from_bytes(&bytes[2..]); + let long_arg_name = OsStrBytes::from_bytes(&bytes[2..]).unwrap(); // If there’s an equals in it, then the string before the // equals will be the flag’s name, and the string after it // will be its value. - if let Some((before, after)) = split_on_equals(long_arg_name) { - let arg = self.lookup_long(before)?; + if let Some((before, after)) = split_on_equals(&long_arg_name) { + let arg = self.lookup_long(&*before)?; let flag = Flag::Long(arg.long); match arg.takes_value { - Necessary(_)|Optional(_) => result_flags.push((flag, Some(after))), - Forbidden => return Err(ParseError::ForbiddenValue { flag }) + Necessary(_) | Optional(_) => result_flags.push((flag, Some(after))), + Forbidden => return Err(ParseError::ForbiddenValue { flag }), } } - // If there’s no equals, then the entire string (apart from // the dashes) is the argument name. else { - let arg = self.lookup_long(long_arg_name)?; + let arg = self.lookup_long(&*long_arg_name)?; let flag = Flag::Long(arg.long); match arg.takes_value { - Forbidden => result_flags.push((flag, None)), + Forbidden => result_flags.push((flag, None)), Necessary(values) => { if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { - return Err(ParseError::NeedsValue { flag, values }) + result_flags.push((flag, Some(next_arg.clone()))); + } else { + return Err(ParseError::NeedsValue { flag, values }); } - }, + } Optional(_) => { if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { + result_flags.push((flag, Some(next_arg.clone()))); + } else { result_flags.push((flag, None)); } } } } } - // If the string starts with *one* dash then it’s one or more // short arguments. else if bytes.starts_with(b"-") && arg != "-" { - let short_arg = OsStr::from_bytes(&bytes[1..]); + let short_arg = OsStr::from_bytes(&bytes[1..]).unwrap(); // If there’s an equals in it, then the argument immediately // before the equals was the one that has the value, with the @@ -232,16 +226,19 @@ impl Args { // There’s no way to give two values in a cluster like this: // it’s an error if any of the first set of arguments actually // takes a value. - if let Some((before, after)) = split_on_equals(short_arg) { - let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap(); + if let Some((before, after)) = split_on_equals(&short_arg) { + let bytes = before.to_bytes(); + let (arg_with_value, other_args) = bytes.split_last().unwrap(); // Process the characters immediately following the dash... for byte in other_args { let arg = self.lookup_short(*byte)?; let flag = Flag::Short(*byte); match arg.takes_value { - Forbidden|Optional(_) => result_flags.push((flag, None)), - Necessary(values) => return Err(ParseError::NeedsValue { flag, values }) + Forbidden | Optional(_) => result_flags.push((flag, None)), + Necessary(values) => { + return Err(ParseError::NeedsValue { flag, values }) + } } } @@ -249,11 +246,10 @@ impl Args { let arg = self.lookup_short(*arg_with_value)?; let flag = Flag::Short(arg.short.unwrap()); match arg.takes_value { - Necessary(_)|Optional(_) => result_flags.push((flag, Some(after))), - Forbidden => return Err(ParseError::ForbiddenValue { flag }) + Necessary(_) | Optional(_) => result_flags.push((flag, Some(after))), + Forbidden => return Err(ParseError::ForbiddenValue { flag }), } } - // If there’s no equals, then every character is parsed as // its own short argument. However, if any of the arguments // takes a value, then the *rest* of the string is used as @@ -271,65 +267,70 @@ impl Args { let arg = self.lookup_short(*byte)?; let flag = Flag::Short(*byte); match arg.takes_value { - Forbidden => result_flags.push((flag, None)), - Necessary(values)|Optional(values) => { + Forbidden => result_flags.push((flag, None)), + Necessary(values) | Optional(values) => { if index < bytes.len() - 1 { - let remnants = &bytes[index+1 ..]; - result_flags.push((flag, Some(OsStr::from_bytes(remnants)))); + let remnants = &bytes[index + 1..]; + result_flags.push(( + flag, + Some(OsStringBytes::from_bytes(remnants).unwrap()), + )); break; - } - else if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { + } else if let Some(next_arg) = inputs.next() { + result_flags.push((flag, Some(next_arg.clone()))); + } else { match arg.takes_value { Forbidden => assert!(false), Necessary(_) => { return Err(ParseError::NeedsValue { flag, values }); - }, + } Optional(_) => { result_flags.push((flag, None)); } } } - } } } } } - // Otherwise, it’s a free string, usually a file name. else { frees.push(arg) } } - Ok(Matches { frees, flags: MatchedFlags { flags: result_flags, strictness } }) + Ok(Matches { + frees, + flags: MatchedFlags { + flags: result_flags, + strictness, + }, + }) } fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> { match self.0.into_iter().find(|arg| arg.short == Some(short)) { - Some(arg) => Ok(arg), - None => Err(ParseError::UnknownShortArgument { attempt: short }) + Some(arg) => Ok(arg), + None => Err(ParseError::UnknownShortArgument { attempt: short }), } } fn lookup_long<'b>(&self, long: &'b OsStr) -> Result<&Arg, ParseError> { match self.0.into_iter().find(|arg| arg.long == long) { - Some(arg) => Ok(arg), - None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() }) + Some(arg) => Ok(arg), + None => Err(ParseError::UnknownArgument { + attempt: long.to_os_string(), + }), } } } - /// The **matches** are the result of parsing the user’s command-line strings. #[derive(PartialEq, Debug)] pub struct Matches<'args> { - /// The flags that were parsed from the user’s input. - pub flags: MatchedFlags<'args>, + pub flags: MatchedFlags, /// All the strings that weren’t matched as arguments, as well as anything /// after the special "--" string. @@ -337,27 +338,26 @@ pub struct Matches<'args> { } #[derive(PartialEq, Debug)] -pub struct MatchedFlags<'args> { - +pub struct MatchedFlags { /// The individual flags from the user’s input, in the order they were /// originally given. /// /// Long and short arguments need to be kept in the same vector because /// we usually want the one nearest the end to count, and to know this, /// we need to know where they are in relation to one another. - flags: Vec<(Flag, Option<&'args OsStr>)>, + flags: Vec<(Flag, Option)>, /// Whether to check for duplicate or redundant arguments. strictness: Strictness, } -impl<'a> MatchedFlags<'a> { - +impl MatchedFlags { /// Whether the given argument was specified. /// Returns `true` if it was, `false` if it wasn’t, and an error in /// strict mode if it was specified more than once. pub fn has(&self, arg: &'static Arg) -> Result { - self.has_where(|flag| flag.matches(arg)).map(|flag| flag.is_some()) + self.has_where(|flag| flag.matches(arg)) + .map(|flag| flag.is_some()) } /// Returns the first found argument that satisfies the predicate, or @@ -366,19 +366,28 @@ impl<'a> MatchedFlags<'a> { /// /// You’ll have to test the resulting flag to see which argument it was. pub fn has_where

(&self, predicate: P) -> Result, Misfire> - where P: Fn(&Flag) -> bool { + where + P: Fn(&Flag) -> bool, + { if self.is_strict() { - let all = self.flags.iter() - .filter(|tuple| tuple.1.is_none() && predicate(&tuple.0)) - .collect::>(); - - if all.len() < 2 { Ok(all.first().map(|t| &t.0)) } - else { Err(Misfire::Duplicate(all[0].0.clone(), all[1].0.clone())) } - } - else { - let any = self.flags.iter().rev() - .find(|tuple| tuple.1.is_none() && predicate(&tuple.0)) - .map(|tuple| &tuple.0); + let all = self + .flags + .iter() + .filter(|tuple| tuple.1.is_none() && predicate(&tuple.0)) + .collect::>(); + + if all.len() < 2 { + Ok(all.first().map(|t| &t.0)) + } else { + Err(Misfire::Duplicate(all[0].0.clone(), all[1].0.clone())) + } + } else { + let any = self + .flags + .iter() + .rev() + .find(|tuple| tuple.1.is_none() && predicate(&tuple.0)) + .map(|tuple| &tuple.0); Ok(any) } } @@ -390,7 +399,7 @@ impl<'a> MatchedFlags<'a> { /// Returns the value of the given argument if it was specified, nothing /// if it wasn’t, and an error in strict mode if it was specified more /// than once. - pub fn get(&self, arg: &'static Arg) -> Result, Misfire> { + pub fn get(&self, arg: &'static Arg) -> Result, Misfire> { self.get_where(|flag| flag.matches(arg)) } @@ -399,20 +408,29 @@ impl<'a> MatchedFlags<'a> { /// multiple arguments matched the predicate. /// /// It’s not possible to tell which flag the value belonged to from this. - pub fn get_where

(&self, predicate: P) -> Result, Misfire> - where P: Fn(&Flag) -> bool { + pub fn get_where

(&self, predicate: P) -> Result, Misfire> + where + P: Fn(&Flag) -> bool, + { if self.is_strict() { - let those = self.flags.iter() - .filter(|tuple| tuple.1.is_some() && predicate(&tuple.0)) - .collect::>(); - - if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) } - else { Err(Misfire::Duplicate(those[0].0.clone(), those[1].0.clone())) } - } - else { - let found = self.flags.iter().rev() - .find(|tuple| tuple.1.is_some() && predicate(&tuple.0)) - .map(|tuple| tuple.1.unwrap()); + let those = self + .flags + .iter() + .filter(|tuple| tuple.1.is_some() && predicate(&tuple.0)) + .collect::>(); + + if those.len() < 2 { + Ok(those.first().cloned().map(|t| t.clone().1.unwrap())) + } else { + Err(Misfire::Duplicate(those[0].0.clone(), those[1].0.clone())) + } + } else { + let found = self + .flags + .iter() + .rev() + .find(|tuple| tuple.1.is_some() && predicate(&tuple.0)) + .map(|tuple| tuple.clone().1.unwrap()); Ok(found) } } @@ -423,7 +441,8 @@ impl<'a> MatchedFlags<'a> { /// Counts the number of occurrences of the given argument, even in /// strict mode. pub fn count(&self, arg: &Arg) -> usize { - self.flags.iter() + self.flags + .iter() .filter(|tuple| tuple.0.matches(arg)) .count() } @@ -435,12 +454,10 @@ impl<'a> MatchedFlags<'a> { } } - /// A problem with the user’s input that meant it couldn’t be parsed into a /// coherent list of arguments. #[derive(PartialEq, Debug)] pub enum ParseError { - /// A flag that has to take a value was not given one. NeedsValue { flag: Flag, values: Option }, @@ -462,26 +479,25 @@ pub enum ParseError { // which would give Misfire a lifetime, which gets used everywhere. And this // only happens when an error occurs, so it’s not really worth it. - /// Splits a string on its `=` character, returning the two substrings on /// either side. Returns `None` if there’s no equals or a string is missing. -fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> { - use std::os::unix::ffi::OsStrExt; - - if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') { - let (before, after) = input.as_bytes().split_at(index); +fn split_on_equals<'a>(input: &'a Cow) -> Option<(OsString, OsString)> { + if let Some(index) = input.to_bytes().iter().position(|elem| *elem == b'=') { + let bytes = input.to_bytes(); + let (before, after) = bytes.split_at(index); // The after string contains the = that we need to remove. if !before.is_empty() && after.len() >= 2 { - return Some((OsStr::from_bytes(before), - OsStr::from_bytes(&after[1..]))) + return Some(( + OsStringBytes::from_bytes(before).unwrap(), + OsStringBytes::from_bytes(&after[1..]).unwrap(), + )); } } None } - /// Creates an `OSString` (used in tests) #[cfg(test)] fn os(input: &'static str) -> OsString { @@ -490,25 +506,27 @@ fn os(input: &'static str) -> OsString { os } - #[cfg(test)] mod split_test { - use super::{split_on_equals, os}; + use std::borrow::Cow; + + use super::{os, split_on_equals}; macro_rules! test_split { ($name:ident: $input:expr => None) => { #[test] fn $name() { - assert_eq!(split_on_equals(&os($input)), - None); + assert_eq!(split_on_equals(&Cow::from(os($input))), None); } }; ($name:ident: $input:expr => $before:expr, $after:expr) => { #[test] fn $name() { - assert_eq!(split_on_equals(&os($input)), - Some((&*os($before), &*os($after)))); + assert_eq!( + split_on_equals(&Cow::from(os($input))), + Some((os($before), os($after))) + ); } }; } @@ -525,7 +543,6 @@ mod split_test { test_split!(more: "this=that=other" => "this", "that=other"); } - #[cfg(test)] mod parse_test { use super::*; @@ -536,12 +553,10 @@ mod parse_test { os } - macro_rules! test { ($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => { #[test] fn $name() { - // Annoyingly the input &strs need to be converted to OsStrings let inputs: Vec = $inputs.as_ref().into_iter().map(|&o| os(o)).collect(); @@ -551,9 +566,12 @@ mod parse_test { let flags = <[_]>::into_vec(Box::new($flags)); - let strictness = Strictness::UseLastArguments; // this isn’t even used + let strictness = Strictness::UseLastArguments; // this isn’t even used let got = Args(TEST_ARGS).parse(inputs.iter(), strictness); - let expected = Ok(Matches { frees, flags: MatchedFlags { flags, strictness } }); + let expected = Ok(Matches { + frees, + flags: MatchedFlags { flags, strictness }, + }); assert_eq!(got, expected); } }; @@ -563,8 +581,12 @@ mod parse_test { fn $name() { use self::ParseError::*; - let strictness = Strictness::UseLastArguments; // this isn’t even used - let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::>(); + let strictness = Strictness::UseLastArguments; // this isn’t even used + let bits = $inputs + .as_ref() + .into_iter() + .map(|&o| os(o)) + .collect::>(); let got = Args(TEST_ARGS).parse(bits.iter(), strictness); assert_eq!(got, Err($error)); @@ -572,16 +594,31 @@ mod parse_test { }; } - const SUGGESTIONS: Values = &[ "example" ]; + const SUGGESTIONS: Values = &["example"]; static TEST_ARGS: &[&Arg] = &[ - &Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }, - &Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }, - &Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }, - &Arg { short: Some(b't'), long: "type", takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) } + &Arg { + short: Some(b'l'), + long: "long", + takes_value: TakesValue::Forbidden, + }, + &Arg { + short: Some(b'v'), + long: "verbose", + takes_value: TakesValue::Forbidden, + }, + &Arg { + short: Some(b'c'), + long: "count", + takes_value: TakesValue::Necessary(None), + }, + &Arg { + short: Some(b't'), + long: "type", + takes_value: TakesValue::Necessary(Some(SUGGESTIONS)), + }, ]; - // Just filenames test!(empty: [] => frees: [], flags: []); test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []); @@ -593,7 +630,6 @@ mod parse_test { test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []); test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []); - // Long args test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]); test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]); @@ -602,14 +638,13 @@ mod parse_test { // Long args with values test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") }); test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count"), values: None }); - test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]); - test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]); + test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsString::from("4"))) ]); + test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsString::from("4"))) ]); // Long args with values and suggestions test!(no_arg_s: ["--type"] => error NeedsValue { flag: Flag::Long("type"), values: Some(SUGGESTIONS) }); - test!(arg_equals_s: ["--type=exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]); - test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]); - + test!(arg_equals_s: ["--type=exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsString::from("exa"))) ]); + test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsString::from("exa"))) ]); // Short args test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]); @@ -620,18 +655,17 @@ mod parse_test { // Short args with values test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') }); test!(short_none: ["-c"] => error NeedsValue { flag: Flag::Short(b'c'), values: None }); - test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]); - test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]); - test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); - test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); - test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); + test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsString::from("4"))) ]); + test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsString::from("4"))) ]); + test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsString::from("two"))) ]); + test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsString::from("two"))) ]); + test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsString::from("two"))) ]); // Short args with values and suggestions test!(short_none_s: ["-t"] => error NeedsValue { flag: Flag::Short(b't'), values: Some(SUGGESTIONS) }); - test!(short_two_together_s: ["-texa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - test!(short_two_equals_s: ["-t=exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - + test!(short_two_together_s: ["-texa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsString::from("exa"))) ]); + test!(short_two_equals_s: ["-t=exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsString::from("exa"))) ]); + test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsString::from("exa"))) ]); // Unknown args test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: os("quiet") }); @@ -642,7 +676,6 @@ mod parse_test { test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' }); } - #[cfg(test)] mod matches_test { use super::*; @@ -661,9 +694,16 @@ mod matches_test { }; } - static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }; - static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }; - + static VERBOSE: Arg = Arg { + short: Some(b'v'), + long: "verbose", + takes_value: TakesValue::Forbidden, + }; + static COUNT: Arg = Arg { + short: Some(b'c'), + long: "count", + takes_value: TakesValue::Necessary(None), + }; test!(short_never: [], has VERBOSE => false); test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true); @@ -672,36 +712,40 @@ mod matches_test { test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true); test!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true); - #[test] fn only_count() { let everything = os("everything"); let flags = MatchedFlags { - flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ], + flags: vec![(Flag::Short(b'c'), Some(everything.clone()))], strictness: Strictness::UseLastArguments, }; - assert_eq!(flags.get(&COUNT), Ok(Some(&*everything))); + assert_eq!(flags.get(&COUNT), Ok(Some(everything))); } #[test] fn rightmost_count() { let everything = os("everything"); - let nothing = os("nothing"); + let nothing = os("nothing"); let flags = MatchedFlags { - flags: vec![ (Flag::Short(b'c'), Some(&*everything)), - (Flag::Short(b'c'), Some(&*nothing)) ], + flags: vec![ + (Flag::Short(b'c'), Some(everything)), + (Flag::Short(b'c'), Some(nothing.clone())), + ], strictness: Strictness::UseLastArguments, }; - assert_eq!(flags.get(&COUNT), Ok(Some(&*nothing))); + assert_eq!(flags.get(&COUNT), Ok(Some(nothing))); } #[test] fn no_count() { - let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments }; + let flags = MatchedFlags { + flags: Vec::new(), + strictness: Strictness::UseLastArguments, + }; assert!(!flags.has(&COUNT).unwrap()); } diff --git a/src/options/style.rs b/src/options/style.rs index c62ae63e..028b23b0 100644 --- a/src/options/style.rs +++ b/src/options/style.rs @@ -45,13 +45,13 @@ impl TerminalColours { None => return Ok(TerminalColours::default()), }; - if word == "always" { + if &*word == "always" { Ok(TerminalColours::Always) } - else if word == "auto" || word == "automatic" { + else if &*word == "auto" || &*word == "automatic" { Ok(TerminalColours::Automatic) } - else if word == "never" { + else if &*word == "never" { Ok(TerminalColours::Never) } else { @@ -124,7 +124,7 @@ impl Styles { /// type mappings or not. The `reset` code needs to be the first one. fn parse_color_vars(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) { use log::warn; - + use crate::options::vars; use crate::style::LSColors; diff --git a/src/options/view.rs b/src/options/view.rs index eaebecf7..cb17f949 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -332,16 +332,16 @@ impl TimeTypes { else if created { return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME)); } - else if word == "mod" || word == "modified" { + else if &*word == "mod" || &*word == "modified" { TimeTypes { modified: true, changed: false, accessed: false, created: false } } - else if word == "ch" || word == "changed" { + else if &*word == "ch" || &*word == "changed" { TimeTypes { modified: false, changed: true, accessed: false, created: false } } - else if word == "acc" || word == "accessed" { + else if &*word == "acc" || &*word == "accessed" { TimeTypes { modified: false, changed: false, accessed: true, created: false } } - else if word == "cr" || word == "created" { + else if &*word == "cr" || &*word == "created" { TimeTypes { modified: false, changed: false, accessed: false, created: true } } else { diff --git a/src/output/file_name.rs b/src/output/file_name.rs index e21b2990..69c01168 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -198,6 +198,17 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { /// The character to be displayed after a file when classifying is on, if /// the file’s type has one associated with it. + #[cfg(windows)] + fn classify_char(&self) -> Option<&'static str> { + if self.file.is_directory() { + Some("/") + } else if self.file.is_link() { + Some("@") + } else { + None + } + } + #[cfg(unix)] fn classify_char(&self) -> Option<&'static str> { if self.file.is_executable_file() { Some("*") @@ -254,11 +265,16 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { fn kind_style(&self) -> Option