Skip to main content

nu_parser/
parse_calls.rs

1use crate::{
2    lite_parser::LiteCommand,
3    parse_helpers::{PERCENT_FORCED_BUILTIN_PARSER_INFO, extract_spread_list, garbage},
4    parse_source::find_dirs_var,
5    type_check::type_compatible,
6};
7use log::trace;
8use nu_engine::DIR_VAR_PARSER_INFO;
9use nu_protocol::{
10    DeclId, Flag, IntoSpanned, ParseError, PositionalArg, ShellError, Signature, Span, Spanned,
11    SyntaxShape, Type, TypeSet,
12    ast::*,
13    did_you_mean,
14    engine::{CommandType, StateWorkingSet},
15};
16use std::str;
17
18/// Return type of `check_call`
19#[derive(Debug, PartialEq, Eq)]
20pub(crate) enum CallKind {
21    Help,
22    Valid,
23    Invalid,
24}
25
26pub(crate) fn check_call(
27    working_set: &mut StateWorkingSet,
28    command: Span,
29    sig: &Signature,
30    call: &Call,
31) -> CallKind {
32    // Allow the call to pass if they pass in the help flag
33    if call.named_iter().any(|(n, _, _)| n.item == "help") {
34        return CallKind::Help;
35    }
36
37    if call.positional_iter().count() < sig.required_positional.len() {
38        let end_offset = call
39            .positional_iter()
40            .last()
41            .map(|last| last.span.end)
42            .unwrap_or(command.end);
43        // Comparing the types of all signature positional arguments against the parsed
44        // expressions found in the call. If one type is not found then it could be assumed
45        // that positional argument is missing from the parsed call
46        for argument in &sig.required_positional {
47            let found = call.positional_iter().fold(false, |ac, expr| {
48                if argument.shape.to_type() == expr.ty || argument.shape == SyntaxShape::Any {
49                    true
50                } else {
51                    ac
52                }
53            });
54            if !found {
55                working_set.error(ParseError::MissingPositional(
56                    argument.name.clone(),
57                    Span::new(end_offset, end_offset),
58                    sig.call_signature(),
59                ));
60                return CallKind::Invalid;
61            }
62        }
63
64        let missing = &sig.required_positional[call.positional_iter().count()];
65        working_set.error(ParseError::MissingPositional(
66            missing.name.clone(),
67            Span::new(end_offset, end_offset),
68            sig.call_signature(),
69        ));
70        return CallKind::Invalid;
71    } else {
72        for req_flag in sig.named.iter().filter(|x| x.required) {
73            if call.named_iter().all(|(n, _, _)| n.item != req_flag.long) {
74                working_set.error(ParseError::MissingRequiredFlag(
75                    req_flag.long.clone(),
76                    command,
77                ));
78                return CallKind::Invalid;
79            }
80        }
81    }
82    CallKind::Valid
83}
84
85fn parse_unknown_arg(
86    working_set: &mut StateWorkingSet,
87    span: Span,
88    signature: &Signature,
89) -> Expression {
90    let shape = signature
91        .rest_positional
92        .as_ref()
93        .map(|arg| arg.shape.clone())
94        .unwrap_or(SyntaxShape::Any);
95
96    crate::parser::parse_value(working_set, span, &shape, None)
97}
98
99fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
100    let contents = working_set.get_span_contents(span);
101
102    if contents.starts_with(b"r#") {
103        crate::parser::parse_raw_string(working_set, span)
104    } else if contents
105        .iter()
106        .any(|b| matches!(b, b'"' | b'\'' | b'(' | b')' | b'`'))
107    {
108        enum State {
109            Bare {
110                from: usize,
111            },
112            BackTickQuote {
113                from: usize,
114            },
115            Quote {
116                from: usize,
117                quote_char: u8,
118                escaped: bool,
119            },
120            Parenthesized {
121                from: usize,
122                depth: usize,
123            },
124        }
125        // Find the spans of parts of the string that can be parsed as their own strings for
126        // concatenation.
127        //
128        // By passing each of these parts to `parse_string()`, we can eliminate the quotes and also
129        // handle string interpolation.
130        let make_span = |from: usize, index: usize| Span {
131            start: span.start + from,
132            end: span.start + index,
133        };
134        let mut spans = vec![];
135        let mut state = State::Bare { from: 0 };
136        let mut index = 0;
137        while index < contents.len() {
138            let ch = contents[index];
139            match &mut state {
140                State::Bare { from } => match ch {
141                    b'"' | b'\'' => {
142                        // Push bare string
143                        if index != *from {
144                            spans.push(make_span(*from, index));
145                        }
146                        // then transition to other state
147                        state = State::Quote {
148                            from: index,
149                            quote_char: ch,
150                            escaped: false,
151                        };
152                    }
153                    b'$' => {
154                        if let Some(&quote_char @ (b'"' | b'\'')) = contents.get(index + 1) {
155                            // Start a dollar quote (interpolated string)
156                            if index != *from {
157                                spans.push(make_span(*from, index));
158                            }
159                            state = State::Quote {
160                                from: index,
161                                quote_char,
162                                escaped: false,
163                            };
164                            // Skip over two chars (the dollar sign and the quote)
165                            index += 2;
166                            continue;
167                        }
168                    }
169                    b'`' => {
170                        if index != *from {
171                            spans.push(make_span(*from, index))
172                        }
173                        state = State::BackTickQuote { from: index }
174                    }
175                    b'(' => {
176                        if index != *from {
177                            spans.push(make_span(*from, index))
178                        }
179                        state = State::Parenthesized {
180                            from: index,
181                            depth: 1,
182                        }
183                    }
184                    // Continue to consume
185                    _ => (),
186                },
187                State::Quote {
188                    from,
189                    quote_char,
190                    escaped,
191                } => match ch {
192                    ch if ch == *quote_char && !*escaped => {
193                        // quoted string ended, just make a new span for it.
194                        spans.push(make_span(*from, index + 1));
195                        // go back to Bare state.
196                        state = State::Bare { from: index + 1 };
197                    }
198                    b'\\' if !*escaped && *quote_char == b'"' => {
199                        // The next token is escaped so it doesn't count (only for double quote)
200                        *escaped = true;
201                    }
202                    _ => {
203                        *escaped = false;
204                    }
205                },
206                State::BackTickQuote { from } => {
207                    if ch == b'`' {
208                        spans.push(make_span(*from, index + 1));
209                        state = State::Bare { from: index + 1 };
210                    }
211                }
212                State::Parenthesized { from, depth } => {
213                    if ch == b')' {
214                        if *depth == 1 {
215                            spans.push(make_span(*from, index + 1));
216                            state = State::Bare { from: index + 1 };
217                        } else {
218                            *depth -= 1;
219                        }
220                    } else if ch == b'(' {
221                        *depth += 1;
222                    }
223                }
224            }
225            index += 1;
226        }
227
228        // Add the final span
229        match state {
230            State::Bare { from }
231            | State::Quote { from, .. }
232            | State::Parenthesized { from, .. }
233            | State::BackTickQuote { from, .. } => {
234                if from < contents.len() {
235                    spans.push(make_span(from, contents.len()));
236                }
237            }
238        }
239
240        // Log the spans that will be parsed
241        if log::log_enabled!(log::Level::Trace) {
242            let contents = spans
243                .iter()
244                .map(|span| String::from_utf8_lossy(working_set.get_span_contents(*span)))
245                .collect::<Vec<_>>();
246
247            trace!("parsing: external string, parts: {contents:?}")
248        }
249
250        // Check if the whole thing is quoted. If not, it should be a glob
251        let quoted =
252            (contents.len() >= 3 && contents.starts_with(b"$\"") && contents.ends_with(b"\""))
253                || is_quoted(contents);
254
255        // Parse each as its own string
256        let exprs: Vec<Expression> = spans
257            .into_iter()
258            .map(|span| crate::parser::parse_string(working_set, span))
259            .collect();
260
261        if exprs
262            .iter()
263            .all(|expr| matches!(expr.expr, Expr::String(..)))
264        {
265            // If the exprs are all strings anyway, just collapse into a single string.
266            let string = exprs
267                .into_iter()
268                .map(|expr| {
269                    let Expr::String(contents) = expr.expr else {
270                        unreachable!("already checked that this was a String")
271                    };
272                    contents
273                })
274                .collect::<String>();
275            if quoted {
276                Expression::new(working_set, Expr::String(string), span, Type::String)
277            } else {
278                Expression::new(
279                    working_set,
280                    Expr::GlobPattern(string, false),
281                    span,
282                    Type::Glob,
283                )
284            }
285        } else {
286            // Flatten any string interpolations contained with the exprs.
287            let exprs = exprs
288                .into_iter()
289                .flat_map(|expr| match expr.expr {
290                    Expr::StringInterpolation(subexprs) => subexprs,
291                    _ => vec![expr],
292                })
293                .collect();
294            // Make an interpolation out of the expressions. Use `GlobInterpolation` if it's a bare
295            // word, so that the unquoted state can get passed through to `run-external`.
296            if quoted {
297                Expression::new(
298                    working_set,
299                    Expr::StringInterpolation(exprs),
300                    span,
301                    Type::String,
302                )
303            } else {
304                Expression::new(
305                    working_set,
306                    Expr::GlobInterpolation(exprs, false),
307                    span,
308                    Type::Glob,
309                )
310            }
311        }
312    } else {
313        crate::parser::parse_glob_pattern(working_set, span)
314    }
315}
316
317fn is_quoted(bytes: &[u8]) -> bool {
318    matches!(bytes, [b'\'', .., b'\''] | [b'"', .., b'"'])
319}
320
321fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument {
322    let contents = working_set.get_span_contents(span);
323
324    if let Some(Spanned { item: _, span }) = extract_spread_list(contents.into_spanned(span)) {
325        ExternalArgument::Spread(crate::parser::parse_value(
326            working_set,
327            span,
328            &SyntaxShape::List(Box::new(SyntaxShape::Any)),
329            None,
330        ))
331    } else {
332        ExternalArgument::Regular(parse_regular_external_arg(working_set, span))
333    }
334}
335
336pub(crate) fn parse_regular_external_arg(
337    working_set: &mut StateWorkingSet,
338    span: Span,
339) -> Expression {
340    match working_set.get_span_contents(span) {
341        [b'$', ..] => crate::parser::parse_dollar_expr(working_set, span, &SyntaxShape::Any, None),
342        [b'(', ..] => crate::parser::parse_paren_expr(working_set, span, &SyntaxShape::Any),
343        [b'[', ..] => crate::parser::parse_list_expression(working_set, span, &SyntaxShape::Any),
344        _ => parse_external_string(working_set, span),
345    }
346}
347
348pub fn parse_external_call(
349    working_set: &mut StateWorkingSet,
350    spans: &[Span],
351    call_span: Span,
352) -> Expression {
353    trace!("parse external");
354
355    let head_span = spans[0];
356
357    let head_contents = working_set.get_span_contents(head_span);
358
359    let head = if let [b'$' | b'(', ..] = head_contents {
360        // the expression is inside external_call, so it's a subexpression
361        let arg = crate::parser::parse_expression(working_set, &[head_span], None);
362        Box::new(arg)
363    } else {
364        Box::new(parse_external_string(working_set, head_span))
365    };
366
367    let args = spans[1..]
368        .iter()
369        .map(|&span| parse_external_arg(working_set, span))
370        .collect();
371
372    Expression::new(
373        working_set,
374        Expr::ExternalCall(head, args),
375        call_span,
376        Type::Any,
377    )
378}
379
380fn ensure_flag_arg_type(
381    working_set: &mut StateWorkingSet,
382    arg_name: String,
383    arg: Expression,
384    arg_shape: &SyntaxShape,
385    long_name_span: Span,
386) -> (Spanned<String>, Expression) {
387    if !type_compatible(&arg_shape.to_type(), &arg.ty) {
388        working_set.error(ParseError::TypeMismatch(
389            arg_shape.to_type(),
390            arg.ty,
391            arg.span,
392        ));
393        (
394            Spanned {
395                item: arg_name,
396                span: long_name_span,
397            },
398            Expression::garbage(working_set, arg.span),
399        )
400    } else {
401        (
402            Spanned {
403                item: arg_name,
404                span: long_name_span,
405            },
406            arg,
407        )
408    }
409}
410
411/// Result of attempting to parse a long flag.
412///
413/// This tri-state enum indicates whether a long flag was found, no flag was found,
414/// or the end-of-options delimiter `--` was found (which stops all flag parsing).
415enum LongFlagParseResult {
416    /// A long flag was successfully parsed: (flag_name, value_expression)
417    FoundFlag(Spanned<String>, Option<Expression>),
418    /// No long flag found at this position
419    NoFlag,
420    /// End-of-options delimiter `--` found; stop flag parsing
421    EndOfOptions,
422}
423
424fn parse_long_flag(
425    working_set: &mut StateWorkingSet,
426    spans: &[Span],
427    spans_idx: &mut usize,
428    sig: &Signature,
429) -> LongFlagParseResult {
430    let arg_span = spans[*spans_idx];
431    let arg_contents = working_set.get_span_contents(arg_span);
432
433    if arg_contents.starts_with(b"--") {
434        // Check for end-of-options delimiter: exactly "--"
435        if arg_contents == b"--" {
436            return LongFlagParseResult::EndOfOptions;
437        }
438
439        // FIXME: only use the first flag you find?
440        let split: Vec<_> = arg_contents.split(|x| *x == b'=').collect();
441        let long_name = String::from_utf8(split[0].into());
442        if let Ok(long_name) = long_name {
443            let long_name = long_name[2..].to_string();
444            if let Some(flag) = sig.get_long_flag(&long_name) {
445                if let Some(arg_shape) = &flag.arg {
446                    if split.len() > 1 {
447                        // and we also have the argument
448                        let long_name_len = long_name.len();
449                        let mut span = arg_span;
450                        span.start += long_name_len + 3; //offset by long flag and '='
451
452                        let arg = crate::parser::parse_value(working_set, span, arg_shape, None);
453                        let (arg_name, val_expression) = ensure_flag_arg_type(
454                            working_set,
455                            long_name,
456                            arg,
457                            arg_shape,
458                            Span::new(arg_span.start, arg_span.start + long_name_len + 2),
459                        );
460                        LongFlagParseResult::FoundFlag(arg_name, Some(val_expression))
461                    } else if let Some(arg) = spans.get(*spans_idx + 1) {
462                        let arg = crate::parser::parse_value(working_set, *arg, arg_shape, None);
463
464                        *spans_idx += 1;
465                        let (arg_name, val_expression) =
466                            ensure_flag_arg_type(working_set, long_name, arg, arg_shape, arg_span);
467                        LongFlagParseResult::FoundFlag(arg_name, Some(val_expression))
468                    } else {
469                        working_set.error(ParseError::MissingFlagParam(
470                            arg_shape.to_string(),
471                            arg_span,
472                        ));
473                        // NOTE: still need to cover this incomplete flag in the final expression
474                        // see https://github.com/nushell/nushell/issues/16375
475                        LongFlagParseResult::FoundFlag(
476                            Spanned {
477                                item: long_name,
478                                span: arg_span,
479                            },
480                            None,
481                        )
482                    }
483                } else {
484                    // A flag with no argument
485                    // It can also takes a boolean value like --x=true
486                    if split.len() > 1 {
487                        // and we also have the argument
488                        let long_name_len = long_name.len();
489                        let mut span = arg_span;
490                        span.start += long_name_len + 3; //offset by long flag and '='
491
492                        let arg = crate::parser::parse_value(
493                            working_set,
494                            span,
495                            &SyntaxShape::Boolean,
496                            None,
497                        );
498
499                        let (arg_name, val_expression) = ensure_flag_arg_type(
500                            working_set,
501                            long_name,
502                            arg,
503                            &SyntaxShape::Boolean,
504                            Span::new(arg_span.start, arg_span.start + long_name_len + 2),
505                        );
506                        LongFlagParseResult::FoundFlag(arg_name, Some(val_expression))
507                    } else {
508                        LongFlagParseResult::FoundFlag(
509                            Spanned {
510                                item: long_name,
511                                span: arg_span,
512                            },
513                            None,
514                        )
515                    }
516                }
517            } else {
518                let suggestion = did_you_mean(sig.get_names(), &long_name)
519                    .map(|name| format!("Did you mean: `--{name}`?"))
520                    .unwrap_or("Use `--help` to see available flags".to_owned());
521                working_set.error(ParseError::UnknownFlag(
522                    sig.name.clone(),
523                    long_name.clone(),
524                    arg_span,
525                    suggestion,
526                ));
527                LongFlagParseResult::FoundFlag(
528                    Spanned {
529                        item: long_name.clone(),
530                        span: arg_span,
531                    },
532                    None,
533                )
534            }
535        } else {
536            working_set.error(ParseError::NonUtf8(arg_span));
537            LongFlagParseResult::FoundFlag(
538                Spanned {
539                    item: "--".into(),
540                    span: arg_span,
541                },
542                None,
543            )
544        }
545    } else {
546        LongFlagParseResult::NoFlag
547    }
548}
549
550fn parse_short_flags(
551    working_set: &mut StateWorkingSet,
552    spans: &[Span],
553    spans_idx: &mut usize,
554    positional_idx: usize,
555    sig: &Signature,
556) -> Option<Vec<Flag>> {
557    let arg_span = spans[*spans_idx];
558
559    let arg_contents = working_set.get_span_contents(arg_span);
560
561    if let Ok(arg_contents_uft8_ref) = str::from_utf8(arg_contents) {
562        if arg_contents_uft8_ref.starts_with('-') && arg_contents_uft8_ref.len() > 1 {
563            let short_flags = &arg_contents_uft8_ref[1..];
564            let num_chars = short_flags.chars().count();
565            let mut found_short_flags = vec![];
566            let mut unmatched_short_flags = vec![];
567            for (offset, short_flag) in short_flags.char_indices() {
568                let short_flag_span = Span::new(
569                    arg_span.start + 1 + offset,
570                    arg_span.start + 1 + offset + short_flag.len_utf8(),
571                );
572                if let Some(flag) = sig.get_short_flag(short_flag) {
573                    // Allow args in short flag batches as long as it is the last flag.
574                    if flag.arg.is_some() && offset < num_chars - 1 {
575                        working_set
576                            .error(ParseError::OnlyLastFlagInBatchCanTakeArg(short_flag_span));
577                        break;
578                    }
579                    found_short_flags.push(flag);
580                } else {
581                    unmatched_short_flags.push(short_flag_span);
582                }
583            }
584
585            if found_short_flags.is_empty()
586                // check to see if we have a negative number
587                && matches!(
588                    sig.get_positional(positional_idx),
589                    Some(PositionalArg {
590                        shape: SyntaxShape::Int | SyntaxShape::Number | SyntaxShape::Float,
591                        ..
592                    })
593                )
594                && String::from_utf8_lossy(working_set.get_span_contents(arg_span))
595                    .parse::<f64>()
596                    .is_ok()
597            {
598                return None;
599            } else if let Some(first) = unmatched_short_flags.first() {
600                let contents = working_set.get_span_contents(*first);
601                working_set.error(ParseError::UnknownFlag(
602                    sig.name.clone(),
603                    format!("-{}", String::from_utf8_lossy(contents)),
604                    *first,
605                    "Use `--help` to see available flags".to_owned(),
606                ));
607            }
608
609            Some(found_short_flags)
610        } else {
611            None
612        }
613    } else {
614        working_set.error(ParseError::NonUtf8(arg_span));
615        None
616    }
617}
618
619fn first_kw_idx(
620    working_set: &StateWorkingSet,
621    signature: &Signature,
622    spans: &[Span],
623    spans_idx: usize,
624    positional_idx: usize,
625) -> (Option<usize>, usize) {
626    for idx in (positional_idx + 1)..signature.num_positionals() {
627        if let Some(PositionalArg {
628            shape: SyntaxShape::Keyword(kw, ..),
629            ..
630        }) = signature.get_positional(idx)
631        {
632            for (span_idx, &span) in spans.iter().enumerate().skip(spans_idx) {
633                let contents = working_set.get_span_contents(span);
634
635                if contents == kw {
636                    return (Some(idx), span_idx);
637                }
638            }
639        }
640    }
641    (None, spans.len())
642}
643
644fn calculate_end_span(
645    working_set: &StateWorkingSet,
646    signature: &Signature,
647    spans: &[Span],
648    spans_idx: usize,
649    positional_idx: usize,
650) -> usize {
651    if signature.rest_positional.is_some() {
652        spans.len()
653    } else {
654        let (kw_pos, kw_idx) =
655            first_kw_idx(working_set, signature, spans, spans_idx, positional_idx);
656
657        if let Some(kw_pos) = kw_pos {
658            // We found a keyword. Keywords, once found, create a guidepost to
659            // show us where the positionals will lay into the arguments. Because they're
660            // keywords, they get to set this by being present
661
662            let positionals_between = kw_pos - positional_idx - 1;
663            if positionals_between >= (kw_idx - spans_idx) {
664                kw_idx
665            } else {
666                kw_idx - positionals_between
667            }
668        } else {
669            // Make space for the remaining require positionals, if we can
670            // spans_idx < spans.len() is an invariant
671            let remaining_spans = spans.len() - (spans_idx + 1);
672            // positional_idx can be larger than required_positional.len() if we have optional args
673            let remaining_positional = signature
674                .required_positional
675                .len()
676                .saturating_sub(positional_idx + 1);
677            // Saturates to 0 when we have too few args
678            let extra_spans = remaining_spans.saturating_sub(remaining_positional);
679            spans_idx + 1 + extra_spans
680        }
681    }
682}
683
684pub(crate) fn parse_oneof(
685    working_set: &mut StateWorkingSet,
686    spans: &[Span],
687    spans_idx: &mut usize,
688    possible_shapes: &Vec<SyntaxShape>,
689    multispan: bool,
690    input_type: Option<&Type>,
691) -> Expression {
692    let starting_spans_idx = *spans_idx;
693    let mut best_guess = None;
694    let mut best_guess_errors = Vec::new();
695    let mut max_first_error_offset = 0;
696    let mut propagate_error = false;
697    for shape in possible_shapes {
698        let starting_error_count = working_set.parse_errors.len();
699        *spans_idx = starting_spans_idx;
700        let value = match multispan {
701            true => parse_multispan_value(working_set, spans, spans_idx, shape, input_type),
702            false => crate::parser::parse_value(working_set, spans[*spans_idx], shape, input_type),
703        };
704
705        let new_errors = &working_set.parse_errors[starting_error_count..];
706        // no new errors found means success
707        let Some(first_error_offset) = new_errors.iter().map(|e| e.span().start).min() else {
708            return value;
709        };
710
711        if first_error_offset > max_first_error_offset {
712            // while trying the possible shapes, ignore Expected type errors
713            // unless they're inside a block, closure, or expression
714            propagate_error = match working_set.parse_errors.last() {
715                Some(ParseError::Expected(_, error_span))
716                | Some(ParseError::ExpectedWithStringMsg(_, error_span)) => {
717                    matches!(
718                        shape,
719                        SyntaxShape::Block | SyntaxShape::Closure(_) | SyntaxShape::Expression
720                    ) && *error_span != spans[*spans_idx]
721                }
722                _ => true,
723            };
724            max_first_error_offset = first_error_offset;
725            best_guess = Some(value);
726            best_guess_errors.clear();
727            best_guess_errors.extend_from_slice(new_errors);
728        }
729        working_set.parse_errors.truncate(starting_error_count);
730    }
731
732    // if best_guess results in new errors further than current span, then accept it
733    // or propagate_error is marked as true for it
734    if max_first_error_offset > spans[starting_spans_idx].start || propagate_error {
735        working_set.parse_errors.extend(best_guess_errors);
736        best_guess.expect("best_guess should not be None here!")
737    } else {
738        working_set.error(ParseError::ExpectedWithStringMsg(
739            format!("one of a list of accepted shapes: {possible_shapes:?}"),
740            spans[starting_spans_idx],
741        ));
742        Expression::garbage(working_set, spans[starting_spans_idx])
743    }
744}
745
746pub fn parse_multispan_value(
747    working_set: &mut StateWorkingSet,
748    spans: &[Span],
749    spans_idx: &mut usize,
750    shape: &SyntaxShape,
751    input_type: Option<&Type>,
752) -> Expression {
753    trace!("parse multispan value");
754    match shape {
755        SyntaxShape::VarWithOptType => {
756            trace!("parsing: var with opt type");
757
758            crate::parser::parse_var_with_opt_type(working_set, spans, spans_idx, false, input_type)
759                .0
760        }
761        SyntaxShape::RowCondition => {
762            trace!("parsing: row condition");
763            let arg = crate::parser::parse_row_condition(working_set, &spans[*spans_idx..]);
764            *spans_idx = spans.len() - 1;
765
766            arg
767        }
768        SyntaxShape::MathExpression => {
769            trace!("parsing: math expression");
770
771            let arg = crate::parser::parse_math_expression(
772                working_set,
773                &spans[*spans_idx..],
774                None,
775                input_type,
776            );
777            *spans_idx = spans.len() - 1;
778
779            arg
780        }
781        SyntaxShape::OneOf(possible_shapes) => parse_oneof(
782            working_set,
783            spans,
784            spans_idx,
785            possible_shapes,
786            true,
787            input_type,
788        ),
789
790        SyntaxShape::Expression => {
791            trace!("parsing: expression");
792
793            // is it subexpression?
794            // Not sure, but let's make it not, so the behavior is the same as previous version of nushell.
795            let arg =
796                crate::parser::parse_expression(working_set, &spans[*spans_idx..], input_type);
797            *spans_idx = spans.len().saturating_sub(1);
798
799            arg
800        }
801        SyntaxShape::Signature => {
802            trace!("parsing: signature");
803
804            let sig = crate::parser::parse_full_signature(working_set, &spans[*spans_idx..], false);
805            *spans_idx = spans.len().saturating_sub(1);
806
807            sig
808        }
809        SyntaxShape::ExternalSignature => {
810            trace!("parsing: external signature");
811
812            let sig = crate::parser::parse_full_signature(working_set, &spans[*spans_idx..], true);
813            *spans_idx = spans.len().saturating_sub(1);
814
815            sig
816        }
817        SyntaxShape::Keyword(keyword, arg) => {
818            trace!(
819                "parsing: keyword({}) {:?}",
820                String::from_utf8_lossy(keyword),
821                arg
822            );
823            let arg_span = spans[*spans_idx];
824
825            let arg_contents = working_set.get_span_contents(arg_span);
826
827            if arg_contents != keyword {
828                // When keywords mismatch, this is a strong indicator of something going wrong.
829                // We won't often override the current error, but as this is a strong indicator
830                // go ahead and override the current error and tell the user about the missing
831                // keyword/literal.
832                working_set.error(ParseError::ExpectedKeyword(
833                    String::from_utf8_lossy(keyword).into(),
834                    arg_span,
835                ))
836            }
837
838            *spans_idx += 1;
839            if *spans_idx >= spans.len() {
840                working_set.error(ParseError::KeywordMissingArgument(
841                    arg.to_string(),
842                    String::from_utf8_lossy(keyword).into(),
843                    Span::new(spans[*spans_idx - 1].end, spans[*spans_idx - 1].end),
844                ));
845                let keyword = Keyword {
846                    keyword: keyword.as_slice().into(),
847                    span: spans[*spans_idx - 1],
848                    expr: Expression::garbage(working_set, arg_span),
849                };
850                return Expression::new(
851                    working_set,
852                    Expr::Keyword(Box::new(keyword)),
853                    arg_span,
854                    Type::Any,
855                );
856            }
857
858            let keyword = Keyword {
859                keyword: keyword.as_slice().into(),
860                span: spans[*spans_idx - 1],
861                expr: parse_multispan_value(working_set, spans, spans_idx, arg, input_type),
862            };
863
864            Expression::new(
865                working_set,
866                Expr::Keyword(Box::new(keyword.clone())),
867                keyword.span.merge(keyword.expr.span),
868                keyword.expr.ty,
869            )
870        }
871        _ => {
872            // All other cases are single-span values
873            let arg_span = spans[*spans_idx];
874
875            crate::parser::parse_value(working_set, arg_span, shape, input_type)
876        }
877    }
878}
879
880pub struct ParsedInternalCall {
881    pub call: Box<Call>,
882    pub output: Type,
883    pub call_kind: CallKind,
884}
885
886/// Sometimes the arguments of an internal command need to be parsed in dedicated functions, e.g. `parse_module`.
887/// If so, `parse_internal_call` should be called with the appropriate parsing level to avoid repetition.
888///
889/// Defaults to `ArgumentParsingLevel::Full`
890#[derive(Default)]
891pub enum ArgumentParsingLevel {
892    #[default]
893    Full,
894    /// Parse only the first `k` arguments
895    FirstK { k: usize },
896}
897
898pub fn parse_internal_call(
899    working_set: &mut StateWorkingSet,
900    command_span: Span,
901    spans: &[Span],
902    decl_id: DeclId,
903    arg_parsing_level: ArgumentParsingLevel,
904    input_type: Option<&Type>,
905) -> ParsedInternalCall {
906    trace!("parsing: internal call (decl id: {})", decl_id.get());
907
908    let mut call = Call::new(command_span);
909    call.decl_id = decl_id;
910    call.head = command_span;
911    let _ = working_set.add_span(call.head);
912
913    let decl = working_set.get_decl(decl_id);
914    let signature = working_set.get_signature(decl);
915
916    enum SpecialCmd {
917        Let,
918        Def,
919        Match,
920        If,
921    }
922
923    impl SpecialCmd {
924        fn from_str(s: &str) -> Option<Self> {
925            Some(match s {
926                "let" => Self::Let,
927                "def" => Self::Def,
928                "match" => Self::Match,
929                "if" => Self::If,
930                _ => return None,
931            })
932        }
933    }
934
935    let special_cmd = decl
936        .is_keyword()
937        .then(|| decl.name())
938        .and_then(SpecialCmd::from_str);
939
940    // TODO: Throw an actual error here, instead of leaning on later type checking code
941    //
942    // `Type::Nothing` is added to inputs to allow uses like:
943    // `ls | sort-by { open -r $in.name | lines | length }`
944    // see https://github.com/nushell/nushell/pull/14922
945    // Incorrect behavior this may cause will be handled by
946    // `check_pipeline_type` in crates/nu-parser/src/type_check.rs
947    let output = signature
948        .get_output_type(
949            input_type
950                .map(|ty| ty.clone().union(Type::Nothing))
951                .as_ref(),
952        )
953        .unwrap_or(Type::Error);
954
955    // This is necessary for some keywords to have proper expression types.
956    // `2 | let x | let y`
957    // - `$x` should be `int`
958    // - `$y` should also be `int`, but for that `let x` as an expression must have the type `int`
959    let mut output_override = None;
960
961    let deprecation = decl.deprecation_info();
962
963    // storing the var ID for later due to borrowing issues
964    let lib_dirs_var_id = match decl.name() {
965        "use" | "overlay use" | "source-env" if decl.is_keyword() => {
966            find_dirs_var(working_set, crate::parse_source::LIB_DIRS_VAR)
967        }
968        "nu-check" if decl.is_builtin() => {
969            find_dirs_var(working_set, crate::parse_source::LIB_DIRS_VAR)
970        }
971        _ => None,
972    };
973
974    // The index into the positional parameter in the definition
975    let mut positional_idx = 0;
976
977    // The index into the spans of argument data given to parse
978    // Starting at the first argument
979    let mut spans_idx = 0;
980
981    if let Some(alias) = decl.as_alias() {
982        if let Expression {
983            expr: Expr::Call(wrapped_call),
984            ..
985        } = &alias.wrapped_call
986        {
987            // Replace this command's call with the aliased call, but keep the alias name
988            call = *wrapped_call.clone();
989            call.head = command_span;
990            // Skip positionals passed to aliased call
991            positional_idx = call.positional_iter().count();
992        } else {
993            working_set.error(ParseError::UnknownState(
994                "Alias does not point to internal call.".to_string(),
995                command_span,
996            ));
997            return ParsedInternalCall {
998                call: Box::new(call),
999                output: Type::Any,
1000                call_kind: CallKind::Invalid,
1001            };
1002        }
1003    }
1004
1005    if let Some(var_id) = lib_dirs_var_id {
1006        call.set_parser_info(
1007            DIR_VAR_PARSER_INFO.to_owned(),
1008            Expression::new(working_set, Expr::Var(var_id), call.head, Type::Any),
1009        );
1010    }
1011
1012    if signature.creates_scope {
1013        working_set.enter_scope();
1014    }
1015
1016    let mut end_of_options = false;
1017
1018    while spans_idx < spans.len() {
1019        let arg_span = spans[spans_idx];
1020
1021        let starting_error_count = working_set.parse_errors.len();
1022
1023        // If we've seen --, skip all flag parsing and go straight to positional parsing
1024        if !end_of_options {
1025            // Check if we're on a long flag, if so, parse
1026            let flag_parse_result = parse_long_flag(working_set, spans, &mut spans_idx, &signature);
1027
1028            match flag_parse_result {
1029                LongFlagParseResult::EndOfOptions => {
1030                    // Switch to positional-only mode so subsequent flags aren't parsed.
1031                    end_of_options = true;
1032
1033                    if signature.allows_unknown_args {
1034                        // For commands that pass through unknown args (extern, def --wrapped,
1035                        // exec, etc.), -- itself must be forwarded to the underlying program.
1036                        let arg = parse_unknown_arg(working_set, arg_span, &signature);
1037                        call.add_unknown(arg);
1038                    }
1039
1040                    spans_idx += 1;
1041                    continue;
1042                }
1043                LongFlagParseResult::FoundFlag(long_name, arg) => {
1044                    // We found a long flag, like --bar
1045                    if working_set.parse_errors[starting_error_count..]
1046                        .iter()
1047                        .any(|x| matches!(x, ParseError::UnknownFlag(_, _, _, _)))
1048                        && signature.allows_unknown_args
1049                    {
1050                        working_set.parse_errors.truncate(starting_error_count);
1051                        let arg = parse_unknown_arg(working_set, arg_span, &signature);
1052
1053                        call.add_unknown(arg);
1054                    } else {
1055                        call.add_named((long_name, None, arg));
1056                    }
1057
1058                    spans_idx += 1;
1059                    continue;
1060                }
1061                LongFlagParseResult::NoFlag => {
1062                    // No long flag found, continue to short flag parsing
1063                }
1064            }
1065        }
1066
1067        // Only try short flag parsing if we haven't seen -- yet
1068        if !end_of_options {
1069            let starting_error_count = working_set.parse_errors.len();
1070
1071            // Check if we're on a short flag or group of short flags, if so, parse
1072            let short_flags = parse_short_flags(
1073                working_set,
1074                spans,
1075                &mut spans_idx,
1076                positional_idx,
1077                &signature,
1078            );
1079
1080            if let Some(mut short_flags) = short_flags {
1081                if short_flags.is_empty() {
1082                    // workaround for completions (PR #6067)
1083                    short_flags.push(Flag {
1084                        long: "".to_string(),
1085                        short: Some('a'),
1086                        arg: None,
1087                        required: false,
1088                        desc: "".to_string(),
1089                        var_id: None,
1090                        default_value: None,
1091                        completion: None,
1092                    })
1093                }
1094
1095                if working_set.parse_errors[starting_error_count..]
1096                    .iter()
1097                    .any(|x| matches!(x, ParseError::UnknownFlag(_, _, _, _)))
1098                    && signature.allows_unknown_args
1099                {
1100                    working_set.parse_errors.truncate(starting_error_count);
1101                    let arg = parse_unknown_arg(working_set, arg_span, &signature);
1102
1103                    call.add_unknown(arg);
1104                } else {
1105                    for flag in short_flags {
1106                        let _ = working_set.add_span(spans[spans_idx]);
1107
1108                        if let Some(arg_shape) = flag.arg {
1109                            if let Some(arg) = spans.get(spans_idx + 1) {
1110                                let arg =
1111                                    crate::parser::parse_value(working_set, *arg, &arg_shape, None);
1112                                let (arg_name, val_expression) = ensure_flag_arg_type(
1113                                    working_set,
1114                                    flag.long.clone(),
1115                                    arg.clone(),
1116                                    &arg_shape,
1117                                    spans[spans_idx],
1118                                );
1119
1120                                if flag.long.is_empty() {
1121                                    if let Some(short) = flag.short {
1122                                        call.add_named((
1123                                            arg_name,
1124                                            Some(Spanned {
1125                                                item: short.to_string(),
1126                                                span: spans[spans_idx],
1127                                            }),
1128                                            Some(val_expression),
1129                                        ));
1130                                    }
1131                                } else {
1132                                    call.add_named((arg_name, None, Some(val_expression)));
1133                                }
1134                                spans_idx += 1;
1135                            } else {
1136                                working_set.error(ParseError::MissingFlagParam(
1137                                    arg_shape.to_string(),
1138                                    arg_span,
1139                                ));
1140                                // NOTE: still need to cover this incomplete flag in the final expression
1141                                // see https://github.com/nushell/nushell/issues/16375
1142                                call.add_named((
1143                                    Spanned {
1144                                        item: String::new(),
1145                                        span: spans[spans_idx],
1146                                    },
1147                                    None,
1148                                    None,
1149                                ));
1150                            }
1151                        } else if flag.long.is_empty() {
1152                            if let Some(short) = flag.short {
1153                                call.add_named((
1154                                    Spanned {
1155                                        item: String::new(),
1156                                        span: spans[spans_idx],
1157                                    },
1158                                    Some(Spanned {
1159                                        item: short.to_string(),
1160                                        span: spans[spans_idx],
1161                                    }),
1162                                    None,
1163                                ));
1164                            }
1165                        } else {
1166                            call.add_named((
1167                                Spanned {
1168                                    item: flag.long.clone(),
1169                                    span: spans[spans_idx],
1170                                },
1171                                None,
1172                                None,
1173                            ));
1174                        }
1175                    }
1176                }
1177
1178                spans_idx += 1;
1179                continue;
1180            }
1181        } // end if !end_of_options (short flags)
1182
1183        {
1184            let contents = working_set.get_span_contents(spans[spans_idx]);
1185
1186            if let Some(Spanned {
1187                span: spread_arg_span,
1188                ..
1189            }) = extract_spread_list(contents.into_spanned(spans[spans_idx]))
1190            {
1191                if signature.rest_positional.is_none() && !signature.allows_unknown_args {
1192                    working_set.error(ParseError::UnexpectedSpreadArg(
1193                        signature.call_signature(),
1194                        arg_span,
1195                    ));
1196                    call.add_positional(Expression::garbage(working_set, arg_span));
1197                } else if positional_idx < signature.required_positional.len() {
1198                    working_set.error(ParseError::MissingPositional(
1199                        signature.required_positional[positional_idx].name.clone(),
1200                        Span::new(spans[spans_idx].start, spans[spans_idx].start),
1201                        signature.call_signature(),
1202                    ));
1203                    call.add_positional(Expression::garbage(working_set, arg_span));
1204                } else {
1205                    let rest_shape = match &signature.rest_positional {
1206                        Some(arg) if matches!(arg.shape, SyntaxShape::ExternalArgument) => {
1207                            // External args aren't parsed inside lists in spread position.
1208                            SyntaxShape::Any
1209                        }
1210                        Some(arg) => arg.shape.clone(),
1211                        None => SyntaxShape::Any,
1212                    };
1213                    // Parse list of arguments to be spread
1214                    let args = crate::parser::parse_value(
1215                        working_set,
1216                        spread_arg_span,
1217                        &SyntaxShape::List(Box::new(rest_shape)),
1218                        None,
1219                    );
1220
1221                    call.add_spread(args);
1222                    // Let the parser know that it's parsing rest arguments now
1223                    positional_idx =
1224                        signature.required_positional.len() + signature.optional_positional.len();
1225                }
1226
1227                spans_idx += 1;
1228                continue;
1229            }
1230        }
1231
1232        // Parse a positional arg if there is one
1233        if let Some(positional) = signature.get_positional(positional_idx) {
1234            let end = calculate_end_span(working_set, &signature, spans, spans_idx, positional_idx);
1235
1236            // Missing arguments before next keyword
1237            if end == spans_idx {
1238                let prev_span = if spans_idx == 0 {
1239                    command_span
1240                } else {
1241                    spans[spans_idx - 1]
1242                };
1243                let whitespace_span = Span::new(prev_span.end, spans[spans_idx].start);
1244                working_set.error(ParseError::MissingPositional(
1245                    positional.name.clone(),
1246                    whitespace_span,
1247                    signature.call_signature(),
1248                ));
1249                call.add_positional(Expression::garbage(working_set, whitespace_span));
1250                positional_idx += 1;
1251                continue;
1252            }
1253            debug_assert!(end <= spans.len());
1254
1255            if spans[..end].is_empty() || spans_idx == end {
1256                working_set.error(ParseError::MissingPositional(
1257                    positional.name.clone(),
1258                    Span::new(spans[spans_idx].end, spans[spans_idx].end),
1259                    signature.call_signature(),
1260                ));
1261                positional_idx += 1;
1262                continue;
1263            }
1264
1265            let compile_error_count = working_set.compile_errors.len();
1266
1267            // HACK: avoid repeated parsing of argument values in special cases
1268            // see https://github.com/nushell/nushell/issues/16398
1269            let arg = match arg_parsing_level {
1270                ArgumentParsingLevel::FirstK { k } if k <= positional_idx => {
1271                    Expression::garbage(working_set, spans[spans_idx])
1272                }
1273                _ => {
1274                    let input_type: Option<Type> = match special_cmd {
1275                        // `let` can assigned from pipeline input, input type is necessary to infer
1276                        // the variable's type correctly
1277                        Some(SpecialCmd::Let)
1278                            if let SyntaxShape::VarWithOptType = &positional.shape =>
1279                        {
1280                            output_override = input_type.cloned();
1281                            input_type.cloned()
1282                        }
1283                        // in a def block, the pipeline input type should be inferred based on the
1284                        // command input-output signature
1285                        Some(SpecialCmd::Def) if &positional.name == "block" => {
1286                            // if we're parsing the `block`, the previous item is the signature
1287                            match call.arguments.last() {
1288                                Some(Argument::Positional(Expression {
1289                                    expr: Expr::Signature(sig),
1290                                    ..
1291                                })) => Some(sig.get_input_type()),
1292                                _ => None,
1293                            }
1294                        }
1295                        Some(SpecialCmd::If) if positional_idx >= 1 => input_type.cloned(),
1296                        Some(SpecialCmd::Match) if &positional.name == "match_block" => {
1297                            input_type.cloned()
1298                        }
1299                        _ => None,
1300                    };
1301
1302                    let expr = parse_multispan_value(
1303                        working_set,
1304                        &spans[..end],
1305                        &mut spans_idx,
1306                        &positional.shape,
1307                        input_type.as_ref(),
1308                    );
1309
1310                    match special_cmd {
1311                        Some(SpecialCmd::Match) if &positional.name == "match_block" => {
1312                            output_override = Some(expr.ty.clone());
1313                        }
1314                        Some(SpecialCmd::If)
1315                            if positional_idx == 1
1316                                && let Expr::Block(block_id) = &expr.expr =>
1317                        {
1318                            let block = working_set.get_block(*block_id);
1319                            let ty = match block.pipelines.is_empty() {
1320                                false => block.output_type(),
1321                                true => input_type.unwrap_or(Type::Any),
1322                            };
1323
1324                            output_override = Some(match output_override {
1325                                Some(existing_ty) => existing_ty.union(ty),
1326                                None => ty,
1327                            });
1328                        }
1329                        Some(SpecialCmd::If)
1330                            if positional_idx == 2
1331                                && let Expr::Keyword(kw) = &expr.expr =>
1332                        {
1333                            let ty = match &kw.expr.expr {
1334                                Expr::Block(block_id) => {
1335                                    let block = working_set.get_block(*block_id);
1336                                    match block.pipelines.is_empty() {
1337                                        false => block.output_type(),
1338                                        true => input_type.unwrap_or(Type::Any),
1339                                    }
1340                                }
1341                                _ => kw.expr.ty.clone(),
1342                            };
1343
1344                            output_override = Some(match output_override {
1345                                Some(existing_ty) => existing_ty.union(ty),
1346                                None => ty,
1347                            });
1348                        }
1349                        _ => {}
1350                    };
1351
1352                    expr
1353                }
1354            };
1355
1356            // HACK: try-catch's signature defines the catch block as a Closure, even though it's
1357            // used like a Block. Because closures are compiled eagerly, this ends up making the
1358            // following code technically invalid:
1359            // ```nu
1360            // loop { try { } catch {|e| break } }
1361            // ```
1362            // Thus, we discard the compilation error here
1363            if let SyntaxShape::OneOf(ref shapes) = positional.shape {
1364                for one_shape in shapes {
1365                    if let SyntaxShape::Keyword(keyword, ..) = one_shape
1366                        && keyword == b"catch"
1367                        && let [nu_protocol::CompileError::NotInALoop { .. }] =
1368                            &working_set.compile_errors[compile_error_count..]
1369                    {
1370                        working_set.compile_errors.truncate(compile_error_count);
1371                    }
1372                }
1373            }
1374
1375            let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) {
1376                working_set.error(ParseError::TypeMismatch(
1377                    positional.shape.to_type(),
1378                    arg.ty,
1379                    arg.span,
1380                ));
1381                Expression::garbage(working_set, arg.span)
1382            } else {
1383                arg
1384            };
1385
1386            call.add_positional(arg);
1387            positional_idx += 1;
1388        } else if signature.allows_unknown_args {
1389            let arg = parse_unknown_arg(working_set, arg_span, &signature);
1390
1391            call.add_unknown(arg);
1392        } else {
1393            call.add_positional(Expression::garbage(working_set, arg_span));
1394            working_set.error(ParseError::ExtraPositional(
1395                signature.call_signature(),
1396                arg_span,
1397            ))
1398        }
1399
1400        spans_idx += 1;
1401    }
1402
1403    // TODO: Inline `check_call`,
1404    // move missing positional checking into the while loop above with two pointers.
1405    // Maybe more `CallKind::Invalid` if errors found during argument parsing.
1406    let call_kind = check_call(working_set, command_span, &signature, &call);
1407
1408    deprecation
1409        .into_iter()
1410        .filter_map(|entry| entry.parse_warning(&signature.name, &call))
1411        .for_each(|warning| {
1412            // FIXME: if two flags are deprecated and both are used in one command,
1413            // the second flag's deprecation won't show until the first flag is removed
1414            // (but it won't be flagged as reported until it is actually reported)
1415            working_set.warning(warning);
1416        });
1417
1418    if signature.creates_scope {
1419        working_set.exit_scope();
1420    }
1421
1422    match special_cmd {
1423        // Not having an else branch means the output can be `nothing`
1424        Some(SpecialCmd::If) if call.arguments.len() < 3 => {
1425            output_override = output_override.map(|ty| ty.union(Type::Nothing))
1426        }
1427        _ => {}
1428    }
1429
1430    let output = output_override.unwrap_or(output);
1431
1432    ParsedInternalCall {
1433        call: Box::new(call),
1434        output,
1435        call_kind,
1436    }
1437}
1438
1439pub fn parse_call(
1440    working_set: &mut StateWorkingSet,
1441    spans: &[Span],
1442    head: Span,
1443    input_type: Option<&Type>,
1444) -> Expression {
1445    trace!("parsing: call");
1446    let call_span = Span::concat(spans);
1447
1448    if spans.is_empty() {
1449        working_set.error(ParseError::UnknownState(
1450            "Encountered command with zero spans".into(),
1451            call_span,
1452        ));
1453        return garbage(working_set, head);
1454    }
1455
1456    let call_sigil = match working_set.get_span_contents(spans[0]).first() {
1457        Some(b'^') => Some(b'^'),
1458        Some(b'%') => Some(b'%'),
1459        _ => None,
1460    };
1461
1462    let mut adjusted_spans = Vec::new();
1463    let resolution_spans = match call_sigil {
1464        Some(b'^') | Some(b'%') => {
1465            adjusted_spans.reserve(spans.len());
1466            adjusted_spans.push(Span::new(spans[0].start + 1, spans[0].end));
1467            adjusted_spans.extend_from_slice(&spans[1..]);
1468            adjusted_spans.as_slice()
1469        }
1470        _ => spans,
1471    };
1472
1473    // `^` always forces external command parsing and must bypass declaration
1474    // resolution, even when an internal command with the same name exists.
1475    if call_sigil == Some(b'^') {
1476        trace!("parsing: forced external call");
1477        return parse_external_call(working_set, resolution_spans, call_span);
1478    }
1479
1480    // Check if we have a percent sigil with a dynamic head (variable or expression).
1481    // Supports two token layouts:
1482    //   - single token: `%$cmd` or `%($cmd)` — stripping `%` leaves `$cmd` / `($cmd)` in [0]
1483    //   - two tokens:   `%` and `($cmd)`    — stripping `%` leaves an empty span in [0]; head is [1]
1484    // If so, defer builtin validation to runtime (the IR compiler will rewrite to `run-internal`).
1485    if call_sigil == Some(b'%') && !resolution_spans.is_empty() {
1486        // Locate the actual head span, skipping an empty leading span.
1487        let (head_idx, head_span) = {
1488            let first = working_set.get_span_contents(resolution_spans[0]);
1489            if first.is_empty() && resolution_spans.len() > 1 {
1490                (1, resolution_spans[1])
1491            } else {
1492                (0, resolution_spans[0])
1493            }
1494        };
1495
1496        let dynamic_head_contents = working_set.get_span_contents(head_span);
1497        let is_dynamic_head = !dynamic_head_contents.is_empty()
1498            && (dynamic_head_contents[0] == b'$' || dynamic_head_contents[0] == b'(');
1499
1500        if is_dynamic_head {
1501            trace!("parsing: dynamic percent builtin dispatch");
1502
1503            let head_expr = crate::parser::parse_expression(working_set, &[head_span], input_type);
1504
1505            // Create a placeholder call; the IR compiler will rewrite this to `run-internal`.
1506            let mut call = Call::new(call_span);
1507            call.decl_id = DeclId::new(0);
1508
1509            // Store the head expression for the IR compiler to pick up.
1510            call.set_parser_info(PERCENT_FORCED_BUILTIN_PARSER_INFO.to_string(), head_expr);
1511
1512            // Mirror the dynamic external-call path by preserving `...expr` as an explicit spread
1513            // argument so runtime dispatch can forward it without flattening first.
1514            for arg_span in resolution_spans.iter().skip(head_idx + 1) {
1515                let contents = working_set.get_span_contents(*arg_span);
1516                if let Some(Spanned { span: arg_span, .. }) =
1517                    extract_spread_list(contents.into_spanned(*arg_span))
1518                {
1519                    let spread_expr = crate::parser::parse_value(
1520                        working_set,
1521                        arg_span,
1522                        &SyntaxShape::List(Box::new(SyntaxShape::Any)),
1523                        None,
1524                    );
1525                    call.arguments.push(Argument::Spread(spread_expr));
1526                } else {
1527                    let arg_expr =
1528                        crate::parser::parse_value(working_set, *arg_span, &SyntaxShape::Any, None);
1529                    call.arguments.push(Argument::Positional(arg_expr));
1530                }
1531            }
1532
1533            return Expression::new(
1534                working_set,
1535                Expr::Call(Box::new(call)),
1536                call_span,
1537                Type::Any,
1538            );
1539        }
1540    }
1541
1542    let (cmd_start, pos, _name, maybe_decl_id) = if call_sigil == Some(b'%') {
1543        find_longest_decl_with_command_type(working_set, resolution_spans, CommandType::Builtin)
1544    } else {
1545        find_longest_decl(working_set, resolution_spans)
1546    };
1547
1548    if let Some(decl_id) = maybe_decl_id {
1549        // Before the internal parsing we check if there is no let or alias declarations
1550        // that are missing their name, e.g.: let = 1 or alias = 2
1551        if resolution_spans.len() > 1 {
1552            let test_equal = working_set.get_span_contents(resolution_spans[1]);
1553
1554            if test_equal == [b'='] {
1555                trace!("incomplete statement");
1556
1557                working_set.error(ParseError::UnknownState(
1558                    "Incomplete statement".into(),
1559                    call_span,
1560                ));
1561                return garbage(working_set, call_span);
1562            }
1563        }
1564
1565        let decl = working_set.get_decl(decl_id);
1566
1567        let parsed_call = if let Some(alias) = decl.as_alias() {
1568            if let Expression {
1569                expr: Expr::ExternalCall(head, args),
1570                span: _,
1571                span_id: _,
1572                ty,
1573            } = &alias.clone().wrapped_call
1574            {
1575                trace!("parsing: alias of external call");
1576
1577                let mut head = head.clone();
1578                head.span = Span::concat(&resolution_spans[cmd_start..pos]); // replacing the spans preserves syntax highlighting
1579
1580                let mut final_args = args.clone().into_vec();
1581                for arg_span in &resolution_spans[pos..] {
1582                    let arg = parse_external_arg(working_set, *arg_span);
1583                    final_args.push(arg);
1584                }
1585
1586                let expression = Expression::new(
1587                    working_set,
1588                    Expr::ExternalCall(head, final_args.into()),
1589                    Span::concat(spans),
1590                    ty.clone(),
1591                );
1592
1593                return expression;
1594            } else {
1595                trace!("parsing: alias of internal call");
1596                parse_internal_call(
1597                    working_set,
1598                    Span::concat(&resolution_spans[cmd_start..pos]),
1599                    &resolution_spans[pos..],
1600                    decl_id,
1601                    ArgumentParsingLevel::Full,
1602                    input_type,
1603                )
1604            }
1605        } else {
1606            trace!("parsing: internal call");
1607            parse_internal_call(
1608                working_set,
1609                Span::concat(&resolution_spans[cmd_start..pos]),
1610                &resolution_spans[pos..],
1611                decl_id,
1612                ArgumentParsingLevel::Full,
1613                input_type,
1614            )
1615        };
1616
1617        Expression::new(
1618            working_set,
1619            Expr::Call(parsed_call.call),
1620            call_span,
1621            parsed_call.output,
1622        )
1623    } else {
1624        if call_sigil == Some(b'%') {
1625            working_set.error(ParseError::LabeledErrorWithHelp {
1626                error: "percent sigil requires a built-in command".into(),
1627                label: "unknown built-in command".into(),
1628                help:
1629                    "remove `%` to use normal resolution, or use `^` to run an external command explicitly".into(),
1630                span: resolution_spans[0],
1631            });
1632
1633            // Preserve expression shape for features like completion while retaining the parse error.
1634            return parse_external_call(working_set, spans, call_span);
1635        }
1636
1637        // We might be parsing left-unbounded range ("..10")
1638        let bytes = working_set.get_span_contents(spans[0]);
1639        trace!("parsing: range {bytes:?}");
1640        if let (Some(b'.'), Some(b'.')) = (bytes.first(), bytes.get(1)) {
1641            trace!("-- found leading range indicator");
1642            let starting_error_count = working_set.parse_errors.len();
1643
1644            if let Some(range_expr) = crate::parser::parse_range(working_set, spans[0]) {
1645                trace!("-- successfully parsed range");
1646                return range_expr;
1647            }
1648            working_set.parse_errors.truncate(starting_error_count);
1649        }
1650        trace!("parsing: external call");
1651
1652        // Otherwise, try external command
1653        parse_external_call(working_set, spans, call_span)
1654    }
1655}
1656
1657fn find_decl_with_command_type(
1658    working_set: &StateWorkingSet<'_>,
1659    name: &[u8],
1660    command_type: CommandType,
1661) -> Option<DeclId> {
1662    // Search all known declarations so `%cmd` can still resolve a built-in even when
1663    // a custom command with the same name shadows it in normal visibility lookup.
1664    for idx in (0..working_set.num_decls()).rev() {
1665        let decl_id = DeclId::new(idx);
1666        let decl = working_set.get_decl(decl_id);
1667        if decl.command_type() == command_type && decl.name().as_bytes() == name {
1668            return Some(decl_id);
1669        }
1670    }
1671
1672    None
1673}
1674
1675fn command_name_from_spans(
1676    working_set: &StateWorkingSet<'_>,
1677    spans: &[Span],
1678    prefix: &[u8],
1679) -> Vec<u8> {
1680    let mut name = Vec::with_capacity(prefix.len() + spans.len() * 2);
1681    name.extend(prefix);
1682
1683    for span in spans {
1684        let name_part = working_set.get_span_contents(*span);
1685        if name.is_empty() {
1686            name.extend(name_part);
1687        } else {
1688            name.push(b' ');
1689            name.extend(name_part);
1690        }
1691    }
1692
1693    name
1694}
1695
1696fn find_longest_decl_with_command_type(
1697    working_set: &StateWorkingSet<'_>,
1698    spans: &[Span],
1699    command_type: CommandType,
1700) -> (
1701    usize,
1702    usize,
1703    Vec<u8>,
1704    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1705) {
1706    let mut pos = spans.len();
1707    let cmd_start = 0;
1708    let mut name_spans = spans.to_vec();
1709
1710    let mut name = command_name_from_spans(working_set, &name_spans, b"");
1711
1712    let mut maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1713
1714    while maybe_decl_id.is_none() {
1715        if name_spans.len() <= 1 {
1716            break;
1717        }
1718
1719        name_spans.pop();
1720        pos -= 1;
1721
1722        name = command_name_from_spans(working_set, &name_spans, b"");
1723
1724        maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1725    }
1726
1727    (cmd_start, pos, name, maybe_decl_id)
1728}
1729
1730pub fn find_longest_decl(
1731    working_set: &mut StateWorkingSet<'_>,
1732    spans: &[Span],
1733) -> (
1734    usize,
1735    usize,
1736    Vec<u8>,
1737    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1738) {
1739    find_longest_decl_with_prefix(working_set, spans, b"")
1740}
1741
1742pub fn find_longest_decl_with_prefix(
1743    working_set: &mut StateWorkingSet<'_>,
1744    spans: &[Span],
1745    prefix: &[u8],
1746) -> (
1747    usize,
1748    usize,
1749    Vec<u8>,
1750    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1751) {
1752    let mut pos = 0;
1753    let cmd_start = pos;
1754    let mut name_spans = vec![];
1755
1756    for word_span in spans[cmd_start..].iter() {
1757        // Find the longest group of words that could form a command
1758
1759        name_spans.push(*word_span);
1760
1761        pos += 1;
1762    }
1763
1764    let mut name = command_name_from_spans(working_set, &name_spans, prefix);
1765
1766    let mut maybe_decl_id = working_set.find_decl(&name);
1767
1768    while maybe_decl_id.is_none() {
1769        // Find the longest command match
1770        if name_spans.len() <= 1 {
1771            // Keep the first word even if it does not match -- could be external command
1772            break;
1773        }
1774
1775        name_spans.pop();
1776        pos -= 1;
1777
1778        name = command_name_from_spans(working_set, &name_spans, prefix);
1779        maybe_decl_id = working_set.find_decl(&name);
1780    }
1781
1782    // If there is a declaration and there are remaining spans, check if it's an alias.
1783    // If it is, try to see if there are sub commands
1784    if let Some(decl_id) = maybe_decl_id
1785        && pos < spans.len()
1786    {
1787        let decl = working_set.get_decl(decl_id);
1788        if let Some(alias) = decl.as_alias() {
1789            // Extract the command name from the alias
1790            // The wrapped_call should be a Call expression for internal commands
1791            if let Expression {
1792                expr: Expr::Call(call),
1793                ..
1794            } = &alias.wrapped_call
1795            {
1796                let aliased_decl_id = call.decl_id;
1797                let aliased_name = working_set.get_decl(aliased_decl_id).name().to_string();
1798
1799                // Try to find a longer match using the aliased command name with remaining spans
1800                let (_, new_pos, new_name, new_decl_id) = find_longest_decl_with_prefix(
1801                    working_set,
1802                    &spans[pos..],
1803                    aliased_name.as_bytes(),
1804                );
1805
1806                // If we find a sub command, use it instead.
1807                if new_decl_id.is_some() && new_pos > 0 {
1808                    let total_pos = pos + new_pos;
1809                    return (cmd_start, total_pos, new_name, new_decl_id);
1810                }
1811            }
1812        }
1813    }
1814
1815    (cmd_start, pos, name, maybe_decl_id)
1816}
1817
1818pub fn parse_attribute(
1819    working_set: &mut StateWorkingSet,
1820    lite_command: &LiteCommand,
1821) -> (Attribute, Option<String>) {
1822    let _ = lite_command
1823        .parts
1824        .first()
1825        .filter(|s| working_set.get_span_contents(**s).starts_with(b"@"))
1826        .expect("Attributes always start with an `@`");
1827
1828    assert!(
1829        lite_command.attribute_idx.is_empty(),
1830        "attributes can't have attributes"
1831    );
1832
1833    let mut spans = lite_command.parts.clone();
1834    if let Some(first) = spans.first_mut() {
1835        first.start += 1;
1836    }
1837    let spans = spans.as_slice();
1838    let attr_span = Span::concat(spans);
1839
1840    let (cmd_start, cmd_end, mut name, decl_id) =
1841        find_longest_decl_with_prefix(working_set, spans, b"attr");
1842
1843    debug_assert!(name.starts_with(b"attr "));
1844    let _ = name.drain(..(b"attr ".len()));
1845
1846    let name_span = Span::concat(&spans[cmd_start..cmd_end]);
1847
1848    let Ok(name) = String::from_utf8(name) else {
1849        working_set.error(ParseError::NonUtf8(name_span));
1850        return (
1851            Attribute {
1852                expr: garbage(working_set, attr_span),
1853            },
1854            None,
1855        );
1856    };
1857
1858    let Some(decl_id) = decl_id else {
1859        working_set.error(ParseError::UnknownCommand(name_span));
1860        return (
1861            Attribute {
1862                expr: garbage(working_set, attr_span),
1863            },
1864            None,
1865        );
1866    };
1867
1868    let decl = working_set.get_decl(decl_id);
1869
1870    let parsed_call = match decl.as_alias() {
1871        // TODO: Once `const def` is available, we should either disallow aliases as attributes OR
1872        // allow them but rather than using the aliases' name, use the name of the aliased command
1873        Some(alias) => match &alias.clone().wrapped_call {
1874            Expression {
1875                expr: Expr::ExternalCall(..),
1876                ..
1877            } => {
1878                let shell_error = ShellError::NotAConstCommand { span: name_span };
1879                working_set.error(shell_error.wrap(working_set, attr_span));
1880                return (
1881                    Attribute {
1882                        expr: garbage(working_set, Span::concat(spans)),
1883                    },
1884                    None,
1885                );
1886            }
1887            _ => {
1888                trace!("parsing: alias of internal call");
1889                parse_internal_call(
1890                    working_set,
1891                    name_span,
1892                    &spans[cmd_end..],
1893                    decl_id,
1894                    ArgumentParsingLevel::Full,
1895                    None,
1896                )
1897            }
1898        },
1899        None => {
1900            trace!("parsing: internal call");
1901            parse_internal_call(
1902                working_set,
1903                name_span,
1904                &spans[cmd_end..],
1905                decl_id,
1906                ArgumentParsingLevel::Full,
1907                None,
1908            )
1909        }
1910    };
1911
1912    (
1913        Attribute {
1914            expr: Expression::new(
1915                working_set,
1916                Expr::Call(parsed_call.call),
1917                Span::concat(spans),
1918                parsed_call.output,
1919            ),
1920        },
1921        Some(name),
1922    )
1923}