From 110a1c716bfc4a7f74f74b3c4f0a881c773fcd06 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Tue, 19 Apr 2016 07:48:41 +0100 Subject: [PATCH] Convert exa into a library This commit removes the 'main' function present in main.rs, renames it to exa.rs, and puts the 'main' function in its own binary. This, I think, makes it more clear how the program works and where the main entry point is. Librarification also means that we can start testing as a whole. Two tests have been added that test everything, passing in raw command-line arguments then comparing against the binary coloured text that gets produced. Casualties include having to specifically mark some code blocks in documentation as 'tests', as rustdoc kept on trying to execute my ANSI art. --- Cargo.toml | 5 ++++ src/bin/main.rs | 19 +++++++++++++ src/{main.rs => exa.rs} | 62 ++++++++++++++++++++++------------------- src/output/details.rs | 6 ++++ src/output/tree.rs | 2 ++ tests/basic.rs | 36 ++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 src/bin/main.rs rename src/{main.rs => exa.rs} (77%) create mode 100644 tests/basic.rs diff --git a/Cargo.toml b/Cargo.toml index 9bac973..ec548fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ authors = [ "ogham@bsago.me" ] [[bin]] name = "exa" +path = "src/bin/main.rs" + +[lib] +name = "exa" +path = "src/exa.rs" [dependencies] ansi_term = "0.7.1" diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..491d793 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,19 @@ +extern crate exa; +use exa::Exa; + +use std::env::args; +use std::io::stdout; +use std::process::exit; + +fn main() { + let args: Vec = args().skip(1).collect(); + let mut stdout = stdout(); + + match Exa::new(&args, &mut stdout) { + Ok(mut exa) => exa.run().expect("IO error"), + Err(e) => { + println!("{}", e); + exit(e.error_code()); + }, + }; +} diff --git a/src/main.rs b/src/exa.rs similarity index 77% rename from src/main.rs rename to src/exa.rs index 325b04f..be3bd85 100644 --- a/src/main.rs +++ b/src/exa.rs @@ -19,13 +19,13 @@ extern crate zoneinfo_compiled; #[cfg(feature="git")] extern crate git2; #[macro_use] extern crate lazy_static; -use std::env; -use std::io::{Write, stdout, Result as IOResult}; +use std::ffi::OsStr; +use std::io::{Write, Result as IOResult}; use std::path::{Component, Path}; -use std::process; use fs::{Dir, File}; use options::{Options, View}; +pub use options::Misfire; mod fs; mod info; @@ -35,27 +35,41 @@ mod term; /// The main program wrapper. -struct Exa<'w, W: Write + 'w> { +pub struct Exa<'w, W: Write + 'w> { /// List of command-line options, having been successfully parsed. - options: Options, + pub options: Options, /// The output handle that we write to. When running the program normally, /// this will be `std::io::Stdout`, but it can accept any struct that’s /// `Write` so we can write into, say, a vector for testing. - writer: &'w mut W, + pub writer: &'w mut W, + + /// List of the free command-line arguments that should correspond to file + /// names (anything that isn’t an option). + pub args: Vec, } impl<'w, W: Write + 'w> Exa<'w, W> { - fn run(&mut self, mut args_file_names: Vec) -> IOResult<()> { + pub fn new(args: &[S], writer: &'w mut W) -> Result, Misfire> + where S: AsRef { + Options::getopts(args).map(move |(opts, args)| Exa { + options: opts, + writer: writer, + args: args, + }) + } + + pub fn run(&mut self) -> IOResult<()> { let mut files = Vec::new(); let mut dirs = Vec::new(); - if args_file_names.is_empty() { - args_file_names.push(".".to_owned()); + // List the current directory by default, like ls. + if self.args.is_empty() { + self.args.push(".".to_owned()); } - for file_name in args_file_names.iter() { + for file_name in self.args.iter() { match File::from_path(Path::new(&file_name), None) { Err(e) => { try!(writeln!(self.writer, "{}: {}", file_name, e)); @@ -74,6 +88,10 @@ impl<'w, W: Write + 'w> Exa<'w, W> { } } + // We want to print a directory’s name before we list it, *except* in + // the case where it’s the only directory, *except* if there are any + // files to print as well. (It’s a double negative) + let no_files = files.is_empty(); let is_only_dir = dirs.len() == 1 && no_files; @@ -87,8 +105,8 @@ impl<'w, W: Write + 'w> Exa<'w, W> { fn print_dirs(&mut self, dir_files: Vec, mut first: bool, is_only_dir: bool) -> IOResult<()> { for dir in dir_files { - // Put a gap between directories, or between the list of files and the - // first directory. + // Put a gap between directories, or between the list of files and + // the first directory. if first { first = false; } @@ -139,6 +157,9 @@ impl<'w, W: Write + 'w> Exa<'w, W> { Ok(()) } + /// Prints the list of files using whichever view is selected. + /// For various annoying logistical reasons, each one handles + /// printing differently... fn print_files(&mut self, dir: Option<&Dir>, files: Vec) -> IOResult<()> { match self.options.view { View::Grid(g) => g.view(&files, self.writer), @@ -148,20 +169,3 @@ impl<'w, W: Write + 'w> Exa<'w, W> { } } } - - -fn main() { - let args: Vec = env::args().skip(1).collect(); - - match Options::getopts(&args) { - Ok((options, paths)) => { - let mut stdout = stdout(); - let mut exa = Exa { options: options, writer: &mut stdout }; - exa.run(paths).expect("IO error"); - }, - Err(e) => { - println!("{}", e); - process::exit(e.error_code()); - }, - }; -} diff --git a/src/output/details.rs b/src/output/details.rs index a6ebb65..f6837c1 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -12,6 +12,7 @@ //! You will probably recognise it from the `ls --long` command. It looks like //! this: //! +//! ```text //! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock //! .rw-r--r-- 547 ben 23 Jun 10:54 Cargo.toml //! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE @@ -19,6 +20,7 @@ //! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png //! drwxr-xr-x - ben 29 Jun 14:50 src //! drwxr-xr-x - ben 28 Jun 19:53 target +//! ``` //! //! The table is constructed by creating a `Table` value, which produces a `Row` //! value for each file. These rows can contain a vector of `Cell`s, or they can @@ -41,6 +43,7 @@ //! //! To illustrate the above: //! +//! ```text //! ┌─────────────────────────────────────────────────────────────────────────┐ //! │ columns: [ Permissions, Size, User, Date(Modified) ] │ //! ├─────────────────────────────────────────────────────────────────────────┤ @@ -50,6 +53,7 @@ //! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │ //! │ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target │ //! └─────────────────────────────────────────────────────────────────────────┘ +//! ``` //! //! Each column in the table needs to be resized to fit its widest argument. This //! means that we must wait until every row has been added to the table before it @@ -61,11 +65,13 @@ //! Finally, files' extended attributes and any errors that occur while statting //! them can also be displayed as their children. It looks like this: //! +//! ```text //! .rw-r--r-- 0 ben 3 Sep 13:26 forbidden //! └── //! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs //! ├── another_greeting (len 2) //! └── greeting (len 5) +//! ``` //! //! These lines also have `None` cells, and the error string or attribute details //! are used in place of the filename. diff --git a/src/output/tree.rs b/src/output/tree.rs index 1cac117..db01cff 100644 --- a/src/output/tree.rs +++ b/src/output/tree.rs @@ -12,6 +12,7 @@ //! affect the file’s information; it’s just used to display a different set of //! Unicode tree characters! The resulting table looks like this: //! +//! ```text //! ┌───────┬───────┬───────────────────────┐ //! │ Depth │ Last │ Output │ //! ├───────┼───────┼───────────────────────┤ @@ -28,6 +29,7 @@ //! │ 2 │ false │ ├── library.png │ //! │ 2 │ true │ └── space.tiff │ //! └───────┴───────┴───────────────────────┘ +//! ``` //! //! Creating the table like this means that each file has to be tested to see //! if it’s the last one in the group. This is usually done by putting all the diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..a1162fa --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,36 @@ +extern crate exa; +use exa::Exa; + +/// --------------------------------------------------------------------------- +/// These tests assume that the ‘generate annoying testcases’ script has been +/// run first. Otherwise, they will break! +/// --------------------------------------------------------------------------- + + +static DIRECTORIES: &'static str = concat!( + "\x1B[1;34m", "attributes", "\x1B[0m", '\n', + "\x1B[1;34m", "links", "\x1B[0m", '\n', + "\x1B[1;34m", "passwd", "\x1B[0m", '\n', + "\x1B[1;34m", "permissions", "\x1B[0m", '\n', +); + +#[test] +fn directories() { + let mut output = Vec::::new(); + Exa::new( &[ "-1", "testcases" ], &mut output).unwrap().run().unwrap(); + assert_eq!(output, DIRECTORIES.as_bytes()); +} + + +static PERMISSIONS: &'static str = concat!( + "\x1B[1;32m", "all-permissions", "\x1B[0m", '\n', + "\x1B[1;34m", "forbidden-directory", "\x1B[0m", '\n', + "no-permissions", '\n', +); + +#[test] +fn permissions() { + let mut output = Vec::::new(); + Exa::new( &[ "-1", "testcases/permissions" ], &mut output).unwrap().run().unwrap(); + assert_eq!(output, PERMISSIONS.as_bytes()); +}