Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #393: add flag to dereference links #1136

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ These options are available when running with `--long` (`-l`):
- **-t**, **--time=(field)**: which timestamp field to use
- **-u**, **--accessed**: use the accessed timestamp field
- **-U**, **--created**: use the created timestamp field
- **-X**, **--dereference**: dereference symlinks for file information
- **-@**, **--extended**: list each file’s extended attributes and sizes
- **--changed**: use the changed timestamp field
- **--git**: list each file’s Git status, if tracked or ignored
Expand Down
8 changes: 6 additions & 2 deletions src/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ impl Dir {

/// Produce an iterator of IO results of trying to read all the files in
/// this directory.
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool) -> Files<'dir, 'ig> {
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool) -> Files<'dir, 'ig> {
Files {
inner: self.contents.iter(),
dir: self,
dotfiles: dots.shows_dotfiles(),
dots: dots.dots(),
git,
git_ignoring,
deref_links,
}
}

Expand Down Expand Up @@ -89,6 +90,9 @@ pub struct Files<'dir, 'ig> {
git: Option<&'ig GitCache>,

git_ignoring: bool,

/// Whether symbolic links should be dereferenced when querying information.
deref_links: bool,
}

impl<'dir, 'ig> Files<'dir, 'ig> {
Expand Down Expand Up @@ -118,7 +122,7 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
}
}

return Some(File::from_args(path.clone(), self.dir, filename)
return Some(File::from_args(path.clone(), self.dir, filename, self.deref_links)
.map_err(|e| (path.clone(), e)))
}

Expand Down
123 changes: 106 additions & 17 deletions src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,17 @@ pub struct File<'dir> {
/// directory’s children, and are in fact added specifically by exa; this
/// means that they should be skipped when recursing.
pub is_all_all: bool,

/// Whether to dereference symbolic links when querying for information.
///
/// For instance, when querying the size of a symbolic link, if
/// dereferencing is enabled, the size of the target will be displayed
/// instead.
pub deref_links: bool,
}

impl<'dir> File<'dir> {
pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> io::Result<File<'dir>>
pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool) -> io::Result<File<'dir>>
where PD: Into<Option<&'dir Dir>>,
FN: Into<Option<String>>
{
Expand All @@ -78,7 +85,7 @@ impl<'dir> File<'dir> {
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = false;

Ok(File { name, ext, path, metadata, parent_dir, is_all_all })
Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links })
}

pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
Expand All @@ -90,7 +97,7 @@ impl<'dir> File<'dir> {
let is_all_all = true;
let parent_dir = Some(parent_dir);

Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all })
Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false })
}

pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
Expand All @@ -101,7 +108,7 @@ impl<'dir> File<'dir> {
let is_all_all = true;
let parent_dir = Some(parent_dir);

Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all })
Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false })
}

/// A file’s name is derived from its string. This needs to handle directories
Expand Down Expand Up @@ -253,7 +260,7 @@ impl<'dir> File<'dir> {
Ok(metadata) => {
let ext = File::ext(&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, deref_links: self.deref_links};
FileTarget::Ok(Box::new(file))
}
Err(e) => {
Expand All @@ -263,6 +270,28 @@ impl<'dir> File<'dir> {
}
}

/// Assuming this file is a symlink, follows that link and any further
/// links recursively, returning the result from following the trail.
///
/// For a working symlink that the user is allowed to follow,
/// this will be the `File` object at the other end, which can then have
/// its name, colour, and other details read.
///
/// For a broken symlink, returns where the file *would* be, if it
/// 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_recurse(&self) -> FileTarget<'dir> {
let target = self.link_target();
if let FileTarget::Ok(f) = target {
if f.is_link() {
return f.link_target_recurse();
} else {
return FileTarget::Ok(f);
}
}
target
}

/// This file’s number of hard links.
///
/// It also reports whether this is both a regular file, and a file with
Expand Down Expand Up @@ -296,14 +325,27 @@ impl<'dir> File<'dir> {
}
}

/// The ID of the user that own this file.
pub fn user(&self) -> f::User {
f::User(self.metadata.uid())
/// The ID of the user that own this file. If dereferencing links, the links
/// may be broken, in which case `None` will be returned.
pub fn user(&self) -> Option<f::User> {
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.user(),
_ => return None,
}
}
Some(f::User(self.metadata.uid()))
}

/// The ID of the group that owns this file.
pub fn group(&self) -> f::Group {
f::Group(self.metadata.gid())
pub fn group(&self) -> Option<f::Group> {
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.group(),
_ => return None,
}
}
Some(f::Group(self.metadata.gid()))
}

