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