boilerplate for rust CLI apps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cli-app-base/examples/rotn.rs

186 lines
4.9 KiB

//! This is a complete example, showing how to use the boilerplate in a real-world application
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate log;
use clap::ArgMatches;
use clappconfig::AppConfig;
use failure::Fallible;
use smart_default::SmartDefault;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Read;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields)]
#[serde(default)]
struct Config {
#[default = "info"]
logging: String,
log_modules: HashMap<String, String>,
#[default = 13]
offset: i32,
}
/// This is a struct created by the `Config::configure()` method
/// (and returned by the `Config::init()`)
struct Main {
config: Config,
input_file: PathBuf,
}
impl AppConfig for Config {
type Init = Main;
/// Logging from the config struct
fn logging(&self) -> &str {
&self.logging
}
/// Exclude some spammy libs from logs
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
Some(vec![
// we do not actually use these here, this is an example
"tokio_reactor",
"tokio_io",
"hyper",
"reqwest",
"mio",
"want",
])
}
/// We do not want a banner
fn print_banner(_name: &str, _version: &str) {
// Disable
}
/// Suppress stderr logging on startup
fn pre_log_println(_message: String) {
// Disable
}
/// Get log module levels to use
/// (take priority over the main log level & suppressing)
fn logging_mod_levels(&self) -> Option<&HashMap<String, String>> {
// we let user configure these
Some(&self.log_modules)
}
/// Add our custom args
fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
// Default impl
clap.arg(
clap::Arg::with_name("input")
.short("i")
.long("input")
.value_name("FILE")
// Work-around for default-config acting as short-circuit (similar to --version and --help)
.required_unless("default-config")
.help("Input file to rotate")
.takes_value(true),
)
.arg(
clap::Arg::with_name("offset")
.short("n")
.long("shift")
.value_name("N")
.help("Positive or negative rotation, default 13")
.takes_value(true),
)
}
/// Here finalize configuration and parse our custom arguments.
///
/// Sometimes these go into `Config`, then we can use `type Init = Config;`
///
/// Here we don't want to have the input path in config, so a different struct is used.
///
/// This can also be solved using `#[serde(skip)]` on the field, but sometimes that is not
/// possible.
fn configure<'a>(mut self, clap: &ArgMatches<'a>) -> Fallible<Self::Init> {
let input = clap.value_of("input").unwrap().to_string();
// Logging is initialized now - feel free to use it
debug!("Input: {}", input);
if input.is_empty() {
bail!("Input is required!");
}
// `?` can be used to abort
let pb = PathBuf::from_str(&input)?.canonicalize()?;
debug!("Input path: {}", pb.display());
if !pb.is_file() {
return Err(format_err!("Input is not a file!"));
}
// Set offset by arg
if let Some(v) = clap.value_of("offset") {
self.offset = v.parse()?;
}
debug!("Rot raw: {}", self.offset);
self.offset = i32::rem_euclid(self.offset, 26);
assert!(self.offset < 26);
debug!("Rot normalized: {}", self.offset);
// We construct the `Init` struct here
Ok(Main {
config: self,
input_file: pb,
})
}
}
fn main() -> Fallible<()> {
// Using custom version (default is CARGO_PKG_VERSION)
let version = format!(
"{} by {}",
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS")
);
let main = Config::init("Rot-N", "rotn_config.json", Some(&version))?;
// rot13
let mut f = OpenOptions::new()
.read(true)
.open(main.input_file)?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
println!("{}", rot_n(buf, main.config.offset as u8));
Ok(())
}
/// Rotate a string
fn rot_n(string: String, rot: u8) -> String {
assert!(rot < 26);
const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string
.chars()
.map(|c| match c {
'A'..='Z' => UPPER[((c as u8 - 'A' as u8 + rot) % 26) as usize] as char,
'a'..='z' => LOWER[((c as u8 - 'a' as u8 + rot) % 26) as usize] as char,
_ => c,
})
.collect()
}