parent
e4d855fecc
commit
c20e16d632
@ -1,2 +1,3 @@ |
|||||||
/target |
/target |
||||||
Cargo.lock |
Cargo.lock |
||||||
|
.idea |
||||||
|
@ -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(()) |
||||||
|
} |
Loading…
Reference in new issue