1use std::{fmt, iter::Peekable, str::CharIndices};
12
13use thiserror::Error;
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Token {
18 pub kind: TokenKind,
20 pub line: usize,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum TokenKind {
27 Word(RawWord),
29 Pipe,
31 Semicolon,
33 DoubleSemicolon,
35 Newline,
37 AndAnd,
39 OrOr,
41 Ampersand,
43 LeftParen,
45 RightParen,
47 LeftBrace,
49 RightBrace,
51 Great,
53 GreatGreat,
55 Less,
57 HereDoc(RawWord),
59 LessParen,
61}
62
63impl fmt::Display for TokenKind {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 let rendered = match self {
66 Self::Word(word) => return write!(formatter, "word {}", word.describe()),
67 Self::HereDoc(_) => "here-document",
68 Self::Pipe => "|",
69 Self::Semicolon => ";",
70 Self::DoubleSemicolon => ";;",
71 Self::Newline => "newline",
72 Self::AndAnd => "&&",
73 Self::OrOr => "||",
74 Self::Ampersand => "&",
75 Self::LeftParen => "(",
76 Self::RightParen => ")",
77 Self::LeftBrace => "{",
78 Self::RightBrace => "}",
79 Self::Great => ">",
80 Self::GreatGreat => ">>",
81 Self::Less => "<",
82 Self::LessParen => "<(",
83 };
84 formatter.write_str(rendered)
85 }
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct RawWord {
91 pub parts: Vec<RawPart>,
93}
94
95impl RawWord {
96 #[must_use]
98 pub fn as_literal(&self) -> Option<&str> {
99 match self.parts.as_slice() {
100 [RawPart::Literal(text)] => Some(text),
101 _ => None,
102 }
103 }
104
105 #[must_use]
107 pub fn describe(&self) -> String {
108 self.as_literal().map_or_else(
109 || "with expansions".to_owned(),
110 |literal| format!("{literal:?}"),
111 )
112 }
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
117pub enum RawPart {
118 Literal(String),
120 SingleQuoted(String),
122 DoubleQuoted(Vec<RawPart>),
124 Parameter(RawParameter),
126 CommandSubstitution(String),
128 Arithmetic(String),
130}
131
132#[derive(Clone, Debug, Eq, PartialEq)]
134pub enum RawParameter {
135 Named {
137 name: String,
139 indices: Vec<RawWord>,
141 },
142 Positional(usize),
144 AllPositional,
146 AllPositionalJoined,
148 PositionalCount,
150 LastStatus,
152}
153
154#[derive(Clone, Debug, Eq, Error, PartialEq)]
156#[error("line {line}: {message}")]
157pub struct LexError {
158 pub line: usize,
160 pub message: String,
162}
163
164const BACKTICK_REJECTION: &str = "backtick command substitution is not supported; use `$( ... )`, which nests and quotes cleanly";
166
167const FD_REDIRECTION_REJECTION: &str = "file-descriptor redirection (`2>`, `>&2`, `2>&1`) is not supported: this shell has one combined output stream, not numbered descriptors; `>` and `>>` write named in-memory buffers";
169
170const HERE_STRING_REJECTION: &str = "the here-string `<<<` is not supported; pipe the value instead, as in `echo \"$x\" | cmd`, or use a here-document `<<EOF ... EOF`";
175
176const CASE_FALLTHROUGH_REJECTION: &str = "`;&` and `;;&` are not supported: a `case` clause here runs alone and never falls through to the next; end every clause with `;;`";
178
179struct PendingHereDoc {
185 delimiter: String,
187 strip_tabs: bool,
189 expand: bool,
191 line: usize,
193 token: usize,
195}
196
197impl LexError {
198 fn new(line: usize, message: impl Into<String>) -> Self {
199 Self {
200 line,
201 message: message.into(),
202 }
203 }
204}
205
206pub fn tokenize(source: &str) -> Result<Vec<Token>, LexError> {
208 Lexer::new(source).run()
209}
210
211struct Lexer<'a> {
212 source: &'a str,
213 chars: Peekable<CharIndices<'a>>,
214 line: usize,
215 tokens: Vec<Token>,
216 parts: Vec<RawPart>,
217 literal: String,
218 word_started: bool,
219 word_line: usize,
220 pending_here_docs: Vec<PendingHereDoc>,
222}
223
224impl<'a> Lexer<'a> {
225 fn new(source: &'a str) -> Self {
226 Self {
227 source,
228 chars: source.char_indices().peekable(),
229 line: 1,
230 tokens: Vec::new(),
231 parts: Vec::new(),
232 literal: String::new(),
233 word_started: false,
234 word_line: 1,
235 pending_here_docs: Vec::new(),
236 }
237 }
238
239 fn run(mut self) -> Result<Vec<Token>, LexError> {
240 while let Some((index, character)) = self.chars.next() {
241 match character {
242 '\n' => {
243 self.finish_word();
244 self.push(TokenKind::Newline);
245 self.line += 1;
246 self.read_pending_here_doc_bodies()?;
248 }
249 ' ' | '\t' | '\r' => self.finish_word(),
250 '#' if !self.word_started => self.skip_comment(),
251 '\\' => self.read_escape()?,
252 '\'' => self.read_single_quoted()?,
253 '"' => self.read_double_quoted()?,
254 '$' => self.read_dollar(index)?,
255 '`' => return Err(LexError::new(self.line, BACKTICK_REJECTION)),
259 '|' | '&' | ';' | '<' | '>' | '(' | ')' => self.read_operator(character)?,
260 '{' | '}' if self.brace_is_reserved_word() => {
261 self.finish_word();
262 self.push(if character == '{' {
263 TokenKind::LeftBrace
264 } else {
265 TokenKind::RightBrace
266 });
267 }
268 other => self.push_literal(other),
269 }
270 }
271 self.finish_word();
272 if let Some(pending) = self.pending_here_docs.first() {
273 return Err(LexError::new(
274 pending.line,
275 format!(
276 "unterminated here-document: the script ended before a line containing exactly {:?}",
277 pending.delimiter
278 ),
279 ));
280 }
281 Ok(self.tokens)
282 }
283
284 fn push(&mut self, kind: TokenKind) {
285 let line = self.line;
286 self.tokens.push(Token { kind, line });
287 }
288
289 fn push_literal(&mut self, character: char) {
290 self.begin_word();
291 self.literal.push(character);
292 }
293
294 fn begin_word(&mut self) {
295 if !self.word_started {
296 self.word_started = true;
297 self.word_line = self.line;
298 }
299 }
300
301 fn flush_literal(&mut self) {
302 if !self.literal.is_empty() {
303 let literal = std::mem::take(&mut self.literal);
304 self.parts.push(RawPart::Literal(literal));
305 }
306 }
307
308 fn push_part(&mut self, part: RawPart) {
309 self.begin_word();
310 self.flush_literal();
311 self.parts.push(part);
312 }
313
314 fn finish_word(&mut self) {
315 if !self.word_started {
316 return;
317 }
318 self.flush_literal();
319 let parts = std::mem::take(&mut self.parts);
320 let line = self.word_line;
321 self.word_started = false;
322 self.tokens.push(Token {
323 kind: TokenKind::Word(RawWord { parts }),
324 line,
325 });
326 }
327
328 fn brace_is_reserved_word(&mut self) -> bool {
332 if self.word_started {
333 return false;
334 }
335 matches!(
336 self.chars.peek().map(|(_, character)| *character),
337 None | Some(' ' | '\t' | '\r' | '\n' | ';' | '&' | '|' | '(' | ')' | '<' | '>')
338 )
339 }
340
341 fn skip_comment(&mut self) {
342 while let Some((_, character)) = self.chars.peek() {
343 if *character == '\n' {
344 return;
345 }
346 self.chars.next();
347 }
348 }
349
350 fn read_escape(&mut self) -> Result<(), LexError> {
351 match self.chars.next() {
352 Some((_, '\n')) => {
353 self.line += 1;
354 Ok(())
355 }
356 Some((_, character)) => {
357 self.push_part(RawPart::SingleQuoted(character.to_string()));
363 Ok(())
364 }
365 None => Err(LexError::new(
366 self.line,
367 "script ends with a trailing backslash",
368 )),
369 }
370 }
371
372 fn read_single_quoted(&mut self) -> Result<(), LexError> {
373 let opened = self.line;
374 let mut text = String::new();
375 loop {
376 match self.chars.next() {
377 Some((_, '\'')) => break,
378 Some((_, character)) => {
379 if character == '\n' {
380 self.line += 1;
381 }
382 text.push(character);
383 }
384 None => {
385 return Err(LexError::new(opened, "unterminated single-quoted string"));
386 }
387 }
388 }
389 self.push_part(RawPart::SingleQuoted(text));
390 Ok(())
391 }
392
393 fn read_double_quoted(&mut self) -> Result<(), LexError> {
394 let parts = self.read_interpolated(Some('"'), "unterminated double-quoted string")?;
395 self.push_part(RawPart::DoubleQuoted(parts));
396 Ok(())
397 }
398
399 fn read_interpolated(
406 &mut self,
407 terminator: Option<char>,
408 unterminated: &str,
409 ) -> Result<Vec<RawPart>, LexError> {
410 let opened = self.line;
411 let escapable: &[char] = if terminator == Some('"') {
412 &['"', '\\', '$', '`']
413 } else {
414 &['\\', '$', '`']
415 };
416 let mut parts = Vec::new();
417 let mut literal = String::new();
418 loop {
419 let Some((index, character)) = self.chars.next() else {
420 if terminator.is_none() {
421 break;
422 }
423 return Err(LexError::new(opened, unterminated));
424 };
425 if Some(character) == terminator {
426 break;
427 }
428 match character {
429 '\\' => match self.chars.next() {
430 Some((_, escaped)) if escapable.contains(&escaped) => literal.push(escaped),
431 Some((_, '\n')) => self.line += 1,
432 Some((_, other)) => {
433 literal.push('\\');
434 literal.push(other);
435 }
436 None => {
437 if terminator.is_none() {
438 literal.push('\\');
439 break;
440 }
441 return Err(LexError::new(opened, unterminated));
442 }
443 },
444 '$' => {
445 let part = self.read_dollar_part(index)?;
446 match part {
447 Some(part) => {
448 if !literal.is_empty() {
449 parts.push(RawPart::Literal(std::mem::take(&mut literal)));
450 }
451 parts.push(part);
452 }
453 None => literal.push('$'),
454 }
455 }
456 '`' => return Err(LexError::new(self.line, BACKTICK_REJECTION)),
457 other => {
458 if other == '\n' {
459 self.line += 1;
460 }
461 literal.push(other);
462 }
463 }
464 }
465 if !literal.is_empty() {
466 parts.push(RawPart::Literal(literal));
467 }
468 Ok(parts)
469 }
470
471 fn read_here_doc_header(&mut self) -> Result<(), LexError> {
473 if self.chars.peek().map(|(_, character)| *character) == Some('<') {
474 return Err(LexError::new(self.line, HERE_STRING_REJECTION));
475 }
476 let strip_tabs = self.chars.peek().map(|(_, character)| *character) == Some('-');
477 if strip_tabs {
478 self.chars.next();
479 }
480 while matches!(
481 self.chars.peek().map(|(_, character)| *character),
482 Some(' ' | '\t')
483 ) {
484 self.chars.next();
485 }
486
487 let (delimiter, expand) = self.read_here_doc_delimiter()?;
488 let line = self.line;
489 let token = self.tokens.len();
490 self.tokens.push(Token {
491 kind: TokenKind::HereDoc(RawWord { parts: Vec::new() }),
492 line,
493 });
494 self.pending_here_docs.push(PendingHereDoc {
495 delimiter,
496 strip_tabs,
497 expand,
498 line,
499 token,
500 });
501 Ok(())
502 }
503
504 fn read_here_doc_delimiter(&mut self) -> Result<(String, bool), LexError> {
509 let line = self.line;
510 let mut delimiter = String::new();
511 let mut quoted = false;
512 while let Some((_, character)) = self.chars.peek().copied() {
513 match character {
514 '\'' | '"' => {
515 self.chars.next();
516 quoted = true;
517 loop {
518 match self.chars.next() {
519 Some((_, closing)) if closing == character => break,
520 Some((_, '\n')) | None => {
521 return Err(LexError::new(
522 line,
523 "unterminated quoted here-document delimiter",
524 ));
525 }
526 Some((_, other)) => delimiter.push(other),
527 }
528 }
529 }
530 '\\' => {
531 self.chars.next();
532 quoted = true;
533 match self.chars.next() {
534 Some((_, '\n')) => {
538 return Err(LexError::new(
539 line,
540 "a here-document delimiter cannot be split across lines; write it on the same line as `<<`",
541 ));
542 }
543 Some((_, escaped)) => delimiter.push(escaped),
544 None => {
545 return Err(LexError::new(
546 line,
547 "script ends with a trailing backslash",
548 ));
549 }
550 }
551 }
552 other if other.is_ascii_alphanumeric() || matches!(other, '_' | '.' | '-') => {
553 delimiter.push(other);
554 self.chars.next();
555 }
556 _ => break,
557 }
558 }
559 if delimiter.is_empty() {
560 return Err(LexError::new(
561 line,
562 "expected a here-document delimiter after `<<`, as in `cat <<EOF`",
563 ));
564 }
565 Ok((delimiter, !quoted))
566 }
567
568 fn read_pending_here_doc_bodies(&mut self) -> Result<(), LexError> {
570 let pending = std::mem::take(&mut self.pending_here_docs);
573 for specification in pending {
574 let mut body = self.read_here_doc_body(&specification)?;
575 body.pop();
580 let parts = if specification.expand {
581 Self::interpolate_here_doc_body(&body, specification.line)?
582 } else if body.is_empty() {
583 Vec::new()
584 } else {
585 vec![RawPart::Literal(body)]
586 };
587 self.tokens[specification.token] = Token {
588 kind: TokenKind::HereDoc(RawWord { parts }),
589 line: specification.line,
590 };
591 }
592 Ok(())
593 }
594
595 fn read_here_doc_body(&mut self, specification: &PendingHereDoc) -> Result<String, LexError> {
597 let mut body = String::new();
598 loop {
599 let mut line = String::new();
600 let mut terminated = false;
601 for (_, character) in self.chars.by_ref() {
602 if character == '\n' {
603 terminated = true;
604 break;
605 }
606 line.push(character);
607 }
608 if terminated {
609 self.line += 1;
610 }
611
612 let content = if specification.strip_tabs {
616 line.trim_start_matches('\t')
617 } else {
618 line.as_str()
619 };
620 if content == specification.delimiter {
621 return Ok(body);
622 }
623 if !terminated {
624 return Err(LexError::new(
625 specification.line,
626 format!(
627 "unterminated here-document: the script ended before a line containing exactly {:?}",
628 specification.delimiter
629 ),
630 ));
631 }
632 body.push_str(content);
633 body.push('\n');
634 }
635 }
636
637 fn interpolate_here_doc_body(body: &str, line: usize) -> Result<Vec<RawPart>, LexError> {
639 let mut lexer = Lexer::new(body);
640 lexer.line = line + 1;
643 lexer
644 .read_interpolated(None, "unterminated here-document")
645 .map_err(|error| LexError::new(error.line, error.message))
646 }
647
648 fn read_dollar(&mut self, index: usize) -> Result<(), LexError> {
649 match self.read_dollar_part(index)? {
650 Some(part) => self.push_part(part),
651 None => self.push_literal('$'),
653 }
654 Ok(())
655 }
656
657 fn read_dollar_part(&mut self, dollar_index: usize) -> Result<Option<RawPart>, LexError> {
658 let Some((_, next)) = self.chars.peek().copied() else {
659 return Ok(None);
660 };
661 match next {
662 '(' => {
663 self.chars.next();
664 if self.chars.peek().map(|(_, character)| *character) == Some('(') {
665 self.chars.next();
666 let body = self.read_balanced(dollar_index, '(', ')', 2, "$(( ... ))")?;
667 return Ok(Some(RawPart::Arithmetic(body)));
668 }
669 let body = self.read_balanced(dollar_index, '(', ')', 1, "$( ... )")?;
670 Ok(Some(RawPart::CommandSubstitution(body)))
671 }
672 '{' => {
673 self.chars.next();
674 self.read_braced_parameter().map(Some)
675 }
676 '?' => {
677 self.chars.next();
678 Ok(Some(RawPart::Parameter(RawParameter::LastStatus)))
679 }
680 '@' => {
681 self.chars.next();
682 Ok(Some(RawPart::Parameter(RawParameter::AllPositional)))
683 }
684 '*' => {
685 self.chars.next();
686 Ok(Some(RawPart::Parameter(RawParameter::AllPositionalJoined)))
687 }
688 '#' => {
689 self.chars.next();
690 Ok(Some(RawPart::Parameter(RawParameter::PositionalCount)))
691 }
692 digit if digit.is_ascii_digit() => {
693 self.chars.next();
694 let position = usize::from(
695 digit
696 .to_digit(10)
697 .and_then(|value| u8::try_from(value).ok())
698 .unwrap_or_default(),
699 );
700 Ok(Some(RawPart::Parameter(RawParameter::Positional(position))))
701 }
702 first if first.is_ascii_alphabetic() || first == '_' => {
703 let name = self.read_name();
704 Ok(Some(RawPart::Parameter(RawParameter::Named {
705 name,
706 indices: Vec::new(),
707 })))
708 }
709 _ => Ok(None),
710 }
711 }
712
713 fn read_name(&mut self) -> String {
714 let mut name = String::new();
715 while let Some((_, character)) = self.chars.peek().copied() {
716 if character.is_ascii_alphanumeric() || character == '_' {
717 name.push(character);
718 self.chars.next();
719 } else {
720 break;
721 }
722 }
723 name
724 }
725
726 fn read_braced_parameter(&mut self) -> Result<RawPart, LexError> {
727 let line = self.line;
728 match self.chars.peek().map(|(_, character)| *character) {
729 Some('?') => {
730 self.chars.next();
731 self.expect_brace_close(line)?;
732 return Ok(RawPart::Parameter(RawParameter::LastStatus));
733 }
734 Some('@') => {
735 self.chars.next();
736 self.expect_brace_close(line)?;
737 return Ok(RawPart::Parameter(RawParameter::AllPositional));
738 }
739 Some('*') => {
740 self.chars.next();
741 self.expect_brace_close(line)?;
742 return Ok(RawPart::Parameter(RawParameter::AllPositionalJoined));
743 }
744 Some('#') => {
745 self.chars.next();
746 if self.chars.peek().map(|(_, character)| *character) == Some('}') {
747 self.chars.next();
748 return Ok(RawPart::Parameter(RawParameter::PositionalCount));
749 }
750 return Err(LexError::new(
751 line,
752 "${#name} length expansion is not supported; use `jq length` or `wc` instead",
753 ));
754 }
755 Some(digit) if digit.is_ascii_digit() => {
756 let mut digits = String::new();
757 while let Some((_, character)) = self.chars.peek().copied() {
758 if character.is_ascii_digit() {
759 digits.push(character);
760 self.chars.next();
761 } else {
762 break;
763 }
764 }
765 self.expect_brace_close(line)?;
766 let position = digits.parse::<usize>().map_err(|_| {
767 LexError::new(
768 line,
769 format!("positional parameter ${digits} is out of range"),
770 )
771 })?;
772 return Ok(RawPart::Parameter(RawParameter::Positional(position)));
773 }
774 _ => {}
775 }
776
777 let name = self.read_name();
778 if name.is_empty() {
779 return Err(LexError::new(line, "empty ${} parameter reference"));
780 }
781
782 let mut indices = Vec::new();
783 loop {
784 match self.chars.peek().map(|(_, character)| *character) {
785 Some('}') => {
786 self.chars.next();
787 break;
788 }
789 Some('[') => {
790 self.chars.next();
791 indices.push(self.read_index_word(line)?);
792 }
793 Some(other) => {
794 return Err(LexError::new(
795 line,
796 format!(
797 "unsupported ${{{name}{other}...}} parameter expansion; this shell keeps only ${{NAME}} and ${{NAME[index]}}"
798 ),
799 ));
800 }
801 None => return Err(LexError::new(line, "unterminated ${} parameter reference")),
802 }
803 }
804
805 Ok(RawPart::Parameter(RawParameter::Named { name, indices }))
806 }
807
808 fn expect_brace_close(&mut self, line: usize) -> Result<(), LexError> {
809 match self.chars.next() {
810 Some((_, '}')) => Ok(()),
811 _ => Err(LexError::new(line, "unterminated ${} parameter reference")),
812 }
813 }
814
815 fn read_index_word(&mut self, line: usize) -> Result<RawWord, LexError> {
820 let mut text = String::new();
821 loop {
822 match self.chars.next() {
823 Some((_, ']')) => break,
824 Some((_, '\n')) => {
825 return Err(LexError::new(line, "unterminated ${NAME[index]} reference"));
826 }
827 Some((_, character)) => text.push(character),
828 None => return Err(LexError::new(line, "unterminated ${NAME[index]} reference")),
829 }
830 }
831 if text == "@" || text == "*" {
832 return Err(LexError::new(
833 line,
834 "${NAME[@]} array expansion is not supported; an unquoted $NAME holding a JSON array already expands element by element",
835 ));
836 }
837 let tokens = tokenize(&text)?;
838 let mut words = tokens.into_iter().filter_map(|token| match token.kind {
839 TokenKind::Word(word) => Some(word),
840 _ => None,
841 });
842 let word = words
843 .next()
844 .ok_or_else(|| LexError::new(line, "empty ${NAME[index]} reference"))?;
845 if words.next().is_some() {
846 return Err(LexError::new(
847 line,
848 "${NAME[index]} accepts exactly one index expression",
849 ));
850 }
851 Ok(word)
852 }
853
854 fn read_balanced(
856 &mut self,
857 dollar_index: usize,
858 open: char,
859 close: char,
860 mut depth: usize,
861 label: &str,
862 ) -> Result<String, LexError> {
863 let opened = self.line;
864 let initial = depth;
865 let start = dollar_index + '$'.len_utf8() + open.len_utf8() * depth;
866 let mut body_end = None;
869 loop {
870 let Some((index, character)) = self.chars.next() else {
871 return Err(LexError::new(opened, format!("unterminated {label}")));
872 };
873 match character {
874 '\n' => self.line += 1,
875 '\\' => {
876 if let Some((_, next)) = self.chars.next() {
877 if next == '\n' {
878 self.line += 1;
879 }
880 continue;
881 }
882 }
883 '\'' => {
884 for (_, quoted) in self.chars.by_ref() {
885 if quoted == '\n' {
886 self.line += 1;
887 }
888 if quoted == '\'' {
889 break;
890 }
891 }
892 continue;
893 }
894 '"' => {
895 let mut escaped = false;
896 for (_, quoted) in self.chars.by_ref() {
897 if quoted == '\n' {
898 self.line += 1;
899 }
900 if escaped {
901 escaped = false;
902 continue;
903 }
904 if quoted == '\\' {
905 escaped = true;
906 continue;
907 }
908 if quoted == '"' {
909 break;
910 }
911 }
912 continue;
913 }
914 matched if matched == open => depth += 1,
915 matched if matched == close => {
916 depth -= 1;
917 if depth + 1 == initial && body_end.is_none() {
918 body_end = Some(index);
919 }
920 if depth == 0 {
921 let end = body_end.unwrap_or(index);
922 return Ok(self.source[start..end].to_owned());
923 }
924 }
925 _ => {}
926 }
927 }
928 }
929
930 fn read_operator(&mut self, character: char) -> Result<(), LexError> {
931 if matches!(character, '<' | '>')
935 && self.word_started
936 && self.parts.is_empty()
937 && !self.literal.is_empty()
938 && self.literal.chars().all(|digit| digit.is_ascii_digit())
939 {
940 return Err(LexError::new(self.line, FD_REDIRECTION_REJECTION));
941 }
942 self.finish_word();
943 let next = self.chars.peek().map(|(_, character)| *character);
944 if matches!((character, next), ('<' | '>', Some('&'))) {
945 return Err(LexError::new(self.line, FD_REDIRECTION_REJECTION));
946 }
947 let kind = match (character, next) {
948 ('|', Some('|')) => {
949 self.chars.next();
950 TokenKind::OrOr
951 }
952 ('|', _) => TokenKind::Pipe,
953 ('&', Some('&')) => {
954 self.chars.next();
955 TokenKind::AndAnd
956 }
957 ('&', _) => TokenKind::Ampersand,
958 (';', Some(';')) => {
959 self.chars.next();
960 if self.chars.peek().map(|(_, character)| *character) == Some('&') {
963 return Err(LexError::new(self.line, CASE_FALLTHROUGH_REJECTION));
964 }
965 TokenKind::DoubleSemicolon
966 }
967 (';', Some('&')) => return Err(LexError::new(self.line, CASE_FALLTHROUGH_REJECTION)),
968 (';', _) => TokenKind::Semicolon,
969 ('>', Some('>')) => {
970 self.chars.next();
971 TokenKind::GreatGreat
972 }
973 ('>', _) => TokenKind::Great,
974 ('<', Some('<')) => {
975 self.chars.next();
976 return self.read_here_doc_header();
977 }
978 ('<', Some('(')) => {
979 self.chars.next();
980 TokenKind::LessParen
981 }
982 ('<', _) => TokenKind::Less,
983 ('(', _) => TokenKind::LeftParen,
984 (')', _) => TokenKind::RightParen,
985 _ => unreachable!("read_operator is only called for operator characters"),
986 };
987 self.push(kind);
988 Ok(())
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::{RawParameter, RawPart, TokenKind, tokenize};
995
996 fn kinds(source: &str) -> Vec<TokenKind> {
997 tokenize(source)
998 .expect("tokenizes")
999 .into_iter()
1000 .map(|token| token.kind)
1001 .collect()
1002 }
1003
1004 fn single_word(source: &str) -> Vec<RawPart> {
1005 match kinds(source).into_iter().next().expect("one token") {
1006 TokenKind::Word(word) => word.parts,
1007 other => panic!("expected a word, found {other}"),
1008 }
1009 }
1010
1011 #[test]
1012 fn splits_words_and_operators() {
1013 assert_eq!(
1014 kinds("echo hi | grep h && true"),
1015 vec![
1016 TokenKind::Word(super::RawWord {
1017 parts: vec![RawPart::Literal("echo".to_owned())]
1018 }),
1019 TokenKind::Word(super::RawWord {
1020 parts: vec![RawPart::Literal("hi".to_owned())]
1021 }),
1022 TokenKind::Pipe,
1023 TokenKind::Word(super::RawWord {
1024 parts: vec![RawPart::Literal("grep".to_owned())]
1025 }),
1026 TokenKind::Word(super::RawWord {
1027 parts: vec![RawPart::Literal("h".to_owned())]
1028 }),
1029 TokenKind::AndAnd,
1030 TokenKind::Word(super::RawWord {
1031 parts: vec![RawPart::Literal("true".to_owned())]
1032 }),
1033 ]
1034 );
1035 }
1036
1037 #[test]
1038 fn comments_run_to_end_of_line() {
1039 assert_eq!(
1040 kinds("echo a # trailing\necho b"),
1041 vec![
1042 TokenKind::Word(super::RawWord {
1043 parts: vec![RawPart::Literal("echo".to_owned())]
1044 }),
1045 TokenKind::Word(super::RawWord {
1046 parts: vec![RawPart::Literal("a".to_owned())]
1047 }),
1048 TokenKind::Newline,
1049 TokenKind::Word(super::RawWord {
1050 parts: vec![RawPart::Literal("echo".to_owned())]
1051 }),
1052 TokenKind::Word(super::RawWord {
1053 parts: vec![RawPart::Literal("b".to_owned())]
1054 }),
1055 ]
1056 );
1057 }
1058
1059 #[test]
1060 fn a_hash_inside_a_word_is_literal() {
1061 assert_eq!(single_word("a#b"), vec![RawPart::Literal("a#b".to_owned())]);
1062 }
1063
1064 #[test]
1065 fn single_quotes_are_fully_literal() {
1066 assert_eq!(
1067 single_word(r#"'$VAR $(cmd) \n'"#),
1068 vec![RawPart::SingleQuoted(r"$VAR $(cmd) \n".to_owned())]
1069 );
1070 }
1071
1072 #[test]
1073 fn double_quotes_interpolate_parameters_and_substitutions() {
1074 assert_eq!(
1075 single_word(r#""a ${NAME} $(echo b)""#),
1076 vec![RawPart::DoubleQuoted(vec![
1077 RawPart::Literal("a ".to_owned()),
1078 RawPart::Parameter(RawParameter::Named {
1079 name: "NAME".to_owned(),
1080 indices: Vec::new(),
1081 }),
1082 RawPart::Literal(" ".to_owned()),
1083 RawPart::CommandSubstitution("echo b".to_owned()),
1084 ])]
1085 );
1086 }
1087
1088 #[test]
1089 fn nested_command_substitution_keeps_its_full_body() {
1090 assert_eq!(
1091 single_word("$(echo $(echo inner) ')' )"),
1092 vec![RawPart::CommandSubstitution(
1093 "echo $(echo inner) ')' ".to_owned()
1094 )]
1095 );
1096 }
1097
1098 #[test]
1099 fn arithmetic_expansion_is_distinguished_from_command_substitution() {
1100 assert_eq!(
1101 single_word("$(( 1 + (2 * 3) ))"),
1102 vec![RawPart::Arithmetic(" 1 + (2 * 3) ".to_owned())]
1103 );
1104 }
1105
1106 #[test]
1107 fn indexed_parameters_capture_their_index_word() {
1108 let parts = single_word("${obj[key]}");
1109 let RawPart::Parameter(RawParameter::Named { name, indices }) = &parts[0] else {
1110 panic!("expected an indexed parameter, found {parts:?}");
1111 };
1112 assert_eq!(name, "obj");
1113 assert_eq!(indices.len(), 1);
1114 assert_eq!(indices[0].as_literal(), Some("key"));
1115 }
1116
1117 #[test]
1118 fn every_dropped_parameter_expansion_is_rejected_by_name() {
1119 for (source, expected) in [
1122 ("echo ${arr[@]}", "${NAME[@]}"),
1123 ("echo ${arr[*]}", "${NAME[@]}"),
1124 ("echo ${#items}", "${#name} length expansion"),
1125 ("echo ${name:-default}", "keeps only"),
1126 ("echo ${name/a/b}", "keeps only"),
1127 ("echo ${name^^}", "keeps only"),
1128 ("echo ${}", "empty ${} parameter reference"),
1129 ("echo ${name", "unterminated"),
1130 ] {
1131 let error = tokenize(source)
1132 .map(|tokens| format!("{tokens:?}"))
1133 .expect_err(source);
1134 assert!(error.message.contains(expected), "{source}: {error}");
1135 }
1136 }
1137
1138 #[test]
1139 fn positional_parameters_cover_at_hash_and_star() {
1140 assert_eq!(
1141 single_word("$@"),
1142 vec![RawPart::Parameter(RawParameter::AllPositional)]
1143 );
1144 assert_eq!(
1145 single_word("$*"),
1146 vec![RawPart::Parameter(RawParameter::AllPositionalJoined)]
1147 );
1148 assert_eq!(
1149 single_word("${*}"),
1150 vec![RawPart::Parameter(RawParameter::AllPositionalJoined)]
1151 );
1152 assert_eq!(
1153 single_word("$#"),
1154 vec![RawPart::Parameter(RawParameter::PositionalCount)]
1155 );
1156 }
1157
1158 #[test]
1159 fn backtick_substitution_is_rejected_by_name() {
1160 for source in ["echo `echo hi`", r#"echo "`echo hi`""#, "x=`date`"] {
1161 let error = tokenize(source).expect_err("backticks are dropped");
1162 assert!(error.message.contains("backtick"), "{source}: {error}");
1163 assert!(error.message.contains("$( ... )"), "{source}: {error}");
1164 }
1165 assert_eq!(
1168 single_word(r"\`"),
1169 vec![RawPart::SingleQuoted("`".to_owned())]
1170 );
1171 assert_eq!(
1172 single_word("'`echo hi`'"),
1173 vec![RawPart::SingleQuoted("`echo hi`".to_owned())]
1174 );
1175 }
1176
1177 #[test]
1178 fn file_descriptor_redirection_is_rejected_by_name() {
1179 for source in ["echo hi 2>buf", "echo hi 2>&1", "echo hi >&2", "cmd 2>>buf"] {
1180 let error = tokenize(source).expect_err("fd redirection is dropped");
1181 assert!(
1182 error.message.contains("file-descriptor redirection"),
1183 "{source}: {error}"
1184 );
1185 }
1186 assert!(kinds("echo 2 > buf").contains(&TokenKind::Great));
1188 }
1189
1190 #[test]
1191 fn case_clause_terminators_are_tokenized_and_fall_through_is_not() {
1192 assert!(kinds("a;;").contains(&TokenKind::DoubleSemicolon));
1193 for source in ["a;;&", "a;&"] {
1194 let error = tokenize(source).expect_err("fall-through is dropped");
1195 assert!(error.message.contains("falls through"), "{source}: {error}");
1196 }
1197 }
1198
1199 #[test]
1200 fn a_here_document_collects_the_lines_after_its_operator() {
1201 let TokenKind::HereDoc(body) = &kinds("cat <<EOF\nhello\nthere\nEOF\n")[1] else {
1202 panic!("expected a here-document token");
1203 };
1204 assert_eq!(
1205 body.parts,
1206 vec![RawPart::Literal("hello\nthere".to_owned())]
1207 );
1208 }
1209
1210 #[test]
1211 fn an_unquoted_here_document_interpolates_and_a_quoted_one_does_not() {
1212 let TokenKind::HereDoc(expanded) = &kinds("cat <<EOF\nid=$id\nEOF\n")[1] else {
1213 panic!("expected a here-document token");
1214 };
1215 assert_eq!(
1216 expanded.parts,
1217 vec![
1218 RawPart::Literal("id=".to_owned()),
1219 RawPart::Parameter(RawParameter::Named {
1220 name: "id".to_owned(),
1221 indices: Vec::new(),
1222 }),
1223 ]
1224 );
1225
1226 for source in [
1228 "cat <<'EOF'\nid=$id\nEOF\n",
1229 "cat <<\"EOF\"\nid=$id\nEOF\n",
1230 "cat <<\\EOF\nid=$id\nEOF\n",
1231 ] {
1232 let TokenKind::HereDoc(literal) = &kinds(source)[1] else {
1233 panic!("expected a here-document token for {source:?}");
1234 };
1235 assert_eq!(
1236 literal.parts,
1237 vec![RawPart::Literal("id=$id".to_owned())],
1238 "{source}"
1239 );
1240 }
1241 }
1242
1243 #[test]
1244 fn a_here_document_body_keeps_backslashes_that_json_depends_on() {
1245 let TokenKind::HereDoc(body) = &kinds("cat <<EOF\n{\"a\": \"\\\"x\\\"\"}\nEOF\n")[1] else {
1248 panic!("expected a here-document token");
1249 };
1250 assert_eq!(
1251 body.parts,
1252 vec![RawPart::Literal("{\"a\": \"\\\"x\\\"\"}".to_owned())]
1253 );
1254 }
1255
1256 #[test]
1257 fn a_dash_here_document_strips_leading_tabs_from_body_and_terminator() {
1258 let TokenKind::HereDoc(body) = &kinds("cat <<-EOF\n\t\tindented\n\tEOF\n")[1] else {
1259 panic!("expected a here-document token");
1260 };
1261 assert_eq!(body.parts, vec![RawPart::Literal("indented".to_owned())]);
1262
1263 assert!(tokenize("cat <<-EOF\nbody\n EOF\n").is_err());
1265 }
1266
1267 #[test]
1268 fn the_rest_of_the_operator_line_is_ordinary_shell() {
1269 let tokens = kinds("cat <<EOF | jq .\n{\"a\":1}\nEOF\n");
1271 assert!(matches!(tokens[1], TokenKind::HereDoc(_)));
1272 assert!(tokens.contains(&TokenKind::Pipe));
1273 }
1274
1275 #[test]
1276 fn several_here_documents_on_one_line_read_their_bodies_in_order() {
1277 let tokens = kinds("f <<A <<B\nfirst\nA\nsecond\nB\n");
1278 let bodies = tokens
1279 .iter()
1280 .filter_map(|token| match token {
1281 TokenKind::HereDoc(body) => Some(body.parts.clone()),
1282 _ => None,
1283 })
1284 .collect::<Vec<_>>();
1285 assert_eq!(
1286 bodies,
1287 vec![
1288 vec![RawPart::Literal("first".to_owned())],
1289 vec![RawPart::Literal("second".to_owned())],
1290 ]
1291 );
1292 }
1293
1294 #[test]
1295 fn a_diagnostic_inside_a_here_document_body_counts_from_the_body() {
1296 let error = tokenize("echo one\ncat <<EOF\nbad `sub`\nEOF\n").expect_err("backticks");
1299 assert_eq!(error.line, 3, "{error}");
1300 }
1301
1302 #[test]
1303 fn a_here_document_delimiter_cannot_be_split_across_lines() {
1304 let error = tokenize("cat <<\\\nEOF\nbody\nEOF\n").expect_err("a split delimiter");
1307 assert!(error.message.contains("cannot be split"), "{error}");
1308 }
1309
1310 #[test]
1311 fn malformed_here_documents_are_rejected_by_name() {
1312 for (source, expected) in [
1313 ("cat <<EOF\nbody\n", "unterminated here-document"),
1314 ("cat <<EOF", "unterminated here-document"),
1315 ("cat <<\n", "expected a here-document delimiter"),
1316 ("cat <<<\"$x\"", "here-string"),
1317 ] {
1318 let error = tokenize(source).expect_err(source);
1319 assert!(error.message.contains(expected), "{source}: {error}");
1320 }
1321 }
1322
1323 #[test]
1324 fn braces_are_reserved_words_only_as_complete_words() {
1325 assert_eq!(
1326 kinds("f() { echo hi; }"),
1327 vec![
1328 TokenKind::Word(super::RawWord {
1329 parts: vec![RawPart::Literal("f".to_owned())]
1330 }),
1331 TokenKind::LeftParen,
1332 TokenKind::RightParen,
1333 TokenKind::LeftBrace,
1334 TokenKind::Word(super::RawWord {
1335 parts: vec![RawPart::Literal("echo".to_owned())]
1336 }),
1337 TokenKind::Word(super::RawWord {
1338 parts: vec![RawPart::Literal("hi".to_owned())]
1339 }),
1340 TokenKind::Semicolon,
1341 TokenKind::RightBrace,
1342 ]
1343 );
1344 assert_eq!(
1346 single_word("{a,b}"),
1347 vec![RawPart::Literal("{a,b}".to_owned())]
1348 );
1349 }
1350
1351 #[test]
1352 fn globbing_characters_are_ordinary_literals() {
1353 assert_eq!(single_word("*"), vec![RawPart::Literal("*".to_owned())]);
1354 assert_eq!(
1355 single_word("a?[b]~"),
1356 vec![RawPart::Literal("a?[b]~".to_owned())]
1357 );
1358 }
1359
1360 #[test]
1361 fn redirection_and_backgrounding_operators_are_tokenized_not_dropped() {
1362 assert!(kinds("echo hi > buf").contains(&TokenKind::Great));
1363 assert!(kinds("echo hi >> buf").contains(&TokenKind::GreatGreat));
1364 assert!(kinds("sleep 1 &").contains(&TokenKind::Ampersand));
1365 assert!(kinds("cat < f").contains(&TokenKind::Less));
1366 assert!(kinds("diff <(a) b").contains(&TokenKind::LessParen));
1367 }
1368
1369 #[test]
1370 fn unterminated_quotes_are_reported_with_a_line() {
1371 let error = tokenize("echo 'open").expect_err("unterminated quote");
1372 assert_eq!(error.line, 1);
1373 assert!(error.message.contains("unterminated"), "{error}");
1374 }
1375}