Просмотр исходного кода

Merge pull request #32 from cafkafk/pr-1136

(exa PR) 1136: Fix #393: add flag to dereference links
Christina Sørensen 2 лет назад
Родитель
Сommit
5564772bdf

+ 1 - 0
README.md

@@ -137,6 +137,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
 - **-Z**, **--context**: list each file’s security context
 - **-@**, **--extended**: list each file’s extended attributes and sizes
 - **--changed**: use the changed timestamp field

+ 50 - 49
completions/fish/eza.fish

@@ -1,35 +1,35 @@
 # Meta-stuff
-complete -c eza -s 'v' -l 'version' -d "Show version of eza"
-complete -c eza -s '?' -l 'help'    -d "Show list of command-line options"
+complete -c eza -s v -l version -d "Show version of eza"
+complete -c eza -s '?' -l help -d "Show list of command-line options"
 
 # Display options
-complete -c eza -s '1' -l 'oneline'      -d "Display one entry per line"
-complete -c eza -s 'l' -l 'long'         -d "Display extended file metadata as a table"
-complete -c eza -s 'G' -l 'grid'         -d "Display entries in a grid"
-complete -c eza -s 'x' -l 'across'       -d "Sort the grid across, rather than downwards"
-complete -c eza -s 'R' -l 'recurse'      -d "Recurse into directories"
-complete -c eza -s 'T' -l 'tree'         -d "Recurse into directories as a tree"
-complete -c eza -s 'F' -l 'classify'     -d "Display type indicator by file names"
-complete -c eza        -l 'color' \
-                       -l 'colour'       -d "When to use terminal colours" -x -a "
+complete -c eza -s 1 -l oneline -d "Display one entry per line"
+complete -c eza -s l -l long -d "Display extended file metadata as a table"
+complete -c eza -s G -l grid -d "Display entries in a grid"
+complete -c eza -s x -l across -d "Sort the grid across, rather than downwards"
+complete -c eza -s R -l recurse -d "Recurse into directories"
+complete -c eza -s T -l tree -d "Recurse into directories as a tree"
+complete -c eza -s F -l classify -d "Display type indicator by file names"
+complete -c eza -l color \
+    -l colour -d "When to use terminal colours" -x -a "
     always\t'Always use colour'
     auto\t'Use colour if standard output is a terminal'
     never\t'Never use colour'
 "
-complete -c eza        -l 'color-scale' \
-                       -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
-complete -c eza        -l 'icons'        -d "Display icons"
-complete -c eza        -l 'no-icons'     -d "Don't display icons"
-complete -c eza        -l 'hyperlink'    -d "Display entries as hyperlinks"
+complete -c eza -l color-scale \
+    -l colour-scale -d "Highlight levels of file sizes distinctly"
+complete -c eza -l icons -d "Display icons"
+complete -c eza -l no-icons -d "Don't display icons"
+complete -c eza -l hyperlink -d "Display entries as hyperlinks"
 
 # Filtering and sorting options
-complete -c eza -l 'group-directories-first' -d "Sort directories before other files"
-complete -c eza -l 'git-ignore'           -d "Ignore files mentioned in '.gitignore'"
-complete -c eza -s 'a' -l 'all'       -d "Show hidden and 'dot' files"
-complete -c eza -s 'd' -l 'list-dirs' -d "List directories like regular files"
-complete -c eza -s 'L' -l 'level'     -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
-complete -c eza -s 'r' -l 'reverse'   -d "Reverse the sort order"
-complete -c eza -s 's' -l 'sort'      -d "Which field to sort by" -x -a "
+complete -c eza -l group-directories-first -d "Sort directories before other files"
+complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'"
+complete -c eza -s a -l all -d "Show hidden and 'dot' files"
+complete -c eza -s d -l list-dirs -d "List directories like regular files"
+complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
+complete -c eza -s r -l reverse -d "Reverse the sort order"
+complete -c eza -s s -l sort -d "Which field to sort by" -x -a "
     accessed\t'Sort by file accessed time'
     age\t'Sort by file modified time (newest first)'
     changed\t'Sort by changed time'
