General imrovements

Added app! macro that can extract package name and version.

Opts can be parsed for the application root prior to launching handling
functions.

Added further integration with the console create

Improved display of description_lists, these now wrap the description
text.

Long and short opts are now optional and one or both can be used.
This commit is contained in:
Joe Bellus 2021-11-13 21:32:30 -05:00
parent e38edc6a68
commit 1b747133e6
9 changed files with 228 additions and 33 deletions

56
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "arkham"
version = "0.1.1"
@ -10,6 +19,7 @@ dependencies = [
"criterion",
"serde",
"serde_json",
"textwrap 0.14.2",
"toml",
]
@ -76,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"bitflags",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
]
@ -312,6 +322,12 @@ dependencies = [
"libc",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "oorandom"
version = "11.1.3"
@ -404,7 +420,10 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
@ -508,6 +527,12 @@ dependencies = [
"serde",
]
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "syn"
version = "1.0.73"
@ -538,6 +563,26 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
"once_cell",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
@ -563,6 +608,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
dependencies = [
"regex",
]
[[package]]
name = "unicode-width"
version = "0.1.8"

View File

@ -19,11 +19,14 @@ console = "0.14.0"
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true}
toml = { version="0.5.8", optional = true}
textwrap = "0.14.2"
[features]
config = ["serde"]
config = ["serialize"]
config_toml = ["toml", "config"]
config_json = ["serde_json", "config"]
serialize = ["serde"]
[dev-dependencies]
criterion = "0.3"

View File

