提交 110a1c71 编写于 作者: B Benjamin Sago

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.
上级 a02f37cb
......@@ -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"
......
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());
},
};
}
......@@ -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<String>,
}
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 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<Dir>, 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<File>) -> 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<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());
},
};
}
......@@ -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
//! └── <Permission denied (os error 13)>
//! .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.
......
......@@ -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
......
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());
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册