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 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
91fn try_nu_highlight(
93 code_string: &str,
94 reject_garbage: bool,
95 engine_state: &EngineState,
96 stack: &mut Stack,
97) -> Option<String> {
98 let highlighter = engine_state.find_decl(b"nu-highlight", &[])?;
99
100 let decl = engine_state.get_decl(highlighter);
101 let mut call = Call::new(Span::unknown());
102 if reject_garbage {
103 call.add_named((
104 Spanned {
105 item: "reject-garbage".into(),
106 span: Span::unknown(),
107 },
108 None,
109 None,
110 ));
111 }
112
113 decl.run(
114 engine_state,
115 stack,
116 &(&call).into(),
117 Value::string(code_string, Span::unknown()).into_pipeline_data(),
118 )
119 .and_then(|pipe| pipe.into_value(Span::unknown()))
120 .and_then(|val| val.coerce_into_string())
121 .ok()
122}
123
124fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
126 try_nu_highlight(code_string, false, engine_state, stack)
127 .unwrap_or_else(|| code_string.to_string())
128}
129
130fn highlight_capture_group(
132 captures: &Captures,
133 engine_state: &EngineState,
134 stack: &mut Stack,
135) -> String {
136 let Some(content) = captures.get(1) else {
137 return String::new();
139 };
140
141 let config_old = stack.get_config(engine_state);
143 let mut config = (*config_old).clone();
144
145 let code_style = Value::record(
149 record! {
150 "attr" => Value::string("di", Span::unknown()),
151 },
152 Span::unknown(),
153 );
154 let color_config = &mut config.color_config;
155 color_config.insert("shape_external".into(), code_style.clone());
156 color_config.insert("shape_external_resolved".into(), code_style.clone());
157 color_config.insert("shape_externalarg".into(), code_style);
158
159 stack.config = Some(Arc::new(config));
161
162 let highlighted = try_nu_highlight(content.into(), true, engine_state, stack)
164 .map(|text| {
166 let resets = text.match_indices(RESET).count();
167 let text = text.replacen(RESET, &format!("{RESET}{DEFAULT_ITALIC}"), resets - 1);
169 format!("{DEFAULT_ITALIC}{text}")
171 });
172
173 stack.config = Some(config_old);
175
176 highlighted.unwrap_or_else(|| highlight_fallback(content.into()))
178}
179
180fn highlight_fallback(text: &str) -> String {
182 format!("{DEFAULT_DIMMED}{DEFAULT_ITALIC}{text}{RESET}")
183}
184
185fn highlight_code<'a>(
189 text: &'a str,
190 engine_state: &EngineState,
191 stack: &mut Stack,
192) -> Cow<'a, str> {
193 let config = stack.get_config(engine_state);
194 if !config.use_ansi_coloring.get(engine_state) {
195 return Cow::Borrowed(text);
196 }
197
198 static PATTERN: &str = r"(?x) # verbose mode
200 (?<![\p{Letter}\d]) # negative look-behind for alphanumeric: ensure backticks are not directly preceded by letter/number.
201 `
202 ([^`\n]+?) # capture characters inside backticks, excluding backticks and newlines. ungreedy.
203 `
204 (?![\p{Letter}\d]) # negative look-ahead for alphanumeric: ensure backticks are not directly followed by letter/number.
205 ";
206 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(PATTERN).expect("valid regex"));
207
208 let do_try_highlight =
209 |captures: &Captures| highlight_capture_group(captures, engine_state, stack);
210 RE.replace_all(text, do_try_highlight)
211}
212
213fn get_alias_documentation(
214 long_desc: &mut String,
215 command: &dyn Command,
216 sig: &Signature,
217 help_style: &HelpStyle,
218 engine_state: &EngineState,
219 stack: &mut Stack,
220) {
221 let help_section_name = &help_style.section_name;
222 let help_subcolor_one = &help_style.subcolor_one;
223
224 let alias_name = &sig.name;
225
226 long_desc.push_str(&format!(
227 "{help_section_name}Alias{RESET}: {help_subcolor_one}{alias_name}{RESET}"
228 ));
229 long_desc.push_str("\n\n");
230
231 let Some(alias) = command.as_alias() else {
232 return;
234 };
235
236 let alias_expansion =
237 String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span));
238
239 long_desc.push_str(&format!(
240 "{help_section_name}Expansion{RESET}:\n {}",
241 nu_highlight_string(&alias_expansion, engine_state, stack)
242 ));
243}
244
245fn get_command_documentation(
246 long_desc: &mut String,
247 command: &dyn Command,
248 sig: &Signature,
249 nu_config: &Config,
250 help_style: &HelpStyle,
251 engine_state: &EngineState,
252 stack: &mut Stack,
253) {
254 let help_section_name = &help_style.section_name;
255 let help_subcolor_one = &help_style.subcolor_one;
256
257 let cmd_name = &sig.name;
258
259 if !sig.search_terms.is_empty() {
260 let _ = write!(
261 long_desc,
262 "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
263 sig.search_terms.join(", "),
264 );
265 }
266
267 let _ = write!(
268 long_desc,
269 "{help_section_name}Usage{RESET}:\n > {}\n",
270 sig.call_signature()
271 );
272
273 let mut subcommands = vec![];
281 let signatures = engine_state.get_signatures_and_declids(true);
282 for (sig, decl_id) in signatures {
283 let command_type = engine_state.get_decl(decl_id).command_type();
284
285 if sig.name.starts_with(&format!("{cmd_name} "))
287 && !matches!(sig.category, Category::Removed)
288 {
289 if command_type == CommandType::Plugin
291 || command_type == CommandType::Alias
292 || command_type == CommandType::Custom
293 {
294 subcommands.push(format!(
295 " {help_subcolor_one}{} {help_section_name}({}){RESET} - {}",
296 sig.name,
297 command_type,
298 highlight_code(&sig.description, engine_state, stack)
299 ));
300 } else {
301 subcommands.push(format!(
302 " {help_subcolor_one}{}{RESET} - {}",
303 sig.name,
304 highlight_code(&sig.description, engine_state, stack)
305 ));
306 }
307 }
308 }
309
310 if !subcommands.is_empty() {
311 let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
312 subcommands.sort();
313 long_desc.push_str(&subcommands.join("\n"));
314 long_desc.push('\n');
315 }
316
317 if !sig.named.is_empty() {
318 long_desc.push_str(&get_flags_section(sig, help_style, |v| match v {
319 FormatterValue::DefaultValue(value) => nu_highlight_string(
320 &value.to_parsable_string(", ", nu_config),
321 engine_state,
322 stack,
323 ),
324 FormatterValue::CodeString(text) => {
325 highlight_code(text, engine_state, stack).to_string()
326 }
327 }))
328 }
329
330 if !sig.required_positional.is_empty()
331 || !sig.optional_positional.is_empty()
332 || sig.rest_positional.is_some()
333 {
334 let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
335 for positional in &sig.required_positional {
336 write_positional(
337 long_desc,
338 positional,
339 PositionalKind::Required,
340 help_style,
341 nu_config,
342 engine_state,
343 stack,
344 );
345 }
346 for positional in &sig.optional_positional {
347 write_positional(
348 long_desc,
349 positional,
350 PositionalKind::Optional,
351 help_style,
352 nu_config,
353 engine_state,
354 stack,
355 );
356 }
357
358 if let Some(rest_positional) = &sig.rest_positional {
359 write_positional(
360 long_desc,
361 rest_positional,
362 PositionalKind::Rest,
363 help_style,
364 nu_config,
365 engine_state,
366 stack,
367 );
368 }
369 }
370
371 fn get_term_width() -> usize {
372 if let Ok((w, _h)) = terminal_size() {
373 w as usize
374 } else {
375 80
376 }
377 }
378
379 if !command.is_keyword() && !sig.input_output_types.is_empty() {
380 if let Some(decl_id) = engine_state.find_decl(b"table", &[]) {
381 let span = Span::unknown();
383 let mut vals = vec![];
384 for (input, output) in &sig.input_output_types {
385 vals.push(Value::record(
386 record! {
387 "input" => Value::string(input.to_string(), span),
388 "output" => Value::string(output.to_string(), span),
389 },
390 span,
391 ));
392 }
393
394 let caller_stack = &mut Stack::new().collect_value();
395 if let Ok(result) = eval_call::<WithoutDebug>(
396 engine_state,
397 caller_stack,
398 &Call {
399 decl_id,
400 head: span,
401 arguments: vec![Argument::Named((
402 Spanned {
403 item: "width".to_string(),
404 span: Span::unknown(),
405 },
406 None,
407 Some(Expression::new_unknown(
408 Expr::Int(get_term_width() as i64 - 2), Span::unknown(),
410 Type::Int,
411 )),
412 ))],
413 parser_info: HashMap::new(),
414 },
415 PipelineData::Value(Value::list(vals, span), None),
416 ) {
417 if let Ok((str, ..)) = result.collect_string_strict(span) {
418 let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
419 for line in str.lines() {
420 let _ = writeln!(long_desc, " {line}");
421 }
422 }
423 }
424 }
425 }
426
427 let examples = command.examples();
428
429 if !examples.is_empty() {
430 let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
431 }
432
433 for example in examples {
434 long_desc.push('\n');
435 long_desc.push_str(" ");
436 long_desc.push_str(&highlight_code(example.description, engine_state, stack));
437
438 if !nu_config.use_ansi_coloring.get(engine_state) {
439 let _ = write!(long_desc, "\n > {}\n", example.example);
440 } else {
441 let code_string = nu_highlight_string(example.example, engine_state, stack);
442 let _ = write!(long_desc, "\n > {code_string}\n");
443 };
444
445 if let Some(result) = &example.result {
446 let mut table_call = Call::new(Span::unknown());
447 if example.example.ends_with("--collapse") {
448 table_call.add_named((
450 Spanned {
451 item: "collapse".to_string(),
452 span: Span::unknown(),
453 },
454 None,
455 None,
456 ))
457 } else {
458 table_call.add_named((
460 Spanned {
461 item: "expand".to_string(),
462 span: Span::unknown(),
463 },
464 None,
465 None,
466 ))
467 }
468 table_call.add_named((
469 Spanned {
470 item: "width".to_string(),
471 span: Span::unknown(),
472 },
473 None,
474 Some(Expression::new_unknown(
475 Expr::Int(get_term_width() as i64 - 2),
476 Span::unknown(),
477 Type::Int,
478 )),
479 ));
480
481 let table = engine_state
482 .find_decl("table".as_bytes(), &[])
483 .and_then(|decl_id| {
484 engine_state
485 .get_decl(decl_id)
486 .run(
487 engine_state,
488 stack,
489 &(&table_call).into(),
490 PipelineData::Value(result.clone(), None),
491 )
492 .ok()
493 });
494
495 for item in table.into_iter().flatten() {
496 let _ = writeln!(
497 long_desc,
498 " {}",
499 item.to_expanded_string("", nu_config)
500 .replace('\n', "\n ")
501 .trim()
502 );
503 }
504 }
505 }
506
507 long_desc.push('\n');
508}
509
510fn update_ansi_from_config(
511 ansi_code: &mut String,
512 engine_state: &EngineState,
513 nu_config: &Config,
514 theme_component: &str,
515) {
516 if let Some(color) = &nu_config.color_config.get(theme_component) {
517 let caller_stack = &mut Stack::new().collect_value();
518 let span = Span::unknown();
519 let span_id = UNKNOWN_SPAN_ID;
520
521 let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
522
523 if let Some(argument) = argument_opt {
525 if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
526 if let Ok(result) = eval_call::<WithoutDebug>(
527 engine_state,
528 caller_stack,
529 &Call {
530 decl_id,
531 head: span,
532 arguments: vec![argument],
533 parser_info: HashMap::new(),
534 },
535 PipelineData::Empty,
536 ) {
537 if let Ok((str, ..)) = result.collect_string_strict(span) {
538 *ansi_code = str;
539 }
540 }
541 }
542 }
543 }
544}
545
546fn get_argument_for_color_value(
547 nu_config: &Config,
548 color: &Value,
549 span: Span,
550 span_id: SpanId,
551) -> Option<Argument> {
552 match color {
553 Value::Record { val, .. } => {
554 let record_exp: Vec<RecordItem> = (**val)
555 .iter()
556 .map(|(k, v)| {
557 RecordItem::Pair(
558 Expression::new_existing(
559 Expr::String(k.clone()),
560 span,
561 span_id,
562 Type::String,
563 ),
564 Expression::new_existing(
565 Expr::String(v.clone().to_expanded_string("", nu_config)),
566 span,
567 span_id,
568 Type::String,
569 ),
570 )
571 })
572 .collect();
573
574 Some(Argument::Positional(Expression::new_existing(
575 Expr::Record(record_exp),
576 Span::unknown(),
577 UNKNOWN_SPAN_ID,
578 Type::Record(
579 [
580 ("fg".to_string(), Type::String),
581 ("attr".to_string(), Type::String),
582 ]
583 .into(),
584 ),
585 )))
586 }
587 Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
588 Expr::String(val.clone()),
589 Span::unknown(),
590 UNKNOWN_SPAN_ID,
591 Type::String,
592 ))),
593 _ => None,
594 }
595}
596
597pub struct HelpStyle {
603 section_name: String,
604 subcolor_one: String,
605 subcolor_two: String,
606}
607
608impl Default for HelpStyle {
609 fn default() -> Self {
610 HelpStyle {
611 section_name: "\x1b[32m".to_string(),
613 subcolor_one: "\x1b[36m".to_string(),
615 subcolor_two: "\x1b[94m".to_string(),
617 }
618 }
619}
620
621impl HelpStyle {
622 pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
630 update_ansi_from_config(
631 &mut self.section_name,
632 engine_state,
633 nu_config,
634 "shape_string",
635 );
636 update_ansi_from_config(
637 &mut self.subcolor_one,
638 engine_state,
639 nu_config,
640 "shape_external",
641 );
642 update_ansi_from_config(
643 &mut self.subcolor_two,
644 engine_state,
645 nu_config,
646 "shape_block",
647 );
648 }
649}
650
651fn document_shape(shape: &SyntaxShape) -> &SyntaxShape {
653 match shape {
654 SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape,
655 _ => shape,
656 }
657}
658
659#[derive(PartialEq)]
660enum PositionalKind {
661 Required,
662 Optional,
663 Rest,
664}
665
666fn write_positional(
667 long_desc: &mut String,
668 positional: &PositionalArg,
669 arg_kind: PositionalKind,
670 help_style: &HelpStyle,
671 nu_config: &Config,
672 engine_state: &EngineState,
673 stack: &mut Stack,
674) {
675 let help_subcolor_one = &help_style.subcolor_one;
676 let help_subcolor_two = &help_style.subcolor_two;
677
678 long_desc.push_str(" ");
680 if arg_kind == PositionalKind::Rest {
681 long_desc.push_str("...");
682 }
683 match &positional.shape {
684 SyntaxShape::Keyword(kw, shape) => {
685 let _ = write!(
686 long_desc,
687 "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
688 String::from_utf8_lossy(kw),
689 document_shape(shape),
690 );
691 }
692 _ => {
693 let _ = write!(
694 long_desc,
695 "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
696 positional.name,
697 document_shape(&positional.shape),
698 );
699 }
700 };
701 if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
702 let _ = write!(
703 long_desc,
704 ": {}",
705 highlight_code(&positional.desc, engine_state, stack)
706 );
707 }
708 if arg_kind == PositionalKind::Optional {
709 if let Some(value) = &positional.default_value {
710 let _ = write!(
711 long_desc,
712 " (optional, default: {})",
713 nu_highlight_string(
714 &value.to_parsable_string(", ", nu_config),
715 engine_state,
716 stack
717 )
718 );
719 } else {
720 long_desc.push_str(" (optional)");
721 };
722 }
723 long_desc.push('\n');
724}
725
726pub enum FormatterValue<'a> {
732 DefaultValue(&'a Value),
734 CodeString(&'a str),
736}
737
738pub fn get_flags_section<F>(
739 signature: &Signature,
740 help_style: &HelpStyle,
741 mut formatter: F, ) -> String
743where
744 F: FnMut(FormatterValue) -> String,
745{
746 let help_section_name = &help_style.section_name;
747 let help_subcolor_one = &help_style.subcolor_one;
748 let help_subcolor_two = &help_style.subcolor_two;
749
750 let mut long_desc = String::new();
751 let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
752 for flag in &signature.named {
753 long_desc.push_str(" ");
755 if let Some(short) = flag.short {
757 let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
758 if !flag.long.is_empty() {
759 let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
760 }
761 }
762 if !flag.long.is_empty() {
763 let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
764 }
765 if flag.required {
766 long_desc.push_str(" (required parameter)")
767 }
768 if let Some(arg) = &flag.arg {
770 let _ = write!(
771 long_desc,
772 " <{help_subcolor_two}{}{RESET}>",
773 document_shape(arg)
774 );
775 }
776 if !flag.desc.is_empty() {
777 let _ = write!(
778 long_desc,
779 ": {}",
780 &formatter(FormatterValue::CodeString(&flag.desc))
781 );
782 }
783 if let Some(value) = &flag.default_value {
784 let _ = write!(
785 long_desc,
786 " (default: {})",
787 &formatter(FormatterValue::DefaultValue(value))
788 );
789 }
790 long_desc.push('\n');
791 }
792 long_desc
793}
794
795#[cfg(test)]
796mod tests {
797 use nu_protocol::UseAnsiColoring;
798
799 use super::*;
800
801 #[test]
802 fn test_code_formatting() {
803 let mut engine_state = EngineState::new();
804 let mut stack = Stack::new();
805
806 let mut config = (*engine_state.config).clone();
808 config.use_ansi_coloring = UseAnsiColoring::True;
809 engine_state.config = Arc::new(config);
810
811 let haystack = "Run the `foo` command";
816 assert!(matches!(
817 highlight_code(haystack, &engine_state, &mut stack),
818 Cow::Owned(_)
819 ));
820
821 let haystack = "foo`bar`";
823 assert!(matches!(
824 highlight_code(haystack, &engine_state, &mut stack),
825 Cow::Borrowed(_)
826 ));
827
828 let haystack = "`my-command` is cool";
830 assert!(matches!(
831 highlight_code(haystack, &engine_state, &mut stack),
832 Cow::Owned(_)
833 ));
834
835 let haystack = r"
837 `command`
838 ";
839 assert!(matches!(
840 highlight_code(haystack, &engine_state, &mut stack),
841 Cow::Owned(_)
842 ));
843
844 let haystack = "// hello `beautiful \n world`";
846 assert!(matches!(
847 highlight_code(haystack, &engine_state, &mut stack),
848 Cow::Borrowed(_)
849 ));
850
851 let haystack = "try running `my cool command`.";
853 assert!(matches!(
854 highlight_code(haystack, &engine_state, &mut stack),
855 Cow::Owned(_)
856 ));
857
858 let haystack = "a command (`my cool command`).";
860 assert!(matches!(
861 highlight_code(haystack, &engine_state, &mut stack),
862 Cow::Owned(_)
863 ));
864
865 let haystack = "```\ncode block\n```";
868 assert!(matches!(
869 highlight_code(haystack, &engine_state, &mut stack),
870 Cow::Borrowed(_)
871 ));
872 }
873}