nu_engine/
documentation.rs

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