diff --git a/Cargo.lock b/Cargo.lock
index 74aa14f..9f7ff28 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,17 +2,17 @@
name = "exa"
version = "0.1.0"
dependencies = [
- "ansi_term 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "getopts 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ansi_term 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getopts 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"git2 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "natord 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "number_prefix 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "users 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "natord 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "number_prefix 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "users 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"regex 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -26,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "getopts"
-version = "0.1.4"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -55,7 +55,7 @@ name = "libressl-pnacl-sys"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "pnacl-build-helper 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pnacl-build-helper 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -83,12 +83,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "natord"
-version = "1.0.6"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "number_prefix"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -107,7 +107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "pnacl-build-helper"
-version = "1.3.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -139,6 +139,6 @@ dependencies = [
[[package]]
name = "users"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index aba4e13..c5ed498 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ name = "exa"
[dependencies]
ansi_term = "0.4.2"
-getopts = "0.1.4"
+getopts = "0.2.0"
natord = "1.0.6"
number_prefix = "0.2.1"
users = "0.2.1"
diff --git a/README.md b/README.md
index 5640235..436a6dd 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ exa is a replacement for `ls` written in Rust.
- **-i**, **--inode**: show inode number column
- **-l**, **--long**: display extended details and attributes
- **-r**, **--reverse**: reverse sort order
+- **-R**, **--recurse**: recurse into subdirectories
- **-s**, **--sort=(field)**: field to sort by
- **-S**, **--blocks**: show number of file system blocks
- **-x**, **--across**: sort multi-column view entries across
diff --git a/src/dir.rs b/src/dir.rs
index f9f8437..8a567fd 100644
--- a/src/dir.rs
+++ b/src/dir.rs
@@ -21,11 +21,11 @@ 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.
- pub fn readdir(path: Path) -> IoResult
{
- fs::readdir(&path).map(|paths| Dir {
+ pub fn readdir(path: &Path) -> IoResult {
+ fs::readdir(path).map(|paths| Dir {
contents: paths,
path: path.clone(),
- git: Git::scan(&path).ok(),
+ git: Git::scan(path).ok(),
})
}
@@ -102,12 +102,11 @@ impl Git {
/// path that gets passed in. This is used for getting the status of
/// directories, which don't really have an 'official' status.
fn dir_status(&self, dir: &Path) -> String {
- let status = self.statuses.iter()
- .filter(|p| p.0.starts_with(dir.as_vec()))
- .fold(git2::Status::empty(), |a, b| a | b.1);
- match status {
- s => format!("{}{}", Git::index_status(s), Git::working_tree_status(s)),
- }
+ let s = self.statuses.iter()
+ .filter(|p| p.0.starts_with(dir.as_vec()))
+ .fold(git2::Status::empty(), |a, b| a | b.1);
+
+ format!("{}{}", Git::index_status(s), Git::working_tree_status(s))
}
/// The character to display if the file has been modified, but not staged.
diff --git a/src/file.rs b/src/file.rs
index 9b986f7..63d50ec 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -45,8 +45,18 @@ impl<'a> File<'a> {
/// Create a new File object from the given Stat result, and other data.
pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>) -> File<'a> {
- let v = path.filename().unwrap(); // fails if / or . or ..
- let filename = String::from_utf8_lossy(v);
+
+ // The filename to display is the last component of the path. However,
+ // the path has no components for `.`, `..`, and `/`, so in these
+ // cases, the entire path is used.
+ let bytes = match path.components().last() {
+ Some(b) => b,
+ None => path.as_vec(),
+ };
+
+ // Convert the string to UTF-8, replacing any invalid characters with
+ // replacement characters.
+ let filename = String::from_utf8_lossy(bytes);
File {
path: path.clone(),
diff --git a/src/main.rs b/src/main.rs
index b713f94..c31c6cb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,8 @@
#![feature(collections, core, io, libc, os, path, std_misc)]
extern crate ansi_term;
+extern crate getopts;
+extern crate natord;
extern crate number_prefix;
extern crate users;
@@ -12,7 +14,7 @@ use std::os::{args, set_exit_status};
use dir::Dir;
use file::File;
-use options::Options;
+use options::{Options, DirAction};
pub mod column;
pub mod dir;
@@ -23,7 +25,7 @@ pub mod output;
pub mod term;
fn exa(options: &Options) {
- let mut dirs: Vec = vec![];
+ let mut dirs: Vec = vec![];
let mut files: Vec = vec![];
// It's only worth printing out directory names if the user supplied
@@ -33,16 +35,14 @@ fn exa(options: &Options) {
// Separate the user-supplied paths into directories and files.
// Files are shown first, and then each directory is expanded
// and listed second.
- for file in options.path_strings() {
+ for file in options.path_strs.iter() {
let path = Path::new(file);
match fs::stat(&path) {
Ok(stat) => {
- if !options.list_dirs && stat.kind == FileType::Directory {
- dirs.push(file.clone());
+ if stat.kind == FileType::Directory && options.dir_action != DirAction::AsFile {
+ dirs.push(path);
}
else {
- // May as well reuse the stat result from earlier
- // instead of just using File::from_path().
files.push(File::with_stat(stat, &path, None));
}
}
@@ -55,10 +55,19 @@ fn exa(options: &Options) {
let mut first = files.is_empty();
if !files.is_empty() {
- options.view(None, files);
+ options.view(None, &files[]);
}
- for dir_name in dirs.iter() {
+ // Directories are put on a stack rather than just being iterated through,
+ // as the vector can change as more directories are added.
+ loop {
+ let dir_path = match dirs.pop() {
+ None => break,
+ Some(f) => f,
+ };
+
+ // Put a gap between directories, or between the list of files and the
+ // first directory.
if first {
first = false;
}
@@ -66,19 +75,30 @@ fn exa(options: &Options) {
print!("\n");
}
- match Dir::readdir(Path::new(dir_name.clone())) {
+ match Dir::readdir(&dir_path) {
Ok(ref dir) => {
let unsorted_files = dir.files();
let files: Vec = options.transform_files(unsorted_files);
- if count > 1 {
- println!("{}:", dir_name);
+ // When recursing, add any directories to the dirs stack
+ // backwards: the *last* element of the stack is used each
+ // time, so by inserting them backwards, they get displayed in
+ // the correct sort order.
+ if options.dir_action == DirAction::Recurse {
+ for dir in files.iter().filter(|f| f.stat.kind == FileType::Directory).rev() {
+ dirs.push(dir.path.clone());
+ }
}
- options.view(Some(dir), files);
+ if count > 1 {
+ println!("{}:", dir_path.display());
+ }
+ count += 1;
+
+ options.view(Some(dir), &files[]);
}
Err(e) => {
- println!("{}: {}", dir_name, e);
+ println!("{}: {}", dir_path.display(), e);
return;
}
};
diff --git a/src/options.rs b/src/options.rs
index a6357d8..8d1f355 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -1,6 +1,3 @@
-extern crate getopts;
-extern crate natord;
-
use dir::Dir;
use file::File;
use column::{Column, SizeFormat};
@@ -11,7 +8,9 @@ use term::dimensions;
use std::ascii::AsciiExt;
use std::cmp::Ordering;
use std::fmt;
-use std::slice::Iter;
+
+use getopts;
+use natord;
use self::Misfire::*;
@@ -19,8 +18,8 @@ use self::Misfire::*;
/// command-line options.
#[derive(PartialEq, Debug)]
pub struct Options {
- pub list_dirs: bool,
- path_strs: Vec,
+ pub dir_action: DirAction,
+ pub path_strs: Vec,
reverse: bool,
show_invisibles: bool,
sort_field: SortField,
@@ -43,6 +42,7 @@ impl Options {
getopts::optflag("l", "long", "display extended details and attributes"),
getopts::optflag("i", "inode", "show each file's inode number"),
getopts::optflag("r", "reverse", "reverse order of files"),
+ getopts::optflag("R", "recurse", "recurse into directories"),
getopts::optopt ("s", "sort", "field to sort by", "WORD"),
getopts::optflag("S", "blocks", "show number of file system blocks"),
getopts::optflag("x", "across", "sort multi-column view entries across"),
@@ -64,7 +64,7 @@ impl Options {
};
Ok(Options {
- list_dirs: matches.opt_present("list-dirs"),
+ dir_action: try!(dir_action(&matches)),
path_strs: if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() },
reverse: matches.opt_present("reverse"),
show_invisibles: matches.opt_present("all"),
@@ -73,13 +73,8 @@ impl Options {
})
}
- /// Iterate over the non-option arguments left oven from getopts.
- pub fn path_strings(&self) -> Iter {
- self.path_strs.iter()
- }
-
/// Display the files using this Option's View.
- pub fn view(&self, dir: Option<&Dir>, files: Vec) {
+ pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
self.view.view(dir, files)
}
@@ -113,7 +108,13 @@ impl Options {
}
}
-/// User-supplied field to sort by
+/// What to do when encountering a directory?
+#[derive(PartialEq, Debug, Copy)]
+pub enum DirAction {
+ AsFile, List, Recurse
+}
+
+/// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy)]
pub enum SortField {
Unsorted, Name, Extension, Size, FileInode
@@ -228,7 +229,7 @@ fn view(matches: &getopts::Matches) -> Result {
/// Finds out which file size the user has asked for.
fn file_size(matches: &getopts::Matches) -> Result {
let binary = matches.opt_present("binary");
- let bytes = matches.opt_present("bytes");
+ let bytes = matches.opt_present("bytes");
match (binary, bytes) {
(true, true ) => Err(Misfire::Conflict("binary", "bytes")),
@@ -238,6 +239,18 @@ fn file_size(matches: &getopts::Matches) -> Result {
}
}
+fn dir_action(matches: &getopts::Matches) -> Result {
+ let recurse = matches.opt_present("recurse");
+ let list = matches.opt_present("list-dirs");
+
+ match (recurse, list) {
+ (true, true ) => Err(Misfire::Conflict("recurse", "list-dirs")),
+ (true, false) => Ok(DirAction::Recurse),
+ (false, true ) => Ok(DirAction::AsFile),
+ (false, false) => Ok(DirAction::List),
+ }
+}
+
#[derive(PartialEq, Copy, Debug)]
pub struct Columns {
size_format: SizeFormat,
@@ -332,15 +345,15 @@ mod test {
#[test]
fn files() {
let opts = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap();
- let args: Vec<&String> = opts.path_strings().collect();
- assert_eq!(args, vec![ &"this file".to_string(), &"that file".to_string() ])
+ let args: Vec = opts.path_strs;
+ assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
}
#[test]
fn no_args() {
let opts = Options::getopts(&[]).unwrap();
- let args: Vec<&String> = opts.path_strings().collect();
- assert_eq!(args, vec![ &".".to_string() ])
+ let args: Vec = opts.path_strs;
+ assert_eq!(args, vec![ ".".to_string() ])
}
#[test]
diff --git a/src/output.rs b/src/output.rs
index 7b8bd39..52251d4 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -18,7 +18,7 @@ pub enum View {
}
impl View {
- pub fn view(&self, dir: Option<&Dir>, files: Vec) {
+ pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
match *self {
View::Grid(across, width) => grid_view(across, width, files),
View::Details(ref cols, header) => details_view(&*cols.for_dir(dir), files, header),
@@ -28,13 +28,13 @@ impl View {
}
/// The lines view literally just displays each file, line-by-line.
-fn lines_view(files: Vec) {
+fn lines_view(files: &[File]) {
for file in files.iter() {
println!("{}", file.file_name_view().text);
}
}
-fn fit_into_grid(across: bool, console_width: usize, files: &Vec) -> Option<(usize, Vec)> {
+fn fit_into_grid(across: bool, console_width: usize, files: &[File]) -> Option<(usize, Vec)> {
// TODO: this function could almost certainly be optimised...
// surely not *all* of the numbers of lines are worth searching through!
@@ -86,8 +86,8 @@ fn fit_into_grid(across: bool, console_width: usize, files: &Vec) -> Optio
return None;
}
-fn grid_view(across: bool, console_width: usize, files: Vec) {
- if let Some((num_lines, widths)) = fit_into_grid(across, console_width, &files) {
+fn grid_view(across: bool, console_width: usize, files: &[File]) {
+ if let Some((num_lines, widths)) = fit_into_grid(across, console_width, files) {
for y in range(0, num_lines) {
for x in range(0, widths.len()) {
let num = if across {
@@ -122,7 +122,7 @@ fn grid_view(across: bool, console_width: usize, files: Vec) {
}
}
-fn details_view(columns: &[Column], files: Vec, header: bool) {
+fn details_view(columns: &[Column], files: &[File], header: bool) {
// The output gets formatted into columns, which looks nicer. To
// do this, we have to write the results into a table, instead of
// displaying each file immediately, then calculating the maximum