@@ -53,44 +53,45 @@ complete -c eza -s 's' -l 'sort'      -d "Which field to sort by" -x -a "
     type\t'Sort by file type'
 "
 
-complete -c eza -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r
-complete -c eza -s 'D' -l 'only-dirs'   -d "List only directories"
+complete -c eza -s I -l ignore-glob -d "Ignore files that match these glob patterns" -r
+complete -c eza -s D -l only-dirs -d "List only directories"
 
 # Long view options
-complete -c eza -s 'b' -l 'binary'   -d "List file sizes with binary prefixes"
-complete -c eza -s 'B' -l 'bytes'    -d "List file sizes in bytes, without any prefixes"
-complete -c eza -s 'g' -l 'group'    -d "List each file's group"
-complete -c eza -s 'h' -l 'header'   -d "Add a header row to each column"
-complete -c eza -s 'H' -l 'links'    -d "List each file's number of hard links"
-complete -c eza -s 'i' -l 'inode'    -d "List each file's inode number"
-complete -c eza -s 'S' -l 'blocks'   -d "List each file's number of filesystem blocks"
-complete -c eza -s 't' -l 'time'     -d "Which timestamp field to list" -x -a "
+complete -c eza -s b -l binary -d "List file sizes with binary prefixes"
+complete -c eza -s B -l bytes -d "List file sizes in bytes, without any prefixes"
+complete -c eza -s g -l group -d "List each file's group"
+complete -c eza -s h -l header -d "Add a header row to each column"
+complete -c eza -s H -l links -d "List each file's number of hard links"
+complete -c eza -s i -l inode -d "List each file's inode number"
+complete -c eza -s S -l blocks -d "List each file's number of filesystem blocks"
+complete -c eza -s t -l time -d "Which timestamp field to list" -x -a "
     modified\t'Display modified time'
     changed\t'Display changed time'
     accessed\t'Display accessed time'
     created\t'Display created time'
 "
-complete -c eza -s 'm' -l 'modified'      -d "Use the modified timestamp field"
-complete -c eza -s 'n' -l 'numeric'       -d "List numeric user and group IDs."
-complete -c eza        -l 'changed'       -d "Use the changed timestamp field"
-complete -c eza -s 'u' -l 'accessed'      -d "Use the accessed timestamp field"
-complete -c eza -s 'U' -l 'created'       -d "Use the created timestamp field"
-complete -c eza        -l 'time-style'    -d "How to format timestamps" -x -a "
+complete -c exa -s X -l dereference -d "dereference symlinks for file information"
+complete -c eza -s m -l modified -d "Use the modified timestamp field"
+complete -c eza -s n -l numeric -d "List numeric user and group IDs."
+complete -c eza -l changed -d "Use the changed timestamp field"
+complete -c eza -s u -l accessed -d "Use the accessed timestamp field"
+complete -c eza -s U -l created -d "Use the created timestamp field"
+complete -c eza -l time-style -d "How to format timestamps" -x -a "
     default\t'Use the default time style'
     iso\t'Display brief ISO timestamps'
     long-iso\t'Display longer ISO timestaps, up to the minute'
     full-iso\t'Display full ISO timestamps, up to the nanosecond'
     relative\t'Display relative timestamps'
 "
-complete -c eza        -l 'no-permissions'    -d "Suppress the permissions field"
-complete -c eza -s 'o' -l 'octal-permissions' -d "List each file's permission in octal format"
-complete -c eza        -l 'no-filesize'       -d "Suppress the filesize field"
-complete -c eza        -l 'no-user'           -d "Suppress the user field"
-complete -c eza        -l 'no-time'           -d "Suppress the time field"
+complete -c eza -l no-permissions -d "Suppress the permissions field"
+complete -c eza -s o -l octal-permissions -d "List each file's permission in octal format"
+complete -c eza -l no-filesize -d "Suppress the filesize field"
+complete -c eza -l no-user -d "Suppress the user field"
+complete -c eza -l no-time -d "Suppress the time field"
 
 # Optional extras
