1use std::borrow::Cow;
2use std::sync::Arc;
3use utf8_chars::BufReadCharsExt;
4
5use crate::{SourcePosition, SourceSpan};
6
7#[derive(Clone, Debug)]
8pub(crate) enum TokenEndReason {
9 EndOfInput,
11 UnescapedNewLine,
13 SpecifiedTerminatingChar,
15 NonNewLineBlank,
17 HereDocumentBodyStart,
19 HereDocumentBodyEnd,
21 HereDocumentEndTag,
23 OperatorStart,
25 OperatorEnd,
27 Other,
29}
30
31pub type TokenLocation = SourceSpan;
33
34#[derive(Clone, Debug)]
36#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
37#[cfg_attr(
38 any(test, feature = "serde"),
39 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
40)]
41pub enum Token {
42 Operator(String, SourceSpan),
44 Word(String, SourceSpan),
46}
47
48impl Token {
49 pub fn to_str(&self) -> &str {
51 match self {
52 Self::Operator(s, _) => s,
53 Self::Word(s, _) => s,
54 }
55 }
56
57 pub const fn location(&self) -> &SourceSpan {
59 match self {
60 Self::Operator(_, l) => l,
61 Self::Word(_, l) => l,
62 }
63 }
64}
65
66#[cfg(feature = "diagnostics")]
67impl From<&Token> for miette::SourceSpan {
68 fn from(token: &Token) -> Self {
69 let start = token.location().start.as_ref();
70 Self::new(start.into(), token.location().length())
71 }
72}
73
74#[derive(Clone, Debug)]
76pub(crate) struct TokenizeResult {
77 pub reason: TokenEndReason,
79 pub token: Option<Token>,
81}
82
83#[derive(thiserror::Error, Debug)]
85pub enum TokenizerError {
86 #[error("unterminated escape sequence")]
88 UnterminatedEscapeSequence,
89
90 #[error("unterminated single quote at {0}")]
92 UnterminatedSingleQuote(SourcePosition),
93
94 #[error("unterminated ANSI C quote at {0}")]
96 UnterminatedAnsiCQuote(SourcePosition),
97
98 #[error("unterminated double quote at {0}")]
100 UnterminatedDoubleQuote(SourcePosition),
101
102 #[error("unterminated backquote near {0}")]
104 UnterminatedBackquote(SourcePosition),
105
106 #[error("unterminated extglob near {0}")]
109 UnterminatedExtendedGlob(SourcePosition),
110
111 #[error("unterminated variable expression")]
113 UnterminatedVariable,
114
115 #[error("unterminated command substitution")]
117 UnterminatedCommandSubstitution,
118
119 #[error("unterminated expansion")]
122 UnterminatedExpansion,
123
124 #[error("failed to decode UTF-8 characters")]
126 FailedDecoding,
127
128 #[error("missing here tag for here document body")]
130 MissingHereTagForDocumentBody,
131
132 #[error("missing here tag '{0}'")]
134 MissingHereTag(String),
135
136 #[error("unterminated here document sequence; tag(s) [{0}] found at: [{1}]")]
138 UnterminatedHereDocuments(String, String),
139
140 #[error("failed to read input")]
142 ReadError(#[from] std::io::Error),
143}
144
145impl TokenizerError {
146 pub const fn is_incomplete(&self) -> bool {
149 matches!(
150 self,
151 Self::UnterminatedEscapeSequence
152 | Self::UnterminatedAnsiCQuote(..)
153 | Self::UnterminatedSingleQuote(..)
154 | Self::UnterminatedDoubleQuote(..)
155 | Self::UnterminatedBackquote(..)
156 | Self::UnterminatedCommandSubstitution
157 | Self::UnterminatedExpansion
158 | Self::UnterminatedVariable
159 | Self::UnterminatedExtendedGlob(..)
160 | Self::UnterminatedHereDocuments(..)
161 )
162 }
163}
164
165#[derive(Debug)]
167pub(crate) struct Tokens<'a> {
168 pub tokens: &'a [Token],
170}
171
172#[derive(Clone, Debug)]
173enum QuoteMode {
174 None,
175 AnsiC(SourcePosition),
176 Single(SourcePosition),
177 Double(SourcePosition),
178}
179
180#[derive(Clone, Debug, Default)]
181enum HereState {
182 #[default]
184 None,
185 NextTokenIsHereTag { remove_tabs: bool },
187 CurrentTokenIsHereTag {
189 remove_tabs: bool,
190 operator_token_result: TokenizeResult,
191 },
192 NextLineIsHereDoc,
195 InHereDocs,
198}
199
200#[derive(Clone, Debug)]
201struct HereTag {
202 tag: String,
203 tag_was_escaped_or_quoted: bool,
204 remove_tabs: bool,
205 position: SourcePosition,
206 tokens: Vec<TokenizeResult>,
207 pending_tokens_after: Vec<TokenizeResult>,
208}
209
210#[derive(Clone, Debug)]
211struct CrossTokenParseState {
212 cursor: SourcePosition,
214 here_state: HereState,
216 current_here_tags: Vec<HereTag>,
218 queued_tokens: Vec<TokenizeResult>,
220 arithmetic_expansion: bool,
222}
223
224#[derive(Clone, Debug, Hash, Eq, PartialEq)]
226pub struct TokenizerOptions {
227 pub enable_extended_globbing: bool,
229 pub posix_mode: bool,
231 pub sh_mode: bool,
233}
234
235impl Default for TokenizerOptions {
236 fn default() -> Self {
237 Self {
238 enable_extended_globbing: true,
239 posix_mode: false,
240 sh_mode: false,
241 }
242 }
243}
244
245pub(crate) struct Tokenizer<'a, R: ?Sized + std::io::BufRead> {
247 char_reader: std::iter::Peekable<utf8_chars::Chars<'a, R>>,
248 cross_state: CrossTokenParseState,
249 options: TokenizerOptions,
250}
251
252#[derive(Clone, Debug)]
254struct TokenParseState {
255 pub start_position: SourcePosition,
256 pub token_so_far: String,
257 pub token_is_operator: bool,
258 pub in_escape: bool,
259 pub quote_mode: QuoteMode,
260}
261
262impl TokenParseState {
263 pub fn new(start_position: &SourcePosition) -> Self {
264 Self {
265 start_position: start_position.to_owned(),
266 token_so_far: String::new(),
267 token_is_operator: false,
268 in_escape: false,
269 quote_mode: QuoteMode::None,
270 }
271 }
272
273 pub fn pop(&mut self, end_position: &SourcePosition) -> Token {
274 let end = Arc::new(end_position.to_owned());
275 let token_location = SourceSpan {
276 start: Arc::new(std::mem::take(&mut self.start_position)),
277 end,
278 };
279
280 let token = if std::mem::take(&mut self.token_is_operator) {
281 Token::Operator(std::mem::take(&mut self.token_so_far), token_location)
282 } else {
283 Token::Word(std::mem::take(&mut self.token_so_far), token_location)
284 };
285
286 end_position.clone_into(&mut self.start_position);
287 self.in_escape = false;
288 self.quote_mode = QuoteMode::None;
289
290 token
291 }
292
293 pub const fn started_token(&self) -> bool {
294 !self.token_so_far.is_empty()
295 }
296
297 pub fn only_blanks_so_far(&self) -> bool {
303 !self.token_so_far.is_empty() && self.token_so_far.chars().all(is_blank)
304 }
305
306 pub fn append_char(&mut self, c: char) {
307 self.token_so_far.push(c);
308 }
309
310 pub fn append_str(&mut self, s: &str) {
311 self.token_so_far.push_str(s);
312 }
313
314 pub const fn unquoted(&self) -> bool {
315 !self.in_escape && matches!(self.quote_mode, QuoteMode::None)
316 }
317
318 pub fn current_token(&self) -> &str {
319 &self.token_so_far
320 }
321
322 pub fn is_specific_operator(&self, operator: &str) -> bool {
323 self.token_is_operator && self.current_token() == operator
324 }
325
326 pub const fn in_operator(&self) -> bool {
327 self.token_is_operator
328 }
329
330 fn is_newline(&self) -> bool {
331 self.token_so_far == "\n"
332 }
333
334 fn replace_with_here_doc(&mut self, s: String) {
335 self.token_so_far = s;
336 }
337
338 #[allow(clippy::too_many_lines)]
339 pub fn delimit_current_token(
340 &mut self,
341 reason: TokenEndReason,
342 cross_token_state: &mut CrossTokenParseState,
343 ) -> Result<Option<TokenizeResult>, TokenizerError> {
344 if !self.started_token() && !matches!(reason, TokenEndReason::HereDocumentBodyEnd) {
347 return Ok(Some(TokenizeResult {
348 reason,
349 token: None,
350 }));
351 }
352
353 let current_here_state = std::mem::take(&mut cross_token_state.here_state);
355 match current_here_state {
356 HereState::NextTokenIsHereTag { remove_tabs } => {
357 let operator_token_result = TokenizeResult {
360 reason,
361 token: Some(self.pop(&cross_token_state.cursor)),
362 };
363
364 cross_token_state.here_state = HereState::CurrentTokenIsHereTag {
365 remove_tabs,
366 operator_token_result,
367 };
368
369 return Ok(None);
370 }
371 HereState::CurrentTokenIsHereTag {
372 remove_tabs,
373 operator_token_result,
374 } => {
375 if self.is_newline() {
376 return Err(TokenizerError::MissingHereTag(
377 self.current_token().to_owned(),
378 ));
379 }
380
381 cross_token_state.here_state = HereState::NextLineIsHereDoc;
382
383 let tag = std::format!("{}\n", self.current_token().trim_ascii_start());
385 let tag_was_escaped_or_quoted = tag.contains(is_quoting_char);
386
387 let tag_token_result = TokenizeResult {
388 reason,
389 token: Some(self.pop(&cross_token_state.cursor)),
390 };
391
392 cross_token_state.current_here_tags.push(HereTag {
393 tag,
394 tag_was_escaped_or_quoted,
395 remove_tabs,
396 position: cross_token_state.cursor.clone(),
397 tokens: vec![operator_token_result, tag_token_result],
398 pending_tokens_after: vec![],
399 });
400
401 return Ok(None);
402 }
403 HereState::NextLineIsHereDoc => {
404 if self.is_newline() {
405 cross_token_state.here_state = HereState::InHereDocs;
406 } else {
407 cross_token_state.here_state = HereState::NextLineIsHereDoc;
408 }
409
410 if let Some(last_here_tag) = cross_token_state.current_here_tags.last_mut() {
411 let token = self.pop(&cross_token_state.cursor);
412 let result = TokenizeResult {
413 reason,
414 token: Some(token),
415 };
416
417 last_here_tag.pending_tokens_after.push(result);
418 } else {
419 return Err(TokenizerError::MissingHereTagForDocumentBody);
420 }
421
422 return Ok(None);
423 }
424 HereState::InHereDocs => {
425 let completed_here_tag = cross_token_state.current_here_tags.remove(0);
427
428 cross_token_state
430 .queued_tokens
431 .extend(completed_here_tag.tokens);
432
433 cross_token_state.queued_tokens.push(TokenizeResult {
435 reason: TokenEndReason::HereDocumentBodyStart,
436 token: None,
437 });
438
439 cross_token_state.queued_tokens.push(TokenizeResult {
441 reason,
442 token: Some(self.pop(&cross_token_state.cursor)),
443 });
444
445 let end_tag = if completed_here_tag.tag_was_escaped_or_quoted {
447 unquote_str(&completed_here_tag.tag)
448 } else {
449 completed_here_tag.tag
450 };
451 self.append_str(end_tag.trim_end_matches('\n'));
452 cross_token_state.queued_tokens.push(TokenizeResult {
453 reason: TokenEndReason::HereDocumentEndTag,
454 token: Some(self.pop(&cross_token_state.cursor)),
455 });
456
457 cross_token_state
460 .queued_tokens
461 .extend(completed_here_tag.pending_tokens_after);
462
463 if cross_token_state.current_here_tags.is_empty() {
464 cross_token_state.here_state = HereState::None;
465 } else {
466 cross_token_state.here_state = HereState::InHereDocs;
467 }
468
469 return Ok(None);
470 }
471 HereState::None => (),
472 }
473
474 let token = self.pop(&cross_token_state.cursor);
475 let result = TokenizeResult {
476 reason,
477 token: Some(token),
478 };
479
480 Ok(Some(result))
481 }
482}
483
484pub fn tokenize_str(input: &str) -> Result<Vec<Token>, TokenizerError> {
490 tokenize_str_with_options(input, &TokenizerOptions::default())
491}
492
493pub fn tokenize_str_with_options(
500 input: &str,
501 options: &TokenizerOptions,
502) -> Result<Vec<Token>, TokenizerError> {
503 uncached_tokenize_string(input, options)
504}
505
506#[cached::macros::cached(
507 name = "TOKENIZE_CACHE",
508 max_size = 64,
509 key = "(String, TokenizerOptions)",
510 convert = r#"{ (input.to_owned(), options.to_owned()) }"#
511)]
512fn uncached_tokenize_string(
513 input: &str,
514 options: &TokenizerOptions,
515) -> Result<Vec<Token>, TokenizerError> {
516 uncached_tokenize_str(input, options)
517}
518
519pub fn uncached_tokenize_str(
526 input: &str,
527 options: &TokenizerOptions,
528) -> Result<Vec<Token>, TokenizerError> {
529 let mut reader = std::io::BufReader::new(input.as_bytes());
530 let mut tokenizer = crate::tokenizer::Tokenizer::new(&mut reader, options);
531
532 let mut tokens = vec![];
533 loop {
534 match tokenizer.next_token()? {
535 TokenizeResult {
536 token: Some(token), ..
537 } => tokens.push(token),
538 TokenizeResult {
539 reason: TokenEndReason::EndOfInput,
540 ..
541 } => break,
542 _ => (),
543 }
544 }
545
546 Ok(tokens)
547}
548
549pub(crate) fn command_substitution_body<'a>(
561 input: &'a str,
562 options: &TokenizerOptions,
563) -> Result<&'a str, TokenizerError> {
564 let mut reader = input.as_bytes();
565 let mut tokenizer = Tokenizer::new(&mut reader, options);
566
567 let mut state = TokenParseState::new(&tokenizer.cross_state.cursor);
571 tokenizer.consume_nested_construct(&mut state, ')', "(", 1)?;
572
573 let body_len = input
576 .chars()
577 .take(tokenizer.cross_state.cursor.index - 1)
578 .map(char::len_utf8)
579 .sum();
580 Ok(input.split_at(body_len).0)
581}
582
583impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {
584 pub fn new(reader: &'a mut R, options: &TokenizerOptions) -> Self {
585 Tokenizer {
586 options: options.clone(),
587 char_reader: reader.chars().peekable(),
588 cross_state: CrossTokenParseState {
589 cursor: SourcePosition {
590 index: 0,
591 line: 1,
592 column: 1,
593 },
594 here_state: HereState::None,
595 current_here_tags: vec![],
596 queued_tokens: vec![],
597 arithmetic_expansion: false,
598 },
599 }
600 }
601
602 #[expect(clippy::unnecessary_wraps)]
603 pub fn current_location(&self) -> Option<SourcePosition> {
604 Some(self.cross_state.cursor.clone())
605 }
606
607 fn next_char(&mut self) -> Result<Option<char>, TokenizerError> {
608 let c = self
609 .char_reader
610 .next()
611 .transpose()
612 .map_err(TokenizerError::ReadError)?;
613
614 if let Some(ch) = c {
615 if ch == '\n' {
616 self.cross_state.cursor.line += 1;
617 self.cross_state.cursor.column = 1;
618 } else {
619 self.cross_state.cursor.column += 1;
620 }
621 self.cross_state.cursor.index += 1;
622 }
623
624 Ok(c)
625 }
626
627 fn consume_char(&mut self) -> Result<(), TokenizerError> {
628 let _ = self.next_char()?;
629 Ok(())
630 }
631
632 fn peek_char(&mut self) -> Result<Option<char>, TokenizerError> {
633 match self.char_reader.peek() {
634 Some(result) => match result {
635 Ok(c) => Ok(Some(*c)),
636 Err(_) => Err(TokenizerError::FailedDecoding),
637 },
638 None => Ok(None),
639 }
640 }
641
642 pub fn next_token(&mut self) -> Result<TokenizeResult, TokenizerError> {
643 self.next_token_until(None, false )
644 }
645
646 fn consume_nested_construct(
657 &mut self,
658 state: &mut TokenParseState,
659 terminating_char: char,
660 nesting_open: &str,
661 mut nesting_count: u32,
662 ) -> Result<(), TokenizerError> {
663 let mut pending_here_doc_tokens = vec![];
664 let mut drain_here_doc_tokens = false;
665
666 loop {
667 let cur_token = if drain_here_doc_tokens && !pending_here_doc_tokens.is_empty() {
668 if pending_here_doc_tokens.len() == 1 {
669 drain_here_doc_tokens = false;
670 }
671 pending_here_doc_tokens.remove(0)
672 } else {
673 let cur_token = self.next_token_until(Some(terminating_char), true)?;
674
675 if matches!(
676 cur_token.reason,
677 TokenEndReason::HereDocumentBodyStart
678 | TokenEndReason::HereDocumentBodyEnd
679 | TokenEndReason::HereDocumentEndTag
680 ) {
681 pending_here_doc_tokens.push(cur_token);
682 continue;
683 }
684 cur_token
685 };
686
687 if matches!(cur_token.reason, TokenEndReason::UnescapedNewLine)
688 && !pending_here_doc_tokens.is_empty()
689 {
690 pending_here_doc_tokens.push(cur_token);
691 drain_here_doc_tokens = true;
692 continue;
693 }
694
695 if let Some(cur_token_value) = cur_token.token {
696 state.append_str(cur_token_value.to_str());
697
698 if matches!(cur_token_value, Token::Operator(o, _) if o == nesting_open) {
699 nesting_count += 1;
700 }
701 }
702
703 match cur_token.reason {
704 TokenEndReason::HereDocumentBodyStart => {
705 state.append_char('\n');
706 }
707 TokenEndReason::NonNewLineBlank => state.append_char(' '),
708 TokenEndReason::SpecifiedTerminatingChar => {
709 nesting_count -= 1;
710 if nesting_count == 0 {
711 break;
712 }
713 state.append_char(self.next_char()?.unwrap());
714 }
715 TokenEndReason::EndOfInput => {
716 return Err(TokenizerError::UnterminatedExpansion);
717 }
718 _ => (),
719 }
720 }
721
722 state.append_char(self.next_char()?.unwrap());
723 Ok(())
724 }
725
726 #[allow(clippy::unwrap_in_result)]
729 #[expect(clippy::too_many_lines)]
730 fn consume_dollar_or_backquote(
731 &mut self,
732 state: &mut TokenParseState,
733 c: char,
734 ) -> Result<(), TokenizerError> {
735 if c == '$' {
736 self.consume_char()?;
738
739 let char_after_dollar_sign = self.peek_char()?;
741 match char_after_dollar_sign {
742 Some('(') => {
743 state.append_char('$');
745
746 state.append_char(self.next_char()?.unwrap());
748
749 let (initial_nesting, is_arithmetic) = if matches!(self.peek_char()?, Some('('))
752 {
753 state.append_char(self.next_char()?.unwrap());
755 (2, true)
756 } else {
757 (1, false)
758 };
759
760 if is_arithmetic {
761 self.cross_state.arithmetic_expansion = true;
762 }
763
764 self.consume_nested_construct(state, ')', "(", initial_nesting)?;
765
766 if is_arithmetic {
767 self.cross_state.arithmetic_expansion = false;
768 }
769 }
770
771 Some('[') => {
772 state.append_char('$');
774
775 state.append_char(self.next_char()?.unwrap());
777
778 self.cross_state.arithmetic_expansion = true;
781
782 self.consume_nested_construct(state, ']', "[", 1)?;
783
784 self.cross_state.arithmetic_expansion = false;
785 }
786
787 Some('{') => {
788 state.append_char('$');
790
791 state.append_char(self.next_char()?.unwrap());
793
794 let mut pending_here_doc_tokens = vec![];
795 let mut drain_here_doc_tokens = false;
796
797 loop {
798 let cur_token =
799 if drain_here_doc_tokens && !pending_here_doc_tokens.is_empty() {
800 if pending_here_doc_tokens.len() == 1 {
801 drain_here_doc_tokens = false;
802 }
803
804 pending_here_doc_tokens.remove(0)
805 } else {
806 let cur_token = self
807 .next_token_until(Some('}'), false )?;
808
809 if matches!(
813 cur_token.reason,
814 TokenEndReason::HereDocumentBodyStart
815 | TokenEndReason::HereDocumentBodyEnd
816 | TokenEndReason::HereDocumentEndTag
817 ) {
818 pending_here_doc_tokens.push(cur_token);
819 continue;
820 }
821
822 cur_token
823 };
824
825 if matches!(cur_token.reason, TokenEndReason::UnescapedNewLine)
826 && !pending_here_doc_tokens.is_empty()
827 {
828 pending_here_doc_tokens.push(cur_token);
829 drain_here_doc_tokens = true;
830 continue;
831 }
832
833 if let Some(cur_token_value) = cur_token.token {
834 state.append_str(cur_token_value.to_str());
835 }
836
837 match cur_token.reason {
838 TokenEndReason::HereDocumentBodyStart => {
839 state.append_char('\n');
840 }
841 TokenEndReason::NonNewLineBlank => state.append_char(' '),
842 TokenEndReason::SpecifiedTerminatingChar => {
843 state.append_char(self.next_char()?.unwrap());
846 break;
847 }
848 TokenEndReason::EndOfInput => {
849 return Err(TokenizerError::UnterminatedVariable);
850 }
851 _ => (),
852 }
853 }
854 }
855 _ => {
856 state.append_char('$');
859 }
860 }
861 } else {
862 let backquote_pos = self.cross_state.cursor.clone();
865 self.consume_char()?;
866
867 state.append_char(c);
869
870 let mut escaping_enabled = false;
872 let mut done = false;
873 while !done {
874 let next_char_in_backquote = self.next_char()?;
876 if let Some(cib) = next_char_in_backquote {
877 state.append_char(cib);
879
880 if !escaping_enabled && cib == '\\' {
882 escaping_enabled = true;
883 } else {
884 if !escaping_enabled && cib == '`' {
886 done = true;
887 }
888 escaping_enabled = false;
889 }
890 } else {
891 return Err(TokenizerError::UnterminatedBackquote(backquote_pos));
892 }
893 }
894 }
895
896 Ok(())
897 }
898
899 #[expect(clippy::cognitive_complexity)]
910 #[expect(clippy::if_same_then_else)]
911 #[expect(clippy::panic_in_result_fn)]
912 #[expect(clippy::too_many_lines)]
913 #[allow(clippy::unwrap_in_result)]
914 fn next_token_until(
915 &mut self,
916 terminating_char: Option<char>,
917 include_space: bool,
918 ) -> Result<TokenizeResult, TokenizerError> {
919 let mut state = TokenParseState::new(&self.cross_state.cursor);
920 let mut result: Option<TokenizeResult> = None;
921
922 while result.is_none() {
923 if !self.cross_state.queued_tokens.is_empty() {
926 return Ok(self.cross_state.queued_tokens.remove(0));
927 }
928
929 let next = self.peek_char()?;
930 let c = next.unwrap_or('\0');
931
932 if next.is_none() {
935 if state.in_escape {
938 return Err(TokenizerError::UnterminatedEscapeSequence);
939 }
940 match state.quote_mode {
941 QuoteMode::None => (),
942 QuoteMode::AnsiC(pos) => {
943 return Err(TokenizerError::UnterminatedAnsiCQuote(pos));
944 }
945 QuoteMode::Single(pos) => {
946 return Err(TokenizerError::UnterminatedSingleQuote(pos));
947 }
948 QuoteMode::Double(pos) => {
949 return Err(TokenizerError::UnterminatedDoubleQuote(pos));
950 }
951 }
952
953 if !matches!(self.cross_state.here_state, HereState::None) {
955 if self.remove_here_end_tag(&mut state, &mut result, false)? {
956 continue;
958 }
959
960 let tag_names = self
961 .cross_state
962 .current_here_tags
963 .iter()
964 .map(|tag| tag.tag.trim())
965 .collect::<Vec<_>>()
966 .join(", ");
967 let tag_positions = self
968 .cross_state
969 .current_here_tags
970 .iter()
971 .map(|tag| std::format!("{}", tag.position))
972 .collect::<Vec<_>>()
973 .join(", ");
974 return Err(TokenizerError::UnterminatedHereDocuments(
975 tag_names,
976 tag_positions,
977 ));
978 }
979
980 result = state
981 .delimit_current_token(TokenEndReason::EndOfInput, &mut self.cross_state)?;
982 } else if matches!(self.cross_state.here_state, HereState::InHereDocs) {
986 if !self.cross_state.current_here_tags.is_empty()
991 && self.cross_state.current_here_tags[0].remove_tabs
992 && (!state.started_token() || state.current_token().ends_with('\n'))
993 && c == '\t'
994 {
995 self.consume_char()?;
997 } else {
998 self.consume_char()?;
999 state.append_char(c);
1000
1001 if c == '\n' {
1003 self.remove_here_end_tag(&mut state, &mut result, true)?;
1004 }
1005 }
1006 } else if state.unquoted()
1013 && !(state.in_operator() && state.is_newline())
1014 && terminating_char == Some(c)
1015 {
1016 result = state.delimit_current_token(
1017 TokenEndReason::SpecifiedTerminatingChar,
1018 &mut self.cross_state,
1019 )?;
1020 } else if state.in_operator() {
1021 let mut hypothetical_token = state.current_token().to_owned();
1027 hypothetical_token.push(c);
1028
1029 if state.unquoted() && self.is_operator(hypothetical_token.as_ref()) {
1030 self.consume_char()?;
1031 state.append_char(c);
1032 } else {
1033 assert!(state.started_token());
1034
1035 if self.cross_state.arithmetic_expansion {
1040 if state.is_specific_operator(")") && c == ')' {
1048 self.cross_state.arithmetic_expansion = false;
1049 }
1050 } else if state.is_specific_operator("<<") {
1051 self.cross_state.here_state =
1052 HereState::NextTokenIsHereTag { remove_tabs: false };
1053 } else if state.is_specific_operator("<<-") {
1054 self.cross_state.here_state =
1055 HereState::NextTokenIsHereTag { remove_tabs: true };
1056 } else if state.is_specific_operator("(") && c == '(' {
1057 self.cross_state.arithmetic_expansion = true;
1058 }
1059
1060 let reason = if state.current_token() == "\n" {
1061 TokenEndReason::UnescapedNewLine
1062 } else {
1063 TokenEndReason::OperatorEnd
1064 };
1065
1066 result = state.delimit_current_token(reason, &mut self.cross_state)?;
1067 }
1068 } else if does_char_newly_affect_quoting(&state, c) {
1072 if c == '\\' {
1073 self.consume_char()?;
1075
1076 if matches!(self.peek_char()?, Some('\n')) {
1077 self.consume_char()?;
1079
1080 } else {
1082 state.in_escape = true;
1083 state.append_char(c);
1084 }
1085 } else if c == '\'' {
1086 if state.token_so_far.ends_with('$') {
1087 state.quote_mode = QuoteMode::AnsiC(self.cross_state.cursor.clone());
1088 } else {
1089 state.quote_mode = QuoteMode::Single(self.cross_state.cursor.clone());
1090 }
1091
1092 self.consume_char()?;
1093 state.append_char(c);
1094 } else if c == '\"' {
1095 state.quote_mode = QuoteMode::Double(self.cross_state.cursor.clone());
1096 self.consume_char()?;
1097 state.append_char(c);
1098 }
1099 }
1100 else if !state.in_escape
1103 && matches!(
1104 state.quote_mode,
1105 QuoteMode::Single(..) | QuoteMode::AnsiC(..)
1106 )
1107 && c == '\''
1108 {
1109 state.quote_mode = QuoteMode::None;
1110 self.consume_char()?;
1111 state.append_char(c);
1112 } else if !state.in_escape
1113 && matches!(state.quote_mode, QuoteMode::Double(..))
1114 && c == '\"'
1115 {
1116 state.quote_mode = QuoteMode::None;
1117 self.consume_char()?;
1118 state.append_char(c);
1119 }
1120 else if state.in_escape {
1124 state.in_escape = false;
1125 self.consume_char()?;
1126 state.append_char(c);
1127 } else if (state.unquoted()
1128 || (matches!(state.quote_mode, QuoteMode::Double(_)) && !state.in_escape))
1129 && (c == '$' || c == '`')
1130 {
1131 self.consume_dollar_or_backquote(&mut state, c)?;
1133 }
1134 else if c == '('
1140 && self.options.enable_extended_globbing
1141 && state.unquoted()
1142 && !state.in_operator()
1143 && state
1144 .current_token()
1145 .ends_with(|x| Self::can_start_extglob(x))
1146 {
1147 self.consume_char()?;
1149 state.append_char(c);
1150
1151 let mut paren_depth = 1;
1152 let mut quote: Option<(char, bool)> = None;
1155 let mut after_dollar = false;
1156
1157 while paren_depth > 0 {
1162 let Some(extglob_char) = self.peek_char()? else {
1163 return Err(TokenizerError::UnterminatedExtendedGlob(
1164 self.cross_state.cursor.clone(),
1165 ));
1166 };
1167 let was_after_dollar = std::mem::take(&mut after_dollar);
1168
1169 let starts_nested_construct = match quote {
1170 None => extglob_char == '`',
1171 Some(('"', _)) => matches!(extglob_char, '`' | '$'),
1172 Some(_) => false,
1173 };
1174 if starts_nested_construct {
1175 self.consume_dollar_or_backquote(&mut state, extglob_char)?;
1176 continue;
1177 }
1178
1179 self.consume_char()?;
1181 state.append_char(extglob_char);
1182
1183 match extglob_char {
1184 '\\' if quote.is_none_or(|(_, escapes)| escapes) => {
1187 if let Some(escaped_char) = self.next_char()? {
1188 state.append_char(escaped_char);
1189 }
1190 }
1191 c if quote.is_some_and(|(close, _)| close == c) => quote = None,
1192 _ if quote.is_some() => (),
1193 '\'' => quote = Some(('\'', was_after_dollar)),
1194 '"' => quote = Some(('"', true)),
1195 '$' => after_dollar = !was_after_dollar,
1197 '(' => paren_depth += 1,
1198 ')' => paren_depth -= 1,
1199 _ => (),
1200 }
1201 }
1202 } else if state.unquoted() && Self::can_start_operator(c) {
1206 if state.started_token() {
1207 result = state.delimit_current_token(
1208 TokenEndReason::OperatorStart,
1209 &mut self.cross_state,
1210 )?;
1211 } else {
1212 state.token_is_operator = true;
1213 self.consume_char()?;
1214 state.append_char(c);
1215 }
1216 } else if state.unquoted() && is_blank(c) {
1220 if state.started_token() {
1221 result = state.delimit_current_token(
1222 TokenEndReason::NonNewLineBlank,
1223 &mut self.cross_state,
1224 )?;
1225 } else if include_space {
1226 state.append_char(c);
1227 } else {
1228 state.start_position.column += 1;
1230 state.start_position.index += 1;
1231 }
1232
1233 self.consume_char()?;
1234 }
1235 else if !state.token_is_operator
1249 && (state.started_token() || matches!(terminating_char, Some('}')))
1250 && !(c == '#' && state.only_blanks_so_far())
1251 {
1252 self.consume_char()?;
1253 state.append_char(c);
1254 } else if c == '#' {
1255 self.consume_char()?;
1257
1258 let mut done = false;
1259 while !done {
1260 done = match self.peek_char()? {
1261 Some('\n') => true,
1262 None => true,
1263 _ => {
1264 self.consume_char()?;
1266 false
1267 }
1268 };
1269 }
1270 } else if state.started_token() {
1272 result =
1274 state.delimit_current_token(TokenEndReason::Other, &mut self.cross_state)?;
1275 } else {
1276 self.consume_char()?;
1279 state.append_char(c);
1280 }
1281 }
1282
1283 let result = result.unwrap();
1284
1285 Ok(result)
1286 }
1287
1288 fn remove_here_end_tag(
1289 &mut self,
1290 state: &mut TokenParseState,
1291 result: &mut Option<TokenizeResult>,
1292 ends_with_newline: bool,
1293 ) -> Result<bool, TokenizerError> {
1294 if self.cross_state.current_here_tags.is_empty() {
1296 return Ok(false);
1297 }
1298
1299 let next_here_tag = &self.cross_state.current_here_tags[0];
1300
1301 let tag_str: Cow<'_, str> = if next_here_tag.tag_was_escaped_or_quoted {
1302 unquote_str(next_here_tag.tag.as_str()).into()
1303 } else {
1304 next_here_tag.tag.as_str().into()
1305 };
1306
1307 let tag_str = if !ends_with_newline {
1308 tag_str
1309 .strip_suffix('\n')
1310 .unwrap_or_else(|| tag_str.as_ref())
1311 } else {
1312 tag_str.as_ref()
1313 };
1314
1315 if let Some(current_token_without_here_tag) = state.current_token().strip_suffix(tag_str) {
1316 if current_token_without_here_tag.is_empty()
1320 || current_token_without_here_tag.ends_with('\n')
1321 {
1322 state.replace_with_here_doc(current_token_without_here_tag.to_owned());
1323
1324 *result = state.delimit_current_token(
1326 TokenEndReason::HereDocumentBodyEnd,
1327 &mut self.cross_state,
1328 )?;
1329
1330 return Ok(true);
1331 }
1332 }
1333 Ok(false)
1334 }
1335
1336 const fn can_start_extglob(c: char) -> bool {
1337 matches!(c, '@' | '!' | '?' | '+' | '*')
1338 }
1339
1340 const fn can_start_operator(c: char) -> bool {
1341 matches!(c, '&' | '(' | ')' | ';' | '\n' | '|' | '<' | '>')
1342 }
1343
1344 fn is_operator(&self, s: &str) -> bool {
1345 if !self.options.sh_mode && matches!(s, "<<<" | "&>" | "&>>" | ";;&" | ";&" | "|&") {
1347 return true;
1348 }
1349
1350 matches!(
1351 s,
1352 "&" | "&&"
1353 | "("
1354 | ")"
1355 | ";"
1356 | ";;"
1357 | "\n"
1358 | "|"
1359 | "||"
1360 | "<"
1361 | ">"
1362 | ">|"
1363 | "<<"
1364 | ">>"
1365 | "<&"
1366 | ">&"
1367 | "<<-"
1368 | "<>"
1369 )
1370 }
1371}
1372
1373impl<R: ?Sized + std::io::BufRead> Iterator for Tokenizer<'_, R> {
1374 type Item = Result<TokenizeResult, TokenizerError>;
1375
1376 fn next(&mut self) -> Option<Self::Item> {
1377 match self.next_token() {
1378 #[expect(clippy::manual_map)]
1379 Ok(result) => match result.token {
1380 Some(_) => Some(Ok(result)),
1381 None => None,
1382 },
1383 Err(e) => Some(Err(e)),
1384 }
1385 }
1386}
1387
1388const fn is_blank(c: char) -> bool {
1389 c == ' ' || c == '\t'
1390}
1391
1392const fn does_char_newly_affect_quoting(state: &TokenParseState, c: char) -> bool {
1393 if state.in_escape {
1395 return false;
1396 }
1397
1398 match state.quote_mode {
1399 QuoteMode::Double(_) | QuoteMode::AnsiC(_) => {
1402 if c == '\\' {
1403 true
1405 } else {
1406 false
1407 }
1408 }
1409 QuoteMode::Single(_) => false,
1411 QuoteMode::None => is_quoting_char(c),
1414 }
1415}
1416
1417const fn is_quoting_char(c: char) -> bool {
1418 matches!(c, '\\' | '\'' | '\"')
1419}
1420
1421pub fn unquote_str(s: &str) -> String {
1427 let mut result = String::new();
1428
1429 let mut in_escape = false;
1430 for c in s.chars() {
1431 match c {
1432 c if in_escape => {
1433 result.push(c);
1434 in_escape = false;
1435 }
1436 '\\' => in_escape = true,
1437 c if is_quoting_char(c) => (),
1438 c => result.push(c),
1439 }
1440 }
1441
1442 result
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447
1448 use super::*;
1449 use anyhow::Result;
1450 use insta::assert_ron_snapshot;
1451 use pretty_assertions::{assert_eq, assert_matches};
1452
1453 #[derive(serde::Serialize, serde::Deserialize)]
1454 struct TokenizerResult<'a> {
1455 input: &'a str,
1456 result: Vec<Token>,
1457 }
1458
1459 fn test_tokenizer(input: &str) -> Result<TokenizerResult<'_>> {
1460 Ok(TokenizerResult {
1461 input,
1462 result: tokenize_str(input)?,
1463 })
1464 }
1465
1466 #[test]
1467 fn tokenize_empty() -> Result<()> {
1468 let tokens = tokenize_str("")?;
1469 assert_eq!(tokens.len(), 0);
1470 Ok(())
1471 }
1472
1473 #[test]
1474 fn tokenize_line_continuation() -> Result<()> {
1475 assert_ron_snapshot!(test_tokenizer(
1476 r"a\
1477bc"
1478 )?);
1479 Ok(())
1480 }
1481
1482 #[test]
1483 fn tokenize_operators() -> Result<()> {
1484 assert_ron_snapshot!(test_tokenizer("a>>b")?);
1485 Ok(())
1486 }
1487
1488 #[test]
1489 fn tokenize_comment() -> Result<()> {
1490 assert_ron_snapshot!(test_tokenizer(
1491 r"a #comment
1492"
1493 )?);
1494 Ok(())
1495 }
1496
1497 #[test]
1498 fn tokenize_comment_in_command_substitution() {
1499 for (prefix, reconstructed_blanks) in [
1509 ("", ""),
1510 (" ", " "),
1511 (" ", " "),
1512 (" ", " "),
1513 ("\t", "\t"),
1514 ("\t\t", "\t "),
1515 (" \t", " "),
1516 ("\t ", "\t "),
1517 ] {
1518 let input = format!("$({prefix}# it's a comment\n)\n");
1519 let tokens = tokenize_str(input.as_str()).unwrap();
1520 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
1521 assert_eq!(
1522 token_strs,
1523 [format!("$({reconstructed_blanks}\n)").as_str(), "\n"],
1524 "tokenizing {input:?}"
1525 );
1526 }
1527 }
1528
1529 #[test]
1530 fn tokenize_comment_at_eof() -> Result<()> {
1531 assert_ron_snapshot!(test_tokenizer(r"a #comment")?);
1532 Ok(())
1533 }
1534
1535 #[test]
1536 fn tokenize_empty_here_doc() -> Result<()> {
1537 assert_ron_snapshot!(test_tokenizer(
1538 r"cat <<HERE
1539HERE
1540"
1541 )?);
1542 Ok(())
1543 }
1544
1545 #[test]
1546 fn tokenize_here_doc() -> Result<()> {
1547 assert_ron_snapshot!(test_tokenizer(
1548 r"cat <<HERE
1549SOMETHING
1550HERE
1551echo after
1552"
1553 )?);
1554 assert_ron_snapshot!(test_tokenizer(
1555 r"cat <<HERE
1556SOMETHING
1557HERE
1558"
1559 )?);
1560 assert_ron_snapshot!(test_tokenizer(
1561 r"cat <<HERE
1562SOMETHING
1563HERE
1564
1565"
1566 )?);
1567 assert_ron_snapshot!(test_tokenizer(
1568 r"cat <<HERE
1569SOMETHING
1570HERE"
1571 )?);
1572 Ok(())
1573 }
1574
1575 #[test]
1576 fn tokenize_here_doc_with_tab_removal() -> Result<()> {
1577 assert_ron_snapshot!(test_tokenizer(
1578 r"cat <<-HERE
1579 SOMETHING
1580 HERE
1581"
1582 )?);
1583 Ok(())
1584 }
1585
1586 #[test]
1587 fn tokenize_here_doc_with_other_tokens() -> Result<()> {
1588 assert_ron_snapshot!(test_tokenizer(
1589 r"cat <<EOF | wc -l
1590A B C
15911 2 3
1592D E F
1593EOF
1594"
1595 )?);
1596 Ok(())
1597 }
1598
1599 #[test]
1600 fn tokenize_multiple_here_docs() -> Result<()> {
1601 assert_ron_snapshot!(test_tokenizer(
1602 r"cat <<HERE1 <<HERE2
1603SOMETHING
1604HERE1
1605OTHER
1606HERE2
1607echo after
1608"
1609 )?);
1610 Ok(())
1611 }
1612
1613 #[test]
1614 fn tokenize_unterminated_here_doc() {
1615 let result = tokenize_str(
1616 r"cat <<HERE
1617SOMETHING
1618",
1619 );
1620 assert!(result.is_err());
1621 }
1622
1623 #[test]
1624 fn tokenize_missing_here_tag() {
1625 let result = tokenize_str(
1626 r"cat <<
1627",
1628 );
1629 assert!(result.is_err());
1630 }
1631
1632 #[test]
1633 fn tokenize_here_doc_in_command_substitution() -> Result<()> {
1634 assert_ron_snapshot!(test_tokenizer(
1635 r"echo $(cat <<HERE
1636TEXT
1637HERE
1638)"
1639 )?);
1640 Ok(())
1641 }
1642
1643 #[test]
1644 fn tokenize_here_doc_in_double_quoted_command_substitution() -> Result<()> {
1645 assert_ron_snapshot!(test_tokenizer(
1646 r#"echo "$(cat <<HERE
1647TEXT
1648HERE
1649)""#
1650 )?);
1651 Ok(())
1652 }
1653
1654 #[test]
1655 fn tokenize_here_doc_in_double_quoted_command_substitution_with_space() -> Result<()> {
1656 assert_ron_snapshot!(test_tokenizer(
1657 r#"echo "$(cat << HERE
1658TEXT
1659HERE
1660)""#
1661 )?);
1662 Ok(())
1663 }
1664
1665 #[test]
1666 fn tokenize_complex_here_docs_in_command_substitution() -> Result<()> {
1667 assert_ron_snapshot!(test_tokenizer(
1668 r"echo $(cat <<HERE1 <<HERE2 | wc -l
1669TEXT
1670HERE1
1671OTHER
1672HERE2
1673)"
1674 )?);
1675 Ok(())
1676 }
1677
1678 #[test]
1679 fn tokenize_simple_backquote() -> Result<()> {
1680 assert_ron_snapshot!(test_tokenizer(r"echo `echo hi`")?);
1681 Ok(())
1682 }
1683
1684 #[test]
1685 fn tokenize_backquote_with_escape() -> Result<()> {
1686 assert_ron_snapshot!(test_tokenizer(r"echo `echo\`hi`")?);
1687 Ok(())
1688 }
1689
1690 #[test]
1691 fn tokenize_unterminated_backquote() {
1692 assert_matches!(
1693 tokenize_str("`"),
1694 Err(TokenizerError::UnterminatedBackquote(_))
1695 );
1696 }
1697
1698 #[test]
1699 fn tokenize_unterminated_command_substitution() {
1700 assert_matches!(
1703 tokenize_str("$("),
1704 Err(TokenizerError::UnterminatedExpansion)
1705 );
1706 }
1707
1708 #[test]
1709 fn command_substitution_body_stops_at_closing_paren() -> Result<()> {
1710 let options = TokenizerOptions::default();
1711 assert_eq!(
1712 command_substitution_body("echo hi) rest", &options)?,
1713 "echo hi"
1714 );
1715 assert_eq!(
1716 command_substitution_body(r#"echo ")" (a)) rest"#, &options)?,
1717 r#"echo ")" (a)"#
1718 );
1719 assert_eq!(
1720 command_substitution_body("cat <<'EOF'\n\"it's ) `\nEOF\n) rest", &options)?,
1721 "cat <<'EOF'\n\"it's ) `\nEOF\n"
1722 );
1723 assert_eq!(
1725 command_substitution_body("cat <<E\n)\nE\n) rest", &options)?,
1726 "cat <<E\n)\nE\n"
1727 );
1728 assert_eq!(
1729 command_substitution_body("cat <<E\n)\nE\necho after) rest", &options)?,
1730 "cat <<E\n)\nE\necho after"
1731 );
1732 assert_eq!(
1734 command_substitution_body(r#"printf "%s" @(")")) rest"#, &options)?,
1735 r#"printf "%s" @(")")"#
1736 );
1737 assert_eq!(
1739 command_substitution_body("echo “é”) ü", &options)?,
1740 "echo “é”"
1741 );
1742 Ok(())
1743 }
1744
1745 #[test]
1746 fn command_substitution_body_unterminated() {
1747 let options = TokenizerOptions::default();
1748 assert_matches!(
1749 command_substitution_body("echo hi", &options),
1750 Err(TokenizerError::UnterminatedExpansion)
1751 );
1752 assert_matches!(
1753 command_substitution_body("echo 'hi)", &options),
1754 Err(TokenizerError::UnterminatedSingleQuote(_))
1755 );
1756 }
1757
1758 #[test]
1759 fn tokenize_unterminated_arithmetic_expansion() {
1760 assert_matches!(
1761 tokenize_str("$(("),
1762 Err(TokenizerError::UnterminatedExpansion)
1763 );
1764 }
1765
1766 #[test]
1767 fn tokenize_unterminated_legacy_arithmetic_expansion() {
1768 assert_matches!(
1769 tokenize_str("$["),
1770 Err(TokenizerError::UnterminatedExpansion)
1771 );
1772 }
1773
1774 #[test]
1775 fn tokenize_command_substitution() -> Result<()> {
1776 assert_ron_snapshot!(test_tokenizer("a$(echo hi)b c")?);
1777 Ok(())
1778 }
1779
1780 #[test]
1781 fn tokenize_command_substitution_with_subshell() -> Result<()> {
1782 assert_ron_snapshot!(test_tokenizer("$( (:) )")?);
1783 Ok(())
1784 }
1785
1786 #[test]
1787 fn tokenize_command_substitution_containing_extglob() -> Result<()> {
1788 assert_ron_snapshot!(test_tokenizer("echo $(echo !(x))")?);
1789 Ok(())
1790 }
1791
1792 #[test]
1793 fn tokenize_extglob_with_quotes_and_escapes() -> Result<()> {
1794 for (input, expected) in [
1795 (r#"@(")") y"#, [r#"@(")")"#, "y"]),
1797 (r"@(a|')'|b)x y", [r"@(a|')'|b)x", "y"]),
1798 (r#"@("(") y"#, [r#"@("(")"#, "y"]),
1799 (r#"@("\")") y"#, [r#"@("\")")"#, "y"]),
1801 (r"@('\')x y", [r"@('\')x", "y"]),
1803 (r#"@(\") y"#, [r#"@(\")"#, "y"]),
1805 (r"@(\)) y", [r"@(\))", "y"]),
1806 (r"@($'\'')x y", [r"@($'\'')x", "y"]),
1808 (r"@($'a\')'|b) y", [r"@($'a\')'|b)", "y"]),
1809 (r"@(\$'a\')x y", [r"@(\$'a\')x", "y"]),
1810 (r"@($$'a\')x y", [r"@($$'a\')x", "y"]),
1812 (r"@($$$'\'')x y", [r"@($$$'\'')x", "y"]),
1813 (r"@(}`'`)x y", [r"@(}`'`)x", "y"]),
1815 (r#"@(`echo \\"`)x y"#, [r#"@(`echo \\"`)x"#, "y"]),
1816 (r"@(`$'\'`)x y", [r"@(`$'\'`)x", "y"]),
1817 (r#"@(`echo ")"`)x y"#, [r#"@(`echo ")"`)x"#, "y"]),
1818 (r#"@("$(echo ")")")x y"#, [r#"@("$(echo ")")")x"#, "y"]),
1820 (r#"@("`echo "\)"`")x y"#, [r#"@("`echo "\)"`")x"#, "y"]),
1821 (r#"@("${u:-"}"}")x y"#, [r#"@("${u:-"}"}")x"#, "y"]),
1822 (r#"@($")")x y"#, [r#"@($")")x"#, "y"]),
1823 (r#"+(a|@(b|")")|'"') y"#, [r#"+(a|@(b|")")|'"')"#, "y"]),
1825 ] {
1826 let tokens = tokenize_str(input)?;
1827 let token_strs: Vec<_> = tokens.iter().map(Token::to_str).collect();
1828 assert_eq!(token_strs, expected, "input: {input}");
1829 }
1830 Ok(())
1831 }
1832
1833 #[test]
1834 fn tokenize_unterminated_construct_in_extglob() {
1835 for input in [r#"@("$(\$"\))x"#, r#"@($"$(\)")x"#, r"@(`)x", r#"@("`)")x"#] {
1837 assert!(tokenize_str(input).is_err(), "input: {input}");
1838 }
1839 }
1840
1841 #[test]
1842 fn tokenize_unterminated_extglob() {
1843 for input in [r"@(a", r#"@(")""#, r"@(')'", r"@(\)"] {
1844 assert_matches!(
1845 tokenize_str(input),
1846 Err(TokenizerError::UnterminatedExtendedGlob(_)),
1847 "input: {input}"
1848 );
1849 }
1850 }
1851
1852 #[test]
1853 fn tokenize_arithmetic_expression() -> Result<()> {
1854 assert_ron_snapshot!(test_tokenizer("a$((1+2))b c")?);
1855 Ok(())
1856 }
1857
1858 #[test]
1859 fn tokenize_arithmetic_expression_with_space() -> Result<()> {
1860 assert_ron_snapshot!(test_tokenizer("$(( 1 ))")?);
1863 Ok(())
1864 }
1865 #[test]
1866 fn tokenize_arithmetic_expression_with_parens() -> Result<()> {
1867 assert_ron_snapshot!(test_tokenizer("$(( (0) ))")?);
1868 Ok(())
1869 }
1870
1871 #[test]
1872 fn tokenize_special_parameters() -> Result<()> {
1873 assert_ron_snapshot!(test_tokenizer("$$")?);
1874 assert_ron_snapshot!(test_tokenizer("$@")?);
1875 assert_ron_snapshot!(test_tokenizer("$!")?);
1876 assert_ron_snapshot!(test_tokenizer("$?")?);
1877 assert_ron_snapshot!(test_tokenizer("$*")?);
1878 Ok(())
1879 }
1880
1881 #[test]
1882 fn tokenize_unbraced_parameter_expansion() -> Result<()> {
1883 assert_ron_snapshot!(test_tokenizer("$x")?);
1884 assert_ron_snapshot!(test_tokenizer("a$x")?);
1885 Ok(())
1886 }
1887
1888 #[test]
1889 fn tokenize_unterminated_parameter_expansion() {
1890 assert_matches!(
1891 tokenize_str("${x"),
1892 Err(TokenizerError::UnterminatedVariable)
1893 );
1894 }
1895
1896 #[test]
1897 fn tokenize_braced_parameter_expansion() -> Result<()> {
1898 assert_ron_snapshot!(test_tokenizer("${x}")?);
1899 assert_ron_snapshot!(test_tokenizer("a${x}b")?);
1900 Ok(())
1901 }
1902
1903 #[test]
1904 fn tokenize_braced_parameter_expansion_with_escaping() -> Result<()> {
1905 assert_ron_snapshot!(test_tokenizer(r"a${x\}}b")?);
1906 Ok(())
1907 }
1908
1909 #[test]
1910 fn tokenize_whitespace() -> Result<()> {
1911 assert_ron_snapshot!(test_tokenizer("1 2 3")?);
1912 Ok(())
1913 }
1914
1915 #[test]
1916 fn tokenize_escaped_whitespace() -> Result<()> {
1917 assert_ron_snapshot!(test_tokenizer(r"1\ 2 3")?);
1918 Ok(())
1919 }
1920
1921 #[test]
1922 fn tokenize_single_quote() -> Result<()> {
1923 assert_ron_snapshot!(test_tokenizer(r"x'a b'y")?);
1924 Ok(())
1925 }
1926
1927 #[test]
1928 fn tokenize_double_quote() -> Result<()> {
1929 assert_ron_snapshot!(test_tokenizer(r#"x"a b"y"#)?);
1930 Ok(())
1931 }
1932
1933 #[test]
1934 fn tokenize_double_quoted_command_substitution() -> Result<()> {
1935 assert_ron_snapshot!(test_tokenizer(r#"x"$(echo hi)"y"#)?);
1936 Ok(())
1937 }
1938
1939 #[test]
1940 fn tokenize_double_quoted_arithmetic_expression() -> Result<()> {
1941 assert_ron_snapshot!(test_tokenizer(r#"x"$((1+2))"y"#)?);
1942 Ok(())
1943 }
1944
1945 #[test]
1946 fn test_quote_removal() {
1947 assert_eq!(unquote_str(r#""hello""#), "hello");
1948 assert_eq!(unquote_str(r"'hello'"), "hello");
1949 assert_eq!(unquote_str(r#""hel\"lo""#), r#"hel"lo"#);
1950 assert_eq!(unquote_str(r"'hel\'lo'"), r"hel'lo");
1951 }
1952}