Merge branch 'env-config' into 'master'
Environment based options/flags See merge request arkham/arkham!3
This commit is contained in:
commit
7f757c76f9
|
@ -10,6 +10,7 @@ building blocks for building attractive and smooth CLIs
|
||||||
* Nested subcommands with their own flags
|
* Nested subcommands with their own flags
|
||||||
* Opts are hierarchal and can be utilized from parent commands
|
* Opts are hierarchal and can be utilized from parent commands
|
||||||
* Automatic usage details for subcommands and bare execution
|
* Automatic usage details for subcommands and bare execution
|
||||||
|
* Flags and options can be set via environment variables
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ fn parse_args(c: &mut Criterion) {
|
||||||
Command::new("thing")
|
Command::new("thing")
|
||||||
.opt(Opt::scalar("config").short("c").long("config"))
|
.opt(Opt::scalar("config").short("c").long("config"))
|
||||||
.handler(|_, ctx, _| {
|
.handler(|_, ctx, _| {
|
||||||
assert_eq!(ctx.get_string("user"), Some("joe".into()));
|
assert_eq!(ctx.get_value("user"), Some("joe".into()));
|
||||||
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
|
assert_eq!(ctx.get_value("config"), Some("c.json".into()));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ fn main() {
|
||||||
|
|
||||||
fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) {
|
fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) {
|
||||||
let v = fibonacci(
|
let v = fibonacci(
|
||||||
ctx.get_string("count")
|
ctx.get_value("count")
|
||||||
.unwrap_or("1".to_string())
|
.unwrap_or("1".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(1),
|
.unwrap_or(1),
|
||||||
|
|
59
src/app.rs
59
src/app.rs
|
@ -10,6 +10,7 @@ pub struct App {
|
||||||
name: Option<&'static str>,
|
name: Option<&'static str>,
|
||||||
version: Option<&'static str>,
|
version: Option<&'static str>,
|
||||||
pub root: Command,
|
pub root: Command,
|
||||||
|
env_prefix: Option<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for App {
|
impl Default for App {
|
||||||
|
@ -18,6 +19,7 @@ impl Default for App {
|
||||||
name: None,
|
name: None,
|
||||||
version: None,
|
version: None,
|
||||||
root: Command::new("root"),
|
root: Command::new("root"),
|
||||||
|
env_prefix: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +70,21 @@ impl App {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the environment variable prefix for option resolution. If set to something like
|
||||||
|
/// APP_NAME a option with the name THING, will look for an environment variable named
|
||||||
|
/// APP_NAME_THING.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```rust
|
||||||
|
/// use arkham::App;
|
||||||
|
/// App::new().env_prefix("APP_NAME");
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
pub fn env_prefix(mut self, prefix: &'static str) -> Self {
|
||||||
|
self.env_prefix = Some(prefix);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds a root level command to the application. This command can then be executed with:
|
/// Adds a root level command to the application. This command can then be executed with:
|
||||||
///
|
///
|
||||||
/// myapp command_name
|
/// myapp command_name
|
||||||
|
@ -131,7 +148,7 @@ impl App {
|
||||||
/// .run_with(vec!["-n".to_string(), "alice".to_string()]);
|
/// .run_with(vec!["-n".to_string(), "alice".to_string()]);
|
||||||
///
|
///
|
||||||
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
|
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
|
||||||
/// println!("Hello, {}", ctx.get_string("name").unwrap());
|
/// println!("Hello, {}", ctx.get_value("name").unwrap());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn run_with(&self, args: Vec<String>) -> Result<()> {
|
pub fn run_with(&self, args: Vec<String>) -> Result<()> {
|
||||||
|
@ -151,7 +168,7 @@ impl App {
|
||||||
/// .run();
|
/// .run();
|
||||||
///
|
///
|
||||||
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
|
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
|
||||||
/// println!("Hello, {}", ctx.get_string("name").unwrap_or_else(|| "unnamed".into()));
|
/// println!("Hello, {}", ctx.get_value("name").unwrap_or_else(|| "unnamed".into()));
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn run(&mut self) -> Result<()> {
|
pub fn run(&mut self) -> Result<()> {
|
||||||
|
@ -193,7 +210,18 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
|
||||||
// Check for short args
|
// Check for short args
|
||||||
if arg.starts_with("-") {
|
if arg.starts_with("-") {
|
||||||
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) {
|
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) {
|
||||||
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
|
match opt.kind {
|
||||||
|
OptKind::Flag => {
|
||||||
|
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
|
||||||
|
}
|
||||||
|
OptKind::String => {
|
||||||
|
if let Some(value) = args.next() {
|
||||||
|
opts.push(ActiveOpt::new(opt.clone(), vec![value.clone()]));
|
||||||
|
} else {
|
||||||
|
return Err(OptError::InvalidOpt(opt.name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ignored.push(arg.clone());
|
ignored.push(arg.clone());
|
||||||
}
|
}
|
||||||
|
@ -225,7 +253,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
|
||||||
|
|
||||||
// Execute the command handler
|
// Execute the command handler
|
||||||
if let Some(handler) = cmd.handler {
|
if let Some(handler) = cmd.handler {
|
||||||
handler(app, &Context::new(cmd.clone(), opts.clone()), &ignored);
|
let mut ctx = Context::new(cmd.clone(), opts.clone());
|
||||||
|
if let Some(prefix) = app.env_prefix {
|
||||||
|
ctx.env_prefix = Some(prefix.to_string());
|
||||||
|
}
|
||||||
|
handler(app, &ctx, &ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -241,7 +273,7 @@ mod tests {
|
||||||
let app = App::new()
|
let app = App::new()
|
||||||
.opt(Opt::scalar("user").short("u").long("user"))
|
.opt(Opt::scalar("user").short("u").long("user"))
|
||||||
.handler(|_, ctx, _| {
|
.handler(|_, ctx, _| {
|
||||||
assert_eq!(ctx.get_string("user"), Some("joe".into()));
|
assert_eq!(ctx.get_value("user"), Some("joe".into()));
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = app.run_with(args);
|
let res = app.run_with(args);
|
||||||
|
@ -263,8 +295,8 @@ mod tests {
|
||||||
Command::new("thing")
|
Command::new("thing")
|
||||||
.opt(Opt::scalar("config").short("c").long("config"))
|
.opt(Opt::scalar("config").short("c").long("config"))
|
||||||
.handler(|_, ctx, _| {
|
.handler(|_, ctx, _| {
|
||||||
assert_eq!(ctx.get_string("user"), Some("joe".into()));
|
assert_eq!(ctx.get_value("user"), Some("joe".into()));
|
||||||
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
|
assert_eq!(ctx.get_value("config"), Some("c.json".into()));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let res = app.run_with(args);
|
let res = app.run_with(args);
|
||||||
|
@ -318,4 +350,17 @@ mod tests {
|
||||||
.run_with(vec!["-u".into()]);
|
.run_with(vec!["-u".into()]);
|
||||||
assert!(r.is_err(), "Should error for invalid short flag");
|
assert!(r.is_err(), "Should error for invalid short flag");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_env_prefix() {
|
||||||
|
std::env::set_var("ARKHAM_thing", "1");
|
||||||
|
App::new()
|
||||||
|
.env_prefix("ARKHAM")
|
||||||
|
.opt(Opt::flag("thing").short("-t").long("--t"))
|
||||||
|
.handler(|_, ctx, _| {
|
||||||
|
assert_eq!(ctx.flag("thing"), true);
|
||||||
|
})
|
||||||
|
.run_with(vec![])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,23 @@ use crate::{
|
||||||
opt::{ActiveOpt, OptKind},
|
opt::{ActiveOpt, OptKind},
|
||||||
Command,
|
Command,
|
||||||
};
|
};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
opts: Vec<ActiveOpt>,
|
opts: Vec<ActiveOpt>,
|
||||||
cmd: Command,
|
cmd: Command,
|
||||||
|
pub(crate) env_prefix: Option<String>,
|
||||||
|
pub(crate) env_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub(crate) fn new(cmd: Command, opts: Vec<ActiveOpt>) -> Self {
|
pub(crate) fn new(cmd: Command, opts: Vec<ActiveOpt>) -> Self {
|
||||||
Self { opts, cmd }
|
Self {
|
||||||
|
opts,
|
||||||
|
cmd,
|
||||||
|
env_prefix: None,
|
||||||
|
env_enabled: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks for the existance of a flag
|
/// Checks for the existance of a flag
|
||||||
|
@ -18,10 +26,23 @@ impl Context {
|
||||||
self.opts
|
self.opts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag))
|
.any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag))
|
||||||
|
|| self.get_env_value(name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_env_value(&self, name: &str) -> Option<String> {
|
||||||
|
if self.env_enabled == false {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = self
|
||||||
|
.env_prefix
|
||||||
|
.as_ref()
|
||||||
|
.map(|pre| format!("{}_{}", pre, name))
|
||||||
|
.unwrap_or(name.to_string());
|
||||||
|
env::var(name).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the value of an option if one exists
|
/// Returns the value of an option if one exists
|
||||||
pub fn get_string(&self, name: &str) -> Option<String> {
|
pub fn get_value(&self, name: &str) -> Option<String> {
|
||||||
self.opts
|
self.opts
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|o| {
|
.find_map(|o| {
|
||||||
|
@ -32,6 +53,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
|
.or(self.get_env_value(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Can be used to display the automatic help message for the current command.
|
/// Can be used to display the automatic help message for the current command.
|
||||||
|
@ -39,3 +61,37 @@ impl Context {
|
||||||
crate::command::print_command_help(&self.cmd, &vec![])
|
crate::command::print_command_help(&self.cmd, &vec![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{App, Opt};
|
||||||
|
#[test]
|
||||||
|
fn test_env() {
|
||||||
|
std::env::set_var("thing", "1");
|
||||||
|
App::new()
|
||||||
|
.opt(Opt::flag("thing").short("-t").long("--t"))
|
||||||
|
.handler(|_, ctx, _| {
|
||||||
|
assert_eq!(ctx.flag("thing"), true);
|
||||||
|
})
|
||||||
|
.run_with(vec![])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::env::set_var("thing", "one");
|
||||||
|
App::new()
|
||||||
|
.opt(Opt::scalar("thing").short("-t").long("--t"))
|
||||||
|
.handler(|_, ctx, _| {
|
||||||
|
assert_eq!(ctx.get_value("thing"), Some("one".into()));
|
||||||
|
})
|
||||||
|
.run_with(vec![])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::env::set_var("thing", "one");
|
||||||
|
App::new()
|
||||||
|
.opt(Opt::scalar("thing").short("t").long("thing"))
|
||||||
|
.handler(|_, ctx, _| {
|
||||||
|
assert_eq!(ctx.get_value("thing"), Some("1".into()));
|
||||||
|
})
|
||||||
|
.run_with(vec!["-t".into(), "1".into()])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue