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 .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 if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
527 if 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 if let Ok((str, ..)) = result.collect_string_strict(span) {
539 *ansi_code = str;
540 }
541 }
542 }
543 }
544 }
545}
546
547fn get_argument_for_color_value(
548 nu_config: &Config,
549 color: &Value,
550 span: Span,
551 span_id: SpanId,
552) -> Option<Argument> {
553 match color {
554 Value::Record { val, .. } => {
555 let record_exp: Vec<RecordItem> = (**val)
556 .iter()
557 .map(|(k, v)| {
558 RecordItem::Pair(
559 Expression::new_existing(
560 Expr::String(k.clone()),
561 span,
562 span_id,
563 Type::String,
564 ),
565 Expression::new_existing(
566 Expr::String(v.clone().to_expanded_string("", nu_config)),
567 span,
568 span_id,
569 Type::String,
570 ),
571 )
572 })
573 .collect();
574
575 Some(Argument::Positional(Expression::new_existing(
576 Expr::Record(record_exp),
577 Span::unknown(),
578 UNKNOWN_SPAN_ID,
579 Type::Record(
580 [
581 ("fg".to_string(), Type::String),
582 ("attr".to_string(), Type::String),
583 ]
584 .into(),
585 ),
586 )))
587 }
588 Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
589 Expr::String(val.clone()),
590 Span::unknown(),
591 UNKNOWN_SPAN_ID,
592 Type::String,
593 ))),
594 _ => None,
595 }
596}
597
598pub struct HelpStyle {
604 section_name: String,
605 subcolor_one: String,
606 subcolor_two: String,
607}
608
609impl Default for HelpStyle {
610 fn default() -> Self {
611 HelpStyle {
612 section_name: "\x1b[32m".to_string(),
614 subcolor_one: "\x1b[36m".to_string(),
616 subcolor_two: "\x1b[94m".to_string(),
618 }
619 }
620}
621
622impl HelpStyle {
623 pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
631 update_ansi_from_config(
632 &mut self.section_name,
633 engine_state,
634 nu_config,
635 "shape_string",
636 );
637 update_ansi_from_config(
638 &mut self.subcolor_one,
639 engine_state,
640 nu_config,
641 "shape_external",
642 );
643 update_ansi_from_config(
644 &mut self.subcolor_two,
645 engine_state,
646 nu_config,
647 "shape_block",
648 );
649 }
650}
651
652#[derive(PartialEq)]
653enum PositionalKind {
654 Required,
655 Optional,
656 Rest,
657}
658
659fn write_positional(
660 long_desc: &mut String,
661 positional: &PositionalArg,
662 arg_kind: PositionalKind,
663 help_style: &HelpStyle,
664 nu_config: &Config,
665 engine_state: &EngineState,
666 stack: &mut Stack,
667) {
668 let help_subcolor_one = &help_style.subcolor_one;
669 let help_subcolor_two = &help_style.subcolor_two;
670
671 long_desc.push_str(" ");
673 if arg_kind == PositionalKind::Rest {
674 long_desc.push_str("...");
675 }
676 match &positional.shape {
677 SyntaxShape::Keyword(kw, shape) => {
678 let _ = write!(
679 long_desc,
680 "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
681 String::from_utf8_lossy(kw),
682 shape,
683 );
684 }
685 _ => {
686 let _ = write!(
687 long_desc,
688 "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
689 positional.name, &positional.shape,
690 );
691 }
692 };
693 if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
694 let _ = write!(
695 long_desc,
696 ": {}",
697 highlight_code(&positional.desc, engine_state, stack)
698 );
699 }
700 if arg_kind == PositionalKind::Optional {
701 if let Some(value) = &positional.default_value {
702 let _ = write!(
703 long_desc,
704 " (optional, default: {})",
705 nu_highlight_string(
706 &value.to_parsable_string(", ", nu_config),
707 engine_state,
708 stack
709 )
710 );
711 } else {
712 long_desc.push_str(" (optional)");
713 };
714 }
715 long_desc.push('\n');
716}
717
718pub enum FormatterValue<'a> {
724 DefaultValue(&'a Value),
726 CodeString(&'a str),
728}
729
730fn write_flag_to_long_desc<F>(
731 flag: &nu_protocol::Flag,
732 long_desc: &mut String,
733 help_subcolor_one: &str,
734 help_subcolor_two: &str,
735 formatter: &mut F,
736) where
737 F: FnMut(FormatterValue) -> String,
738{
739 long_desc.push_str(" ");
741 if let Some(short) = flag.short {
743 let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
744 if !flag.long.is_empty() {
745 let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
746 }
747 }
748 if !flag.long.is_empty() {
749 let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
750 }
751 if flag.required {
752 long_desc.push_str(" (required parameter)")
753 }
754 if let Some(arg) = &flag.arg {
756 let _ = write!(long_desc, " <{help_subcolor_two}{arg}{RESET}>");
757 }
758 if !flag.desc.is_empty() {
759 let _ = write!(
760 long_desc,
761 ": {}",
762 &formatter(FormatterValue::CodeString(&flag.desc))
763 );
764 }
765 if let Some(value) = &flag.default_value {
766 let _ = write!(
767 long_desc,
768 " (default: {})",
769 &formatter(FormatterValue::DefaultValue(value))
770 );
771 }
772 long_desc.push('\n');
773}
774
775pub fn get_flags_section<F>(
776 signature: &Signature,
777 help_style: &HelpStyle,
778 mut formatter: F, ) -> String
780where
781 F: FnMut(FormatterValue) -> String,
782{
783 let help_section_name = &help_style.section_name;
784 let help_subcolor_one = &help_style.subcolor_one;
785 let help_subcolor_two = &help_style.subcolor_two;
786
787 let mut long_desc = String::new();
788 let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
789
790 let help = signature.named.iter().find(|flag| flag.long == "help");
791 let required = signature.named.iter().filter(|flag| flag.required);
792 let optional = signature
793 .named
794 .iter()
795 .filter(|flag| !flag.required && flag.long != "help");
796
797 let flags = required.chain(help).chain(optional);
798
799 for flag in flags {
800 write_flag_to_long_desc(
801 flag,
802 &mut long_desc,
803 help_subcolor_one,
804 help_subcolor_two,
805 &mut formatter,
806 );
807 }
808
809 long_desc
810}
811
812#[cfg(test)]
813mod tests {
814 use nu_protocol::UseAnsiColoring;
815
816 use super::*;
817
818 #[test]
819 fn test_code_formatting() {
820 let mut engine_state = EngineState::new();
821 let mut stack = Stack::new();
822
823 let mut config = (*engine_state.config).clone();
825 config.use_ansi_coloring = UseAnsiColoring::True;
826 engine_state.config = Arc::new(config);
827
828 let haystack = "Run the `foo` command";
833 assert!(matches!(
834 highlight_code(haystack, &engine_state, &mut stack),
835 Cow::Owned(_)
836 ));
837
838 let haystack = "foo`bar`";
840 assert!(matches!(
841 highlight_code(haystack, &engine_state, &mut stack),
842 Cow::Borrowed(_)
843 ));
844
845 let haystack = "`my-command` is cool";
847 assert!(matches!(
848 highlight_code(haystack, &engine_state, &mut stack),
849 Cow::Owned(_)
850 ));
851
852 let haystack = r"
854 `command`
855 ";
856 assert!(matches!(
857 highlight_code(haystack, &engine_state, &mut stack),
858 Cow::Owned(_)
859 ));
860
861 let haystack = "// hello `beautiful \n world`";
863 assert!(matches!(
864 highlight_code(haystack, &engine_state, &mut stack),
865 Cow::Borrowed(_)
866 ));
867
868 let haystack = "try running `my cool command`.";
870 assert!(matches!(
871 highlight_code(haystack, &engine_state, &mut stack),
872 Cow::Owned(_)
873 ));
874
875 let haystack = "a command (`my cool command`).";
877 assert!(matches!(
878 highlight_code(haystack, &engine_state, &mut stack),
879 Cow::Owned(_)
880 ));
881
882 let haystack = "```\ncode block\n```";
885 assert!(matches!(
886 highlight_code(haystack, &engine_state, &mut stack),
887 Cow::Borrowed(_)
888 ));
889 }
890}