This commit is contained in:
Chester Liu 2021-03-26 16:37:17 +08:00
commit 6a642d0f32
12 changed files with 295 additions and 200 deletions

7
Cargo.lock generated
View File

@ -72,6 +72,7 @@ dependencies = [
"natord", "natord",
"num_cpus", "num_cpus",
"number_prefix", "number_prefix",
"os_str_bytes",
"scoped_threadpool", "scoped_threadpool",
"term_grid", "term_grid",
"term_size", "term_size",
@ -268,6 +269,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "os_str_bytes"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e293568965aea261bdf010db17df7030e3c9a275c415d51d6112f7cf9b7af012"
[[package]] [[package]]
name = "pad" name = "pad"
version = "0.1.6" version = "0.1.6"

View File

@ -36,9 +36,14 @@ scoped_threadpool = "0.1"
term_grid = "0.1" term_grid = "0.1"
term_size = "0.3" term_size = "0.3"
unicode-width = "0.1" unicode-width = "0.1"
users = "0.11"
zoneinfo_compiled = "0.5" zoneinfo_compiled = "0.5"
[target.'cfg(unix)'.dependencies]
users = "0.11"
[target.'cfg(windows)'.dependencies]
os_str_bytes = "3.0"
[dependencies.git2] [dependencies.git2]
version = "0.13" version = "0.13"
optional = true optional = true

View File

@ -88,6 +88,7 @@ pub struct Permissions {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct PermissionsPlus { pub struct PermissionsPlus {
pub file_type: Type, pub file_type: Type,
#[cfg(unix)]
pub permissions: Permissions, pub permissions: Permissions,
pub xattrs: bool, pub xattrs: bool,
} }

View File

@ -1,6 +1,7 @@
//! Files, and methods and fields to access their metadata. //! Files, and methods and fields to access their metadata.
use std::io; use std::io;
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
@ -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 /// information queried at least once, so it makes sense to do all this at the
/// start and hold on to all the information. /// start and hold on to all the information.
pub struct File<'dir> { pub struct File<'dir> {
/// The filename portion of this files path, including the extension. /// The filename portion of this files path, including the extension.
/// ///
/// This is used to compare against certain filenames (such as checking if /// This is used to compare against certain filenames (such as checking if
@ -71,14 +71,21 @@ impl<'dir> File<'dir> {
FN: Into<Option<String>> FN: Into<Option<String>>
{ {
let parent_dir = parent_dir.into(); let parent_dir = parent_dir.into();
let name = filename.into().unwrap_or_else(|| File::filename(&path)); let name = filename.into().unwrap_or_else(|| File::filename(&path));
let ext = File::ext(&path); let ext = File::ext(&path);
debug!("Statting file {:?}", &path); debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?; let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = false; 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) -> io::Result<File<'dir>> { pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@ -86,7 +93,7 @@ impl<'dir> File<'dir> {
let ext = File::ext(&path); let ext = File::ext(&path);
debug!("Statting file {:?}", &path); debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?; let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true; let is_all_all = true;
let parent_dir = Some(parent_dir); let parent_dir = Some(parent_dir);
@ -97,7 +104,7 @@ impl<'dir> File<'dir> {
let ext = File::ext(&path); let ext = File::ext(&path);
debug!("Statting file {:?}", &path); debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?; let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true; let is_all_all = true;
let parent_dir = Some(parent_dir); let parent_dir = Some(parent_dir);
@ -109,9 +116,12 @@ impl<'dir> File<'dir> {
/// use the last component as the name. /// use the last component as the name.
pub fn filename(path: &Path) -> String { pub fn filename(path: &Path) -> String {
if let Some(back) = path.components().next_back() { if let Some(back) = path.components().next_back() {
back.as_os_str().to_string_lossy().to_string() let name = back.as_os_str().to_string_lossy().to_string();
} #[cfg(unix)]
else { return name;
#[cfg(windows)]
return name;
} else {
// use the path as fallback // use the path as fallback
error!("Path {:?} has no last component", path); error!("Path {:?} has no last component", path);
path.display().to_string() path.display().to_string()
@ -174,6 +184,7 @@ impl<'dir> File<'dir> {
/// Whether this file is both a regular file *and* executable for the /// Whether this file is both a regular file *and* executable for the
/// current user. An executable file has a different purpose from an /// current user. An executable file has a different purpose from an
/// executable directory, so they should be highlighted differently. /// executable directory, so they should be highlighted differently.
#[cfg(unix)]
pub fn is_executable_file(&self) -> bool { pub fn is_executable_file(&self) -> bool {
let bit = modes::USER_EXECUTE; let bit = modes::USER_EXECUTE;
self.is_file() && (self.metadata.permissions().mode() & bit) == bit self.is_file() && (self.metadata.permissions().mode() & bit) == bit
@ -185,40 +196,40 @@ impl<'dir> File<'dir> {
} }
/// Whether this file is a named pipe on the filesystem. /// Whether this file is a named pipe on the filesystem.
#[cfg(unix)]
pub fn is_pipe(&self) -> bool { pub fn is_pipe(&self) -> bool {
self.metadata.file_type().is_fifo() self.metadata.file_type().is_fifo()
} }
/// Whether this file is a char device on the filesystem. /// Whether this file is a char device on the filesystem.
#[cfg(unix)]
pub fn is_char_device(&self) -> bool { pub fn is_char_device(&self) -> bool {
self.metadata.file_type().is_char_device() self.metadata.file_type().is_char_device()
} }
/// Whether this file is a block device on the filesystem. /// Whether this file is a block device on the filesystem.
#[cfg(unix)]
pub fn is_block_device(&self) -> bool { pub fn is_block_device(&self) -> bool {
self.metadata.file_type().is_block_device() self.metadata.file_type().is_block_device()
} }
/// Whether this file is a socket on the filesystem. /// Whether this file is a socket on the filesystem.
#[cfg(unix)]
pub fn is_socket(&self) -> bool { pub fn is_socket(&self) -> bool {
self.metadata.file_type().is_socket() self.metadata.file_type().is_socket()
} }
/// Re-prefixes the path pointed to by this file, if its a symlink, to /// Re-prefixes the path pointed to by this file, if its a symlink, to
/// make it an absolute path that can be accessed from whichever /// make it an absolute path that can be accessed from whichever
/// directory exa is being run from. /// directory exa is being run from.
fn reorient_target_path(&self, path: &Path) -> PathBuf { fn reorient_target_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() { if path.is_absolute() {
path.to_path_buf() path.to_path_buf()
} } else if let Some(dir) = self.parent_dir {
else if let Some(dir) = self.parent_dir {
dir.join(&*path) dir.join(&*path)
} } else if let Some(parent) = self.path.parent() {
else if let Some(parent) = self.path.parent() {
parent.join(&*path) parent.join(&*path)
} } else {
else {
self.path.join(&*path) self.path.join(&*path)
} }
} }
@ -234,15 +245,14 @@ impl<'dir> File<'dir> {
/// existed. If this file cannot be read at all, returns the error that /// existed. If this file cannot be read at all, returns the error that
/// we got when we tried to read it. /// we got when we tried to read it.
pub fn link_target(&self) -> FileTarget<'dir> { pub fn link_target(&self) -> FileTarget<'dir> {
// We need to be careful to treat the path actually pointed to by // We need to be careful to treat the path actually pointed to by
// this file — which could be absolute or relative — to the path // this file — which could be absolute or relative — to the path
// we actually look up and turn into a `File` — which needs to be // we actually look up and turn into a `File` — which needs to be
// absolute to be accessible from any directory. // absolute to be accessible from any directory.
debug!("Reading link {:?}", &self.path); debug!("Reading link {:?}", &self.path);
let path = match std::fs::read_link(&self.path) { let path = match std::fs::read_link(&self.path) {
Ok(p) => p, Ok(p) => p,
Err(e) => return FileTarget::Err(e), Err(e) => return FileTarget::Err(e),
}; };
let absolute_path = self.reorient_target_path(&path); let absolute_path = self.reorient_target_path(&path);
@ -251,7 +261,7 @@ impl<'dir> File<'dir> {
// follow links. // follow links.
match std::fs::metadata(&absolute_path) { match std::fs::metadata(&absolute_path) {
Ok(metadata) => { Ok(metadata) => {
let ext = File::ext(&path); let ext = File::ext(&path);
let name = File::filename(&path); let name = File::filename(&path);
let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false }; let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false };
FileTarget::Ok(Box::new(file)) FileTarget::Ok(Box::new(file))
@ -270,6 +280,7 @@ impl<'dir> File<'dir> {
/// is uncommon, while you come across directories and other types /// is uncommon, while you come across directories and other types
/// with multiple links much more often. Thus, it should get highlighted /// with multiple links much more often. Thus, it should get highlighted
/// more attentively. /// more attentively.
#[cfg(unix)]
pub fn links(&self) -> f::Links { pub fn links(&self) -> f::Links {
let count = self.metadata.nlink(); let count = self.metadata.nlink();
@ -280,6 +291,7 @@ impl<'dir> File<'dir> {
} }
/// This files inode. /// This files inode.
#[cfg(unix)]
pub fn inode(&self) -> f::Inode { pub fn inode(&self) -> f::Inode {
f::Inode(self.metadata.ino()) f::Inode(self.metadata.ino())
} }
@ -287,21 +299,23 @@ impl<'dir> File<'dir> {
/// This files number of filesystem blocks. /// This files number of filesystem blocks.
/// ///
/// (Not the size of each block, which we dont actually report on) /// (Not the size of each block, which we dont actually report on)
#[cfg(unix)]
pub fn blocks(&self) -> f::Blocks { pub fn blocks(&self) -> f::Blocks {
if self.is_file() || self.is_link() { if self.is_file() || self.is_link() {
f::Blocks::Some(self.metadata.blocks()) f::Blocks::Some(self.metadata.blocks())
} } else {
else {
f::Blocks::None f::Blocks::None
} }
} }
/// The ID of the user that own this file. /// The ID of the user that own this file.
#[cfg(unix)]
pub fn user(&self) -> f::User { pub fn user(&self) -> f::User {
f::User(self.metadata.uid()) f::User(self.metadata.uid())
} }
/// The ID of the group that owns this file. /// The ID of the group that owns this file.
#[cfg(unix)]
pub fn group(&self) -> f::Group { pub fn group(&self) -> f::Group {
f::Group(self.metadata.gid()) f::Group(self.metadata.gid())
} }
@ -316,18 +330,19 @@ impl<'dir> File<'dir> {
/// usually just have a file size of zero. /// usually just have a file size of zero.
pub fn size(&self) -> f::Size { pub fn size(&self) -> f::Size {
if self.is_directory() { if self.is_directory() {
f::Size::None return f::Size::None;
} };
else if self.is_char_device() || self.is_block_device() { #[cfg(unix)]
let dev = self.metadata.rdev(); {
f::Size::DeviceIDs(f::DeviceIDs { if self.is_char_device() || self.is_block_device() {
major: (dev / 256) as u8, let dev = self.metadata.rdev();
minor: (dev % 256) as u8, return 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::Some(self.metadata.len());
} }
/// This files last modified timestamp, if available on this platform. /// This files last modified timestamp, if available on this platform.
@ -336,6 +351,7 @@ impl<'dir> File<'dir> {
} }
/// This files last changed timestamp, if available on this platform. /// This files last changed timestamp, if available on this platform.
#[cfg(unix)]
pub fn changed_time(&self) -> Option<SystemTime> { pub fn changed_time(&self) -> Option<SystemTime> {
let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec()); let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
@ -369,54 +385,59 @@ impl<'dir> File<'dir> {
/// This is used a the leftmost character of the permissions column. /// 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 /// The file type can usually be guessed from the colour of the file, but
/// ls puts this character there. /// ls puts this character there.
#[cfg(windows)]
pub fn type_char(&self) -> f::Type { pub fn type_char(&self) -> f::Type {
if self.is_file() { if self.is_file() {
f::Type::File f::Type::File
} } else if self.is_directory() {
else if self.is_directory() {
f::Type::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 f::Type::Pipe
} } else if self.is_link() {
else if self.is_link() {
f::Type::Link f::Type::Link
} } else if self.is_char_device() {
else if self.is_char_device() {
f::Type::CharDevice f::Type::CharDevice
} } else if self.is_block_device() {
else if self.is_block_device() {
f::Type::BlockDevice f::Type::BlockDevice
} } else if self.is_socket() {
else if self.is_socket() {
f::Type::Socket f::Type::Socket
} } else {
else {
f::Type::Special f::Type::Special
} }
} }
/// This files permissions, with flags for each bit. /// This files permissions, with flags for each bit.
#[cfg(unix)]
pub fn permissions(&self) -> f::Permissions { pub fn permissions(&self) -> f::Permissions {
let bits = self.metadata.mode(); let bits = self.metadata.mode();
let has_bit = |bit| bits & bit == bit; let has_bit = |bit| bits & bit == bit;
f::Permissions { f::Permissions {
user_read: has_bit(modes::USER_READ), user_read: has_bit(modes::USER_READ),
user_write: has_bit(modes::USER_WRITE), user_write: has_bit(modes::USER_WRITE),
user_execute: has_bit(modes::USER_EXECUTE), user_execute: has_bit(modes::USER_EXECUTE),
group_read: has_bit(modes::GROUP_READ), group_read: has_bit(modes::GROUP_READ),
group_write: has_bit(modes::GROUP_WRITE), group_write: has_bit(modes::GROUP_WRITE),
group_execute: has_bit(modes::GROUP_EXECUTE), group_execute: has_bit(modes::GROUP_EXECUTE),
other_read: has_bit(modes::OTHER_READ), other_read: has_bit(modes::OTHER_READ),
other_write: has_bit(modes::OTHER_WRITE), other_write: has_bit(modes::OTHER_WRITE),
other_execute: has_bit(modes::OTHER_EXECUTE), other_execute: has_bit(modes::OTHER_EXECUTE),
sticky: has_bit(modes::STICKY), sticky: has_bit(modes::STICKY),
setgid: has_bit(modes::SETGID), setgid: has_bit(modes::SETGID),
setuid: has_bit(modes::SETUID), setuid: has_bit(modes::SETUID),
} }
} }
@ -437,17 +458,14 @@ impl<'dir> File<'dir> {
} }
} }
impl<'a> AsRef<File<'a>> for File<'a> { impl<'a> AsRef<File<'a>> for File<'a> {
fn as_ref(&self) -> &File<'a> { fn as_ref(&self) -> &File<'a> {
self self
} }
} }
/// The result of following a symlink. /// The result of following a symlink.
pub enum FileTarget<'dir> { pub enum FileTarget<'dir> {
/// The symlink pointed at a file that exists. /// The symlink pointed at a file that exists.
Ok(Box<File<'dir>>), Ok(Box<File<'dir>>),
@ -466,7 +484,6 @@ pub enum FileTarget<'dir> {
} }
impl<'dir> FileTarget<'dir> { impl<'dir> FileTarget<'dir> {
/// Whether this link doesnt lead to a file, for whatever reason. This /// Whether this link doesnt lead to a file, for whatever reason. This
/// gets used to determine how to highlight the link in grid views. /// gets used to determine how to highlight the link in grid views.
pub fn is_broken(&self) -> bool { pub fn is_broken(&self) -> bool {
@ -474,33 +491,32 @@ impl<'dir> FileTarget<'dir> {
} }
} }
/// More readable aliases for the permission bits exposed by libc. /// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)] #[allow(trivial_numeric_casts)]
#[cfg(unix)]
mod modes { mod modes {
// The `libc::mode_t` types actual type varies, but the value returned // The `libc::mode_t` types actual type varies, but the value returned
// from `metadata.permissions().mode()` is always `u32`. // from `metadata.permissions().mode()` is always `u32`.
pub type Mode = u32; pub type Mode = u32;
pub const USER_READ: Mode = libc::S_IRUSR 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_WRITE: Mode = libc::S_IWUSR as Mode;
pub const USER_EXECUTE: Mode = libc::S_IXUSR 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_READ: Mode = libc::S_IRGRP as Mode;
pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode;
pub const GROUP_EXECUTE: Mode = libc::S_IXGRP 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_READ: Mode = libc::S_IROTH as Mode;
pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode;
pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
pub const STICKY: Mode = libc::S_ISVTX as Mode; pub const STICKY: Mode = libc::S_ISVTX as Mode;
pub const SETGID: Mode = libc::S_ISGID as Mode; pub const SETGID: Mode = libc::S_ISGID as Mode;
pub const SETUID: Mode = libc::S_ISUID as Mode; pub const SETUID: Mode = libc::S_ISUID as Mode;
} }
#[cfg(test)] #[cfg(test)]
mod ext_test { mod ext_test {
use super::File; use super::File;
@ -522,7 +538,6 @@ mod ext_test {
} }
} }
#[cfg(test)] #[cfg(test)]
mod filename_test { mod filename_test {
use super::File; use super::File;
@ -555,6 +570,9 @@ mod filename_test {
#[test] #[test]
fn topmost() { 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:\\")));
} }
} }

View File

@ -2,6 +2,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::iter::FromIterator; use std::iter::FromIterator;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::Path; use std::path::Path;
@ -25,7 +26,6 @@ use crate::fs::File;
/// performing the comparison. /// performing the comparison.
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub struct FileFilter { pub struct FileFilter {
/// Whether directories should be listed first, and other types of file /// Whether directories should be listed first, and other types of file
/// second. Some users prefer it like this. /// second. Some users prefer it like this.
pub list_dirs_first: bool, pub list_dirs_first: bool,
@ -137,11 +137,9 @@ impl FileFilter {
} }
} }
/// User-supplied field to sort by. /// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortField { pub enum SortField {
/// Dont apply any sorting. This is usually used as an optimisation in /// Dont apply any sorting. This is usually used as an optimisation in
/// scripts, where the order doesnt matter. /// scripts, where the order doesnt matter.
Unsorted, Unsorted,
@ -157,6 +155,7 @@ pub enum SortField {
/// The files inode, which usually corresponds to the order in which /// The files inode, which usually corresponds to the order in which
/// files were created on the filesystem, more or less. /// files were created on the filesystem, more or less.
#[cfg(unix)]
FileInode, FileInode,
/// The time the file was modified (the “mtime”). /// The time the file was modified (the “mtime”).
@ -183,6 +182,7 @@ pub enum SortField {
/// ///
/// In original Unix, this was, however, meant as creation time. /// In original Unix, this was, however, meant as creation time.
/// https://www.bell-labs.com/usr/dmr/www/cacm.html /// https://www.bell-labs.com/usr/dmr/www/cacm.html
#[cfg(unix)]
ChangedDate, ChangedDate,
/// The time the file was created (the “btime” or “birthtime”). /// The time the file was created (the “btime” or “birthtime”).
@ -221,7 +221,6 @@ pub enum SortField {
/// effects they have. /// effects they have.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortCase { pub enum SortCase {
/// Sort files case-sensitively with uppercase first, with A coming /// Sort files case-sensitively with uppercase first, with A coming
/// before a. /// before a.
ABCabc, ABCabc,
@ -231,7 +230,6 @@ pub enum SortCase {
} }
impl SortField { impl SortField {
/// Compares two files to determine the order they should be listed in, /// Compares two files to determine the order they should be listed in,
/// depending on the search field. /// depending on the search field.
/// ///
@ -250,9 +248,11 @@ impl SortField {
Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
Self::Size => a.metadata.len().cmp(&b.metadata.len()), Self::Size => a.metadata.len().cmp(&b.metadata.len()),
#[cfg(unix)]
Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()), Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
#[cfg(unix)]
Self::ChangedDate => a.changed_time().cmp(&b.changed_time()), Self::ChangedDate => a.changed_time().cmp(&b.changed_time()),
Self::CreatedDate => a.created_time().cmp(&b.created_time()), Self::CreatedDate => a.created_time().cmp(&b.created_time()),
Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a
@ -289,7 +289,6 @@ impl SortField {
} }
} }
/// The **ignore patterns** are a list of globs that are tested against /// The **ignore patterns** are a list of globs that are tested against
/// each filename, and if any of them match, that file isnt displayed. /// each filename, and if any of them match, that file isnt displayed.
/// This lets a user hide, say, text files by ignoring `*.txt`. /// This lets a user hide, say, text files by ignoring `*.txt`.
@ -309,7 +308,6 @@ impl FromIterator<glob::Pattern> for IgnorePatterns {
} }
impl IgnorePatterns { impl IgnorePatterns {
/// Create a new list from the input glob strings, turning the inputs that /// Create a new list from the input glob strings, turning the inputs that
/// are valid glob patterns into an `IgnorePatterns`. The inputs that /// are valid glob patterns into an `IgnorePatterns`. The inputs that
/// dont parse correctly are returned separately. /// dont parse correctly are returned separately.
@ -319,8 +317,8 @@ impl IgnorePatterns {
// Almost all glob patterns are valid, so its worth pre-allocating // Almost all glob patterns are valid, so its worth pre-allocating
// the vector with enough space for all of them. // the vector with enough space for all of them.
let mut patterns = match iter.size_hint() { let mut patterns = match iter.size_hint() {
(_, Some(count)) => Vec::with_capacity(count), (_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(), _ => Vec::new(),
}; };
// Similarly, assume there wont be any errors. // Similarly, assume there wont be any errors.
@ -329,7 +327,7 @@ impl IgnorePatterns {
for input in iter { for input in iter {
match glob::Pattern::new(input) { match glob::Pattern::new(input) {
Ok(pat) => patterns.push(pat), Ok(pat) => patterns.push(pat),
Err(e) => errors.push(e), Err(e) => errors.push(e),
} }
} }
@ -355,11 +353,9 @@ impl IgnorePatterns {
// isnt probably means its in the wrong place // isnt probably means its in the wrong place
} }
/// Whether to ignore or display files that are mentioned in `.gitignore` files. /// Whether to ignore or display files that are mentioned in `.gitignore` files.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum GitIgnore { pub enum GitIgnore {
/// Ignore files that Git would ignore. This means doing a check for a /// Ignore files that Git would ignore. This means doing a check for a
/// `.gitignore` file, possibly recursively up the filesystem tree. /// `.gitignore` file, possibly recursively up the filesystem tree.
CheckAndIgnore, CheckAndIgnore,
@ -375,7 +371,6 @@ pub enum GitIgnore {
// > .gitignore, .git/info/exclude and even your global gitignore globs, // > .gitignore, .git/info/exclude and even your global gitignore globs,
// > usually found in $XDG_CONFIG_HOME/git/ignore. // > usually found in $XDG_CONFIG_HOME/git/ignore.
#[cfg(test)] #[cfg(test)]
mod test_ignores { mod test_ignores {
use super::*; use super::*;
@ -389,23 +384,23 @@ mod test_ignores {
#[test] #[test]
fn ignores_a_glob() { 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!(fails.is_empty());
assert_eq!(false, pats.is_ignored("nothing")); assert_eq!(false, pats.is_ignored("nothing"));
assert_eq!(true, pats.is_ignored("test.mp3")); assert_eq!(true, pats.is_ignored("test.mp3"));
} }
#[test] #[test]
fn ignores_an_exact_filename() { 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!(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")); assert_eq!(false, pats.is_ignored("test.mp3"));
} }
#[test] #[test]
fn ignores_both() { 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!(fails.is_empty());
assert_eq!(true, pats.is_ignored("nothing")); assert_eq!(true, pats.is_ignored("nothing"));
assert_eq!(true, pats.is_ignored("test.mp3")); assert_eq!(true, pats.is_ignored("test.mp3"));

View File

@ -78,16 +78,19 @@ impl SortField {
"age" | "old" | "oldest" => { "age" | "old" | "oldest" => {
Self::ModifiedAge Self::ModifiedAge
} }
#[cfg(unix)]
"ch" | "changed" => { "ch" | "changed" => {
Self::ChangedDate Self::ChangedDate
} }
#[cfg(unix)]
"acc" | "accessed" => { "acc" | "accessed" => {
Self::AccessedDate Self::AccessedDate
} }
#[cfg(unix)]
"cr" | "created" => { "cr" | "created" => {
Self::CreatedDate Self::CreatedDate
} }
#[cfg(unix)]
"inode" => { "inode" => {
Self::FileInode Self::FileInode
} }

View File

@ -27,7 +27,7 @@
//! command-line options, as all the options and their values (such as //! command-line options, as all the options and their values (such as
//! `--sort size`) are guaranteed to just be 8-bit ASCII. //! `--sort size`) are guaranteed to just be 8-bit ASCII.
use std::borrow::Cow;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fmt; use std::fmt;
@ -79,7 +79,6 @@ impl fmt::Display for Flag {
/// Whether redundant arguments should be considered a problem. /// Whether redundant arguments should be considered a problem.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum Strictness { pub enum Strictness {
/// Throw an error when an argument doesnt do anything, either because /// Throw an error when an argument doesnt do anything, either because
/// it requires another argument to be specified, or because two conflict. /// it requires another argument to be specified, or because two conflict.
ComplainAboutRedundantArguments, ComplainAboutRedundantArguments,
@ -93,7 +92,6 @@ pub enum Strictness {
/// arguments. /// arguments.
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
pub enum TakesValue { pub enum TakesValue {
/// This flag has to be followed by a value. /// This flag has to be followed by a value.
/// If theres a fixed set of possible values, they can be printed out /// If theres a fixed set of possible values, they can be printed out
/// with the error text. /// with the error text.
@ -106,11 +104,9 @@ pub enum TakesValue {
Optional(Option<Values>), Optional(Option<Values>),
} }
/// An **argument** can be matched by one of the users input strings. /// An **argument** can be matched by one of the users input strings.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub struct Arg { pub struct Arg {
/// The short argument that matches it, if any. /// The short argument that matches it, if any.
pub short: Option<ShortArg>, pub short: Option<ShortArg>,
@ -134,19 +130,20 @@ impl fmt::Display for Arg {
} }
} }
/// Literally just several args. /// Literally just several args.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Args(pub &'static [&'static Arg]); pub struct Args(pub &'static [&'static Arg]);
impl Args { impl Args {
/// Iterates over the given list of command-line arguments and parses /// Iterates over the given list of command-line arguments and parses
/// them into a list of matched flags and free strings. /// them into a list of matched flags and free strings.
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError> pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
where I: IntoIterator<Item = &'args OsStr> where I: IntoIterator<Item = &'args OsStr>
{ {
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
#[cfg(windows)]
use os_str_bytes::{OsStrBytes, OsStringBytes};
let mut parsing = true; let mut parsing = true;
@ -159,7 +156,7 @@ impl Args {
// doesnt have one in its string so it needs the next one. // doesnt have one in its string so it needs the next one.
let mut inputs = inputs.into_iter(); let mut inputs = inputs.into_iter();
while let Some(arg) = inputs.next() { 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 “--”. // Stop parsing if one of the arguments is the literal string “--”.
// This allows a file named “--arg” to be specified by passing in // This allows a file named “--arg” to be specified by passing in
@ -167,20 +164,18 @@ impl Args {
// doesnt exist. // doesnt exist.
if ! parsing { if ! parsing {
frees.push(arg) frees.push(arg)
} } else if arg == "--" {
else if arg == "--" {
parsing = false; parsing = false;
} }
// If the string starts with *two* dashes then its a long argument. // If the string starts with *two* dashes then its a long argument.
else if bytes.starts_with(b"--") { 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 theres an equals in it, then the string before the // If theres an equals in it, then the string before the
// equals will be the flags name, and the string after it // equals will be the flags name, and the string after it
// will be its value. // will be its value.
if let Some((before, after)) = split_on_equals(long_arg_name) { if let Some((before, after)) = split_on_equals(&long_arg_name) {
let arg = self.lookup_long(before)?; let arg = self.lookup_long(&*before)?;
let flag = Flag::Long(arg.long); let flag = Flag::Long(arg.long);
match arg.takes_value { match arg.takes_value {
TakesValue::Necessary(_) | TakesValue::Necessary(_) |
@ -188,11 +183,10 @@ impl Args {
TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }), TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
} }
} }
// If theres no equals, then the entire string (apart from // If theres no equals, then the entire string (apart from
// the dashes) is the argument name. // the dashes) is the argument name.
else { else {
let arg = self.lookup_long(long_arg_name)?; let arg = self.lookup_long(&*long_arg_name)?;
let flag = Flag::Long(arg.long); let flag = Flag::Long(arg.long);
match arg.takes_value { match arg.takes_value {
TakesValue::Forbidden => { TakesValue::Forbidden => {
@ -200,28 +194,25 @@ impl Args {
} }
TakesValue::Necessary(values) => { TakesValue::Necessary(values) => {
if let Some(next_arg) = inputs.next() { if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg))); result_flags.push((flag, Some(next_arg.clone())));
} } else {
else { return Err(ParseError::NeedsValue { flag, values });
return Err(ParseError::NeedsValue { flag, values })
} }
} }
TakesValue::Optional(_) => { TakesValue::Optional(_) => {
if let Some(next_arg) = inputs.next() { if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg))); result_flags.push((flag, Some(next_arg.clone())));
} } else {
else {
result_flags.push((flag, None)); result_flags.push((flag, None));
} }
} }
} }
} }
} }
// If the string starts with *one* dash then its one or more // If the string starts with *one* dash then its one or more
// short arguments. // short arguments.
else if bytes.starts_with(b"-") && arg != "-" { 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 theres an equals in it, then the argument immediately // If theres an equals in it, then the argument immediately
// before the equals was the one that has the value, with the // before the equals was the one that has the value, with the
@ -235,8 +226,9 @@ impl Args {
// Theres no way to give two values in a cluster like this: // Theres no way to give two values in a cluster like this:
// its an error if any of the first set of arguments actually // its an error if any of the first set of arguments actually
// takes a value. // takes a value.
if let Some((before, after)) = split_on_equals(short_arg) { if let Some((before, after)) = split_on_equals(&short_arg) {
let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap(); let bytes = before.to_bytes();
let (arg_with_value, other_args) = bytes.split_last().unwrap();
// Process the characters immediately following the dash... // Process the characters immediately following the dash...
for byte in other_args { for byte in other_args {
@ -266,7 +258,6 @@ impl Args {
} }
} }
} }
// If theres no equals, then every character is parsed as // If theres no equals, then every character is parsed as
// its own short argument. However, if any of the arguments // its own short argument. However, if any of the arguments
// takes a value, then the *rest* of the string is used as // takes a value, then the *rest* of the string is used as
@ -290,14 +281,15 @@ impl Args {
TakesValue::Necessary(values) | TakesValue::Necessary(values) |
TakesValue::Optional(values) => { TakesValue::Optional(values) => {
if index < bytes.len() - 1 { if index < bytes.len() - 1 {
let remnants = &bytes[index+1 ..]; let remnants = &bytes[index + 1..];
result_flags.push((flag, Some(OsStr::from_bytes(remnants)))); result_flags.push((
flag,
Some(OsStringBytes::from_bytes(remnants).unwrap()),
));
break; break;
} } else if let Some(next_arg) = inputs.next() {
else if let Some(next_arg) = inputs.next() { result_flags.push((flag, Some(next_arg.clone())));
result_flags.push((flag, Some(next_arg))); } else {
}
else {
match arg.takes_value { match arg.takes_value {
TakesValue::Forbidden => { TakesValue::Forbidden => {
unreachable!() unreachable!()
@ -315,14 +307,19 @@ impl Args {
} }
} }
} }
// Otherwise, its a free string, usually a file name. // Otherwise, its a free string, usually a file name.
else { else {
frees.push(arg) 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> { fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> {
@ -340,22 +337,19 @@ impl Args {
} }
} }
/// The **matches** are the result of parsing the users command-line strings. /// The **matches** are the result of parsing the users command-line strings.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Matches<'args> { pub struct Matches<'args> {
/// The flags that were parsed from the users input. /// The flags that were parsed from the users input.
pub flags: MatchedFlags<'args>, pub flags: MatchedFlags<'args> ,
/// All the strings that werent matched as arguments, as well as anything /// All the strings that werent matched as arguments, as well as anything
/// after the special “--” string. /// after the special "--" string.
pub frees: Vec<&'args OsStr>, pub frees: Vec<&'args OsStr>,
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct MatchedFlags<'args> { pub struct MatchedFlags {
/// The individual flags from the users input, in the order they were /// The individual flags from the users input, in the order they were
/// originally given. /// originally given.
/// ///
@ -368,8 +362,7 @@ pub struct MatchedFlags<'args> {
strictness: Strictness, strictness: Strictness,
} }
impl<'a> MatchedFlags<'a> { impl MatchedFlags {
/// Whether the given argument was specified. /// Whether the given argument was specified.
/// Returns `true` if it was, `false` if it wasnt, and an error in /// Returns `true` if it was, `false` if it wasnt, and an error in
/// strict mode if it was specified more than once. /// strict mode if it was specified more than once.
@ -449,7 +442,8 @@ impl<'a> MatchedFlags<'a> {
/// Counts the number of occurrences of the given argument, even in /// Counts the number of occurrences of the given argument, even in
/// strict mode. /// strict mode.
pub fn count(&self, arg: &Arg) -> usize { pub fn count(&self, arg: &Arg) -> usize {
self.flags.iter() self.flags
.iter()
.filter(|tuple| tuple.0.matches(arg)) .filter(|tuple| tuple.0.matches(arg))
.count() .count()
} }
@ -461,12 +455,10 @@ impl<'a> MatchedFlags<'a> {
} }
} }
/// A problem with the users input that meant it couldnt be parsed into a /// A problem with the users input that meant it couldnt be parsed into a
/// coherent list of arguments. /// coherent list of arguments.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum ParseError { pub enum ParseError {
/// A flag that has to take a value was not given one. /// A flag that has to take a value was not given one.
NeedsValue { flag: Flag, values: Option<Values> }, NeedsValue { flag: Flag, values: Option<Values> },
@ -495,14 +487,12 @@ impl fmt::Display for ParseError {
} }
} }
/// Splits a string on its `=` character, returning the two substrings on /// Splits a string on its `=` character, returning the two substrings on
/// either side. Returns `None` if theres no equals or a string is missing. /// either side. Returns `None` if theres no equals or a string is missing.
fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> { fn split_on_equals<'a>(input: &'a Cow<OsStr>) -> Option<(OsString, OsString)> {
use std::os::unix::ffi::OsStrExt; if let Some(index) = input.to_bytes().iter().position(|elem| *elem == b'=') {
let bytes = input.to_bytes();
if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') { let (before, after) = bytes.split_at(index);
let (before, after) = input.as_bytes().split_at(index);
// The after string contains the = that we need to remove. // The after string contains the = that we need to remove.
if ! before.is_empty() && after.len() >= 2 { if ! before.is_empty() && after.len() >= 2 {
@ -550,7 +540,6 @@ mod split_test {
test_split!(more: "this=that=other" => "this", "that=other"); test_split!(more: "this=that=other" => "this", "that=other");
} }
#[cfg(test)] #[cfg(test)]
mod parse_test { mod parse_test {
use super::*; use super::*;
@ -591,16 +580,31 @@ mod parse_test {
}; };
} }
const SUGGESTIONS: Values = &[ "example" ]; const SUGGESTIONS: Values = &["example"];
static TEST_ARGS: &[&Arg] = &[ static TEST_ARGS: &[&Arg] = &[
&Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }, &Arg {
&Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }, short: Some(b'l'),
&Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }, long: "long",
&Arg { short: Some(b't'), long: "type", takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) } 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 // Just filenames
test!(empty: [] => frees: [], flags: []); test!(empty: [] => frees: [], flags: []);
test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []); test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []);
@ -612,7 +616,6 @@ mod parse_test {
test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []); test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []);
test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []); test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []);
// Long args // Long args
test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]); test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]);
test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]); test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]);
@ -621,14 +624,13 @@ mod parse_test {
// Long args with values // Long args with values
test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") }); test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") });
test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count"), values: None }); 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_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsString::from("4"))) ]);
test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]); test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsString::from("4"))) ]);
// Long args with values and suggestions // Long args with values and suggestions
test!(no_arg_s: ["--type"] => error NeedsValue { flag: Flag::Long("type"), values: Some(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_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(OsStr::new("exa"))) ]); test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsString::from("exa"))) ]);
// Short args // Short args
test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]); test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]);
@ -639,18 +641,17 @@ mod parse_test {
// Short args with values // Short args with values
test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') }); 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_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_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(OsStr::new("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(OsStr::new("two"))) ]); 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(OsStr::new("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(OsStr::new("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 // Short args with values and suggestions
test!(short_none_s: ["-t"] => error NeedsValue { flag: Flag::Short(b't'), values: Some(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_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(OsStr::new("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(OsStr::new("exa"))) ]); test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsString::from("exa"))) ]);
// Unknown args // Unknown args
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: OsString::from("quiet") }); test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: OsString::from("quiet") });
@ -661,7 +662,6 @@ mod parse_test {
test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' }); test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' });
} }
#[cfg(test)] #[cfg(test)]
mod matches_test { mod matches_test {
use super::*; use super::*;
@ -680,9 +680,16 @@ mod matches_test {
}; };
} }
static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }; static VERBOSE: Arg = Arg {
static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }; 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_never: [], has VERBOSE => false);
test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true); test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true);
@ -691,17 +698,16 @@ mod matches_test {
test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true); 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!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
#[test] #[test]
fn only_count() { fn only_count() {
let everything = OsString::from("everything"); let everything = OsString::from("everything");
let flags = MatchedFlags { let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ], flags: vec![(Flag::Short(b'c'), Some(everything.clone()))],
strictness: Strictness::UseLastArguments, strictness: Strictness::UseLastArguments,
}; };
assert_eq!(flags.get(&COUNT), Ok(Some(&*everything))); assert_eq!(flags.get(&COUNT), Ok(Some(everything)));
} }
#[test] #[test]
@ -710,17 +716,22 @@ mod matches_test {
let nothing = OsString::from("nothing"); let nothing = OsString::from("nothing");
let flags = MatchedFlags { let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)), flags: vec![
(Flag::Short(b'c'), Some(&*nothing)) ], (Flag::Short(b'c'), Some(everything)),
(Flag::Short(b'c'), Some(nothing.clone())),
],
strictness: Strictness::UseLastArguments, strictness: Strictness::UseLastArguments,
}; };
assert_eq!(flags.get(&COUNT), Ok(Some(&*nothing))); assert_eq!(flags.get(&COUNT), Ok(Some(nothing)));
} }
#[test] #[test]
fn no_count() { fn no_count() {
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments }; let flags = MatchedFlags {
flags: Vec::new(),
strictness: Strictness::UseLastArguments,
};
assert_eq!(flags.has(&COUNT).unwrap(), false); assert_eq!(flags.has(&COUNT).unwrap(), false);
} }

