mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-12-29 02:53:06 +00:00
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.
This commit is contained in:
commit
5cd7609034
@ -24,7 +24,7 @@ exa’s options are similar, but not exactly the same, as `ls`.
|
|||||||
|
|
||||||
### Filtering Options
|
### 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
|
- **-d**, **--list-dirs**: list directories like regular files
|
||||||
- **-L**, **--level=(depth)**: limit the depth of recursion
|
- **-L**, **--level=(depth)**: limit the depth of recursion
|
||||||
- **-r**, **--reverse**: reverse the sort order
|
- **-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
|
- **--group-directories-first**: list directories before other files
|
||||||
- **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore
|
- **-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
|
### Long View Options
|
||||||
|
|
||||||
These options are available when running with --long (`-l`):
|
These options are available when running with --long (`-l`):
|
||||||
|
30
Vagrantfile
vendored
30
Vagrantfile
vendored
@ -369,6 +369,36 @@ Vagrant.configure(2) do |config|
|
|||||||
EOF
|
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
|
# Install kcov for test coverage
|
||||||
# This doesn’t run coverage over the xtests so it’s less useful for now
|
# This doesn’t run coverage over the xtests so it’s less useful for now
|
||||||
if ENV.key?('INSTALL_KCOV')
|
if ENV.key?('INSTALL_KCOV')
|
||||||
|
@ -17,7 +17,7 @@ complete -c exa -l 'colour-scale' -d "Highlight levels of file sizes dist
|
|||||||
|
|
||||||
# Filtering and sorting options
|
# Filtering and sorting options
|
||||||
complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
|
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 '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 '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"
|
complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order"
|
||||||
|
@ -14,7 +14,7 @@ __exa() {
|
|||||||
{--color,--colour}"[When to use terminal colours]" \
|
{--color,--colour}"[When to use terminal colours]" \
|
||||||
{--color,--colour}-scale"[Highlight levels of file sizes distinctly]" \
|
{--color,--colour}-scale"[Highlight levels of file sizes distinctly]" \
|
||||||
--group-directories-first"[Sort directories before other files]" \
|
--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]" \
|
{-d,--list-dirs}"[List directories like regular files]" \
|
||||||
{-L,--level}"+[Limit the depth of recursion]" \
|
{-L,--level}"+[Limit the depth of recursion]" \
|
||||||
{-r,--reverse}"[Reverse the sort order]" \
|
{-r,--reverse}"[Reverse the sort order]" \
|
||||||
|
@ -59,7 +59,8 @@ highlight levels of file sizes distinctly
|
|||||||
.SH FILTERING AND SORTING OPTIONS
|
.SH FILTERING AND SORTING OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-a, \-\-all
|
.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
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
@ -24,7 +24,7 @@ extern crate lazy_static;
|
|||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io::{stderr, Write, Result as IOResult};
|
use std::io::{stderr, Write, Result as IOResult};
|
||||||
use std::path::{Component, Path};
|
use std::path::{Component, PathBuf};
|
||||||
|
|
||||||
use ansi_term::{ANSIStrings, Style};
|
use ansi_term::{ANSIStrings, Style};
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for file_name in &self.args {
|
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) => {
|
Err(e) => {
|
||||||
exit_status = 2;
|
exit_status = 2;
|
||||||
writeln!(stderr(), "{}: {}", file_name, e)?;
|
writeln!(stderr(), "{}: {}", file_name, e)?;
|
||||||
@ -126,7 +126,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
for file in dir.files() {
|
for file in dir.files(self.options.filter.dot_filter) {
|
||||||
match file {
|
match file {
|
||||||
Ok(file) => children.push(file),
|
Ok(file) => children.push(file),
|
||||||
Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
|
Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
|
||||||
|
140
src/fs/dir.rs
140
src/fs/dir.rs
@ -29,26 +29,30 @@ pub struct Dir {
|
|||||||
impl Dir {
|
impl Dir {
|
||||||
|
|
||||||
/// Create a new Dir object filled with all the files in the directory
|
/// 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
|
/// 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
|
/// isn’t actually a directory, or if there’s an IO error that occurs at
|
||||||
/// while scanning.
|
/// any point.
|
||||||
pub fn read_dir(path: &Path, git: bool) -> IOResult<Dir> {
|
///
|
||||||
let reader = fs::read_dir(path)?;
|
/// The `read_dir` iterator doesn’t actually yield the `.` and `..`
|
||||||
let contents = try!(reader.map(|e| e.map(|e| e.path())).collect());
|
/// 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());
|
||||||
|
|
||||||
Ok(Dir {
|
let git = if git { Git::scan(&path).ok() } else { None };
|
||||||
contents: contents,
|
Ok(Dir { contents, path, git })
|
||||||
path: path.to_path_buf(),
|
|
||||||
git: if git { Git::scan(path).ok() } else { None },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(&self) -> Files {
|
pub fn files(&self, dots: DotFilter) -> Files {
|
||||||
Files {
|
Files {
|
||||||
inner: self.contents.iter(),
|
inner: self.contents.iter(),
|
||||||
dir: self,
|
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.
|
/// Iterator over reading the contents of a directory as `File` objects.
|
||||||
pub struct Files<'dir> {
|
pub struct Files<'dir> {
|
||||||
|
|
||||||
|
/// The internal iterator over the paths that have been read already.
|
||||||
inner: SliceIter<'dir, PathBuf>,
|
inner: SliceIter<'dir, PathBuf>,
|
||||||
|
|
||||||
|
/// The directory that begat those paths.
|
||||||
dir: &'dir Dir,
|
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> {
|
impl<'dir> Iterator for Files<'dir> {
|
||||||
type Item = Result<File<'dir>, (PathBuf, io::Error)>;
|
type Item = Result<File<'dir>, (PathBuf, io::Error)>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
149
src/fs/file.rs
149
src/fs/file.rs
@ -53,34 +53,49 @@ pub struct File<'dir> {
|
|||||||
/// However, *directories* that get passed in will produce files that
|
/// However, *directories* that get passed in will produce files that
|
||||||
/// contain a reference to it, which is used in certain operations (such
|
/// contain a reference to it, which is used in certain operations (such
|
||||||
/// as looking up a file's Git status).
|
/// as looking up a file's Git status).
|
||||||
pub dir: Option<&'dir Dir>,
|
pub parent_dir: Option<&'dir Dir>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'dir> File<'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);
|
||||||
|
|
||||||
/// Create a new `File` object from the given `Path`, inside the given
|
Ok(File { path, parent_dir, metadata, ext, name })
|
||||||
/// `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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new File object from the given metadata result, and other data.
|
/// A file’s name is derived from its string. This needs to handle directories
|
||||||
pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>) -> File<'dir> {
|
/// such as `/` or `..`, which have no `file_name` component. So instead, just
|
||||||
let filename = match path.components().next_back() {
|
/// use the last component as the name.
|
||||||
Some(comp) => comp.as_os_str().to_string_lossy().to_string(),
|
pub fn filename(path: &Path) -> String {
|
||||||
None => String::new(),
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
|
||||||
path: path.to_path_buf(),
|
|
||||||
dir: parent,
|
|
||||||
metadata: metadata,
|
|
||||||
ext: ext(path),
|
|
||||||
name: filename,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this file is a directory on the filesystem.
|
/// 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
|
/// 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()`.
|
/// 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> {
|
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
|
/// Whether this file is a regular file on the filesystem - that is, not a
|
||||||
@ -138,13 +153,6 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-prefixes the path pointed to by this file, if it's a symlink, to
|
/// 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
|
/// make it an absolute path that can be accessed from whichever
|
||||||
/// directory exa is being run from.
|
/// directory exa is being run from.
|
||||||
@ -152,7 +160,7 @@ impl<'dir> File<'dir> {
|
|||||||
if path.is_absolute() {
|
if path.is_absolute() {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
}
|
}
|
||||||
else if let Some(dir) = self.dir {
|
else if let Some(dir) = self.parent_dir {
|
||||||
dir.join(&*path)
|
dir.join(&*path)
|
||||||
}
|
}
|
||||||
else if let Some(parent) = self.path.parent() {
|
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
|
// this file -- which could be absolute or relative -- to the path
|
||||||
// we actually look up and turn into a `File` -- which needs to be
|
// we actually look up and turn into a `File` -- which needs to be
|
||||||
// absolute to be accessible from any directory.
|
// absolute to be accessible from any directory.
|
||||||
let display_path = match fs::read_link(&self.path) {
|
let path = match fs::read_link(&self.path) {
|
||||||
Ok(path) => path,
|
Ok(p) => p,
|
||||||
Err(e) => return FileTarget::Err(e),
|
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
|
// Use plain `metadata` instead of `symlink_metadata` - we *want* to
|
||||||
// follow links.
|
// follow links.
|
||||||
if let Ok(metadata) = fs::metadata(&target_path) {
|
if let Ok(metadata) = fs::metadata(&absolute_path) {
|
||||||
FileTarget::Ok(File::with_metadata(metadata, &*display_path, None))
|
let ext = File::ext(&path);
|
||||||
|
let name = File::filename(&path);
|
||||||
|
FileTarget::Ok(File { parent_dir: None, path, ext, metadata, name })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
FileTarget::Broken(display_path)
|
FileTarget::Broken(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +366,7 @@ impl<'dir> File<'dir> {
|
|||||||
pub fn git_status(&self) -> f::Git {
|
pub fn git_status(&self) -> f::Git {
|
||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
|
|
||||||
match self.dir {
|
match self.parent_dir {
|
||||||
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
|
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
let cwd = match current_dir() {
|
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.
|
/// The result of following a symlink.
|
||||||
pub enum FileTarget<'dir> {
|
pub enum FileTarget<'dir> {
|
||||||
|
|
||||||
@ -459,22 +449,59 @@ mod modes {
|
|||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod ext_test {
|
||||||
use super::ext;
|
use super::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extension() {
|
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]
|
#[test]
|
||||||
fn dotfile() {
|
fn dotfile() {
|
||||||
assert_eq!(Some("vimrc".to_string()), ext(Path::new(".vimrc")))
|
assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_extension() {
|
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,5 +1,5 @@
|
|||||||
mod dir;
|
mod dir;
|
||||||
pub use self::dir::Dir;
|
pub use self::dir::{Dir, DotFilter};
|
||||||
|
|
||||||
mod file;
|
mod file;
|
||||||
pub use self::file::{File, FileTarget};
|
pub use self::file::{File, FileTarget};
|
||||||
|
@ -80,7 +80,7 @@ impl<'a> File<'a> {
|
|||||||
if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
if self.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
||||||
true
|
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))
|
self.get_source_files().iter().any(|path| dir.contains(path))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -6,6 +6,7 @@ use glob;
|
|||||||
use natord;
|
use natord;
|
||||||
|
|
||||||
use fs::File;
|
use fs::File;
|
||||||
|
use fs::DotFilter;
|
||||||
use options::misfire::Misfire;
|
use options::misfire::Misfire;
|
||||||
|
|
||||||
|
|
||||||
@ -27,11 +28,12 @@ pub struct FileFilter {
|
|||||||
/// ones, depending on the sort field.
|
/// ones, depending on the sort field.
|
||||||
pub reverse: bool,
|
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
|
/// Files starting with a single “.” are used to determine “system” or
|
||||||
/// “configuration” files that should not be displayed in a regular
|
/// “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,
|
/// This came about more or less by a complete historical accident,
|
||||||
/// when the original `ls` tried to hide `.` and `..`:
|
/// 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
|
/// most of them are or whether they're still needed. Every file name
|
||||||
/// evaluation that goes through my home directory is slowed down by
|
/// evaluation that goes through my home directory is slowed down by
|
||||||
/// this accumulated sludge.
|
/// this accumulated sludge.
|
||||||
show_invisibles: bool,
|
pub dot_filter: DotFilter,
|
||||||
|
|
||||||
/// Glob patterns to ignore. Any file name that matches *any* of these
|
/// Glob patterns to ignore. Any file name that matches *any* of these
|
||||||
/// patterns won't be displayed in the list.
|
/// patterns won't be displayed in the list.
|
||||||
@ -76,7 +78,7 @@ impl FileFilter {
|
|||||||
list_dirs_first: matches.opt_present("group-directories-first"),
|
list_dirs_first: matches.opt_present("group-directories-first"),
|
||||||
reverse: matches.opt_present("reverse"),
|
reverse: matches.opt_present("reverse"),
|
||||||
sort_field: SortField::deduce(matches)?,
|
sort_field: SortField::deduce(matches)?,
|
||||||
show_invisibles: matches.opt_present("all"),
|
dot_filter: DotFilter::deduce(matches)?,
|
||||||
ignore_patterns: IgnorePatterns::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
|
/// Remove every file in the given vector that does *not* pass the
|
||||||
/// filter predicate for files found inside a directory.
|
/// filter predicate for files found inside a directory.
|
||||||
pub fn filter_child_files(&self, files: &mut Vec<File>) {
|
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));
|
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)]
|
#[derive(PartialEq, Default, Debug, Clone)]
|
||||||
struct IgnorePatterns {
|
struct IgnorePatterns {
|
||||||
patterns: Vec<glob::Pattern>,
|
patterns: Vec<glob::Pattern>,
|
||||||
|
@ -17,7 +17,7 @@ DISPLAY OPTIONS
|
|||||||
--colo[u]r-scale highlight levels of file sizes distinctly
|
--colo[u]r-scale highlight levels of file sizes distinctly
|
||||||
|
|
||||||
FILTERING AND SORTING OPTIONS
|
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
|
-d, --list-dirs list directories like regular files
|
||||||
-r, --reverse reverse the sort order
|
-r, --reverse reverse the sort order
|
||||||
-s, --sort SORT_FIELD which field to sort by:
|
-s, --sort SORT_FIELD which field to sort by:
|
||||||
|
@ -69,7 +69,7 @@ impl Options {
|
|||||||
|
|
||||||
// Filtering and sorting options
|
// Filtering and sorting options
|
||||||
opts.optflag("", "group-directories-first", "sort directories before other files");
|
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.optflag("d", "list-dirs", "list directories like regular files");
|
||||||
opts.optopt ("L", "level", "limit the depth of recursion", "DEPTH");
|
opts.optopt ("L", "level", "limit the depth of recursion", "DEPTH");
|
||||||
opts.optflag("r", "reverse", "reverse the sert order");
|
opts.optflag("r", "reverse", "reverse the sert order");
|
||||||
@ -145,6 +145,7 @@ impl Options {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{Options, Misfire, SortField, SortCase};
|
use super::{Options, Misfire, SortField, SortCase};
|
||||||
|
use fs::DotFilter;
|
||||||
use fs::feature::xattr;
|
use fs::feature::xattr;
|
||||||
|
|
||||||
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
||||||
@ -277,4 +278,29 @@ mod test {
|
|||||||
let opts = Options::getopts(&[ "--level", "69105" ]);
|
let opts = Options::getopts(&[ "--level", "69105" ]);
|
||||||
assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
|
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(¬hing).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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +344,7 @@ impl<'a> Render<'a> {
|
|||||||
table.rows.push(row);
|
table.rows.push(row);
|
||||||
|
|
||||||
if let Some(ref dir) = egg.dir {
|
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 {
|
match file_to_add {
|
||||||
Ok(f) => files.push(f),
|
Ok(f) => files.push(f),
|
||||||
Err((path, e)) => errors.push((e, Some(path)))
|
Err((path, e)) => errors.push((e, Some(path)))
|
||||||
|
@ -49,7 +49,7 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
|||||||
pub fn paint(&self) -> TextCellContents {
|
pub fn paint(&self) -> TextCellContents {
|
||||||
let mut bits = Vec::new();
|
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() {
|
if let Some(parent) = self.file.path.parent() {
|
||||||
self.add_parent_bits(&mut bits, parent);
|
self.add_parent_bits(&mut bits, parent);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ DISPLAY OPTIONS
|
|||||||
--colo[u]r-scale highlight levels of file sizes distinctly
|
--colo[u]r-scale highlight levels of file sizes distinctly
|
||||||
|
|
||||||
FILTERING AND SORTING OPTIONS
|
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
|
-d, --list-dirs list directories like regular files
|
||||||
-r, --reverse reverse the sort order
|
-r, --reverse reverse the sort order
|
||||||
-s, --sort SORT_FIELD which field to sort by:
|
-s, --sort SORT_FIELD which field to sort by:
|
||||||
|
1
xtests/hiddens
Normal file
1
xtests/hiddens
Normal file
@ -0,0 +1 @@
|
|||||||
|
visible
|
1
xtests/hiddens_a
Normal file
1
xtests/hiddens_a
Normal file
@ -0,0 +1 @@
|
|||||||
|
..extra-hidden .hidden visible
|
1
xtests/hiddens_aa
Normal file
1
xtests/hiddens_aa
Normal file
@ -0,0 +1 @@
|
|||||||
|
[1;34m.[0m [1;34m..[0m ..extra-hidden .hidden visible
|
1
xtests/hiddens_l
Normal file
1
xtests/hiddens_l
Normal file
@ -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
|
3
xtests/hiddens_la
Normal file
3
xtests/hiddens_la
Normal file
@ -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
|
5
xtests/hiddens_laa
Normal file
5
xtests/hiddens_laa
Normal file
@ -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
|
@ -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/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
|
$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...
|
# And finally...
|
||||||
$exa --help | diff -q - $results/help || exit 1
|
$exa --help | diff -q - $results/help || exit 1
|
||||||
$exa --help --long | diff -q - $results/help_long || exit 1
|
$exa --help --long | diff -q - $results/help_long || exit 1
|
||||||
|
|
||||||
|
|
||||||
echo "All the tests passed!"
|
echo "All the tests passed!"
|
||||||
|
Loading…
Reference in New Issue
Block a user