1use std::borrow::Cow;
2use std::sync::Arc;
3
4use utf8_chars::BufReadCharsExt;
5
6use crate::{SourcePosition, SourceSpan};
7
8mod char_reader;
9use char_reader::CharReader;
10
11#[derive(Clone, Debug)]
12pub(crate) enum TokenEndReason {
13 EndOfInput,
15 UnescapedNewLine,
17 ExpansionClosingChar,
21 NonNewLineBlank,
23 HereDocumentBodyStart,
25 HereDocumentBodyEnd,
27 HereDocumentEndTag,
29 OperatorStart,
31 OperatorEnd,
33 Other,
35}
36
37pub type TokenLocation = SourceSpan;
39
40#[derive(Clone, Debug)]
42#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
43#[cfg_attr(
44 any(test, feature = "serde"),
45 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
46)]
47pub enum Token {
48 Operator(String, SourceSpan),
50 Word(String, SourceSpan),
52}
53
54impl Token {
55 pub fn to_str(&self) -> &str {
57 match self {
58 Self::Operator(s, _) => s,
59 Self::Word(s, _) => s,
60 }
61 }
62
63 pub const fn location(&self) -> &SourceSpan {
65 match self {
66 Self::Operator(_, l) => l,
67 Self::Word(_, l) => l,
68 }
69 }
70}
71
72#[cfg(feature = "diagnostics")]
73impl From<&Token> for miette::SourceSpan {
74 fn from(token: &Token) -> Self {
75 let start = token.location().start.as_ref();
76 Self::new(start.into(), token.location().length())
77 }
78}
79
80#[derive(Clone, Debug)]
82pub(crate) struct TokenizeResult {
83 pub reason: TokenEndReason,
85 pub token: Option<Token>,
87}
88
89#[derive(thiserror::Error, Debug)]
91pub enum TokenizerError {
92 #[error("unterminated escape sequence")]
95 UnterminatedEscapeSequence,
96
97 #[error("unterminated single quote at {0}")]
100 UnterminatedSingleQuote(SourcePosition),
101
102 #[error("unterminated ANSI C quote at {0}")]
105 UnterminatedAnsiCQuote(SourcePosition),
106
107 #[error("unterminated double quote at {0}")]
110 UnterminatedDoubleQuote(SourcePosition),
111
112 #[error("unterminated backquote near {0}")]
115 UnterminatedBackquote(SourcePosition),
116
117 #[error("unterminated extglob near {0}")]
120 UnterminatedExtendedGlob(SourcePosition),
121
122 #[error("unterminated variable expression")]
125 UnterminatedVariable,
126
127 #[error("unterminated command substitution")]
130 UnterminatedCommandSubstitution,
131
132 #[error("unterminated expansion")]
135 UnterminatedExpansion,
136
137 #[error("failed to decode UTF-8 characters")]
139 FailedDecoding,
140
141 #[error("missing here tag for here document body")]
145 MissingHereTagForDocumentBody,
146
147 #[error("missing here-document tag before {0:?}")]
151 MissingHereTag(String),
152
153 #[error("missing here-document tag at end of input")]
160 MissingHereTagAtEndOfInput,
161
162 #[error("unterminated here document; tag(s) {0} declared at {1}")]
165 UnterminatedHereDocuments(String, String),
166
167 #[error("expansions nested too deeply (limit: {})", MAX_EXPANSION_NESTING)]
171 ExpansionNestingTooDeep,
172
173 #[error("failed to read input")]
175 ReadError(#[from] std::io::Error),
176}
177
178impl TokenizerError {
179 pub const fn is_incomplete(&self) -> bool {
182 matches!(
183 self,
184 Self::UnterminatedEscapeSequence
185 | Self::UnterminatedAnsiCQuote(..)
186 | Self::UnterminatedSingleQuote(..)
187 | Self::UnterminatedDoubleQuote(..)
188 | Self::UnterminatedBackquote(..)
189 | Self::UnterminatedCommandSubstitution
190 | Self::UnterminatedExpansion
191 | Self::UnterminatedVariable
192 | Self::UnterminatedExtendedGlob(..)
193 | Self::UnterminatedHereDocuments(..)
194 | Self::MissingHereTagAtEndOfInput
195 )
196 }
197}
198
199#[derive(Debug)]
201pub(crate) struct Tokens<'a> {
202 pub tokens: &'a [Token],
204}
205
206#[derive(Clone, Debug)]
207enum QuoteMode {
208 None,
209 AnsiC(SourcePosition),
210 Single(SourcePosition),
211 Double(SourcePosition),
212}
213
214#[derive(Clone, Debug, Default)]
215enum HereState {
216 #[default]
218 None,
219 NextTokenIsHereTag { remove_tabs: bool },
222 CurrentTokenIsHereTag {
225 remove_tabs: bool,
226 operator_token_result: TokenizeResult,
227 },
228 NextLineIsHereDoc,
231 InHereDocs,
234}
235
236impl HereState {
237 const fn awaiting_tag(&self) -> bool {
240 matches!(
241 self,
242 Self::NextTokenIsHereTag { .. } | Self::CurrentTokenIsHereTag { .. }
243 )
244 }
245}
246
247#[derive(Clone, Debug)]
248struct HereTag {
249 tag: String,
250 tag_was_escaped_or_quoted: bool,
251 remove_tabs: bool,
252 position: SourcePosition,
253 tokens: Vec<TokenizeResult>,
254 pending_tokens_after: Vec<TokenizeResult>,
255}
256
257#[derive(Clone, Debug)]
258struct CrossTokenParseState {
259 cursor: SourcePosition,
261 queued_tokens: Vec<TokenizeResult>,
264 frame: ExpansionFrame,
266}
267
268#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272enum NestedExpansion {
273 CommandSubstitution,
275 ArithmeticExpansion,
277 LegacyArithmeticExpansion,
279 ParameterExpansion,
281}
282
283impl NestedExpansion {
284 const fn closing_char(self) -> char {
286 match self {
287 Self::CommandSubstitution | Self::ArithmeticExpansion => ')',
288 Self::LegacyArithmeticExpansion => ']',
289 Self::ParameterExpansion => '}',
290 }
291 }
292
293 const fn is_arithmetic(self) -> bool {
296 matches!(
297 self,
298 Self::ArithmeticExpansion | Self::LegacyArithmeticExpansion
299 )
300 }
301
302 const fn opening_char(self) -> Option<char> {
306 match self {
307 Self::CommandSubstitution | Self::ArithmeticExpansion => Some('('),
308 Self::LegacyArithmeticExpansion => Some('['),
309 Self::ParameterExpansion => None,
310 }
311 }
312
313 const fn closers_after_opening(self) -> usize {
317 match self {
318 Self::ArithmeticExpansion => 2,
319 _ => 1,
320 }
321 }
322
323 const fn unterminated_error(self) -> TokenizerError {
325 match self {
326 Self::ParameterExpansion => TokenizerError::UnterminatedVariable,
327 _ => TokenizerError::UnterminatedExpansion,
328 }
329 }
330}
331
332#[derive(Clone, Debug, Default)]
340struct ExpansionFrame {
341 expansion: Option<NestedExpansion>,
343 depth: u32,
345 here_state: HereState,
347 current_here_tags: Vec<HereTag>,
350 closers_needed: usize,
355 arithmetic_command_parens: usize,
360}
361
362impl ExpansionFrame {
363 fn note_unquoted_char(&mut self, c: char) {
368 let Some(expansion) = self.expansion else {
369 return;
370 };
371 if expansion.opening_char() == Some(c) {
372 self.closers_needed += 1;
373 } else if expansion.closing_char() == c {
374 debug_assert!(self.closers_needed > 1, "read the expansion's closing char");
377 self.closers_needed -= 1;
378 }
379 }
380}
381
382#[derive(Clone, Debug, Hash, Eq, PartialEq)]
384pub struct TokenizerOptions {
385 pub enable_extended_globbing: bool,
387 pub posix_mode: bool,
389 pub sh_mode: bool,
391}
392
393impl Default for TokenizerOptions {
394 fn default() -> Self {
395 Self {
396 enable_extended_globbing: true,
397 posix_mode: false,
398 sh_mode: false,
399 }
400 }
401}
402
403pub(crate) const MAX_EXPANSION_NESTING: u32 = 32;
413
414pub(crate) struct Tokenizer<'a, R: ?Sized + std::io::BufRead> {
416 char_reader: CharReader<utf8_chars::Chars<'a, R>>,
417 unread: Option<UnreadChars>,
420 read_bodies_left_pending: bool,
426 cross_state: CrossTokenParseState,
427 options: TokenizerOptions,
428}
429
430struct UnreadChars {
432 chars: Vec<char>,
434 resume_at: SourcePosition,
438}
439
440#[derive(Clone, Debug)]
442struct TokenParseState {
443 pub start_position: SourcePosition,
444 pub token_so_far: String,
445 pub token_is_operator: bool,
446 pub in_escape: bool,
447 pub quote_mode: QuoteMode,
448}
449
450impl TokenParseState {
451 pub fn new(start_position: &SourcePosition) -> Self {
452 Self {
453 start_position: start_position.to_owned(),
454 token_so_far: String::new(),
455 token_is_operator: false,
456 in_escape: false,
457 quote_mode: QuoteMode::None,
458 }
459 }
460
461 pub fn pop(&mut self, end_position: &SourcePosition) -> Token {
462 let end = Arc::new(end_position.to_owned());
463 let token_location = SourceSpan {
464 start: Arc::new(std::mem::take(&mut self.start_position)),
465 end,
466 };
467
468 let token = if std::mem::take(&mut self.token_is_operator) {
469 Token::Operator(std::mem::take(&mut self.token_so_far), token_location)
470 } else {
471 Token::Word(std::mem::take(&mut self.token_so_far), token_location)
472 };
473
474 end_position.clone_into(&mut self.start_position);
475 self.in_escape = false;
476 self.quote_mode = QuoteMode::None;
477
478 token
479 }
480
481 pub const fn started_token(&self) -> bool {
482 !self.token_so_far.is_empty()
483 }
484
485 pub fn only_blanks_so_far(&self) -> bool {
492 !self.token_so_far.is_empty() && self.token_so_far.chars().all(is_blank)
493 }
494
495 pub fn append_char(&mut self, c: char) {
496 self.token_so_far.push(c);
497 }
498
499 pub fn append_str(&mut self, s: &str) {
500 self.token_so_far.push_str(s);
501 }
502
503 pub const fn unquoted(&self) -> bool {
504 !self.in_escape && matches!(self.quote_mode, QuoteMode::None)
505 }
506
507 pub fn current_token(&self) -> &str {
508 &self.token_so_far
509 }
510
511 pub fn is_specific_operator(&self, operator: &str) -> bool {
512 self.token_is_operator && self.current_token() == operator
513 }
514
515 pub const fn in_operator(&self) -> bool {
516 self.token_is_operator
517 }
518
519 fn is_newline(&self) -> bool {
520 self.token_so_far == "\n"
521 }
522
523 fn replace_with_here_doc(&mut self, s: String) {
524 self.token_so_far = s;
525 }
526
527 #[allow(clippy::too_many_lines)]
537 pub fn delimit_current_token(
538 &mut self,
539 reason: TokenEndReason,
540 cross_token_state: &mut CrossTokenParseState,
541 ) -> Result<Option<TokenizeResult>, TokenizerError> {
542 if !self.started_token() && !matches!(reason, TokenEndReason::HereDocumentBodyEnd) {
545 return Ok(Some(TokenizeResult {
546 reason,
547 token: None,
548 }));
549 }
550
551 let current_here_state = std::mem::take(&mut cross_token_state.frame.here_state);
552 match current_here_state {
553 HereState::NextTokenIsHereTag { remove_tabs } => {
554 let operator_token_result = TokenizeResult {
558 reason,
559 token: Some(self.pop(&cross_token_state.cursor)),
560 };
561
562 cross_token_state.frame.here_state = HereState::CurrentTokenIsHereTag {
563 remove_tabs,
564 operator_token_result,
565 };
566
567 return Ok(None);
568 }
569 HereState::CurrentTokenIsHereTag {
570 remove_tabs,
571 operator_token_result,
572 } => {
573 if self.token_is_operator {
576 return Err(TokenizerError::MissingHereTag(
577 self.current_token().to_owned(),
578 ));
579 }
580
581 if self.only_blanks_so_far() {
587 self.pop(&cross_token_state.cursor);
588 cross_token_state.frame.here_state = HereState::CurrentTokenIsHereTag {
589 remove_tabs,
590 operator_token_result,
591 };
592 return Ok(None);
593 }
594
595 cross_token_state.frame.here_state = HereState::NextLineIsHereDoc;
596
597 let tag = std::format!("{}\n", self.current_token().trim_start_matches(is_blank));
602 let tag_was_escaped_or_quoted = tag.contains(is_quoting_char);
603
604 let tag_token_result = TokenizeResult {
605 reason,
606 token: Some(self.pop(&cross_token_state.cursor)),
607 };
608
609 cross_token_state.frame.current_here_tags.push(HereTag {
610 tag,
611 tag_was_escaped_or_quoted,
612 remove_tabs,
613 position: cross_token_state.cursor.clone(),
614 tokens: vec![operator_token_result, tag_token_result],
615 pending_tokens_after: vec![],
616 });
617
618 return Ok(None);
619 }
620 HereState::NextLineIsHereDoc => {
621 if self.is_newline() {
622 cross_token_state.frame.here_state = HereState::InHereDocs;
623 } else {
624 cross_token_state.frame.here_state = HereState::NextLineIsHereDoc;
625 }
626
627 if let Some(last_here_tag) = cross_token_state.frame.current_here_tags.last_mut() {
628 let token = self.pop(&cross_token_state.cursor);
629 let result = TokenizeResult {
630 reason,
631 token: Some(token),
632 };
633
634 last_here_tag.pending_tokens_after.push(result);
635 } else {
636 return Err(TokenizerError::MissingHereTagForDocumentBody);
637 }
638
639 return Ok(None);
640 }
641 HereState::InHereDocs => {
642 let completed_here_tag = cross_token_state.frame.current_here_tags.remove(0);
644
645 cross_token_state
647 .queued_tokens
648 .extend(completed_here_tag.tokens);
649
650 cross_token_state.queued_tokens.push(TokenizeResult {
652 reason: TokenEndReason::HereDocumentBodyStart,
653 token: None,
654 });
655
656 cross_token_state.queued_tokens.push(TokenizeResult {
658 reason,
659 token: Some(self.pop(&cross_token_state.cursor)),
660 });
661
662 let end_tag = if completed_here_tag.tag_was_escaped_or_quoted {
664 unquote_str(&completed_here_tag.tag)
665 } else {
666 completed_here_tag.tag
667 };
668 self.append_str(end_tag.trim_end_matches('\n'));
669 cross_token_state.queued_tokens.push(TokenizeResult {
670 reason: TokenEndReason::HereDocumentEndTag,
671 token: Some(self.pop(&cross_token_state.cursor)),
672 });
673
674 cross_token_state
678 .queued_tokens
679 .extend(completed_here_tag.pending_tokens_after);
680
681 if cross_token_state.frame.current_here_tags.is_empty() {
682 cross_token_state.frame.here_state = HereState::None;
683 } else {
684 cross_token_state.frame.here_state = HereState::InHereDocs;
685 }
686
687 return Ok(None);
688 }
689 HereState::None => (),
690 }
691
692 let token = self.pop(&cross_token_state.cursor);
693 let result = TokenizeResult {
694 reason,
695 token: Some(token),
696 };
697
698 Ok(Some(result))
699 }
700}
701
702pub fn tokenize_str(input: &str) -> Result<Vec<Token>, TokenizerError> {
729 tokenize_str_with_options(input, &TokenizerOptions::default())
730}
731
732pub fn tokenize_str_with_options(
760 input: &str,
761 options: &TokenizerOptions,
762) -> Result<Vec<Token>, TokenizerError> {
763 uncached_tokenize_string(input, options)
764}
765
766#[cached::macros::cached(
767 name = "TOKENIZE_CACHE",
768 max_size = 64,
769 key = "(String, TokenizerOptions)",
770 convert = r#"{ (input.to_owned(), options.to_owned()) }"#
771)]
772fn uncached_tokenize_string(
773 input: &str,
774 options: &TokenizerOptions,
775) -> Result<Vec<Token>, TokenizerError> {
776 uncached_tokenize_str(input, options)
777}
778
779pub fn uncached_tokenize_str(
807 input: &str,
808 options: &TokenizerOptions,
809) -> Result<Vec<Token>, TokenizerError> {
810 let mut reader = std::io::BufReader::new(input.as_bytes());
811 let mut tokenizer = crate::tokenizer::Tokenizer::new(&mut reader, options);
812
813 let mut tokens = vec![];
814 loop {
815 match tokenizer.next_token()? {
816 TokenizeResult {
817 token: Some(token), ..
818 } => tokens.push(token),
819 TokenizeResult {
820 reason: TokenEndReason::EndOfInput,
821 ..
822 } => break,
823 _ => (),
824 }
825 }
826
827 Ok(tokens)
828}
829
830pub(crate) fn command_substitution_body<'a>(
844 input: &'a str,
845 options: &TokenizerOptions,
846) -> Result<&'a str, TokenizerError> {
847 expansion_body(input, options, NestedExpansion::CommandSubstitution)
848}
849
850pub(crate) fn legacy_arithmetic_expansion_body<'a>(
855 input: &'a str,
856 options: &TokenizerOptions,
857) -> Result<&'a str, TokenizerError> {
858 expansion_body(input, options, NestedExpansion::LegacyArithmeticExpansion)
859}
860
861fn expansion_body<'a>(
862 input: &'a str,
863 options: &TokenizerOptions,
864 expansion: NestedExpansion,
865) -> Result<&'a str, TokenizerError> {
866 let mut reader = input.as_bytes();
867 let mut tokenizer = Tokenizer::new(&mut reader, options);
868 tokenizer.read_bodies_left_pending = false;
869
870 let mut state = TokenParseState::new(&tokenizer.cross_state.cursor);
874 tokenizer.consume_nested_expansion(&mut state, expansion)?;
875
876 let body_len = input
879 .chars()
880 .take(tokenizer.cross_state.cursor.index - 1)
881 .map(char::len_utf8)
882 .sum();
883 Ok(input.split_at(body_len).0)
884}
885
886impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {
887 pub fn new(reader: &'a mut R, options: &TokenizerOptions) -> Self {
888 Tokenizer {
889 options: options.clone(),
890 char_reader: CharReader::new(reader.chars()),
891 unread: None,
892 read_bodies_left_pending: true,
893 cross_state: CrossTokenParseState {
894 cursor: SourcePosition {
895 index: 0,
896 line: 1,
897 column: 1,
898 },
899 queued_tokens: vec![],
900 frame: ExpansionFrame::default(),
901 },
902 }
903 }
904
905 #[expect(clippy::unnecessary_wraps)]
906 pub fn current_location(&self) -> Option<SourcePosition> {
907 Some(self.cross_state.cursor.clone())
908 }
909
910 fn next_char(&mut self) -> Result<Option<char>, TokenizerError> {
911 let c = match self.unread.as_mut().and_then(|unread| unread.chars.pop()) {
912 Some(c) => Some(c),
913 None => self
914 .char_reader
915 .next()
916 .transpose()
917 .map_err(TokenizerError::ReadError)?,
918 };
919
920 if let Some(ch) = c {
921 if ch == '\n' {
922 self.cross_state.cursor.line += 1;
923 self.cross_state.cursor.column = 1;
924 } else {
925 self.cross_state.cursor.column += 1;
926 }
927 self.cross_state.cursor.index += 1;
928 }
929
930 if let Some(unread) = self.unread.take_if(|unread| unread.chars.is_empty()) {
931 self.cross_state.cursor = unread.resume_at;
932 }
933
934 Ok(c)
935 }
936
937 fn consume_char(&mut self) -> Result<(), TokenizerError> {
938 let _ = self.next_char()?;
939 Ok(())
940 }
941
942 fn peek_char(&mut self) -> Result<Option<char>, TokenizerError> {
943 if let Some(c) = self.unread.as_ref().and_then(|unread| unread.chars.last()) {
944 return Ok(Some(*c));
945 }
946
947 match self.char_reader.peek() {
948 Some(result) => match result {
949 Ok(c) => Ok(Some(*c)),
950 Err(_) => Err(TokenizerError::FailedDecoding),
951 },
952 None => Ok(None),
953 }
954 }
955
956 fn peek_second_char(&mut self) -> Result<Option<char>, TokenizerError> {
958 let peeked = match self
961 .unread
962 .as_ref()
963 .map_or(&[][..], |unread| unread.chars.as_slice())
964 {
965 [.., second, _] => return Ok(Some(*second)),
966 [_] => self.char_reader.peek(),
967 [] => self.char_reader.peek_second(),
968 };
969
970 match peeked {
971 Some(Ok(c)) => Ok(Some(*c)),
972 Some(Err(_)) => Err(TokenizerError::FailedDecoding),
973 None => Ok(None),
974 }
975 }
976
977 fn skip_line_continuations(&mut self) -> Result<(), TokenizerError> {
985 while self.peek_char()? == Some('\\') && self.peek_second_char()? == Some('\n') {
986 self.consume_char()?;
987 self.consume_char()?;
988 }
989 Ok(())
990 }
991
992 fn backslash_is_literal(&self, state: &TokenParseState) -> bool {
997 if state.in_escape || matches!(state.quote_mode, QuoteMode::Single(_) | QuoteMode::AnsiC(_))
998 {
999 return true;
1000 }
1001
1002 let frame = &self.cross_state.frame;
1005 let in_here_doc_body = match frame.here_state {
1006 HereState::InHereDocs => true,
1007 HereState::NextLineIsHereDoc => state.is_newline(),
1008 _ => false,
1009 };
1010 if !in_here_doc_body {
1011 return false;
1012 }
1013
1014 frame
1018 .current_here_tags
1019 .first()
1020 .is_some_and(|tag| tag.tag_was_escaped_or_quoted)
1021 }
1022
1023 fn consume_nested_expansion(
1027 &mut self,
1028 state: &mut TokenParseState,
1029 expansion: NestedExpansion,
1030 ) -> Result<(), TokenizerError> {
1031 let depth = self.cross_state.frame.depth + 1;
1035 if depth > MAX_EXPANSION_NESTING {
1036 return Err(TokenizerError::ExpansionNestingTooDeep);
1037 }
1038
1039 let frame = ExpansionFrame {
1045 expansion: Some(expansion),
1046 depth,
1047 closers_needed: expansion.closers_after_opening(),
1048 ..ExpansionFrame::default()
1049 };
1050
1051 debug_assert!(self.cross_state.queued_tokens.is_empty());
1056 let enclosing = std::mem::replace(&mut self.cross_state.frame, frame);
1057 let result = self.consume_expansion_contents(state, expansion);
1058 debug_assert!(result.is_err() || self.cross_state.queued_tokens.is_empty());
1059 self.cross_state.frame = enclosing;
1060 result
1061 }
1062
1063 fn consume_expansion_contents(
1066 &mut self,
1067 state: &mut TokenParseState,
1068 expansion: NestedExpansion,
1069 ) -> Result<(), TokenizerError> {
1070 let mut bodies = String::new();
1071
1072 loop {
1073 let result = self.next_token()?;
1074
1075 match result.reason {
1076 TokenEndReason::UnescapedNewLine => state.append_str(&std::mem::take(&mut bodies)),
1078 TokenEndReason::ExpansionClosingChar => {
1079 let closing_char = self
1081 .next_char()?
1082 .ok_or_else(|| expansion.unterminated_error())?;
1083
1084 let here_state = &self.cross_state.frame.here_state;
1085 if here_state.awaiting_tag() {
1086 return Err(TokenizerError::MissingHereTag(closing_char.to_string()));
1089 } else if matches!(here_state, HereState::NextLineIsHereDoc)
1090 && self.read_bodies_left_pending
1091 {
1092 self.read_here_doc_bodies_left_pending(state)?;
1093 }
1094
1095 state.append_char(closing_char);
1096 return Ok(());
1097 }
1098 TokenEndReason::EndOfInput => return Err(expansion.unterminated_error()),
1099 _ => (),
1100 }
1101
1102 append_expansion_token(state, &mut bodies, &result);
1103 }
1104 }
1105
1106 fn read_here_doc_bodies_left_pending(
1117 &mut self,
1118 state: &mut TokenParseState,
1119 ) -> Result<(), TokenizerError> {
1120 let rest_of_line_start = self.cross_state.cursor.clone();
1121 let mut rest_of_line = vec![];
1122 while let Some(c) = self.next_char()? {
1123 rest_of_line.push(c);
1124 if c == '\n' {
1125 break;
1126 }
1127 }
1128
1129 self.cross_state.frame.here_state = HereState::InHereDocs;
1133 let mut bodies = String::new();
1134 while !matches!(self.cross_state.frame.here_state, HereState::None)
1135 || !self.cross_state.queued_tokens.is_empty()
1136 {
1137 let result = self.next_token()?;
1138 append_expansion_token(state, &mut bodies, &result);
1139 }
1140 state.append_str(&bodies);
1141 state.append_char('\n');
1142
1143 if !rest_of_line.is_empty() {
1144 rest_of_line.reverse();
1145 let resume_at = std::mem::replace(&mut self.cross_state.cursor, rest_of_line_start);
1146 self.unread = Some(UnreadChars {
1147 chars: rest_of_line,
1148 resume_at,
1149 });
1150 }
1151
1152 Ok(())
1153 }
1154
1155 #[allow(clippy::unwrap_in_result)]
1161 fn consume_dollar_or_backquote(
1162 &mut self,
1163 state: &mut TokenParseState,
1164 c: char,
1165 in_double_quotes: bool,
1166 ) -> Result<(), TokenizerError> {
1167 if c == '$' {
1168 self.consume_char()?;
1170 self.skip_line_continuations()?;
1171
1172 let char_after_dollar_sign = self.peek_char()?;
1174 match char_after_dollar_sign {
1175 Some('(') => {
1176 state.append_char('$');
1178
1179 state.append_char(self.next_char()?.unwrap());
1181
1182 self.skip_line_continuations()?;
1185 let expansion = if matches!(self.peek_char()?, Some('(')) {
1186 state.append_char(self.next_char()?.unwrap());
1188 NestedExpansion::ArithmeticExpansion
1189 } else {
1190 NestedExpansion::CommandSubstitution
1191 };
1192
1193 self.consume_nested_expansion(state, expansion)?;
1194 }
1195
1196 Some('[') => {
1197 state.append_char('$');
1199
1200 state.append_char(self.next_char()?.unwrap());
1202
1203 self.consume_nested_expansion(
1204 state,
1205 NestedExpansion::LegacyArithmeticExpansion,
1206 )?;
1207 }
1208
1209 Some('{') => {
1210 state.append_char('$');
1212
1213 state.append_char(self.next_char()?.unwrap());
1215
1216 self.consume_nested_expansion(state, NestedExpansion::ParameterExpansion)?;
1217 }
1218 Some('$') => {
1220 self.consume_char()?;
1221 state.append_str("$$");
1222 }
1223
1224 Some('\'') if !in_double_quotes => {
1226 state.append_char('$');
1227 state.quote_mode = QuoteMode::AnsiC(self.cross_state.cursor.clone());
1228 state.append_char(self.next_char()?.unwrap());
1229 }
1230
1231 _ => {
1232 state.append_char('$');
1236 }
1237 }
1238 } else {
1239 let backquote_pos = self.cross_state.cursor.clone();
1242 self.consume_char()?;
1243
1244 state.append_char(c);
1246
1247 let mut escaping_enabled = false;
1249 let mut done = false;
1250 while !done {
1251 if !escaping_enabled {
1255 self.skip_line_continuations()?;
1256 }
1257
1258 let next_char_in_backquote = self.next_char()?;
1260 if let Some(cib) = next_char_in_backquote {
1261 state.append_char(cib);
1263
1264 if !escaping_enabled && cib == '\\' {
1266 escaping_enabled = true;
1267 } else {
1268 if !escaping_enabled && cib == '`' {
1270 done = true;
1271 }
1272 escaping_enabled = false;
1273 }
1274 } else {
1275 return Err(TokenizerError::UnterminatedBackquote(backquote_pos));
1276 }
1277 }
1278 }
1279
1280 Ok(())
1281 }
1282
1283 #[expect(clippy::cognitive_complexity)]
1301 #[expect(clippy::if_same_then_else)]
1302 #[expect(clippy::panic_in_result_fn)]
1303 #[expect(clippy::too_many_lines)]
1304 #[allow(clippy::unwrap_in_result)]
1305 pub fn next_token(&mut self) -> Result<TokenizeResult, TokenizerError> {
1306 let mut state = TokenParseState::new(&self.cross_state.cursor);
1307 let mut result: Option<TokenizeResult> = None;
1308
1309 let expansion = self.cross_state.frame.expansion;
1310 let closing_char = expansion.map(NestedExpansion::closing_char);
1311 let include_space = expansion.is_some();
1312 let in_parameter_expansion = expansion == Some(NestedExpansion::ParameterExpansion);
1315 let in_arithmetic_expansion = expansion.is_some_and(NestedExpansion::is_arithmetic);
1316
1317 while result.is_none() {
1318 if !self.cross_state.queued_tokens.is_empty() {
1321 return Ok(self.cross_state.queued_tokens.remove(0));
1322 }
1323
1324 let mut next = self.peek_char()?;
1329 if next == Some('\\')
1330 && self.peek_second_char()? == Some('\n')
1331 && !self.backslash_is_literal(&state)
1332 {
1333 self.skip_line_continuations()?;
1334 if !state.started_token() {
1335 state.start_position = self.cross_state.cursor.clone();
1336 }
1337 next = self.peek_char()?;
1338 }
1339 let c = next.unwrap_or('\0');
1340
1341 if next.is_none() {
1344 if state.in_escape {
1347 return Err(TokenizerError::UnterminatedEscapeSequence);
1348 }
1349 match state.quote_mode {
1350 QuoteMode::None => (),
1351 QuoteMode::AnsiC(pos) => {
1352 return Err(TokenizerError::UnterminatedAnsiCQuote(pos));
1353 }
1354 QuoteMode::Single(pos) => {
1355 return Err(TokenizerError::UnterminatedSingleQuote(pos));
1356 }
1357 QuoteMode::Double(pos) => {
1358 return Err(TokenizerError::UnterminatedDoubleQuote(pos));
1359 }
1360 }
1361
1362 if !matches!(self.cross_state.frame.here_state, HereState::None) {
1364 if self.cross_state.frame.here_state.awaiting_tag() && state.started_token() {
1369 state.delimit_current_token(
1370 TokenEndReason::EndOfInput,
1371 &mut self.cross_state,
1372 )?;
1373 }
1374 if self.cross_state.frame.here_state.awaiting_tag() {
1375 return Err(TokenizerError::MissingHereTagAtEndOfInput);
1376 }
1377
1378 if matches!(self.cross_state.frame.here_state, HereState::InHereDocs)
1382 && self.remove_here_end_tag(&mut state, &mut result, false)?
1383 {
1384 continue;
1387 }
1388
1389 let tag_names = self
1390 .cross_state
1391 .frame
1392 .current_here_tags
1393 .iter()
1394 .map(|tag| std::format!("`{}`", tag.tag.trim_end_matches('\n')))
1395 .collect::<Vec<_>>()
1396 .join(", ");
1397 let tag_positions = self
1398 .cross_state
1399 .frame
1400 .current_here_tags
1401 .iter()
1402 .map(|tag| std::format!("{}", tag.position))
1403 .collect::<Vec<_>>()
1404 .join(", ");
1405 return Err(TokenizerError::UnterminatedHereDocuments(
1406 tag_names,
1407 tag_positions,
1408 ));
1409 }
1410
1411 result = state
1412 .delimit_current_token(TokenEndReason::EndOfInput, &mut self.cross_state)?;
1413 } else if matches!(self.cross_state.frame.here_state, HereState::InHereDocs) {
1417 if !self.cross_state.frame.current_here_tags.is_empty()
1423 && self.cross_state.frame.current_here_tags[0].remove_tabs
1424 && (!state.started_token() || state.current_token().ends_with('\n'))
1425 && c == '\t'
1426 {
1427 self.consume_char()?;
1429 } else {
1430 self.consume_char()?;
1431 state.append_char(c);
1432
1433 if c == '\n' {
1436 self.remove_here_end_tag(&mut state, &mut result, true)?;
1437 } else if c == '\\'
1438 && self
1439 .cross_state
1440 .frame
1441 .current_here_tags
1442 .first()
1443 .is_some_and(|tag| !tag.tag_was_escaped_or_quoted)
1444 {
1445 if let Some(escaped) = self.next_char()? {
1451 state.append_char(escaped);
1452 }
1453 }
1454 }
1455 } else if state.unquoted()
1466 && !state.in_operator()
1467 && closing_char == Some(c)
1468 && self.cross_state.frame.closers_needed == 1
1469 {
1470 let reason = if state.started_token() {
1471 TokenEndReason::Other
1472 } else {
1473 TokenEndReason::ExpansionClosingChar
1474 };
1475 result = state.delimit_current_token(reason, &mut self.cross_state)?;
1476 } else if state.in_operator() {
1477 let mut hypothetical_token = state.current_token().to_owned();
1484 hypothetical_token.push(c);
1485
1486 if state.unquoted() && self.is_operator(hypothetical_token.as_ref()) {
1487 self.consume_char()?;
1488 state.append_char(c);
1489 } else {
1490 assert!(state.started_token());
1491
1492 if in_arithmetic_expansion || in_parameter_expansion {
1503 } else if self.cross_state.frame.arithmetic_command_parens > 0 {
1507 let parens = &mut self.cross_state.frame.arithmetic_command_parens;
1510 if state.is_specific_operator("(") {
1511 *parens += 1;
1512 } else if state.is_specific_operator(")") {
1513 *parens -= 1;
1514 }
1515 } else if state.is_specific_operator("<<") {
1516 self.cross_state.frame.here_state =
1517 HereState::NextTokenIsHereTag { remove_tabs: false };
1518 } else if state.is_specific_operator("<<-") {
1519 self.cross_state.frame.here_state =
1520 HereState::NextTokenIsHereTag { remove_tabs: true };
1521 } else if state.is_specific_operator("(") && c == '(' {
1522 self.cross_state.frame.arithmetic_command_parens = 1;
1524 }
1525
1526 let reason = if state.current_token() == "\n" {
1527 TokenEndReason::UnescapedNewLine
1528 } else {
1529 TokenEndReason::OperatorEnd
1530 };
1531
1532 result = state.delimit_current_token(reason, &mut self.cross_state)?;
1533 }
1534 } else if does_char_newly_affect_quoting(&state, c) {
1539 if c == '\\' {
1540 self.consume_char()?;
1541 state.in_escape = true;
1542 state.append_char(c);
1543 } else if c == '\'' {
1544 state.quote_mode = QuoteMode::Single(self.cross_state.cursor.clone());
1547 self.consume_char()?;
1548 state.append_char(c);
1549 } else if c == '\"' {
1550 state.quote_mode = QuoteMode::Double(self.cross_state.cursor.clone());
1551 self.consume_char()?;
1552 state.append_char(c);
1553 }
1554 }
1555 else if !state.in_escape
1558 && matches!(
1559 state.quote_mode,
1560 QuoteMode::Single(..) | QuoteMode::AnsiC(..)
1561 )
1562 && c == '\''
1563 {
1564 state.quote_mode = QuoteMode::None;
1565 self.consume_char()?;
1566 state.append_char(c);
1567 } else if !state.in_escape
1568 && matches!(state.quote_mode, QuoteMode::Double(..))
1569 && c == '\"'
1570 {
1571 state.quote_mode = QuoteMode::None;
1572 self.consume_char()?;
1573 state.append_char(c);
1574 }
1575 else if state.in_escape {
1579 state.in_escape = false;
1580 self.consume_char()?;
1581 state.append_char(c);
1582 } else if (state.unquoted()
1583 || (matches!(state.quote_mode, QuoteMode::Double(_)) && !state.in_escape))
1584 && (c == '$' || c == '`')
1585 {
1586 let in_double_quotes = matches!(state.quote_mode, QuoteMode::Double(_));
1588 self.consume_dollar_or_backquote(&mut state, c, in_double_quotes)?;
1589 }
1590 else if c == '('
1596 && self.options.enable_extended_globbing
1597 && !in_parameter_expansion
1598 && state.unquoted()
1599 && !state.in_operator()
1600 && state
1601 .current_token()
1602 .ends_with(|x| Self::can_start_extglob(x))
1603 {
1604 self.consume_char()?;
1606 state.append_char(c);
1607
1608 let mut paren_depth = 1;
1609 let mut quote: Option<(char, bool)> = None;
1613 let mut after_dollar = false;
1614
1615 while paren_depth > 0 {
1622 if !matches!(quote, Some(('\'', _))) {
1625 self.skip_line_continuations()?;
1626 }
1627 let Some(extglob_char) = self.peek_char()? else {
1628 return Err(TokenizerError::UnterminatedExtendedGlob(
1629 self.cross_state.cursor.clone(),
1630 ));
1631 };
1632 let was_after_dollar = std::mem::take(&mut after_dollar);
1633
1634 let starts_nested_construct = match quote {
1635 None => extglob_char == '`',
1636 Some(('"', _)) => matches!(extglob_char, '`' | '$'),
1637 Some(_) => false,
1638 };
1639 if starts_nested_construct {
1640 let in_double_quotes = quote.is_some();
1645 self.consume_dollar_or_backquote(
1646 &mut state,
1647 extglob_char,
1648 in_double_quotes,
1649 )?;
1650 continue;
1651 }
1652
1653 self.consume_char()?;
1655 state.append_char(extglob_char);
1656
1657 match extglob_char {
1658 '\\' if quote.is_none_or(|(_, escapes)| escapes) => {
1661 if let Some(escaped_char) = self.next_char()? {
1662 state.append_char(escaped_char);
1663 }
1664 }
1665 c if quote.is_some_and(|(close, _)| close == c) => quote = None,
1666 _ if quote.is_some() => (),
1667 '\'' => quote = Some(('\'', was_after_dollar)),
1668 '"' => quote = Some(('"', true)),
1669 '$' => after_dollar = !was_after_dollar,
1671 '(' => paren_depth += 1,
1672 ')' => paren_depth -= 1,
1673 _ => (),
1674 }
1675 }
1676 } else if state.unquoted() && Self::can_start_operator(c) {
1680 if state.started_token() {
1681 result = state.delimit_current_token(
1682 TokenEndReason::OperatorStart,
1683 &mut self.cross_state,
1684 )?;
1685 } else {
1686 state.token_is_operator = true;
1687 self.consume_char()?;
1688 state.append_char(c);
1689 self.cross_state.frame.note_unquoted_char(c);
1690 }
1691 } else if state.unquoted() && is_blank(c) {
1695 if state.started_token() {
1696 result = state.delimit_current_token(
1697 TokenEndReason::NonNewLineBlank,
1698 &mut self.cross_state,
1699 )?;
1700
1701 if include_space {
1704 continue;
1705 }
1706 } else if include_space {
1707 state.append_char(c);
1708 } else {
1709 state.start_position.column += 1;
1711 state.start_position.index += 1;
1712 }
1713
1714 self.consume_char()?;
1715 }
1716 else if !state.token_is_operator
1725 && (in_parameter_expansion
1726 || (state.started_token() && !(c == '#' && state.only_blanks_so_far())))
1727 {
1728 self.consume_char()?;
1729 state.append_char(c);
1730 if state.unquoted() {
1731 self.cross_state.frame.note_unquoted_char(c);
1732 }
1733 } else if c == '#' {
1734 self.consume_char()?;
1736
1737 let mut done = false;
1738 while !done {
1739 done = match self.peek_char()? {
1740 Some('\n') => true,
1741 None => true,
1742 _ => {
1743 self.consume_char()?;
1746 false
1747 }
1748 };
1749 }
1750 } else if state.started_token() {
1752 result =
1755 state.delimit_current_token(TokenEndReason::Other, &mut self.cross_state)?;
1756 } else {
1757 self.consume_char()?;
1761 state.append_char(c);
1762 self.cross_state.frame.note_unquoted_char(c);
1763 }
1764 }
1765
1766 let result = result.unwrap();
1767
1768 Ok(result)
1769 }
1770
1771 fn remove_here_end_tag(
1772 &mut self,
1773 state: &mut TokenParseState,
1774 result: &mut Option<TokenizeResult>,
1775 ends_with_newline: bool,
1776 ) -> Result<bool, TokenizerError> {
1777 if self.cross_state.frame.current_here_tags.is_empty() {
1779 return Ok(false);
1780 }
1781
1782 let next_here_tag = &self.cross_state.frame.current_here_tags[0];
1783
1784 let tag_str: Cow<'_, str> = if next_here_tag.tag_was_escaped_or_quoted {
1785 unquote_str(next_here_tag.tag.as_str()).into()
1786 } else {
1787 next_here_tag.tag.as_str().into()
1788 };
1789
1790 let tag_str = if !ends_with_newline {
1791 tag_str
1792 .strip_suffix('\n')
1793 .unwrap_or_else(|| tag_str.as_ref())
1794 } else {
1795 tag_str.as_ref()
1796 };
1797
1798 if tag_str.is_empty() {
1803 return Ok(false);
1804 }
1805
1806 if let Some(current_token_without_here_tag) = state.current_token().strip_suffix(tag_str) {
1807 if current_token_without_here_tag.is_empty()
1811 || current_token_without_here_tag.ends_with('\n')
1812 {
1813 state.replace_with_here_doc(current_token_without_here_tag.to_owned());
1814
1815 *result = state.delimit_current_token(
1817 TokenEndReason::HereDocumentBodyEnd,
1818 &mut self.cross_state,
1819 )?;
1820
1821 return Ok(true);
1822 }
1823 }
1824 Ok(false)
1825 }
1826
1827 const fn can_start_extglob(c: char) -> bool {
1828 matches!(c, '@' | '!' | '?' | '+' | '*')
1829 }
1830
1831 const fn can_start_operator(c: char) -> bool {
1832 matches!(c, '&' | '(' | ')' | ';' | '\n' | '|' | '<' | '>')
1833 }
1834
1835 fn is_operator(&self, s: &str) -> bool {
1836 if !self.options.sh_mode && matches!(s, "<<<" | "&>" | "&>>" | ";;&" | ";&" | "|&") {
1838 return true;
1839 }
1840
1841 matches!(
1842 s,
1843 "&" | "&&"
1844 | "("
1845 | ")"
1846 | ";"
1847 | ";;"
1848 | "\n"
1849 | "|"
1850 | "||"
1851 | "<"
1852 | ">"
1853 | ">|"
1854 | "<<"
1855 | ">>"
1856 | "<&"
1857 | ">&"
1858 | "<<-"
1859 | "<>"
1860 )
1861 }
1862}
1863
1864impl<R: ?Sized + std::io::BufRead> Iterator for Tokenizer<'_, R> {
1865 type Item = Result<TokenizeResult, TokenizerError>;
1866
1867 fn next(&mut self) -> Option<Self::Item> {
1868 match self.next_token() {
1869 #[expect(clippy::manual_map)]
1870 Ok(result) => match result.token {
1871 Some(_) => Some(Ok(result)),
1872 None => None,
1873 },
1874 Err(e) => Some(Err(e)),
1875 }
1876 }
1877}
1878
1879fn append_expansion_token(
1884 state: &mut TokenParseState,
1885 bodies: &mut String,
1886 result: &TokenizeResult,
1887) {
1888 let text = result.token.as_ref().map_or("", Token::to_str);
1889 match result.reason {
1890 TokenEndReason::HereDocumentBodyStart => bodies.push('\n'),
1891 TokenEndReason::HereDocumentBodyEnd | TokenEndReason::HereDocumentEndTag => {
1892 bodies.push_str(text);
1893 }
1894 _ => state.append_str(text),
1895 }
1896}
1897
1898const fn is_blank(c: char) -> bool {
1899 c == ' ' || c == '\t'
1900}
1901
1902const fn does_char_newly_affect_quoting(state: &TokenParseState, c: char) -> bool {
1903 if state.in_escape {
1905 return false;
1906 }
1907
1908 match state.quote_mode {
1909 QuoteMode::Double(_) | QuoteMode::AnsiC(_) => {
1912 if c == '\\' {
1913 true
1915 } else {
1916 false
1917 }
1918 }
1919 QuoteMode::Single(_) => false,
1921 QuoteMode::None => is_quoting_char(c),
1924 }
1925}
1926
1927const fn is_quoting_char(c: char) -> bool {
1928 matches!(c, '\\' | '\'' | '\"')
1929}
1930
1931pub fn unquote_str(s: &str) -> String {
1943 let mut result = String::with_capacity(s.len());
1944 let mut quote = None;
1946 let mut chars = s.chars().peekable();
1947
1948 while let Some(c) = chars.next() {
1949 match (c, quote) {
1950 ('\\', None) => result.extend(chars.next()),
1951 ('\\', Some('"')) => {
1952 if !matches!(chars.peek(), Some('$' | '`' | '"' | '\\' | '\n')) {
1953 result.push(c);
1954 }
1955 result.extend(chars.next());
1956 }
1957 (_, Some(q)) if c == q => quote = None,
1958 ('\'' | '"', None) => quote = Some(c),
1959 _ => result.push(c),
1960 }
1961 }
1962
1963 result
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968
1969 use anyhow::Result;
1970 use insta::assert_ron_snapshot;
1971 use pretty_assertions::{assert_eq, assert_matches};
1972
1973 use super::*;
1974
1975 #[derive(serde::Serialize, serde::Deserialize)]
1976 struct TokenizerResult<'a> {
1977 input: &'a str,
1978 result: Vec<Token>,
1979 }
1980
1981 fn test_tokenizer(input: &str) -> Result<TokenizerResult<'_>> {
1982 Ok(TokenizerResult {
1983 input,
1984 result: tokenize_str(input)?,
1985 })
1986 }
1987
1988 #[test]
1989 fn tokenize_empty() -> Result<()> {
1990 let tokens = tokenize_str("")?;
1991 assert_eq!(tokens.len(), 0);
1992 Ok(())
1993 }
1994
1995 #[test]
1996 fn tokenize_line_continuation() -> Result<()> {
1997 assert_ron_snapshot!(test_tokenizer(
1998 r"a\
1999bc"
2000 )?);
2001 Ok(())
2002 }
2003
2004 #[test]
2005 fn tokenize_operators() -> Result<()> {
2006 assert_ron_snapshot!(test_tokenizer("a>>b")?);
2007 Ok(())
2008 }
2009
2010 #[test]
2011 fn tokenize_line_continuations() -> Result<()> {
2012 for (input, expected) in [
2015 ("a&\\\n&b", &["a", "&&", "b"][..]),
2016 ("a|\\\n&b", &["a", "|&", "b"]),
2017 ("a&&\\\n", &["a", "&&"]),
2018 ("a <<\\\n", &["a", "<<"]),
2020 ("a&&\\b", &["a", "&&", "\\b"]),
2021 ("a<\\\n<b\nx\nb\n", &["a", "<<", "b", "x\n", "b", "\n"]),
2022 ("a<<\\b\n$x\nb\n", &["a", "<<", "\\b", "$x\n", "b", "\n"]),
2023 ("$\\\n(a) $\\\n{a}", &["$(a)", "${a}"]),
2024 ("$(\\\n(1))", &["$((1))"]),
2025 (
2026 "$(a\\\nb) ${a\\\nb} $((1+\\\n1))",
2027 &["$(ab)", "${ab}", "$((1+1))"],
2028 ),
2029 ("\"a\\\nb\" `'a\\\nb'`", &["\"ab\"", "`'ab'`"]),
2030 ("@(a\\\nb|\"a\\\nb\")", &["@(ab|\"ab\")"]),
2031 ("a\\\\\n", &["a\\\\", "\n"]),
2033 ("'a\\\nb' $'a\\\nb'", &["'a\\\nb'", "$'a\\\nb'"]),
2034 ("`\\\\\n`", &["`\\\\\n`"]),
2035 ("@('a\\\nb')", &["@('a\\\nb')"]),
2036 ("a #b\\\nc", &["a", "\n", "c"]),
2037 (
2039 "<<a\nb\\\nc\\\\\nd\na\n",
2040 &["<<", "a", "bc\\\\\nd\n", "a", "\n"],
2041 ),
2042 ("<<a\nb\\\na\na\n", &["<<", "a", "ba\n", "a", "\n"]),
2043 ("<<'a'\n\\\nb\na\n", &["<<", "'a'", "\\\nb\n", "a", "\n"]),
2044 ] {
2045 let tokens = tokenize_str(input)?;
2046 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2047 assert_eq!(token_strs, expected, "tokenizing {input:?}");
2048 }
2049 Ok(())
2050 }
2051
2052 #[test]
2053 fn tokenize_token_after_blanks_and_line_continuation() -> Result<()> {
2054 assert_matches!(
2057 &tokenize_str("a; \\\n c")?[..],
2058 [_, _, Token::Word(word, span)]
2059 if word == "c" && (span.start.line, span.start.column, span.start.index) == (2, 3, 7)
2060 );
2061 Ok(())
2062 }
2063
2064 #[test]
2065 fn tokenize_operator_followed_by_backslash() -> Result<()> {
2066 assert_ron_snapshot!(test_tokenizer("a;\\b|\\\n c")?);
2070 Ok(())
2071 }
2072
2073 #[test]
2074 fn tokenize_comment() -> Result<()> {
2075 assert_ron_snapshot!(test_tokenizer(
2076 r"a #comment
2077"
2078 )?);
2079 Ok(())
2080 }
2081
2082 #[test]
2083 fn tokenize_comment_in_command_substitution() {
2084 for prefix in ["", " ", " ", " ", "\t", "\t\t", " \t", "\t "] {
2090 let input = format!("$({prefix}# it's a comment\n)\n");
2091 let tokens = tokenize_str(input.as_str()).unwrap();
2092 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2093 assert_eq!(
2094 token_strs,
2095 [format!("$({prefix}\n)").as_str(), "\n"],
2096 "tokenizing {input:?}"
2097 );
2098 }
2099 }
2100
2101 #[test]
2102 fn tokenize_comment_at_eof() -> Result<()> {
2103 assert_ron_snapshot!(test_tokenizer(r"a #comment")?);
2104 Ok(())
2105 }
2106
2107 #[test]
2108 fn tokenize_empty_here_doc() -> Result<()> {
2109 assert_ron_snapshot!(test_tokenizer(
2110 r"cat <<HERE
2111HERE
2112"
2113 )?);
2114 Ok(())
2115 }
2116
2117 #[test]
2118 fn tokenize_here_doc() -> Result<()> {
2119 assert_ron_snapshot!(test_tokenizer(
2120 r"cat <<HERE
2121SOMETHING
2122HERE
2123echo after
2124"
2125 )?);
2126 assert_ron_snapshot!(test_tokenizer(
2127 r"cat <<HERE
2128SOMETHING
2129HERE
2130"
2131 )?);
2132 assert_ron_snapshot!(test_tokenizer(
2133 r"cat <<HERE
2134SOMETHING
2135HERE
2136
2137"
2138 )?);
2139 assert_ron_snapshot!(test_tokenizer(
2140 r"cat <<HERE
2141SOMETHING
2142HERE"
2143 )?);
2144 Ok(())
2145 }
2146
2147 #[test]
2148 fn tokenize_here_doc_with_tab_removal() -> Result<()> {
2149 assert_ron_snapshot!(test_tokenizer(
2150 r"cat <<-HERE
2151 SOMETHING
2152 HERE
2153"
2154 )?);
2155 Ok(())
2156 }
2157
2158 #[test]
2159 fn tokenize_here_doc_with_other_tokens() -> Result<()> {
2160 assert_ron_snapshot!(test_tokenizer(
2161 r"cat <<EOF | wc -l
2162A B C
21631 2 3
2164D E F
2165EOF
2166"
2167 )?);
2168 Ok(())
2169 }
2170
2171 #[test]
2172 fn tokenize_multiple_here_docs() -> Result<()> {
2173 assert_ron_snapshot!(test_tokenizer(
2174 r"cat <<HERE1 <<HERE2
2175SOMETHING
2176HERE1
2177OTHER
2178HERE2
2179echo after
2180"
2181 )?);
2182 Ok(())
2183 }
2184
2185 #[test]
2186 fn tokenize_unterminated_here_doc() {
2187 let result = tokenize_str(
2188 r"cat <<HERE
2189SOMETHING
2190",
2191 );
2192 assert!(result.is_err());
2193 }
2194
2195 #[test]
2196 fn tokenize_here_tag_with_unterminated_expansion() {
2197 for input in [
2201 "<<E$[\t\t",
2202 "<<-E$[\t\t",
2203 "<<E$(\t\t",
2204 "<<E$((\t\t",
2205 "<<E\"$[\t\t",
2206 "x <<$(( )",
2207 "cat <<'' $(",
2208 ] {
2209 assert_matches!(
2210 tokenize_str(input),
2211 Err(TokenizerError::UnterminatedExpansion),
2212 "for {input:?}"
2213 );
2214 }
2215 }
2216
2217 #[test]
2218 fn tokenize_here_doc_with_empty_quoted_tag_at_end_of_input() {
2219 for input in ["cat <<'' ", "cat <<\"\" "] {
2223 assert_matches!(
2224 tokenize_str(input),
2225 Err(TokenizerError::UnterminatedHereDocuments(..)),
2226 "for {input:?}"
2227 );
2228 }
2229 }
2230
2231 #[test]
2232 fn tokenize_here_tag_containing_expansions() {
2233 for tag in ["$(x)", "E${x}", "$((1 + 2))", "$[1]", "$(echo a)", "\r"] {
2234 let input = std::format!("cat <<{tag}\nbody\n{tag}\n");
2235 assert_matches!(
2236 tokenize_str(input.as_str()),
2237 Ok(tokens) if tokens.iter().any(|t| matches!(t, Token::Word(w, _) if w == "body\n")),
2238 "for {input:?}"
2239 );
2240 }
2241 }
2242
2243 #[test]
2244 fn tokenize_missing_here_tag_reports_what_was_found() {
2245 for (input, found) in [
2246 ("cat <<\n", "\n"),
2247 ("cat <<;", ";"),
2248 ("cat <<)|$(\n)", ")"),
2249 ("echo $(cat << )", ")"),
2250 ("echo $(cat <<)", ")"),
2251 ("echo \"$(cat <<-)\"", ")"),
2252 ] {
2253 assert_matches!(
2254 tokenize_str(input),
2255 Err(TokenizerError::MissingHereTag(actual)) if actual == found,
2256 "for {input:?}"
2257 );
2258 }
2259 }
2260
2261 #[test]
2262 fn tokenize_input_ending_where_here_tag_expected_is_incomplete() {
2263 for input in ["cat << ", "cat << \\\n"] {
2267 assert_matches!(
2268 tokenize_str(input),
2269 Err(e) if e.is_incomplete(),
2270 "for {input:?}"
2271 );
2272 }
2273 }
2274
2275 #[test]
2276 fn tokenize_here_doc_operator_in_parameter_expansion_is_literal() {
2277 for word in ["${x:-<<}", "${x:-<< foo}", "${x:-a<<b}", "${x:-<<-EOF}"] {
2279 let input = std::format!("echo {word}\n");
2280 assert_matches!(
2281 tokenize_str(input.as_str()),
2282 Ok(tokens) if tokens.iter().any(|t| matches!(t, Token::Word(w, _) if w == word)),
2283 "for {input:?}"
2284 );
2285 }
2286 }
2287
2288 #[test]
2289 fn tokenize_words_verbatim() {
2290 for word in [
2291 "${u:-a@(}",
2293 "$${u",
2295 "$$'a\\'x",
2296 "\\$'a\\'x",
2298 "$[ a[1] + 1 ]",
2301 "$[ a[ a[0] ] ]",
2302 "$[ ${a[1]} + 1 ]",
2303 "$[ $[1] + a[$[0]] ]",
2304 "$[ \"[\" ]",
2305 "@(\"$'x'\")",
2307 "\"${x:-a\tb}\"",
2309 ] {
2310 assert_matches!(
2311 tokenize_str(word),
2312 Ok(tokens) if matches!(tokens.as_slice(), [Token::Word(w, _)] if w == word),
2313 "for {word:?}"
2314 );
2315 }
2316 }
2317
2318 #[test]
2319 fn tokenize_here_doc_left_open_by_command_substitution() -> Result<()> {
2320 for (input, expected) in [
2324 (
2325 "x=\"$(cat <<EOF)\"\nbody\nEOF\n",
2326 &["x=\"$(cat <<EOF\nbody\nEOF\n)\"", "\n"][..],
2327 ),
2328 (
2329 "echo \"[$(cat <<EOF | tr a-z A-Z)]\"\nbody\nEOF\n",
2330 &["echo", "\"[$(cat <<EOF | tr a-z A-Z\nbody\nEOF\n)]\"", "\n"],
2331 ),
2332 (
2333 "echo \"[$(cat <<A; cat <<-B)]\"\na\nA\n\tb\n\tB\n",
2334 &["echo", "\"[$(cat <<A; cat <<-B\na\nA\nb\nB\n)]\"", "\n"],
2335 ),
2336 (
2337 "echo \"[$(echo $(cat <<EOF))]\"\nbody\nEOF\n",
2338 &["echo", "\"[$(echo $(cat <<EOF\nbody\nEOF\n))]\"", "\n"],
2339 ),
2340 (
2341 "echo \"[${x:-$(cat <<EOF)}]\"\nbody\nEOF\n",
2342 &["echo", "\"[${x:-$(cat <<EOF\nbody\nEOF\n)}]\"", "\n"],
2343 ),
2344 (
2345 "cat <<A; echo \"[$(cat <<B)]\"\nb\nB\na\nA\n",
2346 &[
2347 "cat",
2348 "<<",
2349 "A",
2350 "a\n",
2351 "A",
2352 ";",
2353 "echo",
2354 "\"[$(cat <<B\nb\nB\n)]\"",
2355 "\n",
2356 ],
2357 ),
2358 (
2359 "echo \"[$(cat <<A)]\" \"[$(cat <<B)]\"\na\nA\nb\nB\n",
2360 &[
2361 "echo",
2362 "\"[$(cat <<A\na\nA\n)]\"",
2363 "\"[$(cat <<B\nb\nB\n)]\"",
2364 "\n",
2365 ],
2366 ),
2367 ] {
2368 let tokens = tokenize_str(input)?;
2369 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2370 assert_eq!(token_strs, expected, "for {input:?}");
2371 }
2372
2373 let tokens = tokenize_str("echo \"$(cat <<EOF)\" x\nbody\nEOF\ny\n")?;
2376 let positions: Vec<_> = tokens
2377 .iter()
2378 .map(|t| {
2379 (
2380 t.to_str(),
2381 t.location().start.line,
2382 t.location().start.column,
2383 )
2384 })
2385 .collect();
2386 assert_eq!(
2387 positions,
2388 [
2389 ("echo", 1, 1),
2390 ("\"$(cat <<EOF\nbody\nEOF\n)\"", 1, 6),
2391 ("x", 1, 21),
2392 ("\n", 1, 22),
2393 ("y", 4, 1),
2394 ("\n", 4, 2),
2395 ]
2396 );
2397
2398 Ok(())
2399 }
2400
2401 #[test]
2402 fn tokenize_unterminated_here_doc_names_its_tag() {
2403 assert_matches!(
2404 tokenize_str("cat <<]|$[\n]"),
2405 Err(TokenizerError::UnterminatedHereDocuments(tags, _)) if tags == "`]`"
2406 );
2407 }
2408
2409 #[test]
2410 fn tokenize_deeply_nested_expansions_is_bounded() {
2411 for (open, close) in [("$(", ")"), ("\"$(", ")\""), ("${x:-", "}"), ("$[", "]")] {
2415 let input = std::format!("echo {}x{}", open.repeat(10_000), close.repeat(10_000));
2416 assert_matches!(
2417 tokenize_str(input.as_str()),
2418 Err(TokenizerError::ExpansionNestingTooDeep),
2419 "for {open:?}"
2420 );
2421 }
2422 }
2423
2424 #[test]
2425 fn tokenize_nesting_up_to_limit() {
2426 let nested = |depth: u32| {
2427 let depth = depth as usize;
2428 tokenize_str(&std::format!(
2429 "echo {}x{}",
2430 "$(".repeat(depth),
2431 ")".repeat(depth)
2432 ))
2433 };
2434 assert!(nested(MAX_EXPANSION_NESTING).is_ok());
2435 assert_matches!(
2436 nested(MAX_EXPANSION_NESTING + 1),
2437 Err(TokenizerError::ExpansionNestingTooDeep)
2438 );
2439 }
2440
2441 #[test]
2442 fn tokenize_missing_here_tag() {
2443 let result = tokenize_str(
2444 r"cat <<
2445",
2446 );
2447 assert!(result.is_err());
2448 }
2449
2450 #[test]
2451 fn tokenize_here_doc_in_command_substitution() -> Result<()> {
2452 assert_ron_snapshot!(test_tokenizer(
2453 r"echo $(cat <<HERE
2454TEXT
2455HERE
2456)"
2457 )?);
2458 Ok(())
2459 }
2460
2461 #[test]
2462 fn tokenize_here_doc_in_double_quoted_command_substitution() -> Result<()> {
2463 assert_ron_snapshot!(test_tokenizer(
2464 r#"echo "$(cat <<HERE
2465TEXT
2466HERE
2467)""#
2468 )?);
2469 Ok(())
2470 }
2471
2472 #[test]
2473 fn tokenize_here_doc_in_double_quoted_command_substitution_with_space() -> Result<()> {
2474 assert_ron_snapshot!(test_tokenizer(
2475 r#"echo "$(cat << HERE
2476TEXT
2477HERE
2478)""#
2479 )?);
2480 Ok(())
2481 }
2482
2483 #[test]
2484 fn tokenize_complex_here_docs_in_command_substitution() -> Result<()> {
2485 assert_ron_snapshot!(test_tokenizer(
2486 r"echo $(cat <<HERE1 <<HERE2 | wc -l
2487TEXT
2488HERE1
2489OTHER
2490HERE2
2491)"
2492 )?);
2493 Ok(())
2494 }
2495
2496 #[test]
2497 fn tokenize_simple_backquote() -> Result<()> {
2498 assert_ron_snapshot!(test_tokenizer(r"echo `echo hi`")?);
2499 Ok(())
2500 }
2501
2502 #[test]
2503 fn tokenize_backquote_with_escape() -> Result<()> {
2504 assert_ron_snapshot!(test_tokenizer(r"echo `echo\`hi`")?);
2505 Ok(())
2506 }
2507
2508 #[test]
2509 fn tokenize_unterminated_backquote() {
2510 assert_matches!(
2511 tokenize_str("`"),
2512 Err(TokenizerError::UnterminatedBackquote(_))
2513 );
2514 }
2515
2516 #[test]
2517 fn tokenize_unterminated_command_substitution() {
2518 assert_matches!(
2522 tokenize_str("$("),
2523 Err(TokenizerError::UnterminatedExpansion)
2524 );
2525 }
2526
2527 #[test]
2528 fn command_substitution_body_stops_at_closing_paren() -> Result<()> {
2529 let options = TokenizerOptions::default();
2530 assert_eq!(
2531 command_substitution_body("echo hi) rest", &options)?,
2532 "echo hi"
2533 );
2534 assert_eq!(
2535 command_substitution_body(r#"echo ")" (a)) rest"#, &options)?,
2536 r#"echo ")" (a)"#
2537 );
2538 assert_eq!(
2539 command_substitution_body("cat <<'EOF'\n\"it's ) `\nEOF\n) rest", &options)?,
2540 "cat <<'EOF'\n\"it's ) `\nEOF\n"
2541 );
2542 assert_eq!(
2545 command_substitution_body("cat <<E\n)\nE\n) rest", &options)?,
2546 "cat <<E\n)\nE\n"
2547 );
2548 assert_eq!(
2549 command_substitution_body("cat <<E\n)\nE\necho after) rest", &options)?,
2550 "cat <<E\n)\nE\necho after"
2551 );
2552 assert_eq!(
2556 command_substitution_body("cat <<EOF) rest\n", &options)?,
2557 "cat <<EOF"
2558 );
2559 assert_eq!(
2560 command_substitution_body("echo $(cat <<EOF)) rest\n", &options)?,
2561 "echo $(cat <<EOF)"
2562 );
2563 assert_eq!(
2566 command_substitution_body(r#"printf "%s" @(")")) rest"#, &options)?,
2567 r#"printf "%s" @(")")"#
2568 );
2569 assert_eq!(
2571 command_substitution_body("echo “é”) ü", &options)?,
2572 "echo “é”"
2573 );
2574 Ok(())
2575 }
2576
2577 #[test]
2578 fn command_substitution_body_unterminated() {
2579 let options = TokenizerOptions::default();
2580 assert_matches!(
2581 command_substitution_body("echo hi", &options),
2582 Err(TokenizerError::UnterminatedExpansion)
2583 );
2584 assert_matches!(
2585 command_substitution_body("echo 'hi)", &options),
2586 Err(TokenizerError::UnterminatedSingleQuote(_))
2587 );
2588 }
2589
2590 #[test]
2591 fn tokenize_unterminated_arithmetic_expansion() {
2592 assert_matches!(
2593 tokenize_str("$(("),
2594 Err(TokenizerError::UnterminatedExpansion)
2595 );
2596 }
2597
2598 #[test]
2599 fn tokenize_unterminated_legacy_arithmetic_expansion() {
2600 assert_matches!(
2601 tokenize_str("$["),
2602 Err(TokenizerError::UnterminatedExpansion)
2603 );
2604 }
2605
2606 #[test]
2607 fn tokenize_command_substitution() -> Result<()> {
2608 assert_ron_snapshot!(test_tokenizer("a$(echo hi)b c")?);
2609 Ok(())
2610 }
2611
2612 #[test]
2613 fn tokenize_command_substitution_with_subshell() -> Result<()> {
2614 assert_ron_snapshot!(test_tokenizer("$( (:) )")?);
2615 Ok(())
2616 }
2617
2618 #[test]
2619 fn tokenize_command_substitution_containing_extglob() -> Result<()> {
2620 assert_ron_snapshot!(test_tokenizer("echo $(echo !(x))")?);
2621 Ok(())
2622 }
2623
2624 #[test]
2625 fn tokenize_extglob_with_quotes_and_escapes() -> Result<()> {
2626 for (input, expected) in [
2627 (r#"@(")") y"#, [r#"@(")")"#, "y"]),
2629 (r"@(a|')'|b)x y", [r"@(a|')'|b)x", "y"]),
2630 (r#"@("(") y"#, [r#"@("(")"#, "y"]),
2631 (r#"@("\")") y"#, [r#"@("\")")"#, "y"]),
2633 (r"@('\')x y", [r"@('\')x", "y"]),
2635 (r#"@(\") y"#, [r#"@(\")"#, "y"]),
2637 (r"@(\)) y", [r"@(\))", "y"]),
2638 (r"@($'\'')x y", [r"@($'\'')x", "y"]),
2640 (r"@($'a\')'|b) y", [r"@($'a\')'|b)", "y"]),
2641 (r"@(\$'a\')x y", [r"@(\$'a\')x", "y"]),
2642 (r"@($$'a\')x y", [r"@($$'a\')x", "y"]),
2644 (r"@($$$'\'')x y", [r"@($$$'\'')x", "y"]),
2645 (r"@(}`'`)x y", [r"@(}`'`)x", "y"]),
2647 (r#"@(`echo \\"`)x y"#, [r#"@(`echo \\"`)x"#, "y"]),
2648 (r"@(`$'\'`)x y", [r"@(`$'\'`)x", "y"]),
2649 (r#"@(`echo ")"`)x y"#, [r#"@(`echo ")"`)x"#, "y"]),
2650 (r#"@("$(echo ")")")x y"#, [r#"@("$(echo ")")")x"#, "y"]),
2652 (r#"@("`echo "\)"`")x y"#, [r#"@("`echo "\)"`")x"#, "y"]),
2653 (r#"@("${u:-"}"}")x y"#, [r#"@("${u:-"}"}")x"#, "y"]),
2654 (r#"@($")")x y"#, [r#"@($")")x"#, "y"]),
2655 (r#"+(a|@(b|")")|'"') y"#, [r#"+(a|@(b|")")|'"')"#, "y"]),
2657 ] {
2658 let tokens = tokenize_str(input)?;
2659 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2660 assert_eq!(token_strs, expected, "input: {input}");
2661 }
2662 Ok(())
2663 }
2664
2665 #[test]
2666 fn tokenize_unterminated_construct_in_extglob() {
2667 for input in [r#"@("$(\$"\))x"#, r#"@($"$(\)")x"#, r"@(`)x", r#"@("`)")x"#] {
2670 assert!(tokenize_str(input).is_err(), "input: {input}");
2671 }
2672 }
2673
2674 #[test]
2675 fn tokenize_unterminated_extglob() {
2676 for input in [r"@(a", r#"@(")""#, r"@(')'", r"@(\)"] {
2677 assert_matches!(
2678 tokenize_str(input),
2679 Err(TokenizerError::UnterminatedExtendedGlob(_)),
2680 "input: {input}"
2681 );
2682 }
2683 }
2684
2685 #[test]
2686 fn tokenize_arithmetic_expression() -> Result<()> {
2687 assert_ron_snapshot!(test_tokenizer("a$((1+2))b c")?);
2688 Ok(())
2689 }
2690
2691 #[test]
2692 fn tokenize_arithmetic_expression_with_space() -> Result<()> {
2693 assert_ron_snapshot!(test_tokenizer("$(( 1 ))")?);
2696 Ok(())
2697 }
2698 #[test]
2699 fn tokenize_arithmetic_expression_with_parens() -> Result<()> {
2700 assert_ron_snapshot!(test_tokenizer("$(( (0) ))")?);
2701 Ok(())
2702 }
2703
2704 #[test]
2705 fn tokenize_special_parameters() -> Result<()> {
2706 assert_ron_snapshot!(test_tokenizer("$$")?);
2707 assert_ron_snapshot!(test_tokenizer("$@")?);
2708 assert_ron_snapshot!(test_tokenizer("$!")?);
2709 assert_ron_snapshot!(test_tokenizer("$?")?);
2710 assert_ron_snapshot!(test_tokenizer("$*")?);
2711 Ok(())
2712 }
2713
2714 #[test]
2715 fn tokenize_unbraced_parameter_expansion() -> Result<()> {
2716 assert_ron_snapshot!(test_tokenizer("$x")?);
2717 assert_ron_snapshot!(test_tokenizer("a$x")?);
2718 Ok(())
2719 }
2720
2721 #[test]
2722 fn tokenize_unterminated_parameter_expansion() {
2723 assert_matches!(
2724 tokenize_str("${x"),
2725 Err(TokenizerError::UnterminatedVariable)
2726 );
2727 }
2728
2729 #[test]
2730 fn tokenize_braced_parameter_expansion() -> Result<()> {
2731 assert_ron_snapshot!(test_tokenizer("${x}")?);
2732 assert_ron_snapshot!(test_tokenizer("a${x}b")?);
2733 Ok(())
2734 }
2735
2736 #[test]
2737 fn tokenize_braced_parameter_expansion_with_escaping() -> Result<()> {
2738 assert_ron_snapshot!(test_tokenizer(r"a${x\}}b")?);
2739 Ok(())
2740 }
2741
2742 #[test]
2743 fn tokenize_whitespace() -> Result<()> {
2744 assert_ron_snapshot!(test_tokenizer("1 2 3")?);
2745 Ok(())
2746 }
2747
2748 #[test]
2749 fn tokenize_escaped_whitespace() -> Result<()> {
2750 assert_ron_snapshot!(test_tokenizer(r"1\ 2 3")?);
2751 Ok(())
2752 }
2753
2754 #[test]
2755 fn tokenize_single_quote() -> Result<()> {
2756 assert_ron_snapshot!(test_tokenizer(r"x'a b'y")?);
2757 Ok(())
2758 }
2759
2760 #[test]
2761 fn tokenize_double_quote() -> Result<()> {
2762 assert_ron_snapshot!(test_tokenizer(r#"x"a b"y"#)?);
2763 Ok(())
2764 }
2765
2766 #[test]
2767 fn tokenize_double_quoted_command_substitution() -> Result<()> {
2768 assert_ron_snapshot!(test_tokenizer(r#"x"$(echo hi)"y"#)?);
2769 Ok(())
2770 }
2771
2772 #[test]
2773 fn tokenize_double_quoted_arithmetic_expression() -> Result<()> {
2774 assert_ron_snapshot!(test_tokenizer(r#"x"$((1+2))"y"#)?);
2775 Ok(())
2776 }
2777
2778 #[test]
2779 fn test_quote_removal() {
2780 assert_eq!(unquote_str(r#""hello""#), "hello");
2781 assert_eq!(unquote_str(r"'hello'"), "hello");
2782 assert_eq!(unquote_str(r#""hel\"lo""#), r#"hel"lo"#);
2783 assert_eq!(unquote_str(r"hel\'lo"), r"hel'lo");
2784 assert_eq!(unquote_str(r"a\ b\\c"), r"a b\c");
2785
2786 assert_eq!(unquote_str(r#"a'b c'd"e f"g"#), "ab cde fg");
2789 assert_eq!(unquote_str(r#"'a"b'"c'd""#), r#"a"bc'd"#);
2790
2791 assert_eq!(unquote_str(r"'a\b'"), r"a\b");
2794 assert_eq!(unquote_str(r"'a\'b"), r"a\b");
2795
2796 assert_eq!(unquote_str(r#""a\$b\`c\"d\\e""#), r#"a$b`c"d\e"#);
2799 assert_eq!(unquote_str(r#""a\b\'c""#), r"a\b\'c");
2800 assert_eq!(unquote_str("\"a\\\nb\""), "a\nb");
2801 assert_eq!(unquote_str("a\\\nb"), "a\nb");
2802
2803 assert_eq!(unquote_str(r"a\"), "a");
2805 assert_eq!(unquote_str(r#""a\"#), r"a\");
2806 assert_eq!(unquote_str(r"'a\"), r"a\");
2807
2808 assert_eq!(unquote_str(r#""a b"#), "a b");
2810 assert_eq!(unquote_str(r#"$HOME'~'"$x""#), "$HOME~$x");
2811 assert_eq!(unquote_str(""), "");
2812 }
2813}