فهرست منبع

Merge pull request #32 from cafkafk/pr-1136

(exa PR) 1136: Fix #393: add flag to dereference links
Christina Sørensen 2 سال پیش
والد
کامیت
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
 - **-t**, **--time=(field)**: which timestamp field to use
 - **-u**, **--accessed**: use the accessed timestamp field
 - **-u**, **--accessed**: use the accessed timestamp field
 - **-U**, **--created**: use the created timestamp field
 - **-U**, **--created**: use the created timestamp field
+- **-X**, **--dereference**: dereference symlinks for file information
 - **-Z**, **--context**: list each file’s security context
 - **-Z**, **--context**: list each file’s security context
 - **-@**, **--extended**: list each file’s extended attributes and sizes
 - **-@**, **--extended**: list each file’s extended attributes and sizes
 - **--changed**: use the changed timestamp field
 - **--changed**: use the changed timestamp field

+ 50 - 49
completions/fish/eza.fish

@@ -1,35 +1,35 @@
 # Meta-stuff
 # 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
 # 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'
     always\t'Always use colour'
     auto\t'Use colour if standard output is a terminal'
     auto\t'Use colour if standard output is a terminal'
     never\t'Never use colour'
     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
 # 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'
     accessed\t'Sort by file accessed time'
     age\t'Sort by file modified time (newest first)'
     age\t'Sort by file modified time (newest first)'
     changed\t'Sort by changed time'
     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'
     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
 # 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'
     modified\t'Display modified time'
     changed\t'Display changed time'
     changed\t'Display changed time'
     accessed\t'Display accessed time'
     accessed\t'Display accessed time'
     created\t'Display created 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'
     default\t'Use the default time style'
     iso\t'Display brief ISO timestamps'
     iso\t'Display brief ISO timestamps'
     long-iso\t'Display longer ISO timestaps, up to the minute'
     long-iso\t'Display longer ISO timestaps, up to the minute'
     full-iso\t'Display full ISO timestamps, up to the nanosecond'
     full-iso\t'Display full ISO timestamps, up to the nanosecond'
     relative\t'Display relative timestamps'
     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
 # 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]" \
         --no-time"[Suppress the time field]" \
         {-u,--accessed}"[Use the accessed timestamp field]" \
         {-u,--accessed}"[Use the accessed timestamp field]" \
         {-U,--created}"[Use the created 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"[List each file's Git status, if tracked]" \
         --git-repos"[List each git-repos status and branch name]" \
         --git-repos"[List each git-repos status and branch name]" \
         --git-repos-no-status"[List each git-repos branch name (much faster)]" \
         --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
     /// Produce an iterator of IO results of trying to read all the files in
     /// this directory.
     /// 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 {
         Files {
             inner:     self.contents.iter(),
             inner:     self.contents.iter(),
             dir:       self,
             dir:       self,
@@ -55,6 +55,7 @@ impl Dir {
             dots:      dots.dots(),
             dots:      dots.dots(),
             git,
             git,
             git_ignoring,
             git_ignoring,
+            deref_links,
         }
         }
     }
     }
 
 
