145 lines
4.9 KiB
Rust
145 lines
4.9 KiB
Rust
use brush_core::{ExecutionResult, builtins};
|
|
use std::io::Write;
|
|
|
|
/// Display command help.
|
|
#[derive(winnow_args::Args)]
|
|
#[arg(disable_help_short, disable_version_flag, disable_help_subcommand)]
|
|
pub(crate) struct HelpCommand {
|
|
/// Display a short description for the commands.
|
|
#[arg(short = 'd')]
|
|
short_description: bool,
|
|
/// Display a man-style page of documentation for the commands.
|
|
#[arg(short = 'm')]
|
|
man_page_style: bool,
|
|
/// Display a short usage summary for the commands.
|
|
#[arg(short = 's')]
|
|
short_usage: bool,
|
|
/// Patterns of topics to display help for.
|
|
#[arg(positional)]
|
|
topic_patterns: Vec<String>,
|
|
}
|
|
|
|
brush_winnow_args_adapter::impl_parse_args!(HelpCommand);
|
|
|
|
impl<SE: brush_core::ShellExtensions> builtins::Builtin<SE> for HelpCommand {
|
|
type Error = brush_core::Error;
|
|
|
|
async fn execute(
|
|
self,
|
|
context: brush_core::ExecutionContext<'_, SE>,
|
|
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
|
if self.topic_patterns.is_empty() {
|
|
Self::display_general_help(&context)?;
|
|
return Ok(ExecutionResult::success());
|
|
}
|
|
|
|
// Match bash: succeed if at least one requested topic pattern matched
|
|
// something; return a non-zero exit code only when none of them matched.
|
|
let mut any_matched = false;
|
|
for topic_pattern in &self.topic_patterns {
|
|
if self.display_help_for_topic_pattern(&context, topic_pattern)? {
|
|
any_matched = true;
|
|
}
|
|
}
|
|
|
|
if any_matched {
|
|
Ok(ExecutionResult::success())
|
|
} else {
|
|
Ok(ExecutionResult::general_error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HelpCommand {
|
|
fn display_general_help(
|
|
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
|
) -> Result<(), brush_core::Error> {
|
|
const COLUMN_COUNT: usize = 3;
|
|
|
|
if let Some(display_str) = context.shell.product_display_str() {
|
|
writeln!(context.stdout(), "{display_str}\n")?;
|
|
}
|
|
|
|
writeln!(
|
|
context.stdout(),
|
|
"The following commands are implemented as shell built-ins:"
|
|
)?;
|
|
|
|
let builtins: Vec<_> = context.shell.builtins().iter().collect();
|
|
let items_per_column = builtins.len().div_ceil(COLUMN_COUNT);
|
|
|
|
for i in 0..items_per_column {
|
|
for j in 0..COLUMN_COUNT {
|
|
if let Some((name, builtin)) = builtins.get(i + j * items_per_column) {
|
|
let prefix = if !builtin.is_enabled() { "*" } else { " " };
|
|
write!(context.stdout(), " {prefix}{name:<20}")?; // adjust 20 to the desired
|
|
// column width
|
|
}
|
|
}
|
|
writeln!(context.stdout())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Displays help for the builtins matching `topic_pattern`, returning whether
|
|
/// at least one topic matched.
|
|
fn display_help_for_topic_pattern(
|
|
&self,
|
|
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
|
topic_pattern: &str,
|
|
) -> Result<bool, brush_core::Error> {
|
|
let pattern = brush_core::patterns::Pattern::from(topic_pattern)
|
|
.set_extended_globbing(context.shell.options().extended_globbing)
|
|
.set_case_insensitive(context.shell.options().case_insensitive_pathname_expansion);
|
|
|
|
let mut matched = false;
|
|
for (builtin_name, builtin_registration) in context.shell.builtins().iter() {
|
|
if pattern.exactly_matches(builtin_name)? {
|
|
self.display_help_for_builtin(context, builtin_name, builtin_registration)?;
|
|
matched = true;
|
|
}
|
|
}
|
|
|
|
if !matched {
|
|
writeln!(context.stderr(), "No help topics match '{topic_pattern}'")?;
|
|
}
|
|
|
|
Ok(matched)
|
|
}
|
|
|
|
fn display_help_for_builtin<SE: brush_core::ShellExtensions>(
|
|
&self,
|
|
context: &brush_core::ExecutionContext<'_, SE>,
|
|
name: &str,
|
|
registration: &builtins::Registration<SE>,
|
|
) -> Result<(), brush_core::Error> {
|
|
let content_type = if self.short_description {
|
|
builtins::ContentType::ShortDescription
|
|
} else if self.man_page_style {
|
|
builtins::ContentType::ManPage
|
|
} else if self.short_usage {
|
|
builtins::ContentType::ShortUsage
|
|
} else {
|
|
builtins::ContentType::DetailedHelp
|
|
};
|
|
|
|
let Some(mut stdout) = context.try_fd(brush_core::openfiles::OpenFiles::STDOUT_FD) else {
|
|
// If there's no stdout, nothing to do.
|
|
return Ok(());
|
|
};
|
|
|
|
// For now, we assume colorized output if stdout is a terminal.
|
|
let options = builtins::ContentOptions {
|
|
colorized: stdout.is_terminal(),
|
|
};
|
|
|
|
let content = registration.content(name, content_type, &options)?;
|
|
|
|
write!(stdout, "{content}")?;
|
|
stdout.flush()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|