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.
This commit is contained in:
Benjamin Sago 2016-04-19 07:48:41 +01:00
parent a02f37cb45
commit 110a1c716b
6 changed files with 101 additions and 29 deletions

View File

@ -5,6 +5,11 @@ authors = [ "ogham@bsago.me" ]
[[bin]] [[bin]]
name = "exa" name = "exa"
path = "src/bin/main.rs"
[lib]
name = "exa"
path = "src/exa.rs"
[dependencies] [dependencies]
ansi_term = "0.7.1" ansi_term = "0.7.1"

19
src/bin/main.rs Normal file
View File

@ -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<String> = 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());
},
};
}

View File

@ -19,13 +19,13 @@ extern crate zoneinfo_compiled;
#[cfg(feature="git")] extern crate git2; #[cfg(feature="git")] extern crate git2;
#[macro_use] extern crate lazy_static; #[macro_use] extern crate lazy_static;
use std::env; use std::ffi::OsStr;
use std::io::{Write, stdout, Result as IOResult}; use std::io::{Write, Result as IOResult};
use std::path::{Component, Path}; use std::path::{Component, Path};
use std::process;
use fs::{Dir, File}; use fs::{Dir, File};
use options::{Options, View}; use options::{Options, View};
pub use options::Misfire;
mod fs; mod fs;
mod info; mod info;
@ -35,27 +35,41 @@ mod term;
/// The main program wrapper. /// 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. /// 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, /// The output handle that we write to. When running the program normally,
/// this will be `std::io::Stdout`, but it can accept any struct thats /// this will be `std::io::Stdout`, but it can accept any struct thats
/// `Write` so we can write into, say, a vector for testing. /// `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 isnt an option).
pub args: Vec<String>,
} }
impl<'w, W: Write + 'w> Exa<'w, W> { impl<'w, W: Write + 'w> Exa<'w, W> {
fn run(&mut self, mut args_file_names: Vec<String>) -> IOResult<()> { pub fn new<S>(args: &[S], writer: &'w mut W) -> Result<Exa<'w, W>, Misfire>
where S: AsRef<OsStr> {
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 files = Vec::new();
let mut dirs = Vec::new(); let mut dirs = Vec::new();
if args_file_names.is_empty() { // List the current directory by default, like ls.
args_file_names.push(".".to_owned()); 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) { match File::from_path(Path::new(&file_name), None) {
Err(e) => { Err(e) => {
try!(writeln!(self.writer, "{}: {}", file_name, 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 directorys name before we list it, *except* in
// the case where its the only directory, *except* if there are any
// files to print as well. (Its a double negative)
let no_files = files.is_empty(); let no_files = files.is_empty();
let is_only_dir = dirs.len() == 1 && no_files; 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<Dir>, mut first: bool, is_only_dir: bool) -> IOResult<()> { fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool) -> IOResult<()> {
for dir in dir_files { for dir in dir_files {
// Put a gap between directories, or between the list of files and the // Put a gap between directories, or between the list of files and
// first directory. // the first directory.
if first { if first {
first = false; first = false;
} }
@ -139,6 +157,9 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
Ok(()) 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<File>) -> IOResult<()> { fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File>) -> IOResult<()> {
match self.options.view { match self.options.view {
View::Grid(g) => g.view(&files, self.writer), 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<String> = 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());
},
};
}

View File

@ -12,6 +12,7 @@
//! You will probably recognise it from the `ls --long` command. It looks like //! You will probably recognise it from the `ls --long` command. It looks like
//! this: //! this:
//! //!
//! ```text
//! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock //! .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-- 547 ben 23 Jun 10:54 Cargo.toml
//! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE //! .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 //! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png
//! drwxr-xr-x - ben 29 Jun 14:50 src //! drwxr-xr-x - ben 29 Jun 14:50 src
//! drwxr-xr-x - ben 28 Jun 19:53 target //! drwxr-xr-x - ben 28 Jun 19:53 target
//! ```
//! //!
//! The table is constructed by creating a `Table` value, which produces a `Row` //! 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 //! value for each file. These rows can contain a vector of `Cell`s, or they can
@ -41,6 +43,7 @@
//! //!
//! To illustrate the above: //! To illustrate the above:
//! //!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────────────┐ //! ┌─────────────────────────────────────────────────────────────────────────┐
//! │ columns: [ Permissions, Size, User, Date(Modified) ] │ //! │ columns: [ Permissions, Size, User, Date(Modified) ] │
//! ├─────────────────────────────────────────────────────────────────────────┤ //! ├─────────────────────────────────────────────────────────────────────────┤
@ -50,6 +53,7 @@
//! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │ //! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │
//! │ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target │ //! │ 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 //! 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 //! 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 //! Finally, files' extended attributes and any errors that occur while statting
//! them can also be displayed as their children. It looks like this: //! 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 forbidden
//! └── <Permission denied (os error 13)> //! └── <Permission denied (os error 13)>
//! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs //! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs
//! ├── another_greeting (len 2) //! ├── another_greeting (len 2)
//! └── greeting (len 5) //! └── greeting (len 5)
//! ```
//! //!
//! These lines also have `None` cells, and the error string or attribute details //! These lines also have `None` cells, and the error string or attribute details
//! are used in place of the filename. //! are used in place of the filename.

View File

@ -12,6 +12,7 @@
//! affect the files information; its just used to display a different set of //! affect the files information; its just used to display a different set of
//! Unicode tree characters! The resulting table looks like this: //! Unicode tree characters! The resulting table looks like this:
//! //!
//! ```text
//! ┌───────┬───────┬───────────────────────┐ //! ┌───────┬───────┬───────────────────────┐
//! │ Depth │ Last │ Output │ //! │ Depth │ Last │ Output │
//! ├───────┼───────┼───────────────────────┤ //! ├───────┼───────┼───────────────────────┤
@ -28,6 +29,7 @@
//! │ 2 │ false │ ├── library.png │ //! │ 2 │ false │ ├── library.png │
//! │ 2 │ true │ └── space.tiff │ //! │ 2 │ true │ └── space.tiff │
//! └───────┴───────┴───────────────────────┘ //! └───────┴───────┴───────────────────────┘
//! ```
//! //!
//! Creating the table like this means that each file has to be tested to see //! Creating the table like this means that each file has to be tested to see
//! if its the last one in the group. This is usually done by putting all the //! if its the last one in the group. This is usually done by putting all the

36
tests/basic.rs Normal file
View File

@ -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::<u8>::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::<u8>::new();
Exa::new( &[ "-1", "testcases/permissions" ], &mut output).unwrap().run().unwrap();
assert_eq!(output, PERMISSIONS.as_bytes());
}