1use logos::{Logos, Span};
17use std::fmt;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::time::{SystemTime, UNIX_EPOCH};
20
21static MARKER_COUNTER: AtomicU64 = AtomicU64::new(0);
23
24const MAX_PAREN_DEPTH: usize = 256;
27
28#[derive(Debug, Clone)]
32struct SpanReplacement {
33 preprocessed_pos: usize,
35 marker_len: usize,
37 original_len: usize,
39}
40
41fn correct_span(span: Span, replacements: &[SpanReplacement]) -> Span {
43 let mut start_adjustment: isize = 0;
44 let mut end_adjustment: isize = 0;
45
46 for r in replacements {
47 let delta = r.original_len as isize - r.marker_len as isize;
49
50 if span.start > r.preprocessed_pos + r.marker_len {
52 start_adjustment += delta;
53 } else if span.start > r.preprocessed_pos {
54 start_adjustment += delta;
57 }
58
59 if span.end > r.preprocessed_pos + r.marker_len {
61 end_adjustment += delta;
62 } else if span.end > r.preprocessed_pos {
63 end_adjustment += delta;
65 }
66 }
67
68 let new_start = (span.start as isize + start_adjustment).max(0) as usize;
69 let new_end = (span.end as isize + end_adjustment).max(new_start as isize) as usize;
70 new_start..new_end
71}
72
73fn unique_marker_id() -> String {
76 let timestamp = SystemTime::now()
77 .duration_since(UNIX_EPOCH)
78 .map(|d| d.as_nanos())
79 .unwrap_or(0);
80 let counter = MARKER_COUNTER.fetch_add(1, Ordering::Relaxed);
81 #[cfg(target_os = "wasi")]
82 let pid = 0u32;
83 #[cfg(not(target_os = "wasi"))]
84 let pid = std::process::id();
85 format!("{:x}_{:x}_{:x}", timestamp, counter, pid)
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct Spanned<T> {
91 pub token: T,
92 pub span: Span,
93}
94
95impl<T> Spanned<T> {
96 pub fn new(token: T, span: Span) -> Self {
97 Self { token, span }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Default)]
103pub enum LexerError {
104 #[default]
105 UnexpectedCharacter,
106 UnterminatedString,
107 UnterminatedVarRef,
108 InvalidEscape,
109 InvalidNumber,
110 AmbiguousBoolean(String),
111 AmbiguousBooleanLike(String),
112 InvalidFloatNoLeading,
113 InvalidFloatNoTrailing,
114 NestingTooDeep,
116 UnterminatedHeredoc { delimiter: String },
120 BackticksNotSupported,
125}
126
127impl fmt::Display for LexerError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 LexerError::UnexpectedCharacter => write!(f, "unexpected character"),
131 LexerError::UnterminatedString => write!(f, "unterminated string"),
132 LexerError::UnterminatedVarRef => write!(f, "unterminated variable reference"),
133 LexerError::InvalidEscape => write!(f, "invalid escape sequence"),
134 LexerError::InvalidNumber => write!(f, "invalid number"),
135 LexerError::AmbiguousBoolean(s) => {
136 write!(f, "ambiguous boolean, use lowercase '{}'", s.to_lowercase())
137 }
138 LexerError::AmbiguousBooleanLike(s) => {
139 let suggest = if s.eq_ignore_ascii_case("yes") { "true" } else { "false" };
140 write!(f, "ambiguous boolean-like '{}', use '{}' or '\"{}\"'", s, suggest, s)
141 }
142 LexerError::InvalidFloatNoLeading => write!(f, "float must have leading digit"),
143 LexerError::InvalidFloatNoTrailing => write!(f, "float must have trailing digit"),
144 LexerError::NestingTooDeep => write!(f, "nesting depth exceeded (max {})", MAX_PAREN_DEPTH),
145 LexerError::UnterminatedHeredoc { delimiter } => {
146 write!(f, "unterminated heredoc, expected closing delimiter `{}` on its own line", delimiter)
147 }
148 LexerError::BackticksNotSupported => {
149 write!(f, "backticks are not supported in kaish; use $(cmd) instead")
150 }
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq)]
178pub struct HereDocData {
179 pub content: String,
180 pub literal: bool,
181 pub strip_tabs: bool,
182 pub body_start_offset: usize,
183}
184
185#[derive(Logos, Debug, Clone, PartialEq)]
186#[logos(error = LexerError)]
187#[logos(skip r"[ \t]+")]
188pub enum Token {
189 #[token("set")]
193 Set,
194
195 #[token("local")]
196 Local,
197
198 #[token("if")]
199 If,
200
201 #[token("then")]
202 Then,
203
204 #[token("else")]
205 Else,
206
207 #[token("elif")]
208 Elif,
209
210 #[token("fi")]
211 Fi,
212
213 #[token("for")]
214 For,
215
216 #[token("while")]
217 While,
218
219 #[token("in")]
220 In,
221
222 #[token("do")]
223 Do,
224
225 #[token("done")]
226 Done,
227
228 #[token("case")]
229 Case,
230
231 #[token("esac")]
232 Esac,
233
234 #[token("function")]
235 Function,
236
237 #[token("break")]
238 Break,
239
240 #[token("continue")]
241 Continue,
242
243 #[token("return")]
244 Return,
245
246 #[token("exit")]
247 Exit,
248
249 #[token("true")]
250 True,
251
252 #[token("false")]
253 False,
254
255 #[token("string")]
259 TypeString,
260
261 #[token("int")]
262 TypeInt,
263
264 #[token("float")]
265 TypeFloat,
266
267 #[token("bool")]
268 TypeBool,
269
270 #[token("&&")]
274 And,
275
276 #[token("||")]
277 Or,
278
279 #[token("==")]
280 EqEq,
281
282 #[token("!=")]
283 NotEq,
284
285 #[token("=~")]
286 Match,
287
288 #[token("!~")]
289 NotMatch,
290
291 #[token(">=")]
292 GtEq,
293
294 #[token("<=")]
295 LtEq,
296
297 #[token(">>")]
298 GtGt,
299
300 #[token("2>&1")]
301 StderrToStdout,
302
303 #[token("1>&2")]
304 StdoutToStderr,
305
306 #[token(">&2")]
307 StdoutToStderr2,
308
309 #[token("2>")]
310 Stderr,
311
312 #[token("&>")]
313 Both,
314
315 #[token("<<<")]
316 HereString,
317
318 #[token("<<")]
319 HereDocStart,
320
321 #[token(";;")]
322 DoubleSemi,
323
324 #[token("=")]
328 Eq,
329
330 #[token("|")]
331 Pipe,
332
333 #[token("&")]
334 Amp,
335
336 #[token(">")]
337 Gt,
338
339 #[token("<")]
340 Lt,
341
342 #[token(";")]
343 Semi,
344
345 #[token(":")]
346 Colon,
347
348 #[token(",")]
349 Comma,
350
351 #[token("..")]
352 DotDot,
353
354 #[token(".")]
355 Dot,
356
357 #[regex(r"~[a-zA-Z0-9_./+-]+", lex_tilde_path, priority = 3)]
359 TildePath(String),
360
361 #[token("~")]
363 Tilde,
364
365 #[regex(r"\.\./[a-zA-Z0-9_./-]+", lex_relative_path, priority = 3)]
371 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.-]*/[a-zA-Z0-9_./-]*", lex_relative_path, priority = 3)]
372 RelativePath(String),
373
374 #[regex(r"\./[a-zA-Z0-9_./-]+", lex_dot_slash_path, priority = 3)]
376 DotSlashPath(String),
377
378 #[regex(r"\.[a-zA-Z_][a-zA-Z0-9_.-]*", lex_dotted_ident, priority = 3)]
384 DottedIdent(String),
385
386 #[token("{")]
387 LBrace,
388
389 #[token("}")]
390 RBrace,
391
392 #[token("[")]
393 LBracket,
394
395 #[token("]")]
396 RBracket,
397
398 #[token("(")]
399 LParen,
400
401 #[token(")")]
402 RParen,
403
404 #[token("*")]
405 Star,
406
407 #[token("!")]
408 Bang,
409
410 #[token("?")]
411 Question,
412
413 GlobWord(String),
416
417 Arithmetic(String),
424
425 #[token("$(")]
427 CmdSubstStart,
428
429 #[regex(r"--[a-zA-Z][a-zA-Z0-9-]*", lex_long_flag, priority = 3)]
435 LongFlag(String),
436
437 #[regex(r"-[a-zA-Z][a-zA-Z0-9-]*", lex_short_flag, priority = 3)]
444 ShortFlag(String),
445
446 #[regex(r"\+[a-zA-Z][a-zA-Z0-9]*", lex_plus_flag, priority = 3)]
448 PlusFlag(String),
449
450 #[token("--")]
452 DoubleDash,
453
454 #[regex(r"\+[^a-zA-Z\s][^\s]*", lex_plus_bare, priority = 2)]
457 PlusBare(String),
458
459 #[regex(r"-[^a-zA-Z0-9\s\-][^\s]*", lex_minus_bare, priority = 1)]
463 MinusBare(String),
464
465 #[regex(r"%[0-9]+", lex_job_spec)]
469 JobSpec(String),
470
471 #[token("-")]
475 MinusAlone,
476
477 #[regex(r#""([^"\\]|\\.)*""#, lex_string)]
483 String(String),
484
485 #[regex(r"'[^']*'", lex_single_string)]
487 SingleString(String),
488
489 #[regex(r"\$\{[^}]+\}", lex_varref)]
491 VarRef(String),
492
493 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", lex_simple_varref)]
495 SimpleVarRef(String),
496
497 #[regex(r"\$[0-9]", lex_positional)]
499 Positional(usize),
500
501 #[token("$@")]
503 AllArgs,
504
505 #[token("$#")]
507 ArgCount,
508
509 #[token("$?")]
511 LastExitCode,
512
513 #[token("$$")]
515 CurrentPid,
516
517 #[regex(r"\$\{#[a-zA-Z_][a-zA-Z0-9_]*\}", lex_var_length)]
519 VarLength(String),
520
521 HereDoc(HereDocData),
524
525 #[regex(r"-?[0-9]+", lex_int, priority = 2)]
527 Int(i64),
528
529 #[regex(r"-?[0-9]+\.[0-9]+", lex_float)]
531 Float(f64),
532
533 #[regex(r"[0-9]+[a-zA-Z_][a-zA-Z0-9_.-]*", lex_number_ident, priority = 3)]
542 NumberIdent(String),
543
544 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
546 InvalidFloatNoLeading,
547
548 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
551 InvalidFloatNoTrailing,
552
553 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
559 Path(String),
560
561 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.-]*", lex_ident)]
568 Ident(String),
569
570 #[regex(r"#[^\n\r]*", allow_greedy = true)]
576 Comment,
577
578 #[regex(r"\n|\r\n")]
580 Newline,
581
582 #[regex(r"\\[ \t]*(\n|\r\n)")]
584 LineContinuation,
585
586 #[token("`", reject_backtick)]
595 BacktickRejected,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
603pub enum TokenCategory {
604 Keyword,
606 Operator,
608 String,
610 Number,
612 Variable,
614 Comment,
616 Punctuation,
618 Command,
620 Path,
622 Flag,
624 Error,
626}
627
628impl Token {
629 pub fn category(&self) -> TokenCategory {
631 match self {
632 Token::If
634 | Token::Then
635 | Token::Else
636 | Token::Elif
637 | Token::Fi
638 | Token::For
639 | Token::In
640 | Token::Do
641 | Token::Done
642 | Token::While
643 | Token::Case
644 | Token::Esac
645 | Token::Function
646 | Token::Return
647 | Token::Break
648 | Token::Continue
649 | Token::Exit
650 | Token::Set
651 | Token::Local
652 | Token::True
653 | Token::False
654 | Token::TypeString
655 | Token::TypeInt
656 | Token::TypeFloat
657 | Token::TypeBool => TokenCategory::Keyword,
658
659 Token::Pipe
661 | Token::And
662 | Token::Or
663 | Token::Amp
664 | Token::Eq
665 | Token::EqEq
666 | Token::NotEq
667 | Token::Match
668 | Token::NotMatch
669 | Token::Lt
670 | Token::Gt
671 | Token::LtEq
672 | Token::GtEq
673 | Token::GtGt
674 | Token::Stderr
675 | Token::Both
676 | Token::HereDocStart
677 | Token::HereString
678 | Token::StderrToStdout
679 | Token::StdoutToStderr
680 | Token::StdoutToStderr2 => TokenCategory::Operator,
681
682 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
684
685 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
687
688 Token::VarRef(_)
690 | Token::SimpleVarRef(_)
691 | Token::Positional(_)
692 | Token::AllArgs
693 | Token::ArgCount
694 | Token::VarLength(_)
695 | Token::LastExitCode
696 | Token::CurrentPid => TokenCategory::Variable,
697
698 Token::LongFlag(_)
700 | Token::ShortFlag(_)
701 | Token::PlusFlag(_)
702 | Token::DoubleDash => TokenCategory::Flag,
703
704 Token::Semi
706 | Token::DoubleSemi
707 | Token::Colon
708 | Token::Comma
709 | Token::Dot
710 | Token::LParen
711 | Token::RParen
712 | Token::LBrace
713 | Token::RBrace
714 | Token::LBracket
715 | Token::RBracket
716 | Token::Bang
717 | Token::Question
718 | Token::Star
719 | Token::Newline
720 | Token::LineContinuation
721 | Token::CmdSubstStart => TokenCategory::Punctuation,
722
723 Token::GlobWord(_) => TokenCategory::Path,
725
726 Token::Comment => TokenCategory::Comment,
728
729 Token::Path(_)
731 | Token::TildePath(_)
732 | Token::RelativePath(_)
733 | Token::Tilde
734 | Token::DotDot
735 | Token::DotSlashPath(_) => TokenCategory::Path,
736
737 Token::Ident(_)
739 | Token::PlusBare(_)
740 | Token::MinusBare(_)
741 | Token::MinusAlone
742 | Token::NumberIdent(_)
743 | Token::DottedIdent(_)
744 | Token::JobSpec(_) => TokenCategory::Command,
745
746 Token::InvalidFloatNoLeading
748 | Token::InvalidFloatNoTrailing
749 | Token::BacktickRejected => TokenCategory::Error,
750 }
751 }
752}
753
754fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
756 parse_string_literal(lex.slice())
757}
758
759fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
761 let s = lex.slice();
762 s[1..s.len() - 1].to_string()
764}
765
766fn lex_varref(lex: &mut logos::Lexer<Token>) -> String {
768 lex.slice().to_string()
770}
771
772fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
774 lex.slice()[1..].to_string()
776}
777
778fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
780 lex.slice()[1..].parse().unwrap_or(0)
782}
783
784fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
786 let s = lex.slice();
788 s[3..s.len() - 1].to_string()
789}
790
791fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
793 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
794}
795
796fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
798 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
799}
800
801fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
805 lex.slice().to_string()
806}
807
808fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
810 lex.slice().to_string()
811}
812
813fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
816 Err(LexerError::InvalidFloatNoLeading)
817}
818
819fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
823 Err(LexerError::BackticksNotSupported)
824}
825
826fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
829 Err(LexerError::InvalidFloatNoTrailing)
830}
831
832fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
834 let s = lex.slice();
835
836 match s.to_lowercase().as_str() {
839 "true" | "false" if s != "true" && s != "false" => {
840 return Err(LexerError::AmbiguousBoolean(s.to_string()));
841 }
842 _ => {}
843 }
844
845 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
847 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
848 }
849
850 Ok(s.to_string())
851}
852
853fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
855 lex.slice()[2..].to_string()
857}
858
859fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
861 lex.slice()[1..].to_string()
863}
864
865fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
867 lex.slice()[1..].to_string()
869}
870
871fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
873 lex.slice().to_string()
874}
875
876fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
878 lex.slice().to_string()
879}
880
881fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
883 lex.slice().to_string()
884}
885
886fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
888 lex.slice().to_string()
889}
890
891fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
893 lex.slice().to_string()
894}
895
896fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
898 lex.slice().to_string()
899}
900
901fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
903 lex.slice().to_string()
904}
905
906impl fmt::Display for Token {
907 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
908 match self {
909 Token::Set => write!(f, "set"),
910 Token::Local => write!(f, "local"),
911 Token::If => write!(f, "if"),
912 Token::Then => write!(f, "then"),
913 Token::Else => write!(f, "else"),
914 Token::Elif => write!(f, "elif"),
915 Token::Fi => write!(f, "fi"),
916 Token::For => write!(f, "for"),
917 Token::While => write!(f, "while"),
918 Token::In => write!(f, "in"),
919 Token::Do => write!(f, "do"),
920 Token::Done => write!(f, "done"),
921 Token::Case => write!(f, "case"),
922 Token::Esac => write!(f, "esac"),
923 Token::Function => write!(f, "function"),
924 Token::Break => write!(f, "break"),
925 Token::Continue => write!(f, "continue"),
926 Token::Return => write!(f, "return"),
927 Token::Exit => write!(f, "exit"),
928 Token::True => write!(f, "true"),
929 Token::False => write!(f, "false"),
930 Token::TypeString => write!(f, "string"),
931 Token::TypeInt => write!(f, "int"),
932 Token::TypeFloat => write!(f, "float"),
933 Token::TypeBool => write!(f, "bool"),
934 Token::And => write!(f, "&&"),
935 Token::Or => write!(f, "||"),
936 Token::EqEq => write!(f, "=="),
937 Token::NotEq => write!(f, "!="),
938 Token::Match => write!(f, "=~"),
939 Token::NotMatch => write!(f, "!~"),
940 Token::GtEq => write!(f, ">="),
941 Token::LtEq => write!(f, "<="),
942 Token::GtGt => write!(f, ">>"),
943 Token::StderrToStdout => write!(f, "2>&1"),
944 Token::StdoutToStderr => write!(f, "1>&2"),
945 Token::StdoutToStderr2 => write!(f, ">&2"),
946 Token::Stderr => write!(f, "2>"),
947 Token::Both => write!(f, "&>"),
948 Token::HereDocStart => write!(f, "<<"),
949 Token::HereString => write!(f, "<<<"),
950 Token::DoubleSemi => write!(f, ";;"),
951 Token::Eq => write!(f, "="),
952 Token::Pipe => write!(f, "|"),
953 Token::Amp => write!(f, "&"),
954 Token::Gt => write!(f, ">"),
955 Token::Lt => write!(f, "<"),
956 Token::Semi => write!(f, ";"),
957 Token::Colon => write!(f, ":"),
958 Token::Comma => write!(f, ","),
959 Token::Dot => write!(f, "."),
960 Token::DotDot => write!(f, ".."),
961 Token::Tilde => write!(f, "~"),
962 Token::TildePath(s) => write!(f, "{}", s),
963 Token::RelativePath(s) => write!(f, "{}", s),
964 Token::DotSlashPath(s) => write!(f, "{}", s),
965 Token::LBrace => write!(f, "{{"),
966 Token::RBrace => write!(f, "}}"),
967 Token::LBracket => write!(f, "["),
968 Token::RBracket => write!(f, "]"),
969 Token::LParen => write!(f, "("),
970 Token::RParen => write!(f, ")"),
971 Token::Star => write!(f, "*"),
972 Token::Bang => write!(f, "!"),
973 Token::Question => write!(f, "?"),
974 Token::GlobWord(s) => write!(f, "GLOB({})", s),
975 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
976 Token::CmdSubstStart => write!(f, "$("),
977 Token::LongFlag(s) => write!(f, "--{}", s),
978 Token::ShortFlag(s) => write!(f, "-{}", s),
979 Token::PlusFlag(s) => write!(f, "+{}", s),
980 Token::DoubleDash => write!(f, "--"),
981 Token::PlusBare(s) => write!(f, "{}", s),
982 Token::MinusBare(s) => write!(f, "{}", s),
983 Token::JobSpec(s) => write!(f, "{}", s),
984 Token::MinusAlone => write!(f, "-"),
985 Token::String(s) => write!(f, "STRING({:?})", s),
986 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
987 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
988 Token::VarRef(v) => write!(f, "VARREF({})", v),
989 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
990 Token::Positional(n) => write!(f, "${}", n),
991 Token::AllArgs => write!(f, "$@"),
992 Token::ArgCount => write!(f, "$#"),
993 Token::LastExitCode => write!(f, "$?"),
994 Token::CurrentPid => write!(f, "$$"),
995 Token::VarLength(v) => write!(f, "${{#{}}}", v),
996 Token::Int(n) => write!(f, "INT({})", n),
997 Token::Float(n) => write!(f, "FLOAT({})", n),
998 Token::Path(s) => write!(f, "PATH({})", s),
999 Token::Ident(s) => write!(f, "IDENT({})", s),
1000 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1001 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1002 Token::Comment => write!(f, "COMMENT"),
1003 Token::Newline => write!(f, "NEWLINE"),
1004 Token::LineContinuation => write!(f, "LINECONT"),
1005 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1007 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1008 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1009 }
1010 }
1011}
1012
1013impl Token {
1014 pub fn is_keyword(&self) -> bool {
1019 matches!(
1020 self,
1021 Token::Set
1022 | Token::Local
1023 | Token::If
1024 | Token::Then
1025 | Token::Else
1026 | Token::Elif
1027 | Token::Fi
1028 | Token::For
1029 | Token::In
1030 | Token::Do
1031 | Token::Done
1032 | Token::While
1033 | Token::Case
1034 | Token::Esac
1035 | Token::Function
1036 | Token::Return
1037 | Token::Break
1038 | Token::Continue
1039 | Token::Exit
1040 | Token::True
1041 | Token::False
1042 )
1043 }
1044
1045 pub fn is_type(&self) -> bool {
1047 matches!(
1048 self,
1049 Token::TypeString
1050 | Token::TypeInt
1051 | Token::TypeFloat
1052 | Token::TypeBool
1053 )
1054 }
1055
1056 pub fn starts_statement(&self) -> bool {
1059 matches!(
1060 self,
1061 Token::Set
1062 | Token::Local
1063 | Token::Function
1064 | Token::If
1065 | Token::For
1066 | Token::While
1067 | Token::Case
1068 | Token::Ident(_)
1069 | Token::LBracket
1070 )
1071 }
1072
1073 pub fn is_value(&self) -> bool {
1075 matches!(
1076 self,
1077 Token::String(_)
1078 | Token::SingleString(_)
1079 | Token::HereDoc(_)
1080 | Token::Arithmetic(_)
1081 | Token::Int(_)
1082 | Token::Float(_)
1083 | Token::True
1084 | Token::False
1085 | Token::VarRef(_)
1086 | Token::SimpleVarRef(_)
1087 | Token::CmdSubstStart
1088 | Token::Path(_)
1089 | Token::GlobWord(_)
1090 | Token::LastExitCode
1091 | Token::CurrentPid
1092 )
1093 }
1094}
1095
1096struct ArithmeticPreprocessResult {
1098 text: String,
1100 arithmetics: Vec<(String, String)>,
1102 replacements: Vec<SpanReplacement>,
1104}
1105
1106fn skip_command_substitution(
1115 chars: &[char],
1116 i: &mut usize,
1117 source_pos: &mut usize,
1118 result: &mut String,
1119) {
1120 result.push('$');
1122 result.push('(');
1123 *i += 2;
1124 *source_pos += 2;
1125
1126 let mut depth: usize = 1;
1127 let mut in_single_quote = false;
1128 let mut in_double_quote = false;
1129
1130 while *i < chars.len() && depth > 0 {
1131 let c = chars[*i];
1132
1133 if in_single_quote {
1134 result.push(c);
1135 *source_pos += c.len_utf8();
1136 *i += 1;
1137 if c == '\'' {
1138 in_single_quote = false;
1139 }
1140 continue;
1141 }
1142
1143 if in_double_quote {
1144 if c == '\\' && *i + 1 < chars.len() {
1145 let next = chars[*i + 1];
1146 if next == '"' || next == '\\' || next == '$' || next == '`' {
1147 result.push(c);
1148 result.push(next);
1149 *source_pos += c.len_utf8() + next.len_utf8();
1150 *i += 2;
1151 continue;
1152 }
1153 }
1154 if c == '"' {
1155 in_double_quote = false;
1156 }
1157 result.push(c);
1158 *source_pos += c.len_utf8();
1159 *i += 1;
1160 continue;
1161 }
1162
1163 match c {
1165 '\'' => {
1166 in_single_quote = true;
1167 result.push(c);
1168 *source_pos += c.len_utf8();
1169 *i += 1;
1170 }
1171 '"' => {
1172 in_double_quote = true;
1173 result.push(c);
1174 *source_pos += c.len_utf8();
1175 *i += 1;
1176 }
1177 '\\' if *i + 1 < chars.len() => {
1178 result.push(c);
1179 result.push(chars[*i + 1]);
1180 *source_pos += c.len_utf8() + chars[*i + 1].len_utf8();
1181 *i += 2;
1182 }
1183 '(' => {
1184 depth += 1;
1185 result.push(c);
1186 *source_pos += c.len_utf8();
1187 *i += 1;
1188 }
1189 ')' => {
1190 depth -= 1;
1191 result.push(c);
1192 *source_pos += c.len_utf8();
1193 *i += 1;
1194 }
1195 _ => {
1196 result.push(c);
1197 *source_pos += c.len_utf8();
1198 *i += 1;
1199 }
1200 }
1201 }
1202}
1203
1204fn preprocess_arithmetic(source: &str) -> Result<ArithmeticPreprocessResult, LexerError> {
1218 let mut result = String::with_capacity(source.len());
1219 let mut arithmetics: Vec<(String, String)> = Vec::new();
1220 let mut replacements: Vec<SpanReplacement> = Vec::new();
1221 let mut source_pos: usize = 0;
1222 let chars_vec: Vec<char> = source.chars().collect();
1223 let mut i = 0;
1224
1225 let mut in_double_quote = false;
1228
1229 while i < chars_vec.len() {
1230 let ch = chars_vec[i];
1231
1232 if !in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1234 result.push(ch);
1235 result.push(chars_vec[i + 1]);
1236 source_pos += ch.len_utf8() + chars_vec[i + 1].len_utf8();
1237 i += 2;
1238 continue;
1239 }
1240
1241 if ch == '\'' && !in_double_quote {
1243 result.push(ch);
1244 i += 1;
1245 source_pos += 1;
1246 while i < chars_vec.len() && chars_vec[i] != '\'' {
1247 result.push(chars_vec[i]);
1248 source_pos += chars_vec[i].len_utf8();
1249 i += 1;
1250 }
1251 if i < chars_vec.len() {
1252 result.push(chars_vec[i]); source_pos += 1;
1254 i += 1;
1255 }
1256 continue;
1257 }
1258
1259 if ch == '"' {
1261 in_double_quote = !in_double_quote;
1262 result.push(ch);
1263 i += 1;
1264 source_pos += 1;
1265 continue;
1266 }
1267
1268 if in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1270 let next = chars_vec[i + 1];
1271 if next == '"' || next == '\\' || next == '$' || next == '`' {
1272 result.push(ch);
1273 result.push(next);
1274 source_pos += ch.len_utf8() + next.len_utf8();
1275 i += 2;
1276 continue;
1277 }
1278 }
1279
1280 if ch == '#' && !in_double_quote {
1288 while i < chars_vec.len() && chars_vec[i] != '\n' && chars_vec[i] != '\r' {
1289 result.push(chars_vec[i]);
1290 source_pos += chars_vec[i].len_utf8();
1291 i += 1;
1292 }
1293 continue;
1294 }
1295
1296 if ch == '$' && i + 1 < chars_vec.len() && chars_vec[i + 1] == '('
1298 && !(i + 2 < chars_vec.len() && chars_vec[i + 2] == '(')
1299 {
1300 skip_command_substitution(&chars_vec, &mut i, &mut source_pos, &mut result);
1301 continue;
1302 }
1303
1304 if ch == '$' && i + 2 < chars_vec.len() && chars_vec[i + 1] == '(' && chars_vec[i + 2] == '(' {
1306 let arith_start_pos = result.len();
1307 let original_start = source_pos;
1308
1309 i += 3;
1311 source_pos += 3;
1312
1313 let mut expr = String::new();
1315 let mut paren_depth: usize = 0;
1316
1317 while i < chars_vec.len() {
1318 let c = chars_vec[i];
1319 match c {
1320 '(' => {
1321 paren_depth += 1;
1322 if paren_depth > MAX_PAREN_DEPTH {
1323 return Err(LexerError::NestingTooDeep);
1324 }
1325 expr.push('(');
1326 i += 1;
1327 source_pos += c.len_utf8();
1328 }
1329 ')' => {
1330 if paren_depth > 0 {
1331 paren_depth -= 1;
1332 expr.push(')');
1333 i += 1;
1334 source_pos += 1;
1335 } else if i + 1 < chars_vec.len() && chars_vec[i + 1] == ')' {
1336 i += 2;
1338 source_pos += 2;
1339 break;
1340 } else {
1341 expr.push(')');
1343 i += 1;
1344 source_pos += 1;
1345 }
1346 }
1347 _ => {
1348 expr.push(c);
1349 i += 1;
1350 source_pos += c.len_utf8();
1351 }
1352 }
1353 }
1354
1355 let original_len = source_pos - original_start;
1357
1358 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1360 let marker_len = marker.len();
1361
1362 replacements.push(SpanReplacement {
1364 preprocessed_pos: arith_start_pos,
1365 marker_len,
1366 original_len,
1367 });
1368
1369 arithmetics.push((marker.clone(), expr));
1370 result.push_str(&marker);
1371 } else {
1372 result.push(ch);
1373 i += 1;
1374 source_pos += ch.len_utf8();
1375 }
1376 }
1377
1378 Ok(ArithmeticPreprocessResult {
1379 text: result,
1380 arithmetics,
1381 replacements,
1382 })
1383}
1384
1385#[derive(Debug, Clone)]
1400struct HeredocReplacement {
1401 marker: String,
1402 body: String,
1403 literal: bool,
1404 strip_tabs: bool,
1405 body_start_offset: usize,
1406}
1407
1408fn preprocess_heredocs(source: &str) -> Result<(String, Vec<HeredocReplacement>), Spanned<LexerError>> {
1420 let mut result = String::with_capacity(source.len());
1421 let mut heredocs: Vec<HeredocReplacement> = Vec::new();
1422 let chars_vec: Vec<char> = source.chars().collect();
1423 let mut i = 0;
1424 let mut pos: usize = 0;
1428
1429 while i < chars_vec.len() {
1430 let ch = chars_vec[i];
1431
1432 if ch == '<'
1436 && chars_vec.get(i + 1) == Some(&'<')
1437 && chars_vec.get(i + 2) == Some(&'<')
1438 {
1439 result.push_str("<<<");
1440 i += 3;
1441 pos += 3;
1442 continue;
1443 }
1444
1445 if ch == '<' && chars_vec.get(i + 1) == Some(&'<') {
1447 let introducer_start = pos;
1450 i += 2; pos += 2;
1452
1453 let strip_tabs = chars_vec.get(i) == Some(&'-');
1455 if strip_tabs {
1456 i += 1;
1457 pos += 1;
1458 }
1459
1460 while let Some(&c) = chars_vec.get(i) {
1462 if c == ' ' || c == '\t' {
1463 i += 1;
1464 pos += 1;
1465 } else {
1466 break;
1467 }
1468 }
1469
1470 let mut delimiter = String::new();
1472 let quoted = chars_vec.get(i) == Some(&'\'') || chars_vec.get(i) == Some(&'"');
1473 let quote_char = if quoted {
1474 let q = chars_vec.get(i).copied();
1475 i += 1;
1476 pos += 1;
1477 q
1478 } else {
1479 None
1480 };
1481
1482 while let Some(&c) = chars_vec.get(i) {
1483 if quoted {
1484 if Some(c) == quote_char {
1485 i += 1; pos += 1;
1487 break;
1488 }
1489 } else if c.is_whitespace() || c == '\n' || c == '\r' {
1490 break;
1491 }
1492 delimiter.push(c);
1493 i += 1;
1494 pos += c.len_utf8();
1495 }
1496
1497 if delimiter.is_empty() {
1498 result.push_str("<<");
1500 if strip_tabs {
1501 result.push('-');
1502 }
1503 continue;
1504 }
1505
1506 let mut after_delimiter = String::new();
1509 while let Some(&c) = chars_vec.get(i) {
1510 if c == '\n' {
1511 i += 1;
1512 pos += 1;
1513 break;
1514 } else if c == '\r' {
1515 i += 1;
1516 pos += 1;
1517 if chars_vec.get(i) == Some(&'\n') {
1518 i += 1;
1519 pos += 1;
1520 }
1521 break;
1522 }
1523 after_delimiter.push(c);
1524 i += 1;
1525 pos += c.len_utf8();
1526 }
1527
1528 let body_start_offset = pos;
1534 let mut content = String::new();
1535 let mut current_line = String::new();
1536
1537 loop {
1538 let next = chars_vec.get(i).copied();
1539 match next {
1540 Some('\n') => {
1541 i += 1;
1542 pos += 1;
1543 let trimmed = if strip_tabs {
1545 current_line.trim_start_matches('\t')
1546 } else {
1547 ¤t_line
1548 };
1549 if trimmed == delimiter {
1550 break;
1552 }
1553 content.push_str(¤t_line);
1555 content.push('\n');
1556 current_line.clear();
1557 }
1558 Some('\r') => {
1559 i += 1;
1560 pos += 1;
1561 let crlf = chars_vec.get(i) == Some(&'\n');
1567 if crlf {
1568 i += 1;
1569 pos += 1;
1570 }
1571 let trimmed = if strip_tabs {
1572 current_line.trim_start_matches('\t')
1573 } else {
1574 ¤t_line
1575 };
1576 if trimmed == delimiter {
1577 break;
1578 }
1579 content.push_str(¤t_line);
1580 content.push_str(if crlf { "\r\n" } else { "\r" });
1581 current_line.clear();
1582 }
1583 Some(c) => {
1584 current_line.push(c);
1585 i += 1;
1586 pos += c.len_utf8();
1587 }
1588 None => {
1589 let trimmed = if strip_tabs {
1592 current_line.trim_start_matches('\t')
1593 } else {
1594 ¤t_line
1595 };
1596 if trimmed == delimiter {
1597 break;
1598 }
1599 let span_end = introducer_start
1604 + 2
1605 + if strip_tabs { 1 } else { 0 }
1606 + delimiter.len();
1607 return Err(Spanned::new(
1608 LexerError::UnterminatedHeredoc {
1609 delimiter: delimiter.clone(),
1610 },
1611 introducer_start..span_end,
1612 ));
1613 }
1614 }
1615 }
1616
1617 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1619 heredocs.push(HeredocReplacement {
1620 marker: marker.clone(),
1621 body: content,
1622 literal: quoted,
1623 strip_tabs,
1624 body_start_offset,
1625 });
1626
1627 result.push_str("<<");
1630 result.push_str(&marker);
1631 result.push_str(&after_delimiter);
1632 result.push('\n');
1633 } else {
1634 result.push(ch);
1635 i += 1;
1636 pos += ch.len_utf8();
1637 }
1638 }
1639
1640 Ok((result, heredocs))
1641}
1642
1643fn mergeable_text(token: &Token) -> Option<String> {
1648 match token {
1649 Token::Ident(s) => Some(s.clone()),
1650 Token::NumberIdent(s) => Some(s.clone()),
1651 Token::DottedIdent(s) => Some(s.clone()),
1652 Token::Colon => Some(":".to_string()),
1653 Token::Int(n) => Some(n.to_string()),
1654 Token::Path(p) => Some(p.clone()),
1655 Token::Float(f) => Some(f.to_string()),
1656 _ => None,
1657 }
1658}
1659
1660fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1669 if tokens.is_empty() {
1670 return tokens;
1671 }
1672
1673 let mut result = Vec::with_capacity(tokens.len());
1674 let mut run: Vec<&Spanned<Token>> = Vec::new();
1675
1676 for token in &tokens {
1677 if run.is_empty() {
1678 if mergeable_text(&token.token).is_some() {
1679 run.push(token);
1680 } else {
1681 result.push(token.clone());
1682 }
1683 continue;
1684 }
1685
1686 let Some(last) = run.last() else { unreachable!() };
1689 let adjacent = last.span.end == token.span.start;
1690
1691 if adjacent && mergeable_text(&token.token).is_some() {
1692 run.push(token);
1693 } else {
1694 flush_colon_run(&mut run, &mut result);
1695 if mergeable_text(&token.token).is_some() {
1696 run.push(token);
1697 } else {
1698 result.push(token.clone());
1699 }
1700 }
1701 }
1702
1703 flush_colon_run(&mut run, &mut result);
1704
1705 result
1706}
1707
1708fn flush_colon_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1710 if run.is_empty() {
1711 return;
1712 }
1713
1714 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
1715
1716 if run.len() >= 2 && has_colon {
1717 let text: String = run
1718 .iter()
1719 .filter_map(|t| mergeable_text(&t.token))
1720 .collect();
1721 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1723 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1724 result.push(Spanned::new(Token::Ident(text), start..end));
1725 } else {
1726 for t in run.iter() {
1727 result.push((*t).clone());
1728 }
1729 }
1730
1731 run.clear();
1732}
1733
1734fn glob_mergeable_text(token: &Token) -> Option<String> {
1739 match token {
1740 Token::Star => Some("*".to_string()),
1741 Token::Question => Some("?".to_string()),
1742 Token::Dot => Some(".".to_string()),
1743 Token::DotDot => Some("..".to_string()),
1744 Token::Ident(s) => Some(s.clone()),
1745 Token::NumberIdent(s) => Some(s.clone()),
1746 Token::DottedIdent(s) => Some(s.clone()),
1747 Token::Path(s) => Some(s.clone()),
1748 Token::Int(n) => Some(n.to_string()),
1749 Token::LBracket => Some("[".to_string()),
1750 Token::RBracket => Some("]".to_string()),
1751 Token::Bang => Some("!".to_string()),
1752 Token::DotSlashPath(s) => Some(s.clone()),
1753 Token::RelativePath(s) => Some(s.clone()),
1754 Token::TildePath(s) => Some(s.clone()),
1755 Token::Tilde => Some("~".to_string()),
1756 Token::LBrace => Some("{".to_string()),
1757 Token::RBrace => Some("}".to_string()),
1758 Token::Comma => Some(",".to_string()),
1759 _ => None,
1760 }
1761}
1762
1763fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1792 if tokens.len() < 2 {
1793 return tokens;
1794 }
1795
1796 let mut result = Vec::with_capacity(tokens.len());
1797 let mut i = 0;
1798
1799 while i < tokens.len() {
1800 let token = &tokens[i];
1801
1802 if let Token::ShortFlag(flag_name) = &token.token {
1804 let mut fused = flag_name.clone();
1806 let mut end_span = token.span.end;
1807 let mut j = i + 1;
1808
1809 while let Some(next) = tokens.get(j) {
1810 if next.span.start == end_span {
1811 if let Token::Colon = &next.token {
1812 fused.push(':');
1813 end_span = next.span.end;
1814 j += 1;
1815 continue;
1816 }
1817 }
1818 break;
1819 }
1820
1821 if j > i + 1 {
1822 let span = token.span.start..end_span;
1824 result.push(Spanned::new(Token::ShortFlag(fused), span));
1825 i = j;
1826 continue;
1827 }
1828 }
1829
1830 result.push(token.clone());
1831 i += 1;
1832 }
1833
1834 result
1835}
1836
1837fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1845 if tokens.is_empty() {
1846 return tokens;
1847 }
1848
1849 let mut result = Vec::with_capacity(tokens.len());
1850 let mut run: Vec<&Spanned<Token>> = Vec::new();
1851
1852 for token in &tokens {
1853 if run.is_empty() {
1854 if glob_mergeable_text(&token.token).is_some() {
1855 run.push(token);
1856 } else {
1857 result.push(token.clone());
1858 }
1859 continue;
1860 }
1861
1862 let Some(last) = run.last() else { unreachable!() };
1864 let adjacent = last.span.end == token.span.start;
1865
1866 if adjacent && glob_mergeable_text(&token.token).is_some() {
1867 run.push(token);
1868 } else {
1869 flush_glob_run(&mut run, &mut result);
1870 if glob_mergeable_text(&token.token).is_some() {
1871 run.push(token);
1872 } else {
1873 result.push(token.clone());
1874 }
1875 }
1876 }
1877
1878 flush_glob_run(&mut run, &mut result);
1879
1880 result
1881}
1882
1883fn flush_glob_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1885 if run.is_empty() {
1886 return;
1887 }
1888
1889 let has_glob = run.iter().any(|t| {
1890 matches!(t.token, Token::Star | Token::Question)
1891 }) || (run.iter().any(|t| matches!(t.token, Token::LBracket))
1892 && run.iter().any(|t| matches!(t.token, Token::RBracket)));
1893
1894 if run.len() >= 2 && has_glob {
1895 let text: String = run
1896 .iter()
1897 .filter_map(|t| glob_mergeable_text(&t.token))
1898 .collect();
1899 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1900 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1901 result.push(Spanned::new(Token::GlobWord(text), start..end));
1902 } else {
1903 for t in run.iter() {
1904 result.push((*t).clone());
1905 }
1906 }
1907
1908 run.clear();
1909}
1910
1911pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
1921 let arith_result = preprocess_arithmetic(source)
1923 .map_err(|e| vec![Spanned::new(e, 0..source.len())])?;
1924
1925 let span_replacements = arith_result.replacements;
1929 let (preprocessed, heredocs) = preprocess_heredocs(&arith_result.text)
1930 .map_err(|e| {
1931 let span = correct_span(e.span, &span_replacements);
1932 vec![Spanned::new(e.token, span)]
1933 })?;
1934
1935 let lexer = Token::lexer(&preprocessed);
1936 let mut tokens = Vec::new();
1937 let mut errors = Vec::new();
1938
1939 for (result, span) in lexer.spanned() {
1940 let corrected_span = correct_span(span, &span_replacements);
1942 match result {
1943 Ok(token) => {
1944 if !matches!(token, Token::Comment | Token::LineContinuation) {
1946 tokens.push(Spanned::new(token, corrected_span));
1947 }
1948 }
1949 Err(err) => {
1950 errors.push(Spanned::new(err, corrected_span));
1951 }
1952 }
1953 }
1954
1955 if !errors.is_empty() {
1956 return Err(errors);
1957 }
1958
1959 let mut final_tokens = Vec::with_capacity(tokens.len());
1961 let mut i = 0;
1962
1963 while i < tokens.len() {
1964 if let Token::Ident(ref name) = tokens[i].token
1966 && name.starts_with("__KAISH_ARITH_") && name.ends_with("__")
1967 && let Some((_, expr)) = arith_result.arithmetics.iter().find(|(marker, _)| marker == name) {
1968 final_tokens.push(Spanned::new(Token::Arithmetic(expr.clone()), tokens[i].span.clone()));
1969 i += 1;
1970 continue;
1971 }
1972
1973 if matches!(tokens[i].token, Token::HereDocStart) {
1975 if i + 1 < tokens.len()
1977 && let Token::Ident(ref name) = tokens[i + 1].token
1978 && name.starts_with("__KAISH_HEREDOC_") && name.ends_with("__") {
1979 if let Some(hd) = heredocs.iter().find(|h| h.marker == *name) {
1981 let mut content = hd.body.clone();
1993 for (marker, expr) in &arith_result.arithmetics {
1994 if content.contains(marker) {
1995 let replacement = if hd.literal {
1996 format!("$(({}))", expr)
1997 } else {
1998 format!("${{__ARITH:{}__}}", expr)
1999 };
2000 content = content.replace(marker, &replacement);
2001 }
2002 }
2003 final_tokens.push(Spanned::new(Token::HereDocStart, tokens[i].span.clone()));
2004 final_tokens.push(Spanned::new(
2005 Token::HereDoc(HereDocData {
2006 content,
2007 literal: hd.literal,
2008 strip_tabs: hd.strip_tabs,
2009 body_start_offset: hd.body_start_offset,
2010 }),
2011 tokens[i + 1].span.clone(),
2012 ));
2013 i += 2;
2014 continue;
2015 }
2016 }
2017 }
2018
2019 let token = if let Token::String(ref s) = tokens[i].token {
2021 let mut new_content = s.clone();
2023 for (marker, expr) in &arith_result.arithmetics {
2024 if new_content.contains(marker) {
2025 new_content = new_content.replace(marker, &format!("${{__ARITH:{}__}}", expr));
2028 }
2029 }
2030 if new_content != *s {
2031 Spanned::new(Token::String(new_content), tokens[i].span.clone())
2032 } else {
2033 tokens[i].clone()
2034 }
2035 } else {
2036 tokens[i].clone()
2037 };
2038 final_tokens.push(token);
2039 i += 1;
2040 }
2041
2042 Ok(merge_glob_adjacent(merge_colon_adjacent(
2043 merge_flag_metachar_adjacent(final_tokens),
2044 )))
2045}
2046
2047pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2051 let lexer = Token::lexer(source);
2052 let mut tokens = Vec::new();
2053 let mut errors = Vec::new();
2054
2055 for (result, span) in lexer.spanned() {
2056 match result {
2057 Ok(token) => {
2058 tokens.push(Spanned::new(token, span));
2059 }
2060 Err(err) => {
2061 errors.push(Spanned::new(err, span));
2062 }
2063 }
2064 }
2065
2066 if errors.is_empty() {
2067 Ok(tokens)
2068 } else {
2069 Err(errors)
2070 }
2071}
2072
2073pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2075 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2077 return Err(LexerError::UnterminatedString);
2078 }
2079
2080 let inner = &source[1..source.len() - 1];
2081 let mut result = String::with_capacity(inner.len());
2082 let mut chars = inner.chars().peekable();
2083
2084 while let Some(ch) = chars.next() {
2085 if ch == '\\' {
2086 match chars.next() {
2087 Some('n') => result.push('\n'),
2088 Some('t') => result.push('\t'),
2089 Some('r') => result.push('\r'),
2090 Some('\\') => result.push('\\'),
2091 Some('"') => result.push('"'),
2092 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2095 Some('u') => {
2096 let mut hex = String::with_capacity(4);
2098 for _ in 0..4 {
2099 match chars.next() {
2100 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2101 _ => return Err(LexerError::InvalidEscape),
2102 }
2103 }
2104 let codepoint = u32::from_str_radix(&hex, 16)
2105 .map_err(|_| LexerError::InvalidEscape)?;
2106 let ch = char::from_u32(codepoint)
2107 .ok_or(LexerError::InvalidEscape)?;
2108 result.push(ch);
2109 }
2110 Some(next) => {
2112 result.push('\\');
2113 result.push(next);
2114 }
2115 None => return Err(LexerError::InvalidEscape),
2116 }
2117 } else {
2118 result.push(ch);
2119 }
2120 }
2121
2122 Ok(result)
2123}
2124
2125pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
2128 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
2130 return Err(LexerError::UnterminatedVarRef);
2131 }
2132
2133 let inner = &source[2..source.len() - 1];
2134
2135 if inner == "?" {
2137 return Ok(vec!["?".to_string()]);
2138 }
2139
2140 let mut segments = Vec::new();
2141 let mut current = String::new();
2142 let mut chars = inner.chars().peekable();
2143
2144 while let Some(ch) = chars.next() {
2145 match ch {
2146 '.' => {
2147 if !current.is_empty() {
2148 segments.push(current.clone());
2149 current.clear();
2150 }
2151 }
2152 '[' => {
2153 if !current.is_empty() {
2154 segments.push(current.clone());
2155 current.clear();
2156 }
2157 let mut index = String::from("[");
2159 while let Some(&c) = chars.peek() {
2160 if let Some(c) = chars.next() {
2161 index.push(c);
2162 }
2163 if c == ']' {
2164 break;
2165 }
2166 }
2167 segments.push(index);
2168 }
2169 _ => {
2170 current.push(ch);
2171 }
2172 }
2173 }
2174
2175 if !current.is_empty() {
2176 segments.push(current);
2177 }
2178
2179 Ok(segments)
2180}
2181
2182pub fn parse_int(source: &str) -> Result<i64, LexerError> {
2184 source.parse().map_err(|_| LexerError::InvalidNumber)
2185}
2186
2187pub fn parse_float(source: &str) -> Result<f64, LexerError> {
2189 source.parse().map_err(|_| LexerError::InvalidNumber)
2190}
2191
2192#[cfg(test)]
2193#[allow(clippy::approx_constant)]
2194mod tests {
2195 use super::*;
2196
2197 fn lex(source: &str) -> Vec<Token> {
2198 tokenize(source)
2199 .expect("lexer should succeed")
2200 .into_iter()
2201 .map(|s| s.token)
2202 .collect()
2203 }
2204
2205 #[test]
2210 fn keywords() {
2211 assert_eq!(lex("set"), vec![Token::Set]);
2212 assert_eq!(lex("if"), vec![Token::If]);
2213 assert_eq!(lex("then"), vec![Token::Then]);
2214 assert_eq!(lex("else"), vec![Token::Else]);
2215 assert_eq!(lex("elif"), vec![Token::Elif]);
2216 assert_eq!(lex("fi"), vec![Token::Fi]);
2217 assert_eq!(lex("for"), vec![Token::For]);
2218 assert_eq!(lex("in"), vec![Token::In]);
2219 assert_eq!(lex("do"), vec![Token::Do]);
2220 assert_eq!(lex("done"), vec![Token::Done]);
2221 assert_eq!(lex("case"), vec![Token::Case]);
2222 assert_eq!(lex("esac"), vec![Token::Esac]);
2223 assert_eq!(lex("function"), vec![Token::Function]);
2224 assert_eq!(lex("true"), vec![Token::True]);
2225 assert_eq!(lex("false"), vec![Token::False]);
2226 }
2227
2228 #[test]
2229 fn double_semicolon() {
2230 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
2231 assert_eq!(lex("echo \"hi\";;"), vec![
2233 Token::Ident("echo".to_string()),
2234 Token::String("hi".to_string()),
2235 Token::DoubleSemi,
2236 ]);
2237 }
2238
2239 #[test]
2240 fn type_keywords() {
2241 assert_eq!(lex("string"), vec![Token::TypeString]);
2242 assert_eq!(lex("int"), vec![Token::TypeInt]);
2243 assert_eq!(lex("float"), vec![Token::TypeFloat]);
2244 assert_eq!(lex("bool"), vec![Token::TypeBool]);
2245 }
2246
2247 #[test]
2252 fn single_char_operators() {
2253 assert_eq!(lex("="), vec![Token::Eq]);
2254 assert_eq!(lex("|"), vec![Token::Pipe]);
2255 assert_eq!(lex("&"), vec![Token::Amp]);
2256 assert_eq!(lex(">"), vec![Token::Gt]);
2257 assert_eq!(lex("<"), vec![Token::Lt]);
2258 assert_eq!(lex(";"), vec![Token::Semi]);
2259 assert_eq!(lex(":"), vec![Token::Colon]);
2260 assert_eq!(lex(","), vec![Token::Comma]);
2261 assert_eq!(lex("."), vec![Token::Dot]);
2262 }
2263
2264 #[test]
2265 fn multi_char_operators() {
2266 assert_eq!(lex("&&"), vec![Token::And]);
2267 assert_eq!(lex("||"), vec![Token::Or]);
2268 assert_eq!(lex("=="), vec![Token::EqEq]);
2269 assert_eq!(lex("!="), vec![Token::NotEq]);
2270 assert_eq!(lex("=~"), vec![Token::Match]);
2271 assert_eq!(lex("!~"), vec![Token::NotMatch]);
2272 assert_eq!(lex(">="), vec![Token::GtEq]);
2273 assert_eq!(lex("<="), vec![Token::LtEq]);
2274 assert_eq!(lex(">>"), vec![Token::GtGt]);
2275 assert_eq!(lex("2>"), vec![Token::Stderr]);
2276 assert_eq!(lex("&>"), vec![Token::Both]);
2277 }
2278
2279 #[test]
2280 fn brackets() {
2281 assert_eq!(lex("{"), vec![Token::LBrace]);
2282 assert_eq!(lex("}"), vec![Token::RBrace]);
2283 assert_eq!(lex("["), vec![Token::LBracket]);
2284 assert_eq!(lex("]"), vec![Token::RBracket]);
2285 assert_eq!(lex("("), vec![Token::LParen]);
2286 assert_eq!(lex(")"), vec![Token::RParen]);
2287 }
2288
2289 #[test]
2294 fn integers() {
2295 assert_eq!(lex("0"), vec![Token::Int(0)]);
2296 assert_eq!(lex("42"), vec![Token::Int(42)]);
2297 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
2298 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
2299 }
2300
2301 #[test]
2302 fn floats() {
2303 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
2304 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
2305 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
2306 }
2307
2308 #[test]
2309 fn strings() {
2310 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
2311 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
2312 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
2314 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
2315 }
2316
2317 #[test]
2318 fn var_refs() {
2319 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
2320 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
2321 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
2322 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
2323 }
2324
2325 #[test]
2330 fn identifiers() {
2331 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
2332 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
2333 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
2334 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
2335 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
2336 }
2337
2338 #[test]
2339 fn keyword_prefix_identifiers() {
2340 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
2342 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
2343 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
2344 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
2345 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
2346 }
2347
2348 #[test]
2353 fn assignment() {
2354 assert_eq!(
2355 lex("set X = 5"),
2356 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2357 );
2358 }
2359
2360 #[test]
2361 fn command_simple() {
2362 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
2363 assert_eq!(
2364 lex(r#"echo "hello""#),
2365 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
2366 );
2367 }
2368
2369 #[test]
2370 fn command_with_args() {
2371 assert_eq!(
2372 lex("cmd arg1 arg2"),
2373 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
2374 );
2375 }
2376
2377 #[test]
2378 fn command_with_named_args() {
2379 assert_eq!(
2380 lex("cmd key=value"),
2381 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
2382 );
2383 }
2384
2385 #[test]
2386 fn pipeline() {
2387 assert_eq!(
2388 lex("a | b | c"),
2389 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
2390 );
2391 }
2392
2393 #[test]
2394 fn if_statement() {
2395 assert_eq!(
2396 lex("if true; then echo; fi"),
2397 vec![
2398 Token::If,
2399 Token::True,
2400 Token::Semi,
2401 Token::Then,
2402 Token::Ident("echo".to_string()),
2403 Token::Semi,
2404 Token::Fi
2405 ]
2406 );
2407 }
2408
2409 #[test]
2410 fn for_loop() {
2411 assert_eq!(
2412 lex("for X in items; do echo; done"),
2413 vec![
2414 Token::For,
2415 Token::Ident("X".to_string()),
2416 Token::In,
2417 Token::Ident("items".to_string()),
2418 Token::Semi,
2419 Token::Do,
2420 Token::Ident("echo".to_string()),
2421 Token::Semi,
2422 Token::Done
2423 ]
2424 );
2425 }
2426
2427 #[test]
2432 fn whitespace_ignored() {
2433 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
2434 }
2435
2436 #[test]
2437 fn newlines_preserved() {
2438 let tokens = lex("a\nb");
2439 assert_eq!(
2440 tokens,
2441 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2442 );
2443 }
2444
2445 #[test]
2446 fn multiple_newlines() {
2447 let tokens = lex("a\n\n\nb");
2448 assert_eq!(
2449 tokens,
2450 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
2451 );
2452 }
2453
2454 #[test]
2459 fn comments_skipped() {
2460 assert_eq!(lex("# comment"), vec![]);
2461 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
2462 assert_eq!(
2463 lex("a # comment\nb"),
2464 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2465 );
2466 }
2467
2468 #[test]
2469 fn comments_preserved_when_requested() {
2470 let tokens = tokenize_with_comments("a # comment")
2471 .expect("should succeed")
2472 .into_iter()
2473 .map(|s| s.token)
2474 .collect::<Vec<_>>();
2475 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
2476 }
2477
2478 #[test]
2483 fn parse_simple_string() {
2484 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
2485 }
2486
2487 #[test]
2488 fn parse_string_with_escapes() {
2489 assert_eq!(
2490 parse_string_literal(r#""hello\nworld""#).expect("ok"),
2491 "hello\nworld"
2492 );
2493 assert_eq!(
2494 parse_string_literal(r#""tab\there""#).expect("ok"),
2495 "tab\there"
2496 );
2497 assert_eq!(
2498 parse_string_literal(r#""quote\"here""#).expect("ok"),
2499 "quote\"here"
2500 );
2501 }
2502
2503 #[test]
2504 fn parse_string_with_unicode() {
2505 assert_eq!(
2506 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
2507 "emoji ❤"
2508 );
2509 }
2510
2511 #[test]
2512 fn parse_string_with_escaped_dollar() {
2513 assert_eq!(
2516 parse_string_literal(r#""\$VAR""#).expect("ok"),
2517 "__KAISH_ESCAPED_DOLLAR__VAR"
2518 );
2519 assert_eq!(
2520 parse_string_literal(r#""cost: \$100""#).expect("ok"),
2521 "cost: __KAISH_ESCAPED_DOLLAR__100"
2522 );
2523 }
2524
2525 #[test]
2530 fn parse_simple_var() {
2531 assert_eq!(
2532 parse_var_ref("${X}").expect("ok"),
2533 vec!["X"]
2534 );
2535 }
2536
2537 #[test]
2538 fn parse_var_with_field() {
2539 assert_eq!(
2540 parse_var_ref("${VAR.field}").expect("ok"),
2541 vec!["VAR", "field"]
2542 );
2543 }
2544
2545 #[test]
2546 fn parse_var_with_index() {
2547 assert_eq!(
2548 parse_var_ref("${VAR[0]}").expect("ok"),
2549 vec!["VAR", "[0]"]
2550 );
2551 }
2552
2553 #[test]
2554 fn parse_var_nested() {
2555 assert_eq!(
2556 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
2557 vec!["VAR", "field", "[0]", "nested"]
2558 );
2559 }
2560
2561 #[test]
2562 fn parse_last_result() {
2563 assert_eq!(
2564 parse_var_ref("${?}").expect("ok"),
2565 vec!["?"]
2566 );
2567 }
2568
2569 #[test]
2574 fn parse_integers() {
2575 assert_eq!(parse_int("0").expect("ok"), 0);
2576 assert_eq!(parse_int("42").expect("ok"), 42);
2577 assert_eq!(parse_int("-1").expect("ok"), -1);
2578 }
2579
2580 #[test]
2581 fn parse_floats() {
2582 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
2583 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
2584 }
2585
2586 #[test]
2591 fn empty_input() {
2592 assert_eq!(lex(""), vec![]);
2593 }
2594
2595 #[test]
2596 fn only_whitespace() {
2597 assert_eq!(lex(" \t\t "), vec![]);
2598 }
2599
2600 #[test]
2601 fn json_array() {
2602 assert_eq!(
2603 lex(r#"[1, 2, 3]"#),
2604 vec![
2605 Token::LBracket,
2606 Token::Int(1),
2607 Token::Comma,
2608 Token::Int(2),
2609 Token::Comma,
2610 Token::Int(3),
2611 Token::RBracket
2612 ]
2613 );
2614 }
2615
2616 #[test]
2617 fn json_object() {
2618 assert_eq!(
2619 lex(r#"{"key": "value"}"#),
2620 vec![
2621 Token::LBrace,
2622 Token::String("key".to_string()),
2623 Token::Colon,
2624 Token::String("value".to_string()),
2625 Token::RBrace
2626 ]
2627 );
2628 }
2629
2630 #[test]
2631 fn redirect_operators() {
2632 assert_eq!(
2633 lex("cmd > file"),
2634 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
2635 );
2636 assert_eq!(
2637 lex("cmd >> file"),
2638 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
2639 );
2640 assert_eq!(
2641 lex("cmd 2> err"),
2642 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
2643 );
2644 assert_eq!(
2645 lex("cmd &> all"),
2646 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
2647 );
2648 }
2649
2650 #[test]
2651 fn background_job() {
2652 assert_eq!(
2653 lex("cmd &"),
2654 vec![Token::Ident("cmd".to_string()), Token::Amp]
2655 );
2656 }
2657
2658 #[test]
2659 fn command_substitution() {
2660 assert_eq!(
2661 lex("$(cmd)"),
2662 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
2663 );
2664 assert_eq!(
2665 lex("$(cmd arg)"),
2666 vec![
2667 Token::CmdSubstStart,
2668 Token::Ident("cmd".to_string()),
2669 Token::Ident("arg".to_string()),
2670 Token::RParen
2671 ]
2672 );
2673 assert_eq!(
2674 lex("$(a | b)"),
2675 vec![
2676 Token::CmdSubstStart,
2677 Token::Ident("a".to_string()),
2678 Token::Pipe,
2679 Token::Ident("b".to_string()),
2680 Token::RParen
2681 ]
2682 );
2683 }
2684
2685 #[test]
2686 fn complex_pipeline() {
2687 assert_eq!(
2688 lex(r#"cat file | grep pattern="foo" | head count=10"#),
2689 vec![
2690 Token::Ident("cat".to_string()),
2691 Token::Ident("file".to_string()),
2692 Token::Pipe,
2693 Token::Ident("grep".to_string()),
2694 Token::Ident("pattern".to_string()),
2695 Token::Eq,
2696 Token::String("foo".to_string()),
2697 Token::Pipe,
2698 Token::Ident("head".to_string()),
2699 Token::Ident("count".to_string()),
2700 Token::Eq,
2701 Token::Int(10),
2702 ]
2703 );
2704 }
2705
2706 #[test]
2711 fn short_flag() {
2712 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2713 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
2714 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
2715 }
2716
2717 #[test]
2718 fn short_flag_combined() {
2719 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
2721 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
2722 }
2723
2724 #[test]
2725 fn job_spec_lexes_as_one_token() {
2726 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
2728 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
2729 assert_eq!(
2730 lex("wait %1 %2"),
2731 vec![
2732 Token::Ident("wait".to_string()),
2733 Token::JobSpec("%1".to_string()),
2734 Token::JobSpec("%2".to_string()),
2735 ]
2736 );
2737 }
2738
2739 #[test]
2740 fn short_flag_with_internal_hyphens_is_one_token() {
2741 assert_eq!(
2745 lex("-not-a-flag"),
2746 vec![Token::ShortFlag("not-a-flag".to_string())]
2747 );
2748 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2752 assert_eq!(lex("-"), vec![Token::MinusAlone]);
2753 }
2754
2755 #[test]
2756 fn long_flag() {
2757 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
2758 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
2759 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
2760 }
2761
2762 #[test]
2763 fn double_dash() {
2764 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2766 }
2767
2768 #[test]
2769 fn flags_vs_negative_numbers() {
2770 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
2772 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2774 assert_eq!(
2777 lex("-1 a"),
2778 vec![Token::Int(-1), Token::Ident("a".to_string())]
2779 );
2780 }
2781
2782 #[test]
2783 fn command_with_flags() {
2784 assert_eq!(
2785 lex("ls -l"),
2786 vec![
2787 Token::Ident("ls".to_string()),
2788 Token::ShortFlag("l".to_string()),
2789 ]
2790 );
2791 assert_eq!(
2792 lex("git commit -m"),
2793 vec![
2794 Token::Ident("git".to_string()),
2795 Token::Ident("commit".to_string()),
2796 Token::ShortFlag("m".to_string()),
2797 ]
2798 );
2799 assert_eq!(
2800 lex("git push --force"),
2801 vec![
2802 Token::Ident("git".to_string()),
2803 Token::Ident("push".to_string()),
2804 Token::LongFlag("force".to_string()),
2805 ]
2806 );
2807 }
2808
2809 #[test]
2810 fn flag_with_value() {
2811 assert_eq!(
2812 lex(r#"git commit -m "message""#),
2813 vec![
2814 Token::Ident("git".to_string()),
2815 Token::Ident("commit".to_string()),
2816 Token::ShortFlag("m".to_string()),
2817 Token::String("message".to_string()),
2818 ]
2819 );
2820 assert_eq!(
2821 lex(r#"--message="hello""#),
2822 vec![
2823 Token::LongFlag("message".to_string()),
2824 Token::Eq,
2825 Token::String("hello".to_string()),
2826 ]
2827 );
2828 }
2829
2830 #[test]
2831 fn end_of_flags_marker() {
2832 assert_eq!(
2833 lex("git checkout -- file"),
2834 vec![
2835 Token::Ident("git".to_string()),
2836 Token::Ident("checkout".to_string()),
2837 Token::DoubleDash,
2838 Token::Ident("file".to_string()),
2839 ]
2840 );
2841 }
2842
2843 #[test]
2848 fn local_keyword() {
2849 assert_eq!(lex("local"), vec![Token::Local]);
2850 assert_eq!(
2851 lex("local X = 5"),
2852 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2853 );
2854 }
2855
2856 #[test]
2857 fn simple_var_ref() {
2858 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
2859 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
2860 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
2861 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
2862 }
2863
2864 #[test]
2865 fn simple_var_ref_in_command() {
2866 assert_eq!(
2867 lex("echo $NAME"),
2868 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
2869 );
2870 }
2871
2872 #[test]
2873 fn single_quoted_strings() {
2874 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
2875 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
2876 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
2877 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
2879 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
2880 }
2881
2882 #[test]
2883 fn test_brackets() {
2884 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
2886 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
2887 assert_eq!(
2888 lex("[[ -f file ]]"),
2889 vec![
2890 Token::LBracket,
2891 Token::LBracket,
2892 Token::ShortFlag("f".to_string()),
2893 Token::Ident("file".to_string()),
2894 Token::RBracket,
2895 Token::RBracket
2896 ]
2897 );
2898 }
2899
2900 #[test]
2901 fn test_expression_syntax() {
2902 assert_eq!(
2903 lex(r#"[[ $X == "value" ]]"#),
2904 vec![
2905 Token::LBracket,
2906 Token::LBracket,
2907 Token::SimpleVarRef("X".to_string()),
2908 Token::EqEq,
2909 Token::String("value".to_string()),
2910 Token::RBracket,
2911 Token::RBracket
2912 ]
2913 );
2914 }
2915
2916 #[test]
2917 fn bash_style_assignment() {
2918 assert_eq!(
2920 lex(r#"NAME="value""#),
2921 vec![
2922 Token::Ident("NAME".to_string()),
2923 Token::Eq,
2924 Token::String("value".to_string())
2925 ]
2926 );
2927 }
2928
2929 #[test]
2930 fn positional_params() {
2931 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
2932 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
2933 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
2934 assert_eq!(lex("$@"), vec![Token::AllArgs]);
2935 assert_eq!(lex("$#"), vec![Token::ArgCount]);
2936 }
2937
2938 #[test]
2939 fn positional_in_context() {
2940 assert_eq!(
2941 lex("echo $1 $2"),
2942 vec![
2943 Token::Ident("echo".to_string()),
2944 Token::Positional(1),
2945 Token::Positional(2),
2946 ]
2947 );
2948 }
2949
2950 #[test]
2951 fn var_length() {
2952 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
2953 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
2954 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
2955 }
2956
2957 #[test]
2958 fn var_length_in_context() {
2959 assert_eq!(
2960 lex("echo ${#NAME}"),
2961 vec![
2962 Token::Ident("echo".to_string()),
2963 Token::VarLength("NAME".to_string()),
2964 ]
2965 );
2966 }
2967
2968 #[test]
2973 fn plus_flag() {
2974 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
2976 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
2977 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
2978 }
2979
2980 #[test]
2981 fn set_with_plus_flag() {
2982 assert_eq!(
2983 lex("set +e"),
2984 vec![
2985 Token::Set,
2986 Token::PlusFlag("e".to_string()),
2987 ]
2988 );
2989 }
2990
2991 #[test]
2992 fn set_with_multiple_flags() {
2993 assert_eq!(
2994 lex("set -e -u"),
2995 vec![
2996 Token::Set,
2997 Token::ShortFlag("e".to_string()),
2998 Token::ShortFlag("u".to_string()),
2999 ]
3000 );
3001 }
3002
3003 #[test]
3004 fn flags_vs_negative_numbers_edge_cases() {
3005 assert_eq!(
3007 lex("-1 a"),
3008 vec![Token::Int(-1), Token::Ident("a".to_string())]
3009 );
3010 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3012 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3014 }
3015
3016 #[test]
3017 fn single_dash_is_minus_alone() {
3018 let result = tokenize("-").expect("should lex");
3020 assert_eq!(result.len(), 1);
3021 assert!(matches!(result[0].token, Token::MinusAlone));
3022 }
3023
3024 #[test]
3025 fn plus_bare_for_date_format() {
3026 let result = tokenize("+%s").expect("should lex");
3028 assert_eq!(result.len(), 1);
3029 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
3030
3031 let result = tokenize("+%Y-%m-%d").expect("should lex");
3033 assert_eq!(result.len(), 1);
3034 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
3035 }
3036
3037 #[test]
3038 fn plus_flag_still_works() {
3039 let result = tokenize("+e").expect("should lex");
3041 assert_eq!(result.len(), 1);
3042 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
3043 }
3044
3045 #[test]
3046 fn while_keyword_vs_while_loop() {
3047 assert_eq!(lex("while"), vec![Token::While]);
3049 assert_eq!(
3051 lex("while true"),
3052 vec![Token::While, Token::True]
3053 );
3054 }
3055
3056 #[test]
3057 fn control_flow_keywords() {
3058 assert_eq!(lex("break"), vec![Token::Break]);
3059 assert_eq!(lex("continue"), vec![Token::Continue]);
3060 assert_eq!(lex("return"), vec![Token::Return]);
3061 assert_eq!(lex("exit"), vec![Token::Exit]);
3062 }
3063
3064 #[test]
3065 fn control_flow_with_numbers() {
3066 assert_eq!(
3067 lex("break 2"),
3068 vec![Token::Break, Token::Int(2)]
3069 );
3070 assert_eq!(
3071 lex("continue 3"),
3072 vec![Token::Continue, Token::Int(3)]
3073 );
3074 assert_eq!(
3075 lex("exit 1"),
3076 vec![Token::Exit, Token::Int(1)]
3077 );
3078 }
3079
3080 #[test]
3085 fn heredoc_simple() {
3086 let source = "cat <<EOF\nhello\nworld\nEOF";
3087 let tokens = lex(source);
3088 assert_eq!(tokens, vec![
3090 Token::Ident("cat".to_string()),
3091 Token::HereDocStart,
3092 Token::HereDoc(HereDocData {
3093 content: "hello\nworld\n".to_string(),
3094 literal: false,
3095 strip_tabs: false,
3096 body_start_offset: 10,
3097 }),
3098 Token::Newline,
3099 ]);
3100 }
3101
3102 #[test]
3103 fn heredoc_empty() {
3104 let source = "cat <<EOF\nEOF";
3105 let tokens = lex(source);
3106 assert_eq!(tokens, vec![
3107 Token::Ident("cat".to_string()),
3108 Token::HereDocStart,
3109 Token::HereDoc(HereDocData {
3110 content: "".to_string(),
3111 literal: false,
3112 strip_tabs: false,
3113 body_start_offset: 10,
3114 }),
3115 Token::Newline,
3116 ]);
3117 }
3118
3119 #[test]
3120 fn heredoc_with_special_chars() {
3121 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
3122 let tokens = lex(source);
3123 assert_eq!(tokens, vec![
3124 Token::Ident("cat".to_string()),
3125 Token::HereDocStart,
3126 Token::HereDoc(HereDocData {
3127 content: "$VAR and \"quoted\" 'single'\n".to_string(),
3128 literal: false,
3129 strip_tabs: false,
3130 body_start_offset: 10,
3131 }),
3132 Token::Newline,
3133 ]);
3134 }
3135
3136 #[test]
3137 fn heredoc_multiline() {
3138 let source = "cat <<END\nline1\nline2\nline3\nEND";
3139 let tokens = lex(source);
3140 assert_eq!(tokens, vec![
3141 Token::Ident("cat".to_string()),
3142 Token::HereDocStart,
3143 Token::HereDoc(HereDocData {
3144 content: "line1\nline2\nline3\n".to_string(),
3145 literal: false,
3146 strip_tabs: false,
3147 body_start_offset: 10,
3148 }),
3149 Token::Newline,
3150 ]);
3151 }
3152
3153 #[test]
3154 fn heredoc_in_command() {
3155 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
3156 let tokens = lex(source);
3157 assert_eq!(tokens, vec![
3158 Token::Ident("cat".to_string()),
3159 Token::HereDocStart,
3160 Token::HereDoc(HereDocData {
3161 content: "hello\n".to_string(),
3162 literal: false,
3163 strip_tabs: false,
3164 body_start_offset: 10,
3165 }),
3166 Token::Newline,
3167 Token::Ident("echo".to_string()),
3168 Token::Ident("goodbye".to_string()),
3169 ]);
3170 }
3171
3172 #[test]
3173 fn heredoc_strip_tabs() {
3174 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
3175 let tokens = lex(source);
3176 assert_eq!(tokens, vec![
3180 Token::Ident("cat".to_string()),
3181 Token::HereDocStart,
3182 Token::HereDoc(HereDocData {
3183 content: "\thello\n\tworld\n".to_string(),
3184 literal: false,
3185 strip_tabs: true,
3186 body_start_offset: 11,
3187 }),
3188 Token::Newline,
3189 ]);
3190 }
3191
3192 #[test]
3197 fn arithmetic_simple() {
3198 let source = "$((1 + 2))";
3199 let tokens = lex(source);
3200 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
3201 }
3202
3203 #[test]
3204 fn arithmetic_in_assignment() {
3205 let source = "X=$((5 * 3))";
3206 let tokens = lex(source);
3207 assert_eq!(tokens, vec![
3208 Token::Ident("X".to_string()),
3209 Token::Eq,
3210 Token::Arithmetic("5 * 3".to_string()),
3211 ]);
3212 }
3213
3214 #[test]
3215 fn arithmetic_with_nested_parens() {
3216 let source = "$((2 * (3 + 4)))";
3217 let tokens = lex(source);
3218 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
3219 }
3220
3221 #[test]
3222 fn arithmetic_with_variable() {
3223 let source = "$((X + 1))";
3224 let tokens = lex(source);
3225 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
3226 }
3227
3228 #[test]
3229 fn arithmetic_command_subst_not_confused() {
3230 let source = "$(echo hello)";
3232 let tokens = lex(source);
3233 assert_eq!(tokens, vec![
3234 Token::CmdSubstStart,
3235 Token::Ident("echo".to_string()),
3236 Token::Ident("hello".to_string()),
3237 Token::RParen,
3238 ]);
3239 }
3240
3241 #[test]
3242 fn arithmetic_nesting_limit() {
3243 let open_parens = "(".repeat(300);
3245 let close_parens = ")".repeat(300);
3246 let source = format!("$(({}1{}))", open_parens, close_parens);
3247 let result = tokenize(&source);
3248 assert!(result.is_err());
3249 let errors = result.unwrap_err();
3250 assert_eq!(errors.len(), 1);
3251 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
3252 }
3253
3254 #[test]
3255 fn arithmetic_nesting_within_limit() {
3256 let source = "$((((1 + 2) * 3)))";
3258 let tokens = lex(source);
3259 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
3260 }
3261
3262 #[test]
3274 fn arithmetic_after_apostrophe_in_comment() {
3275 let source = "# this doesn't work\necho $((1+2))";
3278 let tokens = lex(source);
3279 assert_eq!(tokens, vec![
3280 Token::Newline,
3281 Token::Ident("echo".to_string()),
3282 Token::Arithmetic("1+2".to_string()),
3283 ]);
3284 }
3285
3286 #[test]
3287 fn arithmetic_inside_comment_is_not_expanded() {
3288 let source = "# the $((y)) syntax explained\necho hello";
3290 let tokens = lex(source);
3291 assert_eq!(tokens, vec![
3292 Token::Newline,
3293 Token::Ident("echo".to_string()),
3294 Token::Ident("hello".to_string()),
3295 ]);
3296 }
3297
3298 #[test]
3299 fn backticked_arithmetic_in_comment_is_not_expanded() {
3300 let source = "# the `$((x))` syntax explained\necho $((3+4))";
3304 let tokens = lex(source);
3305 assert_eq!(tokens, vec![
3306 Token::Newline,
3307 Token::Ident("echo".to_string()),
3308 Token::Arithmetic("3+4".to_string()),
3309 ]);
3310 }
3311
3312 #[test]
3313 fn arithmetic_still_works_outside_comments() {
3314 let source = "X=$((1+2)); Y=$((3*4))";
3317 let tokens = lex(source);
3318 assert_eq!(tokens, vec![
3319 Token::Ident("X".to_string()),
3320 Token::Eq,
3321 Token::Arithmetic("1+2".to_string()),
3322 Token::Semi,
3323 Token::Ident("Y".to_string()),
3324 Token::Eq,
3325 Token::Arithmetic("3*4".to_string()),
3326 ]);
3327 }
3328
3329 #[test]
3330 fn arithmetic_inside_double_quotes_still_expands() {
3331 let source = "echo \"# $((1+2))\"";
3334 let tokens = lex(source);
3335 assert_eq!(tokens.len(), 2);
3340 assert!(matches!(tokens[0], Token::Ident(_)));
3341 assert!(matches!(tokens[1], Token::String(_)));
3342 }
3343
3344 #[test]
3357 fn backtick_in_source_is_rejected() {
3358 let result = tokenize("echo `date`");
3359 assert!(result.is_err());
3360 let errors = result.unwrap_err();
3361 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
3362 }
3363
3364 #[test]
3365 fn backtick_in_comment_is_just_comment_text() {
3366 let source = "# use `date` here\necho hi";
3369 let tokens = lex(source);
3370 assert_eq!(tokens, vec![
3371 Token::Newline,
3372 Token::Ident("echo".to_string()),
3373 Token::Ident("hi".to_string()),
3374 ]);
3375 }
3376
3377 #[test]
3378 fn backtick_in_single_quoted_string_is_literal() {
3379 let source = "echo '`date`'";
3382 let tokens = lex(source);
3383 assert_eq!(tokens, vec![
3384 Token::Ident("echo".to_string()),
3385 Token::SingleString("`date`".to_string()),
3386 ]);
3387 }
3388
3389 #[test]
3390 fn backtick_in_double_quoted_string_is_literal() {
3391 let source = "echo \"`date`\"";
3396 let tokens = lex(source);
3397 assert_eq!(tokens.len(), 2);
3398 assert!(matches!(tokens[0], Token::Ident(_)));
3399 match &tokens[1] {
3400 Token::String(s) => assert!(s.contains('`')),
3401 other => panic!("expected Token::String, got {:?}", other),
3402 }
3403 }
3404
3405 #[test]
3406 fn backtick_in_heredoc_body_is_preserved() {
3407 let source = "cat <<EOF\n`date`\nEOF\n";
3410 let tokens = lex(source);
3411 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
3412 assert!(heredoc.is_some(), "expected a HereDoc token");
3413 if let Some(Token::HereDoc(d)) = heredoc {
3414 assert!(d.content.contains('`'));
3415 }
3416 }
3417
3418 #[test]
3423 fn token_categories() {
3424 assert_eq!(Token::If.category(), TokenCategory::Keyword);
3426 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
3427 assert_eq!(Token::For.category(), TokenCategory::Keyword);
3428 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
3429 assert_eq!(Token::True.category(), TokenCategory::Keyword);
3430 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
3431
3432 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
3434 assert_eq!(Token::And.category(), TokenCategory::Operator);
3435 assert_eq!(Token::Or.category(), TokenCategory::Operator);
3436 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
3437 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
3438
3439 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
3441 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
3442 assert_eq!(
3443 Token::HereDoc(HereDocData {
3444 content: "test".to_string(),
3445 literal: false,
3446 strip_tabs: false,
3447 body_start_offset: 0,
3448 }).category(),
3449 TokenCategory::String,
3450 );
3451
3452 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
3454 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
3455 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
3456
3457 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
3459 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
3460 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
3461 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
3462 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
3463 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
3464 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
3465
3466 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
3468 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
3469 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
3470 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
3471
3472 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
3474 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
3475 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
3476 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
3477
3478 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
3480
3481 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
3483
3484 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
3486 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
3487 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
3488
3489 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
3491 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
3492 }
3493
3494 #[test]
3495 fn test_heredoc_piped_to_command() {
3496 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
3499 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
3500 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
3501 assert!(heredoc_pos.is_some(), "should have a heredoc token");
3502 assert!(pipe_pos.is_some(), "should have a pipe token");
3503 assert!(
3504 pipe_pos.unwrap() > heredoc_pos.unwrap(),
3505 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
3506 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
3507 );
3508 }
3509
3510 #[test]
3511 fn test_heredoc_standalone_still_works() {
3512 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
3514 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
3515 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
3516 }
3517
3518 #[test]
3519 fn test_heredoc_preserves_leading_empty_lines() {
3520 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
3522 let heredoc = tokens.iter().find_map(|t| {
3523 if let Token::HereDoc(data) = &t.token {
3524 Some(data.clone())
3525 } else {
3526 None
3527 }
3528 });
3529 assert!(heredoc.is_some(), "should have a heredoc token");
3530 let data = heredoc.unwrap();
3531 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
3532 assert_eq!(data.content, "\nhello\n");
3533 }
3534
3535 #[test]
3536 fn test_heredoc_quoted_delimiter_sets_literal() {
3537 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
3539 let heredoc = tokens.iter().find_map(|t| {
3540 if let Token::HereDoc(data) = &t.token {
3541 Some(data.clone())
3542 } else {
3543 None
3544 }
3545 });
3546 assert!(heredoc.is_some(), "should have a heredoc token");
3547 let data = heredoc.unwrap();
3548 assert!(data.literal, "quoted delimiter should set literal=true");
3549 assert_eq!(data.content, "hello $HOME\n");
3550 }
3551
3552 #[test]
3553 fn test_heredoc_unquoted_delimiter_not_literal() {
3554 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
3556 let heredoc = tokens.iter().find_map(|t| {
3557 if let Token::HereDoc(data) = &t.token {
3558 Some(data.clone())
3559 } else {
3560 None
3561 }
3562 });
3563 assert!(heredoc.is_some(), "should have a heredoc token");
3564 let data = heredoc.unwrap();
3565 assert!(!data.literal, "unquoted delimiter should have literal=false");
3566 }
3567
3568 #[test]
3573 fn colon_double_in_word() {
3574 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
3575 }
3576
3577 #[test]
3578 fn colon_single_in_word() {
3579 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
3580 }
3581
3582 #[test]
3583 fn colon_with_port() {
3584 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
3585 }
3586
3587 #[test]
3588 fn colon_standalone() {
3589 assert_eq!(lex(":"), vec![Token::Colon]);
3590 }
3591
3592 #[test]
3593 fn colon_spaced_no_merge() {
3594 assert_eq!(
3595 lex("foo : bar"),
3596 vec![
3597 Token::Ident("foo".into()),
3598 Token::Colon,
3599 Token::Ident("bar".into()),
3600 ]
3601 );
3602 }
3603
3604 #[test]
3605 fn colon_in_command_arg() {
3606 assert_eq!(
3607 lex("echo foo::bar"),
3608 vec![
3609 Token::Ident("echo".into()),
3610 Token::Ident("foo::bar".into()),
3611 ]
3612 );
3613 }
3614
3615 #[test]
3616 fn colon_trailing() {
3617 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
3619 }
3620
3621 #[test]
3622 fn colon_leading() {
3623 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
3625 }
3626
3627 #[test]
3628 fn colon_with_path() {
3629 assert_eq!(
3631 lex("/usr/bin:8080"),
3632 vec![Token::Ident("/usr/bin:8080".into())]
3633 );
3634 }
3635
3636 #[test]
3641 fn is_keyword_covers_control_flow() {
3642 for t in [
3643 Token::While,
3644 Token::Return,
3645 Token::Break,
3646 Token::Continue,
3647 Token::Exit,
3648 ] {
3649 assert!(t.is_keyword(), "{t:?} should be a keyword");
3650 }
3651 }
3652
3653 #[test]
3654 fn starts_statement_covers_while() {
3655 assert!(Token::While.starts_statement());
3656 }
3657
3658 #[test]
3659 fn is_keyword_rejects_operators() {
3660 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
3661 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
3662 }
3663 }
3664}