1use crate::eval_call;
2use fancy_regex::{Captures, Regex};
3use nu_protocol::{
4 Category, Config, IntoPipelineData, PipelineData, PositionalArg, Signature, Span, SpanId,
5 Spanned, SyntaxShape, Type, Value,
6 ast::{Argument, Call, Expr, Expression, RecordItem},
7 debugger::WithoutDebug,
8 engine::CommandType,
9 engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
10 record,
11};
12use nu_utils::terminal_size;
13use std::{
14 borrow::Cow,
15 collections::HashMap,
16 fmt::Write,
17 sync::{Arc, LazyLock},
18};
19
20const RESET: &str = "\x1b[0m";
22const DEFAULT_COLOR: &str = "\x1b[39m";
24const DEFAULT_DIMMED: &str = "\x1b[2;39m";
26const DEFAULT_ITALIC: &str = "\x1b[3;39m";
28
29pub fn get_full_help(
30 command: &dyn Command,
31 engine_state: &EngineState,
32 stack: &mut Stack,
33) -> String {
34 let stack = &mut stack.start_collect_value();
39
40 let nu_config = stack.get_config(engine_state);
41
42 let sig = engine_state
43 .get_signature(command)
44 .update_from_command(command);
45
46 let mut help_style = HelpStyle::default();
48 help_style.update_from_config(engine_state, &nu_config);
49
50 let mut long_desc = String::new();
51
52 let desc = &sig.description;
53 if !desc.is_empty() {
54 long_desc.push_str(&highlight_code(desc, engine_state, stack));
55 long_desc.push_str("\n\n");
56 }
57
58 let extra_desc = &sig.extra_description;
59 if !extra_desc.is_empty() {
60 long_desc.push_str(&highlight_code(extra_desc, engine_state, stack));
61 long_desc.push_str("\n\n");
62 }
63
64 match command.command_type() {
65 CommandType::Alias => get_alias_documentation(
66 &mut long_desc,
67 command,
68 &sig,
69 &help_style,
70 engine_state,
71 stack,
72 ),
73 _ => get_command_documentation(
74 &mut long_desc,
75 command,
76 &sig,
77 &nu_config,
78 &help_style,
79 engine_state,
80 stack,
81 ),
82 };
83
84 let mut final_help = if !nu_config.use_ansi_coloring.get(engine_state) {
85 nu_utils::strip_ansi_string_likely(long_desc)
86 } else {
87 long_desc
88 };
89
90 if let Some(cmd) = command.as_alias().and_then(|alias| alias.command.as_ref()) {
91 let nested_help = get_full_help(cmd.as_ref(), engine_state, stack);
92 if !nested_help.is_empty() {
93 final_help.push_str("\n\n");
94 final_help.push_str(&nested_help);
95 }
96 }
97
98 final_help
99}
100
101fn try_nu_highlight(
103 code_string: &str,
104 reject_garbage: bool,
105 engine_state: &EngineState,
106 stack: &mut Stack,
107) -> Option<String> {
108 let highlighter = engine_state.find_decl(b"nu-highlight", &[])?;
109
110 let decl = engine_state.get_decl(highlighter);
111 let mut call = Call::new(Span::unknown());
112 if reject_garbage {
113 call.add_named((
114 Spanned {
115 item: "reject-garbage".into(),
116 span: Span::unknown(),
117 },
118 None,
119 None,
120 ));
121 }
122
123 decl.run(
124 engine_state,
125 stack,
126 &(&call).into(),
127 Value::string(code_string, Span::unknown()).into_pipeline_data(),
128 )
129 .and_then(|pipe| pipe.into_value(Span::unknown()))
130 .and_then(|val| val.coerce_into_string())
131 .ok()
132}
133
134fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
136 try_nu_highlight(code_string, false, engine_state, stack)
137 .unwrap_or_else(|| code_string.to_string())
138}
139
140fn highlight_capture_group(
142 captures: &Captures,
143 engine_state: &EngineState,
144 stack: &mut Stack,
145) -> String {
146 let Some(content) = captures.get(1) else {
147 return String::new();
149 };
150
151 let config_old = stack.get_config(engine_state);
153 let mut config = (*config_old).clone();
154
155 let code_style = Value::record(
159 record! {
160 "attr" => Value::string("di", Span::unknown()),
161 },
162 Span::unknown(),
163 );
164 let color_config = &mut config.color_config;
165 color_config.insert("shape_external".into(), code_style.clone());
166 color_config.insert("shape_external_resolved".into(), code_style.clone());
167 color_config.insert("shape_externalarg".into(), code_style);
168
169 stack.config = Some(Arc::new(config));
171
172 let highlighted = try_nu_highlight(content.into(), true, engine_state, stack)
174 .map(|text| {
176 let resets = text.match_indices(RESET).count();
177 let text = text.replacen(
179 RESET,
180 &format!("{RESET}{DEFAULT_ITALIC}"),
181 resets.saturating_sub(1),
182 );
183 format!("{DEFAULT_ITALIC}{text}")
185 });
186
187 stack.config = Some(config_old);
189
190 highlighted.unwrap_or_else(|| highlight_fallback(content.into()))
192}
193
194fn highlight_fallback(text: &str) -> String {
196 format!("{DEFAULT_DIMMED}{DEFAULT_ITALIC}{text}{RESET}")
197}
198
199fn highlight_code<'a>(
203 text: &'a str,
204 engine_state: &EngineState,
205 stack: &mut Stack,
206) -> Cow<'a, str> {
207 let config = stack.get_config(engine_state);
208 if !config.use_ansi_coloring.get(engine_state) {
209 return Cow::Borrowed(text);
210 }
211
212 static PATTERN: &str = r"(?x) # verbose mode
214 (?<![\p{Letter}\d]) # negative look-behind for alphanumeric: ensure backticks are not directly preceded by letter/number.
215 `
216 ([^`\n]+?) # capture characters inside backticks, excluding backticks and newlines. ungreedy.
217 `
218 (?![\p{Letter}\d]) # negative look-ahead for alphanumeric: ensure backticks are not directly followed by letter/number.
219 ";
220 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(PATTERN).expect("valid regex"));
221
222 let do_try_highlight =
223 |captures: &Captures| highlight_capture_group(captures, engine_state, stack);
224 RE.replace_all(text, do_try_highlight)
225}
226
227fn get_alias_documentation(
228 long_desc: &mut String,
229 command: &dyn Command,
230 sig: &Signature,
231 help_style: &HelpStyle,
232 engine_state: &EngineState,
233 stack: &mut Stack,
234) {
235 let help_section_name = &help_style.section_name;
236 let help_subcolor_one = &help_style.subcolor_one;
237
238 let alias_name = &sig.name;
239
240 long_desc.push_str(&format!(
241 "{help_section_name}Alias{RESET}: {help_subcolor_one}{alias_name}{RESET}"
242 ));
243 long_desc.push_str("\n\n");
244
245 let Some(alias) = command.as_alias() else {
246 return;
248 };
249
250 let alias_expansion =
251 String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span));
252
253 long_desc.push_str(&format!(
254 "{help_section_name}Expansion{RESET}:\n {}",
255 nu_highlight_string(&alias_expansion, engine_state, stack)
256 ));
257}
258
259fn get_command_documentation(
260 long_desc: &mut String,
261 command: &dyn Command,
262 sig: &Signature,
263 nu_config: &Config,
264 help_style: &HelpStyle,
265 engine_state: &EngineState,
266 stack: &mut Stack,
267) {
268 let help_section_name = &help_style.section_name;
269 let help_subcolor_one = &help_style.subcolor_one;
270
271 let cmd_name = &sig.name;
272
273 if !sig.search_terms.is_empty() {
274 let _ = write!(
275 long_desc,
276 "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
277 sig.search_terms.join(", "),
278 );
279 }
280
281 let _ = write!(
282 long_desc,
283 "{help_section_name}Usage{RESET}:\n > {}\n",
284 sig.call_signature()
285 );
286
287 let mut subcommands = vec![];
295 let signatures = engine_state.get_signatures_and_declids(true);
296 for (sig, decl_id) in signatures {
297 let command_type = engine_state.get_decl(decl_id).command_type();
298
299 let display_name = engine_state
302 .find_decl_name(decl_id, &[])
303 .map(|bytes| String::from_utf8_lossy(bytes).to_string())
304 .unwrap_or_else(|| sig.name.clone());
305
306 if display_name.starts_with(&format!("{cmd_name} "))
308 && !matches!(sig.category, Category::Removed)
309 {
310 if command_type == CommandType::Plugin
312 || command_type == CommandType::Alias
313 || command_type == CommandType::Custom
314 {
315 subcommands.push(format!(
316 " {help_subcolor_one}{} {help_section_name}({}){RESET} - {}",
317 display_name,
318 command_type,
319 highlight_code(&sig.description, engine_state, stack)
320 ));
321 } else {
322 subcommands.push(format!(
323 " {help_subcolor_one}{}{RESET} - {}",
324 display_name,
325 highlight_code(&sig.description, engine_state, stack)
326 ));
327 }
328 }
329 }
330
331 if !subcommands.is_empty() {
332 let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
333 subcommands.sort();
334 long_desc.push_str(&subcommands.join("\n"));
335 long_desc.push('\n');
336 }
337
338 if !sig.named.is_empty() {
339 long_desc.push_str(&get_flags_section(sig, help_style, |v| match v {
340 FormatterValue::DefaultValue(value) => nu_highlight_string(
341 &value.to_parsable_string(", ", nu_config),
342 engine_state,
343 stack,
344 ),
345 FormatterValue::CodeString(text) => {
346 highlight_code(text, engine_state, stack).to_string()
347 }
348 }))
349 }
350
351 if !sig.required_positional.is_empty()
352 || !sig.optional_positional.is_empty()
353 || sig.rest_positional.is_some()
354 {
355 let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
356 for positional in &sig.required_positional {
357 write_positional(
358 long_desc,
359 positional,
360 PositionalKind::Required,
361 help_style,
362 nu_config,
363 engine_state,
364 stack,
365 );
366 }
367 for positional in &sig.optional_positional {
368 write_positional(
369 long_desc,
370 positional,
371 PositionalKind::Optional,
372 help_style,
373 nu_config,
374 engine_state,
375 stack,
376 );
377 }
378
379 if let Some(rest_positional) = &sig.rest_positional {
380 write_positional(
381 long_desc,
382 rest_positional,
383 PositionalKind::Rest,
384 help_style,
385 nu_config,
386 engine_state,
387 stack,
388 );
389 }
390 }
391
392 fn get_term_width() -> usize {
393 if let Ok((w, _h)) = terminal_size() {
394 w as usize
395 } else {
396 80
397 }
398 }
399
400 if !command.is_keyword()
401 && !sig.input_output_types.is_empty()
402 && let Some(decl_id) = engine_state.find_decl(b"table", &[])
403 {
404 let span = Span::unknown();
406 let mut vals = vec![];
407 for (input, output) in &sig.input_output_types {
408 vals.push(Value::record(
409 record! {
410 "input" => Value::string(input.to_string(), span),
411 "output" => Value::string(output.to_string(), span),
412 },
413 span,
414 ));
415 }
416
417 let caller_stack = &mut Stack::new().collect_value();
418 if let Ok(result) = eval_call::<WithoutDebug>(
419 engine_state,
420 caller_stack,
421 &Call {
422 decl_id,
423 head: span,
424 arguments: vec![Argument::Named((
425 Spanned {
426 item: "width".to_string(),
427 span: Span::unknown(),
428 },
429 None,
430 Some(Expression::new_unknown(
431 Expr::Int(get_term_width() as i64 - 2), Span::unknown(),
433 Type::Int,
434 )),
435 ))],
436 parser_info: HashMap::new(),
437 },
438 PipelineData::value(Value::list(vals, span), None),
439 ) && let Ok((str, ..)) = result.collect_string_strict(span)
440 {
441 let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
442 for line in str.lines() {
443 let _ = writeln!(long_desc, " {line}");
444 }
445 }
446 }
447
448 let examples = command.examples();
449
450 if !examples.is_empty() {
451 let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
452 }
453
454 for example in examples {
455 long_desc.push('\n');
456 long_desc.push_str(" ");
457 long_desc.push_str(&highlight_code(example.description, engine_state, stack));
458
459 if !nu_config.use_ansi_coloring.get(engine_state) {
460 let _ = write!(long_desc, "\n > {}\n", example.example);
461 } else {
462 let code_string = nu_highlight_string(example.example, engine_state, stack);
463 let _ = write!(long_desc, "\n > {code_string}\n");
464 };
465
466 if let Some(result) = &example.result {
467 let mut table_call = Call::new(Span::unknown());
468 if example.example.ends_with("--collapse") {
469 table_call.add_named((
471 Spanned {
472 item: "collapse".to_string(),
473 span: Span::unknown(),
474 },
475 None,
476 None,
477 ))
478 } else {
479 table_call.add_named((
481 Spanned {
482 item: "expand".to_string(),
483 span: Span::unknown(),
484 },
485 None,
486 None,
487 ))
488 }
489 table_call.add_named((
490 Spanned {
491 item: "width".to_string(),
492 span: Span::unknown(),
493 },
494 None,
495 Some(Expression::new_unknown(
496 Expr::Int(get_term_width() as i64 - 2),
497 Span::unknown(),
498 Type::Int,
499 )),
500 ));
501
502 let table = engine_state
503 .find_decl("table".as_bytes(), &[])
504 .and_then(|decl_id| {
505 engine_state
506 .get_decl(decl_id)
507 .run(
508 engine_state,
509 stack,
510 &(&table_call).into(),
511 PipelineData::value(result.clone(), None),
512 )
513 .ok()
514 });
515
516 for item in table.into_iter().flatten() {
517 let _ = writeln!(
518 long_desc,
519 " {}",
520 item.to_expanded_string("", nu_config)
521 .trim_end()
522 .trim_start_matches(|c: char| c.is_whitespace() && c != ' ')
523 .replace('\n', "\n ")
524 );
525 }
526 }
527 }
528
529 long_desc.push('\n');
530}
531
532fn update_ansi_from_config(
533 ansi_code: &mut String,
534 engine_state: &EngineState,
535 nu_config: &Config,
536 theme_component: &str,
537) {
538 if let Some(color) = &nu_config.color_config.get(theme_component) {
539 let caller_stack = &mut Stack::new().collect_value();
540 let span = Span::unknown();
541 let span_id = UNKNOWN_SPAN_ID;
542
543 let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
544
545 if let Some(argument) = argument_opt
547 && let Some(decl_id) = engine_state.find_decl(b"ansi", &[])
548 && let Ok(result) = eval_call::<WithoutDebug>(
549 engine_state,
550 caller_stack,
551 &Call {
552 decl_id,
553 head: span,
554 arguments: vec![argument],
555 parser_info: HashMap::new(),
556 },
557 PipelineData::empty(),
558 )
559 && let Ok((str, ..)) = result.collect_string_strict(span)
560 {
561 *ansi_code = str;
562 }
563 }
564}
565
566fn get_argument_for_color_value(
567 nu_config: &Config,
568 color: &Value,
569 span: Span,
570 span_id: SpanId,
571) -> Option<Argument> {
572 match color {
573 Value::Record { val, .. } => {
574 let record_exp: Vec<RecordItem> = (**val)
575 .iter()
576 .map(|(k, v)| {
577 RecordItem::Pair(
578 Expression::new_existing(
579 Expr::String(k.clone()),
580 span,
581 span_id,
582 Type::String,
583 ),
584 Expression::new_existing(
585 Expr::String(v.clone().to_expanded_string("", nu_config)),
586 span,
587 span_id,
588 Type::String,
589 ),
590 )
591 })
592 .collect();
593
594 Some(Argument::Positional(Expression::new_existing(
595 Expr::Record(record_exp),
596 Span::unknown(),
597 UNKNOWN_SPAN_ID,
598 Type::Record(
599 [
600 ("fg".to_string(), Type::String),
601 ("attr".to_string(), Type::String),
602 ]
603 .into(),
604 ),
605 )))
606 }
607 Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
608 Expr::String(val.clone()),
609 Span::unknown(),
610 UNKNOWN_SPAN_ID,
611 Type::String,
612 ))),
613 _ => None,
614 }
615}
616
617pub struct HelpStyle {
623 section_name: String,
624 subcolor_one: String,
625 subcolor_two: String,
626}
627
628impl Default for HelpStyle {
629 fn default() -> Self {
630 HelpStyle {
631 section_name: "\x1b[32m".to_string(),
633 subcolor_one: "\x1b[36m".to_string(),
635 subcolor_two: "\x1b[94m".to_string(),
637 }
638 }
639}
640
641impl HelpStyle {
642 pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
650 update_ansi_from_config(
651 &mut self.section_name,
652 engine_state,
653 nu_config,
654 "shape_string",
655 );
656 update_ansi_from_config(
657 &mut self.subcolor_one,
658 engine_state,
659 nu_config,
660 "shape_external",
661 );
662 update_ansi_from_config(
663 &mut self.subcolor_two,
664 engine_state,
665 nu_config,
666 "shape_block",
667 );
668 }
669}
670
671#[derive(PartialEq)]
672enum PositionalKind {
673 Required,
674 Optional,
675 Rest,
676}
677
678fn write_positional(
679 long_desc: &mut String,
680 positional: &PositionalArg,
681 arg_kind: PositionalKind,
682 help_style: &HelpStyle,
683 nu_config: &Config,
684 engine_state: &EngineState,
685 stack: &mut Stack,
686) {
687 let help_subcolor_one = &help_style.subcolor_one;
688 let help_subcolor_two = &help_style.subcolor_two;
689
690 long_desc.push_str(" ");
692 if arg_kind == PositionalKind::Rest {
693 long_desc.push_str("...");
694 }
695 match &positional.shape {
696 SyntaxShape::Keyword(kw, shape) => {
697 let _ = write!(
698 long_desc,
699 "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
700 String::from_utf8_lossy(kw),
701 shape,
702 );
703 }
704 _ => {
705 let _ = write!(
706 long_desc,
707 "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
708 positional.name, &positional.shape,
709 );
710 }
711 };
712 if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
713 let _ = write!(
714 long_desc,
715 ": {}",
716 highlight_code(&positional.desc, engine_state, stack)
717 );
718 }
719 if arg_kind == PositionalKind::Optional {
720 if let Some(value) = &positional.default_value {
721 let _ = write!(
722 long_desc,
723 " (optional, default: {})",
724 nu_highlight_string(
725 &value.to_parsable_string(", ", nu_config),
726 engine_state,
727 stack
728 )
729 );
730 } else {
731 long_desc.push_str(" (optional)");
732 };
733 }
734 long_desc.push('\n');
735}
736
737pub enum FormatterValue<'a> {
743 DefaultValue(&'a Value),
745 CodeString(&'a str),
747}
748
749fn write_flag_to_long_desc<F>(
750 flag: &nu_protocol::Flag,
751 long_desc: &mut String,
752 help_subcolor_one: &str,
753 help_subcolor_two: &str,
754 formatter: &mut F,
755) where
756 F: FnMut(FormatterValue) -> String,
757{
758 long_desc.push_str(" ");
760 if let Some(short) = flag.short {
762 let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
763 if !flag.long.is_empty() {
764 let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
765 }
766 }
767 if !flag.long.is_empty() {
768 let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
769 }
770 if flag.required {
771 long_desc.push_str(" (required parameter)")
772 }
773 if let Some(arg) = &flag.arg {
775 let _ = write!(long_desc, " <{help_subcolor_two}{arg}{RESET}>");
776 }
777 if !flag.desc.is_empty() {
778 let _ = write!(
779 long_desc,
780 ": {}",
781 &formatter(FormatterValue::CodeString(&flag.desc))
782 );
783 }
784 if let Some(value) = &flag.default_value {
785 let _ = write!(
786 long_desc,
787 " (default: {})",
788 &formatter(FormatterValue::DefaultValue(value))
789 );
790 }
791 long_desc.push('\n');
792}
793
794pub fn get_flags_section<F>(
795 signature: &Signature,
796 help_style: &HelpStyle,
797 mut formatter: F, ) -> String
799where
800 F: FnMut(FormatterValue) -> String,
801{
802 let help_section_name = &help_style.section_name;
803 let help_subcolor_one = &help_style.subcolor_one;
804 let help_subcolor_two = &help_style.subcolor_two;
805
806 let mut long_desc = String::new();
807 let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
808
809 let help = signature.named.iter().find(|flag| flag.long == "help");
810 let required = signature.named.iter().filter(|flag| flag.required);
811 let optional = signature
812 .named
813 .iter()
814 .filter(|flag| !flag.required && flag.long != "help");
815
816 let flags = required.chain(help).chain(optional);
817
818 for flag in flags {
819 write_flag_to_long_desc(
820 flag,
821 &mut long_desc,
822 help_subcolor_one,
823 help_subcolor_two,
824 &mut formatter,
825 );
826 }
827
828 long_desc
829}
830
831#[cfg(test)]
832mod tests {
833 use nu_protocol::UseAnsiColoring;
834
835 use super::*;
836
837 #[test]
838 fn test_code_formatting() {
839 let mut engine_state = EngineState::new();
840 let mut stack = Stack::new();
841
842 let mut config = (*engine_state.config).clone();
844 config.use_ansi_coloring = UseAnsiColoring::True;
845 engine_state.config = Arc::new(config);
846
847 let haystack = "Run the `foo` command";
852 assert!(matches!(
853 highlight_code(haystack, &engine_state, &mut stack),
854 Cow::Owned(_)
855 ));
856
857 let haystack = "foo`bar`";
859 assert!(matches!(
860 highlight_code(haystack, &engine_state, &mut stack),
861 Cow::Borrowed(_)
862 ));
863
864 let haystack = "`my-command` is cool";
866 assert!(matches!(
867 highlight_code(haystack, &engine_state, &mut stack),
868 Cow::Owned(_)
869 ));
870
871 let haystack = r"
873 `command`
874 ";
875 assert!(matches!(
876 highlight_code(haystack, &engine_state, &mut stack),
877 Cow::Owned(_)
878 ));
879
880 let haystack = "// hello `beautiful \n world`";
882 assert!(matches!(
883 highlight_code(haystack, &engine_state, &mut stack),
884 Cow::Borrowed(_)
885 ));
886
887 let haystack = "try running `my cool command`.";
889 assert!(matches!(
890 highlight_code(haystack, &engine_state, &mut stack),
891 Cow::Owned(_)
892 ));
893
894 let haystack = "a command (`my cool command`).";
896 assert!(matches!(
897 highlight_code(haystack, &engine_state, &mut stack),
898 Cow::Owned(_)
899 ));
900
901 let haystack = "```\ncode block\n```";
904 assert!(matches!(
905 highlight_code(haystack, &engine_state, &mut stack),
906 Cow::Borrowed(_)
907 ));
908 }
909}