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 UnterminatedArithmetic,
120 UnterminatedHeredoc { delimiter: String },
124 BackticksNotSupported,
129}
130
131impl fmt::Display for LexerError {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 LexerError::UnexpectedCharacter => write!(f, "unexpected character"),
135 LexerError::UnterminatedString => write!(f, "unterminated string"),
136 LexerError::UnterminatedVarRef => write!(f, "unterminated variable reference"),
137 LexerError::InvalidEscape => write!(f, "invalid escape sequence"),
138 LexerError::InvalidNumber => write!(f, "invalid number"),
139 LexerError::AmbiguousBoolean(s) => {
140 write!(f, "ambiguous boolean, use lowercase '{}'", s.to_lowercase())
141 }
142 LexerError::AmbiguousBooleanLike(s) => {
143 let suggest = if s.eq_ignore_ascii_case("yes") { "true" } else { "false" };
144 write!(f, "ambiguous boolean-like '{}', use '{}' or '\"{}\"'", s, suggest, s)
145 }
146 LexerError::InvalidFloatNoLeading => write!(f, "float must have leading digit"),
147 LexerError::InvalidFloatNoTrailing => write!(f, "float must have trailing digit"),
148 LexerError::NestingTooDeep => write!(f, "nesting depth exceeded (max {})", MAX_PAREN_DEPTH),
149 LexerError::UnterminatedArithmetic => {
150 write!(f, "unterminated arithmetic expansion, expected closing `))`")
151 }
152 LexerError::UnterminatedHeredoc { delimiter } => {
153 write!(f, "unterminated heredoc, expected closing delimiter `{}` on its own line", delimiter)
154 }
155 LexerError::BackticksNotSupported => {
156 write!(f, "backticks are not supported in kaish; use $(cmd) instead")
157 }
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq)]
185pub struct HereDocData {
186 pub content: String,
187 pub literal: bool,
188 pub strip_tabs: bool,
189 pub body_start_offset: usize,
190}
191
192#[derive(Logos, Debug, Clone, PartialEq)]
193#[logos(error = LexerError)]
194#[logos(skip r"[ \t]+")]
195pub enum Token {
196 #[token("set")]
200 Set,
201
202 #[token("local")]
203 Local,
204
205 #[token("if")]
206 If,
207
208 #[token("then")]
209 Then,
210
211 #[token("else")]
212 Else,
213
214 #[token("elif")]
215 Elif,
216
217 #[token("fi")]
218 Fi,
219
220 #[token("for")]
221 For,
222
223 #[token("while")]
224 While,
225
226 #[token("in")]
227 In,
228
229 #[token("do")]
230 Do,
231
232 #[token("done")]
233 Done,
234
235 #[token("case")]
236 Case,
237
238 #[token("esac")]
239 Esac,
240
241 #[token("function")]
242 Function,
243
244 #[token("break")]
245 Break,
246
247 #[token("continue")]
248 Continue,
249
250 #[token("return")]
251 Return,
252
253 #[token("exit")]
254 Exit,
255
256 #[token("true")]
257 True,
258
259 #[token("false")]
260 False,
261
262 #[token("string")]
266 TypeString,
267
268 #[token("int")]
269 TypeInt,
270
271 #[token("float")]
272 TypeFloat,
273
274 #[token("bool")]
275 TypeBool,
276
277 #[token("&&")]
281 And,
282
283 #[token("||")]
284 Or,
285
286 #[token("==")]
287 EqEq,
288
289 #[token("!=")]
290 NotEq,
291
292 #[token("=~")]
293 Match,
294
295 #[token("!~")]
296 NotMatch,
297
298 #[token(">=")]
299 GtEq,
300
301 #[token("<=")]
302 LtEq,
303
304 #[token(">>")]
305 GtGt,
306
307 #[token("2>&1")]
308 StderrToStdout,
309
310 #[token("1>&2")]
311 StdoutToStderr,
312
313 #[token(">&2")]
314 StdoutToStderr2,
315
316 #[token("2>")]
317 Stderr,
318
319 #[token("&>")]
320 Both,
321
322 #[token("<<<")]
323 HereString,
324
325 #[token("<<")]
326 HereDocStart,
327
328 #[token(";;")]
329 DoubleSemi,
330
331 #[token("=")]
335 Eq,
336
337 #[token("|")]
338 Pipe,
339
340 #[token("&")]
341 Amp,
342
343 #[token(">")]
344 Gt,
345
346 #[token("<")]
347 Lt,
348
349 #[token(";")]
350 Semi,
351
352 #[token(":")]
353 Colon,
354
355 #[token(",")]
356 Comma,
357
358 #[token("...")]
363 DotDotDot,
364
365 #[token("..")]
366 DotDot,
367
368 #[token(".")]
369 Dot,
370
371 #[regex(r"~[a-zA-Z0-9_./+-]+", lex_tilde_path, priority = 3)]
373 TildePath(String),
374
375 #[token("~")]
377 Tilde,
378
379 #[regex(r"\.\./[a-zA-Z0-9_./-]+", lex_relative_path, priority = 3)]
385 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.-]*/[a-zA-Z0-9_./-]*", lex_relative_path, priority = 3)]
386 RelativePath(String),
387
388 #[regex(r"\./[a-zA-Z0-9_./-]+", lex_dot_slash_path, priority = 3)]
390 DotSlashPath(String),
391
392 #[regex(r"\.[a-zA-Z_][a-zA-Z0-9_.-]*", lex_dotted_ident, priority = 3)]
398 DottedIdent(String),
399
400 #[token("{")]
401 LBrace,
402
403 #[token("}")]
404 RBrace,
405
406 #[token("[")]
407 LBracket,
408
409 #[token("]")]
410 RBracket,
411
412 #[token("(")]
413 LParen,
414
415 #[token(")")]
416 RParen,
417
418 #[token("*")]
419 Star,
420
421 #[token("!")]
422 Bang,
423
424 #[token("?")]
425 Question,
426
427 GlobWord(String),
430
431 Arithmetic(String),
438
439 #[token("$(")]
441 CmdSubstStart,
442
443 #[regex(r"--[a-zA-Z][a-zA-Z0-9-]*", lex_long_flag, priority = 3)]
449 LongFlag(String),
450
451 #[regex(r"-[a-zA-Z][a-zA-Z0-9-]*", lex_short_flag, priority = 3)]
460 ShortFlag(String),
461
462 #[regex(r"\+[a-zA-Z][a-zA-Z0-9]*", lex_plus_flag, priority = 3)]
464 PlusFlag(String),
465
466 #[token("--")]
471 DoubleDash,
472
473 #[regex(r"--[^a-zA-Z\s][^\s]*", lex_double_dash_bare, priority = 2)]
485 DoubleDashBare(String),
486
487 #[regex(r"\+[^a-zA-Z\s][^\s]*", lex_plus_bare, priority = 2)]
490 PlusBare(String),
491
492 #[regex(r"-[^a-zA-Z0-9\s\-][^\s]*", lex_minus_bare, priority = 1)]
496 MinusBare(String),
497
498 #[regex(r"%[0-9]+", lex_job_spec)]
502 JobSpec(String),
503
504 #[token("-")]
508 MinusAlone,
509
510 #[regex(r#""([^"\\]|\\.)*""#, lex_string)]
516 String(String),
517
518 #[regex(r"'[^']*'", lex_single_string)]
520 SingleString(String),
521
522 #[regex(r"\$\{[^}]+\}", lex_varref)]
524 VarRef(String),
525
526 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", lex_simple_varref)]
528 SimpleVarRef(String),
529
530 #[regex(r"\$[0-9]", lex_positional)]
532 Positional(usize),
533
534 #[token("$@")]
536 AllArgs,
537
538 #[token("$#")]
540 ArgCount,
541
542 #[token("$?")]
544 LastExitCode,
545
546 #[token("$$")]
548 CurrentPid,
549
550 #[regex(r"\$\{#[a-zA-Z_][a-zA-Z0-9_]*(\[[^\]]*\])*\}", lex_var_length)]
555 VarLength(String),
556
557 HereDoc(HereDocData),
560
561 #[regex(r"-?[0-9]+", lex_int, priority = 2)]
563 Int(i64),
564
565 #[regex(r"-?[0-9]+\.[0-9]+", lex_float)]
567 Float(f64),
568
569 #[regex(r"[0-9]+[a-zA-Z_][a-zA-Z0-9_.-]*", lex_number_ident, priority = 3)]
578 NumberIdent(String),
579
580 #[regex(r"[0-9]+(\.[0-9]+)?(-[0-9a-zA-Z._]+)+", lex_slice_word, priority = 3)]
591 #[regex(r"-[0-9]+[a-zA-Z_][0-9a-zA-Z._-]*", lex_slice_word, priority = 3)]
592 DashNumWord(String),
593
594 #[regex(r"@[a-zA-Z0-9_./@-]*", lex_slice_word, priority = 3)]
599 AtWord(String),
600
601 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
603 InvalidFloatNoLeading,
604
605 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
608 InvalidFloatNoTrailing,
609
610 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
616 Path(String),
617
618 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.@-]*", lex_ident)]
626 Ident(String),
627
628 #[regex(r"#[^\n\r]*", allow_greedy = true)]
634 Comment,
635
636 #[regex(r"\n|\r\n")]
638 Newline,
639
640 #[regex(r"\\[ \t]*(\n|\r\n)")]
642 LineContinuation,
643
644 #[token("`", reject_backtick)]
653 BacktickRejected,
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
661pub enum TokenCategory {
662 Keyword,
664 Operator,
666 String,
668 Number,
670 Variable,
672 Comment,
674 Punctuation,
676 Command,
678 Path,
680 Flag,
682 Error,
684}
685
686impl Token {
687 pub fn category(&self) -> TokenCategory {
689 match self {
690 Token::If
692 | Token::Then
693 | Token::Else
694 | Token::Elif
695 | Token::Fi
696 | Token::For
697 | Token::In
698 | Token::Do
699 | Token::Done
700 | Token::While
701 | Token::Case
702 | Token::Esac
703 | Token::Function
704 | Token::Return
705 | Token::Break
706 | Token::Continue
707 | Token::Exit
708 | Token::Set
709 | Token::Local
710 | Token::True
711 | Token::False
712 | Token::TypeString
713 | Token::TypeInt
714 | Token::TypeFloat
715 | Token::TypeBool => TokenCategory::Keyword,
716
717 Token::Pipe
719 | Token::And
720 | Token::Or
721 | Token::Amp
722 | Token::Eq
723 | Token::EqEq
724 | Token::NotEq
725 | Token::Match
726 | Token::NotMatch
727 | Token::Lt
728 | Token::Gt
729 | Token::LtEq
730 | Token::GtEq
731 | Token::GtGt
732 | Token::Stderr
733 | Token::Both
734 | Token::HereDocStart
735 | Token::HereString
736 | Token::StderrToStdout
737 | Token::StdoutToStderr
738 | Token::StdoutToStderr2 => TokenCategory::Operator,
739
740 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
742
743 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
745
746 Token::VarRef(_)
748 | Token::SimpleVarRef(_)
749 | Token::Positional(_)
750 | Token::AllArgs
751 | Token::ArgCount
752 | Token::VarLength(_)
753 | Token::LastExitCode
754 | Token::CurrentPid => TokenCategory::Variable,
755
756 Token::LongFlag(_)
758 | Token::ShortFlag(_)
759 | Token::PlusFlag(_)
760 | Token::DoubleDash => TokenCategory::Flag,
761
762 Token::Semi
764 | Token::DoubleSemi
765 | Token::Colon
766 | Token::Comma
767 | Token::Dot
768 | Token::LParen
769 | Token::RParen
770 | Token::LBrace
771 | Token::RBrace
772 | Token::LBracket
773 | Token::RBracket
774 | Token::Bang
775 | Token::Question
776 | Token::Star
777 | Token::Newline
778 | Token::LineContinuation
779 | Token::CmdSubstStart
780 | Token::DotDotDot => TokenCategory::Punctuation,
781
782 Token::GlobWord(_) => TokenCategory::Path,
784
785 Token::Comment => TokenCategory::Comment,
787
788 Token::Path(_)
790 | Token::TildePath(_)
791 | Token::RelativePath(_)
792 | Token::Tilde
793 | Token::DotDot
794 | Token::DotSlashPath(_) => TokenCategory::Path,
795
796 Token::Ident(_)
798 | Token::PlusBare(_)
799 | Token::MinusBare(_)
800 | Token::DoubleDashBare(_)
801 | Token::MinusAlone
802 | Token::NumberIdent(_)
803 | Token::DashNumWord(_)
804 | Token::AtWord(_)
805 | Token::DottedIdent(_)
806 | Token::JobSpec(_) => TokenCategory::Command,
807
808 Token::InvalidFloatNoLeading
810 | Token::InvalidFloatNoTrailing
811 | Token::BacktickRejected => TokenCategory::Error,
812 }
813 }
814}
815
816fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
818 parse_string_literal(lex.slice())
819}
820
821fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
823 let s = lex.slice();
824 s[1..s.len() - 1].to_string()
826}
827
828fn lex_varref(lex: &mut logos::Lexer<Token>) -> String {
830 lex.slice().to_string()
832}
833
834fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
836 lex.slice()[1..].to_string()
838}
839
840fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
842 lex.slice()[1..].parse().unwrap_or(0)
844}
845
846fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
848 let s = lex.slice();
850 s[3..s.len() - 1].to_string()
851}
852
853fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
855 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
856}
857
858fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
860 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
861}
862
863fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
867 lex.slice().to_string()
868}
869
870fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
872 lex.slice().to_string()
873}
874
875fn lex_slice_word(lex: &mut logos::Lexer<Token>) -> String {
878 lex.slice().to_string()
879}
880
881fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
884 Err(LexerError::InvalidFloatNoLeading)
885}
886
887fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
891 Err(LexerError::BackticksNotSupported)
892}
893
894fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
897 Err(LexerError::InvalidFloatNoTrailing)
898}
899
900fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
902 let s = lex.slice();
903
904 match s.to_lowercase().as_str() {
907 "true" | "false" if s != "true" && s != "false" => {
908 return Err(LexerError::AmbiguousBoolean(s.to_string()));
909 }
910 _ => {}
911 }
912
913 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
915 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
916 }
917
918 Ok(s.to_string())
919}
920
921fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
923 lex.slice()[2..].to_string()
925}
926
927fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
929 lex.slice()[1..].to_string()
931}
932
933fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
935 lex.slice()[1..].to_string()
937}
938
939fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
941 lex.slice().to_string()
942}
943
944fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
946 lex.slice().to_string()
947}
948
949fn lex_double_dash_bare(lex: &mut logos::Lexer<Token>) -> String {
952 lex.slice().to_string()
953}
954
955fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
957 lex.slice().to_string()
958}
959
960fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
962 lex.slice().to_string()
963}
964
965fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
967 lex.slice().to_string()
968}
969
970fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
972 lex.slice().to_string()
973}
974
975fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
977 lex.slice().to_string()
978}
979
980impl fmt::Display for Token {
981 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
982 match self {
983 Token::Set => write!(f, "set"),
984 Token::Local => write!(f, "local"),
985 Token::If => write!(f, "if"),
986 Token::Then => write!(f, "then"),
987 Token::Else => write!(f, "else"),
988 Token::Elif => write!(f, "elif"),
989 Token::Fi => write!(f, "fi"),
990 Token::For => write!(f, "for"),
991 Token::While => write!(f, "while"),
992 Token::In => write!(f, "in"),
993 Token::Do => write!(f, "do"),
994 Token::Done => write!(f, "done"),
995 Token::Case => write!(f, "case"),
996 Token::Esac => write!(f, "esac"),
997 Token::Function => write!(f, "function"),
998 Token::Break => write!(f, "break"),
999 Token::Continue => write!(f, "continue"),
1000 Token::Return => write!(f, "return"),
1001 Token::Exit => write!(f, "exit"),
1002 Token::True => write!(f, "true"),
1003 Token::False => write!(f, "false"),
1004 Token::TypeString => write!(f, "string"),
1005 Token::TypeInt => write!(f, "int"),
1006 Token::TypeFloat => write!(f, "float"),
1007 Token::TypeBool => write!(f, "bool"),
1008 Token::And => write!(f, "&&"),
1009 Token::Or => write!(f, "||"),
1010 Token::EqEq => write!(f, "=="),
1011 Token::NotEq => write!(f, "!="),
1012 Token::Match => write!(f, "=~"),
1013 Token::NotMatch => write!(f, "!~"),
1014 Token::GtEq => write!(f, ">="),
1015 Token::LtEq => write!(f, "<="),
1016 Token::GtGt => write!(f, ">>"),
1017 Token::StderrToStdout => write!(f, "2>&1"),
1018 Token::StdoutToStderr => write!(f, "1>&2"),
1019 Token::StdoutToStderr2 => write!(f, ">&2"),
1020 Token::Stderr => write!(f, "2>"),
1021 Token::Both => write!(f, "&>"),
1022 Token::HereDocStart => write!(f, "<<"),
1023 Token::HereString => write!(f, "<<<"),
1024 Token::DoubleSemi => write!(f, ";;"),
1025 Token::Eq => write!(f, "="),
1026 Token::Pipe => write!(f, "|"),
1027 Token::Amp => write!(f, "&"),
1028 Token::Gt => write!(f, ">"),
1029 Token::Lt => write!(f, "<"),
1030 Token::Semi => write!(f, ";"),
1031 Token::Colon => write!(f, ":"),
1032 Token::Comma => write!(f, ","),
1033 Token::Dot => write!(f, "."),
1034 Token::DotDot => write!(f, ".."),
1035 Token::DotDotDot => write!(f, "..."),
1036 Token::Tilde => write!(f, "~"),
1037 Token::TildePath(s) => write!(f, "{}", s),
1038 Token::RelativePath(s) => write!(f, "{}", s),
1039 Token::DotSlashPath(s) => write!(f, "{}", s),
1040 Token::LBrace => write!(f, "{{"),
1041 Token::RBrace => write!(f, "}}"),
1042 Token::LBracket => write!(f, "["),
1043 Token::RBracket => write!(f, "]"),
1044 Token::LParen => write!(f, "("),
1045 Token::RParen => write!(f, ")"),
1046 Token::Star => write!(f, "*"),
1047 Token::Bang => write!(f, "!"),
1048 Token::Question => write!(f, "?"),
1049 Token::GlobWord(s) => write!(f, "GLOB({})", s),
1050 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
1051 Token::CmdSubstStart => write!(f, "$("),
1052 Token::LongFlag(s) => write!(f, "--{}", s),
1053 Token::ShortFlag(s) => write!(f, "-{}", s),
1054 Token::PlusFlag(s) => write!(f, "+{}", s),
1055 Token::DoubleDash => write!(f, "--"),
1056 Token::DoubleDashBare(s) => write!(f, "{}", s),
1057 Token::PlusBare(s) => write!(f, "{}", s),
1058 Token::MinusBare(s) => write!(f, "{}", s),
1059 Token::JobSpec(s) => write!(f, "{}", s),
1060 Token::MinusAlone => write!(f, "-"),
1061 Token::String(s) => write!(f, "STRING({:?})", s),
1062 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
1063 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
1064 Token::VarRef(v) => write!(f, "VARREF({})", v),
1065 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
1066 Token::Positional(n) => write!(f, "${}", n),
1067 Token::AllArgs => write!(f, "$@"),
1068 Token::ArgCount => write!(f, "$#"),
1069 Token::LastExitCode => write!(f, "$?"),
1070 Token::CurrentPid => write!(f, "$$"),
1071 Token::VarLength(v) => write!(f, "${{#{}}}", v),
1072 Token::Int(n) => write!(f, "INT({})", n),
1073 Token::Float(n) => write!(f, "FLOAT({})", n),
1074 Token::Path(s) => write!(f, "PATH({})", s),
1075 Token::Ident(s) => write!(f, "IDENT({})", s),
1076 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1077 Token::DashNumWord(s) => write!(f, "DASHNUM({})", s),
1078 Token::AtWord(s) => write!(f, "ATWORD({})", s),
1079 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1080 Token::Comment => write!(f, "COMMENT"),
1081 Token::Newline => write!(f, "NEWLINE"),
1082 Token::LineContinuation => write!(f, "LINECONT"),
1083 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1085 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1086 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1087 }
1088 }
1089}
1090
1091impl Token {
1092 pub fn is_keyword(&self) -> bool {
1097 matches!(
1098 self,
1099 Token::Set
1100 | Token::Local
1101 | Token::If
1102 | Token::Then
1103 | Token::Else
1104 | Token::Elif
1105 | Token::Fi
1106 | Token::For
1107 | Token::In
1108 | Token::Do
1109 | Token::Done
1110 | Token::While
1111 | Token::Case
1112 | Token::Esac
1113 | Token::Function
1114 | Token::Return
1115 | Token::Break
1116 | Token::Continue
1117 | Token::Exit
1118 | Token::True
1119 | Token::False
1120 )
1121 }
1122
1123 pub fn is_type(&self) -> bool {
1125 matches!(
1126 self,
1127 Token::TypeString
1128 | Token::TypeInt
1129 | Token::TypeFloat
1130 | Token::TypeBool
1131 )
1132 }
1133
1134 pub fn starts_statement(&self) -> bool {
1137 matches!(
1138 self,
1139 Token::Set
1140 | Token::Local
1141 | Token::Function
1142 | Token::If
1143 | Token::For
1144 | Token::While
1145 | Token::Case
1146 | Token::Ident(_)
1147 | Token::LBracket
1148 )
1149 }
1150
1151 pub fn is_value(&self) -> bool {
1153 matches!(
1154 self,
1155 Token::String(_)
1156 | Token::SingleString(_)
1157 | Token::HereDoc(_)
1158 | Token::Arithmetic(_)
1159 | Token::Int(_)
1160 | Token::Float(_)
1161 | Token::True
1162 | Token::False
1163 | Token::VarRef(_)
1164 | Token::SimpleVarRef(_)
1165 | Token::CmdSubstStart
1166 | Token::Path(_)
1167 | Token::GlobWord(_)
1168 | Token::LastExitCode
1169 | Token::CurrentPid
1170 )
1171 }
1172}
1173
1174struct ArithmeticPreprocessResult {
1176 text: String,
1178 arithmetics: Vec<(String, String)>,
1180 replacements: Vec<SpanReplacement>,
1182}
1183
1184fn skip_command_substitution(
1193 chars: &[char],
1194 i: &mut usize,
1195 source_pos: &mut usize,
1196 result: &mut String,
1197) {
1198 result.push('$');
1200 result.push('(');
1201 *i += 2;
1202 *source_pos += 2;
1203
1204 let mut depth: usize = 1;
1205 let mut in_single_quote = false;
1206 let mut in_double_quote = false;
1207
1208 while *i < chars.len() && depth > 0 {
1209 let c = chars[*i];
1210
1211 if in_single_quote {
1212 result.push(c);
1213 *source_pos += c.len_utf8();
1214 *i += 1;
1215 if c == '\'' {
1216 in_single_quote = false;
1217 }
1218 continue;
1219 }
1220
1221 if in_double_quote {
1222 if c == '\\' && *i + 1 < chars.len() {
1223 let next = chars[*i + 1];
1224 if next == '"' || next == '\\' || next == '$' || next == '`' {
1225 result.push(c);
1226 result.push(next);
1227 *source_pos += c.len_utf8() + next.len_utf8();
1228 *i += 2;
1229 continue;
1230 }
1231 }
1232 if c == '"' {
1233 in_double_quote = false;
1234 }
1235 result.push(c);
1236 *source_pos += c.len_utf8();
1237 *i += 1;
1238 continue;
1239 }
1240
1241 match c {
1243 '\'' => {
1244 in_single_quote = true;
1245 result.push(c);
1246 *source_pos += c.len_utf8();
1247 *i += 1;
1248 }
1249 '"' => {
1250 in_double_quote = true;
1251 result.push(c);
1252 *source_pos += c.len_utf8();
1253 *i += 1;
1254 }
1255 '\\' if *i + 1 < chars.len() => {
1256 result.push(c);
1257 result.push(chars[*i + 1]);
1258 *source_pos += c.len_utf8() + chars[*i + 1].len_utf8();
1259 *i += 2;
1260 }
1261 '(' => {
1262 depth += 1;
1263 result.push(c);
1264 *source_pos += c.len_utf8();
1265 *i += 1;
1266 }
1267 ')' => {
1268 depth -= 1;
1269 result.push(c);
1270 *source_pos += c.len_utf8();
1271 *i += 1;
1272 }
1273 _ => {
1274 result.push(c);
1275 *source_pos += c.len_utf8();
1276 *i += 1;
1277 }
1278 }
1279 }
1280}
1281
1282fn preprocess_arithmetic(source: &str) -> Result<ArithmeticPreprocessResult, LexerError> {
1296 let mut result = String::with_capacity(source.len());
1297 let mut arithmetics: Vec<(String, String)> = Vec::new();
1298 let mut replacements: Vec<SpanReplacement> = Vec::new();
1299 let mut source_pos: usize = 0;
1300 let chars_vec: Vec<char> = source.chars().collect();
1301 let mut i = 0;
1302
1303 let mut in_double_quote = false;
1306
1307 while i < chars_vec.len() {
1308 let ch = chars_vec[i];
1309
1310 if !in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1312 result.push(ch);
1313 result.push(chars_vec[i + 1]);
1314 source_pos += ch.len_utf8() + chars_vec[i + 1].len_utf8();
1315 i += 2;
1316 continue;
1317 }
1318
1319 if ch == '\'' && !in_double_quote {
1321 result.push(ch);
1322 i += 1;
1323 source_pos += 1;
1324 while i < chars_vec.len() && chars_vec[i] != '\'' {
1325 result.push(chars_vec[i]);
1326 source_pos += chars_vec[i].len_utf8();
1327 i += 1;
1328 }
1329 if i < chars_vec.len() {
1330 result.push(chars_vec[i]); source_pos += 1;
1332 i += 1;
1333 }
1334 continue;
1335 }
1336
1337 if ch == '"' {
1339 in_double_quote = !in_double_quote;
1340 result.push(ch);
1341 i += 1;
1342 source_pos += 1;
1343 continue;
1344 }
1345
1346 if in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1348 let next = chars_vec[i + 1];
1349 if next == '"' || next == '\\' || next == '$' || next == '`' {
1350 result.push(ch);
1351 result.push(next);
1352 source_pos += ch.len_utf8() + next.len_utf8();
1353 i += 2;
1354 continue;
1355 }
1356 }
1357
1358 if ch == '#' && !in_double_quote {
1366 while i < chars_vec.len() && chars_vec[i] != '\n' && chars_vec[i] != '\r' {
1367 result.push(chars_vec[i]);
1368 source_pos += chars_vec[i].len_utf8();
1369 i += 1;
1370 }
1371 continue;
1372 }
1373
1374 if ch == '$' && i + 1 < chars_vec.len() && chars_vec[i + 1] == '('
1376 && !(i + 2 < chars_vec.len() && chars_vec[i + 2] == '(')
1377 {
1378 skip_command_substitution(&chars_vec, &mut i, &mut source_pos, &mut result);
1379 continue;
1380 }
1381
1382 if ch == '$' && i + 2 < chars_vec.len() && chars_vec[i + 1] == '(' && chars_vec[i + 2] == '(' {
1384 let arith_start_pos = result.len();
1385 let original_start = source_pos;
1386
1387 i += 3;
1389 source_pos += 3;
1390
1391 let mut expr = String::new();
1393 let mut paren_depth: usize = 0;
1394 let mut closed = false;
1395
1396 while i < chars_vec.len() {
1397 let c = chars_vec[i];
1398 match c {
1399 '(' => {
1400 paren_depth += 1;
1401 if paren_depth > MAX_PAREN_DEPTH {
1402 return Err(LexerError::NestingTooDeep);
1403 }
1404 expr.push('(');
1405 i += 1;
1406 source_pos += c.len_utf8();
1407 }
1408 ')' => {
1409 if paren_depth > 0 {
1410 paren_depth -= 1;
1411 expr.push(')');
1412 i += 1;
1413 source_pos += 1;
1414 } else if i + 1 < chars_vec.len() && chars_vec[i + 1] == ')' {
1415 i += 2;
1417 source_pos += 2;
1418 closed = true;
1419 break;
1420 } else {
1421 expr.push(')');
1423 i += 1;
1424 source_pos += 1;
1425 }
1426 }
1427 _ => {
1428 expr.push(c);
1429 i += 1;
1430 source_pos += c.len_utf8();
1431 }
1432 }
1433 }
1434
1435 if !closed {
1438 return Err(LexerError::UnterminatedArithmetic);
1439 }
1440
1441 let original_len = source_pos - original_start;
1443
1444 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1446 let marker_len = marker.len();
1447
1448 replacements.push(SpanReplacement {
1450 preprocessed_pos: arith_start_pos,
1451 marker_len,
1452 original_len,
1453 });
1454
1455 arithmetics.push((marker.clone(), expr));
1456 result.push_str(&marker);
1457 } else {
1458 result.push(ch);
1459 i += 1;
1460 source_pos += ch.len_utf8();
1461 }
1462 }
1463
1464 Ok(ArithmeticPreprocessResult {
1465 text: result,
1466 arithmetics,
1467 replacements,
1468 })
1469}
1470
1471#[derive(Debug, Clone)]
1486struct HeredocReplacement {
1487 marker: String,
1488 body: String,
1489 literal: bool,
1490 strip_tabs: bool,
1491 body_start_offset: usize,
1492}
1493
1494fn preprocess_heredocs(source: &str) -> Result<(String, Vec<HeredocReplacement>), Spanned<LexerError>> {
1506 let mut result = String::with_capacity(source.len());
1507 let mut heredocs: Vec<HeredocReplacement> = Vec::new();
1508 let chars_vec: Vec<char> = source.chars().collect();
1509 let mut i = 0;
1510 let mut pos: usize = 0;
1514
1515 while i < chars_vec.len() {
1516 let ch = chars_vec[i];
1517
1518 if ch == '<'
1522 && chars_vec.get(i + 1) == Some(&'<')
1523 && chars_vec.get(i + 2) == Some(&'<')
1524 {
1525 result.push_str("<<<");
1526 i += 3;
1527 pos += 3;
1528 continue;
1529 }
1530
1531 if ch == '<' && chars_vec.get(i + 1) == Some(&'<') {
1533 let introducer_start = pos;
1536 i += 2; pos += 2;
1538
1539 let strip_tabs = chars_vec.get(i) == Some(&'-');
1541 if strip_tabs {
1542 i += 1;
1543 pos += 1;
1544 }
1545
1546 while let Some(&c) = chars_vec.get(i) {
1548 if c == ' ' || c == '\t' {
1549 i += 1;
1550 pos += 1;
1551 } else {
1552 break;
1553 }
1554 }
1555
1556 let mut delimiter = String::new();
1558 let quoted = chars_vec.get(i) == Some(&'\'') || chars_vec.get(i) == Some(&'"');
1559 let quote_char = if quoted {
1560 let q = chars_vec.get(i).copied();
1561 i += 1;
1562 pos += 1;
1563 q
1564 } else {
1565 None
1566 };
1567
1568 while let Some(&c) = chars_vec.get(i) {
1569 if quoted {
1570 if Some(c) == quote_char {
1571 i += 1; pos += 1;
1573 break;
1574 }
1575 } else if c.is_whitespace() || c == '\n' || c == '\r' {
1576 break;
1577 }
1578 delimiter.push(c);
1579 i += 1;
1580 pos += c.len_utf8();
1581 }
1582
1583 if delimiter.is_empty() {
1584 result.push_str("<<");
1586 if strip_tabs {
1587 result.push('-');
1588 }
1589 continue;
1590 }
1591
1592 let mut after_delimiter = String::new();
1595 while let Some(&c) = chars_vec.get(i) {
1596 if c == '\n' {
1597 i += 1;
1598 pos += 1;
1599 break;
1600 } else if c == '\r' {
1601 i += 1;
1602 pos += 1;
1603 if chars_vec.get(i) == Some(&'\n') {
1604 i += 1;
1605 pos += 1;
1606 }
1607 break;
1608 }
1609 after_delimiter.push(c);
1610 i += 1;
1611 pos += c.len_utf8();
1612 }
1613
1614 let body_start_offset = pos;
1620 let mut content = String::new();
1621 let mut current_line = String::new();
1622
1623 loop {
1624 let next = chars_vec.get(i).copied();
1625 match next {
1626 Some('\n') => {
1627 i += 1;
1628 pos += 1;
1629 let trimmed = if strip_tabs {
1631 current_line.trim_start_matches('\t')
1632 } else {
1633 ¤t_line
1634 };
1635 if trimmed == delimiter {
1636 break;
1638 }
1639 content.push_str(¤t_line);
1641 content.push('\n');
1642 current_line.clear();
1643 }
1644 Some('\r') => {
1645 i += 1;
1646 pos += 1;
1647 let crlf = chars_vec.get(i) == Some(&'\n');
1653 if crlf {
1654 i += 1;
1655 pos += 1;
1656 }
1657 let trimmed = if strip_tabs {
1658 current_line.trim_start_matches('\t')
1659 } else {
1660 ¤t_line
1661 };
1662 if trimmed == delimiter {
1663 break;
1664 }
1665 content.push_str(¤t_line);
1666 content.push_str(if crlf { "\r\n" } else { "\r" });
1667 current_line.clear();
1668 }
1669 Some(c) => {
1670 current_line.push(c);
1671 i += 1;
1672 pos += c.len_utf8();
1673 }
1674 None => {
1675 let trimmed = if strip_tabs {
1678 current_line.trim_start_matches('\t')
1679 } else {
1680 ¤t_line
1681 };
1682 if trimmed == delimiter {
1683 break;
1684 }
1685 let span_end = introducer_start
1690 + 2
1691 + if strip_tabs { 1 } else { 0 }
1692 + delimiter.len();
1693 return Err(Spanned::new(
1694 LexerError::UnterminatedHeredoc {
1695 delimiter: delimiter.clone(),
1696 },
1697 introducer_start..span_end,
1698 ));
1699 }
1700 }
1701 }
1702
1703 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1705 heredocs.push(HeredocReplacement {
1706 marker: marker.clone(),
1707 body: content,
1708 literal: quoted,
1709 strip_tabs,
1710 body_start_offset,
1711 });
1712
1713 result.push_str("<<");
1716 result.push_str(&marker);
1717 result.push_str(&after_delimiter);
1718 result.push('\n');
1719 } else {
1720 result.push(ch);
1721 i += 1;
1722 pos += ch.len_utf8();
1723 }
1724 }
1725
1726 Ok((result, heredocs))
1727}
1728
1729fn mergeable_text(token: &Token) -> Option<String> {
1734 match token {
1735 Token::Ident(s) => Some(s.clone()),
1736 Token::NumberIdent(s) => Some(s.clone()),
1737 Token::DashNumWord(s) => Some(s.clone()),
1738 Token::AtWord(s) => Some(s.clone()),
1739 Token::DottedIdent(s) => Some(s.clone()),
1740 Token::Colon => Some(":".to_string()),
1741 Token::Int(n) => Some(n.to_string()),
1742 Token::Path(p) => Some(p.clone()),
1743 Token::Float(f) => Some(f.to_string()),
1744 _ => None,
1745 }
1746}
1747
1748#[derive(Clone, Copy, Default)]
1789struct ValueContext {
1790 in_literal: bool,
1793 in_brace: bool,
1797}
1798
1799fn compute_value_context(tokens: &[Spanned<Token>]) -> Vec<ValueContext> {
1800 let mut ctx = vec![ValueContext::default(); tokens.len()];
1801 let mut bracket_depth: usize = 0;
1802 let mut brace_depth: usize = 0;
1803 let mut test_depth: usize = 0;
1808 let mut expect_value = false;
1809 let mut skip_paired_bracket = false;
1812
1813 for i in 0..tokens.len() {
1814 let tok = &tokens[i].token;
1815
1816 let currently_value = expect_value || bracket_depth > 0 || brace_depth > 0;
1817 ctx[i] = ValueContext {
1818 in_literal: currently_value,
1819 in_brace: brace_depth > 0,
1820 };
1821
1822 if skip_paired_bracket {
1823 skip_paired_bracket = false;
1826 } else {
1827 match tok {
1828 Token::LBracket => {
1829 let next_is_lbracket =
1830 matches!(tokens.get(i + 1).map(|t| &t.token), Some(Token::LBracket));
1831 if next_is_lbracket && !currently_value {
1832 test_depth += 1;
1837 skip_paired_bracket = true;
1838 } else if currently_value {
1839 bracket_depth += 1;
1840 }
1841 }
1844 Token::RBracket => {
1845 let next_is_rbracket =
1846 matches!(tokens.get(i + 1).map(|t| &t.token), Some(Token::RBracket));
1847 if bracket_depth > 0 {
1848 bracket_depth -= 1;
1852 } else if test_depth > 0 && next_is_rbracket {
1853 test_depth -= 1;
1854 skip_paired_bracket = true;
1855 }
1856 }
1857 Token::LBrace if currently_value => brace_depth += 1,
1858 Token::RBrace if brace_depth > 0 => brace_depth -= 1,
1859 _ => {}
1860 }
1861 }
1862
1863 expect_value = match tok {
1868 Token::Eq => test_depth == 0,
1869 Token::In => test_depth > 0,
1870 _ => false,
1871 };
1872 }
1873
1874 ctx
1875}
1876
1877fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1889 if tokens.is_empty() {
1890 return tokens;
1891 }
1892
1893 let value_ctx = compute_value_context(&tokens);
1894 let mut result = Vec::with_capacity(tokens.len());
1895 let mut run: Vec<&Spanned<Token>> = Vec::new();
1896 let mut run_start = 0usize;
1897
1898 for (idx, token) in tokens.iter().enumerate() {
1899 if run.is_empty() {
1900 if mergeable_text(&token.token).is_some() {
1901 run.push(token);
1902 run_start = idx;
1903 } else {
1904 result.push(token.clone());
1905 }
1906 continue;
1907 }
1908
1909 let Some(last) = run.last() else { unreachable!() };
1912 let adjacent = last.span.end == token.span.start;
1913
1914 if adjacent && mergeable_text(&token.token).is_some() {
1915 run.push(token);
1916 } else {
1917 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace);
1918 if mergeable_text(&token.token).is_some() {
1919 run.push(token);
1920 run_start = idx;
1921 } else {
1922 result.push(token.clone());
1923 }
1924 }
1925 }
1926
1927 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace);
1928
1929 result
1930}
1931
1932fn flush_colon_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>, suppress: bool) {
1937 if run.is_empty() {
1938 return;
1939 }
1940
1941 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
1942
1943 if !suppress && run.len() >= 2 && has_colon {
1944 let text: String = run
1945 .iter()
1946 .filter_map(|t| mergeable_text(&t.token))
1947 .collect();
1948 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1950 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1951 result.push(Spanned::new(Token::Ident(text), start..end));
1952 } else {
1953 for t in run.iter() {
1954 result.push((*t).clone());
1955 }
1956 }
1957
1958 run.clear();
1959}
1960
1961fn glob_mergeable_text(token: &Token) -> Option<String> {
1966 match token {
1967 Token::Star => Some("*".to_string()),
1968 Token::Question => Some("?".to_string()),
1969 Token::Dot => Some(".".to_string()),
1970 Token::DotDot => Some("..".to_string()),
1971 Token::Ident(s) => Some(s.clone()),
1972 Token::NumberIdent(s) => Some(s.clone()),
1973 Token::DashNumWord(s) => Some(s.clone()),
1974 Token::AtWord(s) => Some(s.clone()),
1975 Token::DottedIdent(s) => Some(s.clone()),
1976 Token::Path(s) => Some(s.clone()),
1977 Token::Int(n) => Some(n.to_string()),
1978 Token::LBracket => Some("[".to_string()),
1979 Token::RBracket => Some("]".to_string()),
1980 Token::Bang => Some("!".to_string()),
1981 Token::DotSlashPath(s) => Some(s.clone()),
1982 Token::RelativePath(s) => Some(s.clone()),
1983 Token::TildePath(s) => Some(s.clone()),
1984 Token::Tilde => Some("~".to_string()),
1985 Token::LBrace => Some("{".to_string()),
1986 Token::RBrace => Some("}".to_string()),
1987 Token::Comma => Some(",".to_string()),
1988 _ => None,
1989 }
1990}
1991
1992fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
2021 if tokens.len() < 2 {
2022 return tokens;
2023 }
2024
2025 let mut result = Vec::with_capacity(tokens.len());
2026 let mut i = 0;
2027
2028 while i < tokens.len() {
2029 let token = &tokens[i];
2030
2031 if let Token::ShortFlag(flag_name) = &token.token {
2033 let mut fused = flag_name.clone();
2035 let mut end_span = token.span.end;
2036 let mut j = i + 1;
2037
2038 while let Some(next) = tokens.get(j) {
2039 if next.span.start == end_span {
2040 if let Token::Colon = &next.token {
2041 fused.push(':');
2042 end_span = next.span.end;
2043 j += 1;
2044 continue;
2045 }
2046 }
2047 break;
2048 }
2049
2050 if j > i + 1 {
2051 let span = token.span.start..end_span;
2053 result.push(Spanned::new(Token::ShortFlag(fused), span));
2054 i = j;
2055 continue;
2056 }
2057 }
2058
2059 result.push(token.clone());
2060 i += 1;
2061 }
2062
2063 result
2064}
2065
2066fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
2090 if tokens.is_empty() {
2091 return tokens;
2092 }
2093
2094 let value_ctx = compute_value_context(&tokens);
2095 let mut result = Vec::with_capacity(tokens.len());
2096 let mut run: Vec<&Spanned<Token>> = Vec::new();
2097 let mut run_start = 0usize;
2098
2099 for (idx, token) in tokens.iter().enumerate() {
2100 if run.is_empty() {
2101 if glob_mergeable_text(&token.token).is_some() {
2102 run.push(token);
2103 run_start = idx;
2104 } else {
2105 result.push(token.clone());
2106 }
2107 continue;
2108 }
2109
2110 let Some(last) = run.last() else { unreachable!() };
2112 let adjacent = last.span.end == token.span.start;
2113
2114 if adjacent && glob_mergeable_text(&token.token).is_some() {
2115 run.push(token);
2116 } else {
2117 let followed_by_eq = matches!(token.token, Token::Eq);
2122 flush_glob_run(
2123 &mut run,
2124 &mut result,
2125 value_ctx[run_start].in_literal,
2126 followed_by_eq,
2127 );
2128 if glob_mergeable_text(&token.token).is_some() {
2129 run.push(token);
2130 run_start = idx;
2131 } else {
2132 result.push(token.clone());
2133 }
2134 }
2135 }
2136
2137 flush_glob_run(&mut run, &mut result, value_ctx[run_start].in_literal, false);
2140
2141 result
2142}
2143
2144fn flush_glob_run(
2165 run: &mut Vec<&Spanned<Token>>,
2166 result: &mut Vec<Spanned<Token>>,
2167 value_position_suppress: bool,
2168 followed_by_eq: bool,
2169) {
2170 if run.is_empty() {
2171 return;
2172 }
2173
2174 let has_bracket_pair = run.iter().any(|t| matches!(t.token, Token::LBracket))
2175 && run.iter().any(|t| matches!(t.token, Token::RBracket));
2176 let has_star_or_question = run
2177 .iter()
2178 .any(|t| matches!(t.token, Token::Star | Token::Question));
2179 let has_glob = has_star_or_question || has_bracket_pair;
2180
2181 let run_starts_with_ident = matches!(
2190 run.first().map(|t| &t.token),
2191 Some(Token::Ident(_))
2192 );
2193 let lvalue_suppress =
2194 followed_by_eq && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2195 let suppress = (value_position_suppress && has_bracket_pair) || lvalue_suppress;
2196
2197 if !suppress && run.len() >= 2 && has_glob {
2198 let text: String = run
2199 .iter()
2200 .filter_map(|t| glob_mergeable_text(&t.token))
2201 .collect();
2202 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2203 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2204 result.push(Spanned::new(Token::GlobWord(text), start..end));
2205 } else {
2206 for t in run.iter() {
2207 result.push((*t).clone());
2208 }
2209 }
2210
2211 run.clear();
2212}
2213
2214pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2224 let arith_result = preprocess_arithmetic(source)
2226 .map_err(|e| vec![Spanned::new(e, 0..source.len())])?;
2227
2228 let span_replacements = arith_result.replacements;
2232 let (preprocessed, heredocs) = preprocess_heredocs(&arith_result.text)
2233 .map_err(|e| {
2234 let span = correct_span(e.span, &span_replacements);
2235 vec![Spanned::new(e.token, span)]
2236 })?;
2237
2238 let lexer = Token::lexer(&preprocessed);
2239 let mut tokens = Vec::new();
2240 let mut errors = Vec::new();
2241
2242 for (result, span) in lexer.spanned() {
2243 let corrected_span = correct_span(span, &span_replacements);
2245 match result {
2246 Ok(token) => {
2247 if !matches!(token, Token::Comment | Token::LineContinuation) {
2249 tokens.push(Spanned::new(token, corrected_span));
2250 }
2251 }
2252 Err(err) => {
2253 errors.push(Spanned::new(err, corrected_span));
2254 }
2255 }
2256 }
2257
2258 if !errors.is_empty() {
2259 return Err(errors);
2260 }
2261
2262 let mut final_tokens = Vec::with_capacity(tokens.len());
2264 let mut i = 0;
2265
2266 while i < tokens.len() {
2267 if let Token::Ident(ref name) = tokens[i].token
2269 && name.starts_with("__KAISH_ARITH_") && name.ends_with("__")
2270 && let Some((_, expr)) = arith_result.arithmetics.iter().find(|(marker, _)| marker == name) {
2271 final_tokens.push(Spanned::new(Token::Arithmetic(expr.clone()), tokens[i].span.clone()));
2272 i += 1;
2273 continue;
2274 }
2275
2276 if matches!(tokens[i].token, Token::HereDocStart) {
2278 if i + 1 < tokens.len()
2280 && let Token::Ident(ref name) = tokens[i + 1].token
2281 && name.starts_with("__KAISH_HEREDOC_") && name.ends_with("__") {
2282 if let Some(hd) = heredocs.iter().find(|h| h.marker == *name) {
2284 let mut content = hd.body.clone();
2296 for (marker, expr) in &arith_result.arithmetics {
2297 if content.contains(marker) {
2298 let replacement = if hd.literal {
2299 format!("$(({}))", expr)
2300 } else {
2301 format!("${{__ARITH:{}__}}", expr)
2302 };
2303 content = content.replace(marker, &replacement);
2304 }
2305 }
2306 final_tokens.push(Spanned::new(Token::HereDocStart, tokens[i].span.clone()));
2307 final_tokens.push(Spanned::new(
2308 Token::HereDoc(HereDocData {
2309 content,
2310 literal: hd.literal,
2311 strip_tabs: hd.strip_tabs,
2312 body_start_offset: hd.body_start_offset,
2313 }),
2314 tokens[i + 1].span.clone(),
2315 ));
2316 i += 2;
2317 continue;
2318 }
2319 }
2320 }
2321
2322 let token = if let Token::String(ref s) = tokens[i].token {
2324 let mut new_content = s.clone();
2326 for (marker, expr) in &arith_result.arithmetics {
2327 if new_content.contains(marker) {
2328 new_content = new_content.replace(marker, &format!("${{__ARITH:{}__}}", expr));
2331 }
2332 }
2333 if new_content != *s {
2334 Spanned::new(Token::String(new_content), tokens[i].span.clone())
2335 } else {
2336 tokens[i].clone()
2337 }
2338 } else {
2339 tokens[i].clone()
2340 };
2341 final_tokens.push(token);
2342 i += 1;
2343 }
2344
2345 Ok(merge_glob_adjacent(merge_colon_adjacent(
2346 merge_flag_metachar_adjacent(final_tokens),
2347 )))
2348}
2349
2350pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2354 let lexer = Token::lexer(source);
2355 let mut tokens = Vec::new();
2356 let mut errors = Vec::new();
2357
2358 for (result, span) in lexer.spanned() {
2359 match result {
2360 Ok(token) => {
2361 tokens.push(Spanned::new(token, span));
2362 }
2363 Err(err) => {
2364 errors.push(Spanned::new(err, span));
2365 }
2366 }
2367 }
2368
2369 if errors.is_empty() {
2370 Ok(tokens)
2371 } else {
2372 Err(errors)
2373 }
2374}
2375
2376pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2378 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2380 return Err(LexerError::UnterminatedString);
2381 }
2382
2383 let inner = &source[1..source.len() - 1];
2384 let mut result = String::with_capacity(inner.len());
2385 let mut chars = inner.chars().peekable();
2386
2387 while let Some(ch) = chars.next() {
2388 if ch == '\\' {
2389 match chars.next() {
2390 Some('n') => result.push('\n'),
2391 Some('t') => result.push('\t'),
2392 Some('r') => result.push('\r'),
2393 Some('\\') => result.push('\\'),
2394 Some('"') => result.push('"'),
2395 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2398 Some('u') => {
2399 let mut hex = String::with_capacity(4);
2401 for _ in 0..4 {
2402 match chars.next() {
2403 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2404 _ => return Err(LexerError::InvalidEscape),
2405 }
2406 }
2407 let codepoint = u32::from_str_radix(&hex, 16)
2408 .map_err(|_| LexerError::InvalidEscape)?;
2409 let ch = char::from_u32(codepoint)
2410 .ok_or(LexerError::InvalidEscape)?;
2411 result.push(ch);
2412 }
2413 Some(next) => {
2415 result.push('\\');
2416 result.push(next);
2417 }
2418 None => return Err(LexerError::InvalidEscape),
2419 }
2420 } else {
2421 result.push(ch);
2422 }
2423 }
2424
2425 Ok(result)
2426}
2427
2428pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
2431 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
2433 return Err(LexerError::UnterminatedVarRef);
2434 }
2435
2436 let inner = &source[2..source.len() - 1];
2437
2438 if inner == "?" {
2440 return Ok(vec!["?".to_string()]);
2441 }
2442
2443 let mut segments = Vec::new();
2444 let mut current = String::new();
2445 let mut chars = inner.chars().peekable();
2446
2447 while let Some(ch) = chars.next() {
2448 match ch {
2449 '.' => {
2450 if !current.is_empty() {
2451 segments.push(current.clone());
2452 current.clear();
2453 }
2454 }
2455 '[' => {
2456 if !current.is_empty() {
2457 segments.push(current.clone());
2458 current.clear();
2459 }
2460 let mut index = String::from("[");
2462 while let Some(&c) = chars.peek() {
2463 if let Some(c) = chars.next() {
2464 index.push(c);
2465 }
2466 if c == ']' {
2467 break;
2468 }
2469 }
2470 segments.push(index);
2471 }
2472 _ => {
2473 current.push(ch);
2474 }
2475 }
2476 }
2477
2478 if !current.is_empty() {
2479 segments.push(current);
2480 }
2481
2482 Ok(segments)
2483}
2484
2485pub fn parse_int(source: &str) -> Result<i64, LexerError> {
2487 source.parse().map_err(|_| LexerError::InvalidNumber)
2488}
2489
2490pub fn parse_float(source: &str) -> Result<f64, LexerError> {
2492 source.parse().map_err(|_| LexerError::InvalidNumber)
2493}
2494
2495#[cfg(test)]
2496#[allow(clippy::approx_constant)]
2497mod tests {
2498 use super::*;
2499
2500 fn lex(source: &str) -> Vec<Token> {
2501 tokenize(source)
2502 .expect("lexer should succeed")
2503 .into_iter()
2504 .map(|s| s.token)
2505 .collect()
2506 }
2507
2508 #[test]
2513 fn keywords() {
2514 assert_eq!(lex("set"), vec![Token::Set]);
2515 assert_eq!(lex("if"), vec![Token::If]);
2516 assert_eq!(lex("then"), vec![Token::Then]);
2517 assert_eq!(lex("else"), vec![Token::Else]);
2518 assert_eq!(lex("elif"), vec![Token::Elif]);
2519 assert_eq!(lex("fi"), vec![Token::Fi]);
2520 assert_eq!(lex("for"), vec![Token::For]);
2521 assert_eq!(lex("in"), vec![Token::In]);
2522 assert_eq!(lex("do"), vec![Token::Do]);
2523 assert_eq!(lex("done"), vec![Token::Done]);
2524 assert_eq!(lex("case"), vec![Token::Case]);
2525 assert_eq!(lex("esac"), vec![Token::Esac]);
2526 assert_eq!(lex("function"), vec![Token::Function]);
2527 assert_eq!(lex("true"), vec![Token::True]);
2528 assert_eq!(lex("false"), vec![Token::False]);
2529 }
2530
2531 #[test]
2532 fn double_semicolon() {
2533 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
2534 assert_eq!(lex("echo \"hi\";;"), vec![
2536 Token::Ident("echo".to_string()),
2537 Token::String("hi".to_string()),
2538 Token::DoubleSemi,
2539 ]);
2540 }
2541
2542 #[test]
2543 fn type_keywords() {
2544 assert_eq!(lex("string"), vec![Token::TypeString]);
2545 assert_eq!(lex("int"), vec![Token::TypeInt]);
2546 assert_eq!(lex("float"), vec![Token::TypeFloat]);
2547 assert_eq!(lex("bool"), vec![Token::TypeBool]);
2548 }
2549
2550 #[test]
2555 fn single_char_operators() {
2556 assert_eq!(lex("="), vec![Token::Eq]);
2557 assert_eq!(lex("|"), vec![Token::Pipe]);
2558 assert_eq!(lex("&"), vec![Token::Amp]);
2559 assert_eq!(lex(">"), vec![Token::Gt]);
2560 assert_eq!(lex("<"), vec![Token::Lt]);
2561 assert_eq!(lex(";"), vec![Token::Semi]);
2562 assert_eq!(lex(":"), vec![Token::Colon]);
2563 assert_eq!(lex(","), vec![Token::Comma]);
2564 assert_eq!(lex("."), vec![Token::Dot]);
2565 }
2566
2567 #[test]
2568 fn multi_char_operators() {
2569 assert_eq!(lex("&&"), vec![Token::And]);
2570 assert_eq!(lex("||"), vec![Token::Or]);
2571 assert_eq!(lex("=="), vec![Token::EqEq]);
2572 assert_eq!(lex("!="), vec![Token::NotEq]);
2573 assert_eq!(lex("=~"), vec![Token::Match]);
2574 assert_eq!(lex("!~"), vec![Token::NotMatch]);
2575 assert_eq!(lex(">="), vec![Token::GtEq]);
2576 assert_eq!(lex("<="), vec![Token::LtEq]);
2577 assert_eq!(lex(">>"), vec![Token::GtGt]);
2578 assert_eq!(lex("2>"), vec![Token::Stderr]);
2579 assert_eq!(lex("&>"), vec![Token::Both]);
2580 }
2581
2582 #[test]
2583 fn brackets() {
2584 assert_eq!(lex("{"), vec![Token::LBrace]);
2585 assert_eq!(lex("}"), vec![Token::RBrace]);
2586 assert_eq!(lex("["), vec![Token::LBracket]);
2587 assert_eq!(lex("]"), vec![Token::RBracket]);
2588 assert_eq!(lex("("), vec![Token::LParen]);
2589 assert_eq!(lex(")"), vec![Token::RParen]);
2590 }
2591
2592 #[test]
2597 fn integers() {
2598 assert_eq!(lex("0"), vec![Token::Int(0)]);
2599 assert_eq!(lex("42"), vec![Token::Int(42)]);
2600 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
2601 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
2602 }
2603
2604 #[test]
2605 fn floats() {
2606 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
2607 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
2608 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
2609 }
2610
2611 #[test]
2612 fn strings() {
2613 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
2614 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
2615 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
2617 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
2618 }
2619
2620 #[test]
2621 fn var_refs() {
2622 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
2623 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
2624 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
2625 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
2626 }
2627
2628 #[test]
2633 fn identifiers() {
2634 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
2635 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
2636 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
2637 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
2638 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
2639 }
2640
2641 #[test]
2642 fn keyword_prefix_identifiers() {
2643 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
2645 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
2646 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
2647 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
2648 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
2649 }
2650
2651 #[test]
2656 fn assignment() {
2657 assert_eq!(
2658 lex("set X = 5"),
2659 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2660 );
2661 }
2662
2663 #[test]
2664 fn command_simple() {
2665 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
2666 assert_eq!(
2667 lex(r#"echo "hello""#),
2668 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
2669 );
2670 }
2671
2672 #[test]
2673 fn command_with_args() {
2674 assert_eq!(
2675 lex("cmd arg1 arg2"),
2676 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
2677 );
2678 }
2679
2680 #[test]
2681 fn command_with_named_args() {
2682 assert_eq!(
2683 lex("cmd key=value"),
2684 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
2685 );
2686 }
2687
2688 #[test]
2689 fn pipeline() {
2690 assert_eq!(
2691 lex("a | b | c"),
2692 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
2693 );
2694 }
2695
2696 #[test]
2697 fn if_statement() {
2698 assert_eq!(
2699 lex("if true; then echo; fi"),
2700 vec![
2701 Token::If,
2702 Token::True,
2703 Token::Semi,
2704 Token::Then,
2705 Token::Ident("echo".to_string()),
2706 Token::Semi,
2707 Token::Fi
2708 ]
2709 );
2710 }
2711
2712 #[test]
2713 fn for_loop() {
2714 assert_eq!(
2715 lex("for X in items; do echo; done"),
2716 vec![
2717 Token::For,
2718 Token::Ident("X".to_string()),
2719 Token::In,
2720 Token::Ident("items".to_string()),
2721 Token::Semi,
2722 Token::Do,
2723 Token::Ident("echo".to_string()),
2724 Token::Semi,
2725 Token::Done
2726 ]
2727 );
2728 }
2729
2730 #[test]
2735 fn whitespace_ignored() {
2736 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
2737 }
2738
2739 #[test]
2740 fn newlines_preserved() {
2741 let tokens = lex("a\nb");
2742 assert_eq!(
2743 tokens,
2744 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2745 );
2746 }
2747
2748 #[test]
2749 fn multiple_newlines() {
2750 let tokens = lex("a\n\n\nb");
2751 assert_eq!(
2752 tokens,
2753 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
2754 );
2755 }
2756
2757 #[test]
2762 fn comments_skipped() {
2763 assert_eq!(lex("# comment"), vec![]);
2764 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
2765 assert_eq!(
2766 lex("a # comment\nb"),
2767 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2768 );
2769 }
2770
2771 #[test]
2772 fn comments_preserved_when_requested() {
2773 let tokens = tokenize_with_comments("a # comment")
2774 .expect("should succeed")
2775 .into_iter()
2776 .map(|s| s.token)
2777 .collect::<Vec<_>>();
2778 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
2779 }
2780
2781 #[test]
2786 fn parse_simple_string() {
2787 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
2788 }
2789
2790 #[test]
2791 fn parse_string_with_escapes() {
2792 assert_eq!(
2793 parse_string_literal(r#""hello\nworld""#).expect("ok"),
2794 "hello\nworld"
2795 );
2796 assert_eq!(
2797 parse_string_literal(r#""tab\there""#).expect("ok"),
2798 "tab\there"
2799 );
2800 assert_eq!(
2801 parse_string_literal(r#""quote\"here""#).expect("ok"),
2802 "quote\"here"
2803 );
2804 }
2805
2806 #[test]
2807 fn parse_string_with_unicode() {
2808 assert_eq!(
2809 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
2810 "emoji ❤"
2811 );
2812 }
2813
2814 #[test]
2815 fn parse_string_with_escaped_dollar() {
2816 assert_eq!(
2819 parse_string_literal(r#""\$VAR""#).expect("ok"),
2820 "__KAISH_ESCAPED_DOLLAR__VAR"
2821 );
2822 assert_eq!(
2823 parse_string_literal(r#""cost: \$100""#).expect("ok"),
2824 "cost: __KAISH_ESCAPED_DOLLAR__100"
2825 );
2826 }
2827
2828 #[test]
2833 fn parse_simple_var() {
2834 assert_eq!(
2835 parse_var_ref("${X}").expect("ok"),
2836 vec!["X"]
2837 );
2838 }
2839
2840 #[test]
2841 fn parse_var_with_field() {
2842 assert_eq!(
2843 parse_var_ref("${VAR.field}").expect("ok"),
2844 vec!["VAR", "field"]
2845 );
2846 }
2847
2848 #[test]
2849 fn parse_var_with_index() {
2850 assert_eq!(
2851 parse_var_ref("${VAR[0]}").expect("ok"),
2852 vec!["VAR", "[0]"]
2853 );
2854 }
2855
2856 #[test]
2857 fn parse_var_nested() {
2858 assert_eq!(
2859 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
2860 vec!["VAR", "field", "[0]", "nested"]
2861 );
2862 }
2863
2864 #[test]
2865 fn parse_last_result() {
2866 assert_eq!(
2867 parse_var_ref("${?}").expect("ok"),
2868 vec!["?"]
2869 );
2870 }
2871
2872 #[test]
2877 fn parse_integers() {
2878 assert_eq!(parse_int("0").expect("ok"), 0);
2879 assert_eq!(parse_int("42").expect("ok"), 42);
2880 assert_eq!(parse_int("-1").expect("ok"), -1);
2881 }
2882
2883 #[test]
2884 fn parse_floats() {
2885 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
2886 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
2887 }
2888
2889 #[test]
2894 fn empty_input() {
2895 assert_eq!(lex(""), vec![]);
2896 }
2897
2898 #[test]
2899 fn only_whitespace() {
2900 assert_eq!(lex(" \t\t "), vec![]);
2901 }
2902
2903 #[test]
2904 fn json_array() {
2905 assert_eq!(
2906 lex(r#"[1, 2, 3]"#),
2907 vec![
2908 Token::LBracket,
2909 Token::Int(1),
2910 Token::Comma,
2911 Token::Int(2),
2912 Token::Comma,
2913 Token::Int(3),
2914 Token::RBracket
2915 ]
2916 );
2917 }
2918
2919 #[test]
2920 fn json_object() {
2921 assert_eq!(
2922 lex(r#"{"key": "value"}"#),
2923 vec![
2924 Token::LBrace,
2925 Token::String("key".to_string()),
2926 Token::Colon,
2927 Token::String("value".to_string()),
2928 Token::RBrace
2929 ]
2930 );
2931 }
2932
2933 #[test]
2934 fn redirect_operators() {
2935 assert_eq!(
2936 lex("cmd > file"),
2937 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
2938 );
2939 assert_eq!(
2940 lex("cmd >> file"),
2941 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
2942 );
2943 assert_eq!(
2944 lex("cmd 2> err"),
2945 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
2946 );
2947 assert_eq!(
2948 lex("cmd &> all"),
2949 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
2950 );
2951 }
2952
2953 #[test]
2954 fn background_job() {
2955 assert_eq!(
2956 lex("cmd &"),
2957 vec![Token::Ident("cmd".to_string()), Token::Amp]
2958 );
2959 }
2960
2961 #[test]
2962 fn command_substitution() {
2963 assert_eq!(
2964 lex("$(cmd)"),
2965 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
2966 );
2967 assert_eq!(
2968 lex("$(cmd arg)"),
2969 vec![
2970 Token::CmdSubstStart,
2971 Token::Ident("cmd".to_string()),
2972 Token::Ident("arg".to_string()),
2973 Token::RParen
2974 ]
2975 );
2976 assert_eq!(
2977 lex("$(a | b)"),
2978 vec![
2979 Token::CmdSubstStart,
2980 Token::Ident("a".to_string()),
2981 Token::Pipe,
2982 Token::Ident("b".to_string()),
2983 Token::RParen
2984 ]
2985 );
2986 }
2987
2988 #[test]
2989 fn complex_pipeline() {
2990 assert_eq!(
2991 lex(r#"cat file | grep pattern="foo" | head count=10"#),
2992 vec![
2993 Token::Ident("cat".to_string()),
2994 Token::Ident("file".to_string()),
2995 Token::Pipe,
2996 Token::Ident("grep".to_string()),
2997 Token::Ident("pattern".to_string()),
2998 Token::Eq,
2999 Token::String("foo".to_string()),
3000 Token::Pipe,
3001 Token::Ident("head".to_string()),
3002 Token::Ident("count".to_string()),
3003 Token::Eq,
3004 Token::Int(10),
3005 ]
3006 );
3007 }
3008
3009 #[test]
3014 fn short_flag() {
3015 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3016 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
3017 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
3018 }
3019
3020 #[test]
3021 fn short_flag_combined() {
3022 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
3024 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
3025 }
3026
3027 #[test]
3028 fn job_spec_lexes_as_one_token() {
3029 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
3031 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
3032 assert_eq!(
3033 lex("wait %1 %2"),
3034 vec![
3035 Token::Ident("wait".to_string()),
3036 Token::JobSpec("%1".to_string()),
3037 Token::JobSpec("%2".to_string()),
3038 ]
3039 );
3040 }
3041
3042 #[test]
3043 fn short_flag_with_internal_hyphens_is_one_token() {
3044 assert_eq!(
3048 lex("-not-a-flag"),
3049 vec![Token::ShortFlag("not-a-flag".to_string())]
3050 );
3051 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3055 assert_eq!(lex("-"), vec![Token::MinusAlone]);
3056 }
3057
3058 #[test]
3059 fn long_flag() {
3060 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
3061 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
3062 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
3063 }
3064
3065 #[test]
3066 fn double_dash() {
3067 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3069 }
3070
3071 #[test]
3072 fn flags_vs_negative_numbers() {
3073 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3075 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3077 assert_eq!(
3080 lex("-1 a"),
3081 vec![Token::Int(-1), Token::Ident("a".to_string())]
3082 );
3083 }
3084
3085 #[test]
3086 fn command_with_flags() {
3087 assert_eq!(
3088 lex("ls -l"),
3089 vec![
3090 Token::Ident("ls".to_string()),
3091 Token::ShortFlag("l".to_string()),
3092 ]
3093 );
3094 assert_eq!(
3095 lex("git commit -m"),
3096 vec![
3097 Token::Ident("git".to_string()),
3098 Token::Ident("commit".to_string()),
3099 Token::ShortFlag("m".to_string()),
3100 ]
3101 );
3102 assert_eq!(
3103 lex("git push --force"),
3104 vec![
3105 Token::Ident("git".to_string()),
3106 Token::Ident("push".to_string()),
3107 Token::LongFlag("force".to_string()),
3108 ]
3109 );
3110 }
3111
3112 #[test]
3113 fn flag_with_value() {
3114 assert_eq!(
3115 lex(r#"git commit -m "message""#),
3116 vec![
3117 Token::Ident("git".to_string()),
3118 Token::Ident("commit".to_string()),
3119 Token::ShortFlag("m".to_string()),
3120 Token::String("message".to_string()),
3121 ]
3122 );
3123 assert_eq!(
3124 lex(r#"--message="hello""#),
3125 vec![
3126 Token::LongFlag("message".to_string()),
3127 Token::Eq,
3128 Token::String("hello".to_string()),
3129 ]
3130 );
3131 }
3132
3133 #[test]
3134 fn end_of_flags_marker() {
3135 assert_eq!(
3136 lex("git checkout -- file"),
3137 vec![
3138 Token::Ident("git".to_string()),
3139 Token::Ident("checkout".to_string()),
3140 Token::DoubleDash,
3141 Token::Ident("file".to_string()),
3142 ]
3143 );
3144 }
3145
3146 #[test]
3151 fn local_keyword() {
3152 assert_eq!(lex("local"), vec![Token::Local]);
3153 assert_eq!(
3154 lex("local X = 5"),
3155 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3156 );
3157 }
3158
3159 #[test]
3160 fn simple_var_ref() {
3161 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
3162 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
3163 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
3164 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
3165 }
3166
3167 #[test]
3168 fn simple_var_ref_in_command() {
3169 assert_eq!(
3170 lex("echo $NAME"),
3171 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
3172 );
3173 }
3174
3175 #[test]
3176 fn single_quoted_strings() {
3177 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
3178 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
3179 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
3180 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
3182 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
3183 }
3184
3185 #[test]
3186 fn test_brackets() {
3187 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
3189 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
3190 assert_eq!(
3191 lex("[[ -f file ]]"),
3192 vec![
3193 Token::LBracket,
3194 Token::LBracket,
3195 Token::ShortFlag("f".to_string()),
3196 Token::Ident("file".to_string()),
3197 Token::RBracket,
3198 Token::RBracket
3199 ]
3200 );
3201 }
3202
3203 #[test]
3204 fn test_expression_syntax() {
3205 assert_eq!(
3206 lex(r#"[[ $X == "value" ]]"#),
3207 vec![
3208 Token::LBracket,
3209 Token::LBracket,
3210 Token::SimpleVarRef("X".to_string()),
3211 Token::EqEq,
3212 Token::String("value".to_string()),
3213 Token::RBracket,
3214 Token::RBracket
3215 ]
3216 );
3217 }
3218
3219 #[test]
3220 fn bash_style_assignment() {
3221 assert_eq!(
3223 lex(r#"NAME="value""#),
3224 vec![
3225 Token::Ident("NAME".to_string()),
3226 Token::Eq,
3227 Token::String("value".to_string())
3228 ]
3229 );
3230 }
3231
3232 #[test]
3233 fn positional_params() {
3234 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
3235 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
3236 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
3237 assert_eq!(lex("$@"), vec![Token::AllArgs]);
3238 assert_eq!(lex("$#"), vec![Token::ArgCount]);
3239 }
3240
3241 #[test]
3242 fn positional_in_context() {
3243 assert_eq!(
3244 lex("echo $1 $2"),
3245 vec![
3246 Token::Ident("echo".to_string()),
3247 Token::Positional(1),
3248 Token::Positional(2),
3249 ]
3250 );
3251 }
3252
3253 #[test]
3254 fn var_length() {
3255 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3256 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
3257 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
3258 }
3259
3260 #[test]
3261 fn var_length_with_subscript() {
3262 assert_eq!(lex("${#u[tags]}"), vec![Token::VarLength("u[tags]".to_string())]);
3265 assert_eq!(lex("${#a[0]}"), vec![Token::VarLength("a[0]".to_string())]);
3266 assert_eq!(lex("${#a[b][c]}"), vec![Token::VarLength("a[b][c]".to_string())]);
3267 assert_eq!(lex("${#r[$k]}"), vec![Token::VarLength("r[$k]".to_string())]);
3268 }
3269
3270 #[test]
3271 fn var_length_in_context() {
3272 assert_eq!(
3273 lex("echo ${#NAME}"),
3274 vec![
3275 Token::Ident("echo".to_string()),
3276 Token::VarLength("NAME".to_string()),
3277 ]
3278 );
3279 }
3280
3281 #[test]
3286 fn plus_flag() {
3287 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
3289 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
3290 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
3291 }
3292
3293 #[test]
3294 fn set_with_plus_flag() {
3295 assert_eq!(
3296 lex("set +e"),
3297 vec![
3298 Token::Set,
3299 Token::PlusFlag("e".to_string()),
3300 ]
3301 );
3302 }
3303
3304 #[test]
3305 fn set_with_multiple_flags() {
3306 assert_eq!(
3307 lex("set -e -u"),
3308 vec![
3309 Token::Set,
3310 Token::ShortFlag("e".to_string()),
3311 Token::ShortFlag("u".to_string()),
3312 ]
3313 );
3314 }
3315
3316 #[test]
3317 fn flags_vs_negative_numbers_edge_cases() {
3318 assert_eq!(
3320 lex("-1 a"),
3321 vec![Token::Int(-1), Token::Ident("a".to_string())]
3322 );
3323 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3325 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3327 }
3328
3329 #[test]
3330 fn single_dash_is_minus_alone() {
3331 let result = tokenize("-").expect("should lex");
3333 assert_eq!(result.len(), 1);
3334 assert!(matches!(result[0].token, Token::MinusAlone));
3335 }
3336
3337 #[test]
3338 fn plus_bare_for_date_format() {
3339 let result = tokenize("+%s").expect("should lex");
3341 assert_eq!(result.len(), 1);
3342 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
3343
3344 let result = tokenize("+%Y-%m-%d").expect("should lex");
3346 assert_eq!(result.len(), 1);
3347 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
3348 }
3349
3350 #[test]
3351 fn plus_flag_still_works() {
3352 let result = tokenize("+e").expect("should lex");
3354 assert_eq!(result.len(), 1);
3355 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
3356 }
3357
3358 #[test]
3359 fn while_keyword_vs_while_loop() {
3360 assert_eq!(lex("while"), vec![Token::While]);
3362 assert_eq!(
3364 lex("while true"),
3365 vec![Token::While, Token::True]
3366 );
3367 }
3368
3369 #[test]
3370 fn control_flow_keywords() {
3371 assert_eq!(lex("break"), vec![Token::Break]);
3372 assert_eq!(lex("continue"), vec![Token::Continue]);
3373 assert_eq!(lex("return"), vec![Token::Return]);
3374 assert_eq!(lex("exit"), vec![Token::Exit]);
3375 }
3376
3377 #[test]
3378 fn control_flow_with_numbers() {
3379 assert_eq!(
3380 lex("break 2"),
3381 vec![Token::Break, Token::Int(2)]
3382 );
3383 assert_eq!(
3384 lex("continue 3"),
3385 vec![Token::Continue, Token::Int(3)]
3386 );
3387 assert_eq!(
3388 lex("exit 1"),
3389 vec![Token::Exit, Token::Int(1)]
3390 );
3391 }
3392
3393 #[test]
3398 fn heredoc_simple() {
3399 let source = "cat <<EOF\nhello\nworld\nEOF";
3400 let tokens = lex(source);
3401 assert_eq!(tokens, vec![
3403 Token::Ident("cat".to_string()),
3404 Token::HereDocStart,
3405 Token::HereDoc(HereDocData {
3406 content: "hello\nworld\n".to_string(),
3407 literal: false,
3408 strip_tabs: false,
3409 body_start_offset: 10,
3410 }),
3411 Token::Newline,
3412 ]);
3413 }
3414
3415 #[test]
3416 fn heredoc_empty() {
3417 let source = "cat <<EOF\nEOF";
3418 let tokens = lex(source);
3419 assert_eq!(tokens, vec![
3420 Token::Ident("cat".to_string()),
3421 Token::HereDocStart,
3422 Token::HereDoc(HereDocData {
3423 content: "".to_string(),
3424 literal: false,
3425 strip_tabs: false,
3426 body_start_offset: 10,
3427 }),
3428 Token::Newline,
3429 ]);
3430 }
3431
3432 #[test]
3433 fn heredoc_with_special_chars() {
3434 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
3435 let tokens = lex(source);
3436 assert_eq!(tokens, vec![
3437 Token::Ident("cat".to_string()),
3438 Token::HereDocStart,
3439 Token::HereDoc(HereDocData {
3440 content: "$VAR and \"quoted\" 'single'\n".to_string(),
3441 literal: false,
3442 strip_tabs: false,
3443 body_start_offset: 10,
3444 }),
3445 Token::Newline,
3446 ]);
3447 }
3448
3449 #[test]
3450 fn heredoc_multiline() {
3451 let source = "cat <<END\nline1\nline2\nline3\nEND";
3452 let tokens = lex(source);
3453 assert_eq!(tokens, vec![
3454 Token::Ident("cat".to_string()),
3455 Token::HereDocStart,
3456 Token::HereDoc(HereDocData {
3457 content: "line1\nline2\nline3\n".to_string(),
3458 literal: false,
3459 strip_tabs: false,
3460 body_start_offset: 10,
3461 }),
3462 Token::Newline,
3463 ]);
3464 }
3465
3466 #[test]
3467 fn heredoc_in_command() {
3468 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
3469 let tokens = lex(source);
3470 assert_eq!(tokens, vec![
3471 Token::Ident("cat".to_string()),
3472 Token::HereDocStart,
3473 Token::HereDoc(HereDocData {
3474 content: "hello\n".to_string(),
3475 literal: false,
3476 strip_tabs: false,
3477 body_start_offset: 10,
3478 }),
3479 Token::Newline,
3480 Token::Ident("echo".to_string()),
3481 Token::Ident("goodbye".to_string()),
3482 ]);
3483 }
3484
3485 #[test]
3486 fn heredoc_strip_tabs() {
3487 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
3488 let tokens = lex(source);
3489 assert_eq!(tokens, vec![
3493 Token::Ident("cat".to_string()),
3494 Token::HereDocStart,
3495 Token::HereDoc(HereDocData {
3496 content: "\thello\n\tworld\n".to_string(),
3497 literal: false,
3498 strip_tabs: true,
3499 body_start_offset: 11,
3500 }),
3501 Token::Newline,
3502 ]);
3503 }
3504
3505 #[test]
3510 fn arithmetic_simple() {
3511 let source = "$((1 + 2))";
3512 let tokens = lex(source);
3513 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
3514 }
3515
3516 #[test]
3517 fn arithmetic_in_assignment() {
3518 let source = "X=$((5 * 3))";
3519 let tokens = lex(source);
3520 assert_eq!(tokens, vec![
3521 Token::Ident("X".to_string()),
3522 Token::Eq,
3523 Token::Arithmetic("5 * 3".to_string()),
3524 ]);
3525 }
3526
3527 #[test]
3528 fn arithmetic_with_nested_parens() {
3529 let source = "$((2 * (3 + 4)))";
3530 let tokens = lex(source);
3531 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
3532 }
3533
3534 #[test]
3535 fn arithmetic_with_variable() {
3536 let source = "$((X + 1))";
3537 let tokens = lex(source);
3538 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
3539 }
3540
3541 #[test]
3542 fn arithmetic_command_subst_not_confused() {
3543 let source = "$(echo hello)";
3545 let tokens = lex(source);
3546 assert_eq!(tokens, vec![
3547 Token::CmdSubstStart,
3548 Token::Ident("echo".to_string()),
3549 Token::Ident("hello".to_string()),
3550 Token::RParen,
3551 ]);
3552 }
3553
3554 #[test]
3555 fn arithmetic_nesting_limit() {
3556 let open_parens = "(".repeat(300);
3558 let close_parens = ")".repeat(300);
3559 let source = format!("$(({}1{}))", open_parens, close_parens);
3560 let result = tokenize(&source);
3561 assert!(result.is_err());
3562 let errors = result.unwrap_err();
3563 assert_eq!(errors.len(), 1);
3564 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
3565 }
3566
3567 #[test]
3568 fn arithmetic_nesting_within_limit() {
3569 let source = "$((((1 + 2) * 3)))";
3571 let tokens = lex(source);
3572 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
3573 }
3574
3575 #[test]
3587 fn arithmetic_after_apostrophe_in_comment() {
3588 let source = "# this doesn't work\necho $((1+2))";
3591 let tokens = lex(source);
3592 assert_eq!(tokens, vec![
3593 Token::Newline,
3594 Token::Ident("echo".to_string()),
3595 Token::Arithmetic("1+2".to_string()),
3596 ]);
3597 }
3598
3599 #[test]
3600 fn arithmetic_inside_comment_is_not_expanded() {
3601 let source = "# the $((y)) syntax explained\necho hello";
3603 let tokens = lex(source);
3604 assert_eq!(tokens, vec![
3605 Token::Newline,
3606 Token::Ident("echo".to_string()),
3607 Token::Ident("hello".to_string()),
3608 ]);
3609 }
3610
3611 #[test]
3612 fn backticked_arithmetic_in_comment_is_not_expanded() {
3613 let source = "# the `$((x))` syntax explained\necho $((3+4))";
3617 let tokens = lex(source);
3618 assert_eq!(tokens, vec![
3619 Token::Newline,
3620 Token::Ident("echo".to_string()),
3621 Token::Arithmetic("3+4".to_string()),
3622 ]);
3623 }
3624
3625 #[test]
3626 fn arithmetic_still_works_outside_comments() {
3627 let source = "X=$((1+2)); Y=$((3*4))";
3630 let tokens = lex(source);
3631 assert_eq!(tokens, vec![
3632 Token::Ident("X".to_string()),
3633 Token::Eq,
3634 Token::Arithmetic("1+2".to_string()),
3635 Token::Semi,
3636 Token::Ident("Y".to_string()),
3637 Token::Eq,
3638 Token::Arithmetic("3*4".to_string()),
3639 ]);
3640 }
3641
3642 #[test]
3643 fn arithmetic_inside_double_quotes_still_expands() {
3644 let source = "echo \"# $((1+2))\"";
3647 let tokens = lex(source);
3648 assert_eq!(tokens.len(), 2);
3653 assert!(matches!(tokens[0], Token::Ident(_)));
3654 assert!(matches!(tokens[1], Token::String(_)));
3655 }
3656
3657 #[test]
3670 fn backtick_in_source_is_rejected() {
3671 let result = tokenize("echo `date`");
3672 assert!(result.is_err());
3673 let errors = result.unwrap_err();
3674 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
3675 }
3676
3677 #[test]
3678 fn backtick_in_comment_is_just_comment_text() {
3679 let source = "# use `date` here\necho hi";
3682 let tokens = lex(source);
3683 assert_eq!(tokens, vec![
3684 Token::Newline,
3685 Token::Ident("echo".to_string()),
3686 Token::Ident("hi".to_string()),
3687 ]);
3688 }
3689
3690 #[test]
3691 fn backtick_in_single_quoted_string_is_literal() {
3692 let source = "echo '`date`'";
3695 let tokens = lex(source);
3696 assert_eq!(tokens, vec![
3697 Token::Ident("echo".to_string()),
3698 Token::SingleString("`date`".to_string()),
3699 ]);
3700 }
3701
3702 #[test]
3703 fn backtick_in_double_quoted_string_is_literal() {
3704 let source = "echo \"`date`\"";
3709 let tokens = lex(source);
3710 assert_eq!(tokens.len(), 2);
3711 assert!(matches!(tokens[0], Token::Ident(_)));
3712 match &tokens[1] {
3713 Token::String(s) => assert!(s.contains('`')),
3714 other => panic!("expected Token::String, got {:?}", other),
3715 }
3716 }
3717
3718 #[test]
3719 fn backtick_in_heredoc_body_is_preserved() {
3720 let source = "cat <<EOF\n`date`\nEOF\n";
3723 let tokens = lex(source);
3724 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
3725 assert!(heredoc.is_some(), "expected a HereDoc token");
3726 if let Some(Token::HereDoc(d)) = heredoc {
3727 assert!(d.content.contains('`'));
3728 }
3729 }
3730
3731 #[test]
3736 fn token_categories() {
3737 assert_eq!(Token::If.category(), TokenCategory::Keyword);
3739 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
3740 assert_eq!(Token::For.category(), TokenCategory::Keyword);
3741 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
3742 assert_eq!(Token::True.category(), TokenCategory::Keyword);
3743 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
3744
3745 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
3747 assert_eq!(Token::And.category(), TokenCategory::Operator);
3748 assert_eq!(Token::Or.category(), TokenCategory::Operator);
3749 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
3750 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
3751
3752 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
3754 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
3755 assert_eq!(
3756 Token::HereDoc(HereDocData {
3757 content: "test".to_string(),
3758 literal: false,
3759 strip_tabs: false,
3760 body_start_offset: 0,
3761 }).category(),
3762 TokenCategory::String,
3763 );
3764
3765 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
3767 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
3768 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
3769
3770 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
3772 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
3773 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
3774 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
3775 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
3776 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
3777 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
3778
3779 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
3781 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
3782 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
3783 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
3784
3785 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
3787 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
3788 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
3789 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
3790
3791 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
3793
3794 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
3796
3797 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
3799 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
3800 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
3801
3802 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
3804 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
3805 }
3806
3807 #[test]
3808 fn test_heredoc_piped_to_command() {
3809 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
3812 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
3813 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
3814 assert!(heredoc_pos.is_some(), "should have a heredoc token");
3815 assert!(pipe_pos.is_some(), "should have a pipe token");
3816 assert!(
3817 pipe_pos.unwrap() > heredoc_pos.unwrap(),
3818 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
3819 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
3820 );
3821 }
3822
3823 #[test]
3824 fn test_heredoc_standalone_still_works() {
3825 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
3827 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
3828 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
3829 }
3830
3831 #[test]
3832 fn test_heredoc_preserves_leading_empty_lines() {
3833 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
3835 let heredoc = tokens.iter().find_map(|t| {
3836 if let Token::HereDoc(data) = &t.token {
3837 Some(data.clone())
3838 } else {
3839 None
3840 }
3841 });
3842 assert!(heredoc.is_some(), "should have a heredoc token");
3843 let data = heredoc.unwrap();
3844 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
3845 assert_eq!(data.content, "\nhello\n");
3846 }
3847
3848 #[test]
3849 fn test_heredoc_quoted_delimiter_sets_literal() {
3850 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
3852 let heredoc = tokens.iter().find_map(|t| {
3853 if let Token::HereDoc(data) = &t.token {
3854 Some(data.clone())
3855 } else {
3856 None
3857 }
3858 });
3859 assert!(heredoc.is_some(), "should have a heredoc token");
3860 let data = heredoc.unwrap();
3861 assert!(data.literal, "quoted delimiter should set literal=true");
3862 assert_eq!(data.content, "hello $HOME\n");
3863 }
3864
3865 #[test]
3866 fn test_heredoc_unquoted_delimiter_not_literal() {
3867 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
3869 let heredoc = tokens.iter().find_map(|t| {
3870 if let Token::HereDoc(data) = &t.token {
3871 Some(data.clone())
3872 } else {
3873 None
3874 }
3875 });
3876 assert!(heredoc.is_some(), "should have a heredoc token");
3877 let data = heredoc.unwrap();
3878 assert!(!data.literal, "unquoted delimiter should have literal=false");
3879 }
3880
3881 #[test]
3886 fn colon_double_in_word() {
3887 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
3888 }
3889
3890 #[test]
3891 fn colon_single_in_word() {
3892 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
3893 }
3894
3895 #[test]
3896 fn colon_with_port() {
3897 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
3898 }
3899
3900 #[test]
3901 fn colon_standalone() {
3902 assert_eq!(lex(":"), vec![Token::Colon]);
3903 }
3904
3905 #[test]
3906 fn colon_spaced_no_merge() {
3907 assert_eq!(
3908 lex("foo : bar"),
3909 vec![
3910 Token::Ident("foo".into()),
3911 Token::Colon,
3912 Token::Ident("bar".into()),
3913 ]
3914 );
3915 }
3916
3917 #[test]
3918 fn colon_in_command_arg() {
3919 assert_eq!(
3920 lex("echo foo::bar"),
3921 vec![
3922 Token::Ident("echo".into()),
3923 Token::Ident("foo::bar".into()),
3924 ]
3925 );
3926 }
3927
3928 #[test]
3929 fn colon_trailing() {
3930 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
3932 }
3933
3934 #[test]
3935 fn colon_leading() {
3936 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
3938 }
3939
3940 #[test]
3941 fn colon_with_path() {
3942 assert_eq!(
3944 lex("/usr/bin:8080"),
3945 vec![Token::Ident("/usr/bin:8080".into())]
3946 );
3947 }
3948
3949 #[test]
3954 fn is_keyword_covers_control_flow() {
3955 for t in [
3956 Token::While,
3957 Token::Return,
3958 Token::Break,
3959 Token::Continue,
3960 Token::Exit,
3961 ] {
3962 assert!(t.is_keyword(), "{t:?} should be a keyword");
3963 }
3964 }
3965
3966 #[test]
3967 fn starts_statement_covers_while() {
3968 assert!(Token::While.starts_statement());
3969 }
3970
3971 #[test]
3972 fn is_keyword_rejects_operators() {
3973 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
3974 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
3975 }
3976 }
3977}