-complete -c eza -l 'git'                 -d "List each file's Git status, if tracked"
-complete -c eza -l 'git-repos'           -d "List each git-repos status and branch name"
-complete -c eza -l 'git-repos-no-status' -d "List each git-repos branch name (much faster)"
-complete -c eza -s '@' -l 'extended'     -d "List each file's extended attributes and sizes"
-complete -c eza -s 'Z' -l 'context'      -d "List each file's security context"
+complete -c eza -l git -d "List each file's Git status, if tracked"
+complete -c eza -l git-repos -d "List each git-repos status and branch name"
+complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)"
+complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes"
+complete -c eza -s Z -l context -d "List each file's security context"

+ 1 - 0
completions/zsh/_eza

@@ -52,6 +52,7 @@ __eza() {
         --no-time"[Suppress the time field]" \
         {-u,--accessed}"[Use the accessed timestamp field]" \
         {-U,--created}"[Use the created timestamp field]" \
+        {-X,--dereference}"[dereference symlinks for file information]" \
         --git"[List each file's Git status, if tracked]" \
         --git-repos"[List each git-repos status and branch name]" \
         --git-repos-no-status"[List each git-repos branch name (much faster)]" \

+ 6 - 2
src/fs/dir.rs

@@ -47,7 +47,7 @@ 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,
@@ -55,6 +55,7 @@ impl Dir {
             dots:      dots.dots(),
             git,
             git_ignoring,
+            deref_links,
         }
     }
 
@@ -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> {
@@ -125,7 +129,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)))
             }
 

+ 104 - 19
src/fs/file.rs

@@ -69,12 +69,18 @@ pub struct File<'dir> {
     /// 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,
     /// The extended attributes of this file.
     pub extended_attributes: Vec<Attribute>,
 }
 
 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>>
     {
@@ -87,7 +93,7 @@ impl<'dir> File<'dir> {
         let is_all_all = false;
         let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, extended_attributes })
+        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links, extended_attributes })
     }
 
     pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -100,7 +106,7 @@ impl<'dir> File<'dir> {
         let parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, extended_attributes })
+        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false, extended_attributes })
     }
 
     pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -112,7 +118,7 @@ impl<'dir> File<'dir> {
         let parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, extended_attributes })
+        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false, extended_attributes })
     }
 
     /// A file’s name is derived from its string. This needs to handle directories
@@ -285,7 +291,7 @@ impl<'dir> File<'dir> {
                 let ext  = File::ext(&path);
                 let name = File::filename(&path);
                 let extended_attributes = File::gather_extended_attributes(&absolute_path);
-                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, extended_attributes };
+                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, deref_links: self.deref_links, extended_attributes };
                 FileTarget::Ok(Box::new(file))
             }
             Err(e) => {
@@ -295,6 +301,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
@@ -331,16 +359,27 @@ impl<'dir> File<'dir> {
         }
     }
 
-    /// 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 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.
-    #[cfg(unix)]
-    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.
@@ -351,6 +390,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.
     #[cfg(unix)]
     pub fn size(&self) -> f::Size {
         if self.is_link() {
@@ -374,7 +417,12 @@ 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())
         }
     }
@@ -391,12 +439,26 @@ impl<'dir> File<'dir> {
 
     /// 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.
     #[cfg(unix)]
     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 {
@@ -421,12 +483,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’.
@@ -477,11 +553,20 @@ impl<'dir> File<'dir> {
 
     /// This file’s permissions, with flags for each bit.
     #[cfg(unix)]
-    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),
@@ -497,7 +582,7 @@ impl<'dir> File<'dir> {
             sticky:         has_bit(modes::STICKY),
             setgid:         has_bit(modes::SETGID),
             setuid:         has_bit(modes::SETUID),
-        }
+        })
     }
 
     #[cfg(windows)]

