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()
380 && !sig.input_output_types.is_empty()
381 && let Some(decl_id) = engine_state.find_decl(b"table", &[])
382 {
383 let span = Span::unknown();
385 let mut vals = vec![];
386 for (input, output) in &sig.input_output_types {
387 vals.push(Value::record(
388 record! {
389 "input" => Value::string(input.to_string(), span),
390 "output" => Value::string(output.to_string(), span),
391 },
392 span,
393 ));
394 }
395
396 let caller_stack = &mut Stack::new().collect_value();
397 if let Ok(result) = eval_call::<WithoutDebug>(
398 engine_state,
399 caller_stack,
400 &Call {
401 decl_id,
402 head: span,
403 arguments: vec![Argument::Named((
404 Spanned {
405 item: "width".to_string(),
406 span: Span::unknown(),
407 },
408 None,
409 Some(Expression::new_unknown(
410 Expr::Int(get_term_width() as i64 - 2), Span::unknown(),
412 Type::Int,
413 )),
414 ))],
415 parser_info: HashMap::new(),
416 },
417 PipelineData::value(Value::list(vals, span), None),
418 ) && let Ok((str, ..)) = result.collect_string_strict(span)
419 {
420 let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
421 for line in str.lines() {
422 let _ = writeln!(long_desc, " {line}");
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 .trim_end()
501 .trim_start_matches(|c: char| c.is_whitespace() && c != ' ')
502 .replace('\n', "\n ")
503 );
504 }
505 }
506 }
507
508 long_desc.push('\n');
509}
510
511fn update_ansi_from_config(
512 ansi_code: &mut String,
513 engine_state: &EngineState,
514 nu_config: &Config,
515 theme_component: &str,
516) {
517 if let Some(color) = &nu_config.color_config.get(theme_component) {
518 let caller_stack = &mut Stack::new().collect_value();
519 let span = Span::unknown();
520 let span_id = UNKNOWN_SPAN_ID;
521
522 let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
523
524 if let Some(argument) = argument_opt
526 && let Some(decl_id) = engine_state.find_decl(b"ansi", &[])
527 && let Ok(result) = eval_call::<WithoutDebug>(
528 engine_state,
529 caller_stack,
530 &Call {
531 decl_id,
532 head: span,
533 arguments: vec![argument],
534 parser_info: HashMap::new(),
535 },
536 PipelineData::empty(),
537 )
538 && let Ok((str, ..)) = result.collect_string_strict(span)
539 {
540 *ansi_code = str;
541 }
542 }
543}
544
545fn get_argument_for_color_value(
546 nu_config: &Config,
547 color: &Value,
548 span: Span,
549 span_id: SpanId,
550) -> Option<Argument> {
551 match color {
552 Value::Record { val, .. } => {
553 let record_exp: Vec<RecordItem> = (**val)
554 .iter()
555 .map(|(k, v)| {
556 RecordItem::Pair(
557 Expression::new_existing(
558 Expr::String(k.clone()),
559 span,
560 span_id,
561 Type::String,
562 ),
563 Expression::new_existing(
564 Expr::String(v.clone().to_expanded_string("", nu_config)),
565 span,
566 span_id,
567 Type::String,
568 ),
569 )
570 })
571 .collect();
572
573 Some(Argument::Positional(Expression::new_existing(
574 Expr::Record(record_exp),
575 Span::unknown(),
576 UNKNOWN_SPAN_ID,
577 Type::Record(
578 [
579 ("fg".to_string(), Type::String),
580 ("attr".to_string(), Type::String),
581 ]
582 .into(),
583 ),
584 )))
585 }
586 Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
587 Expr::String(val.clone()),
588 Span::unknown(),
589 UNKNOWN_SPAN_ID,
590 Type::String,
591 ))),
592 _ => None,
593 }
594}
595
596pub struct HelpStyle {
602 section_name: String,
603 subcolor_one: String,
604 subcolor_two: String,
605}
606
607impl Default for HelpStyle {
608 fn default() -> Self {
609 HelpStyle {
610 section_name: "\x1b[32m".to_string(),
612 subcolor_one: "\x1b[36m".to_string(),
614 subcolor_two: "\x1b[94m".to_string(),
616 }
617 }
618}
619
620impl HelpStyle {
621 pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
629 update_ansi_from_config(
630 &mut self.section_name,
631 engine_state,
632 nu_config,
633 "shape_string",
634 );
635 update_ansi_from_config(
636 &mut self.subcolor_one,
637 engine_state,
638 nu_config,
639 "shape_external",
640 );
641 update_ansi_from_config(
642 &mut self.subcolor_two,
643 engine_state,
644 nu_config,
645 "shape_block",
646 );
647 }
648}
649
650#[derive(PartialEq)]
651enum PositionalKind {
652 Required,
653 Optional,
654 Rest,
655}
656
657fn write_positional(
658 long_desc: &mut String,
659 positional: &PositionalArg,
660 arg_kind: PositionalKind,
661 help_style: &HelpStyle,
662 nu_config: &Config,
663 engine_state: &EngineState,
664 stack: &mut Stack,
665) {
666 let help_subcolor_one = &help_style.subcolor_one;
667 let help_subcolor_two = &help_style.subcolor_two;
668
669 long_desc.push_str(" ");
671 if arg_kind == PositionalKind::Rest {
672 long_desc.push_str("...");
673 }
674 match &positional.shape {
675 SyntaxShape::Keyword(kw, shape) => {
676 let _ = write!(
677 long_desc,
678 "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
679 String::from_utf8_lossy(kw),
680 shape,
681 );
682 }
683 _ => {
684 let _ = write!(
685 long_desc,
686 "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
687 positional.name, &positional.shape,
688 );
689 }
690 };
691 if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
692 let _ = write!(
693 long_desc,
694 ": {}",
695 highlight_code(&positional.desc, engine_state, stack)
696 );
697 }
698 if arg_kind == PositionalKind::Optional {
699 if let Some(value) = &positional.default_value {
700 let _ = write!(
701 long_desc,
702 " (optional, default: {})",
703 nu_highlight_string(
704 &value.to_parsable_string(", ", nu_config),
705 engine_state,
706 stack
707 )
708 );
709 } else {
710 long_desc.push_str(" (optional)");
711 };
712 }
713 long_desc.push('\n');
714}
715
716pub enum FormatterValue<'a> {
722 DefaultValue(&'a Value),
724 CodeString(&'a str),
726}
727
728fn write_flag_to_long_desc<F>(
729 flag: &nu_protocol::Flag,
730 long_desc: &mut String,
731 help_subcolor_one: &str,
732 help_subcolor_two: &str,
733 formatter: &mut F,
734) where
735 F: FnMut(FormatterValue) -> String,
736{
737 long_desc.push_str(" ");
739 if let Some(short) = flag.short {
741 let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
742 if !flag.long.is_empty() {
743 let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
744 }
745 }
746 if !flag.long.is_empty() {
747 let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
748 }
749 if flag.required {
750 long_desc.push_str(" (required parameter)")
751 }
752 if let Some(arg) = &flag.arg {
754 let _ = write!(long_desc, " <{help_subcolor_two}{arg}{RESET}>");
755 }
756 if !flag.desc.is_empty() {
757 let _ = write!(
758 long_desc,
759 ": {}",
760 &formatter(FormatterValue::CodeString(&flag.desc))
761 );
762 }
763 if let Some(value) = &flag.default_value {
764 let _ = write!(
765 long_desc,
766 " (default: {})",
767 &formatter(FormatterValue::DefaultValue(value))
768 );
769 }
770 long_desc.push('\n');
771}
772
773pub fn get_flags_section<F>(
774 signature: &Signature,
775 help_style: &HelpStyle,
776 mut formatter: F, ) -> String
778where
779 F: FnMut(FormatterValue) -> String,
780{
781 let help_section_name = &help_style.section_name;
782 let help_subcolor_one = &help_style.subcolor_one;
783 let help_subcolor_two = &help_style.subcolor_two;
784
785 let mut long_desc = String::new();
786 let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
787
788 let help = signature.named.iter().find(|flag| flag.long == "help");
789 let required = signature.named.iter().filter(|flag| flag.required);
790 let optional = signature
791 .named
792 .iter()
793 .filter(|flag| !flag.required && flag.long != "help");
794
795 let flags = required.chain(help).chain(optional);
796
797 for flag in flags {
798 write_flag_to_long_desc(
799 flag,
800 &mut long_desc,
801 help_subcolor_one,
802 help_subcolor_two,
803 &mut formatter,
804 );
805 }
806
807 long_desc
808}
809
810#[cfg(test)]
811mod tests {
812 use nu_protocol::UseAnsiColoring;
813
814 use super::*;
815
816 #[test]
817 fn test_code_formatting() {
818 let mut engine_state = EngineState::new();
819 let mut stack = Stack::new();
820
821 let mut config = (*engine_state.config).clone();
823 config.use_ansi_coloring = UseAnsiColoring::True;
824 engine_state.config = Arc::new(config);
825
826 let haystack = "Run the `foo` command";
831 assert!(matches!(
832 highlight_code(haystack, &engine_state, &mut stack),
833 Cow::Owned(_)
834 ));
835
836 let haystack = "foo`bar`";
838 assert!(matches!(
839 highlight_code(haystack, &engine_state, &mut stack),
840 Cow::Borrowed(_)
841 ));
842
843 let haystack = "`my-command` is cool";
845 assert!(matches!(
846 highlight_code(haystack, &engine_state, &mut stack),
847 Cow::Owned(_)
848 ));
849
850 let haystack = r"
852 `command`
853 ";
854 assert!(matches!(
855 highlight_code(haystack, &engine_state, &mut stack),
856 Cow::Owned(_)
857 ));
858
859 let haystack = "// hello `beautiful \n world`";
861 assert!(matches!(
862 highlight_code(haystack, &engine_state, &mut stack),
863 Cow::Borrowed(_)
864 ));
865
866 let haystack = "try running `my cool command`.";
868 assert!(matches!(
869 highlight_code(haystack, &engine_state, &mut stack),
870 Cow::Owned(_)
871 ));
872
873 let haystack = "a command (`my cool command`).";
875 assert!(matches!(
876 highlight_code(haystack, &engine_state, &mut stack),
877 Cow::Owned(_)
878 ));
879
880 let haystack = "```\ncode block\n```";
883 assert!(matches!(
884 highlight_code(haystack, &engine_state, &mut stack),
885 Cow::Borrowed(_)
886 ));
887 }
888}