Skip to main content

nu_parser/
lex.rs

1use nu_protocol::{ParseError, Span};
2
3#[path = "delimiter_diagnostics.rs"]
4mod delimiter_diagnostics;
5use delimiter_diagnostics::{
6    closing_delimiter_str, quote_delimiter_str, unbalanced_closer, unclosed_from_open,
7};
8
9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10pub enum TokenContents {
11    Item,
12    Comment,
13    Pipe,
14    PipePipe,
15    AssignmentOperator,
16    ErrGreaterPipe,
17    OutErrGreaterPipe,
18    Semicolon,
19    OutGreaterThan,
20    OutGreaterGreaterThan,
21    ErrGreaterThan,
22    ErrGreaterGreaterThan,
23    OutErrGreaterThan,
24    OutErrGreaterGreaterThan,
25    Eol,
26}
27
28#[derive(Debug, PartialEq, Eq)]
29pub struct Token {
30    pub contents: TokenContents,
31    pub span: Span,
32}
33
34impl Token {
35    pub fn new(contents: TokenContents, span: Span) -> Token {
36        Token { contents, span }
37    }
38}
39
40#[derive(Clone, Copy, Debug)]
41pub enum BlockKind {
42    Paren,
43    CurlyBracket,
44    SquareBracket,
45    AngleBracket,
46}
47
48/// An open delimiter on the lexer's nesting stack (kind + opener span only).
49///
50/// Opener spans are used only to *label* a real unclosed/unbalanced error for
51/// miette. Indent/structure heuristics must never invent a parse failure — the
52/// stack alone decides whether lexing failed.
53#[derive(Clone, Copy, Debug)]
54pub(crate) struct OpenFrame {
55    pub kind: BlockKind,
56    pub open_span: Span,
57}
58
59// A baseline token is terminated if it's not nested inside of a paired
60// delimiter and the next character is one of: `|`, `;` or any
61// whitespace.
62fn is_item_terminator(
63    block_level: &[OpenFrame],
64    c: u8,
65    additional_whitespace: &[u8],
66    special_tokens: &[u8],
67) -> bool {
68    block_level.is_empty()
69        && (c == b' '
70            || c == b'\t'
71            || c == b'\n'
72            || c == b'\r'
73            || c == b'|'
74            || c == b';'
75            || additional_whitespace.contains(&c)
76            || special_tokens.contains(&c))
77}
78
79/// Assignment operators have special handling distinct from math expressions, as they cause the
80/// rest of the pipeline to be consumed.
81pub fn is_assignment_operator(bytes: &[u8]) -> bool {
82    matches!(bytes, b"=" | b"+=" | b"++=" | b"-=" | b"*=" | b"/=")
83}
84
85// A special token is one that is a byte that stands alone as its own token. For example
86// when parsing a signature you may want to have `:` be able to separate tokens and also
87// to be handled as its own token to notify you you're about to parse a type in the example
88// `foo:bar`
89fn is_special_item(block_level: &[OpenFrame], c: u8, special_tokens: &[u8]) -> bool {
90    block_level.is_empty() && special_tokens.contains(&c)
91}
92
93/// A better place to put the "expected closer" miette label when the real stack
94/// failure is only known at end-of-token (often far from the human mistake).
95///
96/// Never used to invent an error — only to choose spans for an error that
97/// already exists because `block_level` is non-empty at the end.
98#[derive(Clone, Copy, Debug)]
99struct CloserLabelHint {
100    /// Opener most likely related to the missing closer (often an inner `{|…`).
101    open_span: Span,
102    /// Where the missing closer probably belongs.
103    expected_span: Span,
104}
105
106/// True if `c` can legally continue a multi-line construct onto the next line
107/// (so a following line starting with `|` is not a missing-`}` signal).
108fn continues_onto_next_line(c: u8) -> bool {
109    matches!(
110        c,
111        b'|' | b'{' | b'(' | b'[' | b',' | b':' | b'+' | b'-' | b'*' | b'/' | b'=' | b'.'
112    )
113}
114
115/// Advance the delimiter matching for one byte inside a subexpression of an
116/// interpolated string. Shared by `lex_item` and `parse_string_interpolation`
117/// so both scan the same bytes the same way. The stack holds expected closers;
118/// `open` is stored alongside a pushed closer (a span for error reporting, or
119/// `()` when the caller does not need one).
120///
121/// While the innermost open delimiter is a quote, only that quote closes it;
122/// otherwise quotes open nested strings and parens nest. Escapes exist only in
123/// double-quoted strings: returns true when `byte` is a backslash inside a
124/// nested `"` string, in which case the caller must also skip the next byte.
125pub(crate) fn interp_subexpr_step<T>(stack: &mut Vec<(u8, T)>, byte: u8, open: T) -> bool {
126    match stack.last() {
127        Some(&(expected, _)) if expected != b')' => {
128            if expected == b'"' && byte == b'\\' {
129                return true;
130            }
131            if byte == expected {
132                stack.pop();
133            }
134        }
135        _ => match byte {
136            b'\'' | b'"' | b'`' => stack.push((byte, open)),
137            b'(' => stack.push((b')', open)),
138            b')' => {
139                stack.pop();
140            }
141            _ => {}
142        },
143    }
144    false
145}
146
147pub fn lex_item(
148    input: &[u8],
149    curr_offset: &mut usize,
150    span_offset: usize,
151    additional_whitespace: &[u8],
152    special_tokens: &[u8],
153    in_signature: bool,
154) -> (Token, Option<ParseError>) {
155    // Tracks the opening quote character and its span while inside a string.
156    let mut quote_start: Option<(u8, Span)> = None;
157
158    // True while the current string is an interpolated one (its opening quote
159    // directly follows `$`). Inside such a string an unescaped `(` starts a
160    // subexpression, where quotes and parens nest.
161    let mut quote_is_interp = false;
162
163    // Expected closers (with opener spans) while inside a subexpression of an
164    // interpolated string. Non-empty means the string's own closing quote does
165    // not end it yet. Mirrors the delimiter matching that
166    // `parse_string_interpolation` later applies to the same bytes, so the
167    // token ends exactly where the parser will end the string.
168    let mut interp_expr_level: Vec<(u8, Span)> = vec![];
169
170    let mut in_comment = false;
171
172    let token_start = *curr_offset;
173
174    // Paired delimiters with opener spans (for labeling real unclosed errors only).
175    let mut block_level: Vec<OpenFrame> = vec![];
176
177    // Presentation-only: first place a missing `}` may belong (e.g. before a
178    // pipeline step that should have been outside a closure). Used solely when
179    // the stack still has openers at end-of-token — never to invent failures.
180    let mut closer_label_hint: Option<CloserLabelHint> = None;
181
182    // Line tracking for the presentation hint above (not for inventing errors).
183    let mut at_line_start = true;
184    // Last non-whitespace, non-comment char on the previous line (if any).
185    let mut prev_line_continue = false;
186    let mut last_sig_char: Option<u8> = None;
187
188    // The process of slurping up a baseline token repeats:
189    //
190    // - String literal, which begins with `'` or `"`, and continues until
191    //   the same character is encountered again.
192    // - Delimiter pair, which begins with `[`, `(`, or `{`, and continues until
193    //   the matching closing delimiter is found, skipping comments and string
194    //   literals.
195    // - When not nested inside of a delimiter pair, when a terminating
196    //   character (whitespace, `|`, `;` or `#`) is encountered, the baseline
197    //   token is done.
198    // - Otherwise, accumulate the character into the current baseline token.
199    //
200    // Parse *failure* is decided only by the delimiter stack / quotes — never by
201    // line-shape heuristics. Heuristics may only choose spans/help when a real
202    // failure is reported.
203    let mut previous_char = None;
204    while let Some(c) = input.get(*curr_offset) {
205        let c = *c;
206
207        if let Some((start, open_span)) = quote_start {
208            if !interp_expr_level.is_empty() {
209                // Inside a subexpression of an interpolated string; the shared
210                // step keeps this scan and `parse_string_interpolation` on the
211                // same rules, so the token ends where the parser ends the
212                // string.
213                let open = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
214                if interp_subexpr_step(&mut interp_expr_level, c, open)
215                    && input.get(*curr_offset + 1).is_some()
216                {
217                    // Escape inside a nested double-quoted string: consume the
218                    // escaped byte too, so `\"` does not close the string.
219                    *curr_offset += 2;
220                    previous_char = Some(c);
221                    at_line_start = false;
222                    continue;
223                }
224                last_sig_char = Some(c);
225                at_line_start = false;
226                *curr_offset += 1;
227                previous_char = Some(c);
228                continue;
229            }
230            // Check if we're in an escape sequence
231            if c == b'\\' && start == b'"' {
232                // Go ahead and consume the escape character if possible
233                if input.get(*curr_offset + 1).is_some() {
234                    // Successfully escaped the character
235                    *curr_offset += 2;
236                    previous_char = Some(c);
237                    at_line_start = false;
238                    continue;
239                } else {
240                    let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
241                    let end_span = if span.end > span.start {
242                        Span::new(span.end - 1, span.end)
243                    } else {
244                        span
245                    };
246
247                    return (
248                        Token {
249                            contents: TokenContents::Item,
250                            span,
251                        },
252                        Some(unclosed_from_open(
253                            input,
254                            span_offset,
255                            quote_delimiter_str(start),
256                            open_span,
257                            end_span,
258                        )),
259                    );
260                }
261            }
262            // If we encountered the closing quote character for the current
263            // string, we're done with the current string.
264            if c == start {
265                // Also need to check to make sure we aren't escaped
266                quote_start = None;
267            } else if quote_is_interp && c == b'(' {
268                // An unescaped `(` in an interpolated string starts a
269                // subexpression (an escaped one was already consumed by the
270                // escape handling above). The string's closing quote cannot
271                // end it until the matching `)` is found.
272                interp_expr_level.push((
273                    b')',
274                    Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1),
275                ));
276            }
277            last_sig_char = Some(c);
278            at_line_start = false;
279        } else if c == b'#' && !in_comment {
280            // To start a comment, It either need to be the first character of the token or prefixed with whitespace.
281            in_comment = previous_char
282                .map(char::from)
283                .map(char::is_whitespace)
284                .unwrap_or(true);
285        } else if c == b'\n' || c == b'\r' {
286            in_comment = false;
287            if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
288                break;
289            }
290            // Commit previous line's trailing significant char for next-line `|` hints.
291            // For `\r\n`, only commit/reset on `\n` so we don't double-reset.
292            let is_newline_end = c == b'\n' || input.get(*curr_offset + 1) != Some(&b'\n');
293            if is_newline_end {
294                prev_line_continue = last_sig_char.is_some_and(continues_onto_next_line);
295                at_line_start = true;
296                last_sig_char = None;
297            }
298        } else if in_comment {
299            if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
300                break;
301            }
302        } else if is_special_item(&block_level, c, special_tokens) && token_start == *curr_offset {
303            *curr_offset += 1;
304            break;
305        } else if c == b'\'' || c == b'"' || c == b'`' {
306            let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
307            quote_start = Some((c, open_span));
308            // `$"` and `$'` open interpolated strings, where `(` starts a
309            // subexpression. Backtick strings never interpolate.
310            quote_is_interp = c != b'`' && previous_char == Some(b'$');
311            last_sig_char = Some(c);
312            at_line_start = false;
313        } else if c == b'[' {
314            let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
315            block_level.push(OpenFrame {
316                kind: BlockKind::SquareBracket,
317                open_span,
318            });
319            last_sig_char = Some(c);
320            at_line_start = false;
321        } else if c == b'<' && in_signature {
322            let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
323            block_level.push(OpenFrame {
324                kind: BlockKind::AngleBracket,
325                open_span,
326            });
327            last_sig_char = Some(c);
328            at_line_start = false;
329        } else if c == b'>' && in_signature {
330            if let Some(OpenFrame {
331                kind: BlockKind::AngleBracket,
332                ..
333            }) = block_level.last()
334            {
335                let _ = block_level.pop();
336            }
337            last_sig_char = Some(c);
338            at_line_start = false;
339        } else if c == b']' {
340            // Closing `]` — pop matching `[`, else real mismatch if another opener is open.
341            if let Some(OpenFrame {
342                kind: BlockKind::SquareBracket,
343                ..
344            }) = block_level.last()
345            {
346                let _ = block_level.pop();
347            } else if !block_level.is_empty() {
348                *curr_offset += 1;
349                let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
350                let close_span = Span::new(span.end - 1, span.end);
351                return (
352                    Token {
353                        contents: TokenContents::Item,
354                        span,
355                    },
356                    Some(unbalanced_closer("]", "[", &block_level, close_span)),
357                );
358            }
359            last_sig_char = Some(c);
360            at_line_start = false;
361        } else if c == b'{' {
362            // Presentation only: `def name [\n  param\n {` without `]` — the body
363            // `{` is where `]` should have been. Record for labeling if the `[`
364            // is still open at end-of-token (real stack failure).
365            if closer_label_hint.is_none()
366                && let Some(frame) = block_level.last()
367                && matches!(frame.kind, BlockKind::SquareBracket)
368            {
369                closer_label_hint = Some(CloserLabelHint {
370                    open_span: frame.open_span,
371                    expected_span: Span::new(
372                        span_offset + *curr_offset,
373                        span_offset + *curr_offset + 1,
374                    ),
375                });
376            }
377            let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
378            block_level.push(OpenFrame {
379                kind: BlockKind::CurlyBracket,
380                open_span,
381            });
382            last_sig_char = Some(c);
383            at_line_start = false;
384        } else if c == b'}' {
385            // Closing `}` — pop matching `{`, else real mismatch against stack top.
386            if let Some(OpenFrame {
387                kind: BlockKind::CurlyBracket,
388                ..
389            }) = block_level.last()
390            {
391                let _ = block_level.pop();
392            } else {
393                *curr_offset += 1;
394                let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
395                let close_span = Span::new(span.end - 1, span.end);
396                return (
397                    Token {
398                        contents: TokenContents::Item,
399                        span,
400                    },
401                    Some(unbalanced_closer("}", "{", &block_level, close_span)),
402                );
403            }
404            last_sig_char = Some(c);
405            at_line_start = false;
406        } else if c == b'(' {
407            let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
408            block_level.push(OpenFrame {
409                kind: BlockKind::Paren,
410                open_span,
411            });
412            last_sig_char = Some(c);
413            at_line_start = false;
414        } else if c == b')' {
415            // Closing `)` — pop matching `(`, else real mismatch against stack top.
416            if let Some(OpenFrame {
417                kind: BlockKind::Paren,
418                ..
419            }) = block_level.last()
420            {
421                let _ = block_level.pop();
422            } else {
423                *curr_offset += 1;
424                let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
425                let close_span = Span::new(span.end - 1, span.end);
426                return (
427                    Token {
428                        contents: TokenContents::Item,
429                        span,
430                    },
431                    Some(unbalanced_closer(")", "(", &block_level, close_span)),
432                );
433            }
434            last_sig_char = Some(c);
435            at_line_start = false;
436        } else if c == b'r' && input.get(*curr_offset + 1) == Some(b'#').as_ref() {
437            // already checked `r#` pattern, so it's a raw string.
438            let lex_result = lex_raw_string(input, curr_offset, span_offset);
439            let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
440            if let Err(e) = lex_result {
441                return (
442                    Token {
443                        contents: TokenContents::Item,
444                        span,
445                    },
446                    Some(e),
447                );
448            }
449            last_sig_char = Some(b'#');
450            at_line_start = false;
451        } else if c == b'|' && is_redirection(&input[token_start..*curr_offset]) {
452            // matches err>| etc.
453            *curr_offset += 1;
454            break;
455        } else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
456            break;
457        } else if !c.is_ascii_whitespace() {
458            // Presentation hint only: a new line starting with `|` while nested
459            // in `{…}`, when the previous line did not end with a continue char,
460            // often means a missing `}` before this pipeline step (e.g. forgot
461            // to close `{|n| … }` before `| upsert …`).
462            //
463            // We only *record* this; an error is emitted only if the stack is
464            // still non-empty at end-of-token.
465            if c == b'|'
466                && at_line_start
467                && !prev_line_continue
468                && closer_label_hint.is_none()
469                && let Some(frame) = block_level
470                    .iter()
471                    .rev()
472                    .find(|f| matches!(f.kind, BlockKind::CurlyBracket))
473            {
474                closer_label_hint = Some(CloserLabelHint {
475                    open_span: frame.open_span,
476                    expected_span: Span::new(
477                        span_offset + *curr_offset,
478                        span_offset + *curr_offset + 1,
479                    ),
480                });
481            }
482            last_sig_char = Some(c);
483            at_line_start = false;
484        } else if at_line_start && (c == b' ' || c == b'\t') {
485            // stay at line start until real content
486        } else {
487            at_line_start = false;
488        }
489
490        *curr_offset += 1;
491        previous_char = Some(c);
492    }
493
494    let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
495    let end_span = if span.end > span.start {
496        Span::new(span.end - 1, span.end)
497    } else {
498        span
499    };
500
501    // An open delimiter inside an interpolated string's subexpression is more
502    // precise than the enclosing quote. Report the oldest one: in the common
503    // `$"foo (2 + 3"` typo the trailing quote was meant to close the string,
504    // and the actual mistake is the unclosed `(`.
505    if let Some((closer, open_span)) = interp_expr_level.first() {
506        let closer_str = match closer {
507            b')' => ")",
508            delim => quote_delimiter_str(*delim),
509        };
510        return (
511            Token {
512                contents: TokenContents::Item,
513                span,
514            },
515            Some(unclosed_from_open(
516                input,
517                span_offset,
518                closer_str,
519                *open_span,
520                end_span,
521            )),
522        );
523    }
524
525    if let Some((delim, open_span)) = quote_start {
526        // The non-lite parse trims quotes on both sides, so we add the expected quote so that
527        // anyone wanting to consume this partial parse (e.g., completions) will be able to get
528        // correct information from the non-lite parse.
529        return (
530            Token {
531                contents: TokenContents::Item,
532                span,
533            },
534            Some(unclosed_from_open(
535                input,
536                span_offset,
537                quote_delimiter_str(delim),
538                open_span,
539                end_span,
540            )),
541        );
542    }
543
544    // Still-unclosed openers at end of token: real stack failure.
545    // Prefer a recorded closer-label hint when it refers to the *same* open frame
546    // still on the stack (presentation only — error already exists).
547    if let Some(frame) = block_level.last() {
548        let (label_open, label_end) = closer_label_hint
549            .filter(|h| h.open_span == frame.open_span)
550            .map(|h| (h.open_span, h.expected_span))
551            .unwrap_or((frame.open_span, end_span));
552
553        let cause = unclosed_from_open(
554            input,
555            span_offset,
556            closing_delimiter_str(frame.kind),
557            label_open,
558            label_end,
559        );
560
561        return (
562            Token {
563                contents: TokenContents::Item,
564                span,
565            },
566            Some(cause),
567        );
568    }
569
570    // If we didn't accumulate any characters, it's an unexpected error.
571    if *curr_offset - token_start == 0 {
572        return (
573            Token {
574                contents: TokenContents::Item,
575                span,
576            },
577            Some(ParseError::UnexpectedEof("command".to_string(), span)),
578        );
579    }
580
581    let mut err = None;
582    let output = match &input[(span.start - span_offset)..(span.end - span_offset)] {
583        bytes if is_assignment_operator(bytes) => Token {
584            contents: TokenContents::AssignmentOperator,
585            span,
586        },
587        b"out>" | b"o>" => Token {
588            contents: TokenContents::OutGreaterThan,
589            span,
590        },
591        b"out>>" | b"o>>" => Token {
592            contents: TokenContents::OutGreaterGreaterThan,
593            span,
594        },
595        b"out>|" | b"o>|" => {
596            err = Some(ParseError::Expected(
597                "`|`.  Redirecting stdout to a pipe is the same as normal piping.",
598                span,
599            ));
600            Token {
601                // HACK: For more accurate parsing aligned with user intention
602                contents: TokenContents::Pipe,
603                span,
604            }
605        }
606        b"err>" | b"e>" => Token {
607            contents: TokenContents::ErrGreaterThan,
608            span,
609        },
610        b"err>>" | b"e>>" => Token {
611            contents: TokenContents::ErrGreaterGreaterThan,
612            span,
613        },
614        b"err>|" | b"e>|" => Token {
615            contents: TokenContents::ErrGreaterPipe,
616            span,
617        },
618        b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token {
619            contents: TokenContents::OutErrGreaterThan,
620            span,
621        },
622        b"out+err>>" | b"err+out>>" | b"o+e>>" | b"e+o>>" => Token {
623            contents: TokenContents::OutErrGreaterGreaterThan,
624            span,
625        },
626        b"out+err>|" | b"err+out>|" | b"o+e>|" | b"e+o>|" => Token {
627            contents: TokenContents::OutErrGreaterPipe,
628            span,
629        },
630        b"&&" => {
631            err = Some(ParseError::ShellAndAnd(span));
632            Token {
633                // HACK: For more accurate parsing aligned with user intention
634                contents: TokenContents::Pipe,
635                span,
636            }
637        }
638        b"2>" => {
639            err = Some(ParseError::ShellErrRedirect(span));
640            Token {
641                // HACK: For more accurate parsing aligned with user intention
642                contents: TokenContents::ErrGreaterThan,
643                span,
644            }
645        }
646        b"2>&1" => {
647            err = Some(ParseError::ShellOutErrRedirect(span));
648            Token {
649                // HACK: For more accurate parsing aligned with user intention
650                contents: TokenContents::Pipe,
651                span,
652            }
653        }
654        _ => Token {
655            contents: TokenContents::Item,
656            span,
657        },
658    };
659    (output, err)
660}
661
662fn lex_raw_string(
663    input: &[u8],
664    curr_offset: &mut usize,
665    span_offset: usize,
666) -> Result<(), ParseError> {
667    // A raw string literal looks like `echo r#'Look, I can use 'single quotes'!'#`
668    // If the next character is `#` we're probably looking at a raw string literal
669    // so we need to read all the text until we find a closing `#`. This raw string
670    // can contain any character, including newlines and double quotes without needing
671    // to escape them.
672    //
673    // A raw string can contain many `#` as prefix,
674    // incase if there is a `'#` or `#'` in the string itself.
675    // E.g: r##'I can use '#' in a raw string'##
676    let mut prefix_sharp_cnt = 0;
677    let start = *curr_offset;
678    while let Some(b'#') = input.get(start + prefix_sharp_cnt + 1) {
679        prefix_sharp_cnt += 1;
680    }
681
682    // curr_offset is the character `r`, we need to move forward and skip all `#`
683    // characters.
684    //
685    // e.g: r###'<body>
686    //      ^
687    //      ^
688    //   curr_offset
689    *curr_offset += prefix_sharp_cnt + 1;
690    // the next one should be a single quote.
691    if input.get(*curr_offset) != Some(&b'\'') {
692        return Err(ParseError::Expected(
693            "'",
694            Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1),
695        ));
696    }
697
698    *curr_offset += 1;
699    let mut matches = false;
700    while let Some(ch) = input.get(*curr_offset) {
701        // check for postfix '###
702        if *ch == b'#' {
703            let start_ch = input[*curr_offset - prefix_sharp_cnt];
704            let postfix = &input[*curr_offset - prefix_sharp_cnt + 1..=*curr_offset];
705            if start_ch == b'\'' && postfix.iter().all(|x| *x == b'#') {
706                matches = true;
707                break;
708            }
709        }
710        *curr_offset += 1
711    }
712    if !matches {
713        let mut expected = '\''.to_string();
714        expected.push_str(&"#".repeat(prefix_sharp_cnt));
715        return Err(ParseError::UnexpectedEof(
716            expected,
717            Span::new(span_offset + *curr_offset - 1, span_offset + *curr_offset),
718        ));
719    }
720    Ok(())
721}
722
723pub fn lex_signature(
724    input: &[u8],
725    span_offset: usize,
726    additional_whitespace: &[u8],
727    special_tokens: &[u8],
728    skip_comment: bool,
729) -> (Vec<Token>, Option<ParseError>) {
730    let mut state = LexState {
731        input,
732        output: Vec::new(),
733        error: None,
734        span_offset,
735    };
736    lex_internal(
737        &mut state,
738        additional_whitespace,
739        special_tokens,
740        skip_comment,
741        true,
742        None,
743    );
744    (state.output, state.error)
745}
746
747#[derive(Debug)]
748pub struct LexState<'a> {
749    pub input: &'a [u8],
750    pub output: Vec<Token>,
751    pub error: Option<ParseError>,
752    pub span_offset: usize,
753}
754
755/// Lex until the output is `max_tokens` longer than before the call, or until the input is exhausted.
756/// The return value indicates how many tokens the call added to / removed from the output.
757///
758/// The behaviour here is non-obvious when `additional_whitespace` doesn't include newline:
759/// If you pass a `state` where the last token in the output is an Eol, this might *remove* tokens.
760pub fn lex_n_tokens(
761    state: &mut LexState,
762    additional_whitespace: &[u8],
763    special_tokens: &[u8],
764    skip_comment: bool,
765    max_tokens: usize,
766) -> isize {
767    let n_tokens = state.output.len();
768    lex_internal(
769        state,
770        additional_whitespace,
771        special_tokens,
772        skip_comment,
773        false,
774        Some(max_tokens),
775    );
776    // If this lex_internal call reached the end of the input, there may now be fewer tokens
777    // in the output than before.
778    let tokens_n_diff = (state.output.len() as isize) - (n_tokens as isize);
779    let next_offset = state.output.last().map(|token| token.span.end);
780    if let Some(next_offset) = next_offset {
781        state.input = &state.input[next_offset - state.span_offset..];
782        state.span_offset = next_offset;
783    }
784    tokens_n_diff
785}
786
787pub fn lex(
788    input: &[u8],
789    span_offset: usize,
790    additional_whitespace: &[u8],
791    special_tokens: &[u8],
792    skip_comment: bool,
793) -> (Vec<Token>, Option<ParseError>) {
794    let mut state = LexState {
795        input,
796        output: Vec::new(),
797        error: None,
798        span_offset,
799    };
800    lex_internal(
801        &mut state,
802        additional_whitespace,
803        special_tokens,
804        skip_comment,
805        false,
806        None,
807    );
808    (state.output, state.error)
809}
810
811fn lex_internal(
812    state: &mut LexState,
813    additional_whitespace: &[u8],
814    special_tokens: &[u8],
815    skip_comment: bool,
816    // within signatures we want to treat `<` and `>` specially
817    in_signature: bool,
818    max_tokens: Option<usize>,
819) {
820    let initial_output_len = state.output.len();
821
822    let mut curr_offset = 0;
823
824    let mut is_complete = true;
825    while let Some(c) = state.input.get(curr_offset) {
826        if max_tokens
827            .is_some_and(|max_tokens| state.output.len() >= initial_output_len + max_tokens)
828        {
829            break;
830        }
831        let c = *c;
832        if c == b'|' {
833            // If the next character is `|`, it's either `|` or `||`.
834            let idx = curr_offset;
835            let prev_idx = idx;
836            curr_offset += 1;
837
838            // If the next character is `|`, we're looking at a `||`.
839            if let Some(c) = state.input.get(curr_offset)
840                && *c == b'|'
841            {
842                let idx = curr_offset;
843                curr_offset += 1;
844                state.output.push(Token::new(
845                    TokenContents::PipePipe,
846                    Span::new(state.span_offset + prev_idx, state.span_offset + idx + 1),
847                ));
848                continue;
849            }
850
851            // Otherwise, it's just a regular `|` token.
852
853            // Before we push, check to see if the previous character was a newline.
854            // If so, then this is a continuation of the previous line
855            if let Some(prev) = state.output.last_mut() {
856                match prev.contents {
857                    TokenContents::Eol => {
858                        *prev = Token::new(
859                            TokenContents::Pipe,
860                            Span::new(state.span_offset + idx, state.span_offset + idx + 1),
861                        );
862                        // And this is a continuation of the previous line if previous line is a
863                        // comment line (combined with EOL + Comment)
864                        //
865                        // Initially, the last one token is TokenContents::Pipe, we don't need to
866                        // check it, so the beginning offset is 2.
867                        let mut offset = 2;
868                        while state.output.len() > offset {
869                            let index = state.output.len() - offset;
870                            if state.output[index].contents == TokenContents::Comment
871                                && state.output[index - 1].contents == TokenContents::Eol
872                            {
873                                state.output.remove(index - 1);
874                                offset += 1;
875                            } else {
876                                break;
877                            }
878                        }
879                    }
880                    _ => {
881                        state.output.push(Token::new(
882                            TokenContents::Pipe,
883                            Span::new(state.span_offset + idx, state.span_offset + idx + 1),
884                        ));
885                    }
886                }
887            } else {
888                state.output.push(Token::new(
889                    TokenContents::Pipe,
890                    Span::new(state.span_offset + idx, state.span_offset + idx + 1),
891                ));
892            }
893
894            is_complete = false;
895        } else if c == b';' {
896            // If the next character is a `;`, we're looking at a semicolon token.
897
898            if !is_complete && state.error.is_none() {
899                state.error = Some(ParseError::ExtraTokens(Span::new(
900                    curr_offset,
901                    curr_offset + 1,
902                )));
903            }
904            let idx = curr_offset;
905            curr_offset += 1;
906            state.output.push(Token::new(
907                TokenContents::Semicolon,
908                Span::new(state.span_offset + idx, state.span_offset + idx + 1),
909            ));
910        } else if c == b'\r' {
911            // Ignore a stand-alone carriage return
912            curr_offset += 1;
913        } else if c == b'\n' {
914            // If the next character is a newline, we're looking at an EOL (end of line) token.
915            let idx = curr_offset;
916            curr_offset += 1;
917            if !additional_whitespace.contains(&c) {
918                state.output.push(Token::new(
919                    TokenContents::Eol,
920                    Span::new(state.span_offset + idx, state.span_offset + idx + 1),
921                ));
922            }
923        } else if c == b'#' {
924            // If the next character is `#`, we're at the beginning of a line
925            // comment. The comment continues until the next newline.
926            let mut start = curr_offset;
927
928            while let Some(input) = state.input.get(curr_offset) {
929                if *input == b'\n' {
930                    if !skip_comment {
931                        state.output.push(Token::new(
932                            TokenContents::Comment,
933                            Span::new(state.span_offset + start, state.span_offset + curr_offset),
934                        ));
935                    }
936                    start = curr_offset;
937
938                    break;
939                } else {
940                    curr_offset += 1;
941                }
942            }
943            if start != curr_offset && !skip_comment {
944                state.output.push(Token::new(
945                    TokenContents::Comment,
946                    Span::new(state.span_offset + start, state.span_offset + curr_offset),
947                ));
948            }
949        } else if c == b' ' || c == b'\t' || additional_whitespace.contains(&c) {
950            // If the next character is non-newline whitespace, skip it.
951            curr_offset += 1;
952        } else {
953            let (token, err) = lex_item(
954                state.input,
955                &mut curr_offset,
956                state.span_offset,
957                additional_whitespace,
958                special_tokens,
959                in_signature,
960            );
961            if state.error.is_none() {
962                state.error = err;
963            }
964            is_complete = true;
965            state.output.push(token);
966        }
967    }
968}
969
970/// True if this the start of a redirection. Does not match `>>` or `>|` forms.
971fn is_redirection(token: &[u8]) -> bool {
972    matches!(
973        token,
974        b"o>" | b"out>" | b"e>" | b"err>" | b"o+e>" | b"e+o>" | b"out+err>" | b"err+out>"
975    )
976}