Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow users to render usage, help, and errors with ANSI #4248

Merged
merged 4 commits into from Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -227,6 +227,7 @@ Easier to catch changes:
- `ErrorKind::EmptyValue` replaced with `ErrorKind::InvalidValue` to remove an unnecessary special case (#3676, #3968)
- `ErrorKind::UnrecognizedSubcommand` replaced with `ErrorKind::InvalidSubcommand` to remove an unnecessary special case (#3676)
- Changed the default type of `allow_external_subcommands` from `String` to `OsString` as that is less likely to cause bugs in user applications (#3990)
- `Command::render_usage` now returns a `StyledStr`
- *(derive)* Changed the default for arguments from `parse` to `value_parser`, removing `parse` support (#3827, #3981)
- `#[clap(value_parser)]` and `#[clap(action)]` are now redundant
- *(derive)* `subcommand_required(true).arg_required_else_help(true)` is set instead of `SubcommandRequiredElseHelp` to give more meaningful errors when subcommands are missing and to reduce redundancy (#3280)
Expand Down Expand Up @@ -256,6 +257,7 @@ Deprecated
- `Command::trailing_var_arg` in favor of `Arg::trailing_var_arg` to make it clearer which arg it is meant to apply to (#4187)
- `Command::allow_hyphen_values` in favor of `Arg::allow_hyphen_values` to make it clearer which arg it is meant to apply to (#4187)
- `Command::allow_negative_numbers` in favor of `Arg::allow_negative_numbers` to make it clearer which arg it is meant to apply to (#4187)
- *(help)* Deprecated `Command::write_help` and `Command::write_long_help` in favor of `Command::render_help` and `Command::render_long_help`
- *(derive)* `structopt` and `clap` attributes in favor of the more specific `command`, `arg`, and `value` to open the door for [more features](https://github.com/clap-rs/clap/issues/1807) and [clarify relationship to the builder](https://github.com/clap-rs/clap/discussions/4090) (#1807, #4180)
- *(derive)* `#[clap(value_parser)]` and `#[clap(action)]` defaulted attributes (its the default) (#3976)

Expand All @@ -270,8 +272,11 @@ Behavior Changes
- Can now pass runtime generated data to `Command`, `Arg`, `ArgGroup`, `PossibleValue`, etc without managing lifetimes with the `string` feature flag (#2150, #4223)
- New default `error-context`, `help` and `usage` feature flags that can be turned off for smaller binaries (#4236)
- *(error)* `Error::apply` for changing the formatter for dropping binary size (#4111)
- *(error)* `Command::render` which can render to plain text or ANSI escape codes
- *(help)* Show `PossibleValue::help` in long help (`--help`) (#3312)
- *(help)* New `{tab}` variable for `Command::help_template` (#4161)
- *(help)* `Command::render_help` and `Command::render_long_help` which can render to plain text or ANSI escape codes
- *(help)* `Command::render_usage` now returns a `StyledStr` which can render to plain text or ANSI escape codes

### Fixes

Expand Down
7 changes: 2 additions & 5 deletions clap_bench/benches/04_new_help.rs
@@ -1,13 +1,10 @@
use clap::Command;
use clap::{arg, Arg, ArgAction};
use criterion::{criterion_group, criterion_main, Criterion};
use std::io::Cursor;

fn build_help(cmd: &mut Command) -> String {
let mut buf = Cursor::new(Vec::with_capacity(50));
cmd.write_help(&mut buf).unwrap();
let content = buf.into_inner();
String::from_utf8(content).unwrap()
let help = cmd.render_help();
help.to_string()
}

fn app_example1() -> Command {
Expand Down
7 changes: 2 additions & 5 deletions clap_bench/benches/05_ripgrep.rs
Expand Up @@ -6,7 +6,6 @@
use clap::{value_parser, Arg, ArgAction, Command};
use criterion::{criterion_group, criterion_main, Criterion};
use std::collections::HashMap;
use std::io::Cursor;

use lazy_static::lazy_static;

Expand Down Expand Up @@ -281,10 +280,8 @@ fn app_long() -> Command {

/// Build the help text of an application.
fn build_help(cmd: &mut Command) -> String {
let mut buf = Cursor::new(Vec::with_capacity(50));
cmd.write_help(&mut buf).unwrap();
let content = buf.into_inner();
String::from_utf8(content).unwrap()
let help = cmd.render_help();
help.to_string()
}

/// Build a clap application parameterized by usage strings.
Expand Down
52 changes: 41 additions & 11 deletions src/builder/command.rs
Expand Up @@ -759,9 +759,9 @@ impl Command {
c.print()
}

/// Writes the short help message (`-h`) to a [`io::Write`] object.
/// Render the short help message (`-h`) to a [`StyledStr`]
///
/// See also [`Command::write_long_help`].
/// See also [`Command::render_long_help`].
///
/// # Examples
///
Expand All @@ -770,24 +770,24 @@ impl Command {
/// use std::io;
/// let mut cmd = Command::new("myprog");
/// let mut out = io::stdout();
/// cmd.write_help(&mut out).expect("failed to write to stdout");
/// let help = cmd.render_help();
/// println!("{}", help);
/// ```
/// [`io::Write`]: std::io::Write
/// [`-h` (short)]: Arg::help()
/// [`--help` (long)]: Arg::long_help()
pub fn write_help<W: io::Write>(&mut self, w: &mut W) -> io::Result<()> {
pub fn render_help(&mut self) -> StyledStr {
self._build_self(false);

let mut styled = StyledStr::new();
let usage = Usage::new(self);
write_help(&mut styled, self, &usage, false);
write!(w, "{}", styled)?;
w.flush()
styled
}

/// Writes the long help message (`--help`) to a [`io::Write`] object.
/// Render the long help message (`--help`) to a [`StyledStr`].
///
/// See also [`Command::write_help`].
/// See also [`Command::render_help`].
///
/// # Examples
///
Expand All @@ -796,11 +796,41 @@ impl Command {
/// use std::io;
/// let mut cmd = Command::new("myprog");
/// let mut out = io::stdout();
/// cmd.write_long_help(&mut out).expect("failed to write to stdout");
/// let help = cmd.render_long_help();
/// println!("{}", help);
/// ```
/// [`io::Write`]: std::io::Write
/// [`-h` (short)]: Arg::help()
/// [`--help` (long)]: Arg::long_help()
pub fn render_long_help(&mut self) -> StyledStr {
self._build_self(false);

let mut styled = StyledStr::new();
let usage = Usage::new(self);
write_help(&mut styled, self, &usage, true);
styled
}

#[doc(hidden)]
#[cfg_attr(
feature = "deprecated",
deprecated(since = "4.0.0", note = "Replaced with `Command::render_help`")
)]
pub fn write_help<W: io::Write>(&mut self, w: &mut W) -> io::Result<()> {
self._build_self(false);

let mut styled = StyledStr::new();
let usage = Usage::new(self);
write_help(&mut styled, self, &usage, false);
write!(w, "{}", styled)?;
w.flush()
}

#[doc(hidden)]
#[cfg_attr(
feature = "deprecated",
deprecated(since = "4.0.0", note = "Replaced with `Command::render_long_help`")
)]
pub fn write_long_help<W: io::Write>(&mut self, w: &mut W) -> io::Result<()> {
self._build_self(false);

Expand Down Expand Up @@ -869,8 +899,8 @@ impl Command {
/// let mut cmd = Command::new("myprog");
/// println!("{}", cmd.render_usage());
/// ```
pub fn render_usage(&mut self) -> String {
self.render_usage_().unwrap_or_default().to_string()
pub fn render_usage(&mut self) -> StyledStr {
self.render_usage_().unwrap_or_default()
}

pub(crate) fn render_usage_(&mut self) -> Option<StyledStr> {
Expand Down
26 changes: 26 additions & 0 deletions src/builder/styled_str.rs
Expand Up @@ -22,6 +22,12 @@ impl StyledStr {
}
}

/// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling
#[cfg(feature = "color")]
pub fn ansi(&self) -> impl std::fmt::Display + '_ {
AnsiDisplay { styled: self }
}

pub(crate) fn header(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Header), msg.into());
}
Expand Down Expand Up @@ -289,6 +295,26 @@ impl std::fmt::Display for StyledStr {
}
}

#[cfg(feature = "color")]
struct AnsiDisplay<'s> {
styled: &'s StyledStr,
}

#[cfg(feature = "color")]
impl std::fmt::Display for AnsiDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut buffer = termcolor::Buffer::ansi();
self.styled
.write_colored(&mut buffer)
.map_err(|_| std::fmt::Error)?;
let buffer = buffer.into_inner();
let buffer = String::from_utf8(buffer).map_err(|_| std::fmt::Error)?;
std::fmt::Display::fmt(&buffer, f)?;

Ok(())
}
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Style {
Header,
Expand Down
21 changes: 21 additions & 0 deletions src/error/mod.rs
Expand Up @@ -195,6 +195,27 @@ impl<F: ErrorFormatter> Error<F> {
c.print()
}

/// Render the error message to a [`StyledStr`].
///
/// # Example
/// ```no_run
/// use clap::Command;
///
/// match Command::new("Command").try_get_matches() {
/// Ok(matches) => {
/// // do_something
/// },
/// Err(err) => {
/// let err = err.render();
/// println!("{}", err);
/// // do_something
/// },
/// };
/// ```
pub fn render(&self) -> StyledStr {
self.formatted().into_owned()
}

fn new(kind: ErrorKind) -> Self {
Self {
inner: Box::new(ErrorInner {
Expand Down
11 changes: 3 additions & 8 deletions tests/builder/help.rs
Expand Up @@ -1954,11 +1954,7 @@ fn after_help_no_args() {
.disable_version_flag(true)
.after_help("This is after help.");

let help = {
let mut output = Vec::new();
cmd.write_help(&mut output).unwrap();
String::from_utf8(output).unwrap()
};
let help = cmd.render_help().to_string();

assert_eq!(help, AFTER_HELP_NO_ARGS);
}
Expand Down Expand Up @@ -2680,9 +2676,8 @@ Options:
cmd.build();
let subcmd = cmd.find_subcommand_mut("test").unwrap();

let mut buf = Vec::new();
subcmd.write_help(&mut buf).unwrap();
utils::assert_eq(EXPECTED, String::from_utf8(buf).unwrap());
let help = subcmd.render_help().to_string();
utils::assert_eq(EXPECTED, help);
}

#[test]
Expand Down
3 changes: 1 addition & 2 deletions tests/builder/multiple_values.rs
Expand Up @@ -426,8 +426,7 @@ fn optional_value() {
assert!(m.contains_id("port"));
assert_eq!(m.get_one::<String>("port").unwrap(), "42");

let mut help = Vec::new();
cmd.write_help(&mut help).unwrap();
let help = cmd.render_help().to_string();
const HELP: &str = "\
Usage: test [OPTIONS]

Expand Down
13 changes: 8 additions & 5 deletions tests/builder/positionals.rs
Expand Up @@ -211,13 +211,13 @@ fn positional_hyphen_does_not_panic() {
#[test]
fn single_positional_usage_string() {
let mut cmd = Command::new("test").arg(arg!([FILE] "some file"));
crate::utils::assert_eq(cmd.render_usage(), "Usage: test [FILE]");
crate::utils::assert_eq(cmd.render_usage().to_string(), "Usage: test [FILE]");
}

#[test]
fn single_positional_multiple_usage_string() {
let mut cmd = Command::new("test").arg(arg!([FILE]... "some file"));
crate::utils::assert_eq(cmd.render_usage(), "Usage: test [FILE]...");
crate::utils::assert_eq(cmd.render_usage().to_string(), "Usage: test [FILE]...");
}

#[test]
Expand All @@ -226,7 +226,7 @@ fn multiple_positional_usage_string() {
.arg(arg!([FILE] "some file"))
.arg(arg!([FILES]... "some file"));
crate::utils::assert_eq(
cmd.render_usage(),
cmd.render_usage().to_string(),
"\
Usage: test [FILE] [FILES]...",
);
Expand All @@ -237,13 +237,16 @@ fn multiple_positional_one_required_usage_string() {
let mut cmd = Command::new("test")
.arg(arg!(<FILE> "some file"))
.arg(arg!([FILES]... "some file"));
crate::utils::assert_eq(cmd.render_usage(), "Usage: test <FILE> [FILES]...");
crate::utils::assert_eq(
cmd.render_usage().to_string(),
"Usage: test <FILE> [FILES]...",
);
}

#[test]
fn single_positional_required_usage_string() {
let mut cmd = Command::new("test").arg(arg!(<FILE> "some file"));
crate::utils::assert_eq(cmd.render_usage(), "Usage: test <FILE>");
crate::utils::assert_eq(cmd.render_usage().to_string(), "Usage: test <FILE>");
}

// This tests a programmer error and will only succeed with debug_assertions
Expand Down
18 changes: 8 additions & 10 deletions tests/builder/subcommands.rs
Expand Up @@ -343,15 +343,14 @@ fn subcommand_placeholder_test() {
.subcommand_value_name("TEST_PLACEHOLDER")
.subcommand_help_heading("TEST_HEADER");

assert_eq!(&cmd.render_usage(), "Usage: myprog [TEST_PLACEHOLDER]");
assert_eq!(
&cmd.render_usage().to_string(),
"Usage: myprog [TEST_PLACEHOLDER]"
);

let mut help_text = Vec::new();
cmd.write_help(&mut help_text)
.expect("Failed to write to internal buffer");
let help_text = cmd.render_help().to_string();

assert!(String::from_utf8(help_text)
.unwrap()
.contains("TEST_HEADER:"));
assert!(help_text.contains("TEST_HEADER:"));
}

#[test]
Expand Down Expand Up @@ -627,9 +626,8 @@ Options:
let subcmd = cmd.find_subcommand_mut("foo").unwrap();
let subcmd = subcmd.find_subcommand_mut("bar").unwrap();

let mut buf = Vec::new();
subcmd.write_help(&mut buf).unwrap();
utils::assert_eq(EXPECTED, String::from_utf8(buf).unwrap());
let help = subcmd.render_help().to_string();
utils::assert_eq(EXPECTED, help);
}

#[test]
Expand Down
20 changes: 8 additions & 12 deletions tests/derive/app_name.rs
@@ -1,14 +1,16 @@
use clap::CommandFactory;
use clap::Parser;

use crate::utils::get_help;
use crate::utils::get_long_help;

#[test]
fn app_name_in_short_help_from_struct() {
#[derive(Parser)]
#[command(name = "my-cmd")]
struct MyApp {}

let mut help = Vec::new();
MyApp::command().write_help(&mut help).unwrap();
let help = String::from_utf8(help).unwrap();
let help = get_help::<MyApp>();

assert!(help.contains("my-cmd"));
}
Expand All @@ -19,9 +21,7 @@ fn app_name_in_long_help_from_struct() {
#[command(name = "my-cmd")]
struct MyApp {}

let mut help = Vec::new();
MyApp::command().write_long_help(&mut help).unwrap();
let help = String::from_utf8(help).unwrap();
let help = get_help::<MyApp>();

assert!(help.contains("my-cmd"));
}
Expand All @@ -32,9 +32,7 @@ fn app_name_in_short_help_from_enum() {
#[command(name = "my-cmd")]
enum MyApp {}

let mut help = Vec::new();
MyApp::command().write_help(&mut help).unwrap();
let help = String::from_utf8(help).unwrap();
let help = get_help::<MyApp>();

assert!(help.contains("my-cmd"));
}
Expand All @@ -45,9 +43,7 @@ fn app_name_in_long_help_from_enum() {
#[command(name = "my-cmd")]
enum MyApp {}

let mut help = Vec::new();
MyApp::command().write_long_help(&mut help).unwrap();
let help = String::from_utf8(help).unwrap();
let help = get_long_help::<MyApp>();

assert!(help.contains("my-cmd"));
}
Expand Down