ソースを参照

Merge branch 'almost-all'

This merge adds support for `--all --all`, which displays the `.` and `..` directories like how ls does it.

Doing this was harder than it seemed — exa wasn’t filtering these directories out already, they just weren’t being included! So instead of just including them again, there had to be quite a lot of internal restructuring (yes, again) in order for there to be Files like `..` that don’t have the same name in the list as they do on the filesystem.

Fixes #155.
Benjamin Sago 8 年 前
コミット
5cd7609034

+ 3 - 1
README.md

@@ -24,7 +24,7 @@ exa’s options are similar, but not exactly the same, as `ls`.
 
 ### Filtering Options
 
-- **-a**, **--all**: don't hide hidden and 'dot' files
+- **-a**, **--all**: show hidden and 'dot' files
 - **-d**, **--list-dirs**: list directories like regular files
 - **-L**, **--level=(depth)**: limit the depth of recursion
 - **-r**, **--reverse**: reverse the sort order
@@ -32,6 +32,8 @@ exa’s options are similar, but not exactly the same, as `ls`.
 - **--group-directories-first**: list directories before other files
 - **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore
 
+Pass the `--all` option twice to also show the `.` and `..` directories.
+
 ### Long View Options
 
 These options are available when running with --long (`-l`):

+ 30 - 0
Vagrantfile

@@ -369,6 +369,36 @@ Vagrant.configure(2) do |config|
     EOF
 
 
+    # Hidden and dot file testcases.
+    # We need to set the permissions of `.` and `..` because they actually
+    # get displayed in the output here, so this has to come last.
+    config.vm.provision :shell, privileged: false, inline: <<-EOF
+        set -xe
+        shopt -u dotglob
+        GLOBIGNORE=".:.."
+
+        mkdir "#{test_dir}/hiddens"
+        touch "#{test_dir}/hiddens/visible"
+        touch "#{test_dir}/hiddens/.hidden"
+        touch "#{test_dir}/hiddens/..extra-hidden"
+
+        # ./hiddens/
+        touch -t #{some_date}      "#{test_dir}/hiddens/"*
+        chmod 644                  "#{test_dir}/hiddens/"*
+        sudo chown #{user}:#{user} "#{test_dir}/hiddens/"*
+
+        # .
+        touch -t #{some_date} "#{test_dir}/hiddens"
+        chmod 755 "#{test_dir}/hiddens"
+        sudo chown #{user}:#{user} "#{test_dir}/hiddens"
+
+        # ..
+        sudo touch -t #{some_date} "#{test_dir}"
+        sudo chmod 755 "#{test_dir}"
+        sudo chown #{user}:#{user} "#{test_dir}"
+    EOF
+
+
     # Install kcov for test coverage
     # This doesn’t run coverage over the xtests so it’s less useful for now
     if ENV.key?('INSTALL_KCOV')

+ 1 - 1
contrib/completions.fish

@@ -17,7 +17,7 @@ complete -c exa        -l 'colour-scale' -d "Highlight levels of file sizes dist
 
 # Filtering and sorting options
 complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
-complete -c exa -s 'a' -l 'all'       -d "Don't hide hidden and 'dot' files"
+complete -c exa -s 'a' -l 'all'       -d "Show and 'dot' files"
 complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
 complete -c exa -s 'L' -l 'level'     -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9"
 complete -c exa -s 'r' -l 'reverse'   -d "Reverse the sort order"

+ 1 - 1
contrib/completions.zsh

@@ -14,7 +14,7 @@ __exa() {
         {--color,--colour}"[When to use terminal colours]" \
         {--color,--colour}-scale"[Highlight levels of file sizes distinctly]" \
         --group-directories-first"[Sort directories before other files]" \
-        {-a,--all}"[Don't hide hidden and 'dot' files]" \
+        {-a,--all}"[Show hidden and 'dot' files]" \
         {-d,--list-dirs}"[List directories like regular files]" \
         {-L,--level}"+[Limit the depth of recursion]" \
         {-r,--reverse}"[Reverse the sort order]" \

+ 2 - 1
contrib/man/exa.1

@@ -59,7 +59,8 @@ highlight levels of file sizes distinctly
 .SH FILTERING AND SORTING OPTIONS
 .TP
 .B \-a, \-\-all
-don\[aq]t hide hidden and \[aq]dot\[aq] files
+show hidden and \[aq]dot\[aq] files.
+Use this twice to also show the \f[C].\f[] and \f[C]..\f[] directories.
 .RS
 .RE
 .TP

+ 3 - 3
src/exa.rs

@@ -24,7 +24,7 @@ extern crate lazy_static;
 
 use std::ffi::OsStr;
 use std::io::{stderr, Write, Result as IOResult};
-use std::path::{Component, Path};
+use std::path::{Component, PathBuf};
 
 use ansi_term::{ANSIStrings, Style};
 
@@ -75,7 +75,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
         }
 
         for file_name in &self.args {
-            match File::from_path(Path::new(&file_name), None) {
+            match File::new(PathBuf::from(file_name), None, None) {
                 Err(e) => {
                     exit_status = 2;
                     writeln!(stderr(), "{}: {}", file_name, e)?;
@@ -126,7 +126,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
             }
 
             let mut children = Vec::new();
-            for file in dir.files() {
+            for file in dir.files(self.options.filter.dot_filter) {
                 match file {
                     Ok(file)       => children.push(file),
                     Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,

+ 131 - 17
src/fs/dir.rs

@@ -29,26 +29,30 @@ pub struct Dir {
 impl Dir {
 
     /// Create a new Dir object filled with all the files in the directory
-    /// pointed to by the given path. Fails if the directory can't be read, or
-    /// isn't actually a directory, or if there's an IO error that occurs
-    /// while scanning.
-    pub fn read_dir(path: &Path, git: bool) -> IOResult<Dir> {
-        let reader = fs::read_dir(path)?;
-        let contents = try!(reader.map(|e| e.map(|e| e.path())).collect());
-
-        Ok(Dir {
-            contents: contents,
-            path: path.to_path_buf(),
-            git: if git { Git::scan(path).ok() } else { None },
-        })
+    /// pointed to by the given path. Fails if the directory can’t be read, or
+    /// isn’t actually a directory, or if there’s an IO error that occurs at
+    /// any point.
+    ///
+    /// The `read_dir` iterator doesn’t actually yield the `.` and `..`
+    /// entries, so if the user wants to see them, we’ll have to add them
+    /// ourselves after the files have been read.
+    pub fn read_dir(path: PathBuf, git: bool) -> IOResult<Dir> {
+        let contents: Vec<PathBuf> = try!(fs::read_dir(&path)?
+                                                 .map(|result| result.map(|entry| entry.path()))
+                                                 .collect());
+
+        let git = if git { Git::scan(&path).ok() } else { None };
+        Ok(Dir { contents, path, git })
     }
 
     /// Produce an iterator of IO results of trying to read all the files in
     /// this directory.
-    pub fn files(&self) -> Files {
+    pub fn files(&self, dots: DotFilter) -> Files {
         Files {
-            inner: self.contents.iter(),
-            dir: self,
+            inner:     self.contents.iter(),
+            dir:       self,
+            dotfiles:  dots.shows_dotfiles(),
+            dots:      dots.dots(),
         }
     }
 
@@ -80,14 +84,124 @@ impl Dir {
 
 /// Iterator over reading the contents of a directory as `File` objects.
 pub struct Files<'dir> {
+
+    /// The internal iterator over the paths that have been read already.
     inner: SliceIter<'dir, PathBuf>,
+
+    /// The directory that begat those paths.
     dir: &'dir Dir,
+
+    /// Whether to include dotfiles in the list.
+    dotfiles: bool,
+
+    /// Whether the `.` or `..` directories should be produced first, before
+    /// any files have been listed.
+    dots: Dots,
+}
+
+impl<'dir> Files<'dir> {
+    fn parent(&self) -> PathBuf {
+        // We can’t use `Path#parent` here because all it does is remove the
+        // last path component, which is no good for us if the path is
+        // relative. For example, while the parent of `/testcases/files` is
+        // `/testcases`, the parent of `.` is an empty path. Adding `..` on
+        // the end is the only way to get to the *actual* parent directory.
+        self.dir.path.join("..")
+    }
+
+    /// Go through the directory until we encounter a file we can list (which
+    /// varies depending on the dotfile visibility flag)
+    fn next_visible_file(&mut self) -> Option<Result<File<'dir>, (PathBuf, io::Error)>> {
+        loop {
+            if let Some(path) = self.inner.next() {
+                let filename = File::filename(path);
+                if !self.dotfiles && filename.starts_with(".") { continue }
+
+                return Some(File::new(path.clone(), self.dir, filename)
+                                 .map_err(|e| (path.clone(), e)))
+            }
+            else {
+                return None
+            }
+        }
+    }
 }
 
+/// The dot directories that need to be listed before actual files, if any.
+/// If these aren’t being printed, then `FilesNext` is used to skip them.
+enum Dots {
+
+    /// List the `.` directory next.
+    DotNext,
+
+    /// List the `..` directory next.
+    DotDotNext,
+
+    /// Forget about the dot directories and just list files.
+    FilesNext,
+}
+
+
 impl<'dir> Iterator for Files<'dir> {
     type Item = Result<File<'dir>, (PathBuf, io::Error)>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        self.inner.next().map(|path| File::from_path(path, Some(self.dir)).map_err(|t| (path.clone(), t)))
+        if let Dots::DotNext = self.dots {
+            self.dots = Dots::DotDotNext;
+            Some(File::new(self.dir.path.to_path_buf(), self.dir, String::from("."))
+                      .map_err(|e| (Path::new(".").to_path_buf(), e)))
+        }
+        else if let Dots::DotDotNext = self.dots {
+            self.dots = Dots::FilesNext;
+            Some(File::new(self.parent(), self.dir, String::from(".."))
+                      .map_err(|e| (self.parent(), e)))
+        }
+        else {
+            self.next_visible_file()
+        }
     }
-}
+}
+
+
+/// Usually files in Unix use a leading dot to be hidden or visible, but two
+/// entries in particular are "extra-hidden": `.` and `..`, which only become
+/// visible after an extra `-a` option.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum DotFilter {
+
+    /// Shows files, dotfiles, and `.` and `..`.
+    DotfilesAndDots,
+
+    /// Show files and dotfiles, but hide `.` and `..`.
+    Dotfiles,
+
+    /// Just show files, hiding anything beginning with a dot.
+    JustFiles,
+}
+
+impl Default for DotFilter {
+    fn default() -> DotFilter {
+        DotFilter::JustFiles
+    }
+}
+
+impl DotFilter {
+
+    /// Whether this filter should show dotfiles in a listing.
+    fn shows_dotfiles(&self) -> bool {
+        match *self {
+            DotFilter::JustFiles       => false,
+            DotFilter::Dotfiles        => true,
+            DotFilter::DotfilesAndDots => true,
+        }
+    }
+
+    /// Whether this filter should add dot directories to a listing.
+    fn dots(&self) -> Dots {
+        match *self {
+            DotFilter::JustFiles       => Dots::FilesNext,
+            DotFilter::Dotfiles        => Dots::FilesNext,
+            DotFilter::DotfilesAndDots => Dots::DotNext,
+        }
+    }
+}

+ 103 - 76
src/fs/file.rs

@@ -53,34 +53,49 @@ pub struct File<'dir> {
     /// However, *directories* that get passed in will produce files that
     /// contain a reference to it, which is used in certain operations (such
     /// as looking up a file's Git status).
-    pub dir: Option<&'dir Dir>,
+    pub parent_dir: Option<&'dir Dir>,
 }
 
 impl<'dir> File<'dir> {
+    pub fn new<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult<File<'dir>>
+    where PD: Into<Option<&'dir Dir>>,
+          FN: Into<Option<String>>
+    {
+        let parent_dir = parent_dir.into();
+        let metadata   = fs::symlink_metadata(&path)?;
+        let name       = filename.into().unwrap_or_else(|| File::filename(&path));
+        let ext        = File::ext(&path);
+
+        Ok(File { path, parent_dir, metadata, ext, name })
+    }
 
-    /// Create a new `File` object from the given `Path`, inside the given
-    /// `Dir`, if appropriate.
-    ///
-    /// This uses `symlink_metadata` instead of `metadata`, which doesn't
-    /// follow symbolic links.
-    pub fn from_path(path: &Path, parent: Option<&'dir Dir>) -> IOResult<File<'dir>> {
-        fs::symlink_metadata(path).map(|metadata| File::with_metadata(metadata, path, parent))
+    /// A file’s name is derived from its string. This needs to handle directories
+    /// such as `/` or `..`, which have no `file_name` component. So instead, just
+    /// use the last component as the name.
+    pub fn filename(path: &Path) -> String {
+        match path.components().next_back() {
+            Some(back) => back.as_os_str().to_string_lossy().to_string(),
+            None       => path.display().to_string(),  // use the path as fallback
+        }
     }
 
-    /// Create a new File object from the given metadata result, and other data.
-    pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>) -> File<'dir> {
-        let filename = match path.components().next_back() {
-            Some(comp) => comp.as_os_str().to_string_lossy().to_string(),
-            None       => String::new(),
+    /// Extract an extension from a file path, if one is present, in lowercase.
+    ///
+    /// The extension is the series of characters after the last dot. This
+    /// deliberately counts dotfiles, so the ".git" folder has the extension "git".
+    ///
+    /// ASCII lowercasing is used because these extensions are only compared
+    /// against a pre-compiled list of extensions which are known to only exist
+    /// within ASCII, so it's alright.
+    fn ext(path: &Path) -> Option<String> {
+        use std::ascii::AsciiExt;
+
+        let name = match path.file_name() {
+            Some(f) => f.to_string_lossy().to_string(),
+            None => return None,
         };
 
-        File {
-            path:      path.to_path_buf(),
-            dir:       parent,
-            metadata:  metadata,
-            ext:       ext(path),
-            name:      filename,
-        }
+        name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
     }
 
     /// Whether this file is a directory on the filesystem.
@@ -95,7 +110,7 @@ impl<'dir> File<'dir> {
     /// Returns an IO error upon failure, but this shouldn't be used to check
     /// if a `File` is a directory or not! For that, just use `is_directory()`.
     pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
-        Dir::read_dir(&*self.path, scan_for_git)
+        Dir::read_dir(self.path.clone(), scan_for_git)
     }
 
     /// Whether this file is a regular file on the filesystem - that is, not a
@@ -119,32 +134,25 @@ impl<'dir> File<'dir> {
 
     /// Whether this file is a named pipe on the filesystem.
     pub fn is_pipe(&self) -> bool {
-       self.metadata.file_type().is_fifo()
-   }
-
-   /// Whether this file is a char device on the filesystem.
-   pub fn is_char_device(&self) -> bool {
-       self.metadata.file_type().is_char_device()
-   }
-
-   /// Whether this file is a block device on the filesystem.
-   pub fn is_block_device(&self) -> bool {
-       self.metadata.file_type().is_block_device()
-   }
+        self.metadata.file_type().is_fifo()
+    }
 
-   /// Whether this file is a socket on the filesystem.
-   pub fn is_socket(&self) -> bool {
-       self.metadata.file_type().is_socket()
-   }
+    /// Whether this file is a char device on the filesystem.
+    pub fn is_char_device(&self) -> bool {
+        self.metadata.file_type().is_char_device()
+    }
 
+    /// Whether this file is a block device on the filesystem.
+    pub fn is_block_device(&self) -> bool {
+        self.metadata.file_type().is_block_device()
+    }
 
-    /// Whether this file is a dotfile, based on its name. In Unix, file names
-    /// beginning with a dot represent system or configuration files, and
-    /// should be hidden by default.
-    pub fn is_dotfile(&self) -> bool {
-        self.name.starts_with('.')
+    /// Whether this file is a socket on the filesystem.
+    pub fn is_socket(&self) -> bool {
+        self.metadata.file_type().is_socket()
     }
 
+
     /// Re-prefixes the path pointed to by this file, if it's a symlink, to
     /// make it an absolute path that can be accessed from whichever
     /// directory exa is being run from.
@@ -152,7 +160,7 @@ impl<'dir> File<'dir> {
         if path.is_absolute() {
             path.to_path_buf()
         }
-        else if let Some(dir) = self.dir {
+        else if let Some(dir) = self.parent_dir {
             dir.join(&*path)
         }
         else if let Some(parent) = self.path.parent() {
@@ -179,20 +187,22 @@ impl<'dir> File<'dir> {
         // this file -- which could be absolute or relative -- to the path
         // we actually look up and turn into a `File` -- which needs to be
         // absolute to be accessible from any directory.
-        let display_path = match fs::read_link(&self.path) {
-            Ok(path)  => path,
-            Err(e)    => return FileTarget::Err(e),
+        let path = match fs::read_link(&self.path) {
+            Ok(p)   => p,
+            Err(e)  => return FileTarget::Err(e),
         };
 
-        let target_path = self.reorient_target_path(&*display_path);
+        let absolute_path = self.reorient_target_path(&path);
 
         // Use plain `metadata` instead of `symlink_metadata` - we *want* to
         // follow links.
-        if let Ok(metadata) = fs::metadata(&target_path) {
-            FileTarget::Ok(File::with_metadata(metadata, &*display_path, None))
+        if let Ok(metadata) = fs::metadata(&absolute_path) {
+            let ext  = File::ext(&path);
+            let name = File::filename(&path);
+            FileTarget::Ok(File { parent_dir: None, path, ext, metadata, name })
         }
         else {
-            FileTarget::Broken(display_path)
+            FileTarget::Broken(path)
         }
     }
 
@@ -356,7 +366,7 @@ impl<'dir> File<'dir> {
     pub fn git_status(&self) -> f::Git {
         use std::env::current_dir;
 
-        match self.dir {
+        match self.parent_dir {
             None    => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
             Some(d) => {
                 let cwd = match current_dir() {
@@ -378,26 +388,6 @@ impl<'a> AsRef<File<'a>> for File<'a> {
 }
 
 
-/// Extract an extension from a file path, if one is present, in lowercase.
-///
-/// The extension is the series of characters after the last dot. This
-/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
-///
-/// ASCII lowercasing is used because these extensions are only compared
-/// against a pre-compiled list of extensions which are known to only exist
-/// within ASCII, so it's alright.
-fn ext(path: &Path) -> Option<String> {
-    use std::ascii::AsciiExt;
-
-    let name = match path.file_name() {
-        Some(f) => f.to_string_lossy().to_string(),
-        None => return None,
-    };
-
-    name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
-}
-
-
 /// The result of following a symlink.
 pub enum FileTarget<'dir> {
 
@@ -459,22 +449,59 @@ mod modes {
 
 
 #[cfg(test)]
-mod test {
-    use super::ext;
+mod ext_test {
+    use super::File;
     use std::path::Path;
 
     #[test]
     fn extension() {
-        assert_eq!(Some("dat".to_string()), ext(Path::new("fester.dat")))
+        assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")))
     }
 
     #[test]
     fn dotfile() {
-        assert_eq!(Some("vimrc".to_string()), ext(Path::new(".vimrc")))
+        assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
     }
 
     #[test]
     fn no_extension() {
-        assert_eq!(None, ext(Path::new("jarlsberg")))
+        assert_eq!(None, File::ext(Path::new("jarlsberg")))
+    }
+}
+
+
+#[cfg(test)]
+mod filename_test {
+    use super::File;
+    use std::path::Path;
+
+    #[test]
+    fn file() {
+        assert_eq!("fester.dat", File::filename(Path::new("fester.dat")))
+    }
+
+    #[test]
+    fn no_path() {
+        assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")))
+    }
+
+    #[test]
+    fn here() {
+        assert_eq!(".", File::filename(Path::new(".")))
+    }
+
+    #[test]
+    fn there() {
+        assert_eq!("..", File::filename(Path::new("..")))
+    }
+
+    #[test]
+    fn everywhere() {
+        assert_eq!("..", File::filename(Path::new("./..")))
+    }
+
+    #[test]
+    fn topmost() {
+        assert_eq!("/", File::filename(Path::new("/")))
     }
 }

+ 1 - 1
src/fs/mod.rs

@@ -1,5 +1,5 @@
 mod dir;
-pub use self::dir::Dir;
+pub use self::dir::{Dir, DotFilter};
 
 mod file;
 pub use self::file::{File, FileTarget};

+ 1 - 1
src/info/filetype.rs

@@ -80,7 +80,7 @@ impl<'a> File<'a> {
         if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
             true
         }
-        else if let Some(dir) = self.dir {
+        else if let Some(dir) = self.parent_dir {
             self.get_source_files().iter().any(|path| dir.contains(path))
         }
         else {

+ 24 - 8
src/options/filter.rs

@@ -6,6 +6,7 @@ use glob;
 use natord;
 
 use fs::File;
+use fs::DotFilter;
 use options::misfire::Misfire;
 
 
@@ -27,11 +28,12 @@ pub struct FileFilter {
     /// ones, depending on the sort field.
     pub reverse: bool,
 
-    /// Whether to include invisible “dot” files when listing a directory.
+    /// Which invisible “dot” files to include when listing a directory.
     ///
     /// Files starting with a single “.” are used to determine “system” or
     /// “configuration” files that should not be displayed in a regular
-    /// directory listing.
+    /// directory listing, and the directory entries “.” and “..” are
+    /// considered extra-special.
     ///
     /// This came about more or less by a complete historical accident,
     /// when the original `ls` tried to hide `.` and `..`:
@@ -60,7 +62,7 @@ pub struct FileFilter {
     ///   most of them are or whether they're still needed. Every file name
     ///   evaluation that goes through my home directory is slowed down by
     ///   this accumulated sludge.
-    show_invisibles: bool,
+    pub dot_filter: DotFilter,
 
     /// Glob patterns to ignore. Any file name that matches *any* of these
     /// patterns won't be displayed in the list.
@@ -76,7 +78,7 @@ impl FileFilter {
             list_dirs_first: matches.opt_present("group-directories-first"),
             reverse:         matches.opt_present("reverse"),
             sort_field:      SortField::deduce(matches)?,
-            show_invisibles: matches.opt_present("all"),
+            dot_filter:      DotFilter::deduce(matches)?,
             ignore_patterns: IgnorePatterns::deduce(matches)?,
         })
     }
@@ -84,10 +86,6 @@ impl FileFilter {
     /// Remove every file in the given vector that does *not* pass the
     /// filter predicate for files found inside a directory.
     pub fn filter_child_files(&self, files: &mut Vec<File>) {
-        if !self.show_invisibles {
-            files.retain(|f| !f.is_dotfile());
-        }
-
         files.retain(|f| !self.ignore_patterns.is_ignored(f));
     }
 
@@ -252,6 +250,24 @@ impl SortField {
 }
 
 
+impl DotFilter {
+    pub fn deduce(matches: &getopts::Matches) -> Result<DotFilter, Misfire> {
+        let dots = match matches.opt_count("all") {
+            0 => return Ok(DotFilter::JustFiles),
+            1 => DotFilter::Dotfiles,
+            _ => DotFilter::DotfilesAndDots,
+        };
+
+        if matches.opt_present("tree") {
+            Err(Misfire::Useless("all --all", true, "tree"))
+        }
+        else {
+            Ok(dots)
+        }
+    }
+}
+
+
 #[derive(PartialEq, Default, Debug, Clone)]
 struct IgnorePatterns {
     patterns: Vec<glob::Pattern>,

+ 1 - 1
src/options/help.rs

@@ -17,7 +17,7 @@ DISPLAY OPTIONS
   --colo[u]r-scale   highlight levels of file sizes distinctly
 
 FILTERING AND SORTING OPTIONS
-  -a, --all                  don't hide hidden and 'dot' files
+  -a, --all                  show hidden and 'dot' files
   -d, --list-dirs            list directories like regular files
   -r, --reverse              reverse the sort order
   -s, --sort SORT_FIELD      which field to sort by:

+ 27 - 1
src/options/mod.rs

@@ -69,7 +69,7 @@ impl Options {
 
         // Filtering and sorting options
         opts.optflag("",  "group-directories-first", "sort directories before other files");
-        opts.optflag("a", "all",         "don't hide hidden and 'dot' files");
+        opts.optflagmulti("a", "all",    "show hidden and 'dot' files");
         opts.optflag("d", "list-dirs",   "list directories like regular files");
         opts.optopt ("L", "level",       "limit the depth of recursion", "DEPTH");
         opts.optflag("r", "reverse",     "reverse the sert order");
@@ -145,6 +145,7 @@ impl Options {
 #[cfg(test)]
 mod test {
     use super::{Options, Misfire, SortField, SortCase};
+    use fs::DotFilter;
     use fs::feature::xattr;
 
     fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
@@ -277,4 +278,29 @@ mod test {
         let opts = Options::getopts(&[ "--level", "69105" ]);
         assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
     }
+
+    #[test]
+    fn all_all_with_tree() {
+        let opts = Options::getopts(&[ "--all", "--all", "--tree" ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("all --all", true, "tree"))
+    }
+
+    #[test]
+    fn nowt() {
+        let nothing: Vec<String> = Vec::new();
+        let dots = Options::getopts(&nothing).unwrap().0.filter.dot_filter;
+        assert_eq!(dots, DotFilter::JustFiles);
+    }
+
+    #[test]
+    fn all() {
+        let dots = Options::getopts(&[ "--all".to_string() ]).unwrap().0.filter.dot_filter;
+        assert_eq!(dots, DotFilter::Dotfiles);
+    }
+
+    #[test]
+    fn allall() {
+        let dots = Options::getopts(&[ "-a".to_string(), "-a".to_string() ]).unwrap().0.filter.dot_filter;
+        assert_eq!(dots, DotFilter::DotfilesAndDots);
+    }
 }

+ 1 - 1
src/output/details.rs

@@ -344,7 +344,7 @@ impl<'a> Render<'a> {
             table.rows.push(row);
 
             if let Some(ref dir) = egg.dir {
-                for file_to_add in dir.files() {
+                for file_to_add in dir.files(self.filter.dot_filter) {
                     match file_to_add {
                         Ok(f)          => files.push(f),
                         Err((path, e)) => errors.push((e, Some(path)))

+ 1 - 1
src/output/file_name.rs

@@ -49,7 +49,7 @@ impl<'a, 'dir> FileName<'a, 'dir> {
     pub fn paint(&self) -> TextCellContents {
         let mut bits = Vec::new();
 
-        if self.file.dir.is_none() {
+        if self.file.parent_dir.is_none() {
             if let Some(parent) = self.file.path.parent() {
                 self.add_parent_bits(&mut bits, parent);
             }

+ 1 - 1
xtests/help

@@ -16,7 +16,7 @@ DISPLAY OPTIONS
   --colo[u]r-scale   highlight levels of file sizes distinctly
 
 FILTERING AND SORTING OPTIONS
-  -a, --all                  don't hide hidden and 'dot' files
+  -a, --all                  show hidden and 'dot' files
   -d, --list-dirs            list directories like regular files
   -r, --reverse              reverse the sort order
   -s, --sort SORT_FIELD      which field to sort by:

+ 1 - 0
xtests/hiddens

@@ -0,0 +1 @@
+visible

+ 1 - 0
xtests/hiddens_a

@@ -0,0 +1 @@
+..extra-hidden  .hidden  visible

+ 1 - 0
xtests/hiddens_aa

@@ -0,0 +1 @@
+.  ..  ..extra-hidden  .hidden  visible

+ 1 - 0
xtests/hiddens_l

@@ -0,0 +1 @@
+.rw-r--r-- 0 cassowary  1 Jan 12:34 visible

+ 3 - 0
xtests/hiddens_la

@@ -0,0 +1,3 @@
+.rw-r--r-- 0 cassowary  1 Jan 12:34 ..extra-hidden
+.rw-r--r-- 0 cassowary  1 Jan 12:34 .hidden
+.rw-r--r-- 0 cassowary  1 Jan 12:34 visible

+ 5 - 0
xtests/hiddens_laa

@@ -0,0 +1,5 @@
+drwxr-xr-x - cassowary  1 Jan 12:34 .
+drwxr-xr-x - cassowary  1 Jan 12:34 ..
+.rw-r--r-- 0 cassowary  1 Jan 12:34 ..extra-hidden
+.rw-r--r-- 0 cassowary  1 Jan 12:34 .hidden
+.rw-r--r-- 0 cassowary  1 Jan 12:34 visible

+ 13 - 1
xtests/run.sh

@@ -38,7 +38,7 @@ COLUMNS=120 $exa $testcases/files | diff -q - $results/files_120  || exit 1
 COLUMNS=160 $exa $testcases/files | diff -q - $results/files_160  || exit 1
 COLUMNS=200 $exa $testcases/files | diff -q - $results/files_200  || exit 1
 
-COLUMNS=100 $exa $testcases/files/* | diff -q - $results/files_star_100   || exit 1
+COLUMNS=100 $exa $testcases/files/* | diff -q - $results/files_star_100  || exit 1
 COLUMNS=150 $exa $testcases/files/* | diff -q - $results/files_star_150  || exit 1
 COLUMNS=200 $exa $testcases/files/* | diff -q - $results/files_star_200  || exit 1
 
@@ -120,8 +120,20 @@ COLUMNS=80 $exa_binary --colour=automatic $testcases/files -l | diff -q - $resul
 $exa $testcases/git/additions -l --git 2>&1 | diff -q - $results/git_additions  || exit 1
 $exa $testcases/git/edits     -l --git 2>&1 | diff -q - $results/git_edits      || exit 1
 
+
+# Hidden files
+COLUMNS=80 $exa $testcases/hiddens     2>&1 | diff -q - $results/hiddens     || exit 1
+COLUMNS=80 $exa $testcases/hiddens -a  2>&1 | diff -q - $results/hiddens_a   || exit 1
+COLUMNS=80 $exa $testcases/hiddens -aa 2>&1 | diff -q - $results/hiddens_aa  || exit 1
+
+$exa $testcases/hiddens -l     2>&1 | diff -q - $results/hiddens_l    || exit 1
+$exa $testcases/hiddens -l -a  2>&1 | diff -q - $results/hiddens_la   || exit 1
+$exa $testcases/hiddens -l -aa 2>&1 | diff -q - $results/hiddens_laa  || exit 1
+
+
 # And finally...
 $exa --help        | diff -q - $results/help      || exit 1
 $exa --help --long | diff -q - $results/help_long || exit 1
 
+
 echo "All the tests passed!"