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)]
458 ShortFlag(String),
459
460 #[regex(r"\+[a-zA-Z][a-zA-Z0-9]*", lex_plus_flag, priority = 3)]
462 PlusFlag(String),
463
464 #[token("--")]
466 DoubleDash,
467
468 #[regex(r"\+[^a-zA-Z\s][^\s]*", lex_plus_bare, priority = 2)]
471 PlusBare(String),
472
473 #[regex(r"-[^a-zA-Z0-9\s\-][^\s]*", lex_minus_bare, priority = 1)]
477 MinusBare(String),
478
479 #[regex(r"%[0-9]+", lex_job_spec)]
483 JobSpec(String),
484
485 #[token("-")]
489 MinusAlone,
490
491 #[regex(r#""([^"\\]|\\.)*""#, lex_string)]
497 String(String),
498
499 #[regex(r"'[^']*'", lex_single_string)]
501 SingleString(String),
502
503 #[regex(r"\$\{[^}]+\}", lex_varref)]
505 VarRef(String),
506
507 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", lex_simple_varref)]
509 SimpleVarRef(String),
510
511 #[regex(r"\$[0-9]", lex_positional)]
513 Positional(usize),
514
515 #[token("$@")]
517 AllArgs,
518
519 #[token("$#")]
521 ArgCount,
522
523 #[token("$?")]
525 LastExitCode,
526
527 #[token("$$")]
529 CurrentPid,
530
531 #[regex(r"\$\{#[a-zA-Z_][a-zA-Z0-9_]*(\[[^\]]*\])*\}", lex_var_length)]
536 VarLength(String),
537
538 HereDoc(HereDocData),
541
542 #[regex(r"-?[0-9]+", lex_int, priority = 2)]
544 Int(i64),
545
546 #[regex(r"-?[0-9]+\.[0-9]+", lex_float)]
548 Float(f64),
549
550 #[regex(r"[0-9]+[a-zA-Z_][a-zA-Z0-9_.-]*", lex_number_ident, priority = 3)]
559 NumberIdent(String),
560
561 #[regex(r"[0-9]+(\.[0-9]+)?(-[0-9a-zA-Z._]+)+", lex_slice_word, priority = 3)]
572 #[regex(r"-[0-9]+[a-zA-Z_][0-9a-zA-Z._-]*", lex_slice_word, priority = 3)]
573 DashNumWord(String),
574
575 #[regex(r"@[a-zA-Z0-9_./@-]*", lex_slice_word, priority = 3)]
580 AtWord(String),
581
582 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
584 InvalidFloatNoLeading,
585
586 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
589 InvalidFloatNoTrailing,
590
591 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
597 Path(String),
598
599 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.@-]*", lex_ident)]
607 Ident(String),
608
609 #[regex(r"#[^\n\r]*", allow_greedy = true)]
615 Comment,
616
617 #[regex(r"\n|\r\n")]
619 Newline,
620
621 #[regex(r"\\[ \t]*(\n|\r\n)")]
623 LineContinuation,
624
625 #[token("`", reject_backtick)]
634 BacktickRejected,
635}
636
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
642pub enum TokenCategory {
643 Keyword,
645 Operator,
647 String,
649 Number,
651 Variable,
653 Comment,
655 Punctuation,
657 Command,
659 Path,
661 Flag,
663 Error,
665}
666
667impl Token {
668 pub fn category(&self) -> TokenCategory {
670 match self {
671 Token::If
673 | Token::Then
674 | Token::Else
675 | Token::Elif
676 | Token::Fi
677 | Token::For
678 | Token::In
679 | Token::Do
680 | Token::Done
681 | Token::While
682 | Token::Case
683 | Token::Esac
684 | Token::Function
685 | Token::Return
686 | Token::Break
687 | Token::Continue
688 | Token::Exit
689 | Token::Set
690 | Token::Local
691 | Token::True
692 | Token::False
693 | Token::TypeString
694 | Token::TypeInt
695 | Token::TypeFloat
696 | Token::TypeBool => TokenCategory::Keyword,
697
698 Token::Pipe
700 | Token::And
701 | Token::Or
702 | Token::Amp
703 | Token::Eq
704 | Token::EqEq
705 | Token::NotEq
706 | Token::Match
707 | Token::NotMatch
708 | Token::Lt
709 | Token::Gt
710 | Token::LtEq
711 | Token::GtEq
712 | Token::GtGt
713 | Token::Stderr
714 | Token::Both
715 | Token::HereDocStart
716 | Token::HereString
717 | Token::StderrToStdout
718 | Token::StdoutToStderr
719 | Token::StdoutToStderr2 => TokenCategory::Operator,
720
721 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
723
724 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
726
727 Token::VarRef(_)
729 | Token::SimpleVarRef(_)
730 | Token::Positional(_)
731 | Token::AllArgs
732 | Token::ArgCount
733 | Token::VarLength(_)
734 | Token::LastExitCode
735 | Token::CurrentPid => TokenCategory::Variable,
736
737 Token::LongFlag(_)
739 | Token::ShortFlag(_)
740 | Token::PlusFlag(_)
741 | Token::DoubleDash => TokenCategory::Flag,
742
743 Token::Semi
745 | Token::DoubleSemi
746 | Token::Colon
747 | Token::Comma
748 | Token::Dot
749 | Token::LParen
750 | Token::RParen
751 | Token::LBrace
752 | Token::RBrace
753 | Token::LBracket
754 | Token::RBracket
755 | Token::Bang
756 | Token::Question
757 | Token::Star
758 | Token::Newline
759 | Token::LineContinuation
760 | Token::CmdSubstStart
761 | Token::DotDotDot => TokenCategory::Punctuation,
762
763 Token::GlobWord(_) => TokenCategory::Path,
765
766 Token::Comment => TokenCategory::Comment,
768
769 Token::Path(_)
771 | Token::TildePath(_)
772 | Token::RelativePath(_)
773 | Token::Tilde
774 | Token::DotDot
775 | Token::DotSlashPath(_) => TokenCategory::Path,
776
777 Token::Ident(_)
779 | Token::PlusBare(_)
780 | Token::MinusBare(_)
781 | Token::MinusAlone
782 | Token::NumberIdent(_)
783 | Token::DashNumWord(_)
784 | Token::AtWord(_)
785 | Token::DottedIdent(_)
786 | Token::JobSpec(_) => TokenCategory::Command,
787
788 Token::InvalidFloatNoLeading
790 | Token::InvalidFloatNoTrailing
791 | Token::BacktickRejected => TokenCategory::Error,
792 }
793 }
794}
795
796fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
798 parse_string_literal(lex.slice())
799}
800
801fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
803 let s = lex.slice();
804 s[1..s.len() - 1].to_string()
806}
807
808fn lex_varref(lex: &mut logos::Lexer<Token>) -> String {
810 lex.slice().to_string()
812}
813
814fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
816 lex.slice()[1..].to_string()
818}
819
820fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
822 lex.slice()[1..].parse().unwrap_or(0)
824}
825
826fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
828 let s = lex.slice();
830 s[3..s.len() - 1].to_string()
831}
832
833fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
835 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
836}
837
838fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
840 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
841}
842
843fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
847 lex.slice().to_string()
848}
849
850fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
852 lex.slice().to_string()
853}
854
855fn lex_slice_word(lex: &mut logos::Lexer<Token>) -> String {
858 lex.slice().to_string()
859}
860
861fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
864 Err(LexerError::InvalidFloatNoLeading)
865}
866
867fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
871 Err(LexerError::BackticksNotSupported)
872}
873
874fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
877 Err(LexerError::InvalidFloatNoTrailing)
878}
879
880fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
882 let s = lex.slice();
883
884 match s.to_lowercase().as_str() {
887 "true" | "false" if s != "true" && s != "false" => {
888 return Err(LexerError::AmbiguousBoolean(s.to_string()));
889 }
890 _ => {}
891 }
892
893 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
895 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
896 }
897
898 Ok(s.to_string())
899}
900
901fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
903 lex.slice()[2..].to_string()
905}
906
907fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
909 lex.slice()[1..].to_string()
911}
912
913fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
915 lex.slice()[1..].to_string()
917}
918
919fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
921 lex.slice().to_string()
922}
923
924fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
926 lex.slice().to_string()
927}
928
929fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
931 lex.slice().to_string()
932}
933
934fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
936 lex.slice().to_string()
937}
938
939fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
941 lex.slice().to_string()
942}
943
944fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
946 lex.slice().to_string()
947}
948
949fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
951 lex.slice().to_string()
952}
953
954impl fmt::Display for Token {
955 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
956 match self {
957 Token::Set => write!(f, "set"),
958 Token::Local => write!(f, "local"),
959 Token::If => write!(f, "if"),
960 Token::Then => write!(f, "then"),
961 Token::Else => write!(f, "else"),
962 Token::Elif => write!(f, "elif"),
963 Token::Fi => write!(f, "fi"),
964 Token::For => write!(f, "for"),
965 Token::While => write!(f, "while"),
966 Token::In => write!(f, "in"),
967 Token::Do => write!(f, "do"),
968 Token::Done => write!(f, "done"),
969 Token::Case => write!(f, "case"),
970 Token::Esac => write!(f, "esac"),
971 Token::Function => write!(f, "function"),
972 Token::Break => write!(f, "break"),
973 Token::Continue => write!(f, "continue"),
974 Token::Return => write!(f, "return"),
975 Token::Exit => write!(f, "exit"),
976 Token::True => write!(f, "true"),
977 Token::False => write!(f, "false"),
978 Token::TypeString => write!(f, "string"),
979 Token::TypeInt => write!(f, "int"),
980 Token::TypeFloat => write!(f, "float"),
981 Token::TypeBool => write!(f, "bool"),
982 Token::And => write!(f, "&&"),
983 Token::Or => write!(f, "||"),
984 Token::EqEq => write!(f, "=="),
985 Token::NotEq => write!(f, "!="),
986 Token::Match => write!(f, "=~"),
987 Token::NotMatch => write!(f, "!~"),
988 Token::GtEq => write!(f, ">="),
989 Token::LtEq => write!(f, "<="),
990 Token::GtGt => write!(f, ">>"),
991 Token::StderrToStdout => write!(f, "2>&1"),
992 Token::StdoutToStderr => write!(f, "1>&2"),
993 Token::StdoutToStderr2 => write!(f, ">&2"),
994 Token::Stderr => write!(f, "2>"),
995 Token::Both => write!(f, "&>"),
996 Token::HereDocStart => write!(f, "<<"),
997 Token::HereString => write!(f, "<<<"),
998 Token::DoubleSemi => write!(f, ";;"),
999 Token::Eq => write!(f, "="),
1000 Token::Pipe => write!(f, "|"),
1001 Token::Amp => write!(f, "&"),
1002 Token::Gt => write!(f, ">"),
1003 Token::Lt => write!(f, "<"),
1004 Token::Semi => write!(f, ";"),
1005 Token::Colon => write!(f, ":"),
1006 Token::Comma => write!(f, ","),
1007 Token::Dot => write!(f, "."),
1008 Token::DotDot => write!(f, ".."),
1009 Token::DotDotDot => write!(f, "..."),
1010 Token::Tilde => write!(f, "~"),
1011 Token::TildePath(s) => write!(f, "{}", s),
1012 Token::RelativePath(s) => write!(f, "{}", s),
1013 Token::DotSlashPath(s) => write!(f, "{}", s),
1014 Token::LBrace => write!(f, "{{"),
1015 Token::RBrace => write!(f, "}}"),
1016 Token::LBracket => write!(f, "["),
1017 Token::RBracket => write!(f, "]"),
1018 Token::LParen => write!(f, "("),
1019 Token::RParen => write!(f, ")"),
1020 Token::Star => write!(f, "*"),
1021 Token::Bang => write!(f, "!"),
1022 Token::Question => write!(f, "?"),
1023 Token::GlobWord(s) => write!(f, "GLOB({})", s),
1024 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
1025 Token::CmdSubstStart => write!(f, "$("),
1026 Token::LongFlag(s) => write!(f, "--{}", s),
1027 Token::ShortFlag(s) => write!(f, "-{}", s),
1028 Token::PlusFlag(s) => write!(f, "+{}", s),
1029 Token::DoubleDash => write!(f, "--"),
1030 Token::PlusBare(s) => write!(f, "{}", s),
1031 Token::MinusBare(s) => write!(f, "{}", s),
1032 Token::JobSpec(s) => write!(f, "{}", s),
1033 Token::MinusAlone => write!(f, "-"),
1034 Token::String(s) => write!(f, "STRING({:?})", s),
1035 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
1036 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
1037 Token::VarRef(v) => write!(f, "VARREF({})", v),
1038 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
1039 Token::Positional(n) => write!(f, "${}", n),
1040 Token::AllArgs => write!(f, "$@"),
1041 Token::ArgCount => write!(f, "$#"),
1042 Token::LastExitCode => write!(f, "$?"),
1043 Token::CurrentPid => write!(f, "$$"),
1044 Token::VarLength(v) => write!(f, "${{#{}}}", v),
1045 Token::Int(n) => write!(f, "INT({})", n),
1046 Token::Float(n) => write!(f, "FLOAT({})", n),
1047 Token::Path(s) => write!(f, "PATH({})", s),
1048 Token::Ident(s) => write!(f, "IDENT({})", s),
1049 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1050 Token::DashNumWord(s) => write!(f, "DASHNUM({})", s),
1051 Token::AtWord(s) => write!(f, "ATWORD({})", s),
1052 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1053 Token::Comment => write!(f, "COMMENT"),
1054 Token::Newline => write!(f, "NEWLINE"),
1055 Token::LineContinuation => write!(f, "LINECONT"),
1056 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1058 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1059 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1060 }
1061 }
1062}
1063
1064impl Token {
1065 pub fn is_keyword(&self) -> bool {
1070 matches!(
1071 self,
1072 Token::Set
1073 | Token::Local
1074 | Token::If
1075 | Token::Then
1076 | Token::Else
1077 | Token::Elif
1078 | Token::Fi
1079 | Token::For
1080 | Token::In
1081 | Token::Do
1082 | Token::Done
1083 | Token::While
1084 | Token::Case
1085 | Token::Esac
1086 | Token::Function
1087 | Token::Return
1088 | Token::Break
1089 | Token::Continue
1090 | Token::Exit
1091 | Token::True
1092 | Token::False
1093 )
1094 }
1095
1096 pub fn is_type(&self) -> bool {
1098 matches!(
1099 self,
1100 Token::TypeString
1101 | Token::TypeInt
1102 | Token::TypeFloat
1103 | Token::TypeBool
1104 )
1105 }
1106
1107 pub fn starts_statement(&self) -> bool {
1110 matches!(
1111 self,
1112 Token::Set
1113 | Token::Local
1114 | Token::Function
1115 | Token::If
1116 | Token::For
1117 | Token::While
1118 | Token::Case
1119 | Token::Ident(_)
1120 | Token::LBracket
1121 )
1122 }
1123
1124 pub fn is_value(&self) -> bool {
1126 matches!(
1127 self,
1128 Token::String(_)
1129 | Token::SingleString(_)
1130 | Token::HereDoc(_)
1131 | Token::Arithmetic(_)
1132 | Token::Int(_)
1133 | Token::Float(_)
1134 | Token::True
1135 | Token::False
1136 | Token::VarRef(_)
1137 | Token::SimpleVarRef(_)
1138 | Token::CmdSubstStart
1139 | Token::Path(_)
1140 | Token::GlobWord(_)
1141 | Token::LastExitCode
1142 | Token::CurrentPid
1143 )
1144 }
1145}
1146
1147struct ArithmeticPreprocessResult {
1149 text: String,
1151 arithmetics: Vec<(String, String)>,
1153 replacements: Vec<SpanReplacement>,
1155}
1156
1157fn skip_command_substitution(
1166 chars: &[char],
1167 i: &mut usize,
1168 source_pos: &mut usize,
1169 result: &mut String,
1170) {
1171 result.push('$');
1173 result.push('(');
1174 *i += 2;
1175 *source_pos += 2;
1176
1177 let mut depth: usize = 1;
1178 let mut in_single_quote = false;
1179 let mut in_double_quote = false;
1180
1181 while *i < chars.len() && depth > 0 {
1182 let c = chars[*i];
1183
1184 if in_single_quote {
1185 result.push(c);
1186 *source_pos += c.len_utf8();
1187 *i += 1;
1188 if c == '\'' {
1189 in_single_quote = false;
1190 }
1191 continue;
1192 }
1193
1194 if in_double_quote {
1195 if c == '\\' && *i + 1 < chars.len() {
1196 let next = chars[*i + 1];
1197 if next == '"' || next == '\\' || next == '$' || next == '`' {
1198 result.push(c);
1199 result.push(next);
1200 *source_pos += c.len_utf8() + next.len_utf8();
1201 *i += 2;
1202 continue;
1203 }
1204 }
1205 if c == '"' {
1206 in_double_quote = false;
1207 }
1208 result.push(c);
1209 *source_pos += c.len_utf8();
1210 *i += 1;
1211 continue;
1212 }
1213
1214 match c {
1216 '\'' => {
1217 in_single_quote = true;
1218 result.push(c);
1219 *source_pos += c.len_utf8();
1220 *i += 1;
1221 }
1222 '"' => {
1223 in_double_quote = true;
1224 result.push(c);
1225 *source_pos += c.len_utf8();
1226 *i += 1;
1227 }
1228 '\\' if *i + 1 < chars.len() => {
1229 result.push(c);
1230 result.push(chars[*i + 1]);
1231 *source_pos += c.len_utf8() + chars[*i + 1].len_utf8();
1232 *i += 2;
1233 }
1234 '(' => {
1235 depth += 1;
1236 result.push(c);
1237 *source_pos += c.len_utf8();
1238 *i += 1;
1239 }
1240 ')' => {
1241 depth -= 1;
1242 result.push(c);
1243 *source_pos += c.len_utf8();
1244 *i += 1;
1245 }
1246 _ => {
1247 result.push(c);
1248 *source_pos += c.len_utf8();
1249 *i += 1;
1250 }
1251 }
1252 }
1253}
1254
1255fn preprocess_arithmetic(source: &str) -> Result<ArithmeticPreprocessResult, LexerError> {
1269 let mut result = String::with_capacity(source.len());
1270 let mut arithmetics: Vec<(String, String)> = Vec::new();
1271 let mut replacements: Vec<SpanReplacement> = Vec::new();
1272 let mut source_pos: usize = 0;
1273 let chars_vec: Vec<char> = source.chars().collect();
1274 let mut i = 0;
1275
1276 let mut in_double_quote = false;
1279
1280 while i < chars_vec.len() {
1281 let ch = chars_vec[i];
1282
1283 if !in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1285 result.push(ch);
1286 result.push(chars_vec[i + 1]);
1287 source_pos += ch.len_utf8() + chars_vec[i + 1].len_utf8();
1288 i += 2;
1289 continue;
1290 }
1291
1292 if ch == '\'' && !in_double_quote {
1294 result.push(ch);
1295 i += 1;
1296 source_pos += 1;
1297 while i < chars_vec.len() && chars_vec[i] != '\'' {
1298 result.push(chars_vec[i]);
1299 source_pos += chars_vec[i].len_utf8();
1300 i += 1;
1301 }
1302 if i < chars_vec.len() {
1303 result.push(chars_vec[i]); source_pos += 1;
1305 i += 1;
1306 }
1307 continue;
1308 }
1309
1310 if ch == '"' {
1312 in_double_quote = !in_double_quote;
1313 result.push(ch);
1314 i += 1;
1315 source_pos += 1;
1316 continue;
1317 }
1318
1319 if in_double_quote && ch == '\\' && i + 1 < chars_vec.len() {
1321 let next = chars_vec[i + 1];
1322 if next == '"' || next == '\\' || next == '$' || next == '`' {
1323 result.push(ch);
1324 result.push(next);
1325 source_pos += ch.len_utf8() + next.len_utf8();
1326 i += 2;
1327 continue;
1328 }
1329 }
1330
1331 if ch == '#' && !in_double_quote {
1339 while i < chars_vec.len() && chars_vec[i] != '\n' && chars_vec[i] != '\r' {
1340 result.push(chars_vec[i]);
1341 source_pos += chars_vec[i].len_utf8();
1342 i += 1;
1343 }
1344 continue;
1345 }
1346
1347 if ch == '$' && i + 1 < chars_vec.len() && chars_vec[i + 1] == '('
1349 && !(i + 2 < chars_vec.len() && chars_vec[i + 2] == '(')
1350 {
1351 skip_command_substitution(&chars_vec, &mut i, &mut source_pos, &mut result);
1352 continue;
1353 }
1354
1355 if ch == '$' && i + 2 < chars_vec.len() && chars_vec[i + 1] == '(' && chars_vec[i + 2] == '(' {
1357 let arith_start_pos = result.len();
1358 let original_start = source_pos;
1359
1360 i += 3;
1362 source_pos += 3;
1363
1364 let mut expr = String::new();
1366 let mut paren_depth: usize = 0;
1367 let mut closed = false;
1368
1369 while i < chars_vec.len() {
1370 let c = chars_vec[i];
1371 match c {
1372 '(' => {
1373 paren_depth += 1;
1374 if paren_depth > MAX_PAREN_DEPTH {
1375 return Err(LexerError::NestingTooDeep);
1376 }
1377 expr.push('(');
1378 i += 1;
1379 source_pos += c.len_utf8();
1380 }
1381 ')' => {
1382 if paren_depth > 0 {
1383 paren_depth -= 1;
1384 expr.push(')');
1385 i += 1;
1386 source_pos += 1;
1387 } else if i + 1 < chars_vec.len() && chars_vec[i + 1] == ')' {
1388 i += 2;
1390 source_pos += 2;
1391 closed = true;
1392 break;
1393 } else {
1394 expr.push(')');
1396 i += 1;
1397 source_pos += 1;
1398 }
1399 }
1400 _ => {
1401 expr.push(c);
1402 i += 1;
1403 source_pos += c.len_utf8();
1404 }
1405 }
1406 }
1407
1408 if !closed {
1411 return Err(LexerError::UnterminatedArithmetic);
1412 }
1413
1414 let original_len = source_pos - original_start;
1416
1417 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1419 let marker_len = marker.len();
1420
1421 replacements.push(SpanReplacement {
1423 preprocessed_pos: arith_start_pos,
1424 marker_len,
1425 original_len,
1426 });
1427
1428 arithmetics.push((marker.clone(), expr));
1429 result.push_str(&marker);
1430 } else {
1431 result.push(ch);
1432 i += 1;
1433 source_pos += ch.len_utf8();
1434 }
1435 }
1436
1437 Ok(ArithmeticPreprocessResult {
1438 text: result,
1439 arithmetics,
1440 replacements,
1441 })
1442}
1443
1444#[derive(Debug, Clone)]
1459struct HeredocReplacement {
1460 marker: String,
1461 body: String,
1462 literal: bool,
1463 strip_tabs: bool,
1464 body_start_offset: usize,
1465}
1466
1467fn preprocess_heredocs(source: &str) -> Result<(String, Vec<HeredocReplacement>), Spanned<LexerError>> {
1479 let mut result = String::with_capacity(source.len());
1480 let mut heredocs: Vec<HeredocReplacement> = Vec::new();
1481 let chars_vec: Vec<char> = source.chars().collect();
1482 let mut i = 0;
1483 let mut pos: usize = 0;
1487
1488 while i < chars_vec.len() {
1489 let ch = chars_vec[i];
1490
1491 if ch == '<'
1495 && chars_vec.get(i + 1) == Some(&'<')
1496 && chars_vec.get(i + 2) == Some(&'<')
1497 {
1498 result.push_str("<<<");
1499 i += 3;
1500 pos += 3;
1501 continue;
1502 }
1503
1504 if ch == '<' && chars_vec.get(i + 1) == Some(&'<') {
1506 let introducer_start = pos;
1509 i += 2; pos += 2;
1511
1512 let strip_tabs = chars_vec.get(i) == Some(&'-');
1514 if strip_tabs {
1515 i += 1;
1516 pos += 1;
1517 }
1518
1519 while let Some(&c) = chars_vec.get(i) {
1521 if c == ' ' || c == '\t' {
1522 i += 1;
1523 pos += 1;
1524 } else {
1525 break;
1526 }
1527 }
1528
1529 let mut delimiter = String::new();
1531 let quoted = chars_vec.get(i) == Some(&'\'') || chars_vec.get(i) == Some(&'"');
1532 let quote_char = if quoted {
1533 let q = chars_vec.get(i).copied();
1534 i += 1;
1535 pos += 1;
1536 q
1537 } else {
1538 None
1539 };
1540
1541 while let Some(&c) = chars_vec.get(i) {
1542 if quoted {
1543 if Some(c) == quote_char {
1544 i += 1; pos += 1;
1546 break;
1547 }
1548 } else if c.is_whitespace() || c == '\n' || c == '\r' {
1549 break;
1550 }
1551 delimiter.push(c);
1552 i += 1;
1553 pos += c.len_utf8();
1554 }
1555
1556 if delimiter.is_empty() {
1557 result.push_str("<<");
1559 if strip_tabs {
1560 result.push('-');
1561 }
1562 continue;
1563 }
1564
1565 let mut after_delimiter = String::new();
1568 while let Some(&c) = chars_vec.get(i) {
1569 if c == '\n' {
1570 i += 1;
1571 pos += 1;
1572 break;
1573 } else if c == '\r' {
1574 i += 1;
1575 pos += 1;
1576 if chars_vec.get(i) == Some(&'\n') {
1577 i += 1;
1578 pos += 1;
1579 }
1580 break;
1581 }
1582 after_delimiter.push(c);
1583 i += 1;
1584 pos += c.len_utf8();
1585 }
1586
1587 let body_start_offset = pos;
1593 let mut content = String::new();
1594 let mut current_line = String::new();
1595
1596 loop {
1597 let next = chars_vec.get(i).copied();
1598 match next {
1599 Some('\n') => {
1600 i += 1;
1601 pos += 1;
1602 let trimmed = if strip_tabs {
1604 current_line.trim_start_matches('\t')
1605 } else {
1606 ¤t_line
1607 };
1608 if trimmed == delimiter {
1609 break;
1611 }
1612 content.push_str(¤t_line);
1614 content.push('\n');
1615 current_line.clear();
1616 }
1617 Some('\r') => {
1618 i += 1;
1619 pos += 1;
1620 let crlf = chars_vec.get(i) == Some(&'\n');
1626 if crlf {
1627 i += 1;
1628 pos += 1;
1629 }
1630 let trimmed = if strip_tabs {
1631 current_line.trim_start_matches('\t')
1632 } else {
1633 ¤t_line
1634 };
1635 if trimmed == delimiter {
1636 break;
1637 }
1638 content.push_str(¤t_line);
1639 content.push_str(if crlf { "\r\n" } else { "\r" });
1640 current_line.clear();
1641 }
1642 Some(c) => {
1643 current_line.push(c);
1644 i += 1;
1645 pos += c.len_utf8();
1646 }
1647 None => {
1648 let trimmed = if strip_tabs {
1651 current_line.trim_start_matches('\t')
1652 } else {
1653 ¤t_line
1654 };
1655 if trimmed == delimiter {
1656 break;
1657 }
1658 let span_end = introducer_start
1663 + 2
1664 + if strip_tabs { 1 } else { 0 }
1665 + delimiter.len();
1666 return Err(Spanned::new(
1667 LexerError::UnterminatedHeredoc {
1668 delimiter: delimiter.clone(),
1669 },
1670 introducer_start..span_end,
1671 ));
1672 }
1673 }
1674 }
1675
1676 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1678 heredocs.push(HeredocReplacement {
1679 marker: marker.clone(),
1680 body: content,
1681 literal: quoted,
1682 strip_tabs,
1683 body_start_offset,
1684 });
1685
1686 result.push_str("<<");
1689 result.push_str(&marker);
1690 result.push_str(&after_delimiter);
1691 result.push('\n');
1692 } else {
1693 result.push(ch);
1694 i += 1;
1695 pos += ch.len_utf8();
1696 }
1697 }
1698
1699 Ok((result, heredocs))
1700}
1701
1702fn mergeable_text(token: &Token) -> Option<String> {
1707 match token {
1708 Token::Ident(s) => Some(s.clone()),
1709 Token::NumberIdent(s) => Some(s.clone()),
1710 Token::DashNumWord(s) => Some(s.clone()),
1711 Token::AtWord(s) => Some(s.clone()),
1712 Token::DottedIdent(s) => Some(s.clone()),
1713 Token::Colon => Some(":".to_string()),
1714 Token::Int(n) => Some(n.to_string()),
1715 Token::Path(p) => Some(p.clone()),
1716 Token::Float(f) => Some(f.to_string()),
1717 _ => None,
1718 }
1719}
1720
1721#[derive(Clone, Copy, Default)]
1762struct ValueContext {
1763 in_literal: bool,
1766 in_brace: bool,
1770}
1771
1772fn compute_value_context(tokens: &[Spanned<Token>]) -> Vec<ValueContext> {
1773 let mut ctx = vec![ValueContext::default(); tokens.len()];
1774 let mut bracket_depth: usize = 0;
1775 let mut brace_depth: usize = 0;
1776 let mut test_depth: usize = 0;
1781 let mut expect_value = false;
1782 let mut skip_paired_bracket = false;
1785
1786 for i in 0..tokens.len() {
1787 let tok = &tokens[i].token;
1788
1789 let currently_value = expect_value || bracket_depth > 0 || brace_depth > 0;
1790 ctx[i] = ValueContext {
1791 in_literal: currently_value,
1792 in_brace: brace_depth > 0,
1793 };
1794
1795 if skip_paired_bracket {
1796 skip_paired_bracket = false;
1799 } else {
1800 match tok {
1801 Token::LBracket => {
1802 let next_is_lbracket =
1803 matches!(tokens.get(i + 1).map(|t| &t.token), Some(Token::LBracket));
1804 if next_is_lbracket && !currently_value {
1805 test_depth += 1;
1810 skip_paired_bracket = true;
1811 } else if currently_value {
1812 bracket_depth += 1;
1813 }
1814 }
1817 Token::RBracket => {
1818 let next_is_rbracket =
1819 matches!(tokens.get(i + 1).map(|t| &t.token), Some(Token::RBracket));
1820 if bracket_depth > 0 {
1821 bracket_depth -= 1;
1825 } else if test_depth > 0 && next_is_rbracket {
1826 test_depth -= 1;
1827 skip_paired_bracket = true;
1828 }
1829 }
1830 Token::LBrace if currently_value => brace_depth += 1,
1831 Token::RBrace if brace_depth > 0 => brace_depth -= 1,
1832 _ => {}
1833 }
1834 }
1835
1836 expect_value = match tok {
1841 Token::Eq => test_depth == 0,
1842 Token::In => test_depth > 0,
1843 _ => false,
1844 };
1845 }
1846
1847 ctx
1848}
1849
1850fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1862 if tokens.is_empty() {
1863 return tokens;
1864 }
1865
1866 let value_ctx = compute_value_context(&tokens);
1867 let mut result = Vec::with_capacity(tokens.len());
1868 let mut run: Vec<&Spanned<Token>> = Vec::new();
1869 let mut run_start = 0usize;
1870
1871 for (idx, token) in tokens.iter().enumerate() {
1872 if run.is_empty() {
1873 if mergeable_text(&token.token).is_some() {
1874 run.push(token);
1875 run_start = idx;
1876 } else {
1877 result.push(token.clone());
1878 }
1879 continue;
1880 }
1881
1882 let Some(last) = run.last() else { unreachable!() };
1885 let adjacent = last.span.end == token.span.start;
1886
1887 if adjacent && mergeable_text(&token.token).is_some() {
1888 run.push(token);
1889 } else {
1890 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace);
1891 if mergeable_text(&token.token).is_some() {
1892 run.push(token);
1893 run_start = idx;
1894 } else {
1895 result.push(token.clone());
1896 }
1897 }
1898 }
1899
1900 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace);
1901
1902 result
1903}
1904
1905fn flush_colon_run(run: &mut Vec<&Spanned<Token>>, result: &mut Vec<Spanned<Token>>, suppress: bool) {
1910 if run.is_empty() {
1911 return;
1912 }
1913
1914 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
1915
1916 if !suppress && run.len() >= 2 && has_colon {
1917 let text: String = run
1918 .iter()
1919 .filter_map(|t| mergeable_text(&t.token))
1920 .collect();
1921 let start = run.first().map(|t| t.span.start).unwrap_or(0);
1923 let end = run.last().map(|t| t.span.end).unwrap_or(0);
1924 result.push(Spanned::new(Token::Ident(text), start..end));
1925 } else {
1926 for t in run.iter() {
1927 result.push((*t).clone());
1928 }
1929 }
1930
1931 run.clear();
1932}
1933
1934fn glob_mergeable_text(token: &Token) -> Option<String> {
1939 match token {
1940 Token::Star => Some("*".to_string()),
1941 Token::Question => Some("?".to_string()),
1942 Token::Dot => Some(".".to_string()),
1943 Token::DotDot => Some("..".to_string()),
1944 Token::Ident(s) => Some(s.clone()),
1945 Token::NumberIdent(s) => Some(s.clone()),
1946 Token::DashNumWord(s) => Some(s.clone()),
1947 Token::AtWord(s) => Some(s.clone()),
1948 Token::DottedIdent(s) => Some(s.clone()),
1949 Token::Path(s) => Some(s.clone()),
1950 Token::Int(n) => Some(n.to_string()),
1951 Token::LBracket => Some("[".to_string()),
1952 Token::RBracket => Some("]".to_string()),
1953 Token::Bang => Some("!".to_string()),
1954 Token::DotSlashPath(s) => Some(s.clone()),
1955 Token::RelativePath(s) => Some(s.clone()),
1956 Token::TildePath(s) => Some(s.clone()),
1957 Token::Tilde => Some("~".to_string()),
1958 Token::LBrace => Some("{".to_string()),
1959 Token::RBrace => Some("}".to_string()),
1960 Token::Comma => Some(",".to_string()),
1961 _ => None,
1962 }
1963}
1964
1965fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
1994 if tokens.len() < 2 {
1995 return tokens;
1996 }
1997
1998 let mut result = Vec::with_capacity(tokens.len());
1999 let mut i = 0;
2000
2001 while i < tokens.len() {
2002 let token = &tokens[i];
2003
2004 if let Token::ShortFlag(flag_name) = &token.token {
2006 let mut fused = flag_name.clone();
2008 let mut end_span = token.span.end;
2009 let mut j = i + 1;
2010
2011 while let Some(next) = tokens.get(j) {
2012 if next.span.start == end_span {
2013 if let Token::Colon = &next.token {
2014 fused.push(':');
2015 end_span = next.span.end;
2016 j += 1;
2017 continue;
2018 }
2019 }
2020 break;
2021 }
2022
2023 if j > i + 1 {
2024 let span = token.span.start..end_span;
2026 result.push(Spanned::new(Token::ShortFlag(fused), span));
2027 i = j;
2028 continue;
2029 }
2030 }
2031
2032 result.push(token.clone());
2033 i += 1;
2034 }
2035
2036 result
2037}
2038
2039fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
2063 if tokens.is_empty() {
2064 return tokens;
2065 }
2066
2067 let value_ctx = compute_value_context(&tokens);
2068 let mut result = Vec::with_capacity(tokens.len());
2069 let mut run: Vec<&Spanned<Token>> = Vec::new();
2070 let mut run_start = 0usize;
2071
2072 for (idx, token) in tokens.iter().enumerate() {
2073 if run.is_empty() {
2074 if glob_mergeable_text(&token.token).is_some() {
2075 run.push(token);
2076 run_start = idx;
2077 } else {
2078 result.push(token.clone());
2079 }
2080 continue;
2081 }
2082
2083 let Some(last) = run.last() else { unreachable!() };
2085 let adjacent = last.span.end == token.span.start;
2086
2087 if adjacent && glob_mergeable_text(&token.token).is_some() {
2088 run.push(token);
2089 } else {
2090 let followed_by_eq = matches!(token.token, Token::Eq);
2095 flush_glob_run(
2096 &mut run,
2097 &mut result,
2098 value_ctx[run_start].in_literal,
2099 followed_by_eq,
2100 );
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 }
2108 }
2109
2110 flush_glob_run(&mut run, &mut result, value_ctx[run_start].in_literal, false);
2113
2114 result
2115}
2116
2117fn flush_glob_run(
2138 run: &mut Vec<&Spanned<Token>>,
2139 result: &mut Vec<Spanned<Token>>,
2140 value_position_suppress: bool,
2141 followed_by_eq: bool,
2142) {
2143 if run.is_empty() {
2144 return;
2145 }
2146
2147 let has_bracket_pair = run.iter().any(|t| matches!(t.token, Token::LBracket))
2148 && run.iter().any(|t| matches!(t.token, Token::RBracket));
2149 let has_star_or_question = run
2150 .iter()
2151 .any(|t| matches!(t.token, Token::Star | Token::Question));
2152 let has_glob = has_star_or_question || has_bracket_pair;
2153
2154 let run_starts_with_ident = matches!(
2163 run.first().map(|t| &t.token),
2164 Some(Token::Ident(_))
2165 );
2166 let lvalue_suppress =
2167 followed_by_eq && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2168 let suppress = (value_position_suppress && has_bracket_pair) || lvalue_suppress;
2169
2170 if !suppress && run.len() >= 2 && has_glob {
2171 let text: String = run
2172 .iter()
2173 .filter_map(|t| glob_mergeable_text(&t.token))
2174 .collect();
2175 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2176 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2177 result.push(Spanned::new(Token::GlobWord(text), start..end));
2178 } else {
2179 for t in run.iter() {
2180 result.push((*t).clone());
2181 }
2182 }
2183
2184 run.clear();
2185}
2186
2187pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2197 let arith_result = preprocess_arithmetic(source)
2199 .map_err(|e| vec![Spanned::new(e, 0..source.len())])?;
2200
2201 let span_replacements = arith_result.replacements;
2205 let (preprocessed, heredocs) = preprocess_heredocs(&arith_result.text)
2206 .map_err(|e| {
2207 let span = correct_span(e.span, &span_replacements);
2208 vec![Spanned::new(e.token, span)]
2209 })?;
2210
2211 let lexer = Token::lexer(&preprocessed);
2212 let mut tokens = Vec::new();
2213 let mut errors = Vec::new();
2214
2215 for (result, span) in lexer.spanned() {
2216 let corrected_span = correct_span(span, &span_replacements);
2218 match result {
2219 Ok(token) => {
2220 if !matches!(token, Token::Comment | Token::LineContinuation) {
2222 tokens.push(Spanned::new(token, corrected_span));
2223 }
2224 }
2225 Err(err) => {
2226 errors.push(Spanned::new(err, corrected_span));
2227 }
2228 }
2229 }
2230
2231 if !errors.is_empty() {
2232 return Err(errors);
2233 }
2234
2235 let mut final_tokens = Vec::with_capacity(tokens.len());
2237 let mut i = 0;
2238
2239 while i < tokens.len() {
2240 if let Token::Ident(ref name) = tokens[i].token
2242 && name.starts_with("__KAISH_ARITH_") && name.ends_with("__")
2243 && let Some((_, expr)) = arith_result.arithmetics.iter().find(|(marker, _)| marker == name) {
2244 final_tokens.push(Spanned::new(Token::Arithmetic(expr.clone()), tokens[i].span.clone()));
2245 i += 1;
2246 continue;
2247 }
2248
2249 if matches!(tokens[i].token, Token::HereDocStart) {
2251 if i + 1 < tokens.len()
2253 && let Token::Ident(ref name) = tokens[i + 1].token
2254 && name.starts_with("__KAISH_HEREDOC_") && name.ends_with("__") {
2255 if let Some(hd) = heredocs.iter().find(|h| h.marker == *name) {
2257 let mut content = hd.body.clone();
2269 for (marker, expr) in &arith_result.arithmetics {
2270 if content.contains(marker) {
2271 let replacement = if hd.literal {
2272 format!("$(({}))", expr)
2273 } else {
2274 format!("${{__ARITH:{}__}}", expr)
2275 };
2276 content = content.replace(marker, &replacement);
2277 }
2278 }
2279 final_tokens.push(Spanned::new(Token::HereDocStart, tokens[i].span.clone()));
2280 final_tokens.push(Spanned::new(
2281 Token::HereDoc(HereDocData {
2282 content,
2283 literal: hd.literal,
2284 strip_tabs: hd.strip_tabs,
2285 body_start_offset: hd.body_start_offset,
2286 }),
2287 tokens[i + 1].span.clone(),
2288 ));
2289 i += 2;
2290 continue;
2291 }
2292 }
2293 }
2294
2295 let token = if let Token::String(ref s) = tokens[i].token {
2297 let mut new_content = s.clone();
2299 for (marker, expr) in &arith_result.arithmetics {
2300 if new_content.contains(marker) {
2301 new_content = new_content.replace(marker, &format!("${{__ARITH:{}__}}", expr));
2304 }
2305 }
2306 if new_content != *s {
2307 Spanned::new(Token::String(new_content), tokens[i].span.clone())
2308 } else {
2309 tokens[i].clone()
2310 }
2311 } else {
2312 tokens[i].clone()
2313 };
2314 final_tokens.push(token);
2315 i += 1;
2316 }
2317
2318 Ok(merge_glob_adjacent(merge_colon_adjacent(
2319 merge_flag_metachar_adjacent(final_tokens),
2320 )))
2321}
2322
2323pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2327 let lexer = Token::lexer(source);
2328 let mut tokens = Vec::new();
2329 let mut errors = Vec::new();
2330
2331 for (result, span) in lexer.spanned() {
2332 match result {
2333 Ok(token) => {
2334 tokens.push(Spanned::new(token, span));
2335 }
2336 Err(err) => {
2337 errors.push(Spanned::new(err, span));
2338 }
2339 }
2340 }
2341
2342 if errors.is_empty() {
2343 Ok(tokens)
2344 } else {
2345 Err(errors)
2346 }
2347}
2348
2349pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2351 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2353 return Err(LexerError::UnterminatedString);
2354 }
2355
2356 let inner = &source[1..source.len() - 1];
2357 let mut result = String::with_capacity(inner.len());
2358 let mut chars = inner.chars().peekable();
2359
2360 while let Some(ch) = chars.next() {
2361 if ch == '\\' {
2362 match chars.next() {
2363 Some('n') => result.push('\n'),
2364 Some('t') => result.push('\t'),
2365 Some('r') => result.push('\r'),
2366 Some('\\') => result.push('\\'),
2367 Some('"') => result.push('"'),
2368 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2371 Some('u') => {
2372 let mut hex = String::with_capacity(4);
2374 for _ in 0..4 {
2375 match chars.next() {
2376 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2377 _ => return Err(LexerError::InvalidEscape),
2378 }
2379 }
2380 let codepoint = u32::from_str_radix(&hex, 16)
2381 .map_err(|_| LexerError::InvalidEscape)?;
2382 let ch = char::from_u32(codepoint)
2383 .ok_or(LexerError::InvalidEscape)?;
2384 result.push(ch);
2385 }
2386 Some(next) => {
2388 result.push('\\');
2389 result.push(next);
2390 }
2391 None => return Err(LexerError::InvalidEscape),
2392 }
2393 } else {
2394 result.push(ch);
2395 }
2396 }
2397
2398 Ok(result)
2399}
2400
2401pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
2404 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
2406 return Err(LexerError::UnterminatedVarRef);
2407 }
2408
2409 let inner = &source[2..source.len() - 1];
2410
2411 if inner == "?" {
2413 return Ok(vec!["?".to_string()]);
2414 }
2415
2416 let mut segments = Vec::new();
2417 let mut current = String::new();
2418 let mut chars = inner.chars().peekable();
2419
2420 while let Some(ch) = chars.next() {
2421 match ch {
2422 '.' => {
2423 if !current.is_empty() {
2424 segments.push(current.clone());
2425 current.clear();
2426 }
2427 }
2428 '[' => {
2429 if !current.is_empty() {
2430 segments.push(current.clone());
2431 current.clear();
2432 }
2433 let mut index = String::from("[");
2435 while let Some(&c) = chars.peek() {
2436 if let Some(c) = chars.next() {
2437 index.push(c);
2438 }
2439 if c == ']' {
2440 break;
2441 }
2442 }
2443 segments.push(index);
2444 }
2445 _ => {
2446 current.push(ch);
2447 }
2448 }
2449 }
2450
2451 if !current.is_empty() {
2452 segments.push(current);
2453 }
2454
2455 Ok(segments)
2456}
2457
2458pub fn parse_int(source: &str) -> Result<i64, LexerError> {
2460 source.parse().map_err(|_| LexerError::InvalidNumber)
2461}
2462
2463pub fn parse_float(source: &str) -> Result<f64, LexerError> {
2465 source.parse().map_err(|_| LexerError::InvalidNumber)
2466}
2467
2468#[cfg(test)]
2469#[allow(clippy::approx_constant)]
2470mod tests {
2471 use super::*;
2472
2473 fn lex(source: &str) -> Vec<Token> {
2474 tokenize(source)
2475 .expect("lexer should succeed")
2476 .into_iter()
2477 .map(|s| s.token)
2478 .collect()
2479 }
2480
2481 #[test]
2486 fn keywords() {
2487 assert_eq!(lex("set"), vec![Token::Set]);
2488 assert_eq!(lex("if"), vec![Token::If]);
2489 assert_eq!(lex("then"), vec![Token::Then]);
2490 assert_eq!(lex("else"), vec![Token::Else]);
2491 assert_eq!(lex("elif"), vec![Token::Elif]);
2492 assert_eq!(lex("fi"), vec![Token::Fi]);
2493 assert_eq!(lex("for"), vec![Token::For]);
2494 assert_eq!(lex("in"), vec![Token::In]);
2495 assert_eq!(lex("do"), vec![Token::Do]);
2496 assert_eq!(lex("done"), vec![Token::Done]);
2497 assert_eq!(lex("case"), vec![Token::Case]);
2498 assert_eq!(lex("esac"), vec![Token::Esac]);
2499 assert_eq!(lex("function"), vec![Token::Function]);
2500 assert_eq!(lex("true"), vec![Token::True]);
2501 assert_eq!(lex("false"), vec![Token::False]);
2502 }
2503
2504 #[test]
2505 fn double_semicolon() {
2506 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
2507 assert_eq!(lex("echo \"hi\";;"), vec![
2509 Token::Ident("echo".to_string()),
2510 Token::String("hi".to_string()),
2511 Token::DoubleSemi,
2512 ]);
2513 }
2514
2515 #[test]
2516 fn type_keywords() {
2517 assert_eq!(lex("string"), vec![Token::TypeString]);
2518 assert_eq!(lex("int"), vec![Token::TypeInt]);
2519 assert_eq!(lex("float"), vec![Token::TypeFloat]);
2520 assert_eq!(lex("bool"), vec![Token::TypeBool]);
2521 }
2522
2523 #[test]
2528 fn single_char_operators() {
2529 assert_eq!(lex("="), vec![Token::Eq]);
2530 assert_eq!(lex("|"), vec![Token::Pipe]);
2531 assert_eq!(lex("&"), vec![Token::Amp]);
2532 assert_eq!(lex(">"), vec![Token::Gt]);
2533 assert_eq!(lex("<"), vec![Token::Lt]);
2534 assert_eq!(lex(";"), vec![Token::Semi]);
2535 assert_eq!(lex(":"), vec![Token::Colon]);
2536 assert_eq!(lex(","), vec![Token::Comma]);
2537 assert_eq!(lex("."), vec![Token::Dot]);
2538 }
2539
2540 #[test]
2541 fn multi_char_operators() {
2542 assert_eq!(lex("&&"), vec![Token::And]);
2543 assert_eq!(lex("||"), vec![Token::Or]);
2544 assert_eq!(lex("=="), vec![Token::EqEq]);
2545 assert_eq!(lex("!="), vec![Token::NotEq]);
2546 assert_eq!(lex("=~"), vec![Token::Match]);
2547 assert_eq!(lex("!~"), vec![Token::NotMatch]);
2548 assert_eq!(lex(">="), vec![Token::GtEq]);
2549 assert_eq!(lex("<="), vec![Token::LtEq]);
2550 assert_eq!(lex(">>"), vec![Token::GtGt]);
2551 assert_eq!(lex("2>"), vec![Token::Stderr]);
2552 assert_eq!(lex("&>"), vec![Token::Both]);
2553 }
2554
2555 #[test]
2556 fn brackets() {
2557 assert_eq!(lex("{"), vec![Token::LBrace]);
2558 assert_eq!(lex("}"), vec![Token::RBrace]);
2559 assert_eq!(lex("["), vec![Token::LBracket]);
2560 assert_eq!(lex("]"), vec![Token::RBracket]);
2561 assert_eq!(lex("("), vec![Token::LParen]);
2562 assert_eq!(lex(")"), vec![Token::RParen]);
2563 }
2564
2565 #[test]
2570 fn integers() {
2571 assert_eq!(lex("0"), vec![Token::Int(0)]);
2572 assert_eq!(lex("42"), vec![Token::Int(42)]);
2573 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
2574 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
2575 }
2576
2577 #[test]
2578 fn floats() {
2579 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
2580 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
2581 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
2582 }
2583
2584 #[test]
2585 fn strings() {
2586 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
2587 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
2588 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
2590 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
2591 }
2592
2593 #[test]
2594 fn var_refs() {
2595 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
2596 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
2597 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
2598 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
2599 }
2600
2601 #[test]
2606 fn identifiers() {
2607 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
2608 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
2609 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
2610 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
2611 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
2612 }
2613
2614 #[test]
2615 fn keyword_prefix_identifiers() {
2616 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
2618 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
2619 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
2620 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
2621 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
2622 }
2623
2624 #[test]
2629 fn assignment() {
2630 assert_eq!(
2631 lex("set X = 5"),
2632 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
2633 );
2634 }
2635
2636 #[test]
2637 fn command_simple() {
2638 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
2639 assert_eq!(
2640 lex(r#"echo "hello""#),
2641 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
2642 );
2643 }
2644
2645 #[test]
2646 fn command_with_args() {
2647 assert_eq!(
2648 lex("cmd arg1 arg2"),
2649 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
2650 );
2651 }
2652
2653 #[test]
2654 fn command_with_named_args() {
2655 assert_eq!(
2656 lex("cmd key=value"),
2657 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
2658 );
2659 }
2660
2661 #[test]
2662 fn pipeline() {
2663 assert_eq!(
2664 lex("a | b | c"),
2665 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
2666 );
2667 }
2668
2669 #[test]
2670 fn if_statement() {
2671 assert_eq!(
2672 lex("if true; then echo; fi"),
2673 vec![
2674 Token::If,
2675 Token::True,
2676 Token::Semi,
2677 Token::Then,
2678 Token::Ident("echo".to_string()),
2679 Token::Semi,
2680 Token::Fi
2681 ]
2682 );
2683 }
2684
2685 #[test]
2686 fn for_loop() {
2687 assert_eq!(
2688 lex("for X in items; do echo; done"),
2689 vec![
2690 Token::For,
2691 Token::Ident("X".to_string()),
2692 Token::In,
2693 Token::Ident("items".to_string()),
2694 Token::Semi,
2695 Token::Do,
2696 Token::Ident("echo".to_string()),
2697 Token::Semi,
2698 Token::Done
2699 ]
2700 );
2701 }
2702
2703 #[test]
2708 fn whitespace_ignored() {
2709 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
2710 }
2711
2712 #[test]
2713 fn newlines_preserved() {
2714 let tokens = lex("a\nb");
2715 assert_eq!(
2716 tokens,
2717 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2718 );
2719 }
2720
2721 #[test]
2722 fn multiple_newlines() {
2723 let tokens = lex("a\n\n\nb");
2724 assert_eq!(
2725 tokens,
2726 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
2727 );
2728 }
2729
2730 #[test]
2735 fn comments_skipped() {
2736 assert_eq!(lex("# comment"), vec![]);
2737 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
2738 assert_eq!(
2739 lex("a # comment\nb"),
2740 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
2741 );
2742 }
2743
2744 #[test]
2745 fn comments_preserved_when_requested() {
2746 let tokens = tokenize_with_comments("a # comment")
2747 .expect("should succeed")
2748 .into_iter()
2749 .map(|s| s.token)
2750 .collect::<Vec<_>>();
2751 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
2752 }
2753
2754 #[test]
2759 fn parse_simple_string() {
2760 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
2761 }
2762
2763 #[test]
2764 fn parse_string_with_escapes() {
2765 assert_eq!(
2766 parse_string_literal(r#""hello\nworld""#).expect("ok"),
2767 "hello\nworld"
2768 );
2769 assert_eq!(
2770 parse_string_literal(r#""tab\there""#).expect("ok"),
2771 "tab\there"
2772 );
2773 assert_eq!(
2774 parse_string_literal(r#""quote\"here""#).expect("ok"),
2775 "quote\"here"
2776 );
2777 }
2778
2779 #[test]
2780 fn parse_string_with_unicode() {
2781 assert_eq!(
2782 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
2783 "emoji ❤"
2784 );
2785 }
2786
2787 #[test]
2788 fn parse_string_with_escaped_dollar() {
2789 assert_eq!(
2792 parse_string_literal(r#""\$VAR""#).expect("ok"),
2793 "__KAISH_ESCAPED_DOLLAR__VAR"
2794 );
2795 assert_eq!(
2796 parse_string_literal(r#""cost: \$100""#).expect("ok"),
2797 "cost: __KAISH_ESCAPED_DOLLAR__100"
2798 );
2799 }
2800
2801 #[test]
2806 fn parse_simple_var() {
2807 assert_eq!(
2808 parse_var_ref("${X}").expect("ok"),
2809 vec!["X"]
2810 );
2811 }
2812
2813 #[test]
2814 fn parse_var_with_field() {
2815 assert_eq!(
2816 parse_var_ref("${VAR.field}").expect("ok"),
2817 vec!["VAR", "field"]
2818 );
2819 }
2820
2821 #[test]
2822 fn parse_var_with_index() {
2823 assert_eq!(
2824 parse_var_ref("${VAR[0]}").expect("ok"),
2825 vec!["VAR", "[0]"]
2826 );
2827 }
2828
2829 #[test]
2830 fn parse_var_nested() {
2831 assert_eq!(
2832 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
2833 vec!["VAR", "field", "[0]", "nested"]
2834 );
2835 }
2836
2837 #[test]
2838 fn parse_last_result() {
2839 assert_eq!(
2840 parse_var_ref("${?}").expect("ok"),
2841 vec!["?"]
2842 );
2843 }
2844
2845 #[test]
2850 fn parse_integers() {
2851 assert_eq!(parse_int("0").expect("ok"), 0);
2852 assert_eq!(parse_int("42").expect("ok"), 42);
2853 assert_eq!(parse_int("-1").expect("ok"), -1);
2854 }
2855
2856 #[test]
2857 fn parse_floats() {
2858 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
2859 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
2860 }
2861
2862 #[test]
2867 fn empty_input() {
2868 assert_eq!(lex(""), vec![]);
2869 }
2870
2871 #[test]
2872 fn only_whitespace() {
2873 assert_eq!(lex(" \t\t "), vec![]);
2874 }
2875
2876 #[test]
2877 fn json_array() {
2878 assert_eq!(
2879 lex(r#"[1, 2, 3]"#),
2880 vec![
2881 Token::LBracket,
2882 Token::Int(1),
2883 Token::Comma,
2884 Token::Int(2),
2885 Token::Comma,
2886 Token::Int(3),
2887 Token::RBracket
2888 ]
2889 );
2890 }
2891
2892 #[test]
2893 fn json_object() {
2894 assert_eq!(
2895 lex(r#"{"key": "value"}"#),
2896 vec![
2897 Token::LBrace,
2898 Token::String("key".to_string()),
2899 Token::Colon,
2900 Token::String("value".to_string()),
2901 Token::RBrace
2902 ]
2903 );
2904 }
2905
2906 #[test]
2907 fn redirect_operators() {
2908 assert_eq!(
2909 lex("cmd > file"),
2910 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
2911 );
2912 assert_eq!(
2913 lex("cmd >> file"),
2914 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
2915 );
2916 assert_eq!(
2917 lex("cmd 2> err"),
2918 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
2919 );
2920 assert_eq!(
2921 lex("cmd &> all"),
2922 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
2923 );
2924 }
2925
2926 #[test]
2927 fn background_job() {
2928 assert_eq!(
2929 lex("cmd &"),
2930 vec![Token::Ident("cmd".to_string()), Token::Amp]
2931 );
2932 }
2933
2934 #[test]
2935 fn command_substitution() {
2936 assert_eq!(
2937 lex("$(cmd)"),
2938 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
2939 );
2940 assert_eq!(
2941 lex("$(cmd arg)"),
2942 vec![
2943 Token::CmdSubstStart,
2944 Token::Ident("cmd".to_string()),
2945 Token::Ident("arg".to_string()),
2946 Token::RParen
2947 ]
2948 );
2949 assert_eq!(
2950 lex("$(a | b)"),
2951 vec![
2952 Token::CmdSubstStart,
2953 Token::Ident("a".to_string()),
2954 Token::Pipe,
2955 Token::Ident("b".to_string()),
2956 Token::RParen
2957 ]
2958 );
2959 }
2960
2961 #[test]
2962 fn complex_pipeline() {
2963 assert_eq!(
2964 lex(r#"cat file | grep pattern="foo" | head count=10"#),
2965 vec![
2966 Token::Ident("cat".to_string()),
2967 Token::Ident("file".to_string()),
2968 Token::Pipe,
2969 Token::Ident("grep".to_string()),
2970 Token::Ident("pattern".to_string()),
2971 Token::Eq,
2972 Token::String("foo".to_string()),
2973 Token::Pipe,
2974 Token::Ident("head".to_string()),
2975 Token::Ident("count".to_string()),
2976 Token::Eq,
2977 Token::Int(10),
2978 ]
2979 );
2980 }
2981
2982 #[test]
2987 fn short_flag() {
2988 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
2989 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
2990 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
2991 }
2992
2993 #[test]
2994 fn short_flag_combined() {
2995 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
2997 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
2998 }
2999
3000 #[test]
3001 fn job_spec_lexes_as_one_token() {
3002 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
3004 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
3005 assert_eq!(
3006 lex("wait %1 %2"),
3007 vec![
3008 Token::Ident("wait".to_string()),
3009 Token::JobSpec("%1".to_string()),
3010 Token::JobSpec("%2".to_string()),
3011 ]
3012 );
3013 }
3014
3015 #[test]
3016 fn short_flag_with_internal_hyphens_is_one_token() {
3017 assert_eq!(
3021 lex("-not-a-flag"),
3022 vec![Token::ShortFlag("not-a-flag".to_string())]
3023 );
3024 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3028 assert_eq!(lex("-"), vec![Token::MinusAlone]);
3029 }
3030
3031 #[test]
3032 fn long_flag() {
3033 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
3034 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
3035 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
3036 }
3037
3038 #[test]
3039 fn double_dash() {
3040 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3042 }
3043
3044 #[test]
3045 fn flags_vs_negative_numbers() {
3046 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3048 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3050 assert_eq!(
3053 lex("-1 a"),
3054 vec![Token::Int(-1), Token::Ident("a".to_string())]
3055 );
3056 }
3057
3058 #[test]
3059 fn command_with_flags() {
3060 assert_eq!(
3061 lex("ls -l"),
3062 vec![
3063 Token::Ident("ls".to_string()),
3064 Token::ShortFlag("l".to_string()),
3065 ]
3066 );
3067 assert_eq!(
3068 lex("git commit -m"),
3069 vec![
3070 Token::Ident("git".to_string()),
3071 Token::Ident("commit".to_string()),
3072 Token::ShortFlag("m".to_string()),
3073 ]
3074 );
3075 assert_eq!(
3076 lex("git push --force"),
3077 vec![
3078 Token::Ident("git".to_string()),
3079 Token::Ident("push".to_string()),
3080 Token::LongFlag("force".to_string()),
3081 ]
3082 );
3083 }
3084
3085 #[test]
3086 fn flag_with_value() {
3087 assert_eq!(
3088 lex(r#"git commit -m "message""#),
3089 vec![
3090 Token::Ident("git".to_string()),
3091 Token::Ident("commit".to_string()),
3092 Token::ShortFlag("m".to_string()),
3093 Token::String("message".to_string()),
3094 ]
3095 );
3096 assert_eq!(
3097 lex(r#"--message="hello""#),
3098 vec![
3099 Token::LongFlag("message".to_string()),
3100 Token::Eq,
3101 Token::String("hello".to_string()),
3102 ]
3103 );
3104 }
3105
3106 #[test]
3107 fn end_of_flags_marker() {
3108 assert_eq!(
3109 lex("git checkout -- file"),
3110 vec![
3111 Token::Ident("git".to_string()),
3112 Token::Ident("checkout".to_string()),
3113 Token::DoubleDash,
3114 Token::Ident("file".to_string()),
3115 ]
3116 );
3117 }
3118
3119 #[test]
3124 fn local_keyword() {
3125 assert_eq!(lex("local"), vec![Token::Local]);
3126 assert_eq!(
3127 lex("local X = 5"),
3128 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3129 );
3130 }
3131
3132 #[test]
3133 fn simple_var_ref() {
3134 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
3135 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
3136 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
3137 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
3138 }
3139
3140 #[test]
3141 fn simple_var_ref_in_command() {
3142 assert_eq!(
3143 lex("echo $NAME"),
3144 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
3145 );
3146 }
3147
3148 #[test]
3149 fn single_quoted_strings() {
3150 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
3151 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
3152 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
3153 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
3155 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
3156 }
3157
3158 #[test]
3159 fn test_brackets() {
3160 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
3162 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
3163 assert_eq!(
3164 lex("[[ -f file ]]"),
3165 vec![
3166 Token::LBracket,
3167 Token::LBracket,
3168 Token::ShortFlag("f".to_string()),
3169 Token::Ident("file".to_string()),
3170 Token::RBracket,
3171 Token::RBracket
3172 ]
3173 );
3174 }
3175
3176 #[test]
3177 fn test_expression_syntax() {
3178 assert_eq!(
3179 lex(r#"[[ $X == "value" ]]"#),
3180 vec![
3181 Token::LBracket,
3182 Token::LBracket,
3183 Token::SimpleVarRef("X".to_string()),
3184 Token::EqEq,
3185 Token::String("value".to_string()),
3186 Token::RBracket,
3187 Token::RBracket
3188 ]
3189 );
3190 }
3191
3192 #[test]
3193 fn bash_style_assignment() {
3194 assert_eq!(
3196 lex(r#"NAME="value""#),
3197 vec![
3198 Token::Ident("NAME".to_string()),
3199 Token::Eq,
3200 Token::String("value".to_string())
3201 ]
3202 );
3203 }
3204
3205 #[test]
3206 fn positional_params() {
3207 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
3208 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
3209 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
3210 assert_eq!(lex("$@"), vec![Token::AllArgs]);
3211 assert_eq!(lex("$#"), vec![Token::ArgCount]);
3212 }
3213
3214 #[test]
3215 fn positional_in_context() {
3216 assert_eq!(
3217 lex("echo $1 $2"),
3218 vec![
3219 Token::Ident("echo".to_string()),
3220 Token::Positional(1),
3221 Token::Positional(2),
3222 ]
3223 );
3224 }
3225
3226 #[test]
3227 fn var_length() {
3228 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3229 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
3230 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
3231 }
3232
3233 #[test]
3234 fn var_length_with_subscript() {
3235 assert_eq!(lex("${#u[tags]}"), vec![Token::VarLength("u[tags]".to_string())]);
3238 assert_eq!(lex("${#a[0]}"), vec![Token::VarLength("a[0]".to_string())]);
3239 assert_eq!(lex("${#a[b][c]}"), vec![Token::VarLength("a[b][c]".to_string())]);
3240 assert_eq!(lex("${#r[$k]}"), vec![Token::VarLength("r[$k]".to_string())]);
3241 }
3242
3243 #[test]
3244 fn var_length_in_context() {
3245 assert_eq!(
3246 lex("echo ${#NAME}"),
3247 vec![
3248 Token::Ident("echo".to_string()),
3249 Token::VarLength("NAME".to_string()),
3250 ]
3251 );
3252 }
3253
3254 #[test]
3259 fn plus_flag() {
3260 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
3262 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
3263 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
3264 }
3265
3266 #[test]
3267 fn set_with_plus_flag() {
3268 assert_eq!(
3269 lex("set +e"),
3270 vec![
3271 Token::Set,
3272 Token::PlusFlag("e".to_string()),
3273 ]
3274 );
3275 }
3276
3277 #[test]
3278 fn set_with_multiple_flags() {
3279 assert_eq!(
3280 lex("set -e -u"),
3281 vec![
3282 Token::Set,
3283 Token::ShortFlag("e".to_string()),
3284 Token::ShortFlag("u".to_string()),
3285 ]
3286 );
3287 }
3288
3289 #[test]
3290 fn flags_vs_negative_numbers_edge_cases() {
3291 assert_eq!(
3293 lex("-1 a"),
3294 vec![Token::Int(-1), Token::Ident("a".to_string())]
3295 );
3296 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3298 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3300 }
3301
3302 #[test]
3303 fn single_dash_is_minus_alone() {
3304 let result = tokenize("-").expect("should lex");
3306 assert_eq!(result.len(), 1);
3307 assert!(matches!(result[0].token, Token::MinusAlone));
3308 }
3309
3310 #[test]
3311 fn plus_bare_for_date_format() {
3312 let result = tokenize("+%s").expect("should lex");
3314 assert_eq!(result.len(), 1);
3315 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
3316
3317 let result = tokenize("+%Y-%m-%d").expect("should lex");
3319 assert_eq!(result.len(), 1);
3320 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
3321 }
3322
3323 #[test]
3324 fn plus_flag_still_works() {
3325 let result = tokenize("+e").expect("should lex");
3327 assert_eq!(result.len(), 1);
3328 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
3329 }
3330
3331 #[test]
3332 fn while_keyword_vs_while_loop() {
3333 assert_eq!(lex("while"), vec![Token::While]);
3335 assert_eq!(
3337 lex("while true"),
3338 vec![Token::While, Token::True]
3339 );
3340 }
3341
3342 #[test]
3343 fn control_flow_keywords() {
3344 assert_eq!(lex("break"), vec![Token::Break]);
3345 assert_eq!(lex("continue"), vec![Token::Continue]);
3346 assert_eq!(lex("return"), vec![Token::Return]);
3347 assert_eq!(lex("exit"), vec![Token::Exit]);
3348 }
3349
3350 #[test]
3351 fn control_flow_with_numbers() {
3352 assert_eq!(
3353 lex("break 2"),
3354 vec![Token::Break, Token::Int(2)]
3355 );
3356 assert_eq!(
3357 lex("continue 3"),
3358 vec![Token::Continue, Token::Int(3)]
3359 );
3360 assert_eq!(
3361 lex("exit 1"),
3362 vec![Token::Exit, Token::Int(1)]
3363 );
3364 }
3365
3366 #[test]
3371 fn heredoc_simple() {
3372 let source = "cat <<EOF\nhello\nworld\nEOF";
3373 let tokens = lex(source);
3374 assert_eq!(tokens, vec![
3376 Token::Ident("cat".to_string()),
3377 Token::HereDocStart,
3378 Token::HereDoc(HereDocData {
3379 content: "hello\nworld\n".to_string(),
3380 literal: false,
3381 strip_tabs: false,
3382 body_start_offset: 10,
3383 }),
3384 Token::Newline,
3385 ]);
3386 }
3387
3388 #[test]
3389 fn heredoc_empty() {
3390 let source = "cat <<EOF\nEOF";
3391 let tokens = lex(source);
3392 assert_eq!(tokens, vec![
3393 Token::Ident("cat".to_string()),
3394 Token::HereDocStart,
3395 Token::HereDoc(HereDocData {
3396 content: "".to_string(),
3397 literal: false,
3398 strip_tabs: false,
3399 body_start_offset: 10,
3400 }),
3401 Token::Newline,
3402 ]);
3403 }
3404
3405 #[test]
3406 fn heredoc_with_special_chars() {
3407 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
3408 let tokens = lex(source);
3409 assert_eq!(tokens, vec![
3410 Token::Ident("cat".to_string()),
3411 Token::HereDocStart,
3412 Token::HereDoc(HereDocData {
3413 content: "$VAR and \"quoted\" 'single'\n".to_string(),
3414 literal: false,
3415 strip_tabs: false,
3416 body_start_offset: 10,
3417 }),
3418 Token::Newline,
3419 ]);
3420 }
3421
3422 #[test]
3423 fn heredoc_multiline() {
3424 let source = "cat <<END\nline1\nline2\nline3\nEND";
3425 let tokens = lex(source);
3426 assert_eq!(tokens, vec![
3427 Token::Ident("cat".to_string()),
3428 Token::HereDocStart,
3429 Token::HereDoc(HereDocData {
3430 content: "line1\nline2\nline3\n".to_string(),
3431 literal: false,
3432 strip_tabs: false,
3433 body_start_offset: 10,
3434 }),
3435 Token::Newline,
3436 ]);
3437 }
3438
3439 #[test]
3440 fn heredoc_in_command() {
3441 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
3442 let tokens = lex(source);
3443 assert_eq!(tokens, vec![
3444 Token::Ident("cat".to_string()),
3445 Token::HereDocStart,
3446 Token::HereDoc(HereDocData {
3447 content: "hello\n".to_string(),
3448 literal: false,
3449 strip_tabs: false,
3450 body_start_offset: 10,
3451 }),
3452 Token::Newline,
3453 Token::Ident("echo".to_string()),
3454 Token::Ident("goodbye".to_string()),
3455 ]);
3456 }
3457
3458 #[test]
3459 fn heredoc_strip_tabs() {
3460 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
3461 let tokens = lex(source);
3462 assert_eq!(tokens, vec![
3466 Token::Ident("cat".to_string()),
3467 Token::HereDocStart,
3468 Token::HereDoc(HereDocData {
3469 content: "\thello\n\tworld\n".to_string(),
3470 literal: false,
3471 strip_tabs: true,
3472 body_start_offset: 11,
3473 }),
3474 Token::Newline,
3475 ]);
3476 }
3477
3478 #[test]
3483 fn arithmetic_simple() {
3484 let source = "$((1 + 2))";
3485 let tokens = lex(source);
3486 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
3487 }
3488
3489 #[test]
3490 fn arithmetic_in_assignment() {
3491 let source = "X=$((5 * 3))";
3492 let tokens = lex(source);
3493 assert_eq!(tokens, vec![
3494 Token::Ident("X".to_string()),
3495 Token::Eq,
3496 Token::Arithmetic("5 * 3".to_string()),
3497 ]);
3498 }
3499
3500 #[test]
3501 fn arithmetic_with_nested_parens() {
3502 let source = "$((2 * (3 + 4)))";
3503 let tokens = lex(source);
3504 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
3505 }
3506
3507 #[test]
3508 fn arithmetic_with_variable() {
3509 let source = "$((X + 1))";
3510 let tokens = lex(source);
3511 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
3512 }
3513
3514 #[test]
3515 fn arithmetic_command_subst_not_confused() {
3516 let source = "$(echo hello)";
3518 let tokens = lex(source);
3519 assert_eq!(tokens, vec![
3520 Token::CmdSubstStart,
3521 Token::Ident("echo".to_string()),
3522 Token::Ident("hello".to_string()),
3523 Token::RParen,
3524 ]);
3525 }
3526
3527 #[test]
3528 fn arithmetic_nesting_limit() {
3529 let open_parens = "(".repeat(300);
3531 let close_parens = ")".repeat(300);
3532 let source = format!("$(({}1{}))", open_parens, close_parens);
3533 let result = tokenize(&source);
3534 assert!(result.is_err());
3535 let errors = result.unwrap_err();
3536 assert_eq!(errors.len(), 1);
3537 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
3538 }
3539
3540 #[test]
3541 fn arithmetic_nesting_within_limit() {
3542 let source = "$((((1 + 2) * 3)))";
3544 let tokens = lex(source);
3545 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
3546 }
3547
3548 #[test]
3560 fn arithmetic_after_apostrophe_in_comment() {
3561 let source = "# this doesn't work\necho $((1+2))";
3564 let tokens = lex(source);
3565 assert_eq!(tokens, vec![
3566 Token::Newline,
3567 Token::Ident("echo".to_string()),
3568 Token::Arithmetic("1+2".to_string()),
3569 ]);
3570 }
3571
3572 #[test]
3573 fn arithmetic_inside_comment_is_not_expanded() {
3574 let source = "# the $((y)) syntax explained\necho hello";
3576 let tokens = lex(source);
3577 assert_eq!(tokens, vec![
3578 Token::Newline,
3579 Token::Ident("echo".to_string()),
3580 Token::Ident("hello".to_string()),
3581 ]);
3582 }
3583
3584 #[test]
3585 fn backticked_arithmetic_in_comment_is_not_expanded() {
3586 let source = "# the `$((x))` syntax explained\necho $((3+4))";
3590 let tokens = lex(source);
3591 assert_eq!(tokens, vec![
3592 Token::Newline,
3593 Token::Ident("echo".to_string()),
3594 Token::Arithmetic("3+4".to_string()),
3595 ]);
3596 }
3597
3598 #[test]
3599 fn arithmetic_still_works_outside_comments() {
3600 let source = "X=$((1+2)); Y=$((3*4))";
3603 let tokens = lex(source);
3604 assert_eq!(tokens, vec![
3605 Token::Ident("X".to_string()),
3606 Token::Eq,
3607 Token::Arithmetic("1+2".to_string()),
3608 Token::Semi,
3609 Token::Ident("Y".to_string()),
3610 Token::Eq,
3611 Token::Arithmetic("3*4".to_string()),
3612 ]);
3613 }
3614
3615 #[test]
3616 fn arithmetic_inside_double_quotes_still_expands() {
3617 let source = "echo \"# $((1+2))\"";
3620 let tokens = lex(source);
3621 assert_eq!(tokens.len(), 2);
3626 assert!(matches!(tokens[0], Token::Ident(_)));
3627 assert!(matches!(tokens[1], Token::String(_)));
3628 }
3629
3630 #[test]
3643 fn backtick_in_source_is_rejected() {
3644 let result = tokenize("echo `date`");
3645 assert!(result.is_err());
3646 let errors = result.unwrap_err();
3647 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
3648 }
3649
3650 #[test]
3651 fn backtick_in_comment_is_just_comment_text() {
3652 let source = "# use `date` here\necho hi";
3655 let tokens = lex(source);
3656 assert_eq!(tokens, vec![
3657 Token::Newline,
3658 Token::Ident("echo".to_string()),
3659 Token::Ident("hi".to_string()),
3660 ]);
3661 }
3662
3663 #[test]
3664 fn backtick_in_single_quoted_string_is_literal() {
3665 let source = "echo '`date`'";
3668 let tokens = lex(source);
3669 assert_eq!(tokens, vec![
3670 Token::Ident("echo".to_string()),
3671 Token::SingleString("`date`".to_string()),
3672 ]);
3673 }
3674
3675 #[test]
3676 fn backtick_in_double_quoted_string_is_literal() {
3677 let source = "echo \"`date`\"";
3682 let tokens = lex(source);
3683 assert_eq!(tokens.len(), 2);
3684 assert!(matches!(tokens[0], Token::Ident(_)));
3685 match &tokens[1] {
3686 Token::String(s) => assert!(s.contains('`')),
3687 other => panic!("expected Token::String, got {:?}", other),
3688 }
3689 }
3690
3691 #[test]
3692 fn backtick_in_heredoc_body_is_preserved() {
3693 let source = "cat <<EOF\n`date`\nEOF\n";
3696 let tokens = lex(source);
3697 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
3698 assert!(heredoc.is_some(), "expected a HereDoc token");
3699 if let Some(Token::HereDoc(d)) = heredoc {
3700 assert!(d.content.contains('`'));
3701 }
3702 }
3703
3704 #[test]
3709 fn token_categories() {
3710 assert_eq!(Token::If.category(), TokenCategory::Keyword);
3712 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
3713 assert_eq!(Token::For.category(), TokenCategory::Keyword);
3714 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
3715 assert_eq!(Token::True.category(), TokenCategory::Keyword);
3716 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
3717
3718 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
3720 assert_eq!(Token::And.category(), TokenCategory::Operator);
3721 assert_eq!(Token::Or.category(), TokenCategory::Operator);
3722 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
3723 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
3724
3725 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
3727 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
3728 assert_eq!(
3729 Token::HereDoc(HereDocData {
3730 content: "test".to_string(),
3731 literal: false,
3732 strip_tabs: false,
3733 body_start_offset: 0,
3734 }).category(),
3735 TokenCategory::String,
3736 );
3737
3738 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
3740 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
3741 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
3742
3743 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
3745 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
3746 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
3747 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
3748 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
3749 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
3750 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
3751
3752 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
3754 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
3755 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
3756 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
3757
3758 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
3760 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
3761 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
3762 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
3763
3764 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
3766
3767 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
3769
3770 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
3772 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
3773 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
3774
3775 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
3777 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
3778 }
3779
3780 #[test]
3781 fn test_heredoc_piped_to_command() {
3782 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
3785 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
3786 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
3787 assert!(heredoc_pos.is_some(), "should have a heredoc token");
3788 assert!(pipe_pos.is_some(), "should have a pipe token");
3789 assert!(
3790 pipe_pos.unwrap() > heredoc_pos.unwrap(),
3791 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
3792 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
3793 );
3794 }
3795
3796 #[test]
3797 fn test_heredoc_standalone_still_works() {
3798 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
3800 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
3801 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
3802 }
3803
3804 #[test]
3805 fn test_heredoc_preserves_leading_empty_lines() {
3806 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
3808 let heredoc = tokens.iter().find_map(|t| {
3809 if let Token::HereDoc(data) = &t.token {
3810 Some(data.clone())
3811 } else {
3812 None
3813 }
3814 });
3815 assert!(heredoc.is_some(), "should have a heredoc token");
3816 let data = heredoc.unwrap();
3817 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
3818 assert_eq!(data.content, "\nhello\n");
3819 }
3820
3821 #[test]
3822 fn test_heredoc_quoted_delimiter_sets_literal() {
3823 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
3825 let heredoc = tokens.iter().find_map(|t| {
3826 if let Token::HereDoc(data) = &t.token {
3827 Some(data.clone())
3828 } else {
3829 None
3830 }
3831 });
3832 assert!(heredoc.is_some(), "should have a heredoc token");
3833 let data = heredoc.unwrap();
3834 assert!(data.literal, "quoted delimiter should set literal=true");
3835 assert_eq!(data.content, "hello $HOME\n");
3836 }
3837
3838 #[test]
3839 fn test_heredoc_unquoted_delimiter_not_literal() {
3840 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
3842 let heredoc = tokens.iter().find_map(|t| {
3843 if let Token::HereDoc(data) = &t.token {
3844 Some(data.clone())
3845 } else {
3846 None
3847 }
3848 });
3849 assert!(heredoc.is_some(), "should have a heredoc token");
3850 let data = heredoc.unwrap();
3851 assert!(!data.literal, "unquoted delimiter should have literal=false");
3852 }
3853
3854 #[test]
3859 fn colon_double_in_word() {
3860 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
3861 }
3862
3863 #[test]
3864 fn colon_single_in_word() {
3865 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
3866 }
3867
3868 #[test]
3869 fn colon_with_port() {
3870 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
3871 }
3872
3873 #[test]
3874 fn colon_standalone() {
3875 assert_eq!(lex(":"), vec![Token::Colon]);
3876 }
3877
3878 #[test]
3879 fn colon_spaced_no_merge() {
3880 assert_eq!(
3881 lex("foo : bar"),
3882 vec![
3883 Token::Ident("foo".into()),
3884 Token::Colon,
3885 Token::Ident("bar".into()),
3886 ]
3887 );
3888 }
3889
3890 #[test]
3891 fn colon_in_command_arg() {
3892 assert_eq!(
3893 lex("echo foo::bar"),
3894 vec![
3895 Token::Ident("echo".into()),
3896 Token::Ident("foo::bar".into()),
3897 ]
3898 );
3899 }
3900
3901 #[test]
3902 fn colon_trailing() {
3903 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
3905 }
3906
3907 #[test]
3908 fn colon_leading() {
3909 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
3911 }
3912
3913 #[test]
3914 fn colon_with_path() {
3915 assert_eq!(
3917 lex("/usr/bin:8080"),
3918 vec![Token::Ident("/usr/bin:8080".into())]
3919 );
3920 }
3921
3922 #[test]
3927 fn is_keyword_covers_control_flow() {
3928 for t in [
3929 Token::While,
3930 Token::Return,
3931 Token::Break,
3932 Token::Continue,
3933 Token::Exit,
3934 ] {
3935 assert!(t.is_keyword(), "{t:?} should be a keyword");
3936 }
3937 }
3938
3939 #[test]
3940 fn starts_statement_covers_while() {
3941 assert!(Token::While.starts_statement());
3942 }
3943
3944 #[test]
3945 fn is_keyword_rejects_operators() {
3946 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
3947 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
3948 }
3949 }
3950}