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                    // HACK: `def` block parameter is of type `closure`, which is wrong.
1303                    // However, that's used to make sure `def` blocks don't capture mutable
1304                    // variables. (Which is also a HACK)
1305                    //
1306                    // Closure bodies do not get pipeline input type, but `def` bodies should.
1307                    // Thus, we work around the mentioned hack with another one here.
1308                    //
1309                    // This is of course unideal, but it's the way to go to fix the issue without a
1310                    // big refactor, which could neither be done in time for the 0.114.1 patch
1311                    // release, nor would it be appropriate to include in a patch release.
1312                    let expr = match special_cmd {
1313                        Some(SpecialCmd::Def) if &positional.name == "block" => {
1314                            let starting_error_count = working_set.parse_errors.len();
1315
1316                            let out = crate::parse_expressions::parse_closure_expression(
1317                                working_set,
1318                                &positional.shape,
1319                                spans[spans_idx],
1320                                input_type.as_ref(),
1321                            );
1322
1323                            if let Expr::Closure(_) = out.expr {
1324                                out
1325                            } else {
1326                                // on failure, we fallback to the normal code path to keep errors
1327                                // the same as before this hack
1328                                working_set.parse_errors.truncate(starting_error_count);
1329                                parse_multispan_value(
1330                                    working_set,
1331                                    &spans[..end],
1332                                    &mut spans_idx,
1333                                    &positional.shape,
1334                                    input_type.as_ref(),
1335                                )
1336                            }
1337                        }
1338                        _ => parse_multispan_value(
1339                            working_set,
1340                            &spans[..end],
1341                            &mut spans_idx,
1342                            &positional.shape,
1343                            input_type.as_ref(),
1344                        ),
1345                    };
1346
1347                    match special_cmd {
1348                        Some(SpecialCmd::Match) if &positional.name == "match_block" => {
1349                            output_override = Some(expr.ty.clone());
1350                        }
1351                        Some(SpecialCmd::If)
1352                            if positional_idx == 1
1353                                && let Expr::Block(block_id) = &expr.expr =>
1354                        {
1355                            let block = working_set.get_block(*block_id);
1356                            let ty = match block.pipelines.is_empty() {
1357                                false => block.output_type(),
1358                                true => input_type.unwrap_or(Type::Any),
1359                            };
1360
1361                            output_override = Some(match output_override {
1362                                Some(existing_ty) => existing_ty.union(ty),
1363                                None => ty,
1364                            });
1365                        }
1366                        Some(SpecialCmd::If)
1367                            if positional_idx == 2
1368                                && let Expr::Keyword(kw) = &expr.expr =>
1369                        {
1370                            let ty = match &kw.expr.expr {
1371                                Expr::Block(block_id) => {
1372                                    let block = working_set.get_block(*block_id);
1373                                    match block.pipelines.is_empty() {
1374                                        false => block.output_type(),
1375                                        true => input_type.unwrap_or(Type::Any),
1376                                    }
1377                                }
1378                                _ => kw.expr.ty.clone(),
1379                            };
1380
1381                            output_override = Some(match output_override {
1382                                Some(existing_ty) => existing_ty.union(ty),
1383                                None => ty,
1384                            });
1385                        }
1386                        _ => {}
1387                    };
1388
1389                    expr
1390                }
1391            };
1392
1393            // HACK: try-catch's signature defines the catch block as a Closure, even though it's
1394            // used like a Block. Because closures are compiled eagerly, this ends up making the
1395            // following code technically invalid:
1396            // ```nu
1397            // loop { try { } catch {|e| break } }
1398            // ```
1399            // Thus, we discard the compilation error here
1400            if let SyntaxShape::OneOf(ref shapes) = positional.shape {
1401                for one_shape in shapes {
1402                    if let SyntaxShape::Keyword(keyword, ..) = one_shape
1403                        && keyword == b"catch"
1404                        && let [nu_protocol::CompileError::NotInALoop { .. }] =
1405                            &working_set.compile_errors[compile_error_count..]
1406                    {
1407                        working_set.compile_errors.truncate(compile_error_count);
1408                    }
1409                }
1410            }
1411
1412            let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) {
1413                working_set.error(ParseError::TypeMismatch(
1414                    positional.shape.to_type(),
1415                    arg.ty,
1416                    arg.span,
1417                ));
1418                Expression::garbage(working_set, arg.span)
1419            } else {
1420                arg
1421            };
1422
1423            call.add_positional(arg);
1424            positional_idx += 1;
1425        } else if signature.allows_unknown_args {
1426            let arg = parse_unknown_arg(working_set, arg_span, &signature);
1427
1428            call.add_unknown(arg);
1429        } else {
1430            call.add_positional(Expression::garbage(working_set, arg_span));
1431            working_set.error(ParseError::ExtraPositional(
1432                signature.call_signature(),
1433                arg_span,
1434            ))
1435        }
1436
1437        spans_idx += 1;
1438    }
1439
1440    // TODO: Inline `check_call`,
1441    // move missing positional checking into the while loop above with two pointers.
1442    // Maybe more `CallKind::Invalid` if errors found during argument parsing.
1443    let call_kind = check_call(working_set, command_span, &signature, &call);
1444
1445    deprecation
1446        .into_iter()
1447        .filter_map(|entry| entry.parse_warning(&signature.name, &call))
1448        .for_each(|warning| {
1449            // FIXME: if two flags are deprecated and both are used in one command,
1450            // the second flag's deprecation won't show until the first flag is removed
1451            // (but it won't be flagged as reported until it is actually reported)
1452            working_set.warning(warning);
1453        });
1454
1455    if signature.creates_scope {
1456        working_set.exit_scope();
1457    }
1458
1459    match special_cmd {
1460        // Not having an else branch means the output can be `nothing`
1461        Some(SpecialCmd::If) if call.arguments.len() < 3 => {
1462            output_override = output_override.map(|ty| ty.union(Type::Nothing))
1463        }
1464        _ => {}
1465    }
1466
1467    let output = output_override.unwrap_or(output);
1468
1469    ParsedInternalCall {
1470        call: Box::new(call),
1471        output,
1472        call_kind,
1473    }
1474}
1475
1476pub fn parse_call(
1477    working_set: &mut StateWorkingSet,
1478    spans: &[Span],
1479    head: Span,
1480    input_type: Option<&Type>,
1481) -> Expression {
1482    trace!("parsing: call");
1483    let call_span = Span::concat(spans);
1484
1485    if spans.is_empty() {
1486        working_set.error(ParseError::UnknownState(
1487            "Encountered command with zero spans".into(),
1488            call_span,
1489        ));
1490        return garbage(working_set, head);
1491    }
1492
1493    let call_sigil = match working_set.get_span_contents(spans[0]).first() {
1494        Some(b'^') => Some(b'^'),
1495        Some(b'%') => Some(b'%'),
1496        _ => None,
1497    };
1498
1499    let mut adjusted_spans = Vec::new();
1500    let resolution_spans = match call_sigil {
1501        Some(b'^') | Some(b'%') => {
1502            adjusted_spans.reserve(spans.len());
1503            adjusted_spans.push(Span::new(spans[0].start + 1, spans[0].end));
1504            adjusted_spans.extend_from_slice(&spans[1..]);
1505            adjusted_spans.as_slice()
1506        }
1507        _ => spans,
1508    };
1509
1510    // `^` always forces external command parsing and must bypass declaration
1511    // resolution, even when an internal command with the same name exists.
1512    if call_sigil == Some(b'^') {
1513        trace!("parsing: forced external call");
1514        return parse_external_call(working_set, resolution_spans, call_span);
1515    }
1516
1517    // Check if we have a percent sigil with a dynamic head (variable or expression).
1518    // Supports two token layouts:
1519    //   - single token: `%$cmd` or `%($cmd)` — stripping `%` leaves `$cmd` / `($cmd)` in [0]
1520    //   - two tokens:   `%` and `($cmd)`    — stripping `%` leaves an empty span in [0]; head is [1]
1521    // If so, defer builtin validation to runtime (the IR compiler will rewrite to `run-internal`).
1522    if call_sigil == Some(b'%') && !resolution_spans.is_empty() {
1523        // Locate the actual head span, skipping an empty leading span.
1524        let (head_idx, head_span) = {
1525            let first = working_set.get_span_contents(resolution_spans[0]);
1526            if first.is_empty() && resolution_spans.len() > 1 {
1527                (1, resolution_spans[1])
1528            } else {
1529                (0, resolution_spans[0])
1530            }
1531        };
1532
1533        let dynamic_head_contents = working_set.get_span_contents(head_span);
1534        let is_dynamic_head = !dynamic_head_contents.is_empty()
1535            && (dynamic_head_contents[0] == b'$' || dynamic_head_contents[0] == b'(');
1536
1537        if is_dynamic_head {
1538            trace!("parsing: dynamic percent builtin dispatch");
1539
1540            let head_expr = crate::parser::parse_expression(working_set, &[head_span], input_type);
1541
1542            // Create a placeholder call; the IR compiler will rewrite this to `run-internal`.
1543            let mut call = Call::new(call_span);
1544            call.decl_id = DeclId::new(0);
1545
1546            // Store the head expression for the IR compiler to pick up.
1547            call.set_parser_info(PERCENT_FORCED_BUILTIN_PARSER_INFO.to_string(), head_expr);
1548
1549            // Mirror the dynamic external-call path by preserving `...expr` as an explicit spread
1550            // argument so runtime dispatch can forward it without flattening first.
1551            for arg_span in resolution_spans.iter().skip(head_idx + 1) {
1552                let contents = working_set.get_span_contents(*arg_span);
1553                if let Some(Spanned { span: arg_span, .. }) =
1554                    extract_spread_list(contents.into_spanned(*arg_span))
1555                {
1556                    let spread_expr = crate::parser::parse_value(
1557                        working_set,
1558                        arg_span,
1559                        &SyntaxShape::List(Box::new(SyntaxShape::Any)),
1560                        None,
1561                    );
1562                    call.arguments.push(Argument::Spread(spread_expr));
1563                } else {
1564                    let arg_expr =
1565                        crate::parser::parse_value(working_set, *arg_span, &SyntaxShape::Any, None);
1566                    call.arguments.push(Argument::Positional(arg_expr));
1567                }
1568            }
1569
1570            return Expression::new(
1571                working_set,
1572                Expr::Call(Box::new(call)),
1573                call_span,
1574                Type::Any,
1575            );
1576        }
1577    }
1578
1579    let (cmd_start, pos, _name, maybe_decl_id) = if call_sigil == Some(b'%') {
1580        find_longest_decl_with_command_type(working_set, resolution_spans, CommandType::Builtin)
1581    } else {
1582        find_longest_decl(working_set, resolution_spans)
1583    };
1584
1585    if let Some(decl_id) = maybe_decl_id {
1586        // Before the internal parsing we check if there is no let or alias declarations
1587        // that are missing their name, e.g.: let = 1 or alias = 2
1588        if resolution_spans.len() > 1 {
1589            let test_equal = working_set.get_span_contents(resolution_spans[1]);
1590
1591            if test_equal == [b'='] {
1592                trace!("incomplete statement");
1593
1594                working_set.error(ParseError::UnknownState(
1595                    "Incomplete statement".into(),
1596                    call_span,
1597                ));
1598                return garbage(working_set, call_span);
1599            }
1600        }
1601
1602        let decl = working_set.get_decl(decl_id);
1603
1604        let parsed_call = if let Some(alias) = decl.as_alias() {
1605            if let Expression {
1606                expr: Expr::ExternalCall(head, args),
1607                span: _,
1608                span_id: _,
1609                ty,
1610            } = &alias.clone().wrapped_call
1611            {
1612                trace!("parsing: alias of external call");
1613
1614                let mut head = head.clone();
1615                head.span = Span::concat(&resolution_spans[cmd_start..pos]); // replacing the spans preserves syntax highlighting
1616
1617                let mut final_args = args.clone().into_vec();
1618                for arg_span in &resolution_spans[pos..] {
1619                    let arg = parse_external_arg(working_set, *arg_span);
1620                    final_args.push(arg);
1621                }
1622
1623                let expression = Expression::new(
1624                    working_set,
1625                    Expr::ExternalCall(head, final_args.into()),
1626                    Span::concat(spans),
1627                    ty.clone(),
1628                );
1629
1630                return expression;
1631            } else {
1632                trace!("parsing: alias of internal call");
1633                parse_internal_call(
1634                    working_set,
1635                    Span::concat(&resolution_spans[cmd_start..pos]),
1636                    &resolution_spans[pos..],
1637                    decl_id,
1638                    ArgumentParsingLevel::Full,
1639                    input_type,
1640                )
1641            }
1642        } else {
1643            trace!("parsing: internal call");
1644            parse_internal_call(
1645                working_set,
1646                Span::concat(&resolution_spans[cmd_start..pos]),
1647                &resolution_spans[pos..],
1648                decl_id,
1649                ArgumentParsingLevel::Full,
1650                input_type,
1651            )
1652        };
1653
1654        Expression::new(
1655            working_set,
1656            Expr::Call(parsed_call.call),
1657            call_span,
1658            parsed_call.output,
1659        )
1660    } else {
1661        if call_sigil == Some(b'%') {
1662            working_set.error(ParseError::LabeledErrorWithHelp {
1663                error: "percent sigil requires a built-in command".into(),
1664                label: "unknown built-in command".into(),
1665                help:
1666                    "remove `%` to use normal resolution, or use `^` to run an external command explicitly".into(),
1667                span: resolution_spans[0],
1668            });
1669
1670            // Preserve expression shape for features like completion while retaining the parse error.
1671            return parse_external_call(working_set, spans, call_span);
1672        }
1673
1674        // We might be parsing left-unbounded range ("..10")
1675        let bytes = working_set.get_span_contents(spans[0]);
1676        trace!("parsing: range {bytes:?}");
1677        if let (Some(b'.'), Some(b'.')) = (bytes.first(), bytes.get(1)) {
1678            trace!("-- found leading range indicator");
1679            let starting_error_count = working_set.parse_errors.len();
1680
1681            if let Some(range_expr) = crate::parser::parse_range(working_set, spans[0]) {
1682                trace!("-- successfully parsed range");
1683                return range_expr;
1684            }
1685            working_set.parse_errors.truncate(starting_error_count);
1686        }
1687        trace!("parsing: external call");
1688
1689        // Otherwise, try external command
1690        parse_external_call(working_set, spans, call_span)
1691    }
1692}
1693
1694fn find_decl_with_command_type(
1695    working_set: &StateWorkingSet<'_>,
1696    name: &[u8],
1697    command_type: CommandType,
1698) -> Option<DeclId> {
1699    // Search all known declarations so `%cmd` can still resolve a built-in even when
1700    // a custom command with the same name shadows it in normal visibility lookup.
1701    for idx in (0..working_set.num_decls()).rev() {
1702        let decl_id = DeclId::new(idx);
1703        let decl = working_set.get_decl(decl_id);
1704        if decl.command_type() == command_type && decl.name().as_bytes() == name {
1705            return Some(decl_id);
1706        }
1707    }
1708
1709    None
1710}
1711
1712fn command_name_from_spans(
1713    working_set: &StateWorkingSet<'_>,
1714    spans: &[Span],
1715    prefix: &[u8],
1716) -> Vec<u8> {
1717    let mut name = Vec::with_capacity(prefix.len() + spans.len() * 2);
1718    name.extend(prefix);
1719
1720    for span in spans {
1721        let name_part = working_set.get_span_contents(*span);
1722        if name.is_empty() {
1723            name.extend(name_part);
1724        } else {
1725            name.push(b' ');
1726            name.extend(name_part);
1727        }
1728    }
1729
1730    name
1731}
1732
1733fn find_longest_decl_with_command_type(
1734    working_set: &StateWorkingSet<'_>,
1735    spans: &[Span],
1736    command_type: CommandType,
1737) -> (
1738    usize,
1739    usize,
1740    Vec<u8>,
1741    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1742) {
1743    let mut pos = spans.len();
1744    let cmd_start = 0;
1745    let mut name_spans = spans.to_vec();
1746
1747    let mut name = command_name_from_spans(working_set, &name_spans, b"");
1748
1749    let mut maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1750
1751    while maybe_decl_id.is_none() {
1752        if name_spans.len() <= 1 {
1753            break;
1754        }
1755
1756        name_spans.pop();
1757        pos -= 1;
1758
1759        name = command_name_from_spans(working_set, &name_spans, b"");
1760
1761        maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1762    }
1763
1764    (cmd_start, pos, name, maybe_decl_id)
1765}
1766
1767pub fn find_longest_decl(
1768    working_set: &mut StateWorkingSet<'_>,
1769    spans: &[Span],
1770) -> (
1771    usize,
1772    usize,
1773    Vec<u8>,
1774    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1775) {
1776    find_longest_decl_with_prefix(working_set, spans, b"")
1777}
1778
1779pub fn find_longest_decl_with_prefix(
1780    working_set: &mut StateWorkingSet<'_>,
1781    spans: &[Span],
1782    prefix: &[u8],
1783) -> (
1784    usize,
1785    usize,
1786    Vec<u8>,
1787    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1788) {
1789    let mut pos = 0;
1790    let cmd_start = pos;
1791    let mut name_spans = vec![];
1792
1793    for word_span in spans[cmd_start..].iter() {
1794        // Find the longest group of words that could form a command
1795
1796        name_spans.push(*word_span);
1797
1798        pos += 1;
1799    }
1800
1801    let mut name = command_name_from_spans(working_set, &name_spans, prefix);
1802
1803    let mut maybe_decl_id = working_set.find_decl(&name);
1804
1805    while maybe_decl_id.is_none() {
1806        // Find the longest command match
1807        if name_spans.len() <= 1 {
1808            // Keep the first word even if it does not match -- could be external command
1809            break;
1810        }
1811
1812        name_spans.pop();
1813        pos -= 1;
1814
1815        name = command_name_from_spans(working_set, &name_spans, prefix);
1816        maybe_decl_id = working_set.find_decl(&name);
1817    }
1818
1819    // If there is a declaration and there are remaining spans, check if it's an alias.
1820    // If it is, try to see if there are sub commands
1821    if let Some(decl_id) = maybe_decl_id
1822        && pos < spans.len()
1823    {
1824        let decl = working_set.get_decl(decl_id);
1825        if let Some(alias) = decl.as_alias() {
1826            // Extract the command name from the alias
1827            // The wrapped_call should be a Call expression for internal commands
1828            if let Expression {
1829                expr: Expr::Call(call),
1830                ..
1831            } = &alias.wrapped_call
1832            {
1833                let aliased_decl_id = call.decl_id;
1834                let aliased_name = working_set.get_decl(aliased_decl_id).name().to_string();
1835
1836                // Try to find a longer match using the aliased command name with remaining spans
1837                let (_, new_pos, new_name, new_decl_id) = find_longest_decl_with_prefix(
1838                    working_set,
1839                    &spans[pos..],
1840                    aliased_name.as_bytes(),
1841                );
1842
1843                // If we find a sub command, use it instead.
1844                if new_decl_id.is_some() && new_pos > 0 {
1845                    let total_pos = pos + new_pos;
1846                    return (cmd_start, total_pos, new_name, new_decl_id);
1847                }
1848            }
1849        }
1850    }
1851
1852    (cmd_start, pos, name, maybe_decl_id)
1853}
1854
1855pub fn parse_attribute(
1856    working_set: &mut StateWorkingSet,
1857    lite_command: &LiteCommand,
1858) -> (Attribute, Option<String>) {
1859    let _ = lite_command
1860        .parts
1861        .first()
1862        .filter(|s| working_set.get_span_contents(**s).starts_with(b"@"))
1863        .expect("Attributes always start with an `@`");
1864
1865    assert!(
1866        lite_command.attribute_idx.is_empty(),
1867        "attributes can't have attributes"
1868    );
1869
1870    let mut spans = lite_command.parts.clone();
1871    if let Some(first) = spans.first_mut() {
1872        first.start += 1;
1873    }
1874    let spans = spans.as_slice();
1875    let attr_span = Span::concat(spans);
1876
1877    let (cmd_start, cmd_end, mut name, decl_id) =
1878        find_longest_decl_with_prefix(working_set, spans, b"attr");
1879
1880    debug_assert!(name.starts_with(b"attr "));
1881    let _ = name.drain(..(b"attr ".len()));
1882
1883    let name_span = Span::concat(&spans[cmd_start..cmd_end]);
1884
1885    let Ok(name) = String::from_utf8(name) else {
1886        working_set.error(ParseError::NonUtf8(name_span));
1887        return (
1888            Attribute {
1889                expr: garbage(working_set, attr_span),
1890            },
1891            None,
1892        );
1893    };
1894
1895    let Some(decl_id) = decl_id else {
1896        working_set.error(ParseError::UnknownCommand(name_span));
1897        return (
1898            Attribute {
1899                expr: garbage(working_set, attr_span),
1900            },
1901            None,
1902        );
1903    };
1904
1905    let decl = working_set.get_decl(decl_id);
1906
1907    let parsed_call = match decl.as_alias() {
1908        // TODO: Once `const def` is available, we should either disallow aliases as attributes OR
1909        // allow them but rather than using the aliases' name, use the name of the aliased command
1910        Some(alias) => match &alias.clone().wrapped_call {
1911            Expression {
1912                expr: Expr::ExternalCall(..),
1913                ..
1914            } => {
1915                let shell_error = ShellError::NotAConstCommand { span: name_span };
1916                working_set.error(shell_error.wrap(working_set, attr_span));
1917                return (
1918                    Attribute {
1919                        expr: garbage(working_set, Span::concat(spans)),
1920                    },
1921                    None,
1922                );
1923            }
1924            _ => {
1925                trace!("parsing: alias of internal call");
1926                parse_internal_call(
1927                    working_set,
1928                    name_span,
1929                    &spans[cmd_end..],
1930                    decl_id,
1931                    ArgumentParsingLevel::Full,
1932                    None,
1933                )
1934            }
1935        },
1936        None => {
1937            trace!("parsing: internal call");
1938            parse_internal_call(
1939                working_set,
1940                name_span,
1941                &spans[cmd_end..],
1942                decl_id,
1943                ArgumentParsingLevel::Full,
1944                None,
1945            )
1946        }
1947    };
1948
1949    (
1950        Attribute {
1951            expr: Expression::new(
1952                working_set,
1953                Expr::Call(parsed_call.call),
1954                Span::concat(spans),
1955                parsed_call.output,
1956            ),
1957        },
1958        Some(name),
1959    )
1960}