/// This file’s size, if it’s a regular file.
Expand All @@ -314,6 +356,10 @@ impl<'dir> File<'dir> {
///
/// Block and character devices return their device IDs, because they
/// usually just have a file size of zero.
///
/// Links will return the size of their target (recursively through other
/// links) if dereferencing is enabled, otherwise the size of the link
/// itself.
pub fn size(&self) -> f::Size {
if self.is_directory() {
f::Size::None
Expand All @@ -330,18 +376,37 @@ impl<'dir> File<'dir> {
minor: device_ids[7],
})
}
else {
else if self.is_link() && self.deref_links {
match self.link_target() {
FileTarget::Ok(f) => f.size(),
_ => f::Size::None
}
} else {
f::Size::Some(self.metadata.len())
}
}

/// This file’s last modified timestamp, if available on this platform.
pub fn modified_time(&self) -> Option<SystemTime> {
self.metadata.modified().ok()
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => f.metadata.modified().ok(),
_ => None,
}
} else {
self.metadata.modified().ok()
}
}

/// This file’s last changed timestamp, if available on this platform.
pub fn changed_time(&self) -> Option<SystemTime> {
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.changed_time(),
_ => return None,
}
}

let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());

if sec < 0 {
Expand All @@ -361,12 +426,26 @@ impl<'dir> File<'dir> {

/// This file’s last accessed timestamp, if available on this platform.
pub fn accessed_time(&self) -> Option<SystemTime> {
self.metadata.accessed().ok()
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => f.metadata.accessed().ok(),
_ => None,
}
} else {
self.metadata.accessed().ok()
}
}

/// This file’s created timestamp, if available on this platform.
pub fn created_time(&self) -> Option<SystemTime> {
self.metadata.created().ok()
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => f.metadata.created().ok(),
_ => None,
}
} else {
self.metadata.created().ok()
}
}

/// This file’s ‘type’.
Expand Down Expand Up @@ -402,11 +481,21 @@ impl<'dir> File<'dir> {
}

/// This file’s permissions, with flags for each bit.
pub fn permissions(&self) -> f::Permissions {
pub fn permissions(&self) -> Option<f::Permissions> {
if self.is_link() && self.deref_links {
// If the chain of links is broken, we instead fall through and
// return the permissions of the original link, as would have been
// done if we were not dereferencing.
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.permissions(),
_ => return None,
}
}

let bits = self.metadata.mode();
let has_bit = |bit| bits & bit == bit;

f::Permissions {
Some(f::Permissions {
user_read: has_bit(modes::USER_READ),
user_write: has_bit(modes::USER_WRITE),
user_execute: has_bit(modes::USER_EXECUTE),
Expand All @@ -422,7 +511,7 @@ impl<'dir> File<'dir> {
sticky: has_bit(modes::STICKY),
setgid: has_bit(modes::SETGID),
setuid: has_bit(modes::SETUID),
}
})
}

/// Whether this file’s extension is any of the strings that get passed in.
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ impl<'args> Exa<'args> {
let mut exit_status = 0;

for file_path in &self.input_paths {
match File::from_args(PathBuf::from(file_path), None, None) {
match File::from_args(PathBuf::from(file_path), None, None, self.options.view.deref_links) {
Err(e) => {
exit_status = 2;
writeln!(io::stderr(), "{:?}: {}", file_path, e)?;
Expand Down Expand Up @@ -224,7 +224,7 @@ impl<'args> Exa<'args> {

let mut children = Vec::new();
let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore) {
for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore, self.options.view.deref_links) {
match file {
Ok(file) => children.push(file),
Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
Expand Down
17 changes: 9 additions & 8 deletions src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value
pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden };

// display options
pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden };
pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden };
pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden };
pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden };
pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden };
pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden };
pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };
pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden };
pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden };
pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden };
pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden };
pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden };
pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden };
pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };
pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden };

pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) };
pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) };
Expand Down Expand Up @@ -70,7 +71,7 @@ pub static OCTAL: Arg = Arg { short: None, long: "octal-permissions",
pub static ALL_ARGS: Args = Args(&[
&VERSION, &HELP,

&ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY,
&ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS,
&COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE,

&ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
Expand Down
3 changes: 2 additions & 1 deletion src/options/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ impl View {
let mode = Mode::deduce(matches, vars)?;
let width = TerminalWidth::deduce(vars)?;
let file_style = FileStyle::deduce(matches, vars)?;
Ok(Self { mode, width, file_style })
let deref_links = matches.has(&flags::DEREF_LINKS)?;
Ok(Self { mode, width, file_style, deref_links })
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/output/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ impl<'a> Render<'a> {
rows.push(row);

if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring) {
for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring, egg.file.deref_links) {
match file_to_add {
Ok(f) => {
files.push(f);
Expand Down
1 change: 1 addition & 0 deletions src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct View {
pub mode: Mode,
pub width: TerminalWidth,
pub file_style: file_name::Options,
pub deref_links: bool,
}


Expand Down
Loading