View File

@ -235,6 +235,17 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
/// The character to be displayed after a file when classifying is on, if /// The character to be displayed after a file when classifying is on, if
/// the files type has one associated with it. /// the files 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> { fn classify_char(&self) -> Option<&'static str> {
if self.file.is_executable_file() { if self.file.is_executable_file() {
Some("*") Some("*")
@ -295,11 +306,16 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
match self.file { match self.file {
f if f.is_directory() => self.colours.directory(), f if f.is_directory() => self.colours.directory(),
#[cfg(unix)]
f if f.is_executable_file() => self.colours.executable_file(), f if f.is_executable_file() => self.colours.executable_file(),
f if f.is_link() => self.colours.symlink(), f if f.is_link() => self.colours.symlink(),
#[cfg(unix)]
f if f.is_pipe() => self.colours.pipe(), f if f.is_pipe() => self.colours.pipe(),
#[cfg(unix)]
f if f.is_block_device() => self.colours.block_device(), f if f.is_block_device() => self.colours.block_device(),
#[cfg(unix)]
f if f.is_char_device() => self.colours.char_device(), f if f.is_char_device() => self.colours.char_device(),
#[cfg(unix)]
f if f.is_socket() => self.colours.socket(), f if f.is_socket() => self.colours.socket(),
f if ! f.is_file() => self.colours.special(), f if ! f.is_file() => self.colours.special(),
_ => self.colours.colour_file(self.file), _ => self.colours.colour_file(self.file),

View File

@ -7,7 +7,9 @@ pub use self::filetype::Colours as FiletypeColours;
mod git; mod git;
pub use self::git::Colours as GitColours; pub use self::git::Colours as GitColours;
#[cfg(unix)]
mod groups; mod groups;
#[cfg(unix)]
pub use self::groups::Colours as GroupColours; pub use self::groups::Colours as GroupColours;
mod inode; mod inode;
@ -26,7 +28,9 @@ mod times;
pub use self::times::Render as TimeRender; pub use self::times::Render as TimeRender;
// times does too // times does too
#[cfg(unix)]
mod users; mod users;
#[cfg(unix)]
pub use self::users::Colours as UserColours; pub use self::users::Colours as UserColours;
mod octal; mod octal;

View File

@ -8,6 +8,7 @@ use crate::output::render::FiletypeColours;
impl f::PermissionsPlus { impl f::PermissionsPlus {
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell { pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
let mut chars = vec![ self.file_type.render(colours) ]; let mut chars = vec![ self.file_type.render(colours) ];
#[cfg(unix)]
chars.extend(self.permissions.render(colours, self.file_type.is_regular_file())); chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
if self.xattrs { if self.xattrs {

View File

@ -1,6 +1,7 @@
use std::cmp::max; use std::cmp::max;
use std::env; use std::env;
use std::ops::Deref; use std::ops::Deref;
#[cfg(unix)]
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use datetime::TimeZone; use datetime::TimeZone;
@ -8,6 +9,7 @@ use zoneinfo_compiled::{CompiledData, Result as TZResult};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::*; use log::*;
#[cfg(unix)]
use users::UsersCache; use users::UsersCache;
use crate::fs::{File, fields as f}; use crate::fs::{File, fields as f};
@ -29,7 +31,6 @@ pub struct Options {
/// Extra columns to display in the table. /// Extra columns to display in the table.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub struct Columns { pub struct Columns {
/// At least one of these timestamps will be shown. /// At least one of these timestamps will be shown.
pub time_types: TimeTypes, pub time_types: TimeTypes,
@ -52,6 +53,7 @@ impl Columns {
let mut columns = Vec::with_capacity(4); let mut columns = Vec::with_capacity(4);
if self.inode { if self.inode {
#[cfg(unix)]
columns.push(Column::Inode); columns.push(Column::Inode);
} }
@ -60,10 +62,12 @@ impl Columns {
} }
if self.permissions { if self.permissions {
#[cfg(unix)]
columns.push(Column::Permissions); columns.push(Column::Permissions);
} }
if self.links { if self.links {
#[cfg(unix)]
columns.push(Column::HardLinks); columns.push(Column::HardLinks);
} }
@ -72,30 +76,37 @@ impl Columns {
} }
if self.blocks { if self.blocks {
#[cfg(unix)]
columns.push(Column::Blocks); columns.push(Column::Blocks);
} }
if self.user { if self.user {
#[cfg(unix)]
columns.push(Column::User); columns.push(Column::User);
} }
if self.group { if self.group {
#[cfg(unix)]
columns.push(Column::Group); columns.push(Column::Group);
} }
if self.time_types.modified { if self.time_types.modified {
#[cfg(unix)]
columns.push(Column::Timestamp(TimeType::Modified)); columns.push(Column::Timestamp(TimeType::Modified));
} }
if self.time_types.changed { if self.time_types.changed {
#[cfg(unix)]
columns.push(Column::Timestamp(TimeType::Changed)); columns.push(Column::Timestamp(TimeType::Changed));
} }
if self.time_types.created { if self.time_types.created {
#[cfg(unix)]
columns.push(Column::Timestamp(TimeType::Created)); columns.push(Column::Timestamp(TimeType::Created));
} }
if self.time_types.accessed { if self.time_types.accessed {
#[cfg(unix)]
columns.push(Column::Timestamp(TimeType::Accessed)); columns.push(Column::Timestamp(TimeType::Accessed));
} }
@ -107,17 +118,23 @@ impl Columns {
} }
} }
/// A table contains these. /// A table contains these.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum Column { pub enum Column {
#[cfg(unix)]
Permissions, Permissions,
FileSize, FileSize,
#[cfg(unix)]
Timestamp(TimeType), Timestamp(TimeType),
#[cfg(unix)]
Blocks, Blocks,
#[cfg(unix)]
User, User,
#[cfg(unix)]
Group, Group,
#[cfg(unix)]
HardLinks, HardLinks,
#[cfg(unix)]
Inode, Inode,
GitStatus, GitStatus,
Octal, Octal,
@ -132,8 +149,8 @@ pub enum Alignment {
} }
impl Column { impl Column {
/// Get the alignment this column should use. /// Get the alignment this column should use.
#[cfg(unix)]
pub fn alignment(self) -> Alignment { pub fn alignment(self) -> Alignment {
match self { match self {
Self::FileSize | Self::FileSize |
@ -144,18 +161,33 @@ impl Column {
_ => Alignment::Left, _ => Alignment::Left,
} }
} }
#[cfg(windows)]
pub fn alignment(&self) -> Alignment {
match *self {
Column::FileSize | Column::GitStatus => Alignment::Right,
_ => Alignment::Left,
}
}
#[cfg(windows)]
/// Get the text that should be printed at the top, when the user elects /// Get the text that should be printed at the top, when the user elects
/// to have a header row printed. /// to have a header row printed.
pub fn header(self) -> &'static str { pub fn header(self) -> &'static str {
match self { match self {
#[cfg(unix)]
Self::Permissions => "Permissions", Self::Permissions => "Permissions",
Self::FileSize => "Size", Self::FileSize => "Size",
#[cfg(unix)]
Self::Timestamp(t) => t.header(), Self::Timestamp(t) => t.header(),
#[cfg(unix)]
Self::Blocks => "Blocks", Self::Blocks => "Blocks",
#[cfg(unix)]
Self::User => "User", Self::User => "User",
#[cfg(unix)]
Self::Group => "Group", Self::Group => "Group",
#[cfg(unix)]
Self::HardLinks => "Links", Self::HardLinks => "Links",
#[cfg(unix)]
Self::Inode => "inode", Self::Inode => "inode",
Self::GitStatus => "Git", Self::GitStatus => "Git",
Self::Octal => "Octal", Self::Octal => "Octal",
@ -163,11 +195,9 @@ impl Column {
} }
} }
/// Formatting options for file sizes. /// Formatting options for file sizes.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum SizeFormat { pub enum SizeFormat {
/// Format the file size using **decimal** prefixes, such as “kilo”, /// Format the file size using **decimal** prefixes, such as “kilo”,
/// “mega”, or “giga”. /// “mega”, or “giga”.
DecimalBytes, DecimalBytes,
@ -186,7 +216,6 @@ impl Default for SizeFormat {
} }
} }
/// The types of a files time fields. These three fields are standard /// The types of a files time fields. These three fields are standard
/// across most (all?) operating systems. /// across most (all?) operating systems.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
@ -206,7 +235,6 @@ pub enum TimeType {
} }
impl TimeType { impl TimeType {
/// Returns the text to use for a columns heading in the columns output. /// Returns the text to use for a columns heading in the columns output.
pub fn header(self) -> &'static str { pub fn header(self) -> &'static str {
match self { match self {
@ -218,7 +246,6 @@ impl TimeType {
} }
} }
/// Fields for which of a files time fields should be displayed in the /// Fields for which of a files time fields should be displayed in the
/// columns output. /// columns output.
/// ///
@ -228,13 +255,12 @@ impl TimeType {
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct TimeTypes { pub struct TimeTypes {
pub modified: bool, pub modified: bool,
pub changed: bool, pub changed: bool,
pub accessed: bool, pub accessed: bool,
pub created: bool, pub created: bool,
} }
impl Default for TimeTypes { impl Default for TimeTypes {
/// By default, display just the modified time. This is the most /// By default, display just the modified time. This is the most
/// common option, which is why it has this shorthand. /// common option, which is why it has this shorthand.
fn default() -> Self { fn default() -> Self {
@ -247,13 +273,11 @@ impl Default for TimeTypes {
} }
} }
/// The **environment** struct contains any data that could change between /// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the users computers configuration. /// running instances of exa, depending on the users computers configuration.
/// ///
/// Any environment field should be able to be mocked up for test runs. /// Any environment field should be able to be mocked up for test runs.
pub struct Environment { pub struct Environment {
/// Localisation rules for formatting numbers. /// Localisation rules for formatting numbers.
numeric: locale::Numeric, numeric: locale::Numeric,
@ -262,10 +286,12 @@ pub struct Environment {
tz: Option<TimeZone>, tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames. /// Mapping cache of user IDs to usernames.
#[cfg(unix)]
users: Mutex<UsersCache>, users: Mutex<UsersCache>,
} }
impl Environment { impl Environment {
#[cfg(unix)]
pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> { pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
self.users.lock().unwrap() self.users.lock().unwrap()
} }
@ -284,9 +310,10 @@ impl Environment {
let numeric = locale::Numeric::load_user_locale() let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english()); .unwrap_or_else(|_| locale::Numeric::english());
#[cfg(unix)]
let users = Mutex::new(UsersCache::new()); let users = Mutex::new(UsersCache::new());
Self { tz, numeric, users } Self { tz, numeric, #[cfg(unix)] users }
} }
} }
@ -303,7 +330,6 @@ lazy_static! {
static ref ENVIRONMENT: Environment = Environment::load_all(); static ref ENVIRONMENT: Environment = Environment::load_all();
} }
pub struct Table<'a> { pub struct Table<'a> {
columns: Vec<Column>, columns: Vec<Column>,
theme: &'a Theme, theme: &'a Theme,
@ -363,6 +389,7 @@ impl<'a, 'f> Table<'a> {
fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus { fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus { f::PermissionsPlus {
file_type: file.type_char(), file_type: file.type_char(),
#[cfg(unix)]
permissions: file.permissions(), permissions: file.permissions(),
xattrs, xattrs,
} }
@ -376,24 +403,30 @@ impl<'a, 'f> Table<'a> {
fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell { fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
match column { match column {
#[cfg(unix)]
Column::Permissions => { Column::Permissions => {
self.permissions_plus(file, xattrs).render(self.theme) self.permissions_plus(file, xattrs).render(self.theme)
} }
Column::FileSize => { Column::FileSize => {
file.size().render(self.theme, self.size_format, &self.env.numeric) file.size().render(self.theme, self.size_format, &self.env.numeric)
} }
#[cfg(unix)]
Column::HardLinks => { Column::HardLinks => {
file.links().render(self.theme, &self.env.numeric) file.links().render(self.theme, &self.env.numeric)
} }
#[cfg(unix)]
Column::Inode => { Column::Inode => {
file.inode().render(self.theme.ui.inode) file.inode().render(self.theme.ui.inode)
} }
#[cfg(unix)]
Column::Blocks => { Column::Blocks => {
file.blocks().render(self.theme) file.blocks().render(self.theme)
} }
#[cfg(unix)]
Column::User => { Column::User => {
file.user().render(self.theme, &*self.env.lock_users()) file.user().render(self.theme, &*self.env.lock_users())
} }
#[cfg(unix)]
Column::Group => { Column::Group => {
file.group().render(self.theme, &*self.env.lock_users()) file.group().render(self.theme, &*self.env.lock_users())
} }
@ -455,7 +488,6 @@ impl<'a, 'f> Table<'a> {
} }
} }
pub struct TableWidths(Vec<usize>); pub struct TableWidths(Vec<usize>);
impl Deref for TableWidths { impl Deref for TableWidths {

View File

@ -229,6 +229,7 @@ impl render::GitColours for Theme {
fn conflicted(&self) -> Style { self.ui.git.conflicted } fn conflicted(&self) -> Style { self.ui.git.conflicted }
} }
#[cfg(unix)]
impl render::GroupColours for Theme { impl render::GroupColours for Theme {
fn yours(&self) -> Style { self.ui.users.group_yours } fn yours(&self) -> Style { self.ui.users.group_yours }
fn not_yours(&self) -> Style { self.ui.users.group_not_yours } fn not_yours(&self) -> Style { self.ui.users.group_not_yours }
@ -287,6 +288,7 @@ impl render::SizeColours for Theme {
fn minor(&self) -> Style { self.ui.size.minor } fn minor(&self) -> Style { self.ui.size.minor }
} }
#[cfg(unix)]
impl render::UserColours for Theme { impl render::UserColours for Theme {
fn you(&self) -> Style { self.ui.users.user_you } fn you(&self) -> Style { self.ui.users.user_you }
fn someone_else(&self) -> Style { self.ui.users.user_someone_else } fn someone_else(&self) -> Style { self.ui.users.user_someone_else }