Skip to content

Commit

Permalink
Merged branch help
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyllingene committed Feb 21, 2024
2 parents 10e7682 + bbccdfc commit 518c6ee
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 15 deletions.
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,15 @@ categories = ["command-line-interface", ]
# DO NOT add any dependencies!

[features]
default = ["macros"]
default = ["help", "macros"]
help = []
macros = []

[[example]]
name = "help"
required-features = ["help"]

[[example]]
name = "macros"
required-features = ["help"]

17 changes: 17 additions & 0 deletions examples/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use sarge::prelude::*;

fn main() {
let mut parser = ArgumentReader::new();
parser.doc = Some("An example demonstrating automatic documentation generation.".into());
parser.add::<bool>(tag::both('a', "abc").env("ABC").doc("Super duper docs"));
parser.add::<bool>(tag::short('b').env("BAR"));
parser.add::<String>(tag::long("baz-arg"));
parser.add::<u32>(tag::both('f', "foo").doc("Hello, World!"));
parser.add::<bool>(tag::short('x').doc("Testing testing 123"));
parser.add::<bool>(tag::long("xy").doc("Testing testing 456"));
parser.add::<Vec<i8>>(tag::env("ENV_ONLY").doc(
"This is really, really long, multiline argument\ndocumentation, it'll wrap nicely I hope",
));

parser.print_help();
}
36 changes: 36 additions & 0 deletions examples/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use sarge::prelude::*;

sarge! {
> "This is a basic macros example."
Args,

> "Show this help message."
'h' help: bool,

> "The name to greet."
'n' @NAME name: String,

> "The number of times to greet."
> "Defaults to 1."
#ok times: u32,
}

fn main() {
let args = match Args::parse() {
Ok((a, _)) => a,
Err(e) => {
eprintln!("failed to parse arguments: {e}");
Args::print_help();
std::process::exit(1);
}
};

if args.help {
Args::print_help();
return;
}

for _ in 0..args.times.unwrap_or(1) {
println!("Hello, {}!", args.name);
}
}
172 changes: 172 additions & 0 deletions src/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::num::NonZeroUsize;

use crate::tag::{Cli, Full};

#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct DocParams {
pub(crate) max_doc_width: usize,
pub(crate) has_short: bool,
pub(crate) long_width: Option<NonZeroUsize>,
pub(crate) env_width: Option<NonZeroUsize>,
}

/// If `width.is_none()`, returns a single space. Else, returns width + 2 spaces.
fn empty(width: Option<NonZeroUsize>) -> String {
" ".repeat(width.map_or(0, |x| usize::from(x) + 1) + 1)
}

/// Returns the padding necessary for continuing doc lines.
fn doc_newline(params: DocParams) -> String {
let mut width = if params.has_short { 6 } else { 0 };

if let Some(long_width) = params.long_width {
width += usize::from(long_width) + 1;
}

if let Some(env_width) = params.env_width {
width += usize::from(env_width) + 1;
}

" ".repeat(width)
}

fn wrap_doc(doc: &str, params: DocParams) -> String {
assert!(
params.max_doc_width > 5,
"{} is not wide enough for docs",
params.max_doc_width
);

if doc.len() < params.max_doc_width - 1 {
format!(" : {doc}")
} else {
let mut s = String::from(" : ");
let padding = doc_newline(params);

// TODO: add soft wrapping
let mut width = 2;
for ch in doc.chars() {
if width >= params.max_doc_width {
s.push_str("\n ");
s.push_str(&padding);
s.push(ch);
width = 1;
} else if ch == '\n' {
s.push_str("\n ");
s.push_str(&padding);
width = 0;
} else if ch != '\r' {
s.push(ch);
width += 1;
}
}

s
}
}

pub(crate) fn update_params(params: &mut DocParams, arg: &Full) {
if let Some(cli) = &arg.cli {
match cli {
Cli::Short(_) => params.has_short = true,
Cli::Long(long) => {
params.long_width = Some(
params
.long_width
.map_or(0, usize::from)
.max(long.len())
.try_into()
.unwrap(),
);
}
Cli::Both(_, long) => {
params.has_short = true;
params.long_width = Some(
params
.long_width
.map_or(0, usize::from)
.max(long.len())
.try_into()
.unwrap(),
);
}
}
}

if let Some(env) = &arg.env {
params.env_width = Some(
params
.env_width
.map_or(0, usize::from)
.max(env.len())
.try_into()
.unwrap(),
);
}

params.max_doc_width = (80
- if params.has_short { 3 } else { 0 }
- params.long_width.map_or(0, usize::from)
- params.env_width.map_or(0, usize::from))
.max(12);
}

