Use a template to produce the default help message.

This makes some changes to the template system:

- Template tags for optional items (like {author}) now expand to
  nothing when the value is unset instead of a default string (like
  "unknown author").
- Many template tags now emit line-wrapped output to match
  write_default_help.
- Items with long variants now expand to the appropriate thing for -h
  vs --help.
- The now-obsolete {long-about} tag has been removed.
- A few new tags have been added.

These are externally-visible changes, but if this makes it into 3.0
that's probably reasonable?

Note that line-wrapping can have some odd edge cases since it does not
account for preceding/trailing characters on the same line as the tag.
This is already the case in master, but will affect some additional
tags with this changeset. See #2065 for details.

Closes #2002.
This commit is contained in:
Matt Kantor 2020-08-11 20:05:32 -07:00
parent ca6c84fa8a
commit a87320ae88
3 changed files with 107 additions and 154 deletions

View file

@ -724,26 +724,30 @@ impl<'help> App<'help> {
/// **NOTE:** The template system is by design very simple. Therefore, the
/// tags have to be written in the lowercase and without spacing.
///
/// Tags arg given inside curly brackets.
/// Tags are given inside curly brackets.
///
/// Valid tags are:
///
/// * `{bin}` - Binary name.
/// * `{version}` - Version number.
/// * `{author}` - Author information.
/// * `{about}` - General description (from [`App::about`])
/// * `{usage}` - Automatically generated or given usage string.
/// * `{all-args}` - Help for all arguments (options, flags, positionals arguments,
/// and subcommands) including titles.
/// * `{unified}` - Unified help for options and flags. Note, you must *also* set
/// [`AppSettings::UnifiedHelpMessage`] to fully merge both options and
/// flags, otherwise the ordering is "best effort"
/// * `{flags}` - Help for flags.
/// * `{options}` - Help for options.
/// * `{positionals}` - Help for positionals arguments.
/// * `{subcommands}` - Help for subcommands.
/// * `{after-help}` - Help from [`App::after_help`]
/// * `{before-help}` - Help from [`App::before_help`]
/// * `{bin}` - Binary name.
/// * `{version}` - Version number.
/// * `{author}` - Author information.
/// * `{author-with-newline}` - Author followed by `\n`.
/// * `{about}` - Description of the program.
/// * `{about-with-newline}` - Description followed by `\n`.
/// * `{usage}` - Automatically generated or given usage string.
/// * `{all-args}` - Help for all arguments (options, flags, positional
/// arguments, and subcommands) including titles.
/// * `{unified}` - Unified help for options and flags. Note, you must *also*
/// set [`AppSettings::UnifiedHelpMessage`] to fully merge both
/// options and flags, otherwise the ordering is "best effort".
/// * `{flags}` - Help for flags.
/// * `{options}` - Help for options.
/// * `{positionals}` - Help for positional arguments.
/// * `{subcommands}` - Help for subcommands.
/// * `{after-help}` - Info to be displayed after the help message.
/// * `{after-help-padded}` - After help message with additional spacing.
/// * `{before-help}` - Info to be displayed before the help message.
/// * `{before-help-padded}` - Before help message with additional spacing.
///
/// # Examples
///
@ -754,9 +758,6 @@ impl<'help> App<'help> {
/// .help_template("{bin} ({version}) - {usage}")
/// # ;
/// ```
/// [`App::about`]: ./struct.App.html#method.about
/// [`App::after_help`]: ./struct.App.html#method.after_help
/// [`App::before_help`]: ./struct.App.html#method.before_help
/// [`AppSettings::UnifiedHelpMessage`]: ./enum.AppSettings.html#variant.UnifiedHelpMessage
pub fn help_template<S: Into<&'help str>>(mut self, s: S) -> Self {
self.template = Some(s.into());
@ -2448,11 +2449,6 @@ impl<'help> App<'help> {
!self.args.is_empty()
}
#[inline]
pub(crate) fn has_opts(&self) -> bool {
self.get_opts_no_heading().count() > 0
}
#[inline]
pub(crate) fn has_flags(&self) -> bool {
self.get_flags_no_heading().count() > 0

View file

@ -74,6 +74,14 @@ pub(crate) struct Help<'help, 'app, 'parser, 'writer> {
// Public Functions
impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
const DEFAULT_TEMPLATE: &'static str = "\
{before-help-padded}{bin} {version}\n\
{author-with-newline}{about-with-newline}\n\
USAGE:\n {usage}\n\
\n\
{all-args}{after-help-padded}\
";
/// Create a new `Help` instance.
pub(crate) fn new(
w: HelpWriter<'writer>,
@ -116,7 +124,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
} else if let Some(tmpl) = self.parser.app.template {
self.write_templated_help(tmpl)?;
} else {
self.write_default_help()?;
self.write_templated_help(Self::DEFAULT_TEMPLATE)?;
}
self.none("\n")?;
@ -811,13 +819,6 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
Ok(())
}
/// Writes version of a Parser Object to the wrapped stream.
fn write_version(&mut self) -> io::Result<()> {
debug!("Help::write_version");
self.none(self.parser.app.version.unwrap_or(""))?;
Ok(())
}
/// Writes binary name of a Parser Object to the wrapped stream.
fn write_bin_name(&mut self) -> io::Result<()> {
debug!("Help::write_bin_name");
@ -839,98 +840,6 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
}
Ok(())
}
/// Writes default help for a Parser Object to the wrapped stream.
pub(crate) fn write_default_help(&mut self) -> ClapResult<()> {
debug!("Help::write_default_help");
if self.use_long {
if let Some(h) = self
.parser
.app
.before_long_help
.or_else(|| self.parser.app.before_help)
{
self.write_before_after_help(h)?;
self.none("\n\n")?;
}
} else if let Some(h) = self
.parser
.app
.before_help
.or_else(|| self.parser.app.before_long_help)
{
self.write_before_after_help(h)?;
self.none("\n\n")?;
}
macro_rules! write_thing {
($thing:expr) => {{
self.none(&wrap_help(&$thing, self.term_w))?;
self.none("\n")?
}};
}
// Print the version
self.write_bin_name()?;
self.none(" ")?;
self.write_version()?;
self.none("\n")?;
if let Some(author) = self.parser.app.author {
write_thing!(author);
}
if self.use_long && self.parser.app.long_about.is_some() {
debug!("Help::write_default_help: writing long about");
write_thing!(self.parser.app.long_about.unwrap());
} else if self.parser.app.about.is_some() {
debug!("Help::write_default_help: writing about");
write_thing!(self.parser.app.about.unwrap());
}
self.none("\n")?;
self.warning("USAGE:")?;
self.none(&format!(
"\n{}{}\n\n",
TAB,
Usage::new(self.parser).create_usage_no_title(&[])
))?;
let flags = self.parser.has_flags();
let pos = self.parser.has_positionals();
let opts = self.parser.has_opts();
let subcmds = self.parser.has_subcommands();
if flags || opts || pos || subcmds {
self.write_all_args()?;
}
if self.use_long {
if let Some(h) = self
.parser
.app
.after_long_help
.or_else(|| self.parser.app.after_help)
{
if flags || opts || pos || subcmds {
self.none("\n\n")?;
}
self.write_before_after_help(h)?;
}
} else if let Some(h) = self
.parser
.app
.after_help
.or_else(|| self.parser.app.after_long_help)
{
if flags || opts || pos || subcmds {
self.none("\n\n")?;
}
self.write_before_after_help(h)?;
}
self.writer.flush().map_err(Error::from)
}
}
/// Possible results for a copying function that stops when a given
@ -1034,28 +943,13 @@ fn copy_and_capture<R: Read, W: Write>(
impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
/// Write help to stream for the parser in the format defined by the template.
///
/// Tags arg given inside curly brackets:
/// Valid tags are:
/// * `{bin}` - Binary name.
/// * `{version}` - Version number.
/// * `{author}` - Author information.
/// * `{usage}` - Automatically generated or given usage string.
/// * `{all-args}` - Help for all arguments (options, flags, positionals arguments,
/// and subcommands) including titles.
/// * `{unified}` - Unified help for options and flags.
/// * `{flags}` - Help for flags.
/// * `{options}` - Help for options.
/// * `{positionals}` - Help for positionals arguments.
/// * `{subcommands}` - Help for subcommands.
/// * `{after-help}` - Info to be displayed after the help message.
/// * `{before-help}` - Info to be displayed before the help message.
/// For details about the template language see [`App::help_template`].
///
/// The template system is, on purpose, very simple. Therefore, the tags have to be written
/// in the lowercase and without spacing.
/// [`App::help_template`]: ./struct.App.html#method.help_template
fn write_templated_help(&mut self, template: &str) -> ClapResult<()> {
debug!("Help::write_templated_help");
let mut tmplr = Cursor::new(&template);
let mut tag_buf = Cursor::new(vec![0u8; 15]);
let mut tag_buf = Cursor::new(vec![0u8; 20]);
// The strategy is to copy the template from the reader to wrapped stream
// until a tag is found. Depending on its value, the appropriate content is copied
@ -1083,16 +977,41 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
self.write_bin_name()?;
}
b"version" => {
self.none(self.parser.app.version.unwrap_or("unknown version"))?;
if let Some(output) = self.parser.app.version {
self.none(output)?;
}
}
b"author" => {
self.none(self.parser.app.author.unwrap_or("unknown author"))?;
if let Some(output) = self.parser.app.author {
self.none(output)?;
}
}
b"author-with-newline" => {
if let Some(output) = self.parser.app.author {
self.none(&wrap_help(output, self.term_w))?;
self.none("\n")?;
}
}
b"about" => {
self.none(self.parser.app.about.unwrap_or("unknown about"))?;
let about = if self.use_long {
self.parser.app.long_about.or(self.parser.app.about)
} else {
self.parser.app.about
};
if let Some(output) = about {
self.none(output)?;
}
}
b"long-about" => {
self.none(self.parser.app.long_about.unwrap_or("unknown about"))?;
b"about-with-newline" => {
let about = if self.use_long {
self.parser.app.long_about.or(self.parser.app.about)
} else {
self.parser.app.about
};
if let Some(output) = about {
self.none(&wrap_help(output, self.term_w))?;
self.none("\n")?;
}
}
b"usage" => {
self.none(&Usage::new(self.parser).create_usage_no_title(&[]))?;
@ -1124,10 +1043,52 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> {
self.write_subcommands(self.parser.app)?;
}
b"after-help" => {
self.none(self.parser.app.after_help.unwrap_or("unknown after-help"))?;
let after_help = if self.use_long {
self.parser
.app
.after_long_help
.or(self.parser.app.after_help)
} else {
self.parser.app.after_help
};
if let Some(output) = after_help {
self.write_before_after_help(output)?;
}
}
b"after-help-padded" => {
let after_help = if self.use_long {
self.parser
.app
.after_long_help
.or(self.parser.app.after_help)
} else {
self.parser.app.after_help
};
if let Some(output) = after_help {
self.none("\n\n")?;
self.write_before_after_help(output)?;
}
}
b"before-help" => {
self.none(self.parser.app.before_help.unwrap_or("unknown before-help"))?;
let before_help = if self.use_long {
self.parser.app.before_long_help
} else {
self.parser.app.before_help
};
if let Some(output) = before_help {
self.write_before_after_help(output)?;
}
}
b"before-help-padded" => {
let before_help = if self.use_long {
self.parser.app.before_long_help
} else {
self.parser.app.before_help
};
if let Some(output) = before_help {
self.write_before_after_help(output)?;
self.none("\n\n")?;
}
}
// Unknown tag, write it back.
r => {

View file

@ -1790,10 +1790,6 @@ impl<'help, 'app> Parser<'help, 'app> {
self.app.has_args()
}
pub(crate) fn has_opts(&self) -> bool {
self.app.has_opts()
}
pub(crate) fn has_flags(&self) -> bool {
self.app.has_flags()
}