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, } brush_winnow_args_adapter::impl_parse_args!(HelpCommand); impl builtins::Builtin for HelpCommand { type Error = brush_core::Error; async fn execute( self, context: brush_core::ExecutionContext<'_, SE>, ) -> Result { 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 { 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( &self, context: &brush_core::ExecutionContext<'_, SE>, name: &str, registration: &builtins::Registration, ) -> 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(()) } }