pub(crate) fn render_argument(arg: &Full, params: DocParams) -> String {
let mut s = String::from(" ");

if let Some(cli) = &arg.cli {
match cli {
Cli::Short(short) => {
s.push('-');
s.push(*short);
s.push(' ');

// s.push_str(" /");
s.push_str(&empty(params.long_width));
}
Cli::Long(long) => {
if params.has_short {
// s.push_str(" / ");
s.push_str(" ");
}

s.push_str("--");
s.push_str(long);
s.push_str(&" ".repeat(usize::from(params.long_width.unwrap()) - long.len()));
}
Cli::Both(short, long) => {
s.push('-');
s.push(*short);

// s.push_str(" / ");
s.push(' ');

s.push_str("--");
s.push_str(long);
s.push_str(&" ".repeat(usize::from(params.long_width.unwrap()) - long.len()));
}
}
} else {
if params.has_short {
s.push_str(" ");
}

if let Some(width) = params.long_width {
s.push_str(&" ".repeat(usize::from(width) + 1));
}
}

if let Some(env) = &arg.env {
s.push_str(" $");
s.push_str(env);
s.push_str(&" ".repeat(usize::from(params.env_width.unwrap()) - env.len()));
} else {
s.push_str(&empty(params.env_width));
}

if let Some(doc) = &arg.doc {
s.push_str(&wrap_doc(doc, params));
}

s
}
46 changes: 45 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ use std::ops::Deref;
pub mod macros;

pub mod tag;
use help::DocParams;
use tag::Full;

mod error;
pub use error::ArgParseError;

#[cfg(feature = "help")]
mod help;

mod types;
pub use types::{ArgResult, ArgumentType};

Expand All @@ -35,13 +39,13 @@ struct InternalArgument {
val: Option<Option<String>>,
}

#[derive(Clone, Debug)]
/// The results of [`ArgumentReader::parse`]. Used both for retrieving
/// [`ArgumentRef`]s and for accessing the
/// [remainder](Arguments::remainder) of the input arguments.
///
/// `Arguments` implements `Deref<Target = [String]>`, so you can treat it
/// like a `&[String]`.
#[derive(Clone, Debug)]
pub struct Arguments {
args: Vec<InternalArgument>,
remainder: Vec<String>,
Expand Down Expand Up @@ -121,6 +125,11 @@ impl<T: ArgumentType> ArgumentRef<T> {
#[allow(clippy::doc_markdown)]
pub struct ArgumentReader {
args: Vec<InternalArgument>,

/// Program-level documentation.
///
/// Only available on feature `help`.
pub doc: Option<String>,
}

impl ArgumentReader {
Expand All @@ -129,6 +138,41 @@ impl ArgumentReader {
Self::default()
}

/// Prints help for all the arguments.
///
/// Only available on feature `help`.
///
/// # Panics
///
/// If the name of the executable could not be found, panics.
#[cfg(feature = "help")]
pub fn print_help(&self) {
println!(
"{} [options...] <arguments...>",
option_env!("CARGO_BIN_NAME")
.map(String::from)
.or_else(|| std::env::current_exe().ok().map(|s| s
.file_stem()
.unwrap()
.to_string_lossy()
.into_owned()))
.expect("failed to get executable"),
);

if let Some(doc) = &self.doc {
println!("{doc}\n");
}

let mut params = DocParams::default();
for arg in &self.args {
help::update_params(&mut params, &arg.tag);
}

for arg in &self.args {
println!("{}", help::render_argument(&arg.tag, params));
}
}

/// Adds an argument to the parser.
pub fn add<T: ArgumentType>(&mut self, tag: Full) -> ArgumentRef<T> {
let arg = InternalArgument {
Expand Down
Loading

0 comments on commit 518c6ee

Please sign in to comment.