+ 2 - 2
src/main.rs

@@ -177,7 +177,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)?;
@@ -230,7 +230,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)?,

+ 9 - 8
src/options/flags.rs

@@ -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)) };
@@ -75,7 +76,7 @@ pub static SECURITY_CONTEXT:  Arg = Arg { short: Some(b'Z'), long: "context",
 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, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,

+ 2 - 1
src/options/view.rs

@@ -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 })
     }
 }
 

+ 1 - 1
src/output/details.rs

@@ -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);

+ 1 - 0
src/output/mod.rs

@@ -22,6 +22,7 @@ pub struct View {
     pub mode: Mode,
     pub width: TerminalWidth,
     pub file_style: file_name::Options,
+    pub deref_links: bool,
 }
 
 

+ 20 - 11
src/output/render/groups.rs

@@ -5,18 +5,25 @@ use crate::fs::fields as f;
 use crate::output::cell::TextCell;
 use crate::output::table::UserFormat;
 
+pub trait Render{
+    fn render<C: Colours, U: Users+Groups>(self, colours: &C, users: &U, format: UserFormat) -> TextCell;
+}
 
-impl f::Group {
-    pub fn render<C: Colours, U: Users+Groups>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
+impl Render for Option<f::Group> {
+    fn render<C: Colours, U: Users+Groups>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
         use users::os::unix::GroupExt;
 
         let mut style = colours.not_yours();
 
-        let group = match users.get_group_by_gid(self.0) {
-            Some(g)  => (*g).clone(),
-            None     => return TextCell::paint(style, self.0.to_string()),
+        let group = match self {
+            Some(g) => match users.get_group_by_gid(g.0) {
+                Some(g) => (*g).clone(),
+                None    => return TextCell::paint(style, g.0.to_string()),
+            },
+            None => return TextCell::blank(colours.no_group()),
         };
 
+
         let current_uid = users.get_current_uid();
         if let Some(current_user) = users.get_user_by_uid(current_uid) {
 
@@ -40,13 +47,14 @@ impl f::Group {
 pub trait Colours {
     fn yours(&self) -> Style;
     fn not_yours(&self) -> Style;
+    fn no_group(&self) -> Style;
 }
 
 
 #[cfg(test)]
 #[allow(unused_results)]
 pub mod test {
-    use super::Colours;
+    use super::{Colours, Render};
     use crate::fs::fields as f;
     use crate::output::cell::TextCell;
     use crate::output::table::UserFormat;
@@ -63,6 +71,7 @@ pub mod test {
     impl Colours for TestColours {
         fn yours(&self)     -> Style { Fixed(80).normal() }
         fn not_yours(&self) -> Style { Fixed(81).normal() }
+        fn no_group(&self)   -> Style { Black.italic() }
     }
 
 
@@ -71,7 +80,7 @@ pub mod test {
         let mut users = MockUsers::with_current_uid(1000);
         users.add_group(Group::new(100, "folk"));
 
-        let group = f::Group(100);
+        let group = Some(f::Group(100));
         let expected = TextCell::paint_str(Fixed(81).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
 
@@ -84,7 +93,7 @@ pub mod test {
     fn unnamed() {
         let users = MockUsers::with_current_uid(1000);
 
-        let group = f::Group(100);
+        let group = Some(f::Group(100));
         let expected = TextCell::paint_str(Fixed(81).normal(), "100");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
@@ -96,7 +105,7 @@ pub mod test {
         users.add_user(User::new(2, "eve", 100));
         users.add_group(Group::new(100, "folk"));
 
-        let group = f::Group(100);
+        let group = Some(f::Group(100));
         let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
     }
@@ -109,14 +118,14 @@ pub mod test {
         let test_group = Group::new(100, "folk").add_member("eve");
         users.add_group(test_group);
 
-        let group = f::Group(100);
+        let group = Some(f::Group(100));
         let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
     }
 
     #[test]
     fn overflow() {
-        let group = f::Group(2_147_483_648);
+        let group = Some(f::Group(2_147_483_648));
         let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648");
         assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
     }

+ 4 - 2
src/output/render/mod.rs

@@ -10,7 +10,7 @@ pub use self::git::Colours as GitColours;
 #[cfg(unix)]
 mod groups;
 #[cfg(unix)]
-pub use self::groups::Colours as GroupColours;
+pub use self::groups::{Colours as GroupColours, Render as GroupRender};
 
 mod inode;
 // inode uses just one colour
@@ -19,7 +19,7 @@ mod links;
 pub use self::links::Colours as LinksColours;
 
 mod permissions;
-pub use self::permissions::Colours as PermissionsColours;
+pub use self::permissions::{Colours as PermissionsColours, PermissionsPlusRender};
 
 mod size;
 pub use self::size::Colours as SizeColours;
@@ -32,8 +32,10 @@ pub use self::times::Render as TimeRender;
 mod users;
 #[cfg(unix)]
 pub use self::users::Colours as UserColours;
+pub use self::users::Render as UserRender;
 
 mod octal;
+pub use self::octal::Render as OctalPermissionsRender;
 // octal uses just one colour
 
 mod securityctx;

+ 27 - 16
src/output/render/octal.rs

@@ -3,26 +3,37 @@ use ansi_term::Style;
 use crate::fs::fields as f;
 use crate::output::cell::TextCell;
 
+pub trait Render {
+    fn render(&self, style: Style) -> TextCell;
+}
+
+impl Render for Option<f::OctalPermissions> {
+    fn render(&self, style: Style) -> TextCell {
+        match self {
+            Some(p) => {
+                let perm = &p.permissions;
+                let octal_sticky = f::OctalPermissions::bits_to_octal(perm.setuid, perm.setgid, perm.sticky);
+                let octal_owner  = f::OctalPermissions::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute);
+                let octal_group  = f::OctalPermissions::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute);
+                let octal_other  = f::OctalPermissions::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute);
+
+                TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other))
+            },
+            None => TextCell::paint(style, "----".into())
+        }
+    }
+}
 
 impl f::OctalPermissions {
     fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
         u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x)
     }
-
-    pub fn render(&self, style: Style) -> TextCell {
-        let perm = &self.permissions;
-        let octal_sticky = Self::bits_to_octal(perm.setuid, perm.setgid, perm.sticky);
-        let octal_owner  = Self::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute);
-        let octal_group  = Self::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute);
-        let octal_other  = Self::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute);
-
-        TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other))
-    }
 }
 
 
 #[cfg(test)]
 pub mod test {
+    use super::Render;
     use crate::output::cell::TextCell;
     use crate::fs::fields as f;
 
@@ -37,7 +48,7 @@ pub mod test {
             other_read: true, other_write: false, other_execute: true, sticky: false,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "0755");
         assert_eq!(expected, octal.render(Purple.bold()));
@@ -51,7 +62,7 @@ pub mod test {
             other_read: true, other_write: false, other_execute: false, sticky: false,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "0644");
         assert_eq!(expected, octal.render(Purple.bold()));
@@ -65,7 +76,7 @@ pub mod test {
             other_read: false, other_write: false, other_execute: false, sticky: false,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "0600");
         assert_eq!(expected, octal.render(Purple.bold()));
@@ -79,7 +90,7 @@ pub mod test {
             other_read: true, other_write: true,  other_execute: true, sticky: false,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "4777");
         assert_eq!(expected, octal.render(Purple.bold()));
@@ -94,7 +105,7 @@ pub mod test {
             other_read: true, other_write: true,  other_execute: true, sticky: false,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "2777");
         assert_eq!(expected, octal.render(Purple.bold()));
@@ -108,7 +119,7 @@ pub mod test {
             other_read: true, other_write: true,  other_execute: true, sticky: true,
         };
 
-        let octal = f::OctalPermissions{ permissions: bits };
+        let octal = Some(f::OctalPermissions{ permissions: bits });
 
         let expected = TextCell::paint_str(Purple.bold(), "1777");
         assert_eq!(expected, octal.render(Purple.bold()));

+ 72 - 44
src/output/render/permissions.rs

@@ -1,26 +1,43 @@
+use std::iter;
+
 use ansi_term::{ANSIString, Style};
 
 use crate::fs::fields as f;
 use crate::output::cell::{TextCell, DisplayWidth};
 use crate::output::render::FiletypeColours;
 
+pub trait PermissionsPlusRender {
+    fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell;
+}
 
-impl f::PermissionsPlus {
-    #[cfg(unix)]
-    pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
-        let mut chars = vec![ self.file_type.render(colours) ];
-        chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
-
-        if self.xattrs {
-           chars.push(colours.attribute().paint("@"));
-        }
-
-        // As these are all ASCII characters, we can guarantee that they’re
-        // all going to be one character wide, and don’t need to compute the
-        // cell’s display width.
-        TextCell {
-            width:    DisplayWidth::from(chars.len()),
-            contents: chars.into(),
+#[cfg(unix)]
+impl PermissionsPlusRender for Option<f::PermissionsPlus> {
+    fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
+        match self {
+            Some(p) => {
+                let mut chars = vec![ p.file_type.render(colours) ];
+                let permissions = p.permissions;
+                chars.extend(Some(permissions).render(colours, p.file_type.is_regular_file()));
+
+                if p.xattrs {
+                   chars.push(colours.attribute().paint("@"));
+                }
+
+                // As these are all ASCII characters, we can guarantee that they’re
+                // all going to be one character wide, and don’t need to compute the
+                // cell’s display width.
+                TextCell {
+                    width:    DisplayWidth::from(chars.len()),
+                    contents: chars.into(),
+                }
+            },
+            None => {
+                let chars: Vec<_> = iter::repeat(colours.dash().paint("-")).take(10).collect();
+                TextCell {
+                    width:    DisplayWidth::from(chars.len()),
+                    contents: chars.into(),
+                }
+            }
         }
     }
 
@@ -36,28 +53,39 @@ impl f::PermissionsPlus {
     }
 }
 
+pub trait RenderPermissions {
+    fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>>;
+}
 
-impl f::Permissions {
-    pub fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>> {
-
-        let bit = |bit, chr: &'static str, style: Style| {
-            if bit { style.paint(chr) }
-              else { colours.dash().paint("-") }
-        };
-
-        vec![
-            bit(self.user_read,   "r", colours.user_read()),
-            bit(self.user_write,  "w", colours.user_write()),
-            self.user_execute_bit(colours, is_regular_file),
-            bit(self.group_read,  "r", colours.group_read()),
-            bit(self.group_write, "w", colours.group_write()),
-            self.group_execute_bit(colours),
-            bit(self.other_read,  "r", colours.other_read()),
-            bit(self.other_write, "w", colours.other_write()),
-            self.other_execute_bit(colours)
-        ]
+impl RenderPermissions for Option<f::Permissions> {
+    fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>> {
+        match self {
+            Some(p) => {
+                let bit = |bit, chr: &'static str, style: Style| {
+                    if bit { style.paint(chr) }
+                      else { colours.dash().paint("-") }
+                };
+
+                vec![
+                    bit(p.user_read,   "r", colours.user_read()),
+                    bit(p.user_write,  "w", colours.user_write()),
+                    p.user_execute_bit(colours, is_regular_file),
+                    bit(p.group_read,  "r", colours.group_read()),
+                    bit(p.group_write, "w", colours.group_write()),
+                    p.group_execute_bit(colours),
+                    bit(p.other_read,  "r", colours.other_read()),
+                    bit(p.other_write, "w", colours.other_write()),
+                    p.other_execute_bit(colours)
+                ]
+            },
+            None => {
+                iter::repeat(colours.dash().paint("-")).take(9).collect()
+            }
+        }
     }
+}
 
+impl f::Permissions {
     fn user_execute_bit<C: Colours>(&self, colours: &C, is_regular_file: bool) -> ANSIString<'static> {
         match (self.user_execute, self.setuid, is_regular_file) {
             (false, false, _)      => colours.dash().paint("-"),
@@ -143,7 +171,7 @@ pub trait Colours {
 #[cfg(test)]
 #[allow(unused_results)]
 pub mod test {
-    use super::Colours;
+    use super::{Colours, RenderPermissions};
     use crate::output::cell::TextCellContents;
     use crate::fs::fields as f;
 
@@ -173,11 +201,11 @@ pub mod test {
 
     #[test]
     fn negate() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: false,
             group_read: false,  group_write: false,  group_execute: false,  setgid: false,
             other_read: false,  other_write: false,  other_execute: false,  sticky: false,
-        };
+        });
 
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(11).paint("-"),
@@ -191,11 +219,11 @@ pub mod test {
 
     #[test]
     fn affirm() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  true,  user_write:  true,  user_execute:  true,  setuid: false,
             group_read: true,  group_write: true,  group_execute: true,  setgid: false,
             other_read: true,  other_write: true,  other_execute: true,  sticky: false,
-        };
+        });
 
         let expected = TextCellContents::from(vec![
             Fixed(101).paint("r"),  Fixed(102).paint("w"),  Fixed(103).paint("x"),
@@ -209,11 +237,11 @@ pub mod test {
 
     #[test]
     fn specials() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  true,  setuid: true,
             group_read: false,  group_write: false,  group_execute: true,  setgid: true,
             other_read: false,  other_write: false,  other_execute: true,  sticky: true,
-        };
+        });
 
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(110).paint("s"),
@@ -227,11 +255,11 @@ pub mod test {
 
     #[test]
     fn extra_specials() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: true,
             group_read: false,  group_write: false,  group_execute: false,  setgid: true,
             other_read: false,  other_write: false,  other_execute: false,  sticky: true,
-        };
+        });
 
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(111).paint("S"),

