improvements, add example, readme
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
.idea
|
||||||
|
|||||||
+18
-9
@@ -1,17 +1,26 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "appbase"
|
name = "clappconfig"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
publish = false
|
license = "MIT"
|
||||||
|
description = "Clap-based app config boilerplate: set up logging, load config, parse args"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
repository = "https://git.ondrovo.com/packages/cli-app-base"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["clap", "cli", "logging", "boilerplate"]
|
||||||
|
categories = [
|
||||||
|
"command-line-interface"
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.7"
|
||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
serde = "1.0.106"
|
serde = "1.0"
|
||||||
serde_json = "1.0.51"
|
serde_json = "1.0"
|
||||||
clap = "2.33.0"
|
clap = "2.33"
|
||||||
json5 = "0.2.7"
|
json5 = "0.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
smart-default = "0.6"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# clappconfig - CLI app config boilerplate
|
||||||
|
|
||||||
|
```none
|
||||||
|
Clap
|
||||||
|
App
|
||||||
|
Config
|
||||||
|
-----------
|
||||||
|
clappconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
This crate provides a simple CLI app boilerplate that takes care of
|
||||||
|
the most repetitive init tasks:
|
||||||
|
|
||||||
|
- Load JSON5 config from a custom or default path (or use `Default::default()`)
|
||||||
|
- Set up logging, with CLI and config-based overrides
|
||||||
|
- Optionally print the loaded or default config
|
||||||
|
- Show help / version on demand, courtesy of `clap`
|
||||||
|
- Optional start-up banner print
|
||||||
|
|
||||||
|
Supports custom `clap` arguments as well.
|
||||||
|
|
||||||
|
-> The repository is open to improvement PRs.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
- uses `env_logger` by default
|
||||||
|
- level can be set in the config file
|
||||||
|
- log level can be overridden by the `--log` CLI flag
|
||||||
|
- log level can be increased by repeated use of `-v` or `--verbose`
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
See the examples directory.
|
||||||
|
|
||||||
|
The example called "rotn" implements rot13 as a command-line tool.
|
||||||
|
|
||||||
|
```none
|
||||||
|
$ cargo run --example complete -- -h
|
||||||
|
|
||||||
|
Rot-N 0.1.0 by Ondřej Hruška <ondra@ondrovo.com>
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
rotn [FLAGS] [OPTIONS] --input <FILE>
|
||||||
|
|
||||||
|
FLAGS:
|
||||||
|
--default-config Print the default config JSON for reference (or to be piped to a file)
|
||||||
|
--dump-config Print the loaded config struct
|
||||||
|
-h, --help Prints help information
|
||||||
|
-V, --version Prints version information
|
||||||
|
-v, --verbose Increase logging verbosity (repeat to increase)
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-c, --config <FILE> Sets a custom config file (JSON5)
|
||||||
|
-i, --input <FILE> Input file
|
||||||
|
--log <LEVEL> Set logging verbosity (error,warning,info,debug,trace)
|
||||||
|
-n, --shift <N> Positive or negative shift, default 13
|
||||||
|
```
|
||||||
|
|
||||||
|
To get rot13 of this README, run `cargo run --example rotn -- -iREADME.md`.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// This is a JSON5 file with config for the "minimal.rs" example
|
||||||
|
{
|
||||||
|
// a foo
|
||||||
|
"foo": 1234,
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate serde_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use clappconfig::AppConfig;
|
||||||
|
use failure::Fallible;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
struct Config {
|
||||||
|
foo: u32,
|
||||||
|
bar: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig for Config {
|
||||||
|
type Init = Config;
|
||||||
|
|
||||||
|
fn logging(&self) -> &str {
|
||||||
|
"info"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure<'a>(self, _clap: &ArgMatches<'a>) -> Fallible<Config> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Fallible<()> {
|
||||||
|
let cfg = Config::init("Foo", "examples/foo.json", None)?;
|
||||||
|
|
||||||
|
trace!("Trace message");
|
||||||
|
debug!("Debug message");
|
||||||
|
info!("Info message");
|
||||||
|
warn!("Warn message");
|
||||||
|
error!("Error message");
|
||||||
|
|
||||||
|
println!("Welcome to foo, config:\n{:#?}", cfg);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Main {
|
||||||
|
config: Config,
|
||||||
|
input_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig for Config {
|
||||||
|
type Init = Main;
|
||||||
|
|
||||||
|
fn logging(&self) -> &str {
|
||||||
|
&self.logging
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
|
||||||
|
Some(vec![
|
||||||
|
"tokio_reactor",
|
||||||
|
"tokio_io",
|
||||||
|
"hyper",
|
||||||
|
"reqwest",
|
||||||
|
"mio",
|
||||||
|
"want",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print startup banner.
|
||||||
|
/// May be overridden to customize or disable it.
|
||||||
|
fn print_banner(_name: &str, _version: &str) {
|
||||||
|
// Disable
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log messages printed before logging is set up
|
||||||
|
/// May be overridden to customize or disable it.
|
||||||
|
fn pre_log_println(_message: String) {
|
||||||
|
// Disable
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get log module levels to use (take priority over the main log level)
|
||||||
|
fn logging_mod_levels(&self) -> Option<&HashMap<String, String>> {
|
||||||
|
Some(&self.log_modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
.required_unless("default-config")
|
||||||
|
.help("Input file")
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
clap::Arg::with_name("offset")
|
||||||
|
.short("n")
|
||||||
|
.long("shift")
|
||||||
|
.value_name("N")
|
||||||
|
.help("Positive or negative shift, default 13")
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure<'a>(mut self, clap: &ArgMatches<'a>) -> Fallible<Self::Init> {
|
||||||
|
let input = clap.value_of("input").unwrap().to_string();
|
||||||
|
|
||||||
|
debug!("Input: {}", input);
|
||||||
|
|
||||||
|
if input.is_empty() {
|
||||||
|
bail!("Input is required!");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
debug!("Rot normalized: {}", self.offset);
|
||||||
|
|
||||||
|
Ok(Main {
|
||||||
|
config: self,
|
||||||
|
input_file: pb,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rot_n(string: String, rot: u8) -> String {
|
||||||
|
assert!(rot < 26);
|
||||||
|
const LOWER: &[u8] = b"abcdefghihjlmnopqrstuvwxyz";
|
||||||
|
const UPPER: &[u8] = b"ABCDEFGHIHJLMNOPQRSTUVWXYZ";
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
+20
-7
@@ -18,7 +18,19 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
|
|||||||
type Init;
|
type Init;
|
||||||
|
|
||||||
/// Get log level
|
/// Get log level
|
||||||
fn logging(&self) -> &String;
|
fn logging(&self) -> &str;
|
||||||
|
|
||||||
|
/// Print startup banner.
|
||||||
|
/// May be overridden to customize or disable it.
|
||||||
|
fn print_banner(name: &str, version: &str) {
|
||||||
|
println!("{} {}\nRun with -h for help", name, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log messages printed before logging is set up
|
||||||
|
/// May be overridden to customize or disable it.
|
||||||
|
fn pre_log_println(message: String) {
|
||||||
|
println!("{}", message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get names of library modules to suppress from log output (limit to warn or error)
|
/// Get names of library modules to suppress from log output (limit to warn or error)
|
||||||
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
|
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
|
||||||
@@ -103,25 +115,26 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
|
|||||||
/* Load config */
|
/* Load config */
|
||||||
let cfg_file = argv.value_of("config").unwrap_or(cfg_file_name);
|
let cfg_file = argv.value_of("config").unwrap_or(cfg_file_name);
|
||||||
|
|
||||||
println!("{} {}\nRun with -h for help", name, version);
|
Self::print_banner(name, version);
|
||||||
|
|
||||||
let filepath = PathBuf::from_str(cfg_file)?;
|
let filepath = PathBuf::from_str(cfg_file)?;
|
||||||
|
|
||||||
let config: Self = if filepath.is_file() {
|
let config: Self = if filepath.is_file() {
|
||||||
println!("Load config from \"{}\"", cfg_file);
|
let path = filepath.canonicalize()?;
|
||||||
|
Self::pre_log_println(format!("Loading config from: {}", path.display()));
|
||||||
|
|
||||||
let buf = read_file(filepath)?;
|
let buf = read_file(path)?;
|
||||||
json5::from_str(&buf)?
|
json5::from_str(&buf)?
|
||||||
} else {
|
} else {
|
||||||
println!(
|
Self::pre_log_println(format!(
|
||||||
"Config file \"{}\" does not exist, using defaults.",
|
"Config file \"{}\" does not exist, using defaults.",
|
||||||
cfg_file
|
cfg_file
|
||||||
);
|
));
|
||||||
|
|
||||||
Self::default()
|
Self::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut level = config.logging().as_str();
|
let mut level = config.logging();
|
||||||
|
|
||||||
if !LOG_LEVELS.contains(&level) {
|
if !LOG_LEVELS.contains(&level) {
|
||||||
bail!("Invalid default log level: {}", level);
|
bail!("Invalid default log level: {}", level);
|
||||||
|
|||||||
Reference in New Issue
Block a user