Quellcode durchsuchen

Merge branch 'tree'

Ben S vor 11 Jahren
Ursprung
Commit
d5aa3208b6
6 geänderte Dateien mit 143 neuen und 75 gelöschten Zeilen
  1. 0 2
      src/column.rs
  2. 5 2
      src/dir.rs
  3. 30 27
      src/file.rs
  4. 8 6
      src/main.rs
  5. 32 17
      src/options.rs
  6. 68 21
      src/output.rs

+ 0 - 2
src/column.rs

@@ -5,7 +5,6 @@ use ansi_term::Style;
 #[derive(PartialEq, Debug, Copy)]
 pub enum Column {
     Permissions,
-    FileName,
     FileSize(SizeFormat),
     Blocks,
     User,
@@ -49,7 +48,6 @@ impl Column {
     pub fn header(&self) -> &'static str {
         match *self {
             Column::Permissions => "Permissions",
-            Column::FileName    => "Name",
             Column::FileSize(_) => "Size",
             Column::Blocks      => "Blocks",
             Column::User        => "User",

+ 5 - 2
src/dir.rs

@@ -31,11 +31,14 @@ impl Dir {
 
     /// Produce a vector of File objects from an initialised directory,
     /// printing out an error if any of the Files fail to be created.
-    pub fn files(&self) -> Vec<File> {
+    ///
+    /// Passing in `recurse` means that any directories will be scanned for
+    /// their contents, as well.
+    pub fn files(&self, recurse: bool) -> Vec<File> {
         let mut files = vec![];
 
         for path in self.contents.iter() {
-            match File::from_path(path, Some(self)) {
+            match File::from_path(path, Some(self), recurse) {
                 Ok(file) => files.push(file),
                 Err(e)   => println!("{}: {}", path.display(), e),
             }

+ 30 - 27
src/file.rs

@@ -32,6 +32,7 @@ pub struct File<'a> {
     pub ext:   Option<String>,
     pub path:  Path,
     pub stat:  io::FileStat,
+    pub this:  Option<Dir>,
 }
 
 impl<'a> File<'a> {
@@ -39,12 +40,12 @@ impl<'a> File<'a> {
     /// appropriate. Paths specified directly on the command-line have no Dirs.
     ///
     /// This uses lstat instead of stat, which doesn't follow symbolic links.
-    pub fn from_path(path: &Path, parent: Option<&'a Dir>) -> IoResult<File<'a>> {
-        fs::lstat(path).map(|stat| File::with_stat(stat, path, parent))
+    pub fn from_path(path: &Path, parent: Option<&'a Dir>, recurse: bool) -> IoResult<File<'a>> {
+        fs::lstat(path).map(|stat| File::with_stat(stat, path, parent, recurse))
     }
 
     /// 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> {
+    pub fn with_stat(stat: io::FileStat, path: &Path, parent: Option<&'a Dir>, recurse: bool) -> File<'a> {
 
         // The filename to display is the last component of the path. However,
         // the path has no components for `.`, `..`, and `/`, so in these
@@ -58,12 +59,23 @@ impl<'a> File<'a> {
         // replacement characters.
         let filename = String::from_utf8_lossy(bytes);
 
+        // If we are recursing, then the `this` field contains a Dir object
+        // that represents the current File as a directory, if it is a
+        // directory. This is used for the --tree option.
+        let this = if recurse && stat.kind == io::FileType::Directory {
+            Dir::readdir(path).ok()
+        }
+        else {
+            None
+        };
+
         File {
             path:  path.clone(),
             dir:   parent,
             stat:  stat,
             name:  filename.to_string(),
             ext:   ext(filename.as_slice()),
+            this:  this,
         }
     }
 
@@ -82,7 +94,6 @@ impl<'a> File<'a> {
     pub fn display<U: Users>(&self, column: &Column, users_cache: &mut U) -> Cell {
         match *column {
             Permissions  => self.permissions_string(),
-            FileName     => self.file_name_view(),
             FileSize(f)  => self.file_size(f),
             HardLinks    => self.hard_links(),
             Inode        => self.inode(),
@@ -98,15 +109,12 @@ impl<'a> File<'a> {
     ///
     /// It consists of the file name coloured in the appropriate style,
     /// with special formatting for a symlink.
-    pub fn file_name_view(&self) -> Cell {
+    pub fn file_name_view(&self) -> String {
         if self.stat.kind == io::FileType::Symlink {
             self.symlink_file_name_view()
         }
         else {
-            Cell {
-                length: 0,  // This length is ignored (rightmost column)
-                text: self.file_colour().paint(&*self.name).to_string(),
-            }
+            self.file_colour().paint(&*self.name).to_string()
         }
     }
 
@@ -118,7 +126,7 @@ impl<'a> File<'a> {
     /// an error, highlight the target and arrow in red. The error would
     /// be shown out of context, and it's almost always because the
     /// target doesn't exist.
-    fn symlink_file_name_view(&self) -> Cell {
+    fn symlink_file_name_view(&self) -> String {
         let name = &*self.name;
         let style = self.file_colour();
 
@@ -129,26 +137,20 @@ impl<'a> File<'a> {
             };
 
             match self.target_file(&target_path) {
-                Ok(file) => Cell {
-                    length: 0,  // These lengths are never actually used...
-                    text: format!("{} {} {}{}{}",
-                                  style.paint(name),
-                                  GREY.paint("=>"),
-                                  Cyan.paint(target_path.dirname_str().unwrap()),
-                                  Cyan.paint("/"),
-                                  file.file_colour().paint(file.name.as_slice())),
-                },
-                Err(filename) => Cell {
-                    length: 0,  // ...because the rightmost column lengths are ignored!
-                    text: format!("{} {} {}",
-                                  style.paint(name),
-                                  Red.paint("=>"),
-                                  Red.underline().paint(filename.as_slice())),
-                },
+                Ok(file) => format!("{} {} {}{}{}",
+                                   style.paint(name),
+                                   GREY.paint("=>"),
+                                   Cyan.paint(target_path.dirname_str().unwrap()),
+                                   Cyan.paint("/"),
+                                   file.file_colour().paint(file.name.as_slice())),
+                Err(filename) => format!("{} {} {}",
+                                         style.paint(name),
+                                         Red.paint("=>"),
+                                         Red.underline().paint(filename.as_slice())),
             }
         }
         else {
-            Cell::paint(style, name)
+            style.paint(name).to_string()
         }
     }
 
@@ -184,6 +186,7 @@ impl<'a> File<'a> {
                 stat:  stat,
                 name:  filename.to_string(),
                 ext:   ext(filename.as_slice()),
+                this:  None,
             })
         }
         else {

+ 8 - 6
src/main.rs

@@ -39,11 +39,14 @@ fn exa(options: &Options) {
         let path = Path::new(file);
         match fs::stat(&path) {
             Ok(stat) => {
-                if stat.kind == FileType::Directory && options.dir_action != DirAction::AsFile {
+                if stat.kind == FileType::Directory && options.dir_action == DirAction::Tree {
+                    files.push(File::with_stat(stat, &path, None, true));
+                }
+                else if stat.kind == FileType::Directory && options.dir_action != DirAction::AsFile {
                     dirs.push(path);
                 }
                 else {
-                    files.push(File::with_stat(stat, &path, None));
+                    files.push(File::with_stat(stat, &path, None, false));
                 }
             }
             Err(e) => println!("{}: {}", file, e),
@@ -55,7 +58,7 @@ fn exa(options: &Options) {
     let mut first = files.is_empty();
 
     if !files.is_empty() {
-        options.view(None, &files[]);
+        options.view(None, &files[], options.filter);
     }
 
     // Directories are put on a stack rather than just being iterated through,
@@ -77,8 +80,7 @@ fn exa(options: &Options) {
 
         match Dir::readdir(&dir_path) {
             Ok(ref dir) => {
-                let unsorted_files = dir.files();
-                let files: Vec<File> = options.transform_files(unsorted_files);
+                let files = options.transform_files(dir.files(false));
 
                 // When recursing, add any directories to the dirs stack
                 // backwards: the *last* element of the stack is used each
@@ -95,7 +97,7 @@ fn exa(options: &Options) {
                 }
                 count += 1;
 
-                options.view(Some(dir), &files[]);
+                options.view(Some(dir), &files[], options.filter);
             }
             Err(e) => {
                 println!("{}: {}", dir_path.display(), e);

+ 32 - 17
src/options.rs

@@ -20,10 +20,15 @@ use self::Misfire::*;
 pub struct Options {
     pub dir_action: DirAction,
     pub path_strs: Vec<String>,
+    pub filter: FileFilter,
+    view: View,
+}
+
+#[derive(PartialEq, Debug, Copy)]
+pub struct FileFilter {
     reverse: bool,
     show_invisibles: bool,
     sort_field: SortField,
-    view: View,
 }
 
 impl Options {
@@ -45,6 +50,7 @@ impl Options {
             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("T", "tree",      "recurse into subdirectories in a tree view"),
             getopts::optflag("x", "across",    "sort multi-column view entries across"),
             getopts::optflag("?", "help",      "show list of command-line options"),
         ];
@@ -64,20 +70,28 @@ impl Options {
         };
 
         Ok(Options {
-            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"),
-            sort_field:      sort_field,
-            view:            try!(view(&matches)),
+            dir_action: try!(dir_action(&matches)),
+            path_strs:  if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() },
+            view:       try!(view(&matches)),
+            filter:     FileFilter {
+                reverse:         matches.opt_present("reverse"),
+                show_invisibles: matches.opt_present("all"),
+                sort_field:      sort_field,
+            },
         })
     }
 
+    pub fn transform_files<'a>(&self, files: Vec<File<'a>>) -> Vec<File<'a>> {
+        self.filter.transform_files(files)
+    }
+
     /// Display the files using this Option's View.
-    pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
-        self.view.view(dir, files)
+    pub fn view(&self, dir: Option<&Dir>, files: &[File], filter: FileFilter) {
+        self.view.view(dir, files, filter)
     }
+}
 
+impl FileFilter {
     /// Transform the files (sorting, reversing, filtering) before listing them.
     pub fn transform_files<'a>(&self, mut files: Vec<File<'a>>) -> Vec<File<'a>> {
 
@@ -111,7 +125,7 @@ impl Options {
 /// What to do when encountering a directory?
 #[derive(PartialEq, Debug, Copy)]
 pub enum DirAction {
-    AsFile, List, Recurse
+    AsFile, List, Recurse, Tree
 }
 
 /// User-supplied field to sort by.
@@ -189,7 +203,7 @@ fn view(matches: &getopts::Matches) -> Result<View, Misfire> {
             Err(Misfire::Useless("oneline", true, "long"))
         }
         else {
-            Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header")))
+            Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header"), matches.opt_present("tree")))
         }
     }
     else if matches.opt_present("binary") {
@@ -242,12 +256,14 @@ 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");
+    let tree    = matches.opt_present("tree");
 
-    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),
+    match (recurse, list, tree) {
+        (true,  true,  _    ) => Err(Misfire::Conflict("recurse", "list-dirs")),
+        (true,  false, false) => Ok(DirAction::Recurse),
+        (true,  false, true ) => Ok(DirAction::Tree),
+        (false, true,  _    ) => Ok(DirAction::AsFile),
+        (false, false, _    ) => Ok(DirAction::List),
     }
 }
 
@@ -304,7 +320,6 @@ impl Columns {
             }
         }
 
-        columns.push(FileName);
         columns
     }
 }

+ 68 - 21
src/output.rs

@@ -4,24 +4,24 @@ use std::iter::{AdditiveIterator, repeat};
 use column::{Column, Cell};
 use column::Alignment::Left;
 use dir::Dir;
-use file::File;
-use options::Columns;
+use file::{File, GREY};
+use options::{Columns, FileFilter};
 use users::OSUsers;
 
 use ansi_term::Style::Plain;
 
 #[derive(PartialEq, Copy, Debug)]
 pub enum View {
-    Details(Columns, bool),
+    Details(Columns, bool, bool),
     Lines,
     Grid(bool, usize),
 }
 
 impl View {
-    pub fn view(&self, dir: Option<&Dir>, files: &[File]) {
+    pub fn view(&self, dir: Option<&Dir>, files: &[File], filter: FileFilter) {
         match *self {
             View::Grid(across, width)       => grid_view(across, width, files),
-            View::Details(ref cols, header) => details_view(&*cols.for_dir(dir), files, header),
+            View::Details(ref cols, header, tree) => details_view(&*cols.for_dir(dir), files, header, tree, filter),
             View::Lines                     => lines_view(files),
         }
     }
@@ -30,7 +30,7 @@ impl View {
 /// The lines view literally just displays each file, line-by-line.
 fn lines_view(files: &[File]) {
     for file in files.iter() {
-        println!("{}", file.file_name_view().text);
+        println!("{}", file.file_name_view());
     }
 }
 
@@ -122,7 +122,7 @@ fn grid_view(across: bool, console_width: usize, files: &[File]) {
     }
 }
 
-fn details_view(columns: &[Column], files: &[File], header: bool) {
+fn details_view(columns: &[Column], files: &[File], header: bool, tree: bool, filter: FileFilter) {
     // 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
@@ -131,33 +131,80 @@ fn details_view(columns: &[Column], files: &[File], header: bool) {
 
     let mut cache = OSUsers::empty_cache();
 
-    let mut table: Vec<Vec<Cell>> = files.iter()
-        .map(|f| columns.iter().map(|c| f.display(c, &mut cache)).collect())
-        .collect();
+    let mut table = Vec::new();
+    get_files(columns, &mut cache, tree, &mut table, files, 0, filter);
 
     if header {
-        table.insert(0, columns.iter().map(|c| Cell::paint(Plain.underline(), c.header())).collect());
+        let row = Row {
+            depth: 0,
+            cells: columns.iter().map(|c| Cell::paint(Plain.underline(), c.header())).collect(),
+            name: Plain.underline().paint("Name").to_string(),
+            last: false,
+            children: false,
+        };
+
+        table.insert(0, row);
     }
 
     let column_widths: Vec<usize> = range(0, columns.len())
-        .map(|n| table.iter().map(|row| row[n].length).max().unwrap_or(0))
+        .map(|n| table.iter().map(|row| row.cells[n].length).max().unwrap_or(0))
         .collect();
 
+    let mut stack = Vec::new();
+
     for row in table.iter() {
         for (num, column) in columns.iter().enumerate() {
-            if num != 0 {
-                print!(" ");  // Separator
+            let padding = column_widths[num] - row.cells[num].length;
+            print!("{} ", column.alignment().pad_string(&row.cells[num].text, padding));
+        }
+
+        if tree {
+            stack.resize(row.depth  + 1, "├──");
+            stack[row.depth ] = if row.last { "└──" } else { "├──" };
+
+            for i in range(1, row.depth + 1) {
+                print!("{}", GREY.paint(stack[i ]));
             }
 
-            if num == columns.len() - 1 {
-                // The final column doesn't need to have trailing spaces
-                print!("{}", row[num].text);
+            if row.children {
+                stack[row.depth ] = if row.last { "   " } else { "│  " };
             }
-            else {
-                let padding = column_widths[num] - row[num].length;
-                print!("{}", column.alignment().pad_string(&row[num].text, padding));
+
+            if row.depth != 0 {
+                print!(" ");
+            }
+        }
+
+        print!("{}\n", row.name);
+    }
+}
+
+fn get_files(columns: &[Column], cache: &mut OSUsers, recurse: bool, dest: &mut Vec<Row>, src: &[File], depth: usize, filter: FileFilter) {
+    for (index, file) in src.iter().enumerate() {
+
+        let row = Row {
+            depth: depth,
+            cells: columns.iter().map(|c| file.display(c, cache)).collect(),
+            name:  file.file_name_view(),
+            last: index == src.len() - 1,
+            children: file.this.is_some(),
+        };
+
+        dest.push(row);
+
+        if recurse {
+            if let Some(ref dir) = file.this {
+                let files = filter.transform_files(dir.files(true));
+                get_files(columns, cache, recurse, dest, files.as_slice(), depth + 1, filter);
             }
         }
-        print!("\n");
     }
 }
+
+struct Row {
+    pub depth: usize,
+    pub cells: Vec<Cell>,
+    pub name: String,
+    pub last: bool,
+    pub children: bool,
+}