Skip to content

Commit

Permalink
feat(Help): adds the ability for custom help sections
Browse files Browse the repository at this point in the history
Args can now be added to custom help sections. This breaks up the builder pattern a little by adding help section declarations inline, but it's the most intuitive method and doesn't require strange nesting that feels awkward.

```rust
app::new("foo")
    .arg(Arg::with_name("arg1")) // under normal headers
    .help_heading("SPECIAL")
    .arg(Arg::with_name("arg2")) // under SPECIAL: heading
```

Closes clap-rs#805
  • Loading branch information
kbknapp committed Apr 4, 2018
1 parent 4df65d8 commit 8973f22
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 3 deletions.
20 changes: 20 additions & 0 deletions src/app/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,14 @@ impl<'w> Help<'w> {
let opts = parser.has_opts();
let subcmds = parser.has_visible_subcommands();

let custom_headings = custom_headings!(parser.app).fold(0, |acc, arg| {
if arg.help_heading.is_some() {
acc + 1
} else {
acc
}
}) > 0;

let mut first = true;

if pos {
Expand Down Expand Up @@ -731,6 +739,18 @@ impl<'w> Help<'w> {
self.write_args(opts!(parser.app))?;
first = false;
}
if custom_headings {
for heading in parser.app.help_headings.iter()
.filter(|heading| heading.is_some())
.map(|heading| heading.unwrap()) {
if !first {
self.writer.write_all(b"\n\n")?;
}
color!(self, format!("{}:\n", heading), warning)?;
self.write_args(custom_headings!(parser.app).filter(|a| a.help_heading.unwrap() == heading))?;
first = false
}
}
}

if subcmds {
Expand Down
26 changes: 24 additions & 2 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ where
pub help_message: Option<&'a str>,
#[doc(hidden)]
pub version_message: Option<&'a str>,
#[doc(hidden)]
pub help_headings: Vec<Option<&'a str>>,
}

impl<'a, 'b> App<'a, 'b> {
Expand Down Expand Up @@ -634,11 +636,31 @@ impl<'a, 'b> App<'a, 'b> {
/// ```
/// [argument]: ./struct.Arg.html
pub fn arg<A: Into<Arg<'a, 'b>>>(mut self, a: A) -> Self {
self.args.push(a.into());
let help_heading : Option<&'a str> = if let Some(option_str) = self.help_headings.last() {
*option_str
} else {
None
};
let arg = a.into().help_heading(help_heading);
self.args.push(arg);
self
}

/// Set a custom section heading for future args. Every call to arg will
/// have this header (instead of its default header) until a subsequent
/// call to help_heading
pub fn help_heading(mut self, heading: &'a str) -> Self {
self.help_headings.push(Some(heading));
self
}

/// Stop using custom section headings.
pub fn stop_custom_headings(mut self) -> Self {
self.help_headings.push(None);
self
}

/// Adds multiple [arguments] to the list of valid possibilities
/// Adds multiple [arguments] to the list of valid possibilties
///
/// # Examples
///
Expand Down
8 changes: 8 additions & 0 deletions src/args/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ where
pub index: Option<u64>,
#[doc(hidden)]
pub r_ifs: Option<Vec<(&'a str, &'b str)>>,
#[doc(hidden)]
pub help_heading: Option<&'a str>,
}

impl<'a, 'b> Arg<'a, 'b> {
Expand Down Expand Up @@ -3953,6 +3955,12 @@ impl<'a, 'b> Arg<'a, 'b> {
self
}

/// Set a custom heading for this arg to be printed under
pub fn help_heading(mut self, s: Option<&'a str>) -> Self {
self.help_heading = s;
self
}

#[doc(hidden)]
pub fn _build(&mut self) {
if (self.is_set(ArgSettings::UseValueDelimiter)
Expand Down
23 changes: 22 additions & 1 deletion src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ macro_rules! flags {
$app.args.$how()
.filter(|a| !a.settings.is_set(::args::settings::ArgSettings::TakesValue))
.filter(|a| a.short.is_some() || a.long.is_some())
.filter(|a| !a.help_heading.is_some())
};
($app:expr) => {
flags!($app, iter)
Expand All @@ -909,6 +910,7 @@ macro_rules! opts {
$app.args.$how()
.filter(|a| a.settings.is_set(::args::settings::ArgSettings::TakesValue))
.filter(|a| a.short.is_some() || a.long.is_some())
.filter(|a| !a.help_heading.is_some())
};
($app:expr) => {
opts!($app, iter)
Expand All @@ -924,7 +926,9 @@ macro_rules! opts_mut {

macro_rules! positionals {
($app:expr, $how:ident) => {
$app.args.$how().filter(|a| !(a.short.is_some() || a.long.is_some()))
$app.args.$how()
.filter(|a| !a.help_heading.is_some())
.filter(|a| !(a.short.is_some() || a.long.is_some()))
};
($app:expr) => {
positionals!($app, iter)
Expand All @@ -938,6 +942,23 @@ macro_rules! positionals_mut {
}
}

#[allow(unused_macros)]
macro_rules! custom_headings {
($app:expr, $how:ident) => {
$app.args.$how().filter(|a| (a.help_heading.is_some()))
};
($app:expr) => {
custom_headings!($app, iter)
}
}

#[allow(unused_macros)]
macro_rules! custom_headings_mut {
($app:expr) => {
custom_headings!($app, iter_mut)
}
}

macro_rules! subcommands_cloned {
($app:expr, $how:ident) => {
$app.subcommands.$how().cloned()
Expand Down
98 changes: 98 additions & 0 deletions tests/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,23 @@ OPTIONS:
-c, --cafe <FILE> A coffeehouse, coffee shop, or café. [env: ENVVAR=MYVAL]
-p, --pos <VAL> Some vals [possible values: fast, slow]";

static CUSTOM_HELP_SECTION: &'static str = "blorp 1.4
Will M.
does stuff
USAGE:
test --fake <some>:<val>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-f, --fake <some>:<val> some help
NETWORKING:
-n, --no-proxy Do not use system proxy settings";

fn setup() -> App<'static, 'static> {
App::new("test")
.author("Kevin K.")
Expand Down Expand Up @@ -1346,3 +1363,84 @@ fn show_env_vals() {
false
));
}

#[test]
fn custom_headers_headers() {
let app = App::new("blorp")
.author("Will M.")
.about("does stuff")
.version("1.4")
.arg(Arg::from_usage("-f, --fake <some> <val> 'some help'")
.require_delimiter(true)
.value_delimiter(":"),
)
.help_heading("NETWORKING")
.arg(Arg::with_name("no-proxy")
.short("n")
.long("no-proxy")
.help("Do not use system proxy settings")
);

assert!(test::compare_output(
app,
"test --help",
CUSTOM_HELP_SECTION,
false
));
}

static MULTIPLE_CUSTOM_HELP_SECTIONS: &'static str = "blorp 1.4
Will M.
does stuff
USAGE:
test [OPTIONS] --birthday-song <song> --fake <some>:<val>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-f, --fake <some>:<val> some help
-s, --speed <SPEED> How fast? [possible values: fast, slow]
NETWORKING:
-n, --no-proxy Do not use system proxy settings
SPECIAL:
-b, --birthday-song <song> Change which song is played for birthdays";

#[test]
fn multiple_custom_help_headers() {
let app = App::new("blorp")
.author("Will M.")
.about("does stuff")
.version("1.4")
.arg(Arg::from_usage("-f, --fake <some> <val> 'some help'")
.require_delimiter(true)
.value_delimiter(":"),
)
.help_heading("NETWORKING")
.arg(Arg::with_name("no-proxy")
.short("n")
.long("no-proxy")
.help("Do not use system proxy settings")
)
.help_heading("SPECIAL")
.arg(Arg::from_usage("-b, --birthday-song <song> 'Change which song is played for birthdays'"))
.stop_custom_headings()
.arg(Arg::with_name("speed")
.long("speed")
.short("s")
.value_name("SPEED")
.possible_values(&["fast", "slow"])
.help("How fast?")
.takes_value(true));

assert!(test::compare_output(
app,
"test --help",
MULTIPLE_CUSTOM_HELP_SECTIONS,
false
));
}

0 comments on commit 8973f22

Please sign in to comment.