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/src/exa.rs b/src/exa.rs
index aecf717..0376cdb 100644
--- a/src/exa.rs
+++ b/src/exa.rs
@@ -75,14 +75,14 @@ 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(Path::new(&file_name), None, None, None) {
Err(e) => {
exit_status = 2;
writeln!(stderr(), "{}: {}", file_name, e)?;
},
Ok(f) => {
if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
- match f.to_dir(self.options.filter.dot_filter, self.options.should_scan_for_git()) {
+ match f.to_dir(self.options.should_scan_for_git()) {
Ok(d) => dirs.push(d),
Err(e) => 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)?,
@@ -142,7 +142,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
let mut child_dirs = Vec::new();
for child_dir in children.iter().filter(|f| f.is_directory()) {
- match child_dir.to_dir(self.options.filter.dot_filter, false) {
+ match child_dir.to_dir(false) {
Ok(d) => child_dirs.push(d),
Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?,
}
diff --git a/src/fs/dir.rs b/src/fs/dir.rs
index 9314e9e..e9da9ba 100644
--- a/src/fs/dir.rs
+++ b/src/fs/dir.rs
@@ -36,18 +36,10 @@ impl Dir {
/// 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, dots: DotFilter, git: bool) -> IOResult
{
- let mut contents: Vec = try!(fs::read_dir(&path)?
+ 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());
- match dots {
- DotFilter::JustFiles => contents.retain(|p| p.file_name().and_then(|name| name.to_str()).map(|s| !s.starts_with('.')).unwrap_or(true)),
- DotFilter::Dotfiles => {/* Don’t add or remove anything */},
- DotFilter::DotfilesAndDots => {
- contents.insert(0, path.join(".."));
- contents.insert(0, path.join("."));
- }
- }
let git = if git { Git::scan(&path).ok() } else { None };
Ok(Dir { contents, path, git })
@@ -55,10 +47,12 @@ impl Dir {
/// 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(),
}
}
@@ -90,15 +84,83 @@ 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)>> {
+ use fs::file::path_filename;
+
+ loop {
+ if let Some(path) = self.inner.next() {
+ let filen = path_filename(path);
+ if !self.dotfiles && filen.starts_with(".") { continue }
+
+ return Some(File::new(path, Some(self.dir), Some(filen), None)
+ .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, Some(self.dir), Some(String::from(".")), None)
+ .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(), Some(self.dir), Some(String::from("..")), None)
+ .map_err(|e| (self.parent(), e)))
+ }
+ else {
+ self.next_visible_file()
+ }
}
}
@@ -124,3 +186,24 @@ impl Default for 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 853109d..3d32c38 100644
--- a/src/fs/file.rs
+++ b/src/fs/file.rs
@@ -6,7 +6,7 @@ use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
use std::path::{Path, PathBuf};
-use fs::dir::{Dir, DotFilter};
+use fs::dir::Dir;
use fs::fields as f;
@@ -56,31 +56,33 @@ pub struct File<'dir> {
pub dir: Option<&'dir Dir>,
}
-impl<'dir> File<'dir> {
-
- /// 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> {
- 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 path_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(),
- };
+impl<'dir> File<'dir> {
+ pub fn new(path: &Path, parent: Option<&'dir Dir>, mut filename: Option, mut metadata: Option) -> IOResult> {
+ if filename.is_none() {
+ filename = Some(path_filename(path));
+ }
- File {
+ if metadata.is_none() {
+ metadata = Some(fs::symlink_metadata(path)?);
+ }
+
+ Ok(File {
path: path.to_path_buf(),
dir: parent,
- metadata: metadata,
+ metadata: metadata.unwrap(),
ext: ext(path),
- name: filename,
- }
+ name: filename.unwrap(),
+ })
}
/// Whether this file is a directory on the filesystem.
@@ -94,8 +96,8 @@ 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, dots: DotFilter, scan_for_git: bool) -> IOResult {
- Dir::read_dir(self.path.clone(), dots, scan_for_git)
+ pub fn to_dir(&self, scan_for_git: bool) -> IOResult {
+ Dir::read_dir(self.path.clone(), scan_for_git)
}
/// Whether this file is a regular file on the filesystem - that is, not a
@@ -182,7 +184,7 @@ impl<'dir> File<'dir> {
// 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))
+ FileTarget::Ok(File::new(&*display_path, None, None, Some(metadata)).unwrap())
}
else {
FileTarget::Broken(display_path)
diff --git a/src/output/details.rs b/src/output/details.rs
index 405fe83..f514a46 100644
--- a/src/output/details.rs
+++ b/src/output/details.rs
@@ -315,7 +315,7 @@ impl<'a> Render<'a> {
if let Some(r) = self.recurse {
if file.is_directory() && r.tree && !r.is_too_deep(depth) {
- if let Ok(d) = file.to_dir(self.filter.dot_filter, false) {
+ if let Ok(d) = file.to_dir(false) {
dir = Some(d);
}
}
@@ -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)))
diff --git a/xtests/hiddens b/xtests/hiddens
new file mode 100644
index 0000000..9afbc9c
--- /dev/null
+++ b/xtests/hiddens
@@ -0,0 +1 @@
+visible
diff --git a/xtests/hiddens_a b/xtests/hiddens_a
new file mode 100644
index 0000000..5300dfd
--- /dev/null
+++ b/xtests/hiddens_a
@@ -0,0 +1 @@
+..extra-hidden .hidden visible
diff --git a/xtests/hiddens_aa b/xtests/hiddens_aa
new file mode 100644
index 0000000..f2b5c68
--- /dev/null
+++ b/xtests/hiddens_aa
@@ -0,0 +1 @@
+[1;34m.[0m [1;34m..[0m ..extra-hidden .hidden visible
diff --git a/xtests/hiddens_l b/xtests/hiddens_l
new file mode 100644
index 0000000..8e6e048
--- /dev/null
+++ b/xtests/hiddens_l
@@ -0,0 +1 @@
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m visible
diff --git a/xtests/hiddens_la b/xtests/hiddens_la
new file mode 100644
index 0000000..c8d8f9d
--- /dev/null
+++ b/xtests/hiddens_la
@@ -0,0 +1,3 @@
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m ..extra-hidden
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m .hidden
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m visible
diff --git a/xtests/hiddens_laa b/xtests/hiddens_laa
new file mode 100644
index 0000000..de014cb
--- /dev/null
+++ b/xtests/hiddens_laa
@@ -0,0 +1,5 @@
+[1;34md[33mr[31mw[32mx[0m[33mr[38;5;244m-[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34m.[0m
+[1;34md[33mr[31mw[32mx[0m[33mr[38;5;244m-[32mx[33mr[38;5;244m-[32mx[0m [38;5;244m-[0m cassowary [34m 1 Jan 12:34[0m [1;34m..[0m
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m ..extra-hidden
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m .hidden
+.[1;33mr[31mw[0m[38;5;244m-[33mr[38;5;244m--[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m visible
diff --git a/xtests/run.sh b/xtests/run.sh
index 4e36285..8583909 100755
--- a/xtests/run.sh
+++ b/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!"