nu_engine/
documentation.rs

1use crate::eval_call;
2use nu_protocol::{
3    ast::{Argument, Call, Expr, Expression, RecordItem},
4    debugger::WithoutDebug,
5    engine::CommandType,
6    engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
7    record, Category, Config, Example, IntoPipelineData, PipelineData, PositionalArg, Signature,
8    Span, SpanId, Spanned, SyntaxShape, Type, Value,
9};
10use nu_utils::terminal_size;
11use std::{collections::HashMap, fmt::Write};
12
13/// ANSI style reset
14const RESET: &str = "\x1b[0m";
15/// ANSI set default color (as set in the terminal)
16const DEFAULT_COLOR: &str = "\x1b[39m";
17
18pub fn get_full_help(
19    command: &dyn Command,
20    engine_state: &EngineState,
21    stack: &mut Stack,
22) -> String {
23    // Precautionary step to capture any command output generated during this operation. We
24    // internally call several commands (`table`, `ansi`, `nu-highlight`) and get their
25    // `PipelineData` using this `Stack`, any other output should not be redirected like the main
26    // execution.
27    let stack = &mut stack.start_collect_value();
28
29    let signature = engine_state
30        .get_signature(command)
31        .update_from_command(command);
32
33    get_documentation(
34        &signature,
35        &command.examples(),
36        engine_state,
37        stack,
38        command.is_keyword(),
39    )
40}
41
42/// Syntax highlight code using the `nu-highlight` command if available
43fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
44    if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
45        let decl = engine_state.get_decl(highlighter);
46
47        let call = Call::new(Span::unknown());
48
49        if let Ok(output) = decl.run(
50            engine_state,
51            stack,
52            &(&call).into(),
53            Value::string(code_string, Span::unknown()).into_pipeline_data(),
54        ) {
55            let result = output.into_value(Span::unknown());
56            if let Ok(s) = result.and_then(Value::coerce_into_string) {
57                return s; // successfully highlighted string
58            }
59        }
60    }
61    code_string.to_string()
62}
63
64fn get_documentation(
65    sig: &Signature,
66    examples: &[Example],
67    engine_state: &EngineState,
68    stack: &mut Stack,
69    is_parser_keyword: bool,
70) -> String {
71    let nu_config = stack.get_config(engine_state);
72
73    // Create ansi colors
74    let mut help_style = HelpStyle::default();
75    help_style.update_from_config(engine_state, &nu_config);
76    let help_section_name = &help_style.section_name;
77    let help_subcolor_one = &help_style.subcolor_one;
78
79    let cmd_name = &sig.name;
80    let mut long_desc = String::new();
81
82    let desc = &sig.description;
83    if !desc.is_empty() {
84        long_desc.push_str(desc);
85        long_desc.push_str("\n\n");
86    }
87
88    let extra_desc = &sig.extra_description;
89    if !extra_desc.is_empty() {
90        long_desc.push_str(extra_desc);
91        long_desc.push_str("\n\n");
92    }
93
94    if !sig.search_terms.is_empty() {
95        let _ = write!(
96            long_desc,
97            "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
98            sig.search_terms.join(", "),
99        );
100    }
101
102    let _ = write!(
103        long_desc,
104        "{help_section_name}Usage{RESET}:\n  > {}\n",
105        sig.call_signature()
106    );
107
108    // TODO: improve the subcommand name resolution
109    // issues:
110    // - Aliases are included
111    //   - https://github.com/nushell/nushell/issues/11657
112    // - Subcommands are included violating module scoping
113    //   - https://github.com/nushell/nushell/issues/11447
114    //   - https://github.com/nushell/nushell/issues/11625
115    let mut subcommands = vec![];
116    let signatures = engine_state.get_signatures_and_declids(true);
117    for (sig, decl_id) in signatures {
118        let command_type = engine_state.get_decl(decl_id).command_type();
119
120        // Don't display removed/deprecated commands in the Subcommands list
121        if sig.name.starts_with(&format!("{cmd_name} "))
122            && !matches!(sig.category, Category::Removed)
123        {
124            // If it's a plugin, alias, or custom command, display that information in the help
125            if command_type == CommandType::Plugin
126                || command_type == CommandType::Alias
127                || command_type == CommandType::Custom
128            {
129                subcommands.push(format!(
130                    "  {help_subcolor_one}{} {help_section_name}({}){RESET} - {}",
131                    sig.name, command_type, sig.description
132                ));
133            } else {
134                subcommands.push(format!(
135                    "  {help_subcolor_one}{}{RESET} - {}",
136                    sig.name, sig.description
137                ));
138            }
139        }
140    }
141
142    if !subcommands.is_empty() {
143        let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
144        subcommands.sort();
145        long_desc.push_str(&subcommands.join("\n"));
146        long_desc.push('\n');
147    }
148
149    if !sig.named.is_empty() {
150        long_desc.push_str(&get_flags_section(sig, &help_style, |v| {
151            nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack)
152        }))
153    }
154
155    if !sig.required_positional.is_empty()
156        || !sig.optional_positional.is_empty()
157        || sig.rest_positional.is_some()
158    {
159        let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
160        for positional in &sig.required_positional {
161            write_positional(
162                &mut long_desc,
163                positional,
164                PositionalKind::Required,
165                &help_style,
166                &nu_config,
167                engine_state,
168                stack,
169            );
170        }
171        for positional in &sig.optional_positional {
172            write_positional(
173                &mut long_desc,
174                positional,
175                PositionalKind::Optional,
176                &help_style,
177                &nu_config,
178                engine_state,
179                stack,
180            );
181        }
182
183        if let Some(rest_positional) = &sig.rest_positional {
184            write_positional(
185                &mut long_desc,
186                rest_positional,
187                PositionalKind::Rest,
188                &help_style,
189                &nu_config,
190                engine_state,
191                stack,
192            );
193        }
194    }
195
196    fn get_term_width() -> usize {
197        if let Ok((w, _h)) = terminal_size() {
198            w as usize
199        } else {
200            80
201        }
202    }
203
204    if !is_parser_keyword && !sig.input_output_types.is_empty() {
205        if let Some(decl_id) = engine_state.find_decl(b"table", &[]) {
206            // FIXME: we may want to make this the span of the help command in the future
207            let span = Span::unknown();
208            let mut vals = vec![];
209            for (input, output) in &sig.input_output_types {
210                vals.push(Value::record(
211                    record! {
212                        "input" => Value::string(input.to_string(), span),
213                        "output" => Value::string(output.to_string(), span),
214                    },
215                    span,
216                ));
217            }
218
219            let caller_stack = &mut Stack::new().collect_value();
220            if let Ok(result) = eval_call::<WithoutDebug>(
221                engine_state,
222                caller_stack,
223                &Call {
224                    decl_id,
225                    head: span,
226                    arguments: vec![Argument::Named((
227                        Spanned {
228                            item: "width".to_string(),
229                            span: Span::unknown(),
230                        },
231                        None,
232                        Some(Expression::new_unknown(
233                            Expr::Int(get_term_width() as i64 - 2), // padding, see below
234                            Span::unknown(),
235                            Type::Int,
236                        )),
237                    ))],
238                    parser_info: HashMap::new(),
239                },
240                PipelineData::Value(Value::list(vals, span), None),
241            ) {
242                if let Ok((str, ..)) = result.collect_string_strict(span) {
243                    let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
244                    for line in str.lines() {
245                        let _ = writeln!(long_desc, "  {line}");
246                    }
247                }
248            }
249        }
250    }
251
252    if !examples.is_empty() {
253        let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
254    }
255
256    for example in examples {
257        long_desc.push('\n');
258        long_desc.push_str("  ");
259        long_desc.push_str(example.description);
260
261        if !nu_config.use_ansi_coloring.get(engine_state) {
262            let _ = write!(long_desc, "\n  > {}\n", example.example);
263        } else {
264            let code_string = nu_highlight_string(example.example, engine_state, stack);
265            let _ = write!(long_desc, "\n  > {code_string}\n");
266        };
267
268        if let Some(result) = &example.result {
269            let mut table_call = Call::new(Span::unknown());
270            if example.example.ends_with("--collapse") {
271                // collapse the result
272                table_call.add_named((
273                    Spanned {
274                        item: "collapse".to_string(),
275                        span: Span::unknown(),
276                    },
277                    None,
278                    None,
279                ))
280            } else {
281                // expand the result
282                table_call.add_named((
283                    Spanned {
284                        item: "expand".to_string(),
285                        span: Span::unknown(),
286                    },
287                    None,
288                    None,
289                ))
290            }
291            table_call.add_named((
292                Spanned {
293                    item: "width".to_string(),
294                    span: Span::unknown(),
295                },
296                None,
297                Some(Expression::new_unknown(
298                    Expr::Int(get_term_width() as i64 - 2),
299                    Span::unknown(),
300                    Type::Int,
301                )),
302            ));
303
304            let table = engine_state
305                .find_decl("table".as_bytes(), &[])
306                .and_then(|decl_id| {
307                    engine_state
308                        .get_decl(decl_id)
309                        .run(
310                            engine_state,
311                            stack,
312                            &(&table_call).into(),
313                            PipelineData::Value(result.clone(), None),
314                        )
315                        .ok()
316                });
317
318            for item in table.into_iter().flatten() {
319                let _ = writeln!(
320                    long_desc,
321                    "  {}",
322                    item.to_expanded_string("", &nu_config)
323                        .replace('\n', "\n  ")
324                        .trim()
325                );
326            }
327        }
328    }
329
330    long_desc.push('\n');
331
332    if !nu_config.use_ansi_coloring.get(engine_state) {
333        nu_utils::strip_ansi_string_likely(long_desc)
334    } else {
335        long_desc
336    }
337}
338
339fn update_ansi_from_config(
340    ansi_code: &mut String,
341    engine_state: &EngineState,
342    nu_config: &Config,
343    theme_component: &str,
344) {
345    if let Some(color) = &nu_config.color_config.get(theme_component) {
346        let caller_stack = &mut Stack::new().collect_value();
347        let span = Span::unknown();
348        let span_id = UNKNOWN_SPAN_ID;
349
350        let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
351
352        // Call ansi command using argument
353        if let Some(argument) = argument_opt {
354            if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
355                if let Ok(result) = eval_call::<WithoutDebug>(
356                    engine_state,
357                    caller_stack,
358                    &Call {
359                        decl_id,
360                        head: span,
361                        arguments: vec![argument],
362                        parser_info: HashMap::new(),
363                    },
364                    PipelineData::Empty,
365                ) {
366                    if let Ok((str, ..)) = result.collect_string_strict(span) {
367                        *ansi_code = str;
368                    }
369                }
370            }
371        }
372    }
373}
374
375fn get_argument_for_color_value(
376    nu_config: &Config,
377    color: &Value,
378    span: Span,
379    span_id: SpanId,
380) -> Option<Argument> {
381    match color {
382        Value::Record { val, .. } => {
383            let record_exp: Vec<RecordItem> = (**val)
384                .iter()
385                .map(|(k, v)| {
386                    RecordItem::Pair(
387                        Expression::new_existing(
388                            Expr::String(k.clone()),
389                            span,
390                            span_id,
391                            Type::String,
392                        ),
393                        Expression::new_existing(
394                            Expr::String(v.clone().to_expanded_string("", nu_config)),
395                            span,
396                            span_id,
397                            Type::String,
398                        ),
399                    )
400                })
401                .collect();
402
403            Some(Argument::Positional(Expression::new_existing(
404                Expr::Record(record_exp),
405                Span::unknown(),
406                UNKNOWN_SPAN_ID,
407                Type::Record(
408                    [
409                        ("fg".to_string(), Type::String),
410                        ("attr".to_string(), Type::String),
411                    ]
412                    .into(),
413                ),
414            )))
415        }
416        Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
417            Expr::String(val.clone()),
418            Span::unknown(),
419            UNKNOWN_SPAN_ID,
420            Type::String,
421        ))),
422        _ => None,
423    }
424}
425
426/// Contains the settings for ANSI colors in help output
427///
428/// By default contains a fixed set of (4-bit) colors
429///
430/// Can reflect configuration using [`HelpStyle::update_from_config`]
431pub struct HelpStyle {
432    section_name: String,
433    subcolor_one: String,
434    subcolor_two: String,
435}
436
437impl Default for HelpStyle {
438    fn default() -> Self {
439        HelpStyle {
440            // default: green
441            section_name: "\x1b[32m".to_string(),
442            // default: cyan
443            subcolor_one: "\x1b[36m".to_string(),
444            // default: light blue
445            subcolor_two: "\x1b[94m".to_string(),
446        }
447    }
448}
449
450impl HelpStyle {
451    /// Pull colors from the [`Config`]
452    ///
453    /// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme.
454    ///
455    /// Implementation detail: currently executes `ansi` command internally thus requiring the
456    /// [`EngineState`] for execution.
457    /// See <https://github.com/nushell/nushell/pull/10623> for details
458    pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
459        update_ansi_from_config(
460            &mut self.section_name,
461            engine_state,
462            nu_config,
463            "shape_string",
464        );
465        update_ansi_from_config(
466            &mut self.subcolor_one,
467            engine_state,
468            nu_config,
469            "shape_external",
470        );
471        update_ansi_from_config(
472            &mut self.subcolor_two,
473            engine_state,
474            nu_config,
475            "shape_block",
476        );
477    }
478}
479
480/// Make syntax shape presentable by stripping custom completer info
481fn document_shape(shape: &SyntaxShape) -> &SyntaxShape {
482    match shape {
483        SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape,
484        _ => shape,
485    }
486}
487
488#[derive(PartialEq)]
489enum PositionalKind {
490    Required,
491    Optional,
492    Rest,
493}
494
495fn write_positional(
496    long_desc: &mut String,
497    positional: &PositionalArg,
498    arg_kind: PositionalKind,
499    help_style: &HelpStyle,
500    nu_config: &Config,
501    engine_state: &EngineState,
502    stack: &mut Stack,
503) {
504    let help_subcolor_one = &help_style.subcolor_one;
505    let help_subcolor_two = &help_style.subcolor_two;
506
507    // Indentation
508    long_desc.push_str("  ");
509    if arg_kind == PositionalKind::Rest {
510        long_desc.push_str("...");
511    }
512    match &positional.shape {
513        SyntaxShape::Keyword(kw, shape) => {
514            let _ = write!(
515                long_desc,
516                "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
517                String::from_utf8_lossy(kw),
518                document_shape(shape),
519            );
520        }
521        _ => {
522            let _ = write!(
523                long_desc,
524                "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
525                positional.name,
526                document_shape(&positional.shape),
527            );
528        }
529    };
530    if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
531        let _ = write!(long_desc, ": {}", positional.desc);
532    }
533    if arg_kind == PositionalKind::Optional {
534        if let Some(value) = &positional.default_value {
535            let _ = write!(
536                long_desc,
537                " (optional, default: {})",
538                nu_highlight_string(
539                    &value.to_parsable_string(", ", nu_config),
540                    engine_state,
541                    stack
542                )
543            );
544        } else {
545            long_desc.push_str(" (optional)");
546        };
547    }
548    long_desc.push('\n');
549}
550
551pub fn get_flags_section<F>(
552    signature: &Signature,
553    help_style: &HelpStyle,
554    mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight)
555) -> String
556where
557    F: FnMut(&nu_protocol::Value) -> String,
558{
559    let help_section_name = &help_style.section_name;
560    let help_subcolor_one = &help_style.subcolor_one;
561    let help_subcolor_two = &help_style.subcolor_two;
562
563    let mut long_desc = String::new();
564    let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
565    for flag in &signature.named {
566        // Indentation
567        long_desc.push_str("  ");
568        // Short flag shown before long flag
569        if let Some(short) = flag.short {
570            let _ = write!(long_desc, "{help_subcolor_one}-{}{RESET}", short);
571            if !flag.long.is_empty() {
572                let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
573            }
574        }
575        if !flag.long.is_empty() {
576            let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
577        }
578        if flag.required {
579            long_desc.push_str(" (required parameter)")
580        }
581        // Type/Syntax shape info
582        if let Some(arg) = &flag.arg {
583            let _ = write!(
584                long_desc,
585                " <{help_subcolor_two}{}{RESET}>",
586                document_shape(arg)
587            );
588        }
589        if !flag.desc.is_empty() {
590            let _ = write!(long_desc, ": {}", flag.desc);
591        }
592        if let Some(value) = &flag.default_value {
593            let _ = write!(long_desc, " (default: {})", &value_formatter(value));
594        }
595        long_desc.push('\n');
596    }
597    long_desc
598}