Inline the library into the binary

This commit removes the library portion of exa. Cargo now only builds a binary.

The original intent was for exa to have its own internal library, and have the binary just call the library. This is usually done for code cleanliness reasons: it separates the code that implements the purpose of the program (the "plumbing") from the code that the user interacts with (the "porcelain"), ensuring a well-defined interface between the two.

However, in exa, this split was in completely the wrong place. Logging was handled in the binary, but option parsing was handled in the library. The library could theoretically print to any Writer ("for testing", it said), but it's far easier to run integration tests by executing the binary than to change the code to handle unit tests, so this abstraction isn't gaining us anything.

I've also had several people ask me if exa should be packaged for Linux distributions as a library, or just a binary. Clearly, this is confusing!

In several of my other Rust projects, I've done this better, with the command-line option parsing and log printing done on the binary side. It also turns out that you don't need to have a [lib] section in the Cargo.toml, so that's gone too.
This commit is contained in:
Benjamin Sago 2020-10-10 01:43:42 +01:00
parent df81a24dae
commit 5ca3548bb1
5 changed files with 87 additions and 98 deletions

View File

@ -19,14 +19,8 @@ exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
[[bin]]
name = "exa"
path = "src/bin/main.rs"
doc = false
[lib]
name = "exa"
path = "src/exa.rs"
[dependencies]
ansi_term = "0.12.0"
datetime = "0.5"

View File

@ -25,7 +25,7 @@ build-features:
# runs unit tests with every combination of feature flags
test-features:
cargo hack test --feature-powerset --lib -- --quiet
cargo hack test --feature-powerset -- --quiet
# prints versions of the necessary build tools

2
Vagrantfile vendored
View File

@ -92,7 +92,7 @@ Vagrant.configure(2) do |config|
echo -e "#!/bin/sh\ncargo build --manifest-path /vagrant/Cargo.toml \\$@" > /usr/bin/build-exa
ln -sf /usr/bin/build-exa /usr/bin/b
echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml --lib \\$@ -- --quiet" > /usr/bin/test-exa
echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml \\$@ -- --quiet" > /usr/bin/test-exa
ln -sf /usr/bin/test-exa /usr/bin/t
echo -e "#!/bin/sh\n/vagrant/xtests/run.sh" > /usr/bin/run-xtests

View File

@ -1,87 +0,0 @@
extern crate exa;
use exa::Exa;
use std::ffi::OsString;
use std::env::{args_os, var_os};
use std::io::{stdout, stderr, Write, ErrorKind};
use std::process::exit;
fn main() {
configure_logger();
let args: Vec<OsString> = args_os().skip(1).collect();
match Exa::from_args(args.iter(), &mut stdout()) {
Ok(mut exa) => {
match exa.run() {
Ok(exit_status) => exit(exit_status),
Err(e) => {
match e.kind() {
ErrorKind::BrokenPipe => exit(exits::SUCCESS),
_ => {
eprintln!("{}", e);
exit(exits::RUNTIME_ERROR);
},
};
}
};
},
Err(ref e) if e.is_error() => {
let mut stderr = stderr();
writeln!(stderr, "{}", e).unwrap();
if let Some(s) = e.suggestion() {
let _ = writeln!(stderr, "{}", s);
}
exit(exits::OPTIONS_ERROR);
},
Err(ref e) => {
println!("{}", e);
exit(exits::SUCCESS);
},
};
}
/// Sets up a global logger if one is asked for.
/// The EXA_DEBUG environment variable controls whether log messages are
/// displayed or not. Currently there are just two settings (on and off).
///
/// This cant be done in exas own option parsing because that part of it
/// logs as well, so by the time execution gets there, the logger needs to
/// have already been set up.
pub fn configure_logger() {
extern crate env_logger;
extern crate log;
let present = match var_os(exa::vars::EXA_DEBUG) {
Some(debug) => debug.len() > 0,
None => false,
};
let mut logs = env_logger::Builder::new();
if present {
logs.filter(None, log::LevelFilter::Debug);
}
else {
logs.filter(None, log::LevelFilter::Off);
}
logs.init()
}
mod exits {
/// Exit code for when exa runs OK.
pub const SUCCESS: i32 = 0;
/// Exit code for when there was at least one I/O error during execution.
pub const RUNTIME_ERROR: i32 = 1;
/// Exit code for when the command-line options are invalid.
pub const OPTIONS_ERROR: i32 = 3;
}

View File

@ -1,9 +1,9 @@
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_results)]
use std::env::var_os;
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{stderr, Write, Result as IOResult};
use std::io::{stdout, stderr, Write, Result as IOResult, ErrorKind};
use std::path::{Component, PathBuf};
use ansi_term::{ANSIStrings, Style};
@ -25,6 +25,75 @@ mod output;
mod style;
fn main() {
use std::process::exit;
configure_logger();
let args: Vec<OsString> = env::args_os().skip(1).collect();
match Exa::from_args(args.iter(), &mut stdout()) {
Ok(mut exa) => {
match exa.run() {
Ok(exit_status) => exit(exit_status),
Err(e) => {
match e.kind() {
ErrorKind::BrokenPipe => exit(exits::SUCCESS),
_ => {
eprintln!("{}", e);
exit(exits::RUNTIME_ERROR);
},
};
}
};
},
Err(ref e) if e.is_error() => {
let mut stderr = stderr();
writeln!(stderr, "{}", e).unwrap();
if let Some(s) = e.suggestion() {
let _ = writeln!(stderr, "{}", s);
}
exit(exits::OPTIONS_ERROR);
},
Err(ref e) => {
println!("{}", e);
exit(exits::SUCCESS);
},
};
}
/// Sets up a global logger if one is asked for.
/// The EXA_DEBUG environment variable controls whether log messages are
/// displayed or not. Currently there are just two settings (on and off).
///
/// This cant be done in exas own option parsing because that part of it
/// logs as well, so by the time execution gets there, the logger needs to
/// have already been set up.
pub fn configure_logger() {
extern crate env_logger;
extern crate log;
let present = match env::var_os(vars::EXA_DEBUG) {
Some(debug) => debug.len() > 0,
None => false,
};
let mut logs = env_logger::Builder::new();
if present {
logs.filter(None, log::LevelFilter::Debug);
}
else {
logs.filter(None, log::LevelFilter::Off);
}
logs.init()
}
/// The main program wrapper.
pub struct Exa<'args, 'w, W: Write + 'w> {
@ -52,7 +121,7 @@ pub struct Exa<'args, 'w, W: Write + 'w> {
struct LiveVars;
impl Vars for LiveVars {
fn get(&self, name: &'static str) -> Option<OsString> {
var_os(name)
env::var_os(name)
}
}
@ -225,3 +294,16 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
}
}
}
mod exits {
/// Exit code for when exa runs OK.
pub const SUCCESS: i32 = 0;
/// Exit code for when there was at least one I/O error during execution.
pub const RUNTIME_ERROR: i32 = 1;
/// Exit code for when the command-line options are invalid.
pub const OPTIONS_ERROR: i32 = 3;
}