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)]
528 pub fn delimit_current_token(
529 &mut self,
530 reason: TokenEndReason,
531 cross_token_state: &mut CrossTokenParseState,
532 ) -> Result<Option<TokenizeResult>, TokenizerError> {
533 if !self.started_token() && !matches!(reason, TokenEndReason::HereDocumentBodyEnd) {
536 return Ok(Some(TokenizeResult {
537 reason,
538 token: None,
539 }));
540 }
541
542 let current_here_state = std::mem::take(&mut cross_token_state.frame.here_state);
543 match current_here_state {
544 HereState::NextTokenIsHereTag { remove_tabs } => {
545 let operator_token_result = TokenizeResult {
549 reason,
550 token: Some(self.pop(&cross_token_state.cursor)),
551 };
552
553 cross_token_state.frame.here_state = HereState::CurrentTokenIsHereTag {
554 remove_tabs,
555 operator_token_result,
556 };
557
558 return Ok(None);
559 }
560 HereState::CurrentTokenIsHereTag {
561 remove_tabs,
562 operator_token_result,
563 } => {
564 if self.token_is_operator {
567 return Err(TokenizerError::MissingHereTag(
568 self.current_token().to_owned(),
569 ));
570 }
571
572 if self.only_blanks_so_far() {
578 self.pop(&cross_token_state.cursor);
579 cross_token_state.frame.here_state = HereState::CurrentTokenIsHereTag {
580 remove_tabs,
581 operator_token_result,
582 };
583 return Ok(None);
584 }
585
586 cross_token_state.frame.here_state = HereState::NextLineIsHereDoc;
587
588 let tag = std::format!("{}\n", self.current_token().trim_start_matches(is_blank));
593 let tag_was_escaped_or_quoted = tag.contains(is_quoting_char);
594
595 let tag_token_result = TokenizeResult {
596 reason,
597 token: Some(self.pop(&cross_token_state.cursor)),
598 };
599
600 cross_token_state.frame.current_here_tags.push(HereTag {
601 tag,
602 tag_was_escaped_or_quoted,
603 remove_tabs,
604 position: cross_token_state.cursor.clone(),
605 tokens: vec![operator_token_result, tag_token_result],
606 pending_tokens_after: vec![],
607 });
608
609 return Ok(None);
610 }
611 HereState::NextLineIsHereDoc => {
612 if self.is_newline() {
613 cross_token_state.frame.here_state = HereState::InHereDocs;
614 } else {
615 cross_token_state.frame.here_state = HereState::NextLineIsHereDoc;
616 }
617
618 if let Some(last_here_tag) = cross_token_state.frame.current_here_tags.last_mut() {
619 let token = self.pop(&cross_token_state.cursor);
620 let result = TokenizeResult {
621 reason,
622 token: Some(token),
623 };
624
625 last_here_tag.pending_tokens_after.push(result);
626 } else {
627 return Err(TokenizerError::MissingHereTagForDocumentBody);
628 }
629
630 return Ok(None);
631 }
632 HereState::InHereDocs => {
633 let completed_here_tag = cross_token_state.frame.current_here_tags.remove(0);
635
636 cross_token_state
638 .queued_tokens
639 .extend(completed_here_tag.tokens);
640
641 cross_token_state.queued_tokens.push(TokenizeResult {
643 reason: TokenEndReason::HereDocumentBodyStart,
644 token: None,
645 });
646
647 cross_token_state.queued_tokens.push(TokenizeResult {
649 reason,
650 token: Some(self.pop(&cross_token_state.cursor)),
651 });
652
653 let end_tag = if completed_here_tag.tag_was_escaped_or_quoted {
655 unquote_str(&completed_here_tag.tag)
656 } else {
657 completed_here_tag.tag
658 };
659 self.append_str(end_tag.trim_end_matches('\n'));
660 cross_token_state.queued_tokens.push(TokenizeResult {
661 reason: TokenEndReason::HereDocumentEndTag,
662 token: Some(self.pop(&cross_token_state.cursor)),
663 });
664
665 cross_token_state
669 .queued_tokens
670 .extend(completed_here_tag.pending_tokens_after);
671
672 if cross_token_state.frame.current_here_tags.is_empty() {
673 cross_token_state.frame.here_state = HereState::None;
674 } else {
675 cross_token_state.frame.here_state = HereState::InHereDocs;
676 }
677
678 return Ok(None);
679 }
680 HereState::None => (),
681 }
682
683 let token = self.pop(&cross_token_state.cursor);
684 let result = TokenizeResult {
685 reason,
686 token: Some(token),
687 };
688
689 Ok(Some(result))
690 }
691}
692
693pub fn tokenize_str(input: &str) -> Result<Vec<Token>, TokenizerError> {
699 tokenize_str_with_options(input, &TokenizerOptions::default())
700}
701
702pub fn tokenize_str_with_options(
709 input: &str,
710 options: &TokenizerOptions,
711) -> Result<Vec<Token>, TokenizerError> {
712 uncached_tokenize_string(input, options)
713}
714
715#[cached::macros::cached(
716 name = "TOKENIZE_CACHE",
717 max_size = 64,
718 key = "(String, TokenizerOptions)",
719 convert = r#"{ (input.to_owned(), options.to_owned()) }"#
720)]
721fn uncached_tokenize_string(
722 input: &str,
723 options: &TokenizerOptions,
724) -> Result<Vec<Token>, TokenizerError> {
725 uncached_tokenize_str(input, options)
726}
727
728pub fn uncached_tokenize_str(
735 input: &str,
736 options: &TokenizerOptions,
737) -> Result<Vec<Token>, TokenizerError> {
738 let mut reader = std::io::BufReader::new(input.as_bytes());
739 let mut tokenizer = crate::tokenizer::Tokenizer::new(&mut reader, options);
740
741 let mut tokens = vec![];
742 loop {
743 match tokenizer.next_token()? {
744 TokenizeResult {
745 token: Some(token), ..
746 } => tokens.push(token),
747 TokenizeResult {
748 reason: TokenEndReason::EndOfInput,
749 ..
750 } => break,
751 _ => (),
752 }
753 }
754
755 Ok(tokens)
756}
757
758pub(crate) fn command_substitution_body<'a>(
772 input: &'a str,
773 options: &TokenizerOptions,
774) -> Result<&'a str, TokenizerError> {
775 expansion_body(input, options, NestedExpansion::CommandSubstitution)
776}
777
778pub(crate) fn legacy_arithmetic_expansion_body<'a>(
783 input: &'a str,
784 options: &TokenizerOptions,
785) -> Result<&'a str, TokenizerError> {
786 expansion_body(input, options, NestedExpansion::LegacyArithmeticExpansion)
787}
788
789fn expansion_body<'a>(
790 input: &'a str,
791 options: &TokenizerOptions,
792 expansion: NestedExpansion,
793) -> Result<&'a str, TokenizerError> {
794 let mut reader = input.as_bytes();
795 let mut tokenizer = Tokenizer::new(&mut reader, options);
796 tokenizer.read_bodies_left_pending = false;
797
798 let mut state = TokenParseState::new(&tokenizer.cross_state.cursor);
802 tokenizer.consume_nested_expansion(&mut state, expansion)?;
803
804 let body_len = input
807 .chars()
808 .take(tokenizer.cross_state.cursor.index - 1)
809 .map(char::len_utf8)
810 .sum();
811 Ok(input.split_at(body_len).0)
812}
813
814impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {
815 pub fn new(reader: &'a mut R, options: &TokenizerOptions) -> Self {
816 Tokenizer {
817 options: options.clone(),
818 char_reader: CharReader::new(reader.chars()),
819 unread: None,
820 read_bodies_left_pending: true,
821 cross_state: CrossTokenParseState {
822 cursor: SourcePosition {
823 index: 0,
824 line: 1,
825 column: 1,
826 },
827 queued_tokens: vec![],
828 frame: ExpansionFrame::default(),
829 },
830 }
831 }
832
833 #[expect(clippy::unnecessary_wraps)]
834 pub fn current_location(&self) -> Option<SourcePosition> {
835 Some(self.cross_state.cursor.clone())
836 }
837
838 fn next_char(&mut self) -> Result<Option<char>, TokenizerError> {
839 let c = match self.unread.as_mut().and_then(|unread| unread.chars.pop()) {
840 Some(c) => Some(c),
841 None => self
842 .char_reader
843 .next()
844 .transpose()
845 .map_err(TokenizerError::ReadError)?,
846 };
847
848 if let Some(ch) = c {
849 if ch == '\n' {
850 self.cross_state.cursor.line += 1;
851 self.cross_state.cursor.column = 1;
852 } else {
853 self.cross_state.cursor.column += 1;
854 }
855 self.cross_state.cursor.index += 1;
856 }
857
858 if let Some(unread) = self.unread.take_if(|unread| unread.chars.is_empty()) {
859 self.cross_state.cursor = unread.resume_at;
860 }
861
862 Ok(c)
863 }
864
865 fn consume_char(&mut self) -> Result<(), TokenizerError> {
866 let _ = self.next_char()?;
867 Ok(())
868 }
869
870 fn peek_char(&mut self) -> Result<Option<char>, TokenizerError> {
871 if let Some(c) = self.unread.as_ref().and_then(|unread| unread.chars.last()) {
872 return Ok(Some(*c));
873 }
874
875 match self.char_reader.peek() {
876 Some(result) => match result {
877 Ok(c) => Ok(Some(*c)),
878 Err(_) => Err(TokenizerError::FailedDecoding),
879 },
880 None => Ok(None),
881 }
882 }
883
884 fn peek_second_char(&mut self) -> Result<Option<char>, TokenizerError> {
886 let peeked = match self
889 .unread
890 .as_ref()
891 .map_or(&[][..], |unread| unread.chars.as_slice())
892 {
893 [.., second, _] => return Ok(Some(*second)),
894 [_] => self.char_reader.peek(),
895 [] => self.char_reader.peek_second(),
896 };
897
898 match peeked {
899 Some(Ok(c)) => Ok(Some(*c)),
900 Some(Err(_)) => Err(TokenizerError::FailedDecoding),
901 None => Ok(None),
902 }
903 }
904
905 fn skip_line_continuations(&mut self) -> Result<(), TokenizerError> {
913 while self.peek_char()? == Some('\\') && self.peek_second_char()? == Some('\n') {
914 self.consume_char()?;
915 self.consume_char()?;
916 }
917 Ok(())
918 }
919
920 fn backslash_is_literal(&self, state: &TokenParseState) -> bool {
925 if state.in_escape || matches!(state.quote_mode, QuoteMode::Single(_) | QuoteMode::AnsiC(_))
926 {
927 return true;
928 }
929
930 let frame = &self.cross_state.frame;
933 let in_here_doc_body = match frame.here_state {
934 HereState::InHereDocs => true,
935 HereState::NextLineIsHereDoc => state.is_newline(),
936 _ => false,
937 };
938 if !in_here_doc_body {
939 return false;
940 }
941
942 frame
946 .current_here_tags
947 .first()
948 .is_some_and(|tag| tag.tag_was_escaped_or_quoted)
949 }
950
951 fn consume_nested_expansion(
955 &mut self,
956 state: &mut TokenParseState,
957 expansion: NestedExpansion,
958 ) -> Result<(), TokenizerError> {
959 let depth = self.cross_state.frame.depth + 1;
963 if depth > MAX_EXPANSION_NESTING {
964 return Err(TokenizerError::ExpansionNestingTooDeep);
965 }
966
967 let frame = ExpansionFrame {
973 expansion: Some(expansion),
974 depth,
975 closers_needed: expansion.closers_after_opening(),
976 ..ExpansionFrame::default()
977 };
978
979 debug_assert!(self.cross_state.queued_tokens.is_empty());
984 let enclosing = std::mem::replace(&mut self.cross_state.frame, frame);
985 let result = self.consume_expansion_contents(state, expansion);
986 debug_assert!(result.is_err() || self.cross_state.queued_tokens.is_empty());
987 self.cross_state.frame = enclosing;
988 result
989 }
990
991 fn consume_expansion_contents(
994 &mut self,
995 state: &mut TokenParseState,
996 expansion: NestedExpansion,
997 ) -> Result<(), TokenizerError> {
998 let mut bodies = String::new();
999
1000 loop {
1001 let result = self.next_token()?;
1002
1003 match result.reason {
1004 TokenEndReason::UnescapedNewLine => state.append_str(&std::mem::take(&mut bodies)),
1006 TokenEndReason::ExpansionClosingChar => {
1007 let closing_char = self
1009 .next_char()?
1010 .ok_or_else(|| expansion.unterminated_error())?;
1011
1012 let here_state = &self.cross_state.frame.here_state;
1013 if here_state.awaiting_tag() {
1014 return Err(TokenizerError::MissingHereTag(closing_char.to_string()));
1017 } else if matches!(here_state, HereState::NextLineIsHereDoc)
1018 && self.read_bodies_left_pending
1019 {
1020 self.read_here_doc_bodies_left_pending(state)?;
1021 }
1022
1023 state.append_char(closing_char);
1024 return Ok(());
1025 }
1026 TokenEndReason::EndOfInput => return Err(expansion.unterminated_error()),
1027 _ => (),
1028 }
1029
1030 append_expansion_token(state, &mut bodies, &result);
1031 }
1032 }
1033
1034 fn read_here_doc_bodies_left_pending(
1045 &mut self,
1046 state: &mut TokenParseState,
1047 ) -> Result<(), TokenizerError> {
1048 let rest_of_line_start = self.cross_state.cursor.clone();
1049 let mut rest_of_line = vec![];
1050 while let Some(c) = self.next_char()? {
1051 rest_of_line.push(c);
1052 if c == '\n' {
1053 break;
1054 }
1055 }
1056
1057 self.cross_state.frame.here_state = HereState::InHereDocs;
1061 let mut bodies = String::new();
1062 while !matches!(self.cross_state.frame.here_state, HereState::None)
1063 || !self.cross_state.queued_tokens.is_empty()
1064 {
1065 let result = self.next_token()?;
1066 append_expansion_token(state, &mut bodies, &result);
1067 }
1068 state.append_str(&bodies);
1069 state.append_char('\n');
1070
1071 if !rest_of_line.is_empty() {
1072 rest_of_line.reverse();
1073 let resume_at = std::mem::replace(&mut self.cross_state.cursor, rest_of_line_start);
1074 self.unread = Some(UnreadChars {
1075 chars: rest_of_line,
1076 resume_at,
1077 });
1078 }
1079
1080 Ok(())
1081 }
1082
1083 #[allow(clippy::unwrap_in_result)]
1089 fn consume_dollar_or_backquote(
1090 &mut self,
1091 state: &mut TokenParseState,
1092 c: char,
1093 in_double_quotes: bool,
1094 ) -> Result<(), TokenizerError> {
1095 if c == '$' {
1096 self.consume_char()?;
1098 self.skip_line_continuations()?;
1099
1100 let char_after_dollar_sign = self.peek_char()?;
1102 match char_after_dollar_sign {
1103 Some('(') => {
1104 state.append_char('$');
1106
1107 state.append_char(self.next_char()?.unwrap());
1109
1110 self.skip_line_continuations()?;
1113 let expansion = if matches!(self.peek_char()?, Some('(')) {
1114 state.append_char(self.next_char()?.unwrap());
1116 NestedExpansion::ArithmeticExpansion
1117 } else {
1118 NestedExpansion::CommandSubstitution
1119 };
1120
1121 self.consume_nested_expansion(state, expansion)?;
1122 }
1123
1124 Some('[') => {
1125 state.append_char('$');
1127
1128 state.append_char(self.next_char()?.unwrap());
1130
1131 self.consume_nested_expansion(
1132 state,
1133 NestedExpansion::LegacyArithmeticExpansion,
1134 )?;
1135 }
1136
1137 Some('{') => {
1138 state.append_char('$');
1140
1141 state.append_char(self.next_char()?.unwrap());
1143
1144 self.consume_nested_expansion(state, NestedExpansion::ParameterExpansion)?;
1145 }
1146 Some('$') => {
1148 self.consume_char()?;
1149 state.append_str("$$");
1150 }
1151
1152 Some('\'') if !in_double_quotes => {
1154 state.append_char('$');
1155 state.quote_mode = QuoteMode::AnsiC(self.cross_state.cursor.clone());
1156 state.append_char(self.next_char()?.unwrap());
1157 }
1158
1159 _ => {
1160 state.append_char('$');
1164 }
1165 }
1166 } else {
1167 let backquote_pos = self.cross_state.cursor.clone();
1170 self.consume_char()?;
1171
1172 state.append_char(c);
1174
1175 let mut escaping_enabled = false;
1177 let mut done = false;
1178 while !done {
1179 if !escaping_enabled {
1183 self.skip_line_continuations()?;
1184 }
1185
1186 let next_char_in_backquote = self.next_char()?;
1188 if let Some(cib) = next_char_in_backquote {
1189 state.append_char(cib);
1191
1192 if !escaping_enabled && cib == '\\' {
1194 escaping_enabled = true;
1195 } else {
1196 if !escaping_enabled && cib == '`' {
1198 done = true;
1199 }
1200 escaping_enabled = false;
1201 }
1202 } else {
1203 return Err(TokenizerError::UnterminatedBackquote(backquote_pos));
1204 }
1205 }
1206 }
1207
1208 Ok(())
1209 }
1210
1211 #[expect(clippy::cognitive_complexity)]
1218 #[expect(clippy::if_same_then_else)]
1219 #[expect(clippy::panic_in_result_fn)]
1220 #[expect(clippy::too_many_lines)]
1221 #[allow(clippy::unwrap_in_result)]
1222 pub fn next_token(&mut self) -> Result<TokenizeResult, TokenizerError> {
1223 let mut state = TokenParseState::new(&self.cross_state.cursor);
1224 let mut result: Option<TokenizeResult> = None;
1225
1226 let expansion = self.cross_state.frame.expansion;
1227 let closing_char = expansion.map(NestedExpansion::closing_char);
1228 let include_space = expansion.is_some();
1229 let in_parameter_expansion = expansion == Some(NestedExpansion::ParameterExpansion);
1232 let in_arithmetic_expansion = expansion.is_some_and(NestedExpansion::is_arithmetic);
1233
1234 while result.is_none() {
1235 if !self.cross_state.queued_tokens.is_empty() {
1238 return Ok(self.cross_state.queued_tokens.remove(0));
1239 }
1240
1241 let mut next = self.peek_char()?;
1246 if next == Some('\\')
1247 && self.peek_second_char()? == Some('\n')
1248 && !self.backslash_is_literal(&state)
1249 {
1250 self.skip_line_continuations()?;
1251 if !state.started_token() {
1252 state.start_position = self.cross_state.cursor.clone();
1253 }
1254 next = self.peek_char()?;
1255 }
1256 let c = next.unwrap_or('\0');
1257
1258 if next.is_none() {
1261 if state.in_escape {
1264 return Err(TokenizerError::UnterminatedEscapeSequence);
1265 }
1266 match state.quote_mode {
1267 QuoteMode::None => (),
1268 QuoteMode::AnsiC(pos) => {
1269 return Err(TokenizerError::UnterminatedAnsiCQuote(pos));
1270 }
1271 QuoteMode::Single(pos) => {
1272 return Err(TokenizerError::UnterminatedSingleQuote(pos));
1273 }
1274 QuoteMode::Double(pos) => {
1275 return Err(TokenizerError::UnterminatedDoubleQuote(pos));
1276 }
1277 }
1278
1279 if !matches!(self.cross_state.frame.here_state, HereState::None) {
1281 if self.cross_state.frame.here_state.awaiting_tag() && state.started_token() {
1286 state.delimit_current_token(
1287 TokenEndReason::EndOfInput,
1288 &mut self.cross_state,
1289 )?;
1290 }
1291 if self.cross_state.frame.here_state.awaiting_tag() {
1292 return Err(TokenizerError::MissingHereTagAtEndOfInput);
1293 }
1294
1295 if matches!(self.cross_state.frame.here_state, HereState::InHereDocs)
1299 && self.remove_here_end_tag(&mut state, &mut result, false)?
1300 {
1301 continue;
1304 }
1305
1306 let tag_names = self
1307 .cross_state
1308 .frame
1309 .current_here_tags
1310 .iter()
1311 .map(|tag| std::format!("`{}`", tag.tag.trim_end_matches('\n')))
1312 .collect::<Vec<_>>()
1313 .join(", ");
1314 let tag_positions = self
1315 .cross_state
1316 .frame
1317 .current_here_tags
1318 .iter()
1319 .map(|tag| std::format!("{}", tag.position))
1320 .collect::<Vec<_>>()
1321 .join(", ");
1322 return Err(TokenizerError::UnterminatedHereDocuments(
1323 tag_names,
1324 tag_positions,
1325 ));
1326 }
1327
1328 result = state
1329 .delimit_current_token(TokenEndReason::EndOfInput, &mut self.cross_state)?;
1330 } else if matches!(self.cross_state.frame.here_state, HereState::InHereDocs) {
1334 if !self.cross_state.frame.current_here_tags.is_empty()
1340 && self.cross_state.frame.current_here_tags[0].remove_tabs
1341 && (!state.started_token() || state.current_token().ends_with('\n'))
1342 && c == '\t'
1343 {
1344 self.consume_char()?;
1346 } else {
1347 self.consume_char()?;
1348 state.append_char(c);
1349
1350 if c == '\n' {
1353 self.remove_here_end_tag(&mut state, &mut result, true)?;
1354 } else if c == '\\'
1355 && self
1356 .cross_state
1357 .frame
1358 .current_here_tags
1359 .first()
1360 .is_some_and(|tag| !tag.tag_was_escaped_or_quoted)
1361 {
1362 if let Some(escaped) = self.next_char()? {
1368 state.append_char(escaped);
1369 }
1370 }
1371 }
1372 } else if state.unquoted()
1383 && !state.in_operator()
1384 && closing_char == Some(c)
1385 && self.cross_state.frame.closers_needed == 1
1386 {
1387 let reason = if state.started_token() {
1388 TokenEndReason::Other
1389 } else {
1390 TokenEndReason::ExpansionClosingChar
1391 };
1392 result = state.delimit_current_token(reason, &mut self.cross_state)?;
1393 } else if state.in_operator() {
1394 let mut hypothetical_token = state.current_token().to_owned();
1401 hypothetical_token.push(c);
1402
1403 if state.unquoted() && self.is_operator(hypothetical_token.as_ref()) {
1404 self.consume_char()?;
1405 state.append_char(c);
1406 } else {
1407 assert!(state.started_token());
1408
1409 if in_arithmetic_expansion || in_parameter_expansion {
1420 } else if self.cross_state.frame.arithmetic_command_parens > 0 {
1424 let parens = &mut self.cross_state.frame.arithmetic_command_parens;
1427 if state.is_specific_operator("(") {
1428 *parens += 1;
1429 } else if state.is_specific_operator(")") {
1430 *parens -= 1;
1431 }
1432 } else if state.is_specific_operator("<<") {
1433 self.cross_state.frame.here_state =
1434 HereState::NextTokenIsHereTag { remove_tabs: false };
1435 } else if state.is_specific_operator("<<-") {
1436 self.cross_state.frame.here_state =
1437 HereState::NextTokenIsHereTag { remove_tabs: true };
1438 } else if state.is_specific_operator("(") && c == '(' {
1439 self.cross_state.frame.arithmetic_command_parens = 1;
1441 }
1442
1443 let reason = if state.current_token() == "\n" {
1444 TokenEndReason::UnescapedNewLine
1445 } else {
1446 TokenEndReason::OperatorEnd
1447 };
1448
1449 result = state.delimit_current_token(reason, &mut self.cross_state)?;
1450 }
1451 } else if does_char_newly_affect_quoting(&state, c) {
1456 if c == '\\' {
1457 self.consume_char()?;
1458 state.in_escape = true;
1459 state.append_char(c);
1460 } else if c == '\'' {
1461 state.quote_mode = QuoteMode::Single(self.cross_state.cursor.clone());
1464 self.consume_char()?;
1465 state.append_char(c);
1466 } else if c == '\"' {
1467 state.quote_mode = QuoteMode::Double(self.cross_state.cursor.clone());
1468 self.consume_char()?;
1469 state.append_char(c);
1470 }
1471 }
1472 else if !state.in_escape
1475 && matches!(
1476 state.quote_mode,
1477 QuoteMode::Single(..) | QuoteMode::AnsiC(..)
1478 )
1479 && c == '\''
1480 {
1481 state.quote_mode = QuoteMode::None;
1482 self.consume_char()?;
1483 state.append_char(c);
1484 } else if !state.in_escape
1485 && matches!(state.quote_mode, QuoteMode::Double(..))
1486 && c == '\"'
1487 {
1488 state.quote_mode = QuoteMode::None;
1489 self.consume_char()?;
1490 state.append_char(c);
1491 }
1492 else if state.in_escape {
1496 state.in_escape = false;
1497 self.consume_char()?;
1498 state.append_char(c);
1499 } else if (state.unquoted()
1500 || (matches!(state.quote_mode, QuoteMode::Double(_)) && !state.in_escape))
1501 && (c == '$' || c == '`')
1502 {
1503 let in_double_quotes = matches!(state.quote_mode, QuoteMode::Double(_));
1505 self.consume_dollar_or_backquote(&mut state, c, in_double_quotes)?;
1506 }
1507 else if c == '('
1513 && self.options.enable_extended_globbing
1514 && !in_parameter_expansion
1515 && state.unquoted()
1516 && !state.in_operator()
1517 && state
1518 .current_token()
1519 .ends_with(|x| Self::can_start_extglob(x))
1520 {
1521 self.consume_char()?;
1523 state.append_char(c);
1524
1525 let mut paren_depth = 1;
1526 let mut quote: Option<(char, bool)> = None;
1530 let mut after_dollar = false;
1531
1532 while paren_depth > 0 {
1539 if !matches!(quote, Some(('\'', _))) {
1542 self.skip_line_continuations()?;
1543 }
1544 let Some(extglob_char) = self.peek_char()? else {
1545 return Err(TokenizerError::UnterminatedExtendedGlob(
1546 self.cross_state.cursor.clone(),
1547 ));
1548 };
1549 let was_after_dollar = std::mem::take(&mut after_dollar);
1550
1551 let starts_nested_construct = match quote {
1552 None => extglob_char == '`',
1553 Some(('"', _)) => matches!(extglob_char, '`' | '$'),
1554 Some(_) => false,
1555 };
1556 if starts_nested_construct {
1557 let in_double_quotes = quote.is_some();
1562 self.consume_dollar_or_backquote(
1563 &mut state,
1564 extglob_char,
1565 in_double_quotes,
1566 )?;
1567 continue;
1568 }
1569
1570 self.consume_char()?;
1572 state.append_char(extglob_char);
1573
1574 match extglob_char {
1575 '\\' if quote.is_none_or(|(_, escapes)| escapes) => {
1578 if let Some(escaped_char) = self.next_char()? {
1579 state.append_char(escaped_char);
1580 }
1581 }
1582 c if quote.is_some_and(|(close, _)| close == c) => quote = None,
1583 _ if quote.is_some() => (),
1584 '\'' => quote = Some(('\'', was_after_dollar)),
1585 '"' => quote = Some(('"', true)),
1586 '$' => after_dollar = !was_after_dollar,
1588 '(' => paren_depth += 1,
1589 ')' => paren_depth -= 1,
1590 _ => (),
1591 }
1592 }
1593 } else if state.unquoted() && Self::can_start_operator(c) {
1597 if state.started_token() {
1598 result = state.delimit_current_token(
1599 TokenEndReason::OperatorStart,
1600 &mut self.cross_state,
1601 )?;
1602 } else {
1603 state.token_is_operator = true;
1604 self.consume_char()?;
1605 state.append_char(c);
1606 self.cross_state.frame.note_unquoted_char(c);
1607 }
1608 } else if state.unquoted() && is_blank(c) {
1612 if state.started_token() {
1613 result = state.delimit_current_token(
1614 TokenEndReason::NonNewLineBlank,
1615 &mut self.cross_state,
1616 )?;
1617
1618 if include_space {
1621 continue;
1622 }
1623 } else if include_space {
1624 state.append_char(c);
1625 } else {
1626 state.start_position.column += 1;
1628 state.start_position.index += 1;
1629 }
1630
1631 self.consume_char()?;
1632 }
1633 else if !state.token_is_operator
1642 && (in_parameter_expansion
1643 || (state.started_token() && !(c == '#' && state.only_blanks_so_far())))
1644 {
1645 self.consume_char()?;
1646 state.append_char(c);
1647 if state.unquoted() {
1648 self.cross_state.frame.note_unquoted_char(c);
1649 }
1650 } else if c == '#' {
1651 self.consume_char()?;
1653
1654 let mut done = false;
1655 while !done {
1656 done = match self.peek_char()? {
1657 Some('\n') => true,
1658 None => true,
1659 _ => {
1660 self.consume_char()?;
1663 false
1664 }
1665 };
1666 }
1667 } else if state.started_token() {
1669 result =
1672 state.delimit_current_token(TokenEndReason::Other, &mut self.cross_state)?;
1673 } else {
1674 self.consume_char()?;
1678 state.append_char(c);
1679 self.cross_state.frame.note_unquoted_char(c);
1680 }
1681 }
1682
1683 let result = result.unwrap();
1684
1685 Ok(result)
1686 }
1687
1688 fn remove_here_end_tag(
1689 &mut self,
1690 state: &mut TokenParseState,
1691 result: &mut Option<TokenizeResult>,
1692 ends_with_newline: bool,
1693 ) -> Result<bool, TokenizerError> {
1694 if self.cross_state.frame.current_here_tags.is_empty() {
1696 return Ok(false);
1697 }
1698
1699 let next_here_tag = &self.cross_state.frame.current_here_tags[0];
1700
1701 let tag_str: Cow<'_, str> = if next_here_tag.tag_was_escaped_or_quoted {
1702 unquote_str(next_here_tag.tag.as_str()).into()
1703 } else {
1704 next_here_tag.tag.as_str().into()
1705 };
1706
1707 let tag_str = if !ends_with_newline {
1708 tag_str
1709 .strip_suffix('\n')
1710 .unwrap_or_else(|| tag_str.as_ref())
1711 } else {
1712 tag_str.as_ref()
1713 };
1714
1715 if tag_str.is_empty() {
1720 return Ok(false);
1721 }
1722
1723 if let Some(current_token_without_here_tag) = state.current_token().strip_suffix(tag_str) {
1724 if current_token_without_here_tag.is_empty()
1728 || current_token_without_here_tag.ends_with('\n')
1729 {
1730 state.replace_with_here_doc(current_token_without_here_tag.to_owned());
1731
1732 *result = state.delimit_current_token(
1734 TokenEndReason::HereDocumentBodyEnd,
1735 &mut self.cross_state,
1736 )?;
1737
1738 return Ok(true);
1739 }
1740 }
1741 Ok(false)
1742 }
1743
1744 const fn can_start_extglob(c: char) -> bool {
1745 matches!(c, '@' | '!' | '?' | '+' | '*')
1746 }
1747
1748 const fn can_start_operator(c: char) -> bool {
1749 matches!(c, '&' | '(' | ')' | ';' | '\n' | '|' | '<' | '>')
1750 }
1751
1752 fn is_operator(&self, s: &str) -> bool {
1753 if !self.options.sh_mode && matches!(s, "<<<" | "&>" | "&>>" | ";;&" | ";&" | "|&") {
1755 return true;
1756 }
1757
1758 matches!(
1759 s,
1760 "&" | "&&"
1761 | "("
1762 | ")"
1763 | ";"
1764 | ";;"
1765 | "\n"
1766 | "|"
1767 | "||"
1768 | "<"
1769 | ">"
1770 | ">|"
1771 | "<<"
1772 | ">>"
1773 | "<&"
1774 | ">&"
1775 | "<<-"
1776 | "<>"
1777 )
1778 }
1779}
1780
1781impl<R: ?Sized + std::io::BufRead> Iterator for Tokenizer<'_, R> {
1782 type Item = Result<TokenizeResult, TokenizerError>;
1783
1784 fn next(&mut self) -> Option<Self::Item> {
1785 match self.next_token() {
1786 #[expect(clippy::manual_map)]
1787 Ok(result) => match result.token {
1788 Some(_) => Some(Ok(result)),
1789 None => None,
1790 },
1791 Err(e) => Some(Err(e)),
1792 }
1793 }
1794}
1795
1796fn append_expansion_token(
1801 state: &mut TokenParseState,
1802 bodies: &mut String,
1803 result: &TokenizeResult,
1804) {
1805 let text = result.token.as_ref().map_or("", Token::to_str);
1806 match result.reason {
1807 TokenEndReason::HereDocumentBodyStart => bodies.push('\n'),
1808 TokenEndReason::HereDocumentBodyEnd | TokenEndReason::HereDocumentEndTag => {
1809 bodies.push_str(text);
1810 }
1811 _ => state.append_str(text),
1812 }
1813}
1814
1815const fn is_blank(c: char) -> bool {
1816 c == ' ' || c == '\t'
1817}
1818
1819const fn does_char_newly_affect_quoting(state: &TokenParseState, c: char) -> bool {
1820 if state.in_escape {
1822 return false;
1823 }
1824
1825 match state.quote_mode {
1826 QuoteMode::Double(_) | QuoteMode::AnsiC(_) => {
1829 if c == '\\' {
1830 true
1832 } else {
1833 false
1834 }
1835 }
1836 QuoteMode::Single(_) => false,
1838 QuoteMode::None => is_quoting_char(c),
1841 }
1842}
1843
1844const fn is_quoting_char(c: char) -> bool {
1845 matches!(c, '\\' | '\'' | '\"')
1846}
1847
1848pub fn unquote_str(s: &str) -> String {
1860 let mut result = String::with_capacity(s.len());
1861 let mut quote = None;
1863 let mut chars = s.chars().peekable();
1864
1865 while let Some(c) = chars.next() {
1866 match (c, quote) {
1867 ('\\', None) => result.extend(chars.next()),
1868 ('\\', Some('"')) => {
1869 if !matches!(chars.peek(), Some('$' | '`' | '"' | '\\' | '\n')) {
1870 result.push(c);
1871 }
1872 result.extend(chars.next());
1873 }
1874 (_, Some(q)) if c == q => quote = None,
1875 ('\'' | '"', None) => quote = Some(c),
1876 _ => result.push(c),
1877 }
1878 }
1879
1880 result
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885
1886 use anyhow::Result;
1887 use insta::assert_ron_snapshot;
1888 use pretty_assertions::{assert_eq, assert_matches};
1889
1890 use super::*;
1891
1892 #[derive(serde::Serialize, serde::Deserialize)]
1893 struct TokenizerResult<'a> {
1894 input: &'a str,
1895 result: Vec<Token>,
1896 }
1897
1898 fn test_tokenizer(input: &str) -> Result<TokenizerResult<'_>> {
1899 Ok(TokenizerResult {
1900 input,
1901 result: tokenize_str(input)?,
1902 })
1903 }
1904
1905 #[test]
1906 fn tokenize_empty() -> Result<()> {
1907 let tokens = tokenize_str("")?;
1908 assert_eq!(tokens.len(), 0);
1909 Ok(())
1910 }
1911
1912 #[test]
1913 fn tokenize_line_continuation() -> Result<()> {
1914 assert_ron_snapshot!(test_tokenizer(
1915 r"a\
1916bc"
1917 )?);
1918 Ok(())
1919 }
1920
1921 #[test]
1922 fn tokenize_operators() -> Result<()> {
1923 assert_ron_snapshot!(test_tokenizer("a>>b")?);
1924 Ok(())
1925 }
1926
1927 #[test]
1928 fn tokenize_line_continuations() -> Result<()> {
1929 for (input, expected) in [
1932 ("a&\\\n&b", &["a", "&&", "b"][..]),
1933 ("a|\\\n&b", &["a", "|&", "b"]),
1934 ("a&&\\\n", &["a", "&&"]),
1935 ("a <<\\\n", &["a", "<<"]),
1937 ("a&&\\b", &["a", "&&", "\\b"]),
1938 ("a<\\\n<b\nx\nb\n", &["a", "<<", "b", "x\n", "b", "\n"]),
1939 ("a<<\\b\n$x\nb\n", &["a", "<<", "\\b", "$x\n", "b", "\n"]),
1940 ("$\\\n(a) $\\\n{a}", &["$(a)", "${a}"]),
1941 ("$(\\\n(1))", &["$((1))"]),
1942 (
1943 "$(a\\\nb) ${a\\\nb} $((1+\\\n1))",
1944 &["$(ab)", "${ab}", "$((1+1))"],
1945 ),
1946 ("\"a\\\nb\" `'a\\\nb'`", &["\"ab\"", "`'ab'`"]),
1947 ("@(a\\\nb|\"a\\\nb\")", &["@(ab|\"ab\")"]),
1948 ("a\\\\\n", &["a\\\\", "\n"]),
1950 ("'a\\\nb' $'a\\\nb'", &["'a\\\nb'", "$'a\\\nb'"]),
1951 ("`\\\\\n`", &["`\\\\\n`"]),
1952 ("@('a\\\nb')", &["@('a\\\nb')"]),
1953 ("a #b\\\nc", &["a", "\n", "c"]),
1954 (
1956 "<<a\nb\\\nc\\\\\nd\na\n",
1957 &["<<", "a", "bc\\\\\nd\n", "a", "\n"],
1958 ),
1959 ("<<a\nb\\\na\na\n", &["<<", "a", "ba\n", "a", "\n"]),
1960 ("<<'a'\n\\\nb\na\n", &["<<", "'a'", "\\\nb\n", "a", "\n"]),
1961 ] {
1962 let tokens = tokenize_str(input)?;
1963 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
1964 assert_eq!(token_strs, expected, "tokenizing {input:?}");
1965 }
1966 Ok(())
1967 }
1968
1969 #[test]
1970 fn tokenize_token_after_blanks_and_line_continuation() -> Result<()> {
1971 assert_matches!(
1974 &tokenize_str("a; \\\n c")?[..],
1975 [_, _, Token::Word(word, span)]
1976 if word == "c" && (span.start.line, span.start.column, span.start.index) == (2, 3, 7)
1977 );
1978 Ok(())
1979 }
1980
1981 #[test]
1982 fn tokenize_operator_followed_by_backslash() -> Result<()> {
1983 assert_ron_snapshot!(test_tokenizer("a;\\b|\\\n c")?);
1987 Ok(())
1988 }
1989
1990 #[test]
1991 fn tokenize_comment() -> Result<()> {
1992 assert_ron_snapshot!(test_tokenizer(
1993 r"a #comment
1994"
1995 )?);
1996 Ok(())
1997 }
1998
1999 #[test]
2000 fn tokenize_comment_in_command_substitution() {
2001 for prefix in ["", " ", " ", " ", "\t", "\t\t", " \t", "\t "] {
2007 let input = format!("$({prefix}# it's a comment\n)\n");
2008 let tokens = tokenize_str(input.as_str()).unwrap();
2009 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2010 assert_eq!(
2011 token_strs,
2012 [format!("$({prefix}\n)").as_str(), "\n"],
2013 "tokenizing {input:?}"
2014 );
2015 }
2016 }
2017
2018 #[test]
2019 fn tokenize_comment_at_eof() -> Result<()> {
2020 assert_ron_snapshot!(test_tokenizer(r"a #comment")?);
2021 Ok(())
2022 }
2023
2024 #[test]
2025 fn tokenize_empty_here_doc() -> Result<()> {
2026 assert_ron_snapshot!(test_tokenizer(
2027 r"cat <<HERE
2028HERE
2029"
2030 )?);
2031 Ok(())
2032 }
2033
2034 #[test]
2035 fn tokenize_here_doc() -> Result<()> {
2036 assert_ron_snapshot!(test_tokenizer(
2037 r"cat <<HERE
2038SOMETHING
2039HERE
2040echo after
2041"
2042 )?);
2043 assert_ron_snapshot!(test_tokenizer(
2044 r"cat <<HERE
2045SOMETHING
2046HERE
2047"
2048 )?);
2049 assert_ron_snapshot!(test_tokenizer(
2050 r"cat <<HERE
2051SOMETHING
2052HERE
2053
2054"
2055 )?);
2056 assert_ron_snapshot!(test_tokenizer(
2057 r"cat <<HERE
2058SOMETHING
2059HERE"
2060 )?);
2061 Ok(())
2062 }
2063
2064 #[test]
2065 fn tokenize_here_doc_with_tab_removal() -> Result<()> {
2066 assert_ron_snapshot!(test_tokenizer(
2067 r"cat <<-HERE
2068 SOMETHING
2069 HERE
2070"
2071 )?);
2072 Ok(())
2073 }
2074
2075 #[test]
2076 fn tokenize_here_doc_with_other_tokens() -> Result<()> {
2077 assert_ron_snapshot!(test_tokenizer(
2078 r"cat <<EOF | wc -l
2079A B C
20801 2 3
2081D E F
2082EOF
2083"
2084 )?);
2085 Ok(())
2086 }
2087
2088 #[test]
2089 fn tokenize_multiple_here_docs() -> Result<()> {
2090 assert_ron_snapshot!(test_tokenizer(
2091 r"cat <<HERE1 <<HERE2
2092SOMETHING
2093HERE1
2094OTHER
2095HERE2
2096echo after
2097"
2098 )?);
2099 Ok(())
2100 }
2101
2102 #[test]
2103 fn tokenize_unterminated_here_doc() {
2104 let result = tokenize_str(
2105 r"cat <<HERE
2106SOMETHING
2107",
2108 );
2109 assert!(result.is_err());
2110 }
2111
2112 #[test]
2113 fn tokenize_here_tag_with_unterminated_expansion() {
2114 for input in [
2118 "<<E$[\t\t",
2119 "<<-E$[\t\t",
2120 "<<E$(\t\t",
2121 "<<E$((\t\t",
2122 "<<E\"$[\t\t",
2123 "x <<$(( )",
2124 "cat <<'' $(",
2125 ] {
2126 assert_matches!(
2127 tokenize_str(input),
2128 Err(TokenizerError::UnterminatedExpansion),
2129 "for {input:?}"
2130 );
2131 }
2132 }
2133
2134 #[test]
2135 fn tokenize_here_doc_with_empty_quoted_tag_at_end_of_input() {
2136 for input in ["cat <<'' ", "cat <<\"\" "] {
2140 assert_matches!(
2141 tokenize_str(input),
2142 Err(TokenizerError::UnterminatedHereDocuments(..)),
2143 "for {input:?}"
2144 );
2145 }
2146 }
2147
2148 #[test]
2149 fn tokenize_here_tag_containing_expansions() {
2150 for tag in ["$(x)", "E${x}", "$((1 + 2))", "$[1]", "$(echo a)", "\r"] {
2151 let input = std::format!("cat <<{tag}\nbody\n{tag}\n");
2152 assert_matches!(
2153 tokenize_str(input.as_str()),
2154 Ok(tokens) if tokens.iter().any(|t| matches!(t, Token::Word(w, _) if w == "body\n")),
2155 "for {input:?}"
2156 );
2157 }
2158 }
2159
2160 #[test]
2161 fn tokenize_missing_here_tag_reports_what_was_found() {
2162 for (input, found) in [
2163 ("cat <<\n", "\n"),
2164 ("cat <<;", ";"),
2165 ("cat <<)|$(\n)", ")"),
2166 ("echo $(cat << )", ")"),
2167 ("echo $(cat <<)", ")"),
2168 ("echo \"$(cat <<-)\"", ")"),
2169 ] {
2170 assert_matches!(
2171 tokenize_str(input),
2172 Err(TokenizerError::MissingHereTag(actual)) if actual == found,
2173 "for {input:?}"
2174 );
2175 }
2176 }
2177
2178 #[test]
2179 fn tokenize_input_ending_where_here_tag_expected_is_incomplete() {
2180 for input in ["cat << ", "cat << \\\n"] {
2184 assert_matches!(
2185 tokenize_str(input),
2186 Err(e) if e.is_incomplete(),
2187 "for {input:?}"
2188 );
2189 }
2190 }
2191
2192 #[test]
2193 fn tokenize_here_doc_operator_in_parameter_expansion_is_literal() {
2194 for word in ["${x:-<<}", "${x:-<< foo}", "${x:-a<<b}", "${x:-<<-EOF}"] {
2196 let input = std::format!("echo {word}\n");
2197 assert_matches!(
2198 tokenize_str(input.as_str()),
2199 Ok(tokens) if tokens.iter().any(|t| matches!(t, Token::Word(w, _) if w == word)),
2200 "for {input:?}"
2201 );
2202 }
2203 }
2204
2205 #[test]
2206 fn tokenize_words_verbatim() {
2207 for word in [
2208 "${u:-a@(}",
2210 "$${u",
2212 "$$'a\\'x",
2213 "\\$'a\\'x",
2215 "$[ a[1] + 1 ]",
2218 "$[ a[ a[0] ] ]",
2219 "$[ ${a[1]} + 1 ]",
2220 "$[ $[1] + a[$[0]] ]",
2221 "$[ \"[\" ]",
2222 "@(\"$'x'\")",
2224 "\"${x:-a\tb}\"",
2226 ] {
2227 assert_matches!(
2228 tokenize_str(word),
2229 Ok(tokens) if matches!(tokens.as_slice(), [Token::Word(w, _)] if w == word),
2230 "for {word:?}"
2231 );
2232 }
2233 }
2234
2235 #[test]
2236 fn tokenize_here_doc_left_open_by_command_substitution() -> Result<()> {
2237 for (input, expected) in [
2241 (
2242 "x=\"$(cat <<EOF)\"\nbody\nEOF\n",
2243 &["x=\"$(cat <<EOF\nbody\nEOF\n)\"", "\n"][..],
2244 ),
2245 (
2246 "echo \"[$(cat <<EOF | tr a-z A-Z)]\"\nbody\nEOF\n",
2247 &["echo", "\"[$(cat <<EOF | tr a-z A-Z\nbody\nEOF\n)]\"", "\n"],
2248 ),
2249 (
2250 "echo \"[$(cat <<A; cat <<-B)]\"\na\nA\n\tb\n\tB\n",
2251 &["echo", "\"[$(cat <<A; cat <<-B\na\nA\nb\nB\n)]\"", "\n"],
2252 ),
2253 (
2254 "echo \"[$(echo $(cat <<EOF))]\"\nbody\nEOF\n",
2255 &["echo", "\"[$(echo $(cat <<EOF\nbody\nEOF\n))]\"", "\n"],
2256 ),
2257 (
2258 "echo \"[${x:-$(cat <<EOF)}]\"\nbody\nEOF\n",
2259 &["echo", "\"[${x:-$(cat <<EOF\nbody\nEOF\n)}]\"", "\n"],
2260 ),
2261 (
2262 "cat <<A; echo \"[$(cat <<B)]\"\nb\nB\na\nA\n",
2263 &[
2264 "cat",
2265 "<<",
2266 "A",
2267 "a\n",
2268 "A",
2269 ";",
2270 "echo",
2271 "\"[$(cat <<B\nb\nB\n)]\"",
2272 "\n",
2273 ],
2274 ),
2275 (
2276 "echo \"[$(cat <<A)]\" \"[$(cat <<B)]\"\na\nA\nb\nB\n",
2277 &[
2278 "echo",
2279 "\"[$(cat <<A\na\nA\n)]\"",
2280 "\"[$(cat <<B\nb\nB\n)]\"",
2281 "\n",
2282 ],
2283 ),
2284 ] {
2285 let tokens = tokenize_str(input)?;
2286 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2287 assert_eq!(token_strs, expected, "for {input:?}");
2288 }
2289
2290 let tokens = tokenize_str("echo \"$(cat <<EOF)\" x\nbody\nEOF\ny\n")?;
2293 let positions: Vec<_> = tokens
2294 .iter()
2295 .map(|t| {
2296 (
2297 t.to_str(),
2298 t.location().start.line,
2299 t.location().start.column,
2300 )
2301 })
2302 .collect();
2303 assert_eq!(
2304 positions,
2305 [
2306 ("echo", 1, 1),
2307 ("\"$(cat <<EOF\nbody\nEOF\n)\"", 1, 6),
2308 ("x", 1, 21),
2309 ("\n", 1, 22),
2310 ("y", 4, 1),
2311 ("\n", 4, 2),
2312 ]
2313 );
2314
2315 Ok(())
2316 }
2317
2318 #[test]
2319 fn tokenize_unterminated_here_doc_names_its_tag() {
2320 assert_matches!(
2321 tokenize_str("cat <<]|$[\n]"),
2322 Err(TokenizerError::UnterminatedHereDocuments(tags, _)) if tags == "`]`"
2323 );
2324 }
2325
2326 #[test]
2327 fn tokenize_deeply_nested_expansions_is_bounded() {
2328 for (open, close) in [("$(", ")"), ("\"$(", ")\""), ("${x:-", "}"), ("$[", "]")] {
2332 let input = std::format!("echo {}x{}", open.repeat(10_000), close.repeat(10_000));
2333 assert_matches!(
2334 tokenize_str(input.as_str()),
2335 Err(TokenizerError::ExpansionNestingTooDeep),
2336 "for {open:?}"
2337 );
2338 }
2339 }
2340
2341 #[test]
2342 fn tokenize_nesting_up_to_limit() {
2343 let nested = |depth: u32| {
2344 let depth = depth as usize;
2345 tokenize_str(&std::format!(
2346 "echo {}x{}",
2347 "$(".repeat(depth),
2348 ")".repeat(depth)
2349 ))
2350 };
2351 assert!(nested(MAX_EXPANSION_NESTING).is_ok());
2352 assert_matches!(
2353 nested(MAX_EXPANSION_NESTING + 1),
2354 Err(TokenizerError::ExpansionNestingTooDeep)
2355 );
2356 }
2357
2358 #[test]
2359 fn tokenize_missing_here_tag() {
2360 let result = tokenize_str(
2361 r"cat <<
2362",
2363 );
2364 assert!(result.is_err());
2365 }
2366
2367 #[test]
2368 fn tokenize_here_doc_in_command_substitution() -> Result<()> {
2369 assert_ron_snapshot!(test_tokenizer(
2370 r"echo $(cat <<HERE
2371TEXT
2372HERE
2373)"
2374 )?);
2375 Ok(())
2376 }
2377
2378 #[test]
2379 fn tokenize_here_doc_in_double_quoted_command_substitution() -> Result<()> {
2380 assert_ron_snapshot!(test_tokenizer(
2381 r#"echo "$(cat <<HERE
2382TEXT
2383HERE
2384)""#
2385 )?);
2386 Ok(())
2387 }
2388
2389 #[test]
2390 fn tokenize_here_doc_in_double_quoted_command_substitution_with_space() -> Result<()> {
2391 assert_ron_snapshot!(test_tokenizer(
2392 r#"echo "$(cat << HERE
2393TEXT
2394HERE
2395)""#
2396 )?);
2397 Ok(())
2398 }
2399
2400 #[test]
2401 fn tokenize_complex_here_docs_in_command_substitution() -> Result<()> {
2402 assert_ron_snapshot!(test_tokenizer(
2403 r"echo $(cat <<HERE1 <<HERE2 | wc -l
2404TEXT
2405HERE1
2406OTHER
2407HERE2
2408)"
2409 )?);
2410 Ok(())
2411 }
2412
2413 #[test]
2414 fn tokenize_simple_backquote() -> Result<()> {
2415 assert_ron_snapshot!(test_tokenizer(r"echo `echo hi`")?);
2416 Ok(())
2417 }
2418
2419 #[test]
2420 fn tokenize_backquote_with_escape() -> Result<()> {
2421 assert_ron_snapshot!(test_tokenizer(r"echo `echo\`hi`")?);
2422 Ok(())
2423 }
2424
2425 #[test]
2426 fn tokenize_unterminated_backquote() {
2427 assert_matches!(
2428 tokenize_str("`"),
2429 Err(TokenizerError::UnterminatedBackquote(_))
2430 );
2431 }
2432
2433 #[test]
2434 fn tokenize_unterminated_command_substitution() {
2435 assert_matches!(
2439 tokenize_str("$("),
2440 Err(TokenizerError::UnterminatedExpansion)
2441 );
2442 }
2443
2444 #[test]
2445 fn command_substitution_body_stops_at_closing_paren() -> Result<()> {
2446 let options = TokenizerOptions::default();
2447 assert_eq!(
2448 command_substitution_body("echo hi) rest", &options)?,
2449 "echo hi"
2450 );
2451 assert_eq!(
2452 command_substitution_body(r#"echo ")" (a)) rest"#, &options)?,
2453 r#"echo ")" (a)"#
2454 );
2455 assert_eq!(
2456 command_substitution_body("cat <<'EOF'\n\"it's ) `\nEOF\n) rest", &options)?,
2457 "cat <<'EOF'\n\"it's ) `\nEOF\n"
2458 );
2459 assert_eq!(
2462 command_substitution_body("cat <<E\n)\nE\n) rest", &options)?,
2463 "cat <<E\n)\nE\n"
2464 );
2465 assert_eq!(
2466 command_substitution_body("cat <<E\n)\nE\necho after) rest", &options)?,
2467 "cat <<E\n)\nE\necho after"
2468 );
2469 assert_eq!(
2473 command_substitution_body("cat <<EOF) rest\n", &options)?,
2474 "cat <<EOF"
2475 );
2476 assert_eq!(
2477 command_substitution_body("echo $(cat <<EOF)) rest\n", &options)?,
2478 "echo $(cat <<EOF)"
2479 );
2480 assert_eq!(
2483 command_substitution_body(r#"printf "%s" @(")")) rest"#, &options)?,
2484 r#"printf "%s" @(")")"#
2485 );
2486 assert_eq!(
2488 command_substitution_body("echo “é”) ü", &options)?,
2489 "echo “é”"
2490 );
2491 Ok(())
2492 }
2493
2494 #[test]
2495 fn command_substitution_body_unterminated() {
2496 let options = TokenizerOptions::default();
2497 assert_matches!(
2498 command_substitution_body("echo hi", &options),
2499 Err(TokenizerError::UnterminatedExpansion)
2500 );
2501 assert_matches!(
2502 command_substitution_body("echo 'hi)", &options),
2503 Err(TokenizerError::UnterminatedSingleQuote(_))
2504 );
2505 }
2506
2507 #[test]
2508 fn tokenize_unterminated_arithmetic_expansion() {
2509 assert_matches!(
2510 tokenize_str("$(("),
2511 Err(TokenizerError::UnterminatedExpansion)
2512 );
2513 }
2514
2515 #[test]
2516 fn tokenize_unterminated_legacy_arithmetic_expansion() {
2517 assert_matches!(
2518 tokenize_str("$["),
2519 Err(TokenizerError::UnterminatedExpansion)
2520 );
2521 }
2522
2523 #[test]
2524 fn tokenize_command_substitution() -> Result<()> {
2525 assert_ron_snapshot!(test_tokenizer("a$(echo hi)b c")?);
2526 Ok(())
2527 }
2528
2529 #[test]
2530 fn tokenize_command_substitution_with_subshell() -> Result<()> {
2531 assert_ron_snapshot!(test_tokenizer("$( (:) )")?);
2532 Ok(())
2533 }
2534
2535 #[test]
2536 fn tokenize_command_substitution_containing_extglob() -> Result<()> {
2537 assert_ron_snapshot!(test_tokenizer("echo $(echo !(x))")?);
2538 Ok(())
2539 }
2540
2541 #[test]
2542 fn tokenize_extglob_with_quotes_and_escapes() -> Result<()> {
2543 for (input, expected) in [
2544 (r#"@(")") y"#, [r#"@(")")"#, "y"]),
2546 (r"@(a|')'|b)x y", [r"@(a|')'|b)x", "y"]),
2547 (r#"@("(") y"#, [r#"@("(")"#, "y"]),
2548 (r#"@("\")") y"#, [r#"@("\")")"#, "y"]),
2550 (r"@('\')x y", [r"@('\')x", "y"]),
2552 (r#"@(\") y"#, [r#"@(\")"#, "y"]),
2554 (r"@(\)) y", [r"@(\))", "y"]),
2555 (r"@($'\'')x y", [r"@($'\'')x", "y"]),
2557 (r"@($'a\')'|b) y", [r"@($'a\')'|b)", "y"]),
2558 (r"@(\$'a\')x y", [r"@(\$'a\')x", "y"]),
2559 (r"@($$'a\')x y", [r"@($$'a\')x", "y"]),
2561 (r"@($$$'\'')x y", [r"@($$$'\'')x", "y"]),
2562 (r"@(}`'`)x y", [r"@(}`'`)x", "y"]),
2564 (r#"@(`echo \\"`)x y"#, [r#"@(`echo \\"`)x"#, "y"]),
2565 (r"@(`$'\'`)x y", [r"@(`$'\'`)x", "y"]),
2566 (r#"@(`echo ")"`)x y"#, [r#"@(`echo ")"`)x"#, "y"]),
2567 (r#"@("$(echo ")")")x y"#, [r#"@("$(echo ")")")x"#, "y"]),
2569 (r#"@("`echo "\)"`")x y"#, [r#"@("`echo "\)"`")x"#, "y"]),
2570 (r#"@("${u:-"}"}")x y"#, [r#"@("${u:-"}"}")x"#, "y"]),
2571 (r#"@($")")x y"#, [r#"@($")")x"#, "y"]),
2572 (r#"+(a|@(b|")")|'"') y"#, [r#"+(a|@(b|")")|'"')"#, "y"]),
2574 ] {
2575 let tokens = tokenize_str(input)?;
2576 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
2577 assert_eq!(token_strs, expected, "input: {input}");
2578 }
2579 Ok(())
2580 }
2581
2582 #[test]
2583 fn tokenize_unterminated_construct_in_extglob() {
2584 for input in [r#"@("$(\$"\))x"#, r#"@($"$(\)")x"#, r"@(`)x", r#"@("`)")x"#] {
2587 assert!(tokenize_str(input).is_err(), "input: {input}");
2588 }
2589 }
2590
2591 #[test]
2592 fn tokenize_unterminated_extglob() {
2593 for input in [r"@(a", r#"@(")""#, r"@(')'", r"@(\)"] {
2594 assert_matches!(
2595 tokenize_str(input),
2596 Err(TokenizerError::UnterminatedExtendedGlob(_)),
2597 "input: {input}"
2598 );
2599 }
2600 }
2601
2602 #[test]
2603 fn tokenize_arithmetic_expression() -> Result<()> {
2604 assert_ron_snapshot!(test_tokenizer("a$((1+2))b c")?);
2605 Ok(())
2606 }
2607
2608 #[test]
2609 fn tokenize_arithmetic_expression_with_space() -> Result<()> {
2610 assert_ron_snapshot!(test_tokenizer("$(( 1 ))")?);
2613 Ok(())
2614 }
2615 #[test]
2616 fn tokenize_arithmetic_expression_with_parens() -> Result<()> {
2617 assert_ron_snapshot!(test_tokenizer("$(( (0) ))")?);
2618 Ok(())
2619 }
2620
2621 #[test]
2622 fn tokenize_special_parameters() -> Result<()> {
2623 assert_ron_snapshot!(test_tokenizer("$$")?);
2624 assert_ron_snapshot!(test_tokenizer("$@")?);
2625 assert_ron_snapshot!(test_tokenizer("$!")?);
2626 assert_ron_snapshot!(test_tokenizer("$?")?);
2627 assert_ron_snapshot!(test_tokenizer("$*")?);
2628 Ok(())
2629 }
2630
2631 #[test]
2632 fn tokenize_unbraced_parameter_expansion() -> Result<()> {
2633 assert_ron_snapshot!(test_tokenizer("$x")?);
2634 assert_ron_snapshot!(test_tokenizer("a$x")?);
2635 Ok(())
2636 }
2637
2638 #[test]
2639 fn tokenize_unterminated_parameter_expansion() {
2640 assert_matches!(
2641 tokenize_str("${x"),
2642 Err(TokenizerError::UnterminatedVariable)
2643 );
2644 }
2645
2646 #[test]
2647 fn tokenize_braced_parameter_expansion() -> Result<()> {
2648 assert_ron_snapshot!(test_tokenizer("${x}")?);
2649 assert_ron_snapshot!(test_tokenizer("a${x}b")?);
2650 Ok(())
2651 }
2652
2653 #[test]
2654 fn tokenize_braced_parameter_expansion_with_escaping() -> Result<()> {
2655 assert_ron_snapshot!(test_tokenizer(r"a${x\}}b")?);
2656 Ok(())
2657 }
2658
2659 #[test]
2660 fn tokenize_whitespace() -> Result<()> {
2661 assert_ron_snapshot!(test_tokenizer("1 2 3")?);
2662 Ok(())
2663 }
2664
2665 #[test]
2666 fn tokenize_escaped_whitespace() -> Result<()> {
2667 assert_ron_snapshot!(test_tokenizer(r"1\ 2 3")?);
2668 Ok(())
2669 }
2670
2671 #[test]
2672 fn tokenize_single_quote() -> Result<()> {
2673 assert_ron_snapshot!(test_tokenizer(r"x'a b'y")?);
2674 Ok(())
2675 }
2676
2677 #[test]
2678 fn tokenize_double_quote() -> Result<()> {
2679 assert_ron_snapshot!(test_tokenizer(r#"x"a b"y"#)?);
2680 Ok(())
2681 }
2682
2683 #[test]
2684 fn tokenize_double_quoted_command_substitution() -> Result<()> {
2685 assert_ron_snapshot!(test_tokenizer(r#"x"$(echo hi)"y"#)?);
2686 Ok(())
2687 }
2688
2689 #[test]
2690 fn tokenize_double_quoted_arithmetic_expression() -> Result<()> {
2691 assert_ron_snapshot!(test_tokenizer(r#"x"$((1+2))"y"#)?);
2692 Ok(())
2693 }
2694
2695 #[test]
2696 fn test_quote_removal() {
2697 assert_eq!(unquote_str(r#""hello""#), "hello");
2698 assert_eq!(unquote_str(r"'hello'"), "hello");
2699 assert_eq!(unquote_str(r#""hel\"lo""#), r#"hel"lo"#);
2700 assert_eq!(unquote_str(r"hel\'lo"), r"hel'lo");
2701 assert_eq!(unquote_str(r"a\ b\\c"), r"a b\c");
2702
2703 assert_eq!(unquote_str(r#"a'b c'd"e f"g"#), "ab cde fg");
2706 assert_eq!(unquote_str(r#"'a"b'"c'd""#), r#"a"bc'd"#);
2707
2708 assert_eq!(unquote_str(r"'a\b'"), r"a\b");
2711 assert_eq!(unquote_str(r"'a\'b"), r"a\b");
2712
2713 assert_eq!(unquote_str(r#""a\$b\`c\"d\\e""#), r#"a$b`c"d\e"#);
2716 assert_eq!(unquote_str(r#""a\b\'c""#), r"a\b\'c");
2717 assert_eq!(unquote_str("\"a\\\nb\""), "a\nb");
2718 assert_eq!(unquote_str("a\\\nb"), "a\nb");
2719
2720 assert_eq!(unquote_str(r"a\"), "a");
2722 assert_eq!(unquote_str(r#""a\"#), r"a\");
2723 assert_eq!(unquote_str(r"'a\"), r"a\");
2724
2725 assert_eq!(unquote_str(r#""a b"#), "a b");
2727 assert_eq!(unquote_str(r#"$HOME'~'"$x""#), "$HOME~$x");
2728 assert_eq!(unquote_str(""), "");
2729 }
2730}