@ -110,6 +110,29 @@ impl App {
self
}
/// Similar to `App::Command` but allows multiple commands to be added at once.
/// Adds a root level commands to the application. This command can then be executed with:
///
/// myapp command_name
///
/// Help flags will also be generated for the command which will display command
/// information for:
///
/// myapp --help command_name or myapp help command_name
///
/// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// let names = vec!["one", "two", "three"];
/// let commands = names.into_iter().map(|name| Command::new(name).handler(my_handler)).collect();
/// App::new().commands(commands);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn commands(self, commands: Vec<Command>) -> Self {
commands.into_iter().fold(self, |app, cmd| app.command(cmd))
}
/// Adds a root level opt/flag that is available to all commands. Opts are given a name which
/// is used to reference them, as well as a short and long identifier.
///
@ -152,9 +175,7 @@ impl App {
self
}
/// Execute the app and any specified handlers based on the passed arguemnts. This function is
/// mostly used for testing or any situation where you need to pass arbitrary arguments instead
/// of using the ones passed to the application.
/// Execute the app and any specified handlers based on the passed arguemnts. This function is mostly used for testing or any situation where you need to pass arbitrary arguments instead of using the ones passed to the application.
/// Example:
/// ```rust
/// use arkham::{App, Command, Context, Opt};
@ -184,8 +205,7 @@ impl App {
}
}
/// Execute the app and any specified handlers based on the arguments passsed to the
/// application.
/// Execute the app and any specified handlers based on the arguments passsed to the application.
///
/// Example:
/// running with myapp --name alice
@ -203,11 +223,17 @@ impl App {
pub fn run(&mut self) -> Result<()> {
self.run_with(env::args().skip(1).collect())
}
/// Generates a context for the root command allowing flags to be parsed. This can be useful to predetect certian root level arguments before processing sub commands and other app configurations.
pub fn root_context(&self) -> Result<Context> {
let mut ctx = Context::new(self.root.clone());
let args = env::args().skip(1).collect::<Vec<_>>();
populate_args(&mut ctx, &self.root, &args)?;
Ok(ctx)
}
}
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> {
fn populate_args(ctx: &mut Context, cmd: &Command, args: &[String]) -> Result<Vec<String>> {
// Get an iterator for the incomming arguments
let mut args = args.iter();
// We will keep track of any arguments that arent consumed by the current command.
@ -218,7 +244,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
while let Some(arg) = args.next() {
// Check for long args
if arg.starts_with("--") {
if let Some(opt) = cmd.opts.iter().find(|o| o.long == arg[2..]) {
if let Some(opt) = cmd
.opts
.iter()
.find(|o| o.long == Some(arg[2..].to_string()))
{
match opt.kind {
OptKind::Flag => {
ctx.set_flag(opt);
@ -237,9 +267,13 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
continue;
}
// Check for short args
if arg.starts_with("-") {
if arg.starts_with('-') {
for switch in arg.chars().skip(1) {
if let Some(opt) = cmd.opts.iter().find(|o| o.short == switch.to_string()) {
if let Some(opt) = cmd
.opts
.iter()
.find(|o| o.short == Some(switch.to_string()))
{
match opt.kind {
OptKind::Flag => {
ctx.set_flag(opt);
@ -261,6 +295,14 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
ignored.push(arg.clone());
}
Ok(ignored)
}
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> {
let mut ignored = populate_args(ctx, cmd, args)?;
// Find an recurse into sub commands if the remaining argumetns match any subcommand name
if let Some(cmd) = cmd
.commands
@ -381,9 +423,9 @@ mod tests {
.opt(Opt::flag("thingB").short("b").long("b"))
.opt(Opt::flag("thingC").short("c").long("c"))
.handler(|_, ctx, _| {
assert_eq!(ctx.flag("thingA"), true);
assert_eq!(ctx.flag("thingB"), true);
assert_eq!(ctx.flag("thingC"), true);
assert!(ctx.flag("thingA"));
assert!(ctx.flag("thingB"));
assert!(ctx.flag("thingC"));
})
.run_with(vec!["-abc".into()])
.unwrap();

View File

@ -1,3 +1,5 @@
use std::fmt::Debug;
use crate::{
context::Context,
opt::{self, Opt},
@ -16,6 +18,12 @@ pub struct Command {
pub short_desc: Option<String>,
}
impl Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").field("name", &self.name).finish()
}
}
impl Command {
pub fn new<T: Into<String>>(name: T) -> Self {
Command {
@ -79,7 +87,7 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
}
if !cmd.commands.is_empty() {
vox::header("Subcommands");
vox::header("Commands");
vox::description_list(
cmd.commands
.iter()
@ -87,4 +95,5 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
.collect(),
)
}
std::process::exit(1);
}

View File

@ -136,7 +136,7 @@ impl From<bool> for ContextValue {
}
pub struct Context {
pub(crate) cmd: Command,
pub cmd: Command,
pub(crate) config_data: Map,
pub(crate) env_prefix: Option<String>,
pub(crate) env_enabled: bool,

View File

@ -1,3 +1,6 @@
#[macro_use]
mod macros;
mod app;
mod command;
mod context;
@ -8,3 +11,4 @@ pub use app::*;
pub use command::*;
pub use context::*;
pub use opt::*;
pub use vox::*;

8
src/macros.rs Normal file
View File

@ -0,0 +1,8 @@
#[macro_export]
macro_rules! app {
() => {
App::default()
.version(env!("CARGO_PKG_VERSION"))
.name(env!("CARGO_PKG_NAME"))
};
}

View File

@ -6,8 +6,8 @@ pub enum OptError {
#[derive(Clone, Debug)]
pub struct Opt {
pub name: String,
pub short: String,
pub long: String,
pub short: Option<String>,
pub long: Option<String>,
pub desc: Option<String>,
pub(crate) kind: OptKind,
}
@ -23,8 +23,8 @@ impl Opt {
pub fn flag(name: &str) -> Self {
Self {
name: name.into(),
short: "".into(),
long: "".into(),
short: None,
long: None,
kind: OptKind::Flag,
desc: None,
}
@ -40,8 +40,8 @@ impl Opt {
pub fn scalar(name: &str) -> Self {
Self {
name: name.into(),
short: "".into(),
long: "".into(),
short: None,
long: None,
kind: OptKind::String,
desc: None,
}
@ -55,7 +55,7 @@ impl Opt {
/// App::new().opt(Opt::scalar("user").short("u").long("user"));
///```
pub fn short(mut self, short: &str) -> Self {
self.short = short.into();
self.short = Some(short.into());
self
}
@ -67,7 +67,7 @@ impl Opt {
/// App::new().opt(Opt::scalar("user").short("u").long("user"));
///```
pub fn long(mut self, long: &str) -> Self {
self.long = long.into();
self.long = Some(long.into());
self
}
@ -92,10 +92,32 @@ impl Opt {
pub(crate) fn usage(&self) -> String {
match self.kind {
OptKind::Flag => {
format!("-{}, --{}", self.short, self.long)
let short = self
.short
.as_ref()
.map(|s| format!("-{}", s))
.unwrap_or_else(String::new);
let long = self
.long
.as_ref()
.map(|s| format!("--{}", s))
.unwrap_or_else(String::new);
vec![short, long].join(",").trim_matches(',').to_string()
}
OptKind::String => {
format!("-{} [value], --{} [value]", self.short, self.long)
let short = self
.short
.as_ref()
.map(|s| format!("-{} [value]", s))
.unwrap_or_else(String::new);
let long = self
.long
.as_ref()
.map(|s| format!("--{} [value]", s))
.unwrap_or_else(String::new);
vec![short, long].join(",").trim_matches(',').to_string()
}
}
}

View File

@ -2,6 +2,41 @@ use console::style;
use std::collections::HashMap;
use std::fmt::Display;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Color256(u8),
}
impl From<Color> for console::Color {
fn from(fr: Color) -> Self {
match fr {
Color::Black => Self::Black,
Color::Red => Self::Red,
Color::Green => Self::Green,
Color::Yellow => Self::Yellow,
Color::Blue => Self::Blue,
Color::Magenta => Self::Magenta,
Color::Cyan => Self::Cyan,
Color::White => Self::White,
Color::Color256(v) => Self::Color256(v),
}
}
}
pub fn labeled(color: Color, label: &str, msg: &str) {
let label = format!("[{}]", label);
println!("{} {}", console::style(label).fg(color.into()).bold(), msg);
}
pub fn message<T: Display>(str: T) {
println!("{}", style(str).white().bold());
}
@ -20,11 +55,29 @@ pub fn note<T: Display>(str: T) {
}
pub fn description_list(list: HashMap<String, String>) {
let max_length = list.keys().map(|v| v.len()).max().unwrap_or(10) + 3;
for (name, desc) in list {
let spaced_name = format!("{:width$}", name, width = max_length);
println!("{}{}", style(spaced_name).bold(), style(desc).dim())
}
let mut lines = list.into_iter().collect::<Vec<(String, String)>>();
let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3;
let desc_max_length = 80 - key_max_length;
let indent_string = " ".repeat(key_max_length);
lines.sort_by(|(a, _), (b, _)| a.cmp(b));
let output = lines.iter().fold(String::new(), |output, (name, desc)| {
let spaced_key = format!("{:width$}", name, width = key_max_length);
let mut desc_lines: Vec<String> = textwrap::fill(desc, desc_max_length)
.split('\n')
.map(|s| s.to_string())
.collect();
desc_lines.reverse();
let first_line = desc_lines.pop().unwrap_or_else(String::new);
desc_lines.reverse();
output
+ &format!("{}{}", style(spaced_key).bold(), style(first_line).dim())
+ "\n"
+ &desc_lines
.iter()
.map(|s| indent_string.clone() + s + "\n")
.collect::<String>()
});
println!("{}", output);
}
pub fn print<T: Display>(s: T) {