diff --git a/README.md b/README.md
index c02781f..2c3c3f2 100644
--- a/README.md
+++ b/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`):
diff --git a/Vagrantfile b/Vagrantfile
index 91d4295..a10aaf5 100644
--- a/Vagrantfile
+++ b/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')
diff --git a/contrib/completions.fish b/contrib/completions.fish
index 7c13d7c..460cbb9 100644
--- a/contrib/completions.fish
+++ b/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"
diff --git a/contrib/completions.zsh b/contrib/completions.zsh
index e67453c..8f9be11 100644
--- a/contrib/completions.zsh
+++ b/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]" \
diff --git a/contrib/man/exa.1 b/contrib/man/exa.1
index 4aecccd..315862c 100644
--- a/contrib/man/exa.1
+++ b/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
diff --git a/src/exa.rs b/src/exa.rs
index c1ebe8c..f262bc5 100644
--- a/src/exa.rs
+++ b/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)?,
diff --git a/src/fs/dir.rs b/src/fs/dir.rs
index 16adb60..a9e774c 100644
--- a/src/fs/dir.rs
+++ b/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
{
- let reader = fs::read_dir(path)?;
- let contents = try!(reader.map(|e| e.map(|e| e.path())).collect());
+ /// 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 {
+ let contents: Vec = try!(fs::read_dir(&path)?
+ .map(|result| result.map(|entry| entry.path()))
+ .collect());
- Ok(Dir {
- contents: contents,
- path: path.to_path_buf(),
- git: if git { Git::scan(path).ok() } else { None },
- })
+ 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, (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, (PathBuf, io::Error)>;
fn next(&mut self) -> Option {
- 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()
+ }
}
-}
\ No newline at end of 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,
+ }
+ }
+}
diff --git a/src/fs/file.rs b/src/fs/file.rs
index c0a0653..3defdd7 100644
--- a/src/fs/file.rs
+++ b/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(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult>
+ where PD: Into