+ 21 - 12
src/output/render/users.rs

@@ -5,16 +5,23 @@ use crate::fs::fields as f;
 use crate::output::cell::TextCell;
 use crate::output::table::UserFormat;
 
+pub trait Render {
+    fn render<C: Colours, U: Users>(self, colours: &C, users: &U, format: UserFormat) -> TextCell;
+}
 
-impl f::User {
-    pub fn render<C: Colours, U: Users>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
-        let user_name = match (format, users.get_user_by_uid(self.0)) {
-            (_, None)                      => self.0.to_string(),
-            (UserFormat::Numeric, _)       => self.0.to_string(),
+impl Render for Option<f::User> {
+    fn render<C: Colours, U: Users>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
+        let uid = match self {
+            Some(u) => u.0,
+            None    => return TextCell::blank(colours.no_user()),
+        };
+        let user_name = match (format, users.get_user_by_uid(uid)) {
+            (_, None)                      => uid.to_string(),
+            (UserFormat::Numeric, _)       => uid.to_string(),
             (UserFormat::Name, Some(user)) => user.name().to_string_lossy().into(),
         };
 
-        let style = if users.get_current_uid() == self.0 { colours.you() }
+        let style = if users.get_current_uid() == uid { colours.you() }
                                                     else { colours.someone_else() };
         TextCell::paint(style, user_name)
     }
@@ -24,13 +31,14 @@ impl f::User {
 pub trait Colours {
     fn you(&self) -> Style;
     fn someone_else(&self) -> Style;
+    fn no_user(&self) -> Style;
 }
 
 
 #[cfg(test)]
 #[allow(unused_results)]
 pub mod test {
-    use super::Colours;
+    use super::{Colours, Render};
     use crate::fs::fields as f;
     use crate::output::cell::TextCell;
     use crate::output::table::UserFormat;
@@ -46,6 +54,7 @@ pub mod test {
     impl Colours for TestColours {
         fn you(&self)          -> Style { Red.bold() }
         fn someone_else(&self) -> Style { Blue.underline() }
+        fn no_user(&self)      -> Style { Black.italic() }
     }
 
 
@@ -54,7 +63,7 @@ pub mod test {
         let mut users = MockUsers::with_current_uid(1000);
         users.add_user(User::new(1000, "enoch", 100));
 
-        let user = f::User(1000);
+        let user = Some(f::User(1000));
         let expected = TextCell::paint_str(Red.bold(), "enoch");
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
 
@@ -66,7 +75,7 @@ pub mod test {
     fn unnamed() {
         let users = MockUsers::with_current_uid(1000);
 
-        let user = f::User(1000);
+        let user = Some(f::User(1000));
         let expected = TextCell::paint_str(Red.bold(), "1000");
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
@@ -77,21 +86,21 @@ pub mod test {
         let mut users = MockUsers::with_current_uid(0);
         users.add_user(User::new(1000, "enoch", 100));
 
-        let user = f::User(1000);
+        let user = Some(f::User(1000));
         let expected = TextCell::paint_str(Blue.underline(), "enoch");
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
     }
 
     #[test]
     fn different_unnamed() {
-        let user = f::User(1000);
+        let user = Some(f::User(1000));
         let expected = TextCell::paint_str(Blue.underline(), "1000");
         assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
     }
 
     #[test]
     fn overflow() {
-        let user = f::User(2_147_483_648);
+        let user = Some(f::User(2_147_483_648));
         let expected = TextCell::paint_str(Blue.underline(), "2147483648");
         assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
     }

+ 26 - 12
src/output/table.rs

@@ -15,11 +15,19 @@ use users::UsersCache;
 use crate::fs::{File, fields as f};
 use crate::fs::feature::git::GitCache;
 use crate::output::cell::TextCell;
-use crate::output::render::TimeRender;
+use crate::output::render::{
+    GroupRender,
+    OctalPermissionsRender,
+    PermissionsPlusRender,
+    TimeRender,
+    UserRender
+};
 use crate::output::time::TimeFormat;
 use crate::theme::Theme;
 
 
+
+
 /// Options for displaying a table.
 #[derive(PartialEq, Eq, Debug)]
 pub struct Options {
@@ -471,21 +479,27 @@ impl<'a, 'f> Table<'a> {
         self.widths.add_widths(row)
     }
 
-    fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus {
-        f::PermissionsPlus {
-            file_type: file.type_char(),
-            #[cfg(unix)]
-            permissions: file.permissions(),
-            #[cfg(windows)]
-            attributes: file.attributes(),
-            xattrs,
+    fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option<f::PermissionsPlus> {
+        match file.permissions() {
+            Some(p) => Some(f::PermissionsPlus {
+                file_type: file.type_char(),
+                #[cfg(unix)]
+                permissions: p,
+                #[cfg(windows)]
+                attributes: file.attributes(),
+                xattrs
+            }),
+            None => None,
         }
     }
 
     #[cfg(unix)]
-    fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions {
-        f::OctalPermissions {
-            permissions: file.permissions(),
+    fn octal_permissions(&self, file: &File<'_>) -> Option<f::OctalPermissions> {
+        match file.permissions() {
+            Some(p) => Some(f::OctalPermissions {
+                permissions: p,
+            }),
+            None => None,
         }
     }
 

+ 2 - 0
src/theme/mod.rs

@@ -233,6 +233,7 @@ impl render::GitColours for Theme {
 impl render::GroupColours for Theme {
     fn yours(&self)      -> Style { self.ui.users.group_yours }
     fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
+    fn no_group(&self)   -> Style { self.ui.punctuation }
 }
 
 impl render::LinksColours for Theme {
@@ -292,6 +293,7 @@ impl render::SizeColours for Theme {
 impl render::UserColours for Theme {
     fn you(&self)           -> Style { self.ui.users.user_you }
     fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }
+    fn no_user(&self)       -> Style { self.ui.punctuation }
 }
 
 impl FileNameColours for Theme {