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]+(\.[0-9]+)?(-[0-9a-zA-Z._]+)+", lex_slice_word, priority = 3)]
555 #[regex(r"-[0-9]+[a-zA-Z_][0-9a-zA-Z._-]*", lex_slice_word, priority = 3)]
556 DashNumWord(String),
557
558 #[regex(r"@[a-zA-Z0-9_./@-]*", lex_slice_word, priority = 3)]
563 AtWord(String),
564
565 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
567 InvalidFloatNoLeading,
568
569 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
572 InvalidFloatNoTrailing,
573
574 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
580 Path(String),
581
582 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.@-]*", lex_ident)]
590 Ident(String),
591
592 #[regex(r"#[^\n\r]*", allow_greedy = true)]
598 Comment,
599
600 #[regex(r"\n|\r\n")]
602 Newline,
603
604 #[regex(r"\\[ \t]*(\n|\r\n)")]
606 LineContinuation,
607
608 #[token("`", reject_backtick)]
617 BacktickRejected,
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
625pub enum TokenCategory {
626 Keyword,
628 Operator,
630 String,
632 Number,
634 Variable,
636 Comment,
638 Punctuation,
640 Command,
642 Path,
644 Flag,
646 Error,
648}
649
650impl Token {
651 pub fn category(&self) -> TokenCategory {
653 match self {
654 Token::If
656 | Token::Then
657 | Token::Else
658 | Token::Elif
659 | Token::Fi
660 | Token::For
661 | Token::In
662 | Token::Do
663 | Token::Done
664 | Token::While
665 | Token::Case
666 | Token::Esac
667 | Token::Function
668 | Token::Return
669 | Token::Break
670 | Token::Continue
671 | Token::Exit
672 | Token::Set
673 | Token::Local
674 | Token::True
675 | Token::False
676 | Token::TypeString
677 | Token::TypeInt
678 | Token::TypeFloat
679 | Token::TypeBool => TokenCategory::Keyword,
680
681 Token::Pipe
683 | Token::And
684 | Token::Or
685 | Token::Amp
686 | Token::Eq
687 | Token::EqEq
688 | Token::NotEq
689 | Token::Match
690 | Token::NotMatch
691 | Token::Lt
692 | Token::Gt
693 | Token::LtEq
694 | Token::GtEq
695 | Token::GtGt
696 | Token::Stderr
697 | Token::Both
698 | Token::HereDocStart
699 | Token::HereString
700 | Token::StderrToStdout
701 | Token::StdoutToStderr
702 | Token::StdoutToStderr2 => TokenCategory::Operator,
703
704 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
706
707 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
709
710 Token::VarRef(_)
712 | Token::SimpleVarRef(_)
713 | Token::Positional(_)
714 | Token::AllArgs
715 | Token::ArgCount
716 | Token::VarLength(_)
717 | Token::LastExitCode
718 | Token::CurrentPid => TokenCategory::Variable,
719
720 Token::LongFlag(_)
722 | Token::ShortFlag(_)
723 | Token::PlusFlag(_)
724 | Token::DoubleDash => TokenCategory::Flag,
725
726 Token::Semi
728 | Token::DoubleSemi
729 | Token::Colon
730 | Token::Comma
731 | Token::Dot
732 | Token::LParen
733 | Token::RParen
734 | Token::LBrace
735 | Token::RBrace
736 | Token::LBracket
737 | Token::RBracket
738 | Token::Bang
739 | Token::Question
740 | Token::Star
741 | Token::Newline
742 | Token::LineContinuation
743 | Token::CmdSubstStart => TokenCategory::Punctuation,
744
745 Token::GlobWord(_) => TokenCategory::Path,
747
748 Token::Comment => TokenCategory::Comment,
750
751 Token::Path(_)
753 | Token::TildePath(_)
754 | Token::RelativePath(_)
755 | Token::Tilde
756 | Token::DotDot
757 | Token::DotSlashPath(_) => TokenCategory::Path,
758
759 Token::Ident(_)
761 | Token::PlusBare(_)
762 | Token::MinusBare(_)
763 | Token::MinusAlone
764 | Token::NumberIdent(_)
765 | Token::DashNumWord(_)
766 | Token::AtWord(_)
767 | Token::DottedIdent(_)
768 | Token::JobSpec(_) => TokenCategory::Command,
769
770 Token::InvalidFloatNoLeading
772 | Token::InvalidFloatNoTrailing
773 | Token::BacktickRejected => TokenCategory::Error,
774 }
775 }
776}
777
778fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
780 parse_string_literal(lex.slice())
781}
782
783fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
785 let s = lex.slice();
786 s[1..s.len() - 1].to_string()
788}
789
790fn lex_varref(lex: &mut logos::Lexer<Token>) -> String {
792 lex.slice().to_string()
794}
795
796fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
798 lex.slice()[1..].to_string()
800}
801
802fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
804 lex.slice()[1..].parse().unwrap_or(0)
806}
807
808fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
810 let s = lex.slice();
812 s[3..s.len() - 1].to_string()
813}
814
815fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
817 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
818}
819
820fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
822 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
823}
824
825fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
829 lex.slice().to_string()
830}
831
832fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
834 lex.slice().to_string()
835}
836
837fn lex_slice_word(lex: &mut logos::Lexer<Token>) -> String {
840 lex.slice().to_string()
841}
842
843fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
846 Err(LexerError::InvalidFloatNoLeading)
847}
848
849fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
853 Err(LexerError::BackticksNotSupported)
854}
855
856fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
859 Err(LexerError::InvalidFloatNoTrailing)
860}
861
862fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
864 let s = lex.slice();
865
866 match s.to_lowercase().as_str() {
869 "true" | "false" if s != "true" && s != "false" => {
870 return Err(LexerError::AmbiguousBoolean(s.to_string()));
871 }
872 _ => {}
873 }
874
875 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
877 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
878 }
879
880 Ok(s.to_string())
881}
882
883fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
885 lex.slice()[2..].to_string()
887}
888
889fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
891 lex.slice()[1..].to_string()
893}
894
895fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
897 lex.slice()[1..].to_string()
899}
900
901fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
903 lex.slice().to_string()
904}
905
906fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
908 lex.slice().to_string()
909}
910
911fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
913 lex.slice().to_string()
914}
915
916fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
918 lex.slice().to_string()
919}
920
921fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
923 lex.slice().to_string()
924}
925
926fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
928 lex.slice().to_string()
929}
930
931fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
933 lex.slice().to_string()
934}
935
936impl fmt::Display for Token {
937 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938 match self {
939 Token::Set => write!(f, "set"),
940 Token::Local => write!(f, "local"),
941 Token::If => write!(f, "if"),
942 Token::Then => write!(f, "then"),
943 Token::Else => write!(f, "else"),
944 Token::Elif => write!(f, "elif"),
945 Token::Fi => write!(f, "fi"),
946 Token::For => write!(f, "for"),
947 Token::While => write!(f, "while"),
948 Token::In => write!(f, "in"),
949 Token::Do => write!(f, "do"),
950 Token::Done => write!(f, "done"),
951 Token::Case => write!(f, "case"),
952 Token::Esac => write!(f, "esac"),
953 Token::Function => write!(f, "function"),
954 Token::Break => write!(f, "break"),
955 Token::Continue => write!(f, "continue"),
956 Token::Return => write!(f, "return"),
957 Token::Exit => write!(f, "exit"),
958 Token::True => write!(f, "true"),
959 Token::False => write!(f, "false"),
960 Token::TypeString => write!(f, "string"),
961 Token::TypeInt => write!(f, "int"),
962 Token::TypeFloat => write!(f, "float"),
963 Token::TypeBool => write!(f, "bool"),
964 Token::And => write!(f, "&&"),
965 Token::Or => write!(f, "||"),
966 Token::EqEq => write!(f, "=="),
967 Token::NotEq => write!(f, "!="),
968 Token::Match => write!(f, "=~"),
969 Token::NotMatch => write!(f, "!~"),
970 Token::GtEq => write!(f, ">="),
971 Token::LtEq => write!(f, "<="),
972 Token::GtGt => write!(f, ">>"),
973 Token::StderrToStdout => write!(f, "2>&1"),
974 Token::StdoutToStderr => write!(f, "1>&2"),
975 Token::StdoutToStderr2 => write!(f, ">&2"),
976 Token::Stderr => write!(f, "2>"),
977 Token::Both => write!(f, "&>"),
978 Token::HereDocStart => write!(f, "<<"),
979 Token::HereString => write!(f, "<<<"),
980 Token::DoubleSemi => write!(f, ";;"),
981 Token::Eq => write!(f, "="),
982 Token::Pipe => write!(f, "|"),
983 Token::Amp => write!(f, "&"),
984 Token::Gt => write!(f, ">"),
985 Token::Lt => write!(f, "<"),
986 Token::Semi => write!(f, ";"),
987 Token::Colon => write!(f, ":"),
988 Token::Comma => write!(f, ","),
989 Token::Dot => write!(f, "."),
990 Token::DotDot => write!(f, ".."),
991 Token::Tilde => write!(f, "~"),
992 Token::TildePath(s) => write!(f, "{}", s),
993 Token::RelativePath(s) => write!(f, "{}", s),
994 Token::DotSlashPath(s) => write!(f, "{}", s),
995 Token::LBrace => write!(f, "{{"),
996 Token::RBrace => write!(f, "}}"),
997 Token::LBracket => write!(f, "["),
998 Token::RBracket => write!(f, "]"),
999 Token::LParen => write!(f, "("),
1000 Token::RParen => write!(f, ")"),
1001 Token::Star => write!(f, "*"),
1002 Token::Bang => write!(f, "!"),
1003 Token::Question => write!(f, "?"),
1004 Token::GlobWord(s) => write!(f, "GLOB({})", s),
1005 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
1006 Token::CmdSubstStart => write!(f, "$("),
1007 Token::LongFlag(s) => write!(f, "--{}", s),
1008 Token::ShortFlag(s) => write!(f, "-{}", s),
1009 Token::PlusFlag(s) => write!(f, "+{}", s),
1010 Token::DoubleDash => write!(f, "--"),
1011 Token::PlusBare(s) => write!(f, "{}", s),
1012 Token::MinusBare(s) => write!(f, "{}", s),
1013 Token::JobSpec(s) => write!(f, "{}", s),
1014 Token::MinusAlone => write!(f, "-"),
1015 Token::String(s) => write!(f, "STRING({:?})", s),
1016 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
1017 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
1018 Token::VarRef(v) => write!(f, "VARREF({})", v),
1019 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
1020 Token::Positional(n) => write!(f, "${}", n),
1021 Token::AllArgs => write!(f, "$@"),
1022 Token::ArgCount => write!(f, "$#"),
1023 Token::LastExitCode => write!(f, "$?"),
1024 Token::CurrentPid => write!(f, "$$"),
1025 Token::VarLength(v) => write!(f, "${{#{}}}", v),
1026 Token::Int(n) => write!(f, "INT({})", n),
1027 Token::Float(n) => write!(f, "FLOAT({})", n),
1028 Token::Path(s) => write!(f, "PATH({})", s),
1029 Token::Ident(s) => write!(f, "IDENT({})", s),
1030 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1031 Token::DashNumWord(s) => write!(f, "DASHNUM({})", s),
1032 Token::AtWord(s) => write!(f, "ATWORD({})", s),
1033 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1034 Token::Comment => write!(f, "COMMENT"),
1035 Token::Newline => write!(f, "NEWLINE"),
1036 Token::LineContinuation => write!(f, "LINECONT"),
1037 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1039 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1040 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1041 }
1042 }
1043}
1044
1045impl Token {
1046 pub fn is_keyword(&self) -> bool {
1051 matches!(
1052 self,
1053 Token::Set
1054 | Token::Local
1055 | Token::If
1056 | Token::Then
1057 | Token::Else
1058 | Token::Elif
1059 | Token::Fi
1060 | Token::For
1061 | Token::In
1062 | Token::Do
1063 | Token::Done
1064 | Token::While
1065 | Token::Case
1066 | Token::Esac
1067 | Token::Function
1068 | Token::Return
1069 | Token::Break
1070 | Token::Continue
1071 | Token::Exit
1072 | Token::True
1073 | Token::False
1074 )
1075 }
1076
1077 pub fn is_type(&self) -> bool {
1079 matches!(
1080 self,
1081 Token::TypeString
1082 | Token::TypeInt
1083 | Token::TypeFloat
1084 | Token::TypeBool
1085 )
1086 }
1087
1088 pub fn starts_statement(&self) -> bool {
1091 matches!(
1092 self,
1093 Token::Set
1094 | Token::Local
1095 | Token::Function
1096 | Token::If
1097 | Token::For
1098 | Token::While
1099 | Token::Case
1100 | Token::Ident(_)
1101 | Token::LBracket
1102 )
1103 }
1104
1105 pub fn is_value(&self) -> bool {
1107 matches!(
1108 self,
1109 Token::String(_)
1110 | Token::SingleString(_)
1111 | Token::HereDoc(_)
1112 | Token::Arithmetic(_)
1113 | Token::Int(_)
1114 | Token::Float(_)
1115 | Token::True
1116 | Token::False
1117 | Token::VarRef(_)
1118 | Token::SimpleVarRef(_)
1119 | Token::CmdSubstStart
1120 | Token::Path(_)
1121 | Token::GlobWord(_)
1122 | Token::LastExitCode
1123 | Token::CurrentPid
1124 )
1125 }
1126}
1127
1128struct ArithmeticPreprocessResult {
1130 text: String,
1132 arithmetics: Vec<(String, String)>,
1134 replacements: Vec<SpanReplacement>,
1136}
1137
1138fn skip_command_substitution(
1147 chars: &[char],
1148 i: &mut usize,
1149 source_pos: &mut usize,
1150 result: &mut String,
1151) {
1152 result.push('$');
1154 result.push('(');
1155 *i += 2;
1156 *source_pos += 2;
1157
1158 let mut depth: usize = 1;
1159 let mut in_single_quote = false;
1160 let mut in_double_quote = false;
1161
1162 while *i < chars.len() && depth > 0 {
1163 let c = chars[*i];
1164
1165 if in_single_quote {
1166 result.push(c);
1167 *source_pos += c.len_utf8();
1168 *i += 1;
1169 if c == '\'' {
1170 in_single_quote = false;
1171 }
1172 continue;
1173 }
1174
1175 if in_double_quote {
1176 if c == '\\' && *i + 1 < chars.len() {
1177 let next = chars[*i + 1];
1178 if next == '"' || next == '\\' || next == '$' || next == '`' {
1179 result.push(c);
1180 result.push(next);
1181 *source_pos += c.len_utf8() + next.len_utf8();
1182 *i += 2;
1183 continue;
1184 }
1185 }
1186 if c == '"' {
1187 in_double_quote = false;
1188 }
1189 result.push(c);
1190 *source_pos += c.len_utf8();
1191 *i += 1;
1192 continue;
1193 }
1194
1195 match c {
1197 '\'' => {
1198 in_single_quote = true;
1199 result.push(c);
1200 *source_pos += c.len_utf8();
1201 *i += 1;
1202 }
1203 '"' => {
1204 in_double_quote = true;
1205 result.push(c);
1206 *source_pos += c.len_utf8();
1207 *i += 1;
1208 }
1209 '\\' if *i + 1 < chars.len() => {
1210 result.push(c);
1211 result.push(chars[*i + 1]);
1212 *source_pos += c.len_utf8() + chars[*i + 1].len_utf8();
1213 *i += 2;
1214 }
1215 '(' => {
1216 depth += 1;
1217 result.push(c);
1218 *source_pos += c.len_utf8();
1219 *i += 1;
1220 }
1221 ')' => {
1222 depth -= 1;
1223 result.push(c);
1224 *source_pos += c.len_utf8();
1225 *i += 1;
1226 }
1227 _ => {
1228 result.push(c);
1229 *source_pos += c.len_utf8();
1230 *i += 1;
1231 }
1232 }
1233 }
1234}
1235
1236fn preprocess_arithmetic(source: &str) -> Result<ArithmeticPreprocessResult, LexerError> {
1250 let mut result = String::with_capacity(source.len());
1251 let mut arithmetics: Vec<(String, String)> = Vec::new();
1252 let mut replacements: Vec<SpanReplacement> = Vec::new();
1253 let mut source_pos: usize = 0;
1254 let chars_vec: Vec<char> = source.chars().collect();
1255 let mut i = 0;
1256
1257 let mut in_double_quote = false;
1260
1261 while i < chars_vec.len() {
1262 let ch = chars_vec[i];
1263
1264 if !in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1266 result.push(ch);
1267 result.push(chars_vec[i + 1]);
1268 source_pos += ch.len_utf8() + chars_vec[i + 1].len_utf8();
1269 i += 2;
1270 continue;
1271 }
1272
1273 if ch == '\'' && !in_double_quote {
1275 result.push(ch);
1276 i += 1;
1277 source_pos += 1;
1278 while i < chars_vec.len() && chars_vec[i] != '\'' {
1279 result.push(chars_vec[i]);
1280 source_pos += chars_vec[i].len_utf8();
1281 i += 1;
1282 }
1283 if i < chars_vec.len() {
1284 result.push(chars_vec[i]); source_pos += 1;
1286 i += 1;
1287 }
1288 continue;
1289 }
1290
1291 if ch == '"' {
1293 in_double_quote = !in_double_quote;
1294 result.push(ch);
1295 i += 1;
1296 source_pos += 1;
1297 continue;
1298 }
1299
1300 if in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1302 let next = chars_vec[i + 1];
1303 if next == '"' || next == '\\' || next == '$' || next == '`' {
1304 result.push(ch);
1305 result.push(next);
1306 source_pos += ch.len_utf8() + next.len_utf8();
1307 i += 2;
1308 continue;
1309 }
1310 }
1311
1312 if ch == '#' && !in_double_quote {
1320 while i < chars_vec.len() && chars_vec[i] != '\n' && chars_vec[i] != '\r' {
1321 result.push(chars_vec[i]);
1322 source_pos += chars_vec[i].len_utf8();
1323 i += 1;
1324 }
1325 continue;
1326 }
1327
1328 if ch == '$' && i + 1 < chars_vec.len() && chars_vec[i + 1] == '('
1330 && !(i + 2 < chars_vec.len() && chars_vec[i + 2] == '(')
1331 {
1332 skip_command_substitution(&chars_vec, &mut i, &mut source_pos, &mut result);
1333 continue;
1334 }
1335
1336 if ch == '$' && i + 2 < chars_vec.len() && chars_vec[i + 1] == '(' && chars_vec[i + 2] == '(' {
1338 let arith_start_pos = result.len();
1339 let original_start = source_pos;
1340
1341 i += 3;
1343 source_pos += 3;
1344
1345 let mut expr = String::new();
1347 let mut paren_depth: usize = 0;
1348
1349 while i < chars_vec.len() {
1350 let c = chars_vec[i];
1351 match c {
1352 '(' => {
1353 paren_depth += 1;
1354 if paren_depth > MAX_PAREN_DEPTH {
1355 return Err(LexerError::NestingTooDeep);
1356 }
1357 expr.push('(');
1358 i += 1;
1359 source_pos += c.len_utf8();
1360 }
1361 ')' => {
1362 if paren_depth > 0 {
1363 paren_depth -= 1;
1364 expr.push(')');
1365 i += 1;
1366 source_pos += 1;
1367 } else if i + 1 < chars_vec.len() && chars_vec[i + 1] == ')' {
1368 i += 2;
1370 source_pos += 2;
1371 break;
1372 } else {
1373 expr.push(')');
1375 i += 1;
1376 source_pos += 1;
1377 }
1378 }
1379 _ => {
1380 expr.push(c);
1381 i += 1;
1382 source_pos += c.len_utf8();
1383 }
1384 }
1385 }
1386
1387 let original_len = source_pos - original_start;
1389
1390 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1392 let marker_len = marker.len();
1393
1394 replacements.push(SpanReplacement {
1396 preprocessed_pos: arith_start_pos,
1397 marker_len,
1398 original_len,
1399 });
1400
1401 arithmetics.push((marker.clone(), expr));
1402 result.push_str(&marker);
1403 } else {
1404 result.push(ch);
1405 i += 1;
1406 source_pos += ch.len_utf8();
1407 }
1408 }
1409
1410 Ok(ArithmeticPreprocessResult {
1411 text: result,
1412 arithmetics,
1413 replacements,
1414 })
1415}
1416
1417#[derive(Debug, Clone)]
1432struct HeredocReplacement {
1433 marker: String,
1434 body: String,
1435 literal: bool,
1436 strip_tabs: bool,
1437 body_start_offset: usize,
1438}
1439
1440fn preprocess_heredocs(source: &str) -> Result<(String, Vec<HeredocReplacement>), Spanned<LexerError>> {
1452 let mut result = String::with_capacity(source.len());
1453 let mut heredocs: Vec<HeredocReplacement> = Vec::new();
1454 let chars_vec: Vec<char> = source.chars().collect();
1455 let mut i = 0;
1456 let mut pos: usize = 0;
1460
1461 while i < chars_vec.len() {
1462 let ch = chars_vec[i];
1463
1464 if ch == '<'
1468 && chars_vec.get(i + 1) == Some(&'<')
1469 && chars_vec.get(i + 2) == Some(&'<')
1470 {
1471 result.push_str("<<<");
1472 i += 3;
1473 pos += 3;
1474 continue;
1475 }
1476
1477 if ch == '<' && chars_vec.get(i + 1) == Some(&'<') {
1479 let introducer_start = pos;
1482 i += 2; pos += 2;
1484
1485 let strip_tabs = chars_vec.get(i) == Some(&'-');
1487 if strip_tabs {
1488 i += 1;
1489 pos += 1;
1490 }
1491
1492 while let Some(&c) = chars_vec.get(i) {
1494 if c == ' ' || c == '\t' {
1495 i += 1;
1496 pos += 1;
1497 } else {
1498 break;
1499 }
1500 }
1501
1502 let mut delimiter = String::new();
1504 let quoted = chars_vec.get(i) == Some(&'\'') || chars_vec.get(i) == Some(&'"');
1505 let quote_char = if quoted {
1506 let q = chars_vec.get(i).copied();
1507 i += 1;
1508 pos += 1;
1509 q
1510 } else {
1511 None
1512 };
1513
1514 while let Some(&c) = chars_vec.get(i) {
1515 if quoted {
1516 if Some(c) == quote_char {
1517 i += 1; pos += 1;
1519 break;
1520 }
1521 } else if c.is_whitespace() || c == '\n' || c == '\r' {
1522 break;
1523 }
1524 delimiter.push(c);
1525 i += 1;
1526 pos += c.len_utf8();
1527 }
1528
1529 if delimiter.is_empty() {
1530 result.push_str("<<");
1532 if strip_tabs {
1533 result.push('-');
1534 }
1535 continue;
1536 }
1537
1538 let mut after_delimiter = String::new();
1541 while let Some(&c) = chars_vec.get(i) {
1542 if c == '\n' {
1543 i += 1;
1544 pos += 1;
1545 break;
1546 } else if c == '\r' {
1547 i += 1;
1548 pos += 1;
1549 if chars_vec.get(i) == Some(&'\n') {
1550 i += 1;
1551 pos += 1;
1552 }
1553 break;
1554 }
1555 after_delimiter.push(c);
1556 i += 1;
1557 pos += c.len_utf8();
1558 }
1559
1560 let body_start_offset = pos;
1566 let mut content = String::new();
1567 let mut current_line = String::new();
1568
1569 loop {
1570 let next = chars_vec.get(i).copied();
1571 match next {
1572 Some('\n') => {
1573 i += 1;
1574 pos += 1;
1575 let trimmed = if strip_tabs {
1577 current_line.trim_start_matches('\t')
1578 } else {
1579 ¤t_line
1580 };
1581 if trimmed == delimiter {
1582 break;
1584 }
1585 content.push_str(¤t_line);
1587 content.push('\n');
1588 current_line.clear();
1589 }
1590 Some('\r') => {
1591 i += 1;
1592 pos += 1;
1593 let crlf = chars_vec.get(i) == Some(&'\n');
1599 if crlf {
1600 i += 1;
1601 pos += 1;
1602 }
1603 let trimmed = if strip_tabs {
1604 current_line.trim_start_matches('\t')
1605 } else {
1606 ¤t_line
1607 };
1608 if trimmed == delimiter {
1609 break;
1610 }
1611 content.push_str(¤t_line);
1612 content.push_str(if crlf { "\r\n" } else { "\r" });
1613 current_line.clear();
1614 }
1615 Some(c) => {
1616 current_line.push(c);
1617 i += 1;
1618 pos += c.len_utf8();
1619 }
1620 None => {
1621 let trimmed = if strip_tabs {
1624 current_line.trim_start_matches('\t')
1625 } else {
1626 ¤t_line
1627 };
1628 if trimmed == delimiter {
1629 break;
1630 }
1631 let span_end = introducer_start
1636 + 2
1637 + if strip_tabs { 1 } else { 0 }
1638 + delimiter.len();
1639 return Err(Spanned::new(
1640 LexerError::UnterminatedHeredoc {
1641 delimiter: delimiter.clone(),
1642 },
1643 introducer_start..span_end,
1644 ));
1645 }
1646 }
1647 }
1648
1649 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1651 heredocs.push(HeredocReplacement {
1652 marker: marker.clone(),
1653 body: content,
1654 literal: quoted,
1655 strip_tabs,
1656 body_start_offset,
1657 });
1658
1659 result.push_str("<<");
1662 result.push_str(&marker);
1663 result.push_str(&after_delimiter);
1664 result.push('\n');
1665 } else {
1666 result.push(ch);
1667 i += 1;
1668 pos += ch.len_utf8();
1669 }
1670 }
1671
1672 Ok((result, heredocs))
1673}
1674
1675fn mergeable_text(token: &Token) -> Option<String> {
1680 match token {
1681 Token::Ident(s) => Some(s.clone()),
1682 Token::NumberIdent(s) => Some(s.clone()),
1683 Token::DashNumWord(s) => Some(s.clone()),
1684 Token::AtWord(s) => Some(s.clone()),
1685 Token::DottedIdent(s) => Some(s.clone()),
1686 Token::Colon => Some(":".to_string()),
1687 Token::Int(n) => Some(n.to_string()),
1688 Token::Path(p) => Some(p.clone()),
1689 Token::Float(f) => Some(f.to_string()),
1690 _ => None,
1691 }
1692}
1693
1694fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1703 if tokens.is_empty() {
1704 return tokens;
1705 }
1706
1707 let mut result = Vec::with_capacity(tokens.len());
1708 let mut run: Vec<&Spanned<Token>> = Vec::new();
1709
1710 for token in &tokens {
1711 if run.is_empty() {
1712 if mergeable_text(&token.token).is_some() {
1713 run.push(token);
1714 } else {
1715 result.push(token.clone());
1716 }
1717 continue;
1718 }
1719
1720 let Some(last) = run.last() else { unreachable!() };
1723 let adjacent = last.span.end == token.span.start;
1724
1725 if adjacent && mergeable_text(&token.token).is_some() {
1726 run.push(token);
1727 } else {
1728 flush_colon_run(&mut run, &mut result);
1729 if mergeable_text(&token.token).is_some() {
1730 run.push(token);
1731 } else {
1732 result.push(token.clone());
1733 }
1734 }
1735 }
1736
1737 flush_colon_run(&mut run, &mut result);
1738
1739 result
1740}
1741
1742fn flush_colon_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1744 if run.is_empty() {
1745 return;
1746 }
1747
1748 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
1749
1750 if run.len() >= 2 && has_colon {
1751 let text: String = run
1752 .iter()
1753 .filter_map(|t| mergeable_text(&t.token))
1754 .collect();
1755 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1757 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1758 result.push(Spanned::new(Token::Ident(text), start..end));
1759 } else {
1760 for t in run.iter() {
1761 result.push((*t).clone());
1762 }
1763 }
1764
1765 run.clear();
1766}
1767
1768fn glob_mergeable_text(token: &Token) -> Option<String> {
1773 match token {
1774 Token::Star => Some("*".to_string()),
1775 Token::Question => Some("?".to_string()),
1776 Token::Dot => Some(".".to_string()),
1777 Token::DotDot => Some("..".to_string()),
1778 Token::Ident(s) => Some(s.clone()),
1779 Token::NumberIdent(s) => Some(s.clone()),
1780 Token::DashNumWord(s) => Some(s.clone()),
1781 Token::AtWord(s) => Some(s.clone()),
1782 Token::DottedIdent(s) => Some(s.clone()),
1783 Token::Path(s) => Some(s.clone()),
1784 Token::Int(n) => Some(n.to_string()),
1785 Token::LBracket => Some("[".to_string()),
1786 Token::RBracket => Some("]".to_string()),
1787 Token::Bang => Some("!".to_string()),
1788 Token::DotSlashPath(s) => Some(s.clone()),
1789 Token::RelativePath(s) => Some(s.clone()),
1790 Token::TildePath(s) => Some(s.clone()),
1791 Token::Tilde => Some("~".to_string()),
1792 Token::LBrace => Some("{".to_string()),
1793 Token::RBrace => Some("}".to_string()),
1794 Token::Comma => Some(",".to_string()),
1795 _ => None,
1796 }
1797}
1798
1799fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1828 if tokens.len() < 2 {
1829 return tokens;
1830 }
1831
1832 let mut result = Vec::with_capacity(tokens.len());
1833 let mut i = 0;
1834
1835 while i < tokens.len() {
1836 let token = &tokens[i];
1837
1838 if let Token::ShortFlag(flag_name) = &token.token {
1840 let mut fused = flag_name.clone();
1842 let mut end_span = token.span.end;
1843 let mut j = i + 1;
1844
1845 while let Some(next) = tokens.get(j) {
1846 if next.span.start == end_span {
1847 if let Token::Colon = &next.token {
1848 fused.push(':');
1849 end_span = next.span.end;
1850 j += 1;
1851 continue;
1852 }
1853 }
1854 break;
1855 }
1856
1857 if j > i + 1 {
1858 let span = token.span.start..end_span;
1860 result.push(Spanned::new(Token::ShortFlag(fused), span));
1861 i = j;
1862 continue;
1863 }
1864 }
1865
1866 result.push(token.clone());
1867 i += 1;
1868 }
1869
1870 result
1871}
1872
1873fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1881 if tokens.is_empty() {
1882 return tokens;
1883 }
1884
1885 let mut result = Vec::with_capacity(tokens.len());
1886 let mut run: Vec<&Spanned<Token>> = Vec::new();
1887
1888 for token in &tokens {
1889 if run.is_empty() {
1890 if glob_mergeable_text(&token.token).is_some() {
1891 run.push(token);
1892 } else {
1893 result.push(token.clone());
1894 }
1895 continue;
1896 }
1897
1898 let Some(last) = run.last() else { unreachable!() };
1900 let adjacent = last.span.end == token.span.start;
1901
1902 if adjacent && glob_mergeable_text(&token.token).is_some() {
1903 run.push(token);
1904 } else {
1905 flush_glob_run(&mut run, &mut result);
1906 if glob_mergeable_text(&token.token).is_some() {
1907 run.push(token);
1908 } else {
1909 result.push(token.clone());
1910 }
1911 }
1912 }
1913
1914 flush_glob_run(&mut run, &mut result);
1915
1916 result
1917}
1918
1919fn flush_glob_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1921 if run.is_empty() {
1922 return;
1923 }
1924
1925 let has_glob = run.iter().any(|t| {
1926 matches!(t.token, Token::Star | Token::Question)
1927 }) || (run.iter().any(|t| matches!(t.token, Token::LBracket))
1928 && run.iter().any(|t| matches!(t.token, Token::RBracket)));
1929
1930 if run.len() >= 2 && has_glob {
1931 let text: String = run
1932 .iter()
1933 .filter_map(|t| glob_mergeable_text(&t.token))
1934 .collect();
1935 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1936 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1937 result.push(Spanned::new(Token::GlobWord(text), start..end));
1938 } else {
1939 for t in run.iter() {
1940 result.push((*t).clone());
1941 }
1942 }
1943
1944 run.clear();
1945}
1946
1947pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
1957 let arith_result = preprocess_arithmetic(source)
1959 .map_err(|e| vec![Spanned::new(e, 0..source.len())])?;
1960
1961 let span_replacements = arith_result.replacements;
1965 let (preprocessed, heredocs) = preprocess_heredocs(&arith_result.text)
1966 .map_err(|e| {
1967 let span = correct_span(e.span, &span_replacements);
1968 vec![Spanned::new(e.token, span)]
1969 })?;
1970
1971 let lexer = Token::lexer(&preprocessed);
1972 let mut tokens = Vec::new();
1973 let mut errors = Vec::new();
1974
1975 for (result, span) in lexer.spanned() {
1976 let corrected_span = correct_span(span, &span_replacements);
1978 match result {
1979 Ok(token) => {
1980 if !matches!(token, Token::Comment | Token::LineContinuation) {
1982 tokens.push(Spanned::new(token, corrected_span));
1983 }
1984 }
1985 Err(err) => {
1986 errors.push(Spanned::new(err, corrected_span));
1987 }
1988 }
1989 }
1990
1991 if !errors.is_empty() {
1992 return Err(errors);
1993 }
1994
1995 let mut final_tokens = Vec::with_capacity(tokens.len());
1997 let mut i = 0;
1998
1999 while i < tokens.len() {
2000 if let Token::Ident(ref name) = tokens[i].token
2002 && name.starts_with("__KAISH_ARITH_") && name.ends_with("__")
2003 && let Some((_, expr)) = arith_result.arithmetics.iter().find(|(marker, _)| marker == name) {
2004 final_tokens.push(Spanned::new(Token::Arithmetic(expr.clone()), tokens[i].span.clone()));
2005 i += 1;
2006 continue;
2007 }
2008
2009 if matches!(tokens[i].token, Token::HereDocStart) {
2011 if i + 1 < tokens.len()
2013 && let Token::Ident(ref name) = tokens[i + 1].token
2014 && name.starts_with("__KAISH_HEREDOC_") && name.ends_with("__") {
2015 if let Some(hd) = heredocs.iter().find(|h| h.marker == *name) {
2017 let mut content = hd.body.clone();
2029 for (marker, expr) in &arith_result.arithmetics {
2030 if content.contains(marker) {
2031 let replacement = if hd.literal {
2032 format!("$(({}))", expr)
2033 } else {
2034 format!("${{__ARITH:{}__}}", expr)
2035 };
2036 content = content.replace(marker, &replacement);
2037 }
2038 }
2039 final_tokens.push(Spanned::new(Token::HereDocStart, tokens[i].span.clone()));
2040 final_tokens.push(Spanned::new(
2041 Token::HereDoc(HereDocData {
2042 content,
2043 literal: hd.literal,
2044 strip_tabs: hd.strip_tabs,
2045 body_start_offset: hd.body_start_offset,
2046 }),
2047 tokens[i + 1].span.clone(),
2048 ));
2049 i += 2;
2050 continue;
2051 }
2052 }
2053 }
2054
2055 let token = if let Token::String(ref s) = tokens[i].token {
2057 let mut new_content = s.clone();
2059 for (marker, expr) in &arith_result.arithmetics {
2060 if new_content.contains(marker) {
2061 new_content = new_content.replace(marker, &format!("${{__ARITH:{}__}}", expr));
2064 }
2065 }
2066 if new_content != *s {
2067 Spanned::new(Token::String(new_content), tokens[i].span.clone())
2068 } else {
2069 tokens[i].clone()
2070 }
2071 } else {
2072 tokens[i].clone()
2073 };
2074 final_tokens.push(token);
2075 i += 1;
2076 }
2077
2078 Ok(merge_glob_adjacent(merge_colon_adjacent(
2079 merge_flag_metachar_adjacent(final_tokens),
2080 )))
2081}
2082
2083pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2087 let lexer = Token::lexer(source);
2088 let mut tokens = Vec::new();
2089 let mut errors = Vec::new();
2090
2091 for (result, span) in lexer.spanned() {
2092 match result {
2093 Ok(token) => {
2094 tokens.push(Spanned::new(token, span));
2095 }
2096 Err(err) => {
2097 errors.push(Spanned::new(err, span));
2098 }
2099 }
2100 }
2101
2102 if errors.is_empty() {
2103 Ok(tokens)
2104 } else {
2105 Err(errors)
2106 }
2107}
2108
2109pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2111 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2113 return Err(LexerError::UnterminatedString);
2114 }
2115
2116 let inner = &source[1..source.len() - 1];
2117 let mut result = String::with_capacity(inner.len());
2118 let mut chars = inner.chars().peekable();
2119
2120 while let Some(ch) = chars.next() {
2121 if ch == '\\' {
2122 match chars.next() {
2123 Some('n') => result.push('\n'),
2124 Some('t') => result.push('\t'),
2125 Some('r') => result.push('\r'),
2126 Some('\\') => result.push('\\'),
2127 Some('"') => result.push('"'),
2128 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2131 Some('u') => {
2132 let mut hex = String::with_capacity(4);
2134 for _ in 0..4 {
2135 match chars.next() {
2136 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2137 _ => return Err(LexerError::InvalidEscape),
2138 }
2139 }
2140 let codepoint = u32::from_str_radix(&hex, 16)
2141 .map_err(|_| LexerError::InvalidEscape)?;
2142 let ch = char::from_u32(codepoint)
2143 .ok_or(LexerError::InvalidEscape)?;
2144 result.push(ch);
2145 }
2146 Some(next) => {
2148 result.push('\\');
2149 result.push(next);
2150 }
2151 None => return Err(LexerError::InvalidEscape),
2152 }
2153 } else {
2154 result.push(ch);
2155 }
2156 }
2157
2158 Ok(result)
2159}
2160
2161pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
2164 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
2166 return Err(LexerError::UnterminatedVarRef);
2167 }
2168
2169 let inner = &source[2..source.len() - 1];
2170
2171 if inner == "?" {
2173 return Ok(vec!["?".to_string()]);
2174 }
2175
2176 let mut segments = Vec::new();
2177 let mut current = String::new();
2178 let mut chars = inner.chars().peekable();
2179
2180 while let Some(ch) = chars.next() {
2181 match ch {
2182 '.' => {
2183 if !current.is_empty() {
2184 segments.push(current.clone());
2185 current.clear();
2186 }
2187 }
2188 '[' => {
2189 if !current.is_empty() {
2190 segments.push(current.clone());
2191 current.clear();
2192 }
2193 let mut index = String::from("[");
2195 while let Some(&c) = chars.peek() {
2196 if let Some(c) = chars.next() {
2197 index.push(c);
2198 }
2199 if c == ']' {
2200 break;
2201 }
2202 }
2203 segments.push(index);
2204 }
2205 _ => {
2206 current.push(ch);
2207 }
2208 }
2209 }
2210
2211 if !current.is_empty() {
2212 segments.push(current);
2213 }
2214
2215 Ok(segments)
2216}
2217
2218pub fn parse_int(source: &str) -> Result<i64, LexerError> {
2220 source.parse().map_err(|_| LexerError::InvalidNumber)
2221}
2222
2223pub fn parse_float(source: &str) -> Result<f64, LexerError> {
2225 source.parse().map_err(|_| LexerError::InvalidNumber)
2226}
2227
2228#[cfg(test)]
2229#[allow(clippy::approx_constant)]
2230mod tests {
2231 use super::*;
2232
2233 fn lex(source: &str) -> Vec<Token> {
2234 tokenize(source)
2235 .expect("lexer should succeed")
2236 .into_iter()
2237 .map(|s| s.token)
2238 .collect()
2239 }
2240
2241 #[test]
2246 fn keywords() {
2247 assert_eq!(lex("set"), vec![Token::Set]);
2248 assert_eq!(lex("if"), vec![Token::If]);
2249 assert_eq!(lex("then"), vec![Token::Then]);
2250 assert_eq!(lex("else"), vec![Token::Else]);
2251 assert_eq!(lex("elif"), vec![Token::Elif]);
2252 assert_eq!(lex("fi"), vec![Token::Fi]);
2253 assert_eq!(lex("for"), vec![Token::For]);
2254 assert_eq!(lex("in"), vec![Token::In]);
2255 assert_eq!(lex("do"), vec![Token::Do]);
2256 assert_eq!(lex("done"), vec![Token::Done]);
2257 assert_eq!(lex("case"), vec![Token::Case]);
2258 assert_eq!(lex("esac"), vec![Token::Esac]);
2259 assert_eq!(lex("function"), vec![Token::Function]);
2260 assert_eq!(lex("true"), vec![Token::True]);
2261 assert_eq!(lex("false"), vec![Token::False]);
2262 }
2263
2264 #[test]
2265 fn double_semicolon() {
2266 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
2267 assert_eq!(lex("echo \"hi\";;"), vec![
2269 Token::Ident("echo".to_string()),
2270 Token::String("hi".to_string()),
2271 Token::DoubleSemi,
2272 ]);
2273 }
2274
2275 #[test]
2276 fn type_keywords() {
2277 assert_eq!(lex("string"), vec![Token::TypeString]);
2278 assert_eq!(lex("int"), vec![Token::TypeInt]);
2279 assert_eq!(lex("float"), vec![Token::TypeFloat]);
2280 assert_eq!(lex("bool"), vec![Token::TypeBool]);
2281 }
2282
2283 #[test]
2288 fn single_char_operators() {
2289 assert_eq!(lex("="), vec![Token::Eq]);
2290 assert_eq!(lex("|"), vec![Token::Pipe]);
2291 assert_eq!(lex("&"), vec![Token::Amp]);
2292 assert_eq!(lex(">"), vec![Token::Gt]);
2293 assert_eq!(lex("<"), vec![Token::Lt]);
2294 assert_eq!(lex(";"), vec![Token::Semi]);
2295 assert_eq!(lex(":"), vec![Token::Colon]);
2296 assert_eq!(lex(","), vec![Token::Comma]);
2297 assert_eq!(lex("."), vec![Token::Dot]);
2298 }
2299
2300 #[test]
2301 fn multi_char_operators() {
2302 assert_eq!(lex("&&"), vec![Token::And]);
2303 assert_eq!(lex("||"), vec![Token::Or]);
2304 assert_eq!(lex("=="), vec![Token::EqEq]);
2305 assert_eq!(lex("!="), vec![Token::NotEq]);
2306 assert_eq!(lex("=~"), vec![Token::Match]);
2307 assert_eq!(lex("!~"), vec![Token::NotMatch]);
2308 assert_eq!(lex(">="), vec![Token::GtEq]);
2309 assert_eq!(lex("<="), vec![Token::LtEq]);
2310 assert_eq!(lex(">>"), vec![Token::GtGt]);
2311 assert_eq!(lex("2>"), vec![Token::Stderr]);
2312 assert_eq!(lex("&>"), vec![Token::Both]);
2313 }
2314
2315 #[test]
2316 fn brackets() {
2317 assert_eq!(lex("{"), vec![Token::LBrace]);
2318 assert_eq!(lex("}"), vec![Token::RBrace]);
2319 assert_eq!(lex("["), vec![Token::LBracket]);
2320 assert_eq!(lex("]"), vec![Token::RBracket]);
2321 assert_eq!(lex("("), vec![Token::LParen]);
2322 assert_eq!(lex(")"), vec![Token::RParen]);
2323 }
2324
2325 #[test]
2330 fn integers() {
2331 assert_eq!(lex("0"), vec![Token::Int(0)]);
2332 assert_eq!(lex("42"), vec![Token::Int(42)]);
2333 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
2334 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
2335 }
2336
2337 #[test]
2338 fn floats() {
2339 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
2340 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
2341 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
2342 }
2343
2344 #[test]
2345 fn strings() {
2346 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
2347 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
2348 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
2350 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
2351 }
2352
2353 #[test]
2354 fn var_refs() {
2355 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
2356 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
2357 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
2358 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
2359 }
2360
2361 #[test]
2366 fn identifiers() {
2367 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
2368 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
2369 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
2370 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
2371 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
2372 }
2373
2374 #[test]
2375 fn keyword_prefix_identifiers() {
2376 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
2378 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
2379 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
2380 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
2381 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
2382 }
2383
2384 #[test]
2389 fn assignment() {
2390 assert_eq!(
2391 lex("set X = 5"),
2392 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2393 );
2394 }
2395
2396 #[test]
2397 fn command_simple() {
2398 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
2399 assert_eq!(
2400 lex(r#"echo "hello""#),
2401 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
2402 );
2403 }
2404
2405 #[test]
2406 fn command_with_args() {
2407 assert_eq!(
2408 lex("cmd arg1 arg2"),
2409 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
2410 );
2411 }
2412
2413 #[test]
2414 fn command_with_named_args() {
2415 assert_eq!(
2416 lex("cmd key=value"),
2417 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
2418 );
2419 }
2420
2421 #[test]
2422 fn pipeline() {
2423 assert_eq!(
2424 lex("a | b | c"),
2425 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
2426 );
2427 }
2428
2429 #[test]
2430 fn if_statement() {
2431 assert_eq!(
2432 lex("if true; then echo; fi"),
2433 vec![
2434 Token::If,
2435 Token::True,
2436 Token::Semi,
2437 Token::Then,
2438 Token::Ident("echo".to_string()),
2439 Token::Semi,
2440 Token::Fi
2441 ]
2442 );
2443 }
2444
2445 #[test]
2446 fn for_loop() {
2447 assert_eq!(
2448 lex("for X in items; do echo; done"),
2449 vec![
2450 Token::For,
2451 Token::Ident("X".to_string()),
2452 Token::In,
2453 Token::Ident("items".to_string()),
2454 Token::Semi,
2455 Token::Do,
2456 Token::Ident("echo".to_string()),
2457 Token::Semi,
2458 Token::Done
2459 ]
2460 );
2461 }
2462
2463 #[test]
2468 fn whitespace_ignored() {
2469 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
2470 }
2471
2472 #[test]
2473 fn newlines_preserved() {
2474 let tokens = lex("a\nb");
2475 assert_eq!(
2476 tokens,
2477 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2478 );
2479 }
2480
2481 #[test]
2482 fn multiple_newlines() {
2483 let tokens = lex("a\n\n\nb");
2484 assert_eq!(
2485 tokens,
2486 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
2487 );
2488 }
2489
2490 #[test]
2495 fn comments_skipped() {
2496 assert_eq!(lex("# comment"), vec![]);
2497 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
2498 assert_eq!(
2499 lex("a # comment\nb"),
2500 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2501 );
2502 }
2503
2504 #[test]
2505 fn comments_preserved_when_requested() {
2506 let tokens = tokenize_with_comments("a # comment")
2507 .expect("should succeed")
2508 .into_iter()
2509 .map(|s| s.token)
2510 .collect::<Vec<_>>();
2511 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
2512 }
2513
2514 #[test]
2519 fn parse_simple_string() {
2520 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
2521 }
2522
2523 #[test]
2524 fn parse_string_with_escapes() {
2525 assert_eq!(
2526 parse_string_literal(r#""hello\nworld""#).expect("ok"),
2527 "hello\nworld"
2528 );
2529 assert_eq!(
2530 parse_string_literal(r#""tab\there""#).expect("ok"),
2531 "tab\there"
2532 );
2533 assert_eq!(
2534 parse_string_literal(r#""quote\"here""#).expect("ok"),
2535 "quote\"here"
2536 );
2537 }
2538
2539 #[test]
2540 fn parse_string_with_unicode() {
2541 assert_eq!(
2542 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
2543 "emoji ❤"
2544 );
2545 }
2546
2547 #[test]
2548 fn parse_string_with_escaped_dollar() {
2549 assert_eq!(
2552 parse_string_literal(r#""\$VAR""#).expect("ok"),
2553 "__KAISH_ESCAPED_DOLLAR__VAR"
2554 );
2555 assert_eq!(
2556 parse_string_literal(r#""cost: \$100""#).expect("ok"),
2557 "cost: __KAISH_ESCAPED_DOLLAR__100"
2558 );
2559 }
2560
2561 #[test]
2566 fn parse_simple_var() {
2567 assert_eq!(
2568 parse_var_ref("${X}").expect("ok"),
2569 vec!["X"]
2570 );
2571 }
2572
2573 #[test]
2574 fn parse_var_with_field() {
2575 assert_eq!(
2576 parse_var_ref("${VAR.field}").expect("ok"),
2577 vec!["VAR", "field"]
2578 );
2579 }
2580
2581 #[test]
2582 fn parse_var_with_index() {
2583 assert_eq!(
2584 parse_var_ref("${VAR[0]}").expect("ok"),
2585 vec!["VAR", "[0]"]
2586 );
2587 }
2588
2589 #[test]
2590 fn parse_var_nested() {
2591 assert_eq!(
2592 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
2593 vec!["VAR", "field", "[0]", "nested"]
2594 );
2595 }
2596
2597 #[test]
2598 fn parse_last_result() {
2599 assert_eq!(
2600 parse_var_ref("${?}").expect("ok"),
2601 vec!["?"]
2602 );
2603 }
2604
2605 #[test]
2610 fn parse_integers() {
2611 assert_eq!(parse_int("0").expect("ok"), 0);
2612 assert_eq!(parse_int("42").expect("ok"), 42);
2613 assert_eq!(parse_int("-1").expect("ok"), -1);
2614 }
2615
2616 #[test]
2617 fn parse_floats() {
2618 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
2619 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
2620 }
2621
2622 #[test]
2627 fn empty_input() {
2628 assert_eq!(lex(""), vec![]);
2629 }
2630
2631 #[test]
2632 fn only_whitespace() {
2633 assert_eq!(lex(" \t\t "), vec![]);
2634 }
2635
2636 #[test]
2637 fn json_array() {
2638 assert_eq!(
2639 lex(r#"[1, 2, 3]"#),
2640 vec![
2641 Token::LBracket,
2642 Token::Int(1),
2643 Token::Comma,
2644 Token::Int(2),
2645 Token::Comma,
2646 Token::Int(3),
2647 Token::RBracket
2648 ]
2649 );
2650 }
2651
2652 #[test]
2653 fn json_object() {
2654 assert_eq!(
2655 lex(r#"{"key": "value"}"#),
2656 vec![
2657 Token::LBrace,
2658 Token::String("key".to_string()),
2659 Token::Colon,
2660 Token::String("value".to_string()),
2661 Token::RBrace
2662 ]
2663 );
2664 }
2665
2666 #[test]
2667 fn redirect_operators() {
2668 assert_eq!(
2669 lex("cmd > file"),
2670 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
2671 );
2672 assert_eq!(
2673 lex("cmd >> file"),
2674 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
2675 );
2676 assert_eq!(
2677 lex("cmd 2> err"),
2678 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
2679 );
2680 assert_eq!(
2681 lex("cmd &> all"),
2682 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
2683 );
2684 }
2685
2686 #[test]
2687 fn background_job() {
2688 assert_eq!(
2689 lex("cmd &"),
2690 vec![Token::Ident("cmd".to_string()), Token::Amp]
2691 );
2692 }
2693
2694 #[test]
2695 fn command_substitution() {
2696 assert_eq!(
2697 lex("$(cmd)"),
2698 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
2699 );
2700 assert_eq!(
2701 lex("$(cmd arg)"),
2702 vec![
2703 Token::CmdSubstStart,
2704 Token::Ident("cmd".to_string()),
2705 Token::Ident("arg".to_string()),
2706 Token::RParen
2707 ]
2708 );
2709 assert_eq!(
2710 lex("$(a | b)"),
2711 vec![
2712 Token::CmdSubstStart,
2713 Token::Ident("a".to_string()),
2714 Token::Pipe,
2715 Token::Ident("b".to_string()),
2716 Token::RParen
2717 ]
2718 );
2719 }
2720
2721 #[test]
2722 fn complex_pipeline() {
2723 assert_eq!(
2724 lex(r#"cat file | grep pattern="foo" | head count=10"#),
2725 vec![
2726 Token::Ident("cat".to_string()),
2727 Token::Ident("file".to_string()),
2728 Token::Pipe,
2729 Token::Ident("grep".to_string()),
2730 Token::Ident("pattern".to_string()),
2731 Token::Eq,
2732 Token::String("foo".to_string()),
2733 Token::Pipe,
2734 Token::Ident("head".to_string()),
2735 Token::Ident("count".to_string()),
2736 Token::Eq,
2737 Token::Int(10),
2738 ]
2739 );
2740 }
2741
2742 #[test]
2747 fn short_flag() {
2748 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2749 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
2750 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
2751 }
2752
2753 #[test]
2754 fn short_flag_combined() {
2755 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
2757 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
2758 }
2759
2760 #[test]
2761 fn job_spec_lexes_as_one_token() {
2762 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
2764 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
2765 assert_eq!(
2766 lex("wait %1 %2"),
2767 vec![
2768 Token::Ident("wait".to_string()),
2769 Token::JobSpec("%1".to_string()),
2770 Token::JobSpec("%2".to_string()),
2771 ]
2772 );
2773 }
2774
2775 #[test]
2776 fn short_flag_with_internal_hyphens_is_one_token() {
2777 assert_eq!(
2781 lex("-not-a-flag"),
2782 vec![Token::ShortFlag("not-a-flag".to_string())]
2783 );
2784 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2788 assert_eq!(lex("-"), vec![Token::MinusAlone]);
2789 }
2790
2791 #[test]
2792 fn long_flag() {
2793 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
2794 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
2795 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
2796 }
2797
2798 #[test]
2799 fn double_dash() {
2800 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2802 }
2803
2804 #[test]
2805 fn flags_vs_negative_numbers() {
2806 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
2808 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2810 assert_eq!(
2813 lex("-1 a"),
2814 vec![Token::Int(-1), Token::Ident("a".to_string())]
2815 );
2816 }
2817
2818 #[test]
2819 fn command_with_flags() {
2820 assert_eq!(
2821 lex("ls -l"),
2822 vec![
2823 Token::Ident("ls".to_string()),
2824 Token::ShortFlag("l".to_string()),
2825 ]
2826 );
2827 assert_eq!(
2828 lex("git commit -m"),
2829 vec![
2830 Token::Ident("git".to_string()),
2831 Token::Ident("commit".to_string()),
2832 Token::ShortFlag("m".to_string()),
2833 ]
2834 );
2835 assert_eq!(
2836 lex("git push --force"),
2837 vec![
2838 Token::Ident("git".to_string()),
2839 Token::Ident("push".to_string()),
2840 Token::LongFlag("force".to_string()),
2841 ]
2842 );
2843 }
2844
2845 #[test]
2846 fn flag_with_value() {
2847 assert_eq!(
2848 lex(r#"git commit -m "message""#),
2849 vec![
2850 Token::Ident("git".to_string()),
2851 Token::Ident("commit".to_string()),
2852 Token::ShortFlag("m".to_string()),
2853 Token::String("message".to_string()),
2854 ]
2855 );
2856 assert_eq!(
2857 lex(r#"--message="hello""#),
2858 vec![
2859 Token::LongFlag("message".to_string()),
2860 Token::Eq,
2861 Token::String("hello".to_string()),
2862 ]
2863 );
2864 }
2865
2866 #[test]
2867 fn end_of_flags_marker() {
2868 assert_eq!(
2869 lex("git checkout -- file"),
2870 vec![
2871 Token::Ident("git".to_string()),
2872 Token::Ident("checkout".to_string()),
2873 Token::DoubleDash,
2874 Token::Ident("file".to_string()),
2875 ]
2876 );
2877 }
2878
2879 #[test]
2884 fn local_keyword() {
2885 assert_eq!(lex("local"), vec![Token::Local]);
2886 assert_eq!(
2887 lex("local X = 5"),
2888 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2889 );
2890 }
2891
2892 #[test]
2893 fn simple_var_ref() {
2894 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
2895 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
2896 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
2897 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
2898 }
2899
2900 #[test]
2901 fn simple_var_ref_in_command() {
2902 assert_eq!(
2903 lex("echo $NAME"),
2904 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
2905 );
2906 }
2907
2908 #[test]
2909 fn single_quoted_strings() {
2910 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
2911 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
2912 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
2913 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
2915 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
2916 }
2917
2918 #[test]
2919 fn test_brackets() {
2920 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
2922 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
2923 assert_eq!(
2924 lex("[[ -f file ]]"),
2925 vec![
2926 Token::LBracket,
2927 Token::LBracket,
2928 Token::ShortFlag("f".to_string()),
2929 Token::Ident("file".to_string()),
2930 Token::RBracket,
2931 Token::RBracket
2932 ]
2933 );
2934 }
2935
2936 #[test]
2937 fn test_expression_syntax() {
2938 assert_eq!(
2939 lex(r#"[[ $X == "value" ]]"#),
2940 vec![
2941 Token::LBracket,
2942 Token::LBracket,
2943 Token::SimpleVarRef("X".to_string()),
2944 Token::EqEq,
2945 Token::String("value".to_string()),
2946 Token::RBracket,
2947 Token::RBracket
2948 ]
2949 );
2950 }
2951
2952 #[test]
2953 fn bash_style_assignment() {
2954 assert_eq!(
2956 lex(r#"NAME="value""#),
2957 vec![
2958 Token::Ident("NAME".to_string()),
2959 Token::Eq,
2960 Token::String("value".to_string())
2961 ]
2962 );
2963 }
2964
2965 #[test]
2966 fn positional_params() {
2967 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
2968 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
2969 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
2970 assert_eq!(lex("$@"), vec![Token::AllArgs]);
2971 assert_eq!(lex("$#"), vec![Token::ArgCount]);
2972 }
2973
2974 #[test]
2975 fn positional_in_context() {
2976 assert_eq!(
2977 lex("echo $1 $2"),
2978 vec![
2979 Token::Ident("echo".to_string()),
2980 Token::Positional(1),
2981 Token::Positional(2),
2982 ]
2983 );
2984 }
2985
2986 #[test]
2987 fn var_length() {
2988 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
2989 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
2990 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
2991 }
2992
2993 #[test]
2994 fn var_length_in_context() {
2995 assert_eq!(
2996 lex("echo ${#NAME}"),
2997 vec![
2998 Token::Ident("echo".to_string()),
2999 Token::VarLength("NAME".to_string()),
3000 ]
3001 );
3002 }
3003
3004 #[test]
3009 fn plus_flag() {
3010 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
3012 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
3013 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
3014 }
3015
3016 #[test]
3017 fn set_with_plus_flag() {
3018 assert_eq!(
3019 lex("set +e"),
3020 vec![
3021 Token::Set,
3022 Token::PlusFlag("e".to_string()),
3023 ]
3024 );
3025 }
3026
3027 #[test]
3028 fn set_with_multiple_flags() {
3029 assert_eq!(
3030 lex("set -e -u"),
3031 vec![
3032 Token::Set,
3033 Token::ShortFlag("e".to_string()),
3034 Token::ShortFlag("u".to_string()),
3035 ]
3036 );
3037 }
3038
3039 #[test]
3040 fn flags_vs_negative_numbers_edge_cases() {
3041 assert_eq!(
3043 lex("-1 a"),
3044 vec![Token::Int(-1), Token::Ident("a".to_string())]
3045 );
3046 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3048 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3050 }
3051
3052 #[test]
3053 fn single_dash_is_minus_alone() {
3054 let result = tokenize("-").expect("should lex");
3056 assert_eq!(result.len(), 1);
3057 assert!(matches!(result[0].token, Token::MinusAlone));
3058 }
3059
3060 #[test]
3061 fn plus_bare_for_date_format() {
3062 let result = tokenize("+%s").expect("should lex");
3064 assert_eq!(result.len(), 1);
3065 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
3066
3067 let result = tokenize("+%Y-%m-%d").expect("should lex");
3069 assert_eq!(result.len(), 1);
3070 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
3071 }
3072
3073 #[test]
3074 fn plus_flag_still_works() {
3075 let result = tokenize("+e").expect("should lex");
3077 assert_eq!(result.len(), 1);
3078 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
3079 }
3080
3081 #[test]
3082 fn while_keyword_vs_while_loop() {
3083 assert_eq!(lex("while"), vec![Token::While]);
3085 assert_eq!(
3087 lex("while true"),
3088 vec![Token::While, Token::True]
3089 );
3090 }
3091
3092 #[test]
3093 fn control_flow_keywords() {
3094 assert_eq!(lex("break"), vec![Token::Break]);
3095 assert_eq!(lex("continue"), vec![Token::Continue]);
3096 assert_eq!(lex("return"), vec![Token::Return]);
3097 assert_eq!(lex("exit"), vec![Token::Exit]);
3098 }
3099
3100 #[test]
3101 fn control_flow_with_numbers() {
3102 assert_eq!(
3103 lex("break 2"),
3104 vec![Token::Break, Token::Int(2)]
3105 );
3106 assert_eq!(
3107 lex("continue 3"),
3108 vec![Token::Continue, Token::Int(3)]
3109 );
3110 assert_eq!(
3111 lex("exit 1"),
3112 vec![Token::Exit, Token::Int(1)]
3113 );
3114 }
3115
3116 #[test]
3121 fn heredoc_simple() {
3122 let source = "cat <<EOF\nhello\nworld\nEOF";
3123 let tokens = lex(source);
3124 assert_eq!(tokens, vec![
3126 Token::Ident("cat".to_string()),
3127 Token::HereDocStart,
3128 Token::HereDoc(HereDocData {
3129 content: "hello\nworld\n".to_string(),
3130 literal: false,
3131 strip_tabs: false,
3132 body_start_offset: 10,
3133 }),
3134 Token::Newline,
3135 ]);
3136 }
3137
3138 #[test]
3139 fn heredoc_empty() {
3140 let source = "cat <<EOF\nEOF";
3141 let tokens = lex(source);
3142 assert_eq!(tokens, vec![
3143 Token::Ident("cat".to_string()),
3144 Token::HereDocStart,
3145 Token::HereDoc(HereDocData {
3146 content: "".to_string(),
3147 literal: false,
3148 strip_tabs: false,
3149 body_start_offset: 10,
3150 }),
3151 Token::Newline,
3152 ]);
3153 }
3154
3155 #[test]
3156 fn heredoc_with_special_chars() {
3157 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
3158 let tokens = lex(source);
3159 assert_eq!(tokens, vec![
3160 Token::Ident("cat".to_string()),
3161 Token::HereDocStart,
3162 Token::HereDoc(HereDocData {
3163 content: "$VAR and \"quoted\" 'single'\n".to_string(),
3164 literal: false,
3165 strip_tabs: false,
3166 body_start_offset: 10,
3167 }),
3168 Token::Newline,
3169 ]);
3170 }
3171
3172 #[test]
3173 fn heredoc_multiline() {
3174 let source = "cat <<END\nline1\nline2\nline3\nEND";
3175 let tokens = lex(source);
3176 assert_eq!(tokens, vec![
3177 Token::Ident("cat".to_string()),
3178 Token::HereDocStart,
3179 Token::HereDoc(HereDocData {
3180 content: "line1\nline2\nline3\n".to_string(),
3181 literal: false,
3182 strip_tabs: false,
3183 body_start_offset: 10,
3184 }),
3185 Token::Newline,
3186 ]);
3187 }
3188
3189 #[test]
3190 fn heredoc_in_command() {
3191 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
3192 let tokens = lex(source);
3193 assert_eq!(tokens, vec![
3194 Token::Ident("cat".to_string()),
3195 Token::HereDocStart,
3196 Token::HereDoc(HereDocData {
3197 content: "hello\n".to_string(),
3198 literal: false,
3199 strip_tabs: false,
3200 body_start_offset: 10,
3201 }),
3202 Token::Newline,
3203 Token::Ident("echo".to_string()),
3204 Token::Ident("goodbye".to_string()),
3205 ]);
3206 }
3207
3208 #[test]
3209 fn heredoc_strip_tabs() {
3210 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
3211 let tokens = lex(source);
3212 assert_eq!(tokens, vec![
3216 Token::Ident("cat".to_string()),
3217 Token::HereDocStart,
3218 Token::HereDoc(HereDocData {
3219 content: "\thello\n\tworld\n".to_string(),
3220 literal: false,
3221 strip_tabs: true,
3222 body_start_offset: 11,
3223 }),
3224 Token::Newline,
3225 ]);
3226 }
3227
3228 #[test]
3233 fn arithmetic_simple() {
3234 let source = "$((1 + 2))";
3235 let tokens = lex(source);
3236 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
3237 }
3238
3239 #[test]
3240 fn arithmetic_in_assignment() {
3241 let source = "X=$((5 * 3))";
3242 let tokens = lex(source);
3243 assert_eq!(tokens, vec![
3244 Token::Ident("X".to_string()),
3245 Token::Eq,
3246 Token::Arithmetic("5 * 3".to_string()),
3247 ]);
3248 }
3249
3250 #[test]
3251 fn arithmetic_with_nested_parens() {
3252 let source = "$((2 * (3 + 4)))";
3253 let tokens = lex(source);
3254 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
3255 }
3256
3257 #[test]
3258 fn arithmetic_with_variable() {
3259 let source = "$((X + 1))";
3260 let tokens = lex(source);
3261 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
3262 }
3263
3264 #[test]
3265 fn arithmetic_command_subst_not_confused() {
3266 let source = "$(echo hello)";
3268 let tokens = lex(source);
3269 assert_eq!(tokens, vec![
3270 Token::CmdSubstStart,
3271 Token::Ident("echo".to_string()),
3272 Token::Ident("hello".to_string()),
3273 Token::RParen,
3274 ]);
3275 }
3276
3277 #[test]
3278 fn arithmetic_nesting_limit() {
3279 let open_parens = "(".repeat(300);
3281 let close_parens = ")".repeat(300);
3282 let source = format!("$(({}1{}))", open_parens, close_parens);
3283 let result = tokenize(&source);
3284 assert!(result.is_err());
3285 let errors = result.unwrap_err();
3286 assert_eq!(errors.len(), 1);
3287 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
3288 }
3289
3290 #[test]
3291 fn arithmetic_nesting_within_limit() {
3292 let source = "$((((1 + 2) * 3)))";
3294 let tokens = lex(source);
3295 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
3296 }
3297
3298 #[test]
3310 fn arithmetic_after_apostrophe_in_comment() {
3311 let source = "# this doesn't work\necho $((1+2))";
3314 let tokens = lex(source);
3315 assert_eq!(tokens, vec![
3316 Token::Newline,
3317 Token::Ident("echo".to_string()),
3318 Token::Arithmetic("1+2".to_string()),
3319 ]);
3320 }
3321
3322 #[test]
3323 fn arithmetic_inside_comment_is_not_expanded() {
3324 let source = "# the $((y)) syntax explained\necho hello";
3326 let tokens = lex(source);
3327 assert_eq!(tokens, vec![
3328 Token::Newline,
3329 Token::Ident("echo".to_string()),
3330 Token::Ident("hello".to_string()),
3331 ]);
3332 }
3333
3334 #[test]
3335 fn backticked_arithmetic_in_comment_is_not_expanded() {
3336 let source = "# the `$((x))` syntax explained\necho $((3+4))";
3340 let tokens = lex(source);
3341 assert_eq!(tokens, vec![
3342 Token::Newline,
3343 Token::Ident("echo".to_string()),
3344 Token::Arithmetic("3+4".to_string()),
3345 ]);
3346 }
3347
3348 #[test]
3349 fn arithmetic_still_works_outside_comments() {
3350 let source = "X=$((1+2)); Y=$((3*4))";
3353 let tokens = lex(source);
3354 assert_eq!(tokens, vec![
3355 Token::Ident("X".to_string()),
3356 Token::Eq,
3357 Token::Arithmetic("1+2".to_string()),
3358 Token::Semi,
3359 Token::Ident("Y".to_string()),
3360 Token::Eq,
3361 Token::Arithmetic("3*4".to_string()),
3362 ]);
3363 }
3364
3365 #[test]
3366 fn arithmetic_inside_double_quotes_still_expands() {
3367 let source = "echo \"# $((1+2))\"";
3370 let tokens = lex(source);
3371 assert_eq!(tokens.len(), 2);
3376 assert!(matches!(tokens[0], Token::Ident(_)));
3377 assert!(matches!(tokens[1], Token::String(_)));
3378 }
3379
3380 #[test]
3393 fn backtick_in_source_is_rejected() {
3394 let result = tokenize("echo `date`");
3395 assert!(result.is_err());
3396 let errors = result.unwrap_err();
3397 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
3398 }
3399
3400 #[test]
3401 fn backtick_in_comment_is_just_comment_text() {
3402 let source = "# use `date` here\necho hi";
3405 let tokens = lex(source);
3406 assert_eq!(tokens, vec![
3407 Token::Newline,
3408 Token::Ident("echo".to_string()),
3409 Token::Ident("hi".to_string()),
3410 ]);
3411 }
3412
3413 #[test]
3414 fn backtick_in_single_quoted_string_is_literal() {
3415 let source = "echo '`date`'";
3418 let tokens = lex(source);
3419 assert_eq!(tokens, vec![
3420 Token::Ident("echo".to_string()),
3421 Token::SingleString("`date`".to_string()),
3422 ]);
3423 }
3424
3425 #[test]
3426 fn backtick_in_double_quoted_string_is_literal() {
3427 let source = "echo \"`date`\"";
3432 let tokens = lex(source);
3433 assert_eq!(tokens.len(), 2);
3434 assert!(matches!(tokens[0], Token::Ident(_)));
3435 match &tokens[1] {
3436 Token::String(s) => assert!(s.contains('`')),
3437 other => panic!("expected Token::String, got {:?}", other),
3438 }
3439 }
3440
3441 #[test]
3442 fn backtick_in_heredoc_body_is_preserved() {
3443 let source = "cat <<EOF\n`date`\nEOF\n";
3446 let tokens = lex(source);
3447 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
3448 assert!(heredoc.is_some(), "expected a HereDoc token");
3449 if let Some(Token::HereDoc(d)) = heredoc {
3450 assert!(d.content.contains('`'));
3451 }
3452 }
3453
3454 #[test]
3459 fn token_categories() {
3460 assert_eq!(Token::If.category(), TokenCategory::Keyword);
3462 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
3463 assert_eq!(Token::For.category(), TokenCategory::Keyword);
3464 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
3465 assert_eq!(Token::True.category(), TokenCategory::Keyword);
3466 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
3467
3468 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
3470 assert_eq!(Token::And.category(), TokenCategory::Operator);
3471 assert_eq!(Token::Or.category(), TokenCategory::Operator);
3472 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
3473 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
3474
3475 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
3477 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
3478 assert_eq!(
3479 Token::HereDoc(HereDocData {
3480 content: "test".to_string(),
3481 literal: false,
3482 strip_tabs: false,
3483 body_start_offset: 0,
3484 }).category(),
3485 TokenCategory::String,
3486 );
3487
3488 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
3490 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
3491 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
3492
3493 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
3495 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
3496 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
3497 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
3498 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
3499 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
3500 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
3501
3502 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
3504 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
3505 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
3506 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
3507
3508 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
3510 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
3511 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
3512 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
3513
3514 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
3516
3517 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
3519
3520 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
3522 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
3523 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
3524
3525 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
3527 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
3528 }
3529
3530 #[test]
3531 fn test_heredoc_piped_to_command() {
3532 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
3535 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
3536 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
3537 assert!(heredoc_pos.is_some(), "should have a heredoc token");
3538 assert!(pipe_pos.is_some(), "should have a pipe token");
3539 assert!(
3540 pipe_pos.unwrap() > heredoc_pos.unwrap(),
3541 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
3542 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
3543 );
3544 }
3545
3546 #[test]
3547 fn test_heredoc_standalone_still_works() {
3548 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
3550 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
3551 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
3552 }
3553
3554 #[test]
3555 fn test_heredoc_preserves_leading_empty_lines() {
3556 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
3558 let heredoc = tokens.iter().find_map(|t| {
3559 if let Token::HereDoc(data) = &t.token {
3560 Some(data.clone())
3561 } else {
3562 None
3563 }
3564 });
3565 assert!(heredoc.is_some(), "should have a heredoc token");
3566 let data = heredoc.unwrap();
3567 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
3568 assert_eq!(data.content, "\nhello\n");
3569 }
3570
3571 #[test]
3572 fn test_heredoc_quoted_delimiter_sets_literal() {
3573 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
3575 let heredoc = tokens.iter().find_map(|t| {
3576 if let Token::HereDoc(data) = &t.token {
3577 Some(data.clone())
3578 } else {
3579 None
3580 }
3581 });
3582 assert!(heredoc.is_some(), "should have a heredoc token");
3583 let data = heredoc.unwrap();
3584 assert!(data.literal, "quoted delimiter should set literal=true");
3585 assert_eq!(data.content, "hello $HOME\n");
3586 }
3587
3588 #[test]
3589 fn test_heredoc_unquoted_delimiter_not_literal() {
3590 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
3592 let heredoc = tokens.iter().find_map(|t| {
3593 if let Token::HereDoc(data) = &t.token {
3594 Some(data.clone())
3595 } else {
3596 None
3597 }
3598 });
3599 assert!(heredoc.is_some(), "should have a heredoc token");
3600 let data = heredoc.unwrap();
3601 assert!(!data.literal, "unquoted delimiter should have literal=false");
3602 }
3603
3604 #[test]
3609 fn colon_double_in_word() {
3610 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
3611 }
3612
3613 #[test]
3614 fn colon_single_in_word() {
3615 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
3616 }
3617
3618 #[test]
3619 fn colon_with_port() {
3620 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
3621 }
3622
3623 #[test]
3624 fn colon_standalone() {
3625 assert_eq!(lex(":"), vec![Token::Colon]);
3626 }
3627
3628 #[test]
3629 fn colon_spaced_no_merge() {
3630 assert_eq!(
3631 lex("foo : bar"),
3632 vec![
3633 Token::Ident("foo".into()),
3634 Token::Colon,
3635 Token::Ident("bar".into()),
3636 ]
3637 );
3638 }
3639
3640 #[test]
3641 fn colon_in_command_arg() {
3642 assert_eq!(
3643 lex("echo foo::bar"),
3644 vec![
3645 Token::Ident("echo".into()),
3646 Token::Ident("foo::bar".into()),
3647 ]
3648 );
3649 }
3650
3651 #[test]
3652 fn colon_trailing() {
3653 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
3655 }
3656
3657 #[test]
3658 fn colon_leading() {
3659 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
3661 }
3662
3663 #[test]
3664 fn colon_with_path() {
3665 assert_eq!(
3667 lex("/usr/bin:8080"),
3668 vec![Token::Ident("/usr/bin:8080".into())]
3669 );
3670 }
3671
3672 #[test]
3677 fn is_keyword_covers_control_flow() {
3678 for t in [
3679 Token::While,
3680 Token::Return,
3681 Token::Break,
3682 Token::Continue,
3683 Token::Exit,
3684 ] {
3685 assert!(t.is_keyword(), "{t:?} should be a keyword");
3686 }
3687 }
3688
3689 #[test]
3690 fn starts_statement_covers_while() {
3691 assert!(Token::While.starts_statement());
3692 }
3693
3694 #[test]
3695 fn is_keyword_rejects_operators() {
3696 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
3697 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
3698 }
3699 }
3700}