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("..")]
359 DotDot,
360
361 #[token(".")]
362 Dot,
363
364 #[regex(r"~[a-zA-Z0-9_./+-]+", lex_tilde_path, priority = 3)]
366 TildePath(String),
367
368 #[token("~")]
370 Tilde,
371
372 #[regex(r"\.\./[a-zA-Z0-9_./-]+", lex_relative_path, priority = 3)]
378 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.-]*/[a-zA-Z0-9_./-]*", lex_relative_path, priority = 3)]
379 RelativePath(String),
380
381 #[regex(r"\./[a-zA-Z0-9_./-]+", lex_dot_slash_path, priority = 3)]
383 DotSlashPath(String),
384
385 #[regex(r"\.[a-zA-Z_][a-zA-Z0-9_.-]*", lex_dotted_ident, priority = 3)]
391 DottedIdent(String),
392
393 #[token("{")]
394 LBrace,
395
396 #[token("}")]
397 RBrace,
398
399 #[token("[")]
400 LBracket,
401
402 #[token("]")]
403 RBracket,
404
405 #[token("(")]
406 LParen,
407
408 #[token(")")]
409 RParen,
410
411 #[token("*")]
412 Star,
413
414 #[token("!")]
415 Bang,
416
417 #[token("?")]
418 Question,
419
420 GlobWord(String),
423
424 Arithmetic(String),
431
432 #[token("$(")]
434 CmdSubstStart,
435
436 #[regex(r"--[a-zA-Z][a-zA-Z0-9-]*", lex_long_flag, priority = 3)]
442 LongFlag(String),
443
444 #[regex(r"-[a-zA-Z][a-zA-Z0-9-]*", lex_short_flag, priority = 3)]
451 ShortFlag(String),
452
453 #[regex(r"\+[a-zA-Z][a-zA-Z0-9]*", lex_plus_flag, priority = 3)]
455 PlusFlag(String),
456
457 #[token("--")]
459 DoubleDash,
460
461 #[regex(r"\+[^a-zA-Z\s][^\s]*", lex_plus_bare, priority = 2)]
464 PlusBare(String),
465
466 #[regex(r"-[^a-zA-Z0-9\s\-][^\s]*", lex_minus_bare, priority = 1)]
470 MinusBare(String),
471
472 #[regex(r"%[0-9]+", lex_job_spec)]
476 JobSpec(String),
477
478 #[token("-")]
482 MinusAlone,
483
484 #[regex(r#""([^"\\]|\\.)*""#, lex_string)]
490 String(String),
491
492 #[regex(r"'[^']*'", lex_single_string)]
494 SingleString(String),
495
496 #[regex(r"\$\{[^}]+\}", lex_varref)]
498 VarRef(String),
499
500 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", lex_simple_varref)]
502 SimpleVarRef(String),
503
504 #[regex(r"\$[0-9]", lex_positional)]
506 Positional(usize),
507
508 #[token("$@")]
510 AllArgs,
511
512 #[token("$#")]
514 ArgCount,
515
516 #[token("$?")]
518 LastExitCode,
519
520 #[token("$$")]
522 CurrentPid,
523
524 #[regex(r"\$\{#[a-zA-Z_][a-zA-Z0-9_]*\}", lex_var_length)]
526 VarLength(String),
527
528 HereDoc(HereDocData),
531
532 #[regex(r"-?[0-9]+", lex_int, priority = 2)]
534 Int(i64),
535
536 #[regex(r"-?[0-9]+\.[0-9]+", lex_float)]
538 Float(f64),
539
540 #[regex(r"[0-9]+[a-zA-Z_][a-zA-Z0-9_.-]*", lex_number_ident, priority = 3)]
549 NumberIdent(String),
550
551 #[regex(r"[0-9]+(\.[0-9]+)?(-[0-9a-zA-Z._]+)+", lex_slice_word, priority = 3)]
562 #[regex(r"-[0-9]+[a-zA-Z_][0-9a-zA-Z._-]*", lex_slice_word, priority = 3)]
563 DashNumWord(String),
564
565 #[regex(r"@[a-zA-Z0-9_./@-]*", lex_slice_word, priority = 3)]
570 AtWord(String),
571
572 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
574 InvalidFloatNoLeading,
575
576 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
579 InvalidFloatNoTrailing,
580
581 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
587 Path(String),
588
589 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.@-]*", lex_ident)]
597 Ident(String),
598
599 #[regex(r"#[^\n\r]*", allow_greedy = true)]
605 Comment,
606
607 #[regex(r"\n|\r\n")]
609 Newline,
610
611 #[regex(r"\\[ \t]*(\n|\r\n)")]
613 LineContinuation,
614
615 #[token("`", reject_backtick)]
624 BacktickRejected,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
632pub enum TokenCategory {
633 Keyword,
635 Operator,
637 String,
639 Number,
641 Variable,
643 Comment,
645 Punctuation,
647 Command,
649 Path,
651 Flag,
653 Error,
655}
656
657impl Token {
658 pub fn category(&self) -> TokenCategory {
660 match self {
661 Token::If
663 | Token::Then
664 | Token::Else
665 | Token::Elif
666 | Token::Fi
667 | Token::For
668 | Token::In
669 | Token::Do
670 | Token::Done
671 | Token::While
672 | Token::Case
673 | Token::Esac
674 | Token::Function
675 | Token::Return
676 | Token::Break
677 | Token::Continue
678 | Token::Exit
679 | Token::Set
680 | Token::Local
681 | Token::True
682 | Token::False
683 | Token::TypeString
684 | Token::TypeInt
685 | Token::TypeFloat
686 | Token::TypeBool => TokenCategory::Keyword,
687
688 Token::Pipe
690 | Token::And
691 | Token::Or
692 | Token::Amp
693 | Token::Eq
694 | Token::EqEq
695 | Token::NotEq
696 | Token::Match
697 | Token::NotMatch
698 | Token::Lt
699 | Token::Gt
700 | Token::LtEq
701 | Token::GtEq
702 | Token::GtGt
703 | Token::Stderr
704 | Token::Both
705 | Token::HereDocStart
706 | Token::HereString
707 | Token::StderrToStdout
708 | Token::StdoutToStderr
709 | Token::StdoutToStderr2 => TokenCategory::Operator,
710
711 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
713
714 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
716
717 Token::VarRef(_)
719 | Token::SimpleVarRef(_)
720 | Token::Positional(_)
721 | Token::AllArgs
722 | Token::ArgCount
723 | Token::VarLength(_)
724 | Token::LastExitCode
725 | Token::CurrentPid => TokenCategory::Variable,
726
727 Token::LongFlag(_)
729 | Token::ShortFlag(_)
730 | Token::PlusFlag(_)
731 | Token::DoubleDash => TokenCategory::Flag,
732
733 Token::Semi
735 | Token::DoubleSemi
736 | Token::Colon
737 | Token::Comma
738 | Token::Dot
739 | Token::LParen
740 | Token::RParen
741 | Token::LBrace
742 | Token::RBrace
743 | Token::LBracket
744 | Token::RBracket
745 | Token::Bang
746 | Token::Question
747 | Token::Star
748 | Token::Newline
749 | Token::LineContinuation
750 | Token::CmdSubstStart => TokenCategory::Punctuation,
751
752 Token::GlobWord(_) => TokenCategory::Path,
754
755 Token::Comment => TokenCategory::Comment,
757
758 Token::Path(_)
760 | Token::TildePath(_)
761 | Token::RelativePath(_)
762 | Token::Tilde
763 | Token::DotDot
764 | Token::DotSlashPath(_) => TokenCategory::Path,
765
766 Token::Ident(_)
768 | Token::PlusBare(_)
769 | Token::MinusBare(_)
770 | Token::MinusAlone
771 | Token::NumberIdent(_)
772 | Token::DashNumWord(_)
773 | Token::AtWord(_)
774 | Token::DottedIdent(_)
775 | Token::JobSpec(_) => TokenCategory::Command,
776
777 Token::InvalidFloatNoLeading
779 | Token::InvalidFloatNoTrailing
780 | Token::BacktickRejected => TokenCategory::Error,
781 }
782 }
783}
784
785fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
787 parse_string_literal(lex.slice())
788}
789
790fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
792 let s = lex.slice();
793 s[1..s.len() - 1].to_string()
795}
796
797fn lex_varref(lex: &mut logos::Lexer<Token>) -> String {
799 lex.slice().to_string()
801}
802
803fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
805 lex.slice()[1..].to_string()
807}
808
809fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
811 lex.slice()[1..].parse().unwrap_or(0)
813}
814
815fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
817 let s = lex.slice();
819 s[3..s.len() - 1].to_string()
820}
821
822fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
824 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
825}
826
827fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
829 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
830}
831
832fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
836 lex.slice().to_string()
837}
838
839fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
841 lex.slice().to_string()
842}
843
844fn lex_slice_word(lex: &mut logos::Lexer<Token>) -> String {
847 lex.slice().to_string()
848}
849
850fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
853 Err(LexerError::InvalidFloatNoLeading)
854}
855
856fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
860 Err(LexerError::BackticksNotSupported)
861}
862
863fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
866 Err(LexerError::InvalidFloatNoTrailing)
867}
868
869fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
871 let s = lex.slice();
872
873 match s.to_lowercase().as_str() {
876 "true" | "false" if s != "true" && s != "false" => {
877 return Err(LexerError::AmbiguousBoolean(s.to_string()));
878 }
879 _ => {}
880 }
881
882 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
884 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
885 }
886
887 Ok(s.to_string())
888}
889
890fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
892 lex.slice()[2..].to_string()
894}
895
896fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
898 lex.slice()[1..].to_string()
900}
901
902fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
904 lex.slice()[1..].to_string()
906}
907
908fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
910 lex.slice().to_string()
911}
912
913fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
915 lex.slice().to_string()
916}
917
918fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
920 lex.slice().to_string()
921}
922
923fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
925 lex.slice().to_string()
926}
927
928fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
930 lex.slice().to_string()
931}
932
933fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
935 lex.slice().to_string()
936}
937
938fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
940 lex.slice().to_string()
941}
942
943impl fmt::Display for Token {
944 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
945 match self {
946 Token::Set => write!(f, "set"),
947 Token::Local => write!(f, "local"),
948 Token::If => write!(f, "if"),
949 Token::Then => write!(f, "then"),
950 Token::Else => write!(f, "else"),
951 Token::Elif => write!(f, "elif"),
952 Token::Fi => write!(f, "fi"),
953 Token::For => write!(f, "for"),
954 Token::While => write!(f, "while"),
955 Token::In => write!(f, "in"),
956 Token::Do => write!(f, "do"),
957 Token::Done => write!(f, "done"),
958 Token::Case => write!(f, "case"),
959 Token::Esac => write!(f, "esac"),
960 Token::Function => write!(f, "function"),
961 Token::Break => write!(f, "break"),
962 Token::Continue => write!(f, "continue"),
963 Token::Return => write!(f, "return"),
964 Token::Exit => write!(f, "exit"),
965 Token::True => write!(f, "true"),
966 Token::False => write!(f, "false"),
967 Token::TypeString => write!(f, "string"),
968 Token::TypeInt => write!(f, "int"),
969 Token::TypeFloat => write!(f, "float"),
970 Token::TypeBool => write!(f, "bool"),
971 Token::And => write!(f, "&&"),
972 Token::Or => write!(f, "||"),
973 Token::EqEq => write!(f, "=="),
974 Token::NotEq => write!(f, "!="),
975 Token::Match => write!(f, "=~"),
976 Token::NotMatch => write!(f, "!~"),
977 Token::GtEq => write!(f, ">="),
978 Token::LtEq => write!(f, "<="),
979 Token::GtGt => write!(f, ">>"),
980 Token::StderrToStdout => write!(f, "2>&1"),
981 Token::StdoutToStderr => write!(f, "1>&2"),
982 Token::StdoutToStderr2 => write!(f, ">&2"),
983 Token::Stderr => write!(f, "2>"),
984 Token::Both => write!(f, "&>"),
985 Token::HereDocStart => write!(f, "<<"),
986 Token::HereString => write!(f, "<<<"),
987 Token::DoubleSemi => write!(f, ";;"),
988 Token::Eq => write!(f, "="),
989 Token::Pipe => write!(f, "|"),
990 Token::Amp => write!(f, "&"),
991 Token::Gt => write!(f, ">"),
992 Token::Lt => write!(f, "<"),
993 Token::Semi => write!(f, ";"),
994 Token::Colon => write!(f, ":"),
995 Token::Comma => write!(f, ","),
996 Token::Dot => write!(f, "."),
997 Token::DotDot => write!(f, ".."),
998 Token::Tilde => write!(f, "~"),
999 Token::TildePath(s) => write!(f, "{}", s),
1000 Token::RelativePath(s) => write!(f, "{}", s),
1001 Token::DotSlashPath(s) => write!(f, "{}", s),
1002 Token::LBrace => write!(f, "{{"),
1003 Token::RBrace => write!(f, "}}"),
1004 Token::LBracket => write!(f, "["),
1005 Token::RBracket => write!(f, "]"),
1006 Token::LParen => write!(f, "("),
1007 Token::RParen => write!(f, ")"),
1008 Token::Star => write!(f, "*"),
1009 Token::Bang => write!(f, "!"),
1010 Token::Question => write!(f, "?"),
1011 Token::GlobWord(s) => write!(f, "GLOB({})", s),
1012 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
1013 Token::CmdSubstStart => write!(f, "$("),
1014 Token::LongFlag(s) => write!(f, "--{}", s),
1015 Token::ShortFlag(s) => write!(f, "-{}", s),
1016 Token::PlusFlag(s) => write!(f, "+{}", s),
1017 Token::DoubleDash => write!(f, "--"),
1018 Token::PlusBare(s) => write!(f, "{}", s),
1019 Token::MinusBare(s) => write!(f, "{}", s),
1020 Token::JobSpec(s) => write!(f, "{}", s),
1021 Token::MinusAlone => write!(f, "-"),
1022 Token::String(s) => write!(f, "STRING({:?})", s),
1023 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
1024 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
1025 Token::VarRef(v) => write!(f, "VARREF({})", v),
1026 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
1027 Token::Positional(n) => write!(f, "${}", n),
1028 Token::AllArgs => write!(f, "$@"),
1029 Token::ArgCount => write!(f, "$#"),
1030 Token::LastExitCode => write!(f, "$?"),
1031 Token::CurrentPid => write!(f, "$$"),
1032 Token::VarLength(v) => write!(f, "${{#{}}}", v),
1033 Token::Int(n) => write!(f, "INT({})", n),
1034 Token::Float(n) => write!(f, "FLOAT({})", n),
1035 Token::Path(s) => write!(f, "PATH({})", s),
1036 Token::Ident(s) => write!(f, "IDENT({})", s),
1037 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1038 Token::DashNumWord(s) => write!(f, "DASHNUM({})", s),
1039 Token::AtWord(s) => write!(f, "ATWORD({})", s),
1040 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1041 Token::Comment => write!(f, "COMMENT"),
1042 Token::Newline => write!(f, "NEWLINE"),
1043 Token::LineContinuation => write!(f, "LINECONT"),
1044 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1046 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1047 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1048 }
1049 }
1050}
1051
1052impl Token {
1053 pub fn is_keyword(&self) -> bool {
1058 matches!(
1059 self,
1060 Token::Set
1061 | Token::Local
1062 | Token::If
1063 | Token::Then
1064 | Token::Else
1065 | Token::Elif
1066 | Token::Fi
1067 | Token::For
1068 | Token::In
1069 | Token::Do
1070 | Token::Done
1071 | Token::While
1072 | Token::Case
1073 | Token::Esac
1074 | Token::Function
1075 | Token::Return
1076 | Token::Break
1077 | Token::Continue
1078 | Token::Exit
1079 | Token::True
1080 | Token::False
1081 )
1082 }
1083
1084 pub fn is_type(&self) -> bool {
1086 matches!(
1087 self,
1088 Token::TypeString
1089 | Token::TypeInt
1090 | Token::TypeFloat
1091 | Token::TypeBool
1092 )
1093 }
1094
1095 pub fn starts_statement(&self) -> bool {
1098 matches!(
1099 self,
1100 Token::Set
1101 | Token::Local
1102 | Token::Function
1103 | Token::If
1104 | Token::For
1105 | Token::While
1106 | Token::Case
1107 | Token::Ident(_)
1108 | Token::LBracket
1109 )
1110 }
1111
1112 pub fn is_value(&self) -> bool {
1114 matches!(
1115 self,
1116 Token::String(_)
1117 | Token::SingleString(_)
1118 | Token::HereDoc(_)
1119 | Token::Arithmetic(_)
1120 | Token::Int(_)
1121 | Token::Float(_)
1122 | Token::True
1123 | Token::False
1124 | Token::VarRef(_)
1125 | Token::SimpleVarRef(_)
1126 | Token::CmdSubstStart
1127 | Token::Path(_)
1128 | Token::GlobWord(_)
1129 | Token::LastExitCode
1130 | Token::CurrentPid
1131 )
1132 }
1133}
1134
1135struct ArithmeticPreprocessResult {
1137 text: String,
1139 arithmetics: Vec<(String, String)>,
1141 replacements: Vec<SpanReplacement>,
1143}
1144
1145fn skip_command_substitution(
1154 chars: &[char],
1155 i: &mut usize,
1156 source_pos: &mut usize,
1157 result: &mut String,
1158) {
1159 result.push('$');
1161 result.push('(');
1162 *i += 2;
1163 *source_pos += 2;
1164
1165 let mut depth: usize = 1;
1166 let mut in_single_quote = false;
1167 let mut in_double_quote = false;
1168
1169 while *i < chars.len() && depth > 0 {
1170 let c = chars[*i];
1171
1172 if in_single_quote {
1173 result.push(c);
1174 *source_pos += c.len_utf8();
1175 *i += 1;
1176 if c == '\'' {
1177 in_single_quote = false;
1178 }
1179 continue;
1180 }
1181
1182 if in_double_quote {
1183 if c == '\\' && *i + 1 < chars.len() {
1184 let next = chars[*i + 1];
1185 if next == '"' || next == '\\' || next == '$' || next == '`' {
1186 result.push(c);
1187 result.push(next);
1188 *source_pos += c.len_utf8() + next.len_utf8();
1189 *i += 2;
1190 continue;
1191 }
1192 }
1193 if c == '"' {
1194 in_double_quote = false;
1195 }
1196 result.push(c);
1197 *source_pos += c.len_utf8();
1198 *i += 1;
1199 continue;
1200 }
1201
1202 match c {
1204 '\'' => {
1205 in_single_quote = true;
1206 result.push(c);
1207 *source_pos += c.len_utf8();
1208 *i += 1;
1209 }
1210 '"' => {
1211 in_double_quote = true;
1212 result.push(c);
1213 *source_pos += c.len_utf8();
1214 *i += 1;
1215 }
1216 '\\' if *i + 1 < chars.len() => {
1217 result.push(c);
1218 result.push(chars[*i + 1]);
1219 *source_pos += c.len_utf8() + chars[*i + 1].len_utf8();
1220 *i += 2;
1221 }
1222 '(' => {
1223 depth += 1;
1224 result.push(c);
1225 *source_pos += c.len_utf8();
1226 *i += 1;
1227 }
1228 ')' => {
1229 depth -= 1;
1230 result.push(c);
1231 *source_pos += c.len_utf8();
1232 *i += 1;
1233 }
1234 _ => {
1235 result.push(c);
1236 *source_pos += c.len_utf8();
1237 *i += 1;
1238 }
1239 }
1240 }
1241}
1242
1243fn preprocess_arithmetic(source: &str) -> Result<ArithmeticPreprocessResult, LexerError> {
1257 let mut result = String::with_capacity(source.len());
1258 let mut arithmetics: Vec<(String, String)> = Vec::new();
1259 let mut replacements: Vec<SpanReplacement> = Vec::new();
1260 let mut source_pos: usize = 0;
1261 let chars_vec: Vec<char> = source.chars().collect();
1262 let mut i = 0;
1263
1264 let mut in_double_quote = false;
1267
1268 while i < chars_vec.len() {
1269 let ch = chars_vec[i];
1270
1271 if !in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1273 result.push(ch);
1274 result.push(chars_vec[i + 1]);
1275 source_pos += ch.len_utf8() + chars_vec[i + 1].len_utf8();
1276 i += 2;
1277 continue;
1278 }
1279
1280 if ch == '\'' && !in_double_quote {
1282 result.push(ch);
1283 i += 1;
1284 source_pos += 1;
1285 while i < chars_vec.len() && chars_vec[i] != '\'' {
1286 result.push(chars_vec[i]);
1287 source_pos += chars_vec[i].len_utf8();
1288 i += 1;
1289 }
1290 if i < chars_vec.len() {
1291 result.push(chars_vec[i]); source_pos += 1;
1293 i += 1;
1294 }
1295 continue;
1296 }
1297
1298 if ch == '"' {
1300 in_double_quote = !in_double_quote;
1301 result.push(ch);
1302 i += 1;
1303 source_pos += 1;
1304 continue;
1305 }
1306
1307 if in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1309 let next = chars_vec[i + 1];
1310 if next == '"' || next == '\\' || next == '$' || next == '`' {
1311 result.push(ch);
1312 result.push(next);
1313 source_pos += ch.len_utf8() + next.len_utf8();
1314 i += 2;
1315 continue;
1316 }
1317 }
1318
1319 if ch == '#' && !in_double_quote {
1327 while i < chars_vec.len() && chars_vec[i] != '\n' && chars_vec[i] != '\r' {
1328 result.push(chars_vec[i]);
1329 source_pos += chars_vec[i].len_utf8();
1330 i += 1;
1331 }
1332 continue;
1333 }
1334
1335 if ch == '$' && i + 1 < chars_vec.len() && chars_vec[i + 1] == '('
1337 && !(i + 2 < chars_vec.len() && chars_vec[i + 2] == '(')
1338 {
1339 skip_command_substitution(&chars_vec, &mut i, &mut source_pos, &mut result);
1340 continue;
1341 }
1342
1343 if ch == '$' && i + 2 < chars_vec.len() && chars_vec[i + 1] == '(' && chars_vec[i + 2] == '(' {
1345 let arith_start_pos = result.len();
1346 let original_start = source_pos;
1347
1348 i += 3;
1350 source_pos += 3;
1351
1352 let mut expr = String::new();
1354 let mut paren_depth: usize = 0;
1355 let mut closed = false;
1356
1357 while i < chars_vec.len() {
1358 let c = chars_vec[i];
1359 match c {
1360 '(' => {
1361 paren_depth += 1;
1362 if paren_depth > MAX_PAREN_DEPTH {
1363 return Err(LexerError::NestingTooDeep);
1364 }
1365 expr.push('(');
1366 i += 1;
1367 source_pos += c.len_utf8();
1368 }
1369 ')' => {
1370 if paren_depth > 0 {
1371 paren_depth -= 1;
1372 expr.push(')');
1373 i += 1;
1374 source_pos += 1;
1375 } else if i + 1 < chars_vec.len() && chars_vec[i + 1] == ')' {
1376 i += 2;
1378 source_pos += 2;
1379 closed = true;
1380 break;
1381 } else {
1382 expr.push(')');
1384 i += 1;
1385 source_pos += 1;
1386 }
1387 }
1388 _ => {
1389 expr.push(c);
1390 i += 1;
1391 source_pos += c.len_utf8();
1392 }
1393 }
1394 }
1395
1396 if !closed {
1399 return Err(LexerError::UnterminatedArithmetic);
1400 }
1401
1402 let original_len = source_pos - original_start;
1404
1405 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1407 let marker_len = marker.len();
1408
1409 replacements.push(SpanReplacement {
1411 preprocessed_pos: arith_start_pos,
1412 marker_len,
1413 original_len,
1414 });
1415
1416 arithmetics.push((marker.clone(), expr));
1417 result.push_str(&marker);
1418 } else {
1419 result.push(ch);
1420 i += 1;
1421 source_pos += ch.len_utf8();
1422 }
1423 }
1424
1425 Ok(ArithmeticPreprocessResult {
1426 text: result,
1427 arithmetics,
1428 replacements,
1429 })
1430}
1431
1432#[derive(Debug, Clone)]
1447struct HeredocReplacement {
1448 marker: String,
1449 body: String,
1450 literal: bool,
1451 strip_tabs: bool,
1452 body_start_offset: usize,
1453}
1454
1455fn preprocess_heredocs(source: &str) -> Result<(String, Vec<HeredocReplacement>), Spanned<LexerError>> {
1467 let mut result = String::with_capacity(source.len());
1468 let mut heredocs: Vec<HeredocReplacement> = Vec::new();
1469 let chars_vec: Vec<char> = source.chars().collect();
1470 let mut i = 0;
1471 let mut pos: usize = 0;
1475
1476 while i < chars_vec.len() {
1477 let ch = chars_vec[i];
1478
1479 if ch == '<'
1483 && chars_vec.get(i + 1) == Some(&'<')
1484 && chars_vec.get(i + 2) == Some(&'<')
1485 {
1486 result.push_str("<<<");
1487 i += 3;
1488 pos += 3;
1489 continue;
1490 }
1491
1492 if ch == '<' && chars_vec.get(i + 1) == Some(&'<') {
1494 let introducer_start = pos;
1497 i += 2; pos += 2;
1499
1500 let strip_tabs = chars_vec.get(i) == Some(&'-');
1502 if strip_tabs {
1503 i += 1;
1504 pos += 1;
1505 }
1506
1507 while let Some(&c) = chars_vec.get(i) {
1509 if c == ' ' || c == '\t' {
1510 i += 1;
1511 pos += 1;
1512 } else {
1513 break;
1514 }
1515 }
1516
1517 let mut delimiter = String::new();
1519 let quoted = chars_vec.get(i) == Some(&'\'') || chars_vec.get(i) == Some(&'"');
1520 let quote_char = if quoted {
1521 let q = chars_vec.get(i).copied();
1522 i += 1;
1523 pos += 1;
1524 q
1525 } else {
1526 None
1527 };
1528
1529 while let Some(&c) = chars_vec.get(i) {
1530 if quoted {
1531 if Some(c) == quote_char {
1532 i += 1; pos += 1;
1534 break;
1535 }
1536 } else if c.is_whitespace() || c == '\n' || c == '\r' {
1537 break;
1538 }
1539 delimiter.push(c);
1540 i += 1;
1541 pos += c.len_utf8();
1542 }
1543
1544 if delimiter.is_empty() {
1545 result.push_str("<<");
1547 if strip_tabs {
1548 result.push('-');
1549 }
1550 continue;
1551 }
1552
1553 let mut after_delimiter = String::new();
1556 while let Some(&c) = chars_vec.get(i) {
1557 if c == '\n' {
1558 i += 1;
1559 pos += 1;
1560 break;
1561 } else if c == '\r' {
1562 i += 1;
1563 pos += 1;
1564 if chars_vec.get(i) == Some(&'\n') {
1565 i += 1;
1566 pos += 1;
1567 }
1568 break;
1569 }
1570 after_delimiter.push(c);
1571 i += 1;
1572 pos += c.len_utf8();
1573 }
1574
1575 let body_start_offset = pos;
1581 let mut content = String::new();
1582 let mut current_line = String::new();
1583
1584 loop {
1585 let next = chars_vec.get(i).copied();
1586 match next {
1587 Some('\n') => {
1588 i += 1;
1589 pos += 1;
1590 let trimmed = if strip_tabs {
1592 current_line.trim_start_matches('\t')
1593 } else {
1594 ¤t_line
1595 };
1596 if trimmed == delimiter {
1597 break;
1599 }
1600 content.push_str(¤t_line);
1602 content.push('\n');
1603 current_line.clear();
1604 }
1605 Some('\r') => {
1606 i += 1;
1607 pos += 1;
1608 let crlf = chars_vec.get(i) == Some(&'\n');
1614 if crlf {
1615 i += 1;
1616 pos += 1;
1617 }
1618 let trimmed = if strip_tabs {
1619 current_line.trim_start_matches('\t')
1620 } else {
1621 ¤t_line
1622 };
1623 if trimmed == delimiter {
1624 break;
1625 }
1626 content.push_str(¤t_line);
1627 content.push_str(if crlf { "\r\n" } else { "\r" });
1628 current_line.clear();
1629 }
1630 Some(c) => {
1631 current_line.push(c);
1632 i += 1;
1633 pos += c.len_utf8();
1634 }
1635 None => {
1636 let trimmed = if strip_tabs {
1639 current_line.trim_start_matches('\t')
1640 } else {
1641 ¤t_line
1642 };
1643 if trimmed == delimiter {
1644 break;
1645 }
1646 let span_end = introducer_start
1651 + 2
1652 + if strip_tabs { 1 } else { 0 }
1653 + delimiter.len();
1654 return Err(Spanned::new(
1655 LexerError::UnterminatedHeredoc {
1656 delimiter: delimiter.clone(),
1657 },
1658 introducer_start..span_end,
1659 ));
1660 }
1661 }
1662 }
1663
1664 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1666 heredocs.push(HeredocReplacement {
1667 marker: marker.clone(),
1668 body: content,
1669 literal: quoted,
1670 strip_tabs,
1671 body_start_offset,
1672 });
1673
1674 result.push_str("<<");
1677 result.push_str(&marker);
1678 result.push_str(&after_delimiter);
1679 result.push('\n');
1680 } else {
1681 result.push(ch);
1682 i += 1;
1683 pos += ch.len_utf8();
1684 }
1685 }
1686
1687 Ok((result, heredocs))
1688}
1689
1690fn mergeable_text(token: &Token) -> Option<String> {
1695 match token {
1696 Token::Ident(s) => Some(s.clone()),
1697 Token::NumberIdent(s) => Some(s.clone()),
1698 Token::DashNumWord(s) => Some(s.clone()),
1699 Token::AtWord(s) => Some(s.clone()),
1700 Token::DottedIdent(s) => Some(s.clone()),
1701 Token::Colon => Some(":".to_string()),
1702 Token::Int(n) => Some(n.to_string()),
1703 Token::Path(p) => Some(p.clone()),
1704 Token::Float(f) => Some(f.to_string()),
1705 _ => None,
1706 }
1707}
1708
1709fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1718 if tokens.is_empty() {
1719 return tokens;
1720 }
1721
1722 let mut result = Vec::with_capacity(tokens.len());
1723 let mut run: Vec<&Spanned<Token>> = Vec::new();
1724
1725 for token in &tokens {
1726 if run.is_empty() {
1727 if mergeable_text(&token.token).is_some() {
1728 run.push(token);
1729 } else {
1730 result.push(token.clone());
1731 }
1732 continue;
1733 }
1734
1735 let Some(last) = run.last() else { unreachable!() };
1738 let adjacent = last.span.end == token.span.start;
1739
1740 if adjacent && mergeable_text(&token.token).is_some() {
1741 run.push(token);
1742 } else {
1743 flush_colon_run(&mut run, &mut result);
1744 if mergeable_text(&token.token).is_some() {
1745 run.push(token);
1746 } else {
1747 result.push(token.clone());
1748 }
1749 }
1750 }
1751
1752 flush_colon_run(&mut run, &mut result);
1753
1754 result
1755}
1756
1757fn flush_colon_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1759 if run.is_empty() {
1760 return;
1761 }
1762
1763 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
1764
1765 if run.len() >= 2 && has_colon {
1766 let text: String = run
1767 .iter()
1768 .filter_map(|t| mergeable_text(&t.token))
1769 .collect();
1770 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1772 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1773 result.push(Spanned::new(Token::Ident(text), start..end));
1774 } else {
1775 for t in run.iter() {
1776 result.push((*t).clone());
1777 }
1778 }
1779
1780 run.clear();
1781}
1782
1783fn glob_mergeable_text(token: &Token) -> Option<String> {
1788 match token {
1789 Token::Star => Some("*".to_string()),
1790 Token::Question => Some("?".to_string()),
1791 Token::Dot => Some(".".to_string()),
1792 Token::DotDot => Some("..".to_string()),
1793 Token::Ident(s) => Some(s.clone()),
1794 Token::NumberIdent(s) => Some(s.clone()),
1795 Token::DashNumWord(s) => Some(s.clone()),
1796 Token::AtWord(s) => Some(s.clone()),
1797 Token::DottedIdent(s) => Some(s.clone()),
1798 Token::Path(s) => Some(s.clone()),
1799 Token::Int(n) => Some(n.to_string()),
1800 Token::LBracket => Some("[".to_string()),
1801 Token::RBracket => Some("]".to_string()),
1802 Token::Bang => Some("!".to_string()),
1803 Token::DotSlashPath(s) => Some(s.clone()),
1804 Token::RelativePath(s) => Some(s.clone()),
1805 Token::TildePath(s) => Some(s.clone()),
1806 Token::Tilde => Some("~".to_string()),
1807 Token::LBrace => Some("{".to_string()),
1808 Token::RBrace => Some("}".to_string()),
1809 Token::Comma => Some(",".to_string()),
1810 _ => None,
1811 }
1812}
1813
1814fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1843 if tokens.len() < 2 {
1844 return tokens;
1845 }
1846
1847 let mut result = Vec::with_capacity(tokens.len());
1848 let mut i = 0;
1849
1850 while i < tokens.len() {
1851 let token = &tokens[i];
1852
1853 if let Token::ShortFlag(flag_name) = &token.token {
1855 let mut fused = flag_name.clone();
1857 let mut end_span = token.span.end;
1858 let mut j = i + 1;
1859
1860 while let Some(next) = tokens.get(j) {
1861 if next.span.start == end_span {
1862 if let Token::Colon = &next.token {
1863 fused.push(':');
1864 end_span = next.span.end;
1865 j += 1;
1866 continue;
1867 }
1868 }
1869 break;
1870 }
1871
1872 if j > i + 1 {
1873 let span = token.span.start..end_span;
1875 result.push(Spanned::new(Token::ShortFlag(fused), span));
1876 i = j;
1877 continue;
1878 }
1879 }
1880
1881 result.push(token.clone());
1882 i += 1;
1883 }
1884
1885 result
1886}
1887
1888fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1896 if tokens.is_empty() {
1897 return tokens;
1898 }
1899
1900 let mut result = Vec::with_capacity(tokens.len());
1901 let mut run: Vec<&Spanned<Token>> = Vec::new();
1902
1903 for token in &tokens {
1904 if run.is_empty() {
1905 if glob_mergeable_text(&token.token).is_some() {
1906 run.push(token);
1907 } else {
1908 result.push(token.clone());
1909 }
1910 continue;
1911 }
1912
1913 let Some(last) = run.last() else { unreachable!() };
1915 let adjacent = last.span.end == token.span.start;
1916
1917 if adjacent && glob_mergeable_text(&token.token).is_some() {
1918 run.push(token);
1919 } else {
1920 flush_glob_run(&mut run, &mut result);
1921 if glob_mergeable_text(&token.token).is_some() {
1922 run.push(token);
1923 } else {
1924 result.push(token.clone());
1925 }
1926 }
1927 }
1928
1929 flush_glob_run(&mut run, &mut result);
1930
1931 result
1932}
1933
1934fn flush_glob_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>) {
1936 if run.is_empty() {
1937 return;
1938 }
1939
1940 let has_glob = run.iter().any(|t| {
1941 matches!(t.token, Token::Star | Token::Question)
1942 }) || (run.iter().any(|t| matches!(t.token, Token::LBracket))
1943 && run.iter().any(|t| matches!(t.token, Token::RBracket)));
1944
1945 if run.len() >= 2 && has_glob {
1946 let text: String = run
1947 .iter()
1948 .filter_map(|t| glob_mergeable_text(&t.token))
1949 .collect();
1950 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1951 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1952 result.push(Spanned::new(Token::GlobWord(text), start..end));
1953 } else {
1954 for t in run.iter() {
1955 result.push((*t).clone());
1956 }
1957 }
1958
1959 run.clear();
1960}
1961
1962pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
1972 let arith_result = preprocess_arithmetic(source)
1974 .map_err(|e| vec![Spanned::new(e, 0..source.len())])?;
1975
1976 let span_replacements = arith_result.replacements;
1980 let (preprocessed, heredocs) = preprocess_heredocs(&arith_result.text)
1981 .map_err(|e| {
1982 let span = correct_span(e.span, &span_replacements);
1983 vec![Spanned::new(e.token, span)]
1984 })?;
1985
1986 let lexer = Token::lexer(&preprocessed);
1987 let mut tokens = Vec::new();
1988 let mut errors = Vec::new();
1989
1990 for (result, span) in lexer.spanned() {
1991 let corrected_span = correct_span(span, &span_replacements);
1993 match result {
1994 Ok(token) => {
1995 if !matches!(token, Token::Comment | Token::LineContinuation) {
1997 tokens.push(Spanned::new(token, corrected_span));
1998 }
1999 }
2000 Err(err) => {
2001 errors.push(Spanned::new(err, corrected_span));
2002 }
2003 }
2004 }
2005
2006 if !errors.is_empty() {
2007 return Err(errors);
2008 }
2009
2010 let mut final_tokens = Vec::with_capacity(tokens.len());
2012 let mut i = 0;
2013
2014 while i < tokens.len() {
2015 if let Token::Ident(ref name) = tokens[i].token
2017 && name.starts_with("__KAISH_ARITH_") && name.ends_with("__")
2018 && let Some((_, expr)) = arith_result.arithmetics.iter().find(|(marker, _)| marker == name) {
2019 final_tokens.push(Spanned::new(Token::Arithmetic(expr.clone()), tokens[i].span.clone()));
2020 i += 1;
2021 continue;
2022 }
2023
2024 if matches!(tokens[i].token, Token::HereDocStart) {
2026 if i + 1 < tokens.len()
2028 && let Token::Ident(ref name) = tokens[i + 1].token
2029 && name.starts_with("__KAISH_HEREDOC_") && name.ends_with("__") {
2030 if let Some(hd) = heredocs.iter().find(|h| h.marker == *name) {
2032 let mut content = hd.body.clone();
2044 for (marker, expr) in &arith_result.arithmetics {
2045 if content.contains(marker) {
2046 let replacement = if hd.literal {
2047 format!("$(({}))", expr)
2048 } else {
2049 format!("${{__ARITH:{}__}}", expr)
2050 };
2051 content = content.replace(marker, &replacement);
2052 }
2053 }
2054 final_tokens.push(Spanned::new(Token::HereDocStart, tokens[i].span.clone()));
2055 final_tokens.push(Spanned::new(
2056 Token::HereDoc(HereDocData {
2057 content,
2058 literal: hd.literal,
2059 strip_tabs: hd.strip_tabs,
2060 body_start_offset: hd.body_start_offset,
2061 }),
2062 tokens[i + 1].span.clone(),
2063 ));
2064 i += 2;
2065 continue;
2066 }
2067 }
2068 }
2069
2070 let token = if let Token::String(ref s) = tokens[i].token {
2072 let mut new_content = s.clone();
2074 for (marker, expr) in &arith_result.arithmetics {
2075 if new_content.contains(marker) {
2076 new_content = new_content.replace(marker, &format!("${{__ARITH:{}__}}", expr));
2079 }
2080 }
2081 if new_content != *s {
2082 Spanned::new(Token::String(new_content), tokens[i].span.clone())
2083 } else {
2084 tokens[i].clone()
2085 }
2086 } else {
2087 tokens[i].clone()
2088 };
2089 final_tokens.push(token);
2090 i += 1;
2091 }
2092
2093 Ok(merge_glob_adjacent(merge_colon_adjacent(
2094 merge_flag_metachar_adjacent(final_tokens),
2095 )))
2096}
2097
2098pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2102 let lexer = Token::lexer(source);
2103 let mut tokens = Vec::new();
2104 let mut errors = Vec::new();
2105
2106 for (result, span) in lexer.spanned() {
2107 match result {
2108 Ok(token) => {
2109 tokens.push(Spanned::new(token, span));
2110 }
2111 Err(err) => {
2112 errors.push(Spanned::new(err, span));
2113 }
2114 }
2115 }
2116
2117 if errors.is_empty() {
2118 Ok(tokens)
2119 } else {
2120 Err(errors)
2121 }
2122}
2123
2124pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2126 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2128 return Err(LexerError::UnterminatedString);
2129 }
2130
2131 let inner = &source[1..source.len() - 1];
2132 let mut result = String::with_capacity(inner.len());
2133 let mut chars = inner.chars().peekable();
2134
2135 while let Some(ch) = chars.next() {
2136 if ch == '\\' {
2137 match chars.next() {
2138 Some('n') => result.push('\n'),
2139 Some('t') => result.push('\t'),
2140 Some('r') => result.push('\r'),
2141 Some('\\') => result.push('\\'),
2142 Some('"') => result.push('"'),
2143 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2146 Some('u') => {
2147 let mut hex = String::with_capacity(4);
2149 for _ in 0..4 {
2150 match chars.next() {
2151 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2152 _ => return Err(LexerError::InvalidEscape),
2153 }
2154 }
2155 let codepoint = u32::from_str_radix(&hex, 16)
2156 .map_err(|_| LexerError::InvalidEscape)?;
2157 let ch = char::from_u32(codepoint)
2158 .ok_or(LexerError::InvalidEscape)?;
2159 result.push(ch);
2160 }
2161 Some(next) => {
2163 result.push('\\');
2164 result.push(next);
2165 }
2166 None => return Err(LexerError::InvalidEscape),
2167 }
2168 } else {
2169 result.push(ch);
2170 }
2171 }
2172
2173 Ok(result)
2174}
2175
2176pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
2179 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
2181 return Err(LexerError::UnterminatedVarRef);
2182 }
2183
2184 let inner = &source[2..source.len() - 1];
2185
2186 if inner == "?" {
2188 return Ok(vec!["?".to_string()]);
2189 }
2190
2191 let mut segments = Vec::new();
2192 let mut current = String::new();
2193 let mut chars = inner.chars().peekable();
2194
2195 while let Some(ch) = chars.next() {
2196 match ch {
2197 '.' => {
2198 if !current.is_empty() {
2199 segments.push(current.clone());
2200 current.clear();
2201 }
2202 }
2203 '[' => {
2204 if !current.is_empty() {
2205 segments.push(current.clone());
2206 current.clear();
2207 }
2208 let mut index = String::from("[");
2210 while let Some(&c) = chars.peek() {
2211 if let Some(c) = chars.next() {
2212 index.push(c);
2213 }
2214 if c == ']' {
2215 break;
2216 }
2217 }
2218 segments.push(index);
2219 }
2220 _ => {
2221 current.push(ch);
2222 }
2223 }
2224 }
2225
2226 if !current.is_empty() {
2227 segments.push(current);
2228 }
2229
2230 Ok(segments)
2231}
2232
2233pub fn parse_int(source: &str) -> Result<i64, LexerError> {
2235 source.parse().map_err(|_| LexerError::InvalidNumber)
2236}
2237
2238pub fn parse_float(source: &str) -> Result<f64, LexerError> {
2240 source.parse().map_err(|_| LexerError::InvalidNumber)
2241}
2242
2243#[cfg(test)]
2244#[allow(clippy::approx_constant)]
2245mod tests {
2246 use super::*;
2247
2248 fn lex(source: &str) -> Vec<Token> {
2249 tokenize(source)
2250 .expect("lexer should succeed")
2251 .into_iter()
2252 .map(|s| s.token)
2253 .collect()
2254 }
2255
2256 #[test]
2261 fn keywords() {
2262 assert_eq!(lex("set"), vec![Token::Set]);
2263 assert_eq!(lex("if"), vec![Token::If]);
2264 assert_eq!(lex("then"), vec![Token::Then]);
2265 assert_eq!(lex("else"), vec![Token::Else]);
2266 assert_eq!(lex("elif"), vec![Token::Elif]);
2267 assert_eq!(lex("fi"), vec![Token::Fi]);
2268 assert_eq!(lex("for"), vec![Token::For]);
2269 assert_eq!(lex("in"), vec![Token::In]);
2270 assert_eq!(lex("do"), vec![Token::Do]);
2271 assert_eq!(lex("done"), vec![Token::Done]);
2272 assert_eq!(lex("case"), vec![Token::Case]);
2273 assert_eq!(lex("esac"), vec![Token::Esac]);
2274 assert_eq!(lex("function"), vec![Token::Function]);
2275 assert_eq!(lex("true"), vec![Token::True]);
2276 assert_eq!(lex("false"), vec![Token::False]);
2277 }
2278
2279 #[test]
2280 fn double_semicolon() {
2281 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
2282 assert_eq!(lex("echo \"hi\";;"), vec![
2284 Token::Ident("echo".to_string()),
2285 Token::String("hi".to_string()),
2286 Token::DoubleSemi,
2287 ]);
2288 }
2289
2290 #[test]
2291 fn type_keywords() {
2292 assert_eq!(lex("string"), vec![Token::TypeString]);
2293 assert_eq!(lex("int"), vec![Token::TypeInt]);
2294 assert_eq!(lex("float"), vec![Token::TypeFloat]);
2295 assert_eq!(lex("bool"), vec![Token::TypeBool]);
2296 }
2297
2298 #[test]
2303 fn single_char_operators() {
2304 assert_eq!(lex("="), vec![Token::Eq]);
2305 assert_eq!(lex("|"), vec![Token::Pipe]);
2306 assert_eq!(lex("&"), vec![Token::Amp]);
2307 assert_eq!(lex(">"), vec![Token::Gt]);
2308 assert_eq!(lex("<"), vec![Token::Lt]);
2309 assert_eq!(lex(";"), vec![Token::Semi]);
2310 assert_eq!(lex(":"), vec![Token::Colon]);
2311 assert_eq!(lex(","), vec![Token::Comma]);
2312 assert_eq!(lex("."), vec![Token::Dot]);
2313 }
2314
2315 #[test]
2316 fn multi_char_operators() {
2317 assert_eq!(lex("&&"), vec![Token::And]);
2318 assert_eq!(lex("||"), vec![Token::Or]);
2319 assert_eq!(lex("=="), vec![Token::EqEq]);
2320 assert_eq!(lex("!="), vec![Token::NotEq]);
2321 assert_eq!(lex("=~"), vec![Token::Match]);
2322 assert_eq!(lex("!~"), vec![Token::NotMatch]);
2323 assert_eq!(lex(">="), vec![Token::GtEq]);
2324 assert_eq!(lex("<="), vec![Token::LtEq]);
2325 assert_eq!(lex(">>"), vec![Token::GtGt]);
2326 assert_eq!(lex("2>"), vec![Token::Stderr]);
2327 assert_eq!(lex("&>"), vec![Token::Both]);
2328 }
2329
2330 #[test]
2331 fn brackets() {
2332 assert_eq!(lex("{"), vec![Token::LBrace]);
2333 assert_eq!(lex("}"), vec![Token::RBrace]);
2334 assert_eq!(lex("["), vec![Token::LBracket]);
2335 assert_eq!(lex("]"), vec![Token::RBracket]);
2336 assert_eq!(lex("("), vec![Token::LParen]);
2337 assert_eq!(lex(")"), vec![Token::RParen]);
2338 }
2339
2340 #[test]
2345 fn integers() {
2346 assert_eq!(lex("0"), vec![Token::Int(0)]);
2347 assert_eq!(lex("42"), vec![Token::Int(42)]);
2348 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
2349 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
2350 }
2351
2352 #[test]
2353 fn floats() {
2354 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
2355 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
2356 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
2357 }
2358
2359 #[test]
2360 fn strings() {
2361 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
2362 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
2363 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
2365 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
2366 }
2367
2368 #[test]
2369 fn var_refs() {
2370 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
2371 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
2372 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
2373 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
2374 }
2375
2376 #[test]
2381 fn identifiers() {
2382 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
2383 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
2384 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
2385 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
2386 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
2387 }
2388
2389 #[test]
2390 fn keyword_prefix_identifiers() {
2391 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
2393 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
2394 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
2395 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
2396 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
2397 }
2398
2399 #[test]
2404 fn assignment() {
2405 assert_eq!(
2406 lex("set X = 5"),
2407 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2408 );
2409 }
2410
2411 #[test]
2412 fn command_simple() {
2413 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
2414 assert_eq!(
2415 lex(r#"echo "hello""#),
2416 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
2417 );
2418 }
2419
2420 #[test]
2421 fn command_with_args() {
2422 assert_eq!(
2423 lex("cmd arg1 arg2"),
2424 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
2425 );
2426 }
2427
2428 #[test]
2429 fn command_with_named_args() {
2430 assert_eq!(
2431 lex("cmd key=value"),
2432 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
2433 );
2434 }
2435
2436 #[test]
2437 fn pipeline() {
2438 assert_eq!(
2439 lex("a | b | c"),
2440 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
2441 );
2442 }
2443
2444 #[test]
2445 fn if_statement() {
2446 assert_eq!(
2447 lex("if true; then echo; fi"),
2448 vec![
2449 Token::If,
2450 Token::True,
2451 Token::Semi,
2452 Token::Then,
2453 Token::Ident("echo".to_string()),
2454 Token::Semi,
2455 Token::Fi
2456 ]
2457 );
2458 }
2459
2460 #[test]
2461 fn for_loop() {
2462 assert_eq!(
2463 lex("for X in items; do echo; done"),
2464 vec![
2465 Token::For,
2466 Token::Ident("X".to_string()),
2467 Token::In,
2468 Token::Ident("items".to_string()),
2469 Token::Semi,
2470 Token::Do,
2471 Token::Ident("echo".to_string()),
2472 Token::Semi,
2473 Token::Done
2474 ]
2475 );
2476 }
2477
2478 #[test]
2483 fn whitespace_ignored() {
2484 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
2485 }
2486
2487 #[test]
2488 fn newlines_preserved() {
2489 let tokens = lex("a\nb");
2490 assert_eq!(
2491 tokens,
2492 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2493 );
2494 }
2495
2496 #[test]
2497 fn multiple_newlines() {
2498 let tokens = lex("a\n\n\nb");
2499 assert_eq!(
2500 tokens,
2501 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
2502 );
2503 }
2504
2505 #[test]
2510 fn comments_skipped() {
2511 assert_eq!(lex("# comment"), vec![]);
2512 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
2513 assert_eq!(
2514 lex("a # comment\nb"),
2515 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2516 );
2517 }
2518
2519 #[test]
2520 fn comments_preserved_when_requested() {
2521 let tokens = tokenize_with_comments("a # comment")
2522 .expect("should succeed")
2523 .into_iter()
2524 .map(|s| s.token)
2525 .collect::<Vec<_>>();
2526 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
2527 }
2528
2529 #[test]
2534 fn parse_simple_string() {
2535 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
2536 }
2537
2538 #[test]
2539 fn parse_string_with_escapes() {
2540 assert_eq!(
2541 parse_string_literal(r#""hello\nworld""#).expect("ok"),
2542 "hello\nworld"
2543 );
2544 assert_eq!(
2545 parse_string_literal(r#""tab\there""#).expect("ok"),
2546 "tab\there"
2547 );
2548 assert_eq!(
2549 parse_string_literal(r#""quote\"here""#).expect("ok"),
2550 "quote\"here"
2551 );
2552 }
2553
2554 #[test]
2555 fn parse_string_with_unicode() {
2556 assert_eq!(
2557 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
2558 "emoji ❤"
2559 );
2560 }
2561
2562 #[test]
2563 fn parse_string_with_escaped_dollar() {
2564 assert_eq!(
2567 parse_string_literal(r#""\$VAR""#).expect("ok"),
2568 "__KAISH_ESCAPED_DOLLAR__VAR"
2569 );
2570 assert_eq!(
2571 parse_string_literal(r#""cost: \$100""#).expect("ok"),
2572 "cost: __KAISH_ESCAPED_DOLLAR__100"
2573 );
2574 }
2575
2576 #[test]
2581 fn parse_simple_var() {
2582 assert_eq!(
2583 parse_var_ref("${X}").expect("ok"),
2584 vec!["X"]
2585 );
2586 }
2587
2588 #[test]
2589 fn parse_var_with_field() {
2590 assert_eq!(
2591 parse_var_ref("${VAR.field}").expect("ok"),
2592 vec!["VAR", "field"]
2593 );
2594 }
2595
2596 #[test]
2597 fn parse_var_with_index() {
2598 assert_eq!(
2599 parse_var_ref("${VAR[0]}").expect("ok"),
2600 vec!["VAR", "[0]"]
2601 );
2602 }
2603
2604 #[test]
2605 fn parse_var_nested() {
2606 assert_eq!(
2607 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
2608 vec!["VAR", "field", "[0]", "nested"]
2609 );
2610 }
2611
2612 #[test]
2613 fn parse_last_result() {
2614 assert_eq!(
2615 parse_var_ref("${?}").expect("ok"),
2616 vec!["?"]
2617 );
2618 }
2619
2620 #[test]
2625 fn parse_integers() {
2626 assert_eq!(parse_int("0").expect("ok"), 0);
2627 assert_eq!(parse_int("42").expect("ok"), 42);
2628 assert_eq!(parse_int("-1").expect("ok"), -1);
2629 }
2630
2631 #[test]
2632 fn parse_floats() {
2633 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
2634 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
2635 }
2636
2637 #[test]
2642 fn empty_input() {
2643 assert_eq!(lex(""), vec![]);
2644 }
2645
2646 #[test]
2647 fn only_whitespace() {
2648 assert_eq!(lex(" \t\t "), vec![]);
2649 }
2650
2651 #[test]
2652 fn json_array() {
2653 assert_eq!(
2654 lex(r#"[1, 2, 3]"#),
2655 vec![
2656 Token::LBracket,
2657 Token::Int(1),
2658 Token::Comma,
2659 Token::Int(2),
2660 Token::Comma,
2661 Token::Int(3),
2662 Token::RBracket
2663 ]
2664 );
2665 }
2666
2667 #[test]
2668 fn json_object() {
2669 assert_eq!(
2670 lex(r#"{"key": "value"}"#),
2671 vec![
2672 Token::LBrace,
2673 Token::String("key".to_string()),
2674 Token::Colon,
2675 Token::String("value".to_string()),
2676 Token::RBrace
2677 ]
2678 );
2679 }
2680
2681 #[test]
2682 fn redirect_operators() {
2683 assert_eq!(
2684 lex("cmd > file"),
2685 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
2686 );
2687 assert_eq!(
2688 lex("cmd >> file"),
2689 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
2690 );
2691 assert_eq!(
2692 lex("cmd 2> err"),
2693 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
2694 );
2695 assert_eq!(
2696 lex("cmd &> all"),
2697 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
2698 );
2699 }
2700
2701 #[test]
2702 fn background_job() {
2703 assert_eq!(
2704 lex("cmd &"),
2705 vec![Token::Ident("cmd".to_string()), Token::Amp]
2706 );
2707 }
2708
2709 #[test]
2710 fn command_substitution() {
2711 assert_eq!(
2712 lex("$(cmd)"),
2713 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
2714 );
2715 assert_eq!(
2716 lex("$(cmd arg)"),
2717 vec![
2718 Token::CmdSubstStart,
2719 Token::Ident("cmd".to_string()),
2720 Token::Ident("arg".to_string()),
2721 Token::RParen
2722 ]
2723 );
2724 assert_eq!(
2725 lex("$(a | b)"),
2726 vec![
2727 Token::CmdSubstStart,
2728 Token::Ident("a".to_string()),
2729 Token::Pipe,
2730 Token::Ident("b".to_string()),
2731 Token::RParen
2732 ]
2733 );
2734 }
2735
2736 #[test]
2737 fn complex_pipeline() {
2738 assert_eq!(
2739 lex(r#"cat file | grep pattern="foo" | head count=10"#),
2740 vec![
2741 Token::Ident("cat".to_string()),
2742 Token::Ident("file".to_string()),
2743 Token::Pipe,
2744 Token::Ident("grep".to_string()),
2745 Token::Ident("pattern".to_string()),
2746 Token::Eq,
2747 Token::String("foo".to_string()),
2748 Token::Pipe,
2749 Token::Ident("head".to_string()),
2750 Token::Ident("count".to_string()),
2751 Token::Eq,
2752 Token::Int(10),
2753 ]
2754 );
2755 }
2756
2757 #[test]
2762 fn short_flag() {
2763 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2764 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
2765 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
2766 }
2767
2768 #[test]
2769 fn short_flag_combined() {
2770 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
2772 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
2773 }
2774
2775 #[test]
2776 fn job_spec_lexes_as_one_token() {
2777 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
2779 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
2780 assert_eq!(
2781 lex("wait %1 %2"),
2782 vec![
2783 Token::Ident("wait".to_string()),
2784 Token::JobSpec("%1".to_string()),
2785 Token::JobSpec("%2".to_string()),
2786 ]
2787 );
2788 }
2789
2790 #[test]
2791 fn short_flag_with_internal_hyphens_is_one_token() {
2792 assert_eq!(
2796 lex("-not-a-flag"),
2797 vec![Token::ShortFlag("not-a-flag".to_string())]
2798 );
2799 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2803 assert_eq!(lex("-"), vec![Token::MinusAlone]);
2804 }
2805
2806 #[test]
2807 fn long_flag() {
2808 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
2809 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
2810 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
2811 }
2812
2813 #[test]
2814 fn double_dash() {
2815 assert_eq!(lex("--"), vec![Token::DoubleDash]);
2817 }
2818
2819 #[test]
2820 fn flags_vs_negative_numbers() {
2821 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
2823 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2825 assert_eq!(
2828 lex("-1 a"),
2829 vec![Token::Int(-1), Token::Ident("a".to_string())]
2830 );
2831 }
2832
2833 #[test]
2834 fn command_with_flags() {
2835 assert_eq!(
2836 lex("ls -l"),
2837 vec![
2838 Token::Ident("ls".to_string()),
2839 Token::ShortFlag("l".to_string()),
2840 ]
2841 );
2842 assert_eq!(
2843 lex("git commit -m"),
2844 vec![
2845 Token::Ident("git".to_string()),
2846 Token::Ident("commit".to_string()),
2847 Token::ShortFlag("m".to_string()),
2848 ]
2849 );
2850 assert_eq!(
2851 lex("git push --force"),
2852 vec![
2853 Token::Ident("git".to_string()),
2854 Token::Ident("push".to_string()),
2855 Token::LongFlag("force".to_string()),
2856 ]
2857 );
2858 }
2859
2860 #[test]
2861 fn flag_with_value() {
2862 assert_eq!(
2863 lex(r#"git commit -m "message""#),
2864 vec![
2865 Token::Ident("git".to_string()),
2866 Token::Ident("commit".to_string()),
2867 Token::ShortFlag("m".to_string()),
2868 Token::String("message".to_string()),
2869 ]
2870 );
2871 assert_eq!(
2872 lex(r#"--message="hello""#),
2873 vec![
2874 Token::LongFlag("message".to_string()),
2875 Token::Eq,
2876 Token::String("hello".to_string()),
2877 ]
2878 );
2879 }
2880
2881 #[test]
2882 fn end_of_flags_marker() {
2883 assert_eq!(
2884 lex("git checkout -- file"),
2885 vec![
2886 Token::Ident("git".to_string()),
2887 Token::Ident("checkout".to_string()),
2888 Token::DoubleDash,
2889 Token::Ident("file".to_string()),
2890 ]
2891 );
2892 }
2893
2894 #[test]
2899 fn local_keyword() {
2900 assert_eq!(lex("local"), vec![Token::Local]);
2901 assert_eq!(
2902 lex("local X = 5"),
2903 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2904 );
2905 }
2906
2907 #[test]
2908 fn simple_var_ref() {
2909 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
2910 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
2911 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
2912 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
2913 }
2914
2915 #[test]
2916 fn simple_var_ref_in_command() {
2917 assert_eq!(
2918 lex("echo $NAME"),
2919 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
2920 );
2921 }
2922
2923 #[test]
2924 fn single_quoted_strings() {
2925 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
2926 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
2927 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
2928 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
2930 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
2931 }
2932
2933 #[test]
2934 fn test_brackets() {
2935 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
2937 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
2938 assert_eq!(
2939 lex("[[ -f file ]]"),
2940 vec![
2941 Token::LBracket,
2942 Token::LBracket,
2943 Token::ShortFlag("f".to_string()),
2944 Token::Ident("file".to_string()),
2945 Token::RBracket,
2946 Token::RBracket
2947 ]
2948 );
2949 }
2950
2951 #[test]
2952 fn test_expression_syntax() {
2953 assert_eq!(
2954 lex(r#"[[ $X == "value" ]]"#),
2955 vec![
2956 Token::LBracket,
2957 Token::LBracket,
2958 Token::SimpleVarRef("X".to_string()),
2959 Token::EqEq,
2960 Token::String("value".to_string()),
2961 Token::RBracket,
2962 Token::RBracket
2963 ]
2964 );
2965 }
2966
2967 #[test]
2968 fn bash_style_assignment() {
2969 assert_eq!(
2971 lex(r#"NAME="value""#),
2972 vec![
2973 Token::Ident("NAME".to_string()),
2974 Token::Eq,
2975 Token::String("value".to_string())
2976 ]
2977 );
2978 }
2979
2980 #[test]
2981 fn positional_params() {
2982 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
2983 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
2984 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
2985 assert_eq!(lex("$@"), vec![Token::AllArgs]);
2986 assert_eq!(lex("$#"), vec![Token::ArgCount]);
2987 }
2988
2989 #[test]
2990 fn positional_in_context() {
2991 assert_eq!(
2992 lex("echo $1 $2"),
2993 vec![
2994 Token::Ident("echo".to_string()),
2995 Token::Positional(1),
2996 Token::Positional(2),
2997 ]
2998 );
2999 }
3000
3001 #[test]
3002 fn var_length() {
3003 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3004 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
3005 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
3006 }
3007
3008 #[test]
3009 fn var_length_in_context() {
3010 assert_eq!(
3011 lex("echo ${#NAME}"),
3012 vec![
3013 Token::Ident("echo".to_string()),
3014 Token::VarLength("NAME".to_string()),
3015 ]
3016 );
3017 }
3018
3019 #[test]
3024 fn plus_flag() {
3025 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
3027 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
3028 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
3029 }
3030
3031 #[test]
3032 fn set_with_plus_flag() {
3033 assert_eq!(
3034 lex("set +e"),
3035 vec![
3036 Token::Set,
3037 Token::PlusFlag("e".to_string()),
3038 ]
3039 );
3040 }
3041
3042 #[test]
3043 fn set_with_multiple_flags() {
3044 assert_eq!(
3045 lex("set -e -u"),
3046 vec![
3047 Token::Set,
3048 Token::ShortFlag("e".to_string()),
3049 Token::ShortFlag("u".to_string()),
3050 ]
3051 );
3052 }
3053
3054 #[test]
3055 fn flags_vs_negative_numbers_edge_cases() {
3056 assert_eq!(
3058 lex("-1 a"),
3059 vec![Token::Int(-1), Token::Ident("a".to_string())]
3060 );
3061 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3063 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3065 }
3066
3067 #[test]
3068 fn single_dash_is_minus_alone() {
3069 let result = tokenize("-").expect("should lex");
3071 assert_eq!(result.len(), 1);
3072 assert!(matches!(result[0].token, Token::MinusAlone));
3073 }
3074
3075 #[test]
3076 fn plus_bare_for_date_format() {
3077 let result = tokenize("+%s").expect("should lex");
3079 assert_eq!(result.len(), 1);
3080 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
3081
3082 let result = tokenize("+%Y-%m-%d").expect("should lex");
3084 assert_eq!(result.len(), 1);
3085 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
3086 }
3087
3088 #[test]
3089 fn plus_flag_still_works() {
3090 let result = tokenize("+e").expect("should lex");
3092 assert_eq!(result.len(), 1);
3093 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
3094 }
3095
3096 #[test]
3097 fn while_keyword_vs_while_loop() {
3098 assert_eq!(lex("while"), vec![Token::While]);
3100 assert_eq!(
3102 lex("while true"),
3103 vec![Token::While, Token::True]
3104 );
3105 }
3106
3107 #[test]
3108 fn control_flow_keywords() {
3109 assert_eq!(lex("break"), vec![Token::Break]);
3110 assert_eq!(lex("continue"), vec![Token::Continue]);
3111 assert_eq!(lex("return"), vec![Token::Return]);
3112 assert_eq!(lex("exit"), vec![Token::Exit]);
3113 }
3114
3115 #[test]
3116 fn control_flow_with_numbers() {
3117 assert_eq!(
3118 lex("break 2"),
3119 vec![Token::Break, Token::Int(2)]
3120 );
3121 assert_eq!(
3122 lex("continue 3"),
3123 vec![Token::Continue, Token::Int(3)]
3124 );
3125 assert_eq!(
3126 lex("exit 1"),
3127 vec![Token::Exit, Token::Int(1)]
3128 );
3129 }
3130
3131 #[test]
3136 fn heredoc_simple() {
3137 let source = "cat <<EOF\nhello\nworld\nEOF";
3138 let tokens = lex(source);
3139 assert_eq!(tokens, vec![
3141 Token::Ident("cat".to_string()),
3142 Token::HereDocStart,
3143 Token::HereDoc(HereDocData {
3144 content: "hello\nworld\n".to_string(),
3145 literal: false,
3146 strip_tabs: false,
3147 body_start_offset: 10,
3148 }),
3149 Token::Newline,
3150 ]);
3151 }
3152
3153 #[test]
3154 fn heredoc_empty() {
3155 let source = "cat <<EOF\nEOF";
3156 let tokens = lex(source);
3157 assert_eq!(tokens, vec![
3158 Token::Ident("cat".to_string()),
3159 Token::HereDocStart,
3160 Token::HereDoc(HereDocData {
3161 content: "".to_string(),
3162 literal: false,
3163 strip_tabs: false,
3164 body_start_offset: 10,
3165 }),
3166 Token::Newline,
3167 ]);
3168 }
3169
3170 #[test]
3171 fn heredoc_with_special_chars() {
3172 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
3173 let tokens = lex(source);
3174 assert_eq!(tokens, vec![
3175 Token::Ident("cat".to_string()),
3176 Token::HereDocStart,
3177 Token::HereDoc(HereDocData {
3178 content: "$VAR and \"quoted\" 'single'\n".to_string(),
3179 literal: false,
3180 strip_tabs: false,
3181 body_start_offset: 10,
3182 }),
3183 Token::Newline,
3184 ]);
3185 }
3186
3187 #[test]
3188 fn heredoc_multiline() {
3189 let source = "cat <<END\nline1\nline2\nline3\nEND";
3190 let tokens = lex(source);
3191 assert_eq!(tokens, vec![
3192 Token::Ident("cat".to_string()),
3193 Token::HereDocStart,
3194 Token::HereDoc(HereDocData {
3195 content: "line1\nline2\nline3\n".to_string(),
3196 literal: false,
3197 strip_tabs: false,
3198 body_start_offset: 10,
3199 }),
3200 Token::Newline,
3201 ]);
3202 }
3203
3204 #[test]
3205 fn heredoc_in_command() {
3206 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
3207 let tokens = lex(source);
3208 assert_eq!(tokens, vec![
3209 Token::Ident("cat".to_string()),
3210 Token::HereDocStart,
3211 Token::HereDoc(HereDocData {
3212 content: "hello\n".to_string(),
3213 literal: false,
3214 strip_tabs: false,
3215 body_start_offset: 10,
3216 }),
3217 Token::Newline,
3218 Token::Ident("echo".to_string()),
3219 Token::Ident("goodbye".to_string()),
3220 ]);
3221 }
3222
3223 #[test]
3224 fn heredoc_strip_tabs() {
3225 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
3226 let tokens = lex(source);
3227 assert_eq!(tokens, vec![
3231 Token::Ident("cat".to_string()),
3232 Token::HereDocStart,
3233 Token::HereDoc(HereDocData {
3234 content: "\thello\n\tworld\n".to_string(),
3235 literal: false,
3236 strip_tabs: true,
3237 body_start_offset: 11,
3238 }),
3239 Token::Newline,
3240 ]);
3241 }
3242
3243 #[test]
3248 fn arithmetic_simple() {
3249 let source = "$((1 + 2))";
3250 let tokens = lex(source);
3251 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
3252 }
3253
3254 #[test]
3255 fn arithmetic_in_assignment() {
3256 let source = "X=$((5 * 3))";
3257 let tokens = lex(source);
3258 assert_eq!(tokens, vec![
3259 Token::Ident("X".to_string()),
3260 Token::Eq,
3261 Token::Arithmetic("5 * 3".to_string()),
3262 ]);
3263 }
3264
3265 #[test]
3266 fn arithmetic_with_nested_parens() {
3267 let source = "$((2 * (3 + 4)))";
3268 let tokens = lex(source);
3269 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
3270 }
3271
3272 #[test]
3273 fn arithmetic_with_variable() {
3274 let source = "$((X + 1))";
3275 let tokens = lex(source);
3276 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
3277 }
3278
3279 #[test]
3280 fn arithmetic_command_subst_not_confused() {
3281 let source = "$(echo hello)";
3283 let tokens = lex(source);
3284 assert_eq!(tokens, vec![
3285 Token::CmdSubstStart,
3286 Token::Ident("echo".to_string()),
3287 Token::Ident("hello".to_string()),
3288 Token::RParen,
3289 ]);
3290 }
3291
3292 #[test]
3293 fn arithmetic_nesting_limit() {
3294 let open_parens = "(".repeat(300);
3296 let close_parens = ")".repeat(300);
3297 let source = format!("$(({}1{}))", open_parens, close_parens);
3298 let result = tokenize(&source);
3299 assert!(result.is_err());
3300 let errors = result.unwrap_err();
3301 assert_eq!(errors.len(), 1);
3302 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
3303 }
3304
3305 #[test]
3306 fn arithmetic_nesting_within_limit() {
3307 let source = "$((((1 + 2) * 3)))";
3309 let tokens = lex(source);
3310 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
3311 }
3312
3313 #[test]
3325 fn arithmetic_after_apostrophe_in_comment() {
3326 let source = "# this doesn't work\necho $((1+2))";
3329 let tokens = lex(source);
3330 assert_eq!(tokens, vec![
3331 Token::Newline,
3332 Token::Ident("echo".to_string()),
3333 Token::Arithmetic("1+2".to_string()),
3334 ]);
3335 }
3336
3337 #[test]
3338 fn arithmetic_inside_comment_is_not_expanded() {
3339 let source = "# the $((y)) syntax explained\necho hello";
3341 let tokens = lex(source);
3342 assert_eq!(tokens, vec![
3343 Token::Newline,
3344 Token::Ident("echo".to_string()),
3345 Token::Ident("hello".to_string()),
3346 ]);
3347 }
3348
3349 #[test]
3350 fn backticked_arithmetic_in_comment_is_not_expanded() {
3351 let source = "# the `$((x))` syntax explained\necho $((3+4))";
3355 let tokens = lex(source);
3356 assert_eq!(tokens, vec![
3357 Token::Newline,
3358 Token::Ident("echo".to_string()),
3359 Token::Arithmetic("3+4".to_string()),
3360 ]);
3361 }
3362
3363 #[test]
3364 fn arithmetic_still_works_outside_comments() {
3365 let source = "X=$((1+2)); Y=$((3*4))";
3368 let tokens = lex(source);
3369 assert_eq!(tokens, vec![
3370 Token::Ident("X".to_string()),
3371 Token::Eq,
3372 Token::Arithmetic("1+2".to_string()),
3373 Token::Semi,
3374 Token::Ident("Y".to_string()),
3375 Token::Eq,
3376 Token::Arithmetic("3*4".to_string()),
3377 ]);
3378 }
3379
3380 #[test]
3381 fn arithmetic_inside_double_quotes_still_expands() {
3382 let source = "echo \"# $((1+2))\"";
3385 let tokens = lex(source);
3386 assert_eq!(tokens.len(), 2);
3391 assert!(matches!(tokens[0], Token::Ident(_)));
3392 assert!(matches!(tokens[1], Token::String(_)));
3393 }
3394
3395 #[test]
3408 fn backtick_in_source_is_rejected() {
3409 let result = tokenize("echo `date`");
3410 assert!(result.is_err());
3411 let errors = result.unwrap_err();
3412 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
3413 }
3414
3415 #[test]
3416 fn backtick_in_comment_is_just_comment_text() {
3417 let source = "# use `date` here\necho hi";
3420 let tokens = lex(source);
3421 assert_eq!(tokens, vec![
3422 Token::Newline,
3423 Token::Ident("echo".to_string()),
3424 Token::Ident("hi".to_string()),
3425 ]);
3426 }
3427
3428 #[test]
3429 fn backtick_in_single_quoted_string_is_literal() {
3430 let source = "echo '`date`'";
3433 let tokens = lex(source);
3434 assert_eq!(tokens, vec![
3435 Token::Ident("echo".to_string()),
3436 Token::SingleString("`date`".to_string()),
3437 ]);
3438 }
3439
3440 #[test]
3441 fn backtick_in_double_quoted_string_is_literal() {
3442 let source = "echo \"`date`\"";
3447 let tokens = lex(source);
3448 assert_eq!(tokens.len(), 2);
3449 assert!(matches!(tokens[0], Token::Ident(_)));
3450 match &tokens[1] {
3451 Token::String(s) => assert!(s.contains('`')),
3452 other => panic!("expected Token::String, got {:?}", other),
3453 }
3454 }
3455
3456 #[test]
3457 fn backtick_in_heredoc_body_is_preserved() {
3458 let source = "cat <<EOF\n`date`\nEOF\n";
3461 let tokens = lex(source);
3462 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
3463 assert!(heredoc.is_some(), "expected a HereDoc token");
3464 if let Some(Token::HereDoc(d)) = heredoc {
3465 assert!(d.content.contains('`'));
3466 }
3467 }
3468
3469 #[test]
3474 fn token_categories() {
3475 assert_eq!(Token::If.category(), TokenCategory::Keyword);
3477 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
3478 assert_eq!(Token::For.category(), TokenCategory::Keyword);
3479 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
3480 assert_eq!(Token::True.category(), TokenCategory::Keyword);
3481 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
3482
3483 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
3485 assert_eq!(Token::And.category(), TokenCategory::Operator);
3486 assert_eq!(Token::Or.category(), TokenCategory::Operator);
3487 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
3488 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
3489
3490 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
3492 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
3493 assert_eq!(
3494 Token::HereDoc(HereDocData {
3495 content: "test".to_string(),
3496 literal: false,
3497 strip_tabs: false,
3498 body_start_offset: 0,
3499 }).category(),
3500 TokenCategory::String,
3501 );
3502
3503 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
3505 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
3506 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
3507
3508 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
3510 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
3511 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
3512 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
3513 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
3514 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
3515 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
3516
3517 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
3519 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
3520 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
3521 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
3522
3523 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
3525 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
3526 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
3527 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
3528
3529 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
3531
3532 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
3534
3535 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
3537 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
3538 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
3539
3540 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
3542 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
3543 }
3544
3545 #[test]
3546 fn test_heredoc_piped_to_command() {
3547 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
3550 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
3551 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
3552 assert!(heredoc_pos.is_some(), "should have a heredoc token");
3553 assert!(pipe_pos.is_some(), "should have a pipe token");
3554 assert!(
3555 pipe_pos.unwrap() > heredoc_pos.unwrap(),
3556 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
3557 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
3558 );
3559 }
3560
3561 #[test]
3562 fn test_heredoc_standalone_still_works() {
3563 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
3565 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
3566 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
3567 }
3568
3569 #[test]
3570 fn test_heredoc_preserves_leading_empty_lines() {
3571 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
3573 let heredoc = tokens.iter().find_map(|t| {
3574 if let Token::HereDoc(data) = &t.token {
3575 Some(data.clone())
3576 } else {
3577 None
3578 }
3579 });
3580 assert!(heredoc.is_some(), "should have a heredoc token");
3581 let data = heredoc.unwrap();
3582 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
3583 assert_eq!(data.content, "\nhello\n");
3584 }
3585
3586 #[test]
3587 fn test_heredoc_quoted_delimiter_sets_literal() {
3588 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
3590 let heredoc = tokens.iter().find_map(|t| {
3591 if let Token::HereDoc(data) = &t.token {
3592 Some(data.clone())
3593 } else {
3594 None
3595 }
3596 });
3597 assert!(heredoc.is_some(), "should have a heredoc token");
3598 let data = heredoc.unwrap();
3599 assert!(data.literal, "quoted delimiter should set literal=true");
3600 assert_eq!(data.content, "hello $HOME\n");
3601 }
3602
3603 #[test]
3604 fn test_heredoc_unquoted_delimiter_not_literal() {
3605 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
3607 let heredoc = tokens.iter().find_map(|t| {
3608 if let Token::HereDoc(data) = &t.token {
3609 Some(data.clone())
3610 } else {
3611 None
3612 }
3613 });
3614 assert!(heredoc.is_some(), "should have a heredoc token");
3615 let data = heredoc.unwrap();
3616 assert!(!data.literal, "unquoted delimiter should have literal=false");
3617 }
3618
3619 #[test]
3624 fn colon_double_in_word() {
3625 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
3626 }
3627
3628 #[test]
3629 fn colon_single_in_word() {
3630 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
3631 }
3632
3633 #[test]
3634 fn colon_with_port() {
3635 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
3636 }
3637
3638 #[test]
3639 fn colon_standalone() {
3640 assert_eq!(lex(":"), vec![Token::Colon]);
3641 }
3642
3643 #[test]
3644 fn colon_spaced_no_merge() {
3645 assert_eq!(
3646 lex("foo : bar"),
3647 vec![
3648 Token::Ident("foo".into()),
3649 Token::Colon,
3650 Token::Ident("bar".into()),
3651 ]
3652 );
3653 }
3654
3655 #[test]
3656 fn colon_in_command_arg() {
3657 assert_eq!(
3658 lex("echo foo::bar"),
3659 vec![
3660 Token::Ident("echo".into()),
3661 Token::Ident("foo::bar".into()),
3662 ]
3663 );
3664 }
3665
3666 #[test]
3667 fn colon_trailing() {
3668 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
3670 }
3671
3672 #[test]
3673 fn colon_leading() {
3674 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
3676 }
3677
3678 #[test]
3679 fn colon_with_path() {
3680 assert_eq!(
3682 lex("/usr/bin:8080"),
3683 vec![Token::Ident("/usr/bin:8080".into())]
3684 );
3685 }
3686
3687 #[test]
3692 fn is_keyword_covers_control_flow() {
3693 for t in [
3694 Token::While,
3695 Token::Return,
3696 Token::Break,
3697 Token::Continue,
3698 Token::Exit,
3699 ] {
3700 assert!(t.is_keyword(), "{t:?} should be a keyword");
3701 }
3702 }
3703
3704 #[test]
3705 fn starts_statement_covers_while() {
3706 assert!(Token::While.starts_statement());
3707 }
3708
3709 #[test]
3710 fn is_keyword_rejects_operators() {
3711 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
3712 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
3713 }
3714 }
3715}