mirror of
https://github.com/Llewellynvdm/exa.git
synced 2025-02-01 18:18:24 +00:00
checkpoint
This commit is contained in:
parent
78ba0b8973
commit
7f717c3af3
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
}
|
||||
|
232
src/fs/file.rs
232
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<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult<File<'dir>>
|
||||
where PD: Into<Option<&'dir Dir>>,
|
||||
FN: Into<Option<String>>
|
||||
where
|
||||
PD: Into<Option<&'dir Dir>>,
|
||||
FN: Into<Option<String>>,
|
||||
{
|
||||
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<File<'dir>> {
|
||||
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<File<'dir>> {
|
||||
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<String> {
|
||||
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<File<'a>> 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<File<'dir>>),
|
||||
|
||||
@ -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:\\")));
|
||||
}
|
||||
}
|
||||
|
105
src/fs/filter.rs
105
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<F>)
|
||||
where F: AsRef<File<'a>> {
|
||||
|
||||
where
|
||||
F: AsRef<File<'a>>,
|
||||
{
|
||||
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::Unsorted => Ordering::Equal,
|
||||
|
||||
SortField::Name(ABCabc) => natord::compare(&a.name, &b.name),
|
||||
SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
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::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::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<glob::Pattern> for IgnorePatterns {
|
||||
fn from_iter<I: IntoIterator<Item = glob::Pattern>>(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<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
|
||||
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(
|
||||
iter: I,
|
||||
) -> (Self, Vec<glob::PatternError>) {
|
||||
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"));
|
||||
|
@ -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,
|
||||
|
@ -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<Values>),
|
||||
}
|
||||
|
||||
|
||||
/// 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<ShortArg>,
|
||||
|
||||
@ -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<Matches<'args>, ParseError>
|
||||
where I: IntoIterator<Item=&'args OsString> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
pub fn parse<'args, I>(
|
||||
&self,
|
||||
inputs: I,
|
||||
strictness: Strictness,
|
||||
) -> Result<Matches<'args>, ParseError>
|
||||
where
|
||||
I: IntoIterator<Item = &'args OsString>,
|
||||
{
|
||||
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)));
|
||||
result_flags.push((flag, Some(next_arg.clone())));
|
||||
} else {
|
||||
return Err(ParseError::NeedsValue { flag, values });
|
||||
}
|
||||
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<OsString>)>,
|
||||
|
||||
/// 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<bool, Misfire> {
|
||||
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<P>(&self, predicate: P) -> Result<Option<&Flag>, 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::<Vec<_>>();
|
||||
let all = self
|
||||
.flags
|
||||
.iter()
|
||||
.filter(|tuple| tuple.1.is_none() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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);
|
||||
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<Option<&OsStr>, Misfire> {
|
||||
pub fn get(&self, arg: &'static Arg) -> Result<Option<OsString>, 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<P>(&self, predicate: P) -> Result<Option<&OsStr>, Misfire>
|
||||
where P: Fn(&Flag) -> bool {
|
||||
pub fn get_where<P>(&self, predicate: P) -> Result<Option<OsString>, 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::<Vec<_>>();
|
||||
let those = self
|
||||
.flags
|
||||
.iter()
|
||||
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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());
|
||||
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<Values> },
|
||||
|
||||
@ -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<OsStr>) -> 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<OsString> = $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::<Vec<OsString>>();
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t even used
|
||||
let bits = $inputs
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.map(|&o| os(o))
|
||||
.collect::<Vec<OsString>>();
|
||||
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());
|
||||
}
|
||||
|
@ -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<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) {
|
||||
use log::warn;
|
||||
|
||||
|
||||
use crate::options::vars;
|
||||
use crate::style::LSColors;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<Style> {
|
||||
Some(match self.file {
|
||||
f if f.is_directory() => self.colours.directory(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_executable_file() => self.colours.executable_file(),
|
||||
f if f.is_link() => self.colours.symlink(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_pipe() => self.colours.pipe(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_block_device() => self.colours.block_device(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_char_device() => self.colours.char_device(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_socket() => self.colours.socket(),
|
||||
f if !f.is_file() => self.colours.special(),
|
||||
_ => return None,
|
||||
|
@ -7,7 +7,9 @@ pub use self::filetype::Colours as FiletypeColours;
|
||||
mod git;
|
||||
pub use self::git::Colours as GitColours;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod groups;
|
||||
#[cfg(unix)]
|
||||
pub use self::groups::Colours as GroupColours;
|
||||
|
||||
mod inode;
|
||||
@ -26,5 +28,7 @@ mod times;
|
||||
pub use self::times::Render as TimeRender;
|
||||
// times does too
|
||||
|
||||
#[cfg(unix)]
|
||||
mod users;
|
||||
#[cfg(unix)]
|
||||
pub use self::users::Colours as UserColours;
|
||||
|
@ -8,6 +8,7 @@ use crate::output::render::FiletypeColours;
|
||||
impl f::PermissionsPlus {
|
||||
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
|
||||
let mut chars = vec![ self.file_type.render(colours) ];
|
||||
#[cfg(unix)]
|
||||
chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
|
||||
|
||||
if self.xattrs {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::cmp::max;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
#[cfg(unix)]
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use datetime::TimeZone;
|
||||
@ -10,15 +11,15 @@ use locale;
|
||||
|
||||
use log::debug;
|
||||
|
||||
#[cfg(unix)]
|
||||
use users::UsersCache;
|
||||
|
||||
use crate::style::Colours;
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::{fields as f, File};
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::render::TimeRender;
|
||||
use crate::output::time::TimeFormat;
|
||||
use crate::fs::{File, fields as f};
|
||||
use crate::fs::feature::git::GitCache;
|
||||
|
||||
use crate::style::Colours;
|
||||
|
||||
/// Options for displaying a table.
|
||||
pub struct Options {
|
||||
@ -39,7 +40,6 @@ impl fmt::Debug for Options {
|
||||
/// Extra columns to display in the table.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Columns {
|
||||
|
||||
/// At least one of these timestamps will be shown.
|
||||
pub time_types: TimeTypes,
|
||||
|
||||
@ -61,14 +61,17 @@ impl Columns {
|
||||
let mut columns = Vec::with_capacity(4);
|
||||
|
||||
if self.inode {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Inode);
|
||||
}
|
||||
|
||||
if self.permissions {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Permissions);
|
||||
}
|
||||
|
||||
if self.links {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::HardLinks);
|
||||
}
|
||||
|
||||
@ -77,34 +80,41 @@ impl Columns {
|
||||
}
|
||||
|
||||
if self.blocks {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Blocks);
|
||||
}
|
||||
|
||||
if self.user {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::User);
|
||||
}
|
||||
|
||||
if self.group {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Group);
|
||||
}
|
||||
|
||||
if self.time_types.modified {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Timestamp(TimeType::Modified));
|
||||
}
|
||||
|
||||
if self.time_types.changed {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Timestamp(TimeType::Changed));
|
||||
}
|
||||
|
||||
if self.time_types.created {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Timestamp(TimeType::Created));
|
||||
}
|
||||
|
||||
if self.time_types.accessed {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Timestamp(TimeType::Accessed));
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && self.git && actually_enable_git {
|
||||
if cfg!(feature = "git") && self.git && actually_enable_git {
|
||||
columns.push(Column::GitStatus);
|
||||
}
|
||||
|
||||
@ -112,17 +122,23 @@ impl Columns {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A table contains these.
|
||||
#[derive(Debug)]
|
||||
pub enum Column {
|
||||
#[cfg(unix)]
|
||||
Permissions,
|
||||
FileSize,
|
||||
#[cfg(unix)]
|
||||
Timestamp(TimeType),
|
||||
#[cfg(unix)]
|
||||
Blocks,
|
||||
#[cfg(unix)]
|
||||
User,
|
||||
#[cfg(unix)]
|
||||
Group,
|
||||
#[cfg(unix)]
|
||||
HardLinks,
|
||||
#[cfg(unix)]
|
||||
Inode,
|
||||
GitStatus,
|
||||
}
|
||||
@ -131,12 +147,13 @@ pub enum Column {
|
||||
/// right-aligned, and text is left-aligned.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Alignment {
|
||||
Left, Right,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Column {
|
||||
|
||||
/// Get the alignment this column should use.
|
||||
#[cfg(unix)]
|
||||
pub fn alignment(&self) -> Alignment {
|
||||
match *self {
|
||||
Column::FileSize
|
||||
@ -144,32 +161,45 @@ impl Column {
|
||||
| Column::Inode
|
||||
| Column::Blocks
|
||||
| Column::GitStatus => Alignment::Right,
|
||||
_ => 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
|
||||
/// to have a header row printed.
|
||||
pub fn header(&self) -> &'static str {
|
||||
match *self {
|
||||
Column::Permissions => "Permissions",
|
||||
Column::FileSize => "Size",
|
||||
Column::Timestamp(t) => t.header(),
|
||||
Column::Blocks => "Blocks",
|
||||
Column::User => "User",
|
||||
Column::Group => "Group",
|
||||
Column::HardLinks => "Links",
|
||||
Column::Inode => "inode",
|
||||
Column::GitStatus => "Git",
|
||||
#[cfg(unix)]
|
||||
Column::Permissions => "Permissions",
|
||||
Column::FileSize => "Size",
|
||||
#[cfg(unix)]
|
||||
Column::Timestamp(t) => t.header(),
|
||||
#[cfg(unix)]
|
||||
Column::Blocks => "Blocks",
|
||||
#[cfg(unix)]
|
||||
Column::User => "User",
|
||||
#[cfg(unix)]
|
||||
Column::Group => "Group",
|
||||
#[cfg(unix)]
|
||||
Column::HardLinks => "Links",
|
||||
#[cfg(unix)]
|
||||
Column::Inode => "inode",
|
||||
Column::GitStatus => "Git",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Formatting options for file sizes.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SizeFormat {
|
||||
|
||||
/// Format the file size using **decimal** prefixes, such as “kilo”,
|
||||
/// “mega”, or “giga”.
|
||||
DecimalBytes,
|
||||
@ -188,7 +218,6 @@ impl Default for SizeFormat {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The types of a file’s time fields. These three fields are standard
|
||||
/// across most (all?) operating systems.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
@ -207,19 +236,17 @@ pub enum TimeType {
|
||||
}
|
||||
|
||||
impl TimeType {
|
||||
|
||||
/// Returns the text to use for a column’s heading in the columns output.
|
||||
pub fn header(self) -> &'static str {
|
||||
match self {
|
||||
TimeType::Modified => "Date Modified",
|
||||
TimeType::Changed => "Date Changed",
|
||||
TimeType::Accessed => "Date Accessed",
|
||||
TimeType::Created => "Date Created",
|
||||
TimeType::Modified => "Date Modified",
|
||||
TimeType::Changed => "Date Changed",
|
||||
TimeType::Accessed => "Date Accessed",
|
||||
TimeType::Created => "Date Created",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Fields for which of a file’s time fields should be displayed in the
|
||||
/// columns output.
|
||||
///
|
||||
@ -228,29 +255,29 @@ impl TimeType {
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct TimeTypes {
|
||||
pub modified: bool,
|
||||
pub changed: bool,
|
||||
pub changed: bool,
|
||||
pub accessed: bool,
|
||||
pub created: bool,
|
||||
pub created: bool,
|
||||
}
|
||||
|
||||
impl Default for TimeTypes {
|
||||
|
||||
/// By default, display just the ‘modified’ time. This is the most
|
||||
/// common option, which is why it has this shorthand.
|
||||
fn default() -> TimeTypes {
|
||||
TimeTypes { modified: true, changed: false, accessed: false, created: false }
|
||||
TimeTypes {
|
||||
modified: true,
|
||||
changed: false,
|
||||
accessed: false,
|
||||
created: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// The **environment** struct contains any data that could change between
|
||||
/// running instances of exa, depending on the user's computer's configuration.
|
||||
///
|
||||
/// Any environment field should be able to be mocked up for test runs.
|
||||
pub struct Environment {
|
||||
|
||||
/// Localisation rules for formatting numbers.
|
||||
numeric: locale::Numeric,
|
||||
|
||||
@ -259,10 +286,12 @@ pub struct Environment {
|
||||
tz: Option<TimeZone>,
|
||||
|
||||
/// Mapping cache of user IDs to usernames.
|
||||
#[cfg(unix)]
|
||||
users: Mutex<UsersCache>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
#[cfg(unix)]
|
||||
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
|
||||
self.users.lock().unwrap()
|
||||
}
|
||||
@ -276,12 +305,17 @@ impl Environment {
|
||||
}
|
||||
};
|
||||
|
||||
let numeric = locale::Numeric::load_user_locale()
|
||||
.unwrap_or_else(|_| locale::Numeric::english());
|
||||
|
||||
let numeric =
|
||||
locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());
|
||||
#[cfg(unix)]
|
||||
let users = Mutex::new(UsersCache::new());
|
||||
|
||||
Environment { tz, numeric, users }
|
||||
Environment {
|
||||
tz,
|
||||
numeric,
|
||||
#[cfg(unix)]
|
||||
users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +323,6 @@ fn determine_time_zone() -> TZResult<TimeZone> {
|
||||
TimeZone::from_file("/etc/localtime")
|
||||
}
|
||||
|
||||
|
||||
pub struct Table<'a> {
|
||||
columns: Vec<Column>,
|
||||
colours: &'a Colours,
|
||||
@ -311,10 +344,13 @@ impl<'a, 'f> Table<'a> {
|
||||
let widths = TableWidths::zero(columns.len());
|
||||
|
||||
Table {
|
||||
colours, widths, columns, git,
|
||||
env: &options.env,
|
||||
colours,
|
||||
widths,
|
||||
columns,
|
||||
git,
|
||||
env: &options.env,
|
||||
time_format: &options.time_format,
|
||||
size_format: options.size_format,
|
||||
size_format: options.size_format,
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,17 +359,21 @@ impl<'a, 'f> Table<'a> {
|
||||
}
|
||||
|
||||
pub fn header_row(&self) -> Row {
|
||||
let cells = self.columns.iter()
|
||||
.map(|c| TextCell::paint_str(self.colours.header, c.header()))
|
||||
.collect();
|
||||
let cells = self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| TextCell::paint_str(self.colours.header, c.header()))
|
||||
.collect();
|
||||
|
||||
Row { cells }
|
||||
}
|
||||
|
||||
pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
|
||||
let cells = self.columns.iter()
|
||||
.map(|c| self.display(file, c, xattrs))
|
||||
.collect();
|
||||
let cells = self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| self.display(file, c, xattrs))
|
||||
.collect();
|
||||
|
||||
Row { cells }
|
||||
}
|
||||
@ -345,6 +385,7 @@ impl<'a, 'f> Table<'a> {
|
||||
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
|
||||
f::PermissionsPlus {
|
||||
file_type: file.type_char(),
|
||||
#[cfg(unix)]
|
||||
permissions: file.permissions(),
|
||||
xattrs,
|
||||
}
|
||||
@ -354,19 +395,44 @@ impl<'a, 'f> Table<'a> {
|
||||
use crate::output::table::TimeType::*;
|
||||
|
||||
match *column {
|
||||
Column::Permissions => self.permissions_plus(file, xattrs).render(self.colours),
|
||||
Column::FileSize => file.size().render(self.colours, self.size_format, &self.env.numeric),
|
||||
Column::HardLinks => file.links().render(self.colours, &self.env.numeric),
|
||||
Column::Inode => file.inode().render(self.colours.inode),
|
||||
Column::Blocks => file.blocks().render(self.colours),
|
||||
Column::User => file.user().render(self.colours, &*self.env.lock_users()),
|
||||
Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
|
||||
Column::GitStatus => self.git_status(file).render(self.colours),
|
||||
#[cfg(unix)]
|
||||
Column::Permissions => self.permissions_plus(file, xattrs).render(self.colours),
|
||||
Column::FileSize => {
|
||||
file.size()
|
||||
.render(self.colours, self.size_format, &self.env.numeric)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::HardLinks => file.links().render(self.colours, &self.env.numeric),
|
||||
#[cfg(unix)]
|
||||
Column::Inode => file.inode().render(self.colours.inode),
|
||||
#[cfg(unix)]
|
||||
Column::Blocks => file.blocks().render(self.colours),
|
||||
#[cfg(unix)]
|
||||
Column::User => file.user().render(self.colours, &*self.env.lock_users()),
|
||||
#[cfg(unix)]
|
||||
Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
|
||||
Column::GitStatus => self.git_status(file).render(self.colours),
|
||||
|
||||
Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Changed) => file.changed_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Accessed) => file.accessed_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
#[cfg(unix)]
|
||||
Column::Timestamp(Modified) => {
|
||||
file.modified_time()
|
||||
.render(self.colours.date, &self.env.tz, &self.time_format)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Timestamp(Changed) => {
|
||||
file.changed_time()
|
||||
.render(self.colours.date, &self.env.tz, &self.time_format)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Timestamp(Created) => {
|
||||
file.created_time()
|
||||
.render(self.colours.date, &self.env.tz, &self.time_format)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Timestamp(Accessed) => {
|
||||
file.accessed_time()
|
||||
.render(self.colours.date, &self.env.tz, &self.time_format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -384,8 +450,14 @@ impl<'a, 'f> Table<'a> {
|
||||
let padding = width - *this_cell.width;
|
||||
|
||||
match self.columns[n].alignment() {
|
||||
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
|
||||
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
|
||||
Alignment::Left => {
|
||||
cell.append(this_cell);
|
||||
cell.add_spaces(padding);
|
||||
}
|
||||
Alignment::Right => {
|
||||
cell.add_spaces(padding);
|
||||
cell.append(this_cell);
|
||||
}
|
||||
}
|
||||
|
||||
cell.add_spaces(1);
|
||||
@ -395,8 +467,6 @@ impl<'a, 'f> Table<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct TableWidths(Vec<usize>);
|
||||
|
||||
impl Deref for TableWidths {
|
||||
@ -409,7 +479,7 @@ impl Deref for TableWidths {
|
||||
|
||||
impl TableWidths {
|
||||
pub fn zero(count: usize) -> TableWidths {
|
||||
TableWidths(vec![ 0; count ])
|
||||
TableWidths(vec![0; count])
|
||||
}
|
||||
|
||||
pub fn add_widths(&mut self, row: &Row) {
|
||||
|
@ -396,6 +396,7 @@ impl render::GitColours for Colours {
|
||||
fn ignored(&self) -> Style { self.git.ignored }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl render::GroupColours for Colours {
|
||||
fn yours(&self) -> Style { self.users.group_yours }
|
||||
fn not_yours(&self) -> Style { self.users.group_not_yours }
|
||||
@ -452,6 +453,7 @@ impl render::SizeColours for Colours {
|
||||
fn minor(&self) -> Style { self.size.minor }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl render::UserColours for Colours {
|
||||
fn you(&self) -> Style { self.users.user_you }
|
||||
fn someone_else(&self) -> Style { self.users.user_someone_else }
|
||||
|
Loading…
x
Reference in New Issue
Block a user