Files
sausage/userland/brush/source/brush-builtins/src/help.rs
T
2026-11-11 22:04:16 +00:00

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(())
}
}