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        // Skip the leading "--" in the byte layer to avoid an extra allocation.
442        let long_name = String::from_utf8(split[0][2..].into());
443        if let Ok(long_name) = long_name {
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                // Move `long_name`; the clone was already consumed above.
528                LongFlagParseResult::FoundFlag(
529                    Spanned {
530                        item: long_name,
531                        span: arg_span,
532                    },
533                    None,
534                )
535            }
536        } else {
537            working_set.error(ParseError::NonUtf8(arg_span));
538            LongFlagParseResult::FoundFlag(
539                Spanned {
540                    item: "--".into(),
541                    span: arg_span,
542                },
543                None,
544            )
545        }
546    } else {
547        LongFlagParseResult::NoFlag
548    }
549}
550
551/// Check if a syntax shape can accept a negative number (for distinguishing short flags from
552/// negative numeric arguments like `-1`).
553fn shape_allows_negative_number(shape: &SyntaxShape) -> bool {
554    matches!(
555        shape,
556        SyntaxShape::Int | SyntaxShape::Number | SyntaxShape::Float
557    ) || matches!(shape, SyntaxShape::OneOf(shapes) if shapes.iter().any(shape_allows_negative_number))
558}
559
560fn parse_short_flags(
561    working_set: &mut StateWorkingSet,
562    spans: &[Span],
563    spans_idx: &mut usize,
564    positional_idx: usize,
565    sig: &Signature,
566) -> Option<Vec<Flag>> {
567    let arg_span = spans[*spans_idx];
568
569    let arg_contents = working_set.get_span_contents(arg_span);
570
571    if let Ok(arg_contents_uft8_ref) = str::from_utf8(arg_contents) {
572        if arg_contents_uft8_ref.starts_with('-') && arg_contents_uft8_ref.len() > 1 {
573            let short_flags = &arg_contents_uft8_ref[1..];
574            let num_chars = short_flags.chars().count();
575            let mut found_short_flags = vec![];
576            let mut unmatched_short_flags = vec![];
577            for (offset, short_flag) in short_flags.char_indices() {
578                let short_flag_span = Span::new(
579                    arg_span.start + 1 + offset,
580                    arg_span.start + 1 + offset + short_flag.len_utf8(),
581                );
582                if let Some(flag) = sig.get_short_flag(short_flag) {
583                    // Allow args in short flag batches as long as it is the last flag.
584                    if flag.arg.is_some() && offset < num_chars - 1 {
585                        working_set
586                            .error(ParseError::OnlyLastFlagInBatchCanTakeArg(short_flag_span));
587                        break;
588                    }
589                    found_short_flags.push(flag);
590                } else {
591                    unmatched_short_flags.push(short_flag_span);
592                }
593            }
594
595            if found_short_flags.is_empty()
596                // check to see if we have a negative number
597                && sig
598                    .get_positional(positional_idx)
599                    .is_some_and(|p| shape_allows_negative_number(&p.shape))
600                && String::from_utf8_lossy(working_set.get_span_contents(arg_span))
601                    .parse::<f64>()
602                    .is_ok()
603            {
604                return None;
605            } else if let Some(first) = unmatched_short_flags.first() {
606                let contents = working_set.get_span_contents(*first);
607                working_set.error(ParseError::UnknownFlag(
608                    sig.name.clone(),
609                    format!("-{}", String::from_utf8_lossy(contents)),
610                    *first,
611                    "Use `--help` to see available flags".to_owned(),
612                ));
613            }
614
615            Some(found_short_flags)
616        } else {
617            None
618        }
619    } else {
620        working_set.error(ParseError::NonUtf8(arg_span));
621        None
622    }
623}
624
625fn first_kw_idx(
626    working_set: &StateWorkingSet,
627    signature: &Signature,
628    spans: &[Span],
629    spans_idx: usize,
630    positional_idx: usize,
631) -> (Option<usize>, usize) {
632    for idx in (positional_idx + 1)..signature.num_positionals() {
633        if let Some(PositionalArg {
634            shape: SyntaxShape::Keyword(kw, ..),
635            ..
636        }) = signature.get_positional(idx)
637        {
638            for (span_idx, &span) in spans.iter().enumerate().skip(spans_idx) {
639                let contents = working_set.get_span_contents(span);
640
641                if contents == kw {
642                    return (Some(idx), span_idx);
643                }
644            }
645        }
646    }
647    (None, spans.len())
648}
649
650fn calculate_end_span(
651    working_set: &StateWorkingSet,
652    signature: &Signature,
653    spans: &[Span],
654    spans_idx: usize,
655    positional_idx: usize,
656) -> usize {
657    if signature.rest_positional.is_some() {
658        spans.len()
659    } else {
660        let (kw_pos, kw_idx) =
661            first_kw_idx(working_set, signature, spans, spans_idx, positional_idx);
662
663        if let Some(kw_pos) = kw_pos {
664            // We found a keyword. Keywords, once found, create a guidepost to
665            // show us where the positionals will lay into the arguments. Because they're
666            // keywords, they get to set this by being present
667
668            let positionals_between = kw_pos - positional_idx - 1;
669            if positionals_between >= (kw_idx - spans_idx) {
670                kw_idx
671            } else {
672                kw_idx - positionals_between
673            }
674        } else {
675            // Make space for the remaining require positionals, if we can
676            // spans_idx < spans.len() is an invariant
677            let remaining_spans = spans.len() - (spans_idx + 1);
678            // positional_idx can be larger than required_positional.len() if we have optional args
679            let remaining_positional = signature
680                .required_positional
681                .len()
682                .saturating_sub(positional_idx + 1);
683            // Saturates to 0 when we have too few args
684            let extra_spans = remaining_spans.saturating_sub(remaining_positional);
685            spans_idx + 1 + extra_spans
686        }
687    }
688}
689
690pub(crate) fn parse_oneof(
691    working_set: &mut StateWorkingSet,
692    spans: &[Span],
693    spans_idx: &mut usize,
694    possible_shapes: &Vec<SyntaxShape>,
695    multispan: bool,
696    input_type: Option<&Type>,
697) -> Expression {
698    let starting_spans_idx = *spans_idx;
699    let mut best_guess = None;
700    let mut best_guess_errors = Vec::new();
701    let mut max_first_error_offset = 0;
702    let mut propagate_error = false;
703    for shape in possible_shapes {
704        let starting_error_count = working_set.parse_errors.len();
705        *spans_idx = starting_spans_idx;
706        let value = match multispan {
707            true => parse_multispan_value(working_set, spans, spans_idx, shape, input_type),
708            false => crate::parser::parse_value(working_set, spans[*spans_idx], shape, input_type),
709        };
710
711        let new_errors = &working_set.parse_errors[starting_error_count..];
712        // no new errors found means success
713        let Some(first_error_offset) = new_errors.iter().map(|e| e.span().start).min() else {
714            return value;
715        };
716
717        if first_error_offset > max_first_error_offset {
718            // while trying the possible shapes, ignore Expected type errors
719            // unless they're inside a block, closure, or expression
720            propagate_error = match working_set.parse_errors.last() {
721                Some(ParseError::Expected(_, error_span))
722                | Some(ParseError::ExpectedWithStringMsg(_, error_span)) => {
723                    matches!(
724                        shape,
725                        SyntaxShape::Block | SyntaxShape::Closure(_) | SyntaxShape::Expression
726                    ) && *error_span != spans[*spans_idx]
727                }
728                _ => true,
729            };
730            max_first_error_offset = first_error_offset;
731            best_guess = Some(value);
732            best_guess_errors.clear();
733            best_guess_errors.extend_from_slice(new_errors);
734        }
735        working_set.parse_errors.truncate(starting_error_count);
736    }
737
738    // if best_guess results in new errors further than current span, then accept it
739    // or propagate_error is marked as true for it
740    if max_first_error_offset > spans[starting_spans_idx].start || propagate_error {
741        working_set.parse_errors.extend(best_guess_errors);
742        best_guess.expect("best_guess should not be None here!")
743    } else {
744        working_set.error(ParseError::ExpectedWithStringMsg(
745            format!("one of a list of accepted shapes: {possible_shapes:?}"),
746            spans[starting_spans_idx],
747        ));
748        Expression::garbage(working_set, spans[starting_spans_idx])
749    }
750}
751
752pub fn parse_multispan_value(
753    working_set: &mut StateWorkingSet,
754    spans: &[Span],
755    spans_idx: &mut usize,
756    shape: &SyntaxShape,
757    input_type: Option<&Type>,
758) -> Expression {
759    trace!("parse multispan value");
760    match shape {
761        SyntaxShape::VarWithOptType => {
762            trace!("parsing: var with opt type");
763
764            crate::parser::parse_var_with_opt_type(working_set, spans, spans_idx, false, input_type)
765                .0
766        }
767        SyntaxShape::RowCondition => {
768            trace!("parsing: row condition");
769            let arg = crate::parser::parse_row_condition(working_set, &spans[*spans_idx..]);
770            *spans_idx = spans.len() - 1;
771
772            arg
773        }
774        SyntaxShape::MathExpression => {
775            trace!("parsing: math expression");
776
777            let arg = crate::parser::parse_math_expression(
778                working_set,
779                &spans[*spans_idx..],
780                None,
781                input_type,
782            );
783            *spans_idx = spans.len() - 1;
784
785            arg
786        }
787        SyntaxShape::OneOf(possible_shapes) => parse_oneof(
788            working_set,
789            spans,
790            spans_idx,
791            possible_shapes,
792            true,
793            input_type,
794        ),
795
796        SyntaxShape::Expression => {
797            trace!("parsing: expression");
798
799            // is it subexpression?
800            // Not sure, but let's make it not, so the behavior is the same as previous version of nushell.
801            let arg =
802                crate::parser::parse_expression(working_set, &spans[*spans_idx..], input_type);
803            *spans_idx = spans.len().saturating_sub(1);
804
805            arg
806        }
807        SyntaxShape::Signature => {
808            trace!("parsing: signature");
809
810            let sig = crate::parser::parse_full_signature(working_set, &spans[*spans_idx..], false);
811            *spans_idx = spans.len().saturating_sub(1);
812
813            sig
814        }
815        SyntaxShape::ExternalSignature => {
816            trace!("parsing: external signature");
817
818            let sig = crate::parser::parse_full_signature(working_set, &spans[*spans_idx..], true);
819            *spans_idx = spans.len().saturating_sub(1);
820
821            sig
822        }
823        SyntaxShape::Keyword(keyword, arg) => {
824            trace!(
825                "parsing: keyword({}) {:?}",
826                String::from_utf8_lossy(keyword),
827                arg
828            );
829            let arg_span = spans[*spans_idx];
830
831            let arg_contents = working_set.get_span_contents(arg_span);
832
833            if arg_contents != keyword {
834                // When keywords mismatch, this is a strong indicator of something going wrong.
835                // We won't often override the current error, but as this is a strong indicator
836                // go ahead and override the current error and tell the user about the missing
837                // keyword/literal.
838                working_set.error(ParseError::ExpectedKeyword(
839                    String::from_utf8_lossy(keyword).into(),
840                    arg_span,
841                ))
842            }
843
844            *spans_idx += 1;
845            if *spans_idx >= spans.len() {
846                working_set.error(ParseError::KeywordMissingArgument(
847                    arg.to_string(),
848                    String::from_utf8_lossy(keyword).into(),
849                    Span::new(spans[*spans_idx - 1].end, spans[*spans_idx - 1].end),
850                ));
851                let keyword = Keyword {
852                    keyword: keyword.as_slice().into(),
853                    span: spans[*spans_idx - 1],
854                    expr: Expression::garbage(working_set, arg_span),
855                };
856                return Expression::new(
857                    working_set,
858                    Expr::Keyword(Box::new(keyword)),
859                    arg_span,
860                    Type::Any,
861                );
862            }
863
864            let keyword = Keyword {
865                keyword: keyword.as_slice().into(),
866                span: spans[*spans_idx - 1],
867                expr: parse_multispan_value(working_set, spans, spans_idx, arg, input_type),
868            };
869
870            // Extract fields before boxing so the whole Keyword tree can be moved in.
871            let kw_span = keyword.span;
872            let expr_span = keyword.expr.span;
873            let ty = keyword.expr.ty.clone();
874            Expression::new(
875                working_set,
876                Expr::Keyword(Box::new(keyword)),
877                kw_span.merge(expr_span),
878                ty,
879            )
880        }
881        _ => {
882            // All other cases are single-span values
883            let arg_span = spans[*spans_idx];
884
885            crate::parser::parse_value(working_set, arg_span, shape, input_type)
886        }
887    }
888}
889
890pub struct ParsedInternalCall {
891    pub call: Box<Call>,
892    pub output: Type,
893    pub call_kind: CallKind,
894}
895
896/// Sometimes the arguments of an internal command need to be parsed in dedicated functions, e.g. `parse_module`.
897/// If so, `parse_internal_call` should be called with the appropriate parsing level to avoid repetition.
898///
899/// Defaults to `ArgumentParsingLevel::Full`
900#[derive(Default)]
901pub enum ArgumentParsingLevel {
902    #[default]
903    Full,
904    /// Parse only the first `k` arguments
905    FirstK { k: usize },
906}
907
908/// Build a `Spanned<String>` for a short flag character at the given span.
909#[inline]
910fn short_spanned(short: char, span: Span) -> Spanned<String> {
911    Spanned {
912        item: short.to_string(),
913        span,
914    }
915}
916
917pub fn parse_internal_call(
918    working_set: &mut StateWorkingSet,
919    command_span: Span,
920    spans: &[Span],
921    decl_id: DeclId,
922    arg_parsing_level: ArgumentParsingLevel,
923    input_type: Option<&Type>,
924) -> ParsedInternalCall {
925    trace!("parsing: internal call (decl id: {})", decl_id.get());
926
927    let mut call = Call::new(command_span);
928    call.decl_id = decl_id;
929    call.head = command_span;
930    let _ = working_set.add_span(call.head);
931
932    let decl = working_set.get_decl(decl_id);
933    let signature = working_set.get_signature(decl);
934
935    enum SpecialCmd {
936        Let,
937        Def,
938        Match,
939        If,
940    }
941
942    impl SpecialCmd {
943        fn from_str(s: &str) -> Option<Self> {
944            Some(match s {
945                "let" => Self::Let,
946                "def" => Self::Def,
947                "match" => Self::Match,
948                "if" => Self::If,
949                _ => return None,
950            })
951        }
952    }
953
954    let special_cmd = decl
955        .is_keyword()
956        .then(|| decl.name())
957        .and_then(SpecialCmd::from_str);
958
959    // TODO: Throw an actual error here, instead of leaning on later type checking code
960    //
961    // `Type::Nothing` is added to inputs to allow uses like:
962    // `ls | sort-by { open -r $in.name | lines | length }`
963    // see https://github.com/nushell/nushell/pull/14922
964    // Incorrect behavior this may cause will be handled by
965    // `check_pipeline_type` in crates/nu-parser/src/type_check.rs
966    let output = signature
967        .get_output_type(
968            input_type
969                .map(|ty| ty.clone().union(Type::Nothing))
970                .as_ref(),
971        )
972        .unwrap_or(Type::Error);
973
974    // This is necessary for some keywords to have proper expression types.
975    // `2 | let x | let y`
976    // - `$x` should be `int`
977    // - `$y` should also be `int`, but for that `let x` as an expression must have the type `int`
978    let mut output_override = None;
979
980    let deprecation = decl.deprecation_info();
981
982    // storing the var ID for later due to borrowing issues
983    let lib_dirs_var_id = match decl.name() {
984        "use" | "overlay use" | "source-env" if decl.is_keyword() => {
985            find_dirs_var(working_set, crate::parse_source::LIB_DIRS_VAR)
986        }
987        "nu-check" if decl.is_builtin() => {
988            find_dirs_var(working_set, crate::parse_source::LIB_DIRS_VAR)
989        }
990        _ => None,
991    };
992
993    // The index into the positional parameter in the definition
994    let mut positional_idx = 0;
995
996    // The index into the spans of argument data given to parse
997    // Starting at the first argument
998    let mut spans_idx = 0;
999
1000    if let Some(alias) = decl.as_alias() {
1001        if let Expression {
1002            expr: Expr::Call(wrapped_call),
1003            ..
1004        } = &alias.wrapped_call
1005        {
1006            // Replace this command's call with the aliased call, but keep the alias name
1007            call = *wrapped_call.clone();
1008            call.head = command_span;
1009            // Skip positionals passed to aliased call
1010            positional_idx = call.positional_iter().count();
1011        } else {
1012            working_set.error(ParseError::UnknownState(
1013                "Alias does not point to internal call.".to_string(),
1014                command_span,
1015            ));
1016            return ParsedInternalCall {
1017                call: Box::new(call),
1018                output: Type::Any,
1019                call_kind: CallKind::Invalid,
1020            };
1021        }
1022    }
1023
1024    if let Some(var_id) = lib_dirs_var_id {
1025        call.set_parser_info(
1026            DIR_VAR_PARSER_INFO.to_owned(),
1027            Expression::new(working_set, Expr::Var(var_id), call.head, Type::Any),
1028        );
1029    }
1030
1031    if signature.creates_scope {
1032        working_set.enter_scope();
1033    }
1034
1035    let mut end_of_options = false;
1036
1037    while spans_idx < spans.len() {
1038        let arg_span = spans[spans_idx];
1039
1040        let starting_error_count = working_set.parse_errors.len();
1041
1042        // If we've seen --, skip all flag parsing and go straight to positional parsing
1043        if !end_of_options {
1044            // Check if we're on a long flag, if so, parse
1045            let flag_parse_result = parse_long_flag(working_set, spans, &mut spans_idx, &signature);
1046
1047            match flag_parse_result {
1048                LongFlagParseResult::EndOfOptions => {
1049                    // Switch to positional-only mode so subsequent flags aren't parsed.
1050                    end_of_options = true;
1051
1052                    if signature.allows_unknown_args {
1053                        // For commands that pass through unknown args (extern, def --wrapped,
1054                        // exec, etc.), -- itself must be forwarded to the underlying program.
1055                        let arg = parse_unknown_arg(working_set, arg_span, &signature);
1056                        call.add_unknown(arg);
1057                    }
1058
1059                    spans_idx += 1;
1060                    continue;
1061                }
1062                LongFlagParseResult::FoundFlag(long_name, arg) => {
1063                    // We found a long flag, like --bar
1064                    if working_set.parse_errors[starting_error_count..]
1065                        .iter()
1066                        .any(|x| matches!(x, ParseError::UnknownFlag(_, _, _, _)))
1067                        && signature.allows_unknown_args
1068                    {
1069                        working_set.parse_errors.truncate(starting_error_count);
1070                        let arg = parse_unknown_arg(working_set, arg_span, &signature);
1071
1072                        call.add_unknown(arg);
1073                    } else {
1074                        call.add_named((long_name, None, arg));
1075                    }
1076
1077                    spans_idx += 1;
1078                    continue;
1079                }
1080                LongFlagParseResult::NoFlag => {
1081                    // No long flag found, continue to short flag parsing
1082                }
1083            }
1084        }
1085
1086        // Only try short flag parsing if we haven't seen -- yet
1087        if !end_of_options {
1088            let starting_error_count = working_set.parse_errors.len();
1089
1090            // Check if we're on a short flag or group of short flags, if so, parse
1091            let short_flags = parse_short_flags(
1092                working_set,
1093                spans,
1094                &mut spans_idx,
1095                positional_idx,
1096                &signature,
1097            );
1098
1099            if let Some(short_flags) = short_flags {
1100                if working_set.parse_errors[starting_error_count..]
1101                    .iter()
1102                    .any(|x| matches!(x, ParseError::UnknownFlag(_, _, _, _)))
1103                    && signature.allows_unknown_args
1104                {
1105                    working_set.parse_errors.truncate(starting_error_count);
1106                    let arg = parse_unknown_arg(working_set, arg_span, &signature);
1107
1108                    call.add_unknown(arg);
1109                } else {
1110                    for flag in short_flags {
1111                        let _ = working_set.add_span(spans[spans_idx]);
1112
1113                        if let Some(arg_shape) = flag.arg {
1114                            if let Some(arg) = spans.get(spans_idx + 1) {
1115                                let arg =
1116                                    crate::parser::parse_value(working_set, *arg, &arg_shape, None);
1117                                let (arg_name, val_expression) = ensure_flag_arg_type(
1118                                    working_set,
1119                                    flag.long.clone(),
1120                                    arg.clone(),
1121                                    &arg_shape,
1122                                    spans[spans_idx],
1123                                );
1124
1125                                if flag.long.is_empty() {
1126                                    if let Some(short) = flag.short {
1127                                        call.add_named((
1128                                            arg_name,
1129                                            Some(short_spanned(short, spans[spans_idx])),
1130                                            Some(val_expression),
1131                                        ));
1132                                    }
1133                                } else {
1134                                    call.add_named((arg_name, None, Some(val_expression)));
1135                                }
1136                                spans_idx += 1;
1137                            } else {
1138                                working_set.error(ParseError::MissingFlagParam(
1139                                    arg_shape.to_string(),
1140                                    arg_span,
1141                                ));
1142                                // NOTE: still need to cover this incomplete flag in the final expression
1143                                // see https://github.com/nushell/nushell/issues/16375
1144                                // Preserve the flag's identity so completion can tell
1145                                // which flag is still awaiting a value.
1146                                call.add_named((
1147                                    Spanned {
1148                                        item: flag.long.clone(),
1149                                        span: spans[spans_idx],
1150                                    },
1151                                    flag.short
1152                                        .map(|short| short_spanned(short, spans[spans_idx])),
1153                                    None,
1154                                ));
1155                            }
1156                        } else if flag.long.is_empty() {
1157                            if let Some(short) = flag.short {
1158                                call.add_named((
1159                                    Spanned {
1160                                        item: String::new(),
1161                                        span: spans[spans_idx],
1162                                    },
1163                                    Some(short_spanned(short, spans[spans_idx])),
1164                                    None,
1165                                ));
1166                            }
1167                        } else {
1168                            call.add_named((
1169                                Spanned {
1170                                    item: flag.long.clone(),
1171                                    span: spans[spans_idx],
1172                                },
1173                                None,
1174                                None,
1175                            ));
1176                        }
1177                    }
1178                }
1179
1180                spans_idx += 1;
1181                continue;
1182            }
1183        } // end if !end_of_options (short flags)
1184
1185        {
1186            let contents = working_set.get_span_contents(spans[spans_idx]);
1187
1188            if let Some(Spanned {
1189                span: spread_arg_span,
1190                ..
1191            }) = extract_spread_list(contents.into_spanned(spans[spans_idx]))
1192            {
1193                if signature.rest_positional.is_none() && !signature.allows_unknown_args {
1194                    working_set.error(ParseError::UnexpectedSpreadArg(
1195                        signature.call_signature(),
1196                        arg_span,
1197                    ));
1198                    call.add_positional(Expression::garbage(working_set, arg_span));
1199                } else if positional_idx < signature.required_positional.len() {
1200                    working_set.error(ParseError::MissingPositional(
1201                        signature.required_positional[positional_idx].name.clone(),
1202                        Span::new(spans[spans_idx].start, spans[spans_idx].start),
1203                        signature.call_signature(),
1204                    ));
1205                    call.add_positional(Expression::garbage(working_set, arg_span));
1206                } else {
1207                    let rest_shape = match &signature.rest_positional {
1208                        Some(arg) if matches!(arg.shape, SyntaxShape::ExternalArgument) => {
1209                            // External args aren't parsed inside lists in spread position.
1210                            SyntaxShape::Any
1211                        }
1212                        Some(arg) => arg.shape.clone(),
1213                        None => SyntaxShape::Any,
1214                    };
1215                    // Parse list of arguments to be spread
1216                    let args = crate::parser::parse_value(
1217                        working_set,
1218                        spread_arg_span,
1219                        &SyntaxShape::List(Box::new(rest_shape)),
1220                        None,
1221                    );
1222
1223                    call.add_spread(args);
1224                    // Let the parser know that it's parsing rest arguments now
1225                    positional_idx =
1226                        signature.required_positional.len() + signature.optional_positional.len();
1227                }
1228
1229                spans_idx += 1;
1230                continue;
1231            }
1232        }
1233
1234        // Parse a positional arg if there is one
1235        if let Some(positional) = signature.get_positional(positional_idx) {
1236            let end = calculate_end_span(working_set, &signature, spans, spans_idx, positional_idx);
1237
1238            // Missing arguments before next keyword
1239            if end == spans_idx {
1240                let prev_span = if spans_idx == 0 {
1241                    command_span
1242                } else {
1243                    spans[spans_idx - 1]
1244                };
1245                let whitespace_span = Span::new(prev_span.end, spans[spans_idx].start);
1246                working_set.error(ParseError::MissingPositional(
1247                    positional.name.clone(),
1248                    whitespace_span,
1249                    signature.call_signature(),
1250                ));
1251                call.add_positional(Expression::garbage(working_set, whitespace_span));
1252                positional_idx += 1;
1253                continue;
1254            }
1255            debug_assert!(end <= spans.len());
1256
1257            if spans[..end].is_empty() || spans_idx == end {
1258                working_set.error(ParseError::MissingPositional(
1259                    positional.name.clone(),
1260                    Span::new(spans[spans_idx].end, spans[spans_idx].end),
1261                    signature.call_signature(),
1262                ));
1263                positional_idx += 1;
1264                continue;
1265            }
1266
1267            let compile_error_count = working_set.compile_errors.len();
1268
1269            // HACK: avoid repeated parsing of argument values in special cases
1270            // see https://github.com/nushell/nushell/issues/16398
1271            let arg = match arg_parsing_level {
1272                ArgumentParsingLevel::FirstK { k } if k <= positional_idx => {
1273                    Expression::garbage(working_set, spans[spans_idx])
1274                }
1275                _ => {
1276                    let input_type: Option<Type> = match special_cmd {
1277                        // `let` can assigned from pipeline input, input type is necessary to infer
1278                        // the variable's type correctly
1279                        Some(SpecialCmd::Let)
1280                            if let SyntaxShape::VarWithOptType = &positional.shape =>
1281                        {
1282                            output_override = input_type.cloned();
1283                            input_type.cloned()
1284                        }
1285                        // in a def block, the pipeline input type should be inferred based on the
1286                        // command input-output signature
1287                        Some(SpecialCmd::Def) if &positional.name == "block" => {
1288                            // if we're parsing the `block`, the previous item is the signature
1289                            match call.arguments.last() {
1290                                Some(Argument::Positional(Expression {
1291                                    expr: Expr::Signature(sig),
1292                                    ..
1293                                })) => Some(sig.get_input_type()),
1294                                _ => None,
1295                            }
1296                        }
1297                        Some(SpecialCmd::If) if positional_idx >= 1 => input_type.cloned(),
1298                        Some(SpecialCmd::Match) if &positional.name == "match_block" => {
1299                            input_type.cloned()
1300                        }
1301                        _ => None,
1302                    };
1303
1304                    // HACK: `def` block parameter is of type `closure`, which is wrong.
1305                    // However, that's used to make sure `def` blocks don't capture mutable
1306                    // variables. (Which is also a HACK)
1307                    //
1308                    // Closure bodies do not get pipeline input type, but `def` bodies should.
1309                    // Thus, we work around the mentioned hack with another one here.
1310                    //
1311                    // This is of course unideal, but it's the way to go to fix the issue without a
1312                    // big refactor, which could neither be done in time for the 0.114.1 patch
1313                    // release, nor would it be appropriate to include in a patch release.
1314                    let expr = match special_cmd {
1315                        Some(SpecialCmd::Def) if &positional.name == "block" => {
1316                            let starting_error_count = working_set.parse_errors.len();
1317
1318                            let out = crate::parse_expressions::parse_closure_expression(
1319                                working_set,
1320                                &positional.shape,
1321                                spans[spans_idx],
1322                                input_type.as_ref(),
1323                            );
1324
1325                            if let Expr::Closure(_) = out.expr {
1326                                out
1327                            } else {
1328                                // on failure, we fallback to the normal code path to keep errors
1329                                // the same as before this hack
1330                                working_set.parse_errors.truncate(starting_error_count);
1331                                parse_multispan_value(
1332                                    working_set,
1333                                    &spans[..end],
1334                                    &mut spans_idx,
1335                                    &positional.shape,
1336                                    input_type.as_ref(),
1337                                )
1338                            }
1339                        }
1340                        _ => parse_multispan_value(
1341                            working_set,
1342                            &spans[..end],
1343                            &mut spans_idx,
1344                            &positional.shape,
1345                            input_type.as_ref(),
1346                        ),
1347                    };
1348
1349                    match special_cmd {
1350                        Some(SpecialCmd::Match) if &positional.name == "match_block" => {
1351                            output_override = Some(expr.ty.clone());
1352                        }
1353                        Some(SpecialCmd::If)
1354                            if positional_idx == 1
1355                                && let Expr::Block(block_id) = &expr.expr =>
1356                        {
1357                            let block = working_set.get_block(*block_id);
1358                            let ty = match block.pipelines.is_empty() {
1359                                false => block.output_type(),
1360                                true => input_type.unwrap_or(Type::Any),
1361                            };
1362
1363                            output_override = Some(match output_override {
1364                                Some(existing_ty) => existing_ty.union(ty),
1365                                None => ty,
1366                            });
1367                        }
1368                        Some(SpecialCmd::If)
1369                            if positional_idx == 2
1370                                && let Expr::Keyword(kw) = &expr.expr =>
1371                        {
1372                            let ty = match &kw.expr.expr {
1373                                Expr::Block(block_id) => {
1374                                    let block = working_set.get_block(*block_id);
1375                                    match block.pipelines.is_empty() {
1376                                        false => block.output_type(),
1377                                        true => input_type.unwrap_or(Type::Any),
1378                                    }
1379                                }
1380                                _ => kw.expr.ty.clone(),
1381                            };
1382
1383                            output_override = Some(match output_override {
1384                                Some(existing_ty) => existing_ty.union(ty),
1385                                None => ty,
1386                            });
1387                        }
1388                        _ => {}
1389                    };
1390
1391                    expr
1392                }
1393            };
1394
1395            // HACK: try-catch's signature defines the catch block as a Closure, even though it's
1396            // used like a Block. Because closures are compiled eagerly, this ends up making the
1397            // following code technically invalid:
1398            // ```nu
1399            // loop { try { } catch {|e| break } }
1400            // ```
1401            // Thus, we discard the compilation error here
1402            if let SyntaxShape::OneOf(ref shapes) = positional.shape {
1403                for one_shape in shapes {
1404                    if let SyntaxShape::Keyword(keyword, ..) = one_shape
1405                        && keyword == b"catch"
1406                        && let [nu_protocol::CompileError::NotInALoop { .. }] =
1407                            &working_set.compile_errors[compile_error_count..]
1408                    {
1409                        working_set.compile_errors.truncate(compile_error_count);
1410                    }
1411                }
1412            }
1413
1414            let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) {
1415                working_set.error(ParseError::TypeMismatch(
1416                    positional.shape.to_type(),
1417                    arg.ty,
1418                    arg.span,
1419                ));
1420                Expression::garbage(working_set, arg.span)
1421            } else {
1422                arg
1423            };
1424
1425            call.add_positional(arg);
1426            positional_idx += 1;
1427        } else if signature.allows_unknown_args {
1428            let arg = parse_unknown_arg(working_set, arg_span, &signature);
1429
1430            call.add_unknown(arg);
1431        } else {
1432            call.add_positional(Expression::garbage(working_set, arg_span));
1433            working_set.error(ParseError::ExtraPositional(
1434                signature.call_signature(),
1435                arg_span,
1436            ))
1437        }
1438
1439        spans_idx += 1;
1440    }
1441
1442    // TODO: Inline `check_call`,
1443    // move missing positional checking into the while loop above with two pointers.
1444    // Maybe more `CallKind::Invalid` if errors found during argument parsing.
1445    let call_kind = check_call(working_set, command_span, &signature, &call);
1446
1447    deprecation
1448        .into_iter()
1449        .filter_map(|entry| entry.parse_warning(&signature.name, &call))
1450        .for_each(|warning| {
1451            // FIXME: if two flags are deprecated and both are used in one command,
1452            // the second flag's deprecation won't show until the first flag is removed
1453            // (but it won't be flagged as reported until it is actually reported)
1454            working_set.warning(warning);
1455        });
1456
1457    if signature.creates_scope {
1458        working_set.exit_scope();
1459    }
1460
1461    match special_cmd {
1462        // Not having an else branch means the output can be `nothing`
1463        Some(SpecialCmd::If) if call.arguments.len() < 3 => {
1464            output_override = output_override.map(|ty| ty.union(Type::Nothing))
1465        }
1466        _ => {}
1467    }
1468
1469    let output = output_override.unwrap_or(output);
1470
1471    ParsedInternalCall {
1472        call: Box::new(call),
1473        output,
1474        call_kind,
1475    }
1476}
1477
1478pub fn parse_call(
1479    working_set: &mut StateWorkingSet,
1480    spans: &[Span],
1481    head: Span,
1482    input_type: Option<&Type>,
1483) -> Expression {
1484    trace!("parsing: call");
1485    let call_span = Span::concat(spans);
1486
1487    if spans.is_empty() {
1488        working_set.error(ParseError::UnknownState(
1489            "Encountered command with zero spans".into(),
1490            call_span,
1491        ));
1492        return garbage(working_set, head);
1493    }
1494
1495    let call_sigil = match working_set.get_span_contents(spans[0]).first() {
1496        Some(b'^') => Some(b'^'),
1497        Some(b'%') => Some(b'%'),
1498        _ => None,
1499    };
1500
1501    let mut adjusted_spans = Vec::new();
1502    let resolution_spans = match call_sigil {
1503        Some(b'^') | Some(b'%') => {
1504            adjusted_spans.reserve(spans.len());
1505            adjusted_spans.push(Span::new(spans[0].start + 1, spans[0].end));
1506            adjusted_spans.extend_from_slice(&spans[1..]);
1507            adjusted_spans.as_slice()
1508        }
1509        _ => spans,
1510    };
1511
1512    // `^` always forces external command parsing and must bypass declaration
1513    // resolution, even when an internal command with the same name exists.
1514    if call_sigil == Some(b'^') {
1515        trace!("parsing: forced external call");
1516        return parse_external_call(working_set, resolution_spans, call_span);
1517    }
1518
1519    // Check if we have a percent sigil with a dynamic head (variable or expression).
1520    // Supports two token layouts:
1521    //   - single token: `%$cmd` or `%($cmd)` — stripping `%` leaves `$cmd` / `($cmd)` in [0]
1522    //   - two tokens:   `%` and `($cmd)`    — stripping `%` leaves an empty span in [0]; head is [1]
1523    // If so, defer builtin validation to runtime (the IR compiler will rewrite to `run-internal`).
1524    if call_sigil == Some(b'%') && !resolution_spans.is_empty() {
1525        // Locate the actual head span, skipping an empty leading span.
1526        let (head_idx, head_span) = {
1527            let first = working_set.get_span_contents(resolution_spans[0]);
1528            if first.is_empty() && resolution_spans.len() > 1 {
1529                (1, resolution_spans[1])
1530            } else {
1531                (0, resolution_spans[0])
1532            }
1533        };
1534
1535        let dynamic_head_contents = working_set.get_span_contents(head_span);
1536        let is_dynamic_head = !dynamic_head_contents.is_empty()
1537            && (dynamic_head_contents[0] == b'$' || dynamic_head_contents[0] == b'(');
1538
1539        if is_dynamic_head {
1540            trace!("parsing: dynamic percent builtin dispatch");
1541
1542            let head_expr = crate::parser::parse_expression(working_set, &[head_span], input_type);
1543
1544            // Create a placeholder call; the IR compiler will rewrite this to `run-internal`.
1545            let mut call = Call::new(call_span);
1546            call.decl_id = DeclId::new(0);
1547
1548            // Store the head expression for the IR compiler to pick up.
1549            call.set_parser_info(PERCENT_FORCED_BUILTIN_PARSER_INFO.to_string(), head_expr);
1550
1551            // Mirror the dynamic external-call path by preserving `...expr` as an explicit spread
1552            // argument so runtime dispatch can forward it without flattening first.
1553            for arg_span in resolution_spans.iter().skip(head_idx + 1) {
1554                let contents = working_set.get_span_contents(*arg_span);
1555                if let Some(Spanned { span: arg_span, .. }) =
1556                    extract_spread_list(contents.into_spanned(*arg_span))
1557                {
1558                    let spread_expr = crate::parser::parse_value(
1559                        working_set,
1560                        arg_span,
1561                        &SyntaxShape::List(Box::new(SyntaxShape::Any)),
1562                        None,
1563                    );
1564                    call.arguments.push(Argument::Spread(spread_expr));
1565                } else {
1566                    let arg_expr =
1567                        crate::parser::parse_value(working_set, *arg_span, &SyntaxShape::Any, None);
1568                    call.arguments.push(Argument::Positional(arg_expr));
1569                }
1570            }
1571
1572            return Expression::new(
1573                working_set,
1574                Expr::Call(Box::new(call)),
1575                call_span,
1576                Type::Any,
1577            );
1578        }
1579    }
1580
1581    let (cmd_start, pos, _name, maybe_decl_id) = if call_sigil == Some(b'%') {
1582        find_longest_decl_with_command_type(working_set, resolution_spans, CommandType::Builtin)
1583    } else {
1584        find_longest_decl(working_set, resolution_spans)
1585    };
1586
1587    if let Some(decl_id) = maybe_decl_id {
1588        // Before the internal parsing we check if there is no let or alias declarations
1589        // that are missing their name, e.g.: let = 1 or alias = 2
1590        if resolution_spans.len() > 1 {
1591            let test_equal = working_set.get_span_contents(resolution_spans[1]);
1592
1593            if test_equal == *b"=" {
1594                trace!("incomplete statement");
1595
1596                working_set.error(ParseError::UnknownState(
1597                    "Incomplete statement".into(),
1598                    call_span,
1599                ));
1600                return garbage(working_set, call_span);
1601            }
1602        }
1603
1604        let decl = working_set.get_decl(decl_id);
1605
1606        let parsed_call = if let Some(alias) = decl.as_alias() {
1607            if let Expression {
1608                expr: Expr::ExternalCall(head, args),
1609                span: _,
1610                span_id: _,
1611                ty,
1612            } = &alias.clone().wrapped_call
1613            {
1614                trace!("parsing: alias of external call");
1615
1616                let mut head = head.clone();
1617                head.span = Span::concat(&resolution_spans[cmd_start..pos]); // replacing the spans preserves syntax highlighting
1618
1619                let mut final_args = args.clone().into_vec();
1620                for arg_span in &resolution_spans[pos..] {
1621                    let arg = parse_external_arg(working_set, *arg_span);
1622                    final_args.push(arg);
1623                }
1624
1625                let expression = Expression::new(
1626                    working_set,
1627                    Expr::ExternalCall(head, final_args.into()),
1628                    Span::concat(spans),
1629                    ty.clone(),
1630                );
1631
1632                return expression;
1633            } else {
1634                trace!("parsing: alias of internal call");
1635                parse_internal_call(
1636                    working_set,
1637                    Span::concat(&resolution_spans[cmd_start..pos]),
1638                    &resolution_spans[pos..],
1639                    decl_id,
1640                    ArgumentParsingLevel::Full,
1641                    input_type,
1642                )
1643            }
1644        } else {
1645            trace!("parsing: internal call");
1646            parse_internal_call(
1647                working_set,
1648                Span::concat(&resolution_spans[cmd_start..pos]),
1649                &resolution_spans[pos..],
1650                decl_id,
1651                ArgumentParsingLevel::Full,
1652                input_type,
1653            )
1654        };
1655
1656        Expression::new(
1657            working_set,
1658            Expr::Call(parsed_call.call),
1659            call_span,
1660            parsed_call.output,
1661        )
1662    } else {
1663        if call_sigil == Some(b'%') {
1664            working_set.error(ParseError::LabeledErrorWithHelp {
1665                error: "percent sigil requires a built-in command".into(),
1666                label: "unknown built-in command".into(),
1667                help:
1668                    "remove `%` to use normal resolution, or use `^` to run an external command explicitly".into(),
1669                span: resolution_spans[0],
1670            });
1671
1672            // Preserve expression shape for completion while retaining the parse error.
1673            // Use the sigil-stripped spans so the head span excludes the `%`, matching `^`.
1674            return parse_external_call(working_set, resolution_spans, call_span);
1675        }
1676
1677        // We might be parsing left-unbounded range ("..10")
1678        let bytes = working_set.get_span_contents(spans[0]);
1679        trace!("parsing: range {bytes:?}");
1680        if let (Some(b'.'), Some(b'.')) = (bytes.first(), bytes.get(1)) {
1681            trace!("-- found leading range indicator");
1682            let starting_error_count = working_set.parse_errors.len();
1683
1684            if let Some(range_expr) = crate::parser::parse_range(working_set, spans[0]) {
1685                trace!("-- successfully parsed range");
1686                return range_expr;
1687            }
1688            working_set.parse_errors.truncate(starting_error_count);
1689        }
1690        trace!("parsing: external call");
1691
1692        // Otherwise, try external command
1693        parse_external_call(working_set, spans, call_span)
1694    }
1695}
1696
1697fn find_decl_with_command_type(
1698    working_set: &StateWorkingSet<'_>,
1699    name: &[u8],
1700    command_type: CommandType,
1701) -> Option<DeclId> {
1702    // Search all known declarations so `%cmd` can still resolve a built-in even when
1703    // a custom command with the same name shadows it in normal visibility lookup.
1704    for idx in (0..working_set.num_decls()).rev() {
1705        let decl_id = DeclId::new(idx);
1706        let decl = working_set.get_decl(decl_id);
1707        if decl.command_type() == command_type && decl.name().as_bytes() == name {
1708            return Some(decl_id);
1709        }
1710    }
1711
1712    None
1713}
1714
1715fn command_name_from_spans(
1716    working_set: &StateWorkingSet<'_>,
1717    spans: &[Span],
1718    prefix: &[u8],
1719) -> Vec<u8> {
1720    let mut name = Vec::with_capacity(prefix.len() + spans.len() * 2);
1721    name.extend(prefix);
1722
1723    for span in spans {
1724        let name_part = working_set.get_span_contents(*span);
1725        if name.is_empty() {
1726            name.extend(name_part);
1727        } else {
1728            name.push(b' ');
1729            name.extend(name_part);
1730        }
1731    }
1732
1733    name
1734}
1735
1736fn find_longest_decl_with_command_type(
1737    working_set: &StateWorkingSet<'_>,
1738    spans: &[Span],
1739    command_type: CommandType,
1740) -> (
1741    usize,
1742    usize,
1743    Vec<u8>,
1744    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1745) {
1746    let mut pos = spans.len();
1747    let cmd_start = 0;
1748    let mut name_spans = spans.to_vec();
1749
1750    let mut name = command_name_from_spans(working_set, &name_spans, b"");
1751
1752    let mut maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1753
1754    while maybe_decl_id.is_none() {
1755        if name_spans.len() <= 1 {
1756            break;
1757        }
1758
1759        name_spans.pop();
1760        pos -= 1;
1761
1762        name = command_name_from_spans(working_set, &name_spans, b"");
1763
1764        maybe_decl_id = find_decl_with_command_type(working_set, &name, command_type);
1765    }
1766
1767    (cmd_start, pos, name, maybe_decl_id)
1768}
1769
1770pub fn find_longest_decl(
1771    working_set: &mut StateWorkingSet<'_>,
1772    spans: &[Span],
1773) -> (
1774    usize,
1775    usize,
1776    Vec<u8>,
1777    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1778) {
1779    find_longest_decl_with_prefix(working_set, spans, b"")
1780}
1781
1782pub fn find_longest_decl_with_prefix(
1783    working_set: &mut StateWorkingSet<'_>,
1784    spans: &[Span],
1785    prefix: &[u8],
1786) -> (
1787    usize,
1788    usize,
1789    Vec<u8>,
1790    Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
1791) {
1792    let mut pos = 0;
1793    let cmd_start = pos;
1794    let mut name_spans = vec![];
1795
1796    for word_span in spans[cmd_start..].iter() {
1797        // Find the longest group of words that could form a command
1798
1799        name_spans.push(*word_span);
1800
1801        pos += 1;
1802    }
1803
1804    let mut name = command_name_from_spans(working_set, &name_spans, prefix);
1805
1806    let mut maybe_decl_id = working_set.find_decl(&name);
1807
1808    while maybe_decl_id.is_none() {
1809        // Find the longest command match
1810        if name_spans.len() <= 1 {
1811            // Keep the first word even if it does not match -- could be external command
1812            break;
1813        }
1814
1815        name_spans.pop();
1816        pos -= 1;
1817
1818        name = command_name_from_spans(working_set, &name_spans, prefix);
1819        maybe_decl_id = working_set.find_decl(&name);
1820    }
1821
1822    // If there is a declaration and there are remaining spans, check if it's an alias.
1823    // If it is, try to see if there are sub commands
1824    if let Some(decl_id) = maybe_decl_id
1825        && pos < spans.len()
1826    {
1827        let decl = working_set.get_decl(decl_id);
1828        if let Some(alias) = decl.as_alias() {
1829            // Extract the command name from the alias
1830            // The wrapped_call should be a Call expression for internal commands
1831            if let Expression {
1832                expr: Expr::Call(call),
1833                ..
1834            } = &alias.wrapped_call
1835            {
1836                let aliased_decl_id = call.decl_id;
1837                let aliased_name = working_set.get_decl(aliased_decl_id).name().to_string();
1838
1839                // Try to find a longer match using the aliased command name with remaining spans
1840                let (_, new_pos, new_name, new_decl_id) = find_longest_decl_with_prefix(
1841                    working_set,
1842                    &spans[pos..],
1843                    aliased_name.as_bytes(),
1844                );
1845
1846                // If we find a sub command, use it instead.
1847                if new_decl_id.is_some() && new_pos > 0 {
1848                    let total_pos = pos + new_pos;
1849                    return (cmd_start, total_pos, new_name, new_decl_id);
1850                }
1851            }
1852        }
1853    }
1854
1855    (cmd_start, pos, name, maybe_decl_id)
1856}
1857
1858/// Re-parse a command head that greedily matched a multi-word subcommand as its next
1859/// shorter reading, demoting the trailing words to arguments over the real buffer spans.
1860///
1861/// `head` must be resolvable in `working_set`; returns `None` for single-word heads.
1862pub fn parse_shorter_head_reading(
1863    working_set: &mut StateWorkingSet,
1864    head: Span,
1865    input_type: Option<&Type>,
1866) -> Option<Expression> {
1867    let contents = working_set.get_span_contents(head).to_vec();
1868    let (tokens, _) = crate::lex::lex(&contents, head.start, &[], &[], true);
1869    let spans: Vec<Span> = tokens.into_iter().map(|token| token.span).collect();
1870
1871    // Only multi-word heads have a shorter reading.
1872    let (_, greedy_pos, _, _) = find_longest_decl(working_set, &spans);
1873    if greedy_pos <= 1 {
1874        return None;
1875    }
1876
1877    // Longest proper-prefix command; everything past it becomes arguments.
1878    let call_span = Span::concat(&spans);
1879    let (cmd_start, pos, _, maybe_decl_id) =
1880        find_longest_decl(working_set, &spans[..greedy_pos - 1]);
1881
1882    let expression = match maybe_decl_id {
1883        Some(decl_id) => {
1884            let parsed = parse_internal_call(
1885                working_set,
1886                Span::concat(&spans[cmd_start..pos]),
1887                &spans[pos..],
1888                decl_id,
1889                ArgumentParsingLevel::Full,
1890                input_type,
1891            );
1892            Expression::new(
1893                working_set,
1894                Expr::Call(parsed.call),
1895                call_span,
1896                parsed.output,
1897            )
1898        }
1899        // Otherwise the shorter reading is an external call.
1900        None => parse_external_call(working_set, &spans, call_span),
1901    };
1902
1903    Some(expression)
1904}
1905
1906pub fn parse_attribute(
1907    working_set: &mut StateWorkingSet,
1908    lite_command: &LiteCommand,
1909) -> (Attribute, Option<String>) {
1910    let _ = lite_command
1911        .parts
1912        .first()
1913        .filter(|s| working_set.get_span_contents(**s).starts_with(b"@"))
1914        .expect("Attributes always start with an `@`");
1915
1916    assert!(
1917        lite_command.attribute_idx.is_empty(),
1918        "attributes can't have attributes"
1919    );
1920
1921    let mut spans = lite_command.parts.clone();
1922    if let Some(first) = spans.first_mut() {
1923        first.start += 1;
1924    }
1925    let spans = spans.as_slice();
1926    let attr_span = Span::concat(spans);
1927
1928    let (cmd_start, cmd_end, mut name, decl_id) =
1929        find_longest_decl_with_prefix(working_set, spans, b"attr");
1930
1931    debug_assert!(name.starts_with(b"attr "));
1932    let _ = name.drain(..(b"attr ".len()));
1933
1934    let name_span = Span::concat(&spans[cmd_start..cmd_end]);
1935
1936    let Ok(name) = String::from_utf8(name) else {
1937        working_set.error(ParseError::NonUtf8(name_span));
1938        return (
1939            Attribute {
1940                expr: garbage(working_set, attr_span),
1941            },
1942            None,
1943        );
1944    };
1945
1946    let Some(decl_id) = decl_id else {
1947        working_set.error(ParseError::UnknownCommand(name_span));
1948        return (
1949            Attribute {
1950                expr: garbage(working_set, attr_span),
1951            },
1952            None,
1953        );
1954    };
1955
1956    let decl = working_set.get_decl(decl_id);
1957
1958    let parsed_call = match decl.as_alias() {
1959        // TODO: Once `const def` is available, we should either disallow aliases as attributes OR
1960        // allow them but rather than using the aliases' name, use the name of the aliased command
1961        Some(alias) => match &alias.clone().wrapped_call {
1962            Expression {
1963                expr: Expr::ExternalCall(..),
1964                ..
1965            } => {
1966                let shell_error = ShellError::NotAConstCommand { span: name_span };
1967                working_set.error(shell_error.wrap(working_set, attr_span));
1968                return (
1969                    Attribute {
1970                        expr: garbage(working_set, Span::concat(spans)),
1971                    },
1972                    None,
1973                );
1974            }
1975            _ => {
1976                trace!("parsing: alias of internal call");
1977                parse_internal_call(
1978                    working_set,
1979                    name_span,
1980                    &spans[cmd_end..],
1981                    decl_id,
1982                    ArgumentParsingLevel::Full,
1983                    None,
1984                )
1985            }
1986        },
1987        None => {
1988            trace!("parsing: internal call");
1989            parse_internal_call(
1990                working_set,
1991                name_span,
1992                &spans[cmd_end..],
1993                decl_id,
1994                ArgumentParsingLevel::Full,
1995                None,
1996            )
1997        }
1998    };
1999
2000    (
2001        Attribute {
2002            expr: Expression::new(
2003                working_set,
2004                Expr::Call(parsed_call.call),
2005                Span::concat(spans),
2006                parsed_call.output,
2007            ),
2008        },
2009        Some(name),
2010    )
2011}