Przeglądaj źródła

Merge branch 'recursion'

Ben S 11 lat temu
rodzic
commit
7acc1b09d5
8 zmienionych plików z 105 dodań i 62 usunięć
  1. 12 12
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 1 0
      README.md
  4. 8 9
      src/dir.rs
  5. 12 2
      src/file.rs
  6. 33 13
      src/main.rs
  7. 32 19
      src/options.rs
  8. 6 6
      src/output.rs

+ 12 - 12
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"
 

+ 1 - 1
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"

+ 1 - 0
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

+ 8 - 9
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<Dir> {
-        fs::readdir(&path).map(|paths| Dir {
+    pub fn readdir(path: &Path) -> IoResult<Dir> {
+        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.

+ 12 - 2
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(),

+ 33 - 13
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<String> = vec![];
+    let mut dirs: Vec<Path> = vec![];
     let mut files: Vec<File> = 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<File> = options.transform_files(unsorted_files);
 
+                // 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());
+                    }
+                }
+
                 if count > 1 {
-                    println!("{}:", dir_name);
+                    println!("{}:", dir_path.display());
                 }
+                count += 1;
 
-                options.view(Some(dir), files);
+                options.view(Some(dir), &files[]);
             }
             Err(e) => {
-                println!("{}: {}", dir_name, e);
+                println!("{}: {}", dir_path.display(), e);
                 return;
             }
         };

+ 32 - 19
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<String>,
+    pub dir_action: DirAction,
+    pub path_strs: Vec<String>,
     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<String> {
-        self.path_strs.iter()
-    }
-
     /// Display the files using this Option's View.
-    pub fn view(&self, dir: Option<&Dir>, files: Vec<File>) {
+    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<View, Misfire> {
 /// Finds out which file size the user has asked for.
 fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
     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<SizeFormat, Misfire> {
     }
 }
 
+fn dir_action(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
+    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<String> = 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<String> = opts.path_strs;
+        assert_eq!(args, vec![ ".".to_string() ])
     }
 
     #[test]

+ 6 - 6
src/output.rs

@@ -18,7 +18,7 @@ pub enum View {
 }
 
 impl View {
-    pub fn view(&self, dir: Option<&Dir>, files: Vec<File>) {
+    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<File>) {
+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<File>) -> Option<(usize, Vec<usize>)> {
+fn fit_into_grid(across: bool, console_width: usize, files: &[File]) -> Option<(usize, Vec<usize>)> {
     // 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<File>) -> Optio
     return None;
 }
 
-fn grid_view(across: bool, console_width: usize, files: Vec<File>) {
-    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<File>) {
     }
 }
 
-fn details_view(columns: &[Column], files: Vec<File>, 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