@@ -89,6 +90,9 @@ pub struct Files<'dir, 'ig> {
     git: Option<&'ig GitCache>,
     git: Option<&'ig GitCache>,
 
 
     git_ignoring: bool,
     git_ignoring: bool,
+
+    /// Whether symbolic links should be dereferenced when querying information.
+    deref_links: bool,
 }
 }
 
 
 impl<'dir, 'ig> Files<'dir, 'ig> {
 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)))
                                  .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.
     /// means that they should be skipped when recursing.
     pub is_all_all: bool,
     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.
     /// The extended attributes of this file.
     pub extended_attributes: Vec<Attribute>,
     pub extended_attributes: Vec<Attribute>,
 }
 }
 
 
 impl<'dir> File<'dir> {
 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>>,
     where PD: Into<Option<&'dir Dir>>,
           FN: Into<Option<String>>
           FN: Into<Option<String>>
     {
     {
@@ -87,7 +93,7 @@ impl<'dir> File<'dir> {
         let is_all_all = false;
         let is_all_all = false;
         let extended_attributes = File::gather_extended_attributes(&path);
         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>> {
     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 parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
         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>> {
     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 parent_dir = Some(parent_dir);
         let extended_attributes = File::gather_extended_attributes(&path);
         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
     /// 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 ext  = File::ext(&path);
                 let name = File::filename(&path);
                 let name = File::filename(&path);
                 let extended_attributes = File::gather_extended_attributes(&absolute_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))
                 FileTarget::Ok(Box::new(file))
             }
             }
             Err(e) => {
             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.
     /// This file’s number of hard links.
     ///
     ///
     /// It also reports whether this is both a regular file, and a file with
     /// 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.
     /// 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.
     /// 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
     /// Block and character devices return their device IDs, because they
     /// usually just have a file size of zero.
     /// 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)]
     #[cfg(unix)]
     pub fn size(&self) -> f::Size {
     pub fn size(&self) -> f::Size {
         if self.is_link() {
         if self.is_link() {
@@ -374,7 +417,12 @@ impl<'dir> File<'dir> {
                 minor: device_ids[7],
                 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())
             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.
     /// This file’s last modified timestamp, if available on this platform.
     pub fn modified_time(&self) -> Option<SystemTime> {
     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.
     /// This file’s last changed timestamp, if available on this platform.
     #[cfg(unix)]
     #[cfg(unix)]
     pub fn changed_time(&self) -> Option<SystemTime> {
     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());
         let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
 
 
         if sec < 0 {
         if sec < 0 {
@@ -421,12 +483,26 @@ impl<'dir> File<'dir> {
 
 
     /// This file’s last accessed timestamp, if available on this platform.
     /// This file’s last accessed timestamp, if available on this platform.
     pub fn accessed_time(&self) -> Option<SystemTime> {
     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.
     /// This file’s created timestamp, if available on this platform.
     pub fn created_time(&self) -> Option<SystemTime> {
     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’.
     /// This file’s ‘type’.
@@ -477,11 +553,20 @@ impl<'dir> File<'dir> {
 
 
     /// This file’s permissions, with flags for each bit.
     /// This file’s permissions, with flags for each bit.
     #[cfg(unix)]
     #[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 bits = self.metadata.mode();
         let has_bit = |bit| bits & bit == bit;
         let has_bit = |bit| bits & bit == bit;
 
 
-        f::Permissions {
+        Some(f::Permissions {
             user_read:      has_bit(modes::USER_READ),
             user_read:      has_bit(modes::USER_READ),
             user_write:     has_bit(modes::USER_WRITE),
             user_write:     has_bit(modes::USER_WRITE),
             user_execute:   has_bit(modes::USER_EXECUTE),
             user_execute:   has_bit(modes::USER_EXECUTE),
@@ -497,7 +582,7 @@ impl<'dir> File<'dir> {
             sticky:         has_bit(modes::STICKY),
             sticky:         has_bit(modes::STICKY),
             setgid:         has_bit(modes::SETGID),
             setgid:         has_bit(modes::SETGID),
             setuid:         has_bit(modes::SETUID),
             setuid:         has_bit(modes::SETUID),
-        }
+        })
     }
     }
 
 
     #[cfg(windows)]
     #[cfg(windows)]

+ 2 - 2
src/main.rs

@@ -177,7 +177,7 @@ impl<'args> Exa<'args> {
         let mut exit_status = 0;
         let mut exit_status = 0;
 
 
         for file_path in &self.input_paths {
         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) => {
                 Err(e) => {
                     exit_status = 2;
                     exit_status = 2;
                     writeln!(io::stderr(), "{:?}: {}", file_path, e)?;
                     writeln!(io::stderr(), "{:?}: {}", file_path, e)?;
@@ -230,7 +230,7 @@ impl<'args> Exa<'args> {
 
 
             let mut children = Vec::new();
             let mut children = Vec::new();
             let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
             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 {
                 match file {
                     Ok(file)        => children.push(file),
                     Ok(file)        => children.push(file),
                     Err((path, e))  => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
                     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 };
 pub static HELP:    Arg = Arg { short: Some(b'?'), long: "help",     takes_value: TakesValue::Forbidden };
 
 
 // display options
 // 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 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)) };
 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(&[
 pub static ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
     &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,
     &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE,
 
 
     &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
     &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 mode = Mode::deduce(matches, vars)?;
         let width = TerminalWidth::deduce(vars)?;
         let width = TerminalWidth::deduce(vars)?;
         let file_style = FileStyle::deduce(matches, 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);
             rows.push(row);
 
 
             if let Some(ref dir) = egg.dir {
             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 {
                     match file_to_add {
                         Ok(f) => {
                         Ok(f) => {
                             files.push(f);
                             files.push(f);

+ 1 - 0
src/output/mod.rs

@@ -22,6 +22,7 @@ pub struct View {
     pub mode: Mode,
     pub mode: Mode,
     pub width: TerminalWidth,
     pub width: TerminalWidth,
     pub file_style: file_name::Options,
     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::cell::TextCell;
 use crate::output::table::UserFormat;
 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;
         use users::os::unix::GroupExt;
 
 
         let mut style = colours.not_yours();
         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();
         let current_uid = users.get_current_uid();
         if let Some(current_user) = users.get_user_by_uid(current_uid) {
         if let Some(current_user) = users.get_user_by_uid(current_uid) {
 
 
@@ -40,13 +47,14 @@ impl f::Group {
 pub trait Colours {
 pub trait Colours {
     fn yours(&self) -> Style;
     fn yours(&self) -> Style;
     fn not_yours(&self) -> Style;
     fn not_yours(&self) -> Style;
+    fn no_group(&self) -> Style;
 }
 }
 
 
 
 
 #[cfg(test)]
 #[cfg(test)]
 #[allow(unused_results)]
 #[allow(unused_results)]
 pub mod test {
 pub mod test {
-    use super::Colours;
+    use super::{Colours, Render};
     use crate::fs::fields as f;
     use crate::fs::fields as f;
     use crate::output::cell::TextCell;
     use crate::output::cell::TextCell;
     use crate::output::table::UserFormat;
     use crate::output::table::UserFormat;
@@ -63,6 +71,7 @@ pub mod test {
     impl Colours for TestColours {
     impl Colours for TestColours {
         fn yours(&self)     -> Style { Fixed(80).normal() }
         fn yours(&self)     -> Style { Fixed(80).normal() }
         fn not_yours(&self) -> Style { Fixed(81).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);
         let mut users = MockUsers::with_current_uid(1000);
         users.add_group(Group::new(100, "folk"));
         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");
         let expected = TextCell::paint_str(Fixed(81).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
 
 
@@ -84,7 +93,7 @@ pub mod test {
     fn unnamed() {
     fn unnamed() {
         let users = MockUsers::with_current_uid(1000);
         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");
         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::Name));
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
         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_user(User::new(2, "eve", 100));
         users.add_group(Group::new(100, "folk"));
         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");
         let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
         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");
         let test_group = Group::new(100, "folk").add_member("eve");
         users.add_group(test_group);
         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");
         let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
         assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
     }
     }
 
 
     #[test]
     #[test]
     fn overflow() {
     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");
         let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648");
         assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
         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)]
 #[cfg(unix)]
 mod groups;
 mod groups;
 #[cfg(unix)]
 #[cfg(unix)]
-pub use self::groups::Colours as GroupColours;
+pub use self::groups::{Colours as GroupColours, Render as GroupRender};
 
 
 mod inode;
 mod inode;
 // inode uses just one colour
 // inode uses just one colour
@@ -19,7 +19,7 @@ mod links;
 pub use self::links::Colours as LinksColours;
 pub use self::links::Colours as LinksColours;
 
 
 mod permissions;
 mod permissions;
-pub use self::permissions::Colours as PermissionsColours;
+pub use self::permissions::{Colours as PermissionsColours, PermissionsPlusRender};
 
 
 mod size;
 mod size;
 pub use self::size::Colours as SizeColours;
 pub use self::size::Colours as SizeColours;
@@ -32,8 +32,10 @@ pub use self::times::Render as TimeRender;
 mod users;
 mod users;
 #[cfg(unix)]
 #[cfg(unix)]
 pub use self::users::Colours as UserColours;
 pub use self::users::Colours as UserColours;
+pub use self::users::Render as UserRender;
 
 
 mod octal;
 mod octal;
+pub use self::octal::Render as OctalPermissionsRender;
 // octal uses just one colour
 // octal uses just one colour
 
 
 mod securityctx;
 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::fs::fields as f;
 use crate::output::cell::TextCell;
 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 {
 impl f::OctalPermissions {
     fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
     fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
         u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x)
         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)]
 #[cfg(test)]
 pub mod test {
 pub mod test {
+    use super::Render;
     use crate::output::cell::TextCell;
     use crate::output::cell::TextCell;
     use crate::fs::fields as f;
     use crate::fs::fields as f;
 
 
@@ -37,7 +48,7 @@ pub mod test {
             other_read: true, other_write: false, other_execute: true, sticky: false,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "0755");
         assert_eq!(expected, octal.render(Purple.bold()));
         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,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "0644");
         assert_eq!(expected, octal.render(Purple.bold()));
         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,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "0600");
         assert_eq!(expected, octal.render(Purple.bold()));
         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,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "4777");
         assert_eq!(expected, octal.render(Purple.bold()));
         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,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "2777");
         assert_eq!(expected, octal.render(Purple.bold()));
         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,
             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");
         let expected = TextCell::paint_str(Purple.bold(), "1777");
         assert_eq!(expected, octal.render(Purple.bold()));
         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 ansi_term::{ANSIString, Style};
 
 
 use crate::fs::fields as f;
 use crate::fs::fields as f;
 use crate::output::cell::{TextCell, DisplayWidth};
 use crate::output::cell::{TextCell, DisplayWidth};
 use crate::output::render::FiletypeColours;
 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> {
     fn user_execute_bit<C: Colours>(&self, colours: &C, is_regular_file: bool) -> ANSIString<'static> {
         match (self.user_execute, self.setuid, is_regular_file) {
         match (self.user_execute, self.setuid, is_regular_file) {
             (false, false, _)      => colours.dash().paint("-"),
             (false, false, _)      => colours.dash().paint("-"),
@@ -143,7 +171,7 @@ pub trait Colours {
 #[cfg(test)]
 #[cfg(test)]
 #[allow(unused_results)]
 #[allow(unused_results)]
 pub mod test {
 pub mod test {
-    use super::Colours;
+    use super::{Colours, RenderPermissions};
     use crate::output::cell::TextCellContents;
     use crate::output::cell::TextCellContents;
     use crate::fs::fields as f;
     use crate::fs::fields as f;
 
 
@@ -173,11 +201,11 @@ pub mod test {
 
 
     #[test]
     #[test]
     fn negate() {
     fn negate() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: false,
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: false,
             group_read: false,  group_write: false,  group_execute: false,  setgid: false,
             group_read: false,  group_write: false,  group_execute: false,  setgid: false,
             other_read: false,  other_write: false,  other_execute: false,  sticky: false,
             other_read: false,  other_write: false,  other_execute: false,  sticky: false,
-        };
+        });
 
 
         let expected = TextCellContents::from(vec![
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(11).paint("-"),
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(11).paint("-"),
@@ -191,11 +219,11 @@ pub mod test {
 
 
     #[test]
     #[test]
     fn affirm() {
     fn affirm() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  true,  user_write:  true,  user_execute:  true,  setuid: false,
             user_read:  true,  user_write:  true,  user_execute:  true,  setuid: false,
             group_read: true,  group_write: true,  group_execute: true,  setgid: false,
             group_read: true,  group_write: true,  group_execute: true,  setgid: false,
             other_read: true,  other_write: true,  other_execute: true,  sticky: false,
             other_read: true,  other_write: true,  other_execute: true,  sticky: false,
-        };
+        });
 
 
         let expected = TextCellContents::from(vec![
         let expected = TextCellContents::from(vec![
             Fixed(101).paint("r"),  Fixed(102).paint("w"),  Fixed(103).paint("x"),
             Fixed(101).paint("r"),  Fixed(102).paint("w"),  Fixed(103).paint("x"),
@@ -209,11 +237,11 @@ pub mod test {
 
 
     #[test]
     #[test]
     fn specials() {
     fn specials() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  true,  setuid: true,
             user_read:  false,  user_write:  false,  user_execute:  true,  setuid: true,
             group_read: false,  group_write: false,  group_execute: true,  setgid: true,
             group_read: false,  group_write: false,  group_execute: true,  setgid: true,
             other_read: false,  other_write: false,  other_execute: true,  sticky: true,
             other_read: false,  other_write: false,  other_execute: true,  sticky: true,
-        };
+        });
 
 
         let expected = TextCellContents::from(vec![
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(110).paint("s"),
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(110).paint("s"),
@@ -227,11 +255,11 @@ pub mod test {
 
 
     #[test]
     #[test]
     fn extra_specials() {
     fn extra_specials() {
-        let bits = f::Permissions {
+        let bits = Some(f::Permissions {
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: true,
             user_read:  false,  user_write:  false,  user_execute:  false,  setuid: true,
             group_read: false,  group_write: false,  group_execute: false,  setgid: true,
             group_read: false,  group_write: false,  group_execute: false,  setgid: true,
             other_read: false,  other_write: false,  other_execute: false,  sticky: true,
             other_read: false,  other_write: false,  other_execute: false,  sticky: true,
-        };
+        });
 
 
         let expected = TextCellContents::from(vec![
         let expected = TextCellContents::from(vec![
             Fixed(11).paint("-"),  Fixed(11).paint("-"),  Fixed(111).paint("S"),
             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::cell::TextCell;
 use crate::output::table::UserFormat;
 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(),
             (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() };
                                                     else { colours.someone_else() };
         TextCell::paint(style, user_name)
         TextCell::paint(style, user_name)
     }
     }
@@ -24,13 +31,14 @@ impl f::User {
 pub trait Colours {
 pub trait Colours {
     fn you(&self) -> Style;
     fn you(&self) -> Style;
     fn someone_else(&self) -> Style;
     fn someone_else(&self) -> Style;
+    fn no_user(&self) -> Style;
 }
 }
 
 
 
 
 #[cfg(test)]
 #[cfg(test)]
 #[allow(unused_results)]
 #[allow(unused_results)]
 pub mod test {
 pub mod test {
-    use super::Colours;
+    use super::{Colours, Render};
     use crate::fs::fields as f;
     use crate::fs::fields as f;
     use crate::output::cell::TextCell;
     use crate::output::cell::TextCell;
     use crate::output::table::UserFormat;
     use crate::output::table::UserFormat;
@@ -46,6 +54,7 @@ pub mod test {
     impl Colours for TestColours {
     impl Colours for TestColours {
         fn you(&self)          -> Style { Red.bold() }
         fn you(&self)          -> Style { Red.bold() }
         fn someone_else(&self) -> Style { Blue.underline() }
         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);
         let mut users = MockUsers::with_current_uid(1000);
         users.add_user(User::new(1000, "enoch", 100));
         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");
         let expected = TextCell::paint_str(Red.bold(), "enoch");
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
 
 
@@ -66,7 +75,7 @@ pub mod test {
     fn unnamed() {
     fn unnamed() {
         let users = MockUsers::with_current_uid(1000);
         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");
         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::Name));
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
@@ -77,21 +86,21 @@ pub mod test {
         let mut users = MockUsers::with_current_uid(0);
         let mut users = MockUsers::with_current_uid(0);
         users.add_user(User::new(1000, "enoch", 100));
         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");
         let expected = TextCell::paint_str(Blue.underline(), "enoch");
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
         assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
     }
     }
 
 
     #[test]
     #[test]
     fn different_unnamed() {
     fn different_unnamed() {
-        let user = f::User(1000);
+        let user = Some(f::User(1000));
         let expected = TextCell::paint_str(Blue.underline(), "1000");
         let expected = TextCell::paint_str(Blue.underline(), "1000");
         assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
         assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
     }
     }
 
 
     #[test]
     #[test]
     fn overflow() {
     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");
         let expected = TextCell::paint_str(Blue.underline(), "2147483648");
         assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
         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::{File, fields as f};
 use crate::fs::feature::git::GitCache;
 use crate::fs::feature::git::GitCache;
 use crate::output::cell::TextCell;
 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::output::time::TimeFormat;
 use crate::theme::Theme;
 use crate::theme::Theme;
 
 
 
 
+
+
 /// Options for displaying a table.
 /// Options for displaying a table.
 #[derive(PartialEq, Eq, Debug)]
 #[derive(PartialEq, Eq, Debug)]
 pub struct Options {
 pub struct Options {
@@ -471,21 +479,27 @@ impl<'a, 'f> Table<'a> {
         self.widths.add_widths(row)
         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)]
     #[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 {
 impl render::GroupColours for Theme {
     fn yours(&self)      -> Style { self.ui.users.group_yours }
     fn yours(&self)      -> Style { self.ui.users.group_yours }
     fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
     fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
+    fn no_group(&self)   -> Style { self.ui.punctuation }
 }
 }
 
 
 impl render::LinksColours for Theme {
 impl render::LinksColours for Theme {
@@ -292,6 +293,7 @@ impl render::SizeColours for Theme {
 impl render::UserColours for Theme {
 impl render::UserColours for Theme {
     fn you(&self)           -> Style { self.ui.users.user_you }
     fn you(&self)           -> Style { self.ui.users.user_you }
     fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }
     fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }
+    fn no_user(&self)       -> Style { self.ui.punctuation }
 }
 }
 
 
 impl FileNameColours for Theme {
 impl FileNameColours for Theme {