use fancy_regex::Regex; use itertools::Itertools; use nu_ansi_term::{ Color::{Default, Red, White}, Style, }; use nu_color_config::get_color_config; use nu_engine::{get_full_help, CallExt}; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, span, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; use std::borrow::Borrow; #[derive(Clone)] pub struct Help; impl Command for Help { fn name(&self) -> &str { "help" } fn signature(&self) -> Signature { Signature::build("help") .input_output_types(vec![(Type::Nothing, Type::String)]) .rest( "rest", SyntaxShape::String, "the name of command to get help on", ) .named( "find", SyntaxShape::String, "string to find in command names, usage, and search terms", Some('f'), ) .category(Category::Core) } fn usage(&self) -> &str { "Display help information about commands." } fn run( &self, engine_state: &EngineState, stack: &mut Stack, call: &Call, _input: PipelineData, ) -> Result<PipelineData, ShellError> { help(engine_state, stack, call) } fn examples(&self) -> Vec<Example> { vec![ Example { description: "show all commands and sub-commands", example: "help commands", result: None, }, Example { description: "show help for single command", example: "help match", result: None, }, Example { description: "show help for single sub-command", example: "help str lpad", result: None, }, Example { description: "search for string in command names, usage and search terms", example: "help --find char", result: None, }, ] } } fn help( engine_state: &EngineState, stack: &mut Stack, call: &Call, ) -> Result<PipelineData, ShellError> { let head = call.head; let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?; let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?; let commands = engine_state.get_decl_ids_sorted(false); let config = engine_state.get_config(); let color_hm = get_color_config(config); let default_style = Style::new().fg(Default).on(Default); let string_style = match color_hm.get("string") { Some(style) => style, None => &default_style, }; if let Some(f) = find { let org_search_string = f.item.clone(); let search_string = f.item.to_lowercase(); let mut found_cmds_vec = Vec::new(); for decl_id in commands { let mut cols = vec![]; let mut vals = vec![]; let decl = engine_state.get_decl(decl_id); let sig = decl.signature().update_from_command(decl.borrow()); let key = sig.name; let usage = sig.usage; let search_terms = sig.search_terms; let matches_term = if !search_terms.is_empty() { search_terms .iter() .any(|term| term.to_lowercase().contains(&search_string)) } else { false }; let key_match = key.to_lowercase().contains(&search_string); let usage_match = usage.to_lowercase().contains(&search_string); if key_match || usage_match || matches_term { cols.push("name".into()); vals.push(Value::String { val: if key_match { highlight_search_string(&key, &org_search_string, string_style)? } else { key }, span: head, }); cols.push("category".into()); vals.push(Value::String { val: sig.category.to_string(), span: head, }); cols.push("is_plugin".into()); vals.push(Value::Bool { val: decl.is_plugin().is_some(), span: head, }); cols.push("is_custom".into()); vals.push(Value::Bool { val: decl.is_custom_command(), span: head, }); cols.push("is_keyword".into()); vals.push(Value::Bool { val: decl.is_parser_keyword(), span: head, }); cols.push("usage".into()); vals.push(Value::String { val: if usage_match { highlight_search_string(&usage, &org_search_string, string_style)? } else { usage }, span: head, }); cols.push("signatures".into()); vals.push(Value::String { val: sig .input_output_types .iter() .map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) .join("\n"), span: head, }); cols.push("search_terms".into()); vals.push(if search_terms.is_empty() { Value::nothing(head) } else { Value::String { val: if matches_term { search_terms .iter() .map(|term| { if term.to_lowercase().contains(&search_string) { match highlight_search_string( term, &org_search_string, string_style, ) { Ok(s) => s, Err(_) => { string_style.paint(term.to_string()).to_string() } } } else { string_style.paint(term.to_string()).to_string() } }) .collect::<Vec<_>>() .join(", ") } else { search_terms.join(", ") }, span: head, } }); found_cmds_vec.push(Value::Record { cols, vals, span: head, }); } } return Ok(found_cmds_vec .into_iter() .into_pipeline_data(engine_state.ctrlc.clone())); } if !rest.is_empty() { let mut found_cmds_vec = Vec::new(); if rest[0].item == "commands" { for decl_id in commands { let mut cols = vec![]; let mut vals = vec![]; let decl = engine_state.get_decl(decl_id); let sig = decl.signature().update_from_command(decl.borrow()); let key = sig.name; let usage = sig.usage; let search_terms = sig.search_terms; cols.push("name".into()); vals.push(Value::String { val: key, span: head, }); cols.push("category".into()); vals.push(Value::String { val: sig.category.to_string(), span: head, }); cols.push("is_plugin".into()); vals.push(Value::Bool { val: decl.is_plugin().is_some(), span: head, }); cols.push("is_custom".into()); vals.push(Value::Bool { val: decl.is_custom_command(), span: head, }); cols.push("is_keyword".into()); vals.push(Value::Bool { val: decl.is_parser_keyword(), span: head, }); cols.push("usage".into()); vals.push(Value::String { val: usage, span: head, }); cols.push("signatures".into()); vals.push(Value::String { val: sig .input_output_types .iter() .map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape())) .join("\n"), span: head, }); cols.push("search_terms".into()); vals.push(if search_terms.is_empty() { Value::nothing(head) } else { Value::String { val: search_terms.join(", "), span: head, } }); found_cmds_vec.push(Value::Record { cols, vals, span: head, }); } Ok(found_cmds_vec .into_iter() .into_pipeline_data(engine_state.ctrlc.clone())) } else { let mut name = String::new(); for r in &rest { if !name.is_empty() { name.push(' '); } name.push_str(&r.item); } let output = engine_state .get_signatures_with_examples(false) .iter() .filter(|(signature, _, _, _)| signature.name == name) .map(|(signature, examples, _, _)| { get_full_help(signature, examples, engine_state, stack) }) .collect::<Vec<String>>(); if !output.is_empty() { Ok(Value::String { val: output.join("======================\n\n"), span: call.head, } .into_pipeline_data()) } else { Err(ShellError::CommandNotFound(span(&[ rest[0].span, rest[rest.len() - 1].span, ]))) } } } else { let msg = r#"Welcome to Nushell. Here are some tips to help you get started. * help commands - list all available commands * help <command name> - display help about a particular command * help --find <text to search> - search through all of help Nushell works on the idea of a "pipeline". Pipelines are commands connected with the '|' character. Each stage in the pipeline works together to load, parse, and display information to you. [Examples] List the files in the current directory, sorted by size: ls | sort-by size Get information about the current system: sys | get host Get the processes on your system actively using CPU: ps | where cpu > 0 You can also learn more at https://www.nushell.sh/book/"#; Ok(Value::String { val: msg.into(), span: head, } .into_pipeline_data()) } } // Highlight the search string using ANSI escape sequences and regular expressions. pub fn highlight_search_string( haystack: &str, needle: &str, string_style: &Style, ) -> Result<String, ShellError> { let regex_string = format!("(?i){}", needle); let regex = match Regex::new(®ex_string) { Ok(regex) => regex, Err(err) => { return Err(ShellError::GenericError( "Could not compile regex".into(), err.to_string(), Some(Span::test_data()), None, Vec::new(), )); } }; // strip haystack to remove existing ansi style let stripped_haystack = nu_utils::strip_ansi_likely(haystack); let mut last_match_end = 0; let style = Style::new().fg(White).on(Red); let mut highlighted = String::new(); for cap in regex.captures_iter(stripped_haystack.as_ref()) { match cap { Ok(capture) => { let start = match capture.get(0) { Some(acap) => acap.start(), None => 0, }; let end = match capture.get(0) { Some(acap) => acap.end(), None => 0, }; highlighted.push_str( &string_style .paint(&stripped_haystack[last_match_end..start]) .to_string(), ); highlighted.push_str(&style.paint(&stripped_haystack[start..end]).to_string()); last_match_end = end; } Err(e) => { return Err(ShellError::GenericError( "Error with regular expression capture".into(), e.to_string(), None, None, Vec::new(), )); } } } highlighted.push_str( &string_style .paint(&stripped_haystack[last_match_end..]) .to_string(), ); Ok(highlighted) }