Browse Source

Use builder pattern in clap

No good reason, other than enabling UT on arg parsing and letting myself to
compare several clap approaches for solving same problem.
pull/2/head
JuanLeon Lahoz 5 years ago
parent
commit
b6554629df
  1. 1
      Cargo.toml
  2. 154
      src/app.rs
  3. 133
      src/main.rs
  4. 14
      src/reader.rs

1
Cargo.toml

@ -3,6 +3,7 @@ name = "lowcharts"
version = "0.1.0"
authors = ["JuanLeon Lahoz <juanleon.lahoz@gmail.com>"]
edition = "2018"
description = "Tool to draw low-resolution graphs in terminal"
[dependencies]
clap = "3.0.0-beta.2"

154
src/app.rs

@ -0,0 +1,154 @@
use clap::{self, App, Arg, AppSettings};
// fn add_common_options<'a> (app: App<'a>) -> App<'a> {
fn add_common_options (app: App) -> App {
const LONG_RE_ABOUT: &str = "\
A regular expression used for capturing the values to be plotted inside input
lines.
By default this will use a capture group named `value`. If not present, it will
use first capture group.
If no regex is used, a number per line is expected (something that can be parsed
as float).
Examples of regex are ' 200 \\d+ ([0-9.]+)' (where there is one anonymous capture
group) and 'a(a)? (?P<value>[0-9.]+)' (where there are two capture groups, and
the named one will be used).
";
app
.arg(
Arg::new("max")
.long("max")
.short('M')
.about("Filter out values bigger than this")
.takes_value(true)
)
.arg(
Arg::new("min")
.long("min")
.short('m')
.about("Filter out values smaller than this")
.takes_value(true)
)
.arg(
Arg::new("width")
.long("width")
.short('w')
.about("Use this many characters as terminal width")
.default_value("110")
.takes_value(true)
)
.arg(
Arg::new("regex")
.long("regex")
.short('R')
.about("Use a regex to capture input values")
.long_about(LONG_RE_ABOUT)
.takes_value(true)
)
.arg(
Arg::new("input")
.about("Input file")
.default_value("-")
.long_about("If not present or a single dash, standard input will be used")
)
}
pub fn get_app() -> App<'static> {
let mut hist = App::new("hist")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.about("Plot an histogram from input values")
.arg(
Arg::new("intervals")
.long("intervals")
.short('i')
.about("Use that many buckets to classify data")
.default_value("20")
.takes_value(true)
);
hist = add_common_options(hist);
let mut plot = App::new("plot")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.about("Plot an 2d plot where y-values are averages of input values")
.arg(
Arg::new("height")
.long("height")
.short('h')
.about("Use that many `rows` for the plot")
.default_value("40")
.takes_value(true)
);
plot = add_common_options(plot);
App::new("lowcharts")
.author(clap::crate_authors!())
.version(clap::crate_version!())
.about(clap::crate_description!())
.max_term_width(100)
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::SubcommandRequired)
.arg(
Arg::new("color")
.short('c')
.long("color")
.about("Use colors in the output")
.possible_values(&["auto", "no", "yes"])
.takes_value(true)
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.about("Be more verbose")
.takes_value(false)
)
.subcommand(hist)
.subcommand(plot)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hist_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "--verbose", "hist", "foo"];
let m = get_app().get_matches_from(arg_vec);
assert!(m.is_present("verbose"));
if let Some(sub_m) = m.subcommand_matches("hist") {
assert_eq!("foo", sub_m.value_of("input").unwrap());
assert!(sub_m.value_of("max").is_none());
assert!(sub_m.value_of("min").is_none());
assert!(sub_m.value_of("regex").is_none());
assert_eq!("110", sub_m.value_of("width").unwrap());
assert_eq!("20", sub_m.value_of("intervals").unwrap());
} else {
assert!(false, "Subcommand `hist` not detected");
}
}
#[test]
fn plot_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "plot", "--max", "1.1", "-m", "0.9", "--height", "11"];
let m = get_app().get_matches_from(arg_vec);
assert!(!m.is_present("verbose"));
if let Some(sub_m) = m.subcommand_matches("plot") {
assert_eq!("-", sub_m.value_of("input").unwrap());
assert_eq!("1.1", sub_m.value_of("max").unwrap());
assert_eq!("0.9", sub_m.value_of("min").unwrap());
assert_eq!("11", sub_m.value_of("height").unwrap());
} else {
assert!(false, "Subcommand `plot` not detected");
}
}
}

133
src/main.rs

