Skip to content

Commit

Permalink
Merge pull request #4248 from epage/ansi
Browse files Browse the repository at this point in the history
feat: Allow users to render usage, help, and errors with ANSI
  • Loading branch information
epage committed Sep 22, 2022
2 parents 8e51065 + 037b075 commit 0380d8d
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 92 deletions.
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

0 comments on commit 0380d8d

Please sign in to comment.