@ -1,6 +1,6 @@
use clap::ArgMatches;
use std::env;
use clap::{AppSettings, Clap};
use isatty::stdout_isatty;
use regex::Regex;
use yansi::Color::Red;
@ -14,6 +14,7 @@ mod histogram;
mod plot;
mod reader;
mod stats;
mod app;
fn disable_color_if_needed(option: &str) {
match option {
@ -30,74 +31,19 @@ fn disable_color_if_needed(option: &str) {
}
}
/// Tool to draw low-resolution graphs in terminal
#[derive(Clap)]
#[clap(setting = AppSettings::ColoredHelp)]
struct Opts {
/// Input file. If not present or a single dash, standard input will be used.
#[clap(default_value = "-")]
input: String,
/// Filter out values bigger than this
#[clap(long)]
max: Option<f64>,
/// Filter out values smaller than this
#[clap(long)]
min: Option<f64>,
/// Use colors in the output. Auto means "yes if tty with TERM != dumb and
/// no redirects".
#[clap(short, long, default_value = "auto", possible_values = &["auto", "no", "yes"])]
color: String,
/// Use this many characters as terminal width
#[clap(long, default_value = "110")]
width: usize,
/// Use a regex to capture input values. By default this will use a capture
/// group named "value". If not present, it will use first capture group.
/// If not present, a number per line is expected. Examples of regex are '
/// 200 \d+ ([0-9.]+)' (1 anonymous capture group) or 'a(a)?
/// (?P<value>[0-9.]+)' (a named capture group).
#[clap(long)]
regex: Option<String>,
#[clap(long)]
/// Be more verbose
verbose: bool,
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
enum SubCommand {
/// Plot an histogram from input values
Hist(Hist),
/// Plot an 2d plot where y-values are averages of input values (as many
/// averages as wide is the plot)
Plot(Plot),
}
#[derive(Clap)]
#[clap(setting = AppSettings::ColoredHelp)]
struct Hist {
/// Use that many intervals
#[clap(long, default_value = "20")]
intervals: usize,
}
#[derive(Clap)]
#[clap(setting = AppSettings::ColoredHelp)]
struct Plot {
/// Use that many rows for the plot
#[clap(long, default_value = "40")]
height: usize,
}
fn main() {
let opts: Opts = Opts::parse();
disable_color_if_needed(&opts.color);
fn get_reader(matches: &ArgMatches, verbose: bool) -> reader::DataReader {
let mut builder = reader::DataReaderBuilder::default();
builder.verbose(opts.verbose);
if opts.min.is_some() || opts.max.is_some() {
builder.range(opts.min.unwrap_or(f64::NEG_INFINITY)..opts.max.unwrap_or(f64::INFINITY));
builder.verbose(verbose);
if matches.is_present("min") || matches.is_present("max") {
let min = matches.value_of_t("min").unwrap_or(f64::NEG_INFINITY);
let max = matches.value_of_t("max").unwrap_or(f64::INFINITY);
if min > max {
eprintln!("[{}] Minimum should be smaller than maximum", Red.paint("ERROR"));
std::process::exit(1);
}
builder.range(min..max);
}
if let Some(string) = opts.regex {
if let Some(string) = matches.value_of("regex") {
match Regex::new(&string) {
Ok(re) => {
builder.regex(re);
@ -108,29 +54,54 @@ fn main() {
}
};
}
let reader = builder.build().unwrap();
builder.build().unwrap()
}
fn main() {
let matches = app::get_app().get_matches();
if let Some(c) = matches.value_of("color") {
disable_color_if_needed(c);
}
let sub_matches = match matches.subcommand_name() {
Some("hist") => {
matches.subcommand_matches("hist").unwrap()
},
Some("plot") => {
matches.subcommand_matches("plot").unwrap()
},
_ => {
eprintln!("[{}] Invalid subcommand", Red.paint("ERROR"));
std::process::exit(1);
}
};
let reader = get_reader(&sub_matches, matches.is_present("verbose"));
let vec = reader.read(&opts.input);
let vec = reader.read(sub_matches.value_of("input").unwrap_or("-"));
if vec.is_empty() {
eprintln!("[{}]: No data", Yellow.paint("WARN"));
eprintln!("[{}]: No data to process", Yellow.paint("WARN"));
std::process::exit(0);
}
let stats = stats::Stats::new(&vec);
match opts.subcmd {
SubCommand::Hist(o) => {
let width = sub_matches.value_of_t("width").unwrap();
match matches.subcommand_name() {
Some("hist") => {
let intervals = sub_matches.value_of_t("intervals").unwrap();
let mut histogram = histogram::Histogram::new(
o.intervals,
(stats.max - stats.min) / o.intervals as f64,
intervals,
(stats.max - stats.min) / intervals as f64,
stats,
);
histogram.load(&vec);
println!("{:width$}", histogram, width = opts.width);
}
SubCommand::Plot(o) => {
let mut plot = plot::Plot::new(opts.width, o.height, stats);
println!("{:width$}", histogram, width = width);
},
Some("plot") => {
let mut plot = plot::Plot::new(width, sub_matches.value_of_t("height").unwrap(), stats);
plot.load(&vec);
print!("{}", plot);
}
}
},
_ => ()
};
}

14
src/reader.rs

@ -65,12 +65,14 @@ impl DataReader {
match line.parse::<f64>() {
Ok(n) => Some(n),
Err(parse_error) => {
eprintln!(
"[{}] Cannot parse float ({}) at '{}'",
Red.paint("ERROR"),
parse_error,
line
);
if self.verbose {
eprintln!(
"[{}] Cannot parse float ({}) at '{}'",
Red.paint("ERROR"),
parse_error,
line
);
}
None
}
}

Loading…
Cancel
Save