1use logos::{Logos, Span};
37use std::fmt;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::time::UNIX_EPOCH;
40use kaish_types::clock::system_now;
41
42static MARKER_COUNTER: AtomicU64 = AtomicU64::new(0);
44
45const MAX_PAREN_DEPTH: usize = 256;
48
49
50fn unique_marker_id() -> String {
53 let timestamp = system_now()
54 .duration_since(UNIX_EPOCH)
55 .map(|d| d.as_nanos())
56 .unwrap_or(0);
57 let counter = MARKER_COUNTER.fetch_add(1, Ordering::Relaxed);
58 #[cfg(target_family = "wasm")]
61 let pid = 0u32;
62 #[cfg(not(target_family = "wasm"))]
63 let pid = std::process::id();
64 format!("{:x}_{:x}_{:x}", timestamp, counter, pid)
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct Spanned<T> {
70 pub token: T,
71 pub span: Span,
72}
73
74impl<T> Spanned<T> {
75 pub fn new(token: T, span: Span) -> Self {
76 Self { token, span }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Default)]
82pub enum LexerError {
83 #[default]
84 UnexpectedCharacter,
85 UnterminatedString,
86 UnterminatedVarRef,
87 InvalidEscape,
88 InvalidNumber,
89 AmbiguousBoolean(String),
90 AmbiguousBooleanLike(String),
91 InvalidFloatNoLeading,
92 InvalidFloatNoTrailing,
93 NestingTooDeep,
95 UnterminatedArithmetic,
99 UnterminatedHeredoc { delimiter: String },
103 BackticksNotSupported,
108 ArithmeticInVarRef,
114}
115
116impl fmt::Display for LexerError {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 LexerError::UnexpectedCharacter => write!(f, "unexpected character"),
120 LexerError::UnterminatedString => write!(f, "unterminated string"),
121 LexerError::UnterminatedVarRef => write!(f, "unterminated variable reference"),
122 LexerError::InvalidEscape => write!(f, "invalid escape sequence"),
123 LexerError::InvalidNumber => write!(f, "invalid number"),
124 LexerError::AmbiguousBoolean(s) => {
125 write!(f, "ambiguous boolean, use lowercase '{}'", s.to_lowercase())
126 }
127 LexerError::AmbiguousBooleanLike(s) => {
128 let suggest = if s.eq_ignore_ascii_case("yes") { "true" } else { "false" };
129 write!(f, "ambiguous boolean-like '{}', use '{}' or '\"{}\"'", s, suggest, s)
130 }
131 LexerError::InvalidFloatNoLeading => write!(f, "float must have leading digit"),
132 LexerError::InvalidFloatNoTrailing => write!(f, "float must have trailing digit"),
133 LexerError::NestingTooDeep => write!(f, "nesting depth exceeded (max {})", MAX_PAREN_DEPTH),
134 LexerError::UnterminatedArithmetic => {
135 write!(f, "unterminated arithmetic expansion, expected closing `))`")
136 }
137 LexerError::UnterminatedHeredoc { delimiter } => {
138 write!(f, "unterminated heredoc, expected closing delimiter `{}` on its own line", delimiter)
139 }
140 LexerError::BackticksNotSupported => {
141 write!(f, "backticks are not supported in kaish; use $(cmd) instead")
142 }
143 LexerError::ArithmeticInVarRef => {
144 write!(
145 f,
146 "arithmetic expansion inside ${{...}} is not supported; \
147 assign it to a variable first, e.g. N=$((expr)); ${{X:-$N}}"
148 )
149 }
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
178pub struct HereDocData {
179 pub content: String,
180 pub literal: bool,
181 pub strip_tabs: bool,
182 pub body_start_offset: usize,
183}
184
185#[derive(Logos, Debug, Clone, PartialEq)]
186#[logos(error = LexerError)]
187#[logos(skip r"[ \t]+")]
188pub enum Token {
189 #[token("set")]
193 Set,
194
195 #[token("local")]
196 Local,
197
198 #[token("if")]
199 If,
200
201 #[token("then")]
202 Then,
203
204 #[token("else")]
205 Else,
206
207 #[token("elif")]
208 Elif,
209
210 #[token("fi")]
211 Fi,
212
213 #[token("for")]
214 For,
215
216 #[token("while")]
217 While,
218
219 #[token("in")]
220 In,
221
222 #[token("do")]
223 Do,
224
225 #[token("done")]
226 Done,
227
228 #[token("case")]
229 Case,
230
231 #[token("esac")]
232 Esac,
233
234 #[token("function")]
235 Function,
236
237 #[token("break")]
238 Break,
239
240 #[token("continue")]
241 Continue,
242
243 #[token("return")]
244 Return,
245
246 #[token("exit")]
247 Exit,
248
249 #[token("true")]
250 True,
251
252 #[token("false")]
253 False,
254
255 #[token("string")]
259 TypeString,
260
261 #[token("int")]
262 TypeInt,
263
264 #[token("float")]
265 TypeFloat,
266
267 #[token("bool")]
268 TypeBool,
269
270 #[token("&&")]
274 And,
275
276 #[token("||")]
277 Or,
278
279 #[token("==")]
280 EqEq,
281
282 #[token("!=")]
283 NotEq,
284
285 #[token("=~")]
286 Match,
287
288 #[token("!~")]
289 NotMatch,
290
291 #[token(">=")]
292 GtEq,
293
294 #[token("<=")]
295 LtEq,
296
297 #[token(">>")]
298 GtGt,
299
300 #[token("2>&1")]
301 StderrToStdout,
302
303 #[token("1>&2")]
304 StdoutToStderr,
305
306 #[token(">&2")]
307 StdoutToStderr2,
308
309 #[token("2>")]
310 Stderr,
311
312 #[token("&>")]
313 Both,
314
315 #[token("<<<")]
316 HereString,
317
318 #[token("<<")]
319 HereDocStart,
320
321 #[token(";;")]
322 DoubleSemi,
323
324 #[token("=")]
328 Eq,
329
330 #[token("|")]
331 Pipe,
332
333 #[token("&")]
334 Amp,
335
336 #[token(">")]
337 Gt,
338
339 #[token("<")]
340 Lt,
341
342 #[token(";")]
343 Semi,
344
345 #[token(":")]
346 Colon,
347
348 #[token(",")]
349 Comma,
350
351 #[token("...")]
356 DotDotDot,
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)]
453 ShortFlag(String),
454
455 #[regex(r"\+[a-zA-Z][a-zA-Z0-9]*", lex_plus_flag, priority = 3)]
457 PlusFlag(String),
458
459 #[token("--")]
464 DoubleDash,
465
466 #[regex(r"--[^a-zA-Z\s()|&;<>][^\s()|&;<>]*", lex_double_dash_bare, priority = 2)]
485 DoubleDashBare(String),
486
487 #[regex(r"\+[^a-zA-Z\s()|&;<>][^\s()|&;<>]*", lex_plus_bare, priority = 2)]
491 PlusBare(String),
492
493 #[regex(r"-[^a-zA-Z0-9\s\-()|&;<>][^\s()|&;<>]*", lex_minus_bare, priority = 1)]
498 MinusBare(String),
499
500 #[regex(r"%[0-9]+", lex_job_spec)]
504 JobSpec(String),
505
506 #[token("-")]
510 MinusAlone,
511
512 #[regex(r#""([^"\\]|\\.)*""#, lex_string)]
518 String(String),
519
520 #[regex(r"'[^']*'", lex_single_string)]
522 SingleString(String),
523
524 #[regex(r"\$\{", lex_varref)]
532 VarRef(String),
533
534 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", lex_simple_varref)]
536 SimpleVarRef(String),
537
538 #[regex(r"\$[0-9]", lex_positional)]
540 Positional(usize),
541
542 #[token("$@")]
544 AllArgs,
545
546 #[token("$#")]
548 ArgCount,
549
550 #[token("$?")]
552 LastExitCode,
553
554 #[token("$$")]
556 CurrentPid,
557
558 #[regex(r"\$\{#[a-zA-Z_][a-zA-Z0-9_]*(\[[^\]]*\])*\}", lex_var_length)]
563 VarLength(String),
564
565 HereDoc(HereDocData),
568
569 #[regex(r"-?[0-9]+", lex_int, priority = 2)]
571 Int(i64),
572
573 #[regex(r"-?[0-9]+\.[0-9]+", lex_float)]
575 Float(f64),
576
577 #[regex(r"[0-9]+[a-zA-Z_][a-zA-Z0-9_.-]*", lex_number_ident, priority = 3)]
586 NumberIdent(String),
587
588 #[regex(r"[0-9]+(\.[0-9]+)?(-[0-9a-zA-Z._]+)+", lex_slice_word, priority = 3)]
599 #[regex(r"-[0-9]+[a-zA-Z_][0-9a-zA-Z._-]*", lex_slice_word, priority = 3)]
600 DashNumWord(String),
601
602 #[regex(r"@[a-zA-Z0-9_./@-]*", lex_slice_word, priority = 3)]
607 AtWord(String),
608
609 #[regex(r"\.[0-9]+", lex_invalid_float_no_leading, priority = 3)]
611 InvalidFloatNoLeading,
612
613 #[regex(r"[0-9]+\.", lex_invalid_float_no_trailing, priority = 2)]
616 InvalidFloatNoTrailing,
617
618 #[regex(r"/[a-zA-Z0-9_./+-]*", lex_path)]
624 Path(String),
625
626 #[regex(r"[a-zA-Z_][a-zA-Z0-9_.@-]*", lex_ident)]
634 Ident(String),
635
636 #[regex(r"#[^\n\r]*", allow_greedy = true)]
642 Comment,
643
644 #[regex(r"\n|\r\n")]
646 Newline,
647
648 #[regex(r"\\[ \t]*(\n|\r\n)")]
650 LineContinuation,
651
652 #[token("`", reject_backtick)]
661 BacktickRejected,
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
669pub enum TokenCategory {
670 Keyword,
672 Operator,
674 String,
676 Number,
678 Variable,
680 Comment,
682 Punctuation,
684 Command,
686 Path,
688 Flag,
690 Error,
692}
693
694impl Token {
695 pub fn category(&self) -> TokenCategory {
697 match self {
698 Token::If
700 | Token::Then
701 | Token::Else
702 | Token::Elif
703 | Token::Fi
704 | Token::For
705 | Token::In
706 | Token::Do
707 | Token::Done
708 | Token::While
709 | Token::Case
710 | Token::Esac
711 | Token::Function
712 | Token::Return
713 | Token::Break
714 | Token::Continue
715 | Token::Exit
716 | Token::Set
717 | Token::Local
718 | Token::True
719 | Token::False
720 | Token::TypeString
721 | Token::TypeInt
722 | Token::TypeFloat
723 | Token::TypeBool => TokenCategory::Keyword,
724
725 Token::Pipe
727 | Token::And
728 | Token::Or
729 | Token::Amp
730 | Token::Eq
731 | Token::EqEq
732 | Token::NotEq
733 | Token::Match
734 | Token::NotMatch
735 | Token::Lt
736 | Token::Gt
737 | Token::LtEq
738 | Token::GtEq
739 | Token::GtGt
740 | Token::Stderr
741 | Token::Both
742 | Token::HereDocStart
743 | Token::HereString
744 | Token::StderrToStdout
745 | Token::StdoutToStderr
746 | Token::StdoutToStderr2 => TokenCategory::Operator,
747
748 Token::String(_) | Token::SingleString(_) | Token::HereDoc(_) => TokenCategory::String,
750
751 Token::Int(_) | Token::Float(_) | Token::Arithmetic(_) => TokenCategory::Number,
753
754 Token::VarRef(_)
756 | Token::SimpleVarRef(_)
757 | Token::Positional(_)
758 | Token::AllArgs
759 | Token::ArgCount
760 | Token::VarLength(_)
761 | Token::LastExitCode
762 | Token::CurrentPid => TokenCategory::Variable,
763
764 Token::LongFlag(_)
766 | Token::ShortFlag(_)
767 | Token::PlusFlag(_)
768 | Token::DoubleDash => TokenCategory::Flag,
769
770 Token::Semi
772 | Token::DoubleSemi
773 | Token::Colon
774 | Token::Comma
775 | Token::Dot
776 | Token::LParen
777 | Token::RParen
778 | Token::LBrace
779 | Token::RBrace
780 | Token::LBracket
781 | Token::RBracket
782 | Token::Bang
783 | Token::Question
784 | Token::Star
785 | Token::Newline
786 | Token::LineContinuation
787 | Token::CmdSubstStart
788 | Token::DotDotDot => TokenCategory::Punctuation,
789
790 Token::GlobWord(_) => TokenCategory::Path,
792
793 Token::Comment => TokenCategory::Comment,
795
796 Token::Path(_)
798 | Token::TildePath(_)
799 | Token::RelativePath(_)
800 | Token::Tilde
801 | Token::DotDot
802 | Token::DotSlashPath(_) => TokenCategory::Path,
803
804 Token::Ident(_)
806 | Token::PlusBare(_)
807 | Token::MinusBare(_)
808 | Token::DoubleDashBare(_)
809 | Token::MinusAlone
810 | Token::NumberIdent(_)
811 | Token::DashNumWord(_)
812 | Token::AtWord(_)
813 | Token::DottedIdent(_)
814 | Token::JobSpec(_) => TokenCategory::Command,
815
816 Token::InvalidFloatNoLeading
818 | Token::InvalidFloatNoTrailing
819 | Token::BacktickRejected => TokenCategory::Error,
820 }
821 }
822}
823
824fn lex_string(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
826 parse_string_literal(lex.slice())
827}
828
829fn lex_single_string(lex: &mut logos::Lexer<Token>) -> String {
831 let s = lex.slice();
832 s[1..s.len() - 1].to_string()
834}
835
836fn lex_varref(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
846 let mut depth = 1usize;
847 let mut extra = 0usize;
848 for c in lex.remainder().chars() {
849 extra += c.len_utf8();
850 match c {
851 '{' => depth += 1,
852 '}' => {
853 depth -= 1;
854 if depth == 0 {
855 if extra == 1 {
856 return Err(LexerError::UnexpectedCharacter);
859 }
860 lex.bump(extra);
861 return Ok(lex.slice().to_string());
862 }
863 }
864 _ => {}
865 }
866 }
867 Err(LexerError::UnterminatedVarRef)
868}
869
870fn lex_simple_varref(lex: &mut logos::Lexer<Token>) -> String {
872 lex.slice()[1..].to_string()
874}
875
876fn lex_positional(lex: &mut logos::Lexer<Token>) -> usize {
878 lex.slice()[1..].parse().unwrap_or(0)
880}
881
882fn lex_var_length(lex: &mut logos::Lexer<Token>) -> String {
884 let s = lex.slice();
886 s[3..s.len() - 1].to_string()
887}
888
889fn lex_int(lex: &mut logos::Lexer<Token>) -> Result<i64, LexerError> {
891 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
892}
893
894fn lex_float(lex: &mut logos::Lexer<Token>) -> Result<f64, LexerError> {
896 lex.slice().parse().map_err(|_| LexerError::InvalidNumber)
897}
898
899fn lex_number_ident(lex: &mut logos::Lexer<Token>) -> String {
903 lex.slice().to_string()
904}
905
906fn lex_dotted_ident(lex: &mut logos::Lexer<Token>) -> String {
908 lex.slice().to_string()
909}
910
911fn lex_slice_word(lex: &mut logos::Lexer<Token>) -> String {
914 lex.slice().to_string()
915}
916
917fn lex_invalid_float_no_leading(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
920 Err(LexerError::InvalidFloatNoLeading)
921}
922
923fn reject_backtick(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
927 Err(LexerError::BackticksNotSupported)
928}
929
930fn lex_invalid_float_no_trailing(_lex: &mut logos::Lexer<Token>) -> Result<(), LexerError> {
933 Err(LexerError::InvalidFloatNoTrailing)
934}
935
936fn lex_ident(lex: &mut logos::Lexer<Token>) -> Result<String, LexerError> {
938 let s = lex.slice();
939
940 match s.to_lowercase().as_str() {
943 "true" | "false" if s != "true" && s != "false" => {
944 return Err(LexerError::AmbiguousBoolean(s.to_string()));
945 }
946 _ => {}
947 }
948
949 if s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("no") {
951 return Err(LexerError::AmbiguousBooleanLike(s.to_string()));
952 }
953
954 Ok(s.to_string())
955}
956
957fn lex_long_flag(lex: &mut logos::Lexer<Token>) -> String {
959 lex.slice()[2..].to_string()
961}
962
963fn lex_short_flag(lex: &mut logos::Lexer<Token>) -> String {
965 lex.slice()[1..].to_string()
967}
968
969fn lex_plus_flag(lex: &mut logos::Lexer<Token>) -> String {
971 lex.slice()[1..].to_string()
973}
974
975fn lex_plus_bare(lex: &mut logos::Lexer<Token>) -> String {
977 lex.slice().to_string()
978}
979
980fn lex_minus_bare(lex: &mut logos::Lexer<Token>) -> String {
982 lex.slice().to_string()
983}
984
985fn lex_double_dash_bare(lex: &mut logos::Lexer<Token>) -> String {
988 lex.slice().to_string()
989}
990
991fn lex_job_spec(lex: &mut logos::Lexer<Token>) -> String {
993 lex.slice().to_string()
994}
995
996fn lex_path(lex: &mut logos::Lexer<Token>) -> String {
998 lex.slice().to_string()
999}
1000
1001fn lex_tilde_path(lex: &mut logos::Lexer<Token>) -> String {
1003 lex.slice().to_string()
1004}
1005
1006fn lex_relative_path(lex: &mut logos::Lexer<Token>) -> String {
1008 lex.slice().to_string()
1009}
1010
1011fn lex_dot_slash_path(lex: &mut logos::Lexer<Token>) -> String {
1013 lex.slice().to_string()
1014}
1015
1016impl fmt::Display for Token {
1017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1018 match self {
1019 Token::Set => write!(f, "set"),
1020 Token::Local => write!(f, "local"),
1021 Token::If => write!(f, "if"),
1022 Token::Then => write!(f, "then"),
1023 Token::Else => write!(f, "else"),
1024 Token::Elif => write!(f, "elif"),
1025 Token::Fi => write!(f, "fi"),
1026 Token::For => write!(f, "for"),
1027 Token::While => write!(f, "while"),
1028 Token::In => write!(f, "in"),
1029 Token::Do => write!(f, "do"),
1030 Token::Done => write!(f, "done"),
1031 Token::Case => write!(f, "case"),
1032 Token::Esac => write!(f, "esac"),
1033 Token::Function => write!(f, "function"),
1034 Token::Break => write!(f, "break"),
1035 Token::Continue => write!(f, "continue"),
1036 Token::Return => write!(f, "return"),
1037 Token::Exit => write!(f, "exit"),
1038 Token::True => write!(f, "true"),
1039 Token::False => write!(f, "false"),
1040 Token::TypeString => write!(f, "string"),
1041 Token::TypeInt => write!(f, "int"),
1042 Token::TypeFloat => write!(f, "float"),
1043 Token::TypeBool => write!(f, "bool"),
1044 Token::And => write!(f, "&&"),
1045 Token::Or => write!(f, "||"),
1046 Token::EqEq => write!(f, "=="),
1047 Token::NotEq => write!(f, "!="),
1048 Token::Match => write!(f, "=~"),
1049 Token::NotMatch => write!(f, "!~"),
1050 Token::GtEq => write!(f, ">="),
1051 Token::LtEq => write!(f, "<="),
1052 Token::GtGt => write!(f, ">>"),
1053 Token::StderrToStdout => write!(f, "2>&1"),
1054 Token::StdoutToStderr => write!(f, "1>&2"),
1055 Token::StdoutToStderr2 => write!(f, ">&2"),
1056 Token::Stderr => write!(f, "2>"),
1057 Token::Both => write!(f, "&>"),
1058 Token::HereDocStart => write!(f, "<<"),
1059 Token::HereString => write!(f, "<<<"),
1060 Token::DoubleSemi => write!(f, ";;"),
1061 Token::Eq => write!(f, "="),
1062 Token::Pipe => write!(f, "|"),
1063 Token::Amp => write!(f, "&"),
1064 Token::Gt => write!(f, ">"),
1065 Token::Lt => write!(f, "<"),
1066 Token::Semi => write!(f, ";"),
1067 Token::Colon => write!(f, ":"),
1068 Token::Comma => write!(f, ","),
1069 Token::Dot => write!(f, "."),
1070 Token::DotDot => write!(f, ".."),
1071 Token::DotDotDot => write!(f, "..."),
1072 Token::Tilde => write!(f, "~"),
1073 Token::TildePath(s) => write!(f, "{}", s),
1074 Token::RelativePath(s) => write!(f, "{}", s),
1075 Token::DotSlashPath(s) => write!(f, "{}", s),
1076 Token::LBrace => write!(f, "{{"),
1077 Token::RBrace => write!(f, "}}"),
1078 Token::LBracket => write!(f, "["),
1079 Token::RBracket => write!(f, "]"),
1080 Token::LParen => write!(f, "("),
1081 Token::RParen => write!(f, ")"),
1082 Token::Star => write!(f, "*"),
1083 Token::Bang => write!(f, "!"),
1084 Token::Question => write!(f, "?"),
1085 Token::GlobWord(s) => write!(f, "GLOB({})", s),
1086 Token::Arithmetic(s) => write!(f, "ARITHMETIC({})", s),
1087 Token::CmdSubstStart => write!(f, "$("),
1088 Token::LongFlag(s) => write!(f, "--{}", s),
1089 Token::ShortFlag(s) => write!(f, "-{}", s),
1090 Token::PlusFlag(s) => write!(f, "+{}", s),
1091 Token::DoubleDash => write!(f, "--"),
1092 Token::DoubleDashBare(s) => write!(f, "{}", s),
1093 Token::PlusBare(s) => write!(f, "{}", s),
1094 Token::MinusBare(s) => write!(f, "{}", s),
1095 Token::JobSpec(s) => write!(f, "{}", s),
1096 Token::MinusAlone => write!(f, "-"),
1097 Token::String(s) => write!(f, "STRING({:?})", s),
1098 Token::SingleString(s) => write!(f, "SINGLESTRING({:?})", s),
1099 Token::HereDoc(d) => write!(f, "HEREDOC({:?}, literal={})", d.content, d.literal),
1100 Token::VarRef(v) => write!(f, "VARREF({})", v),
1101 Token::SimpleVarRef(v) => write!(f, "SIMPLEVARREF({})", v),
1102 Token::Positional(n) => write!(f, "${}", n),
1103 Token::AllArgs => write!(f, "$@"),
1104 Token::ArgCount => write!(f, "$#"),
1105 Token::LastExitCode => write!(f, "$?"),
1106 Token::CurrentPid => write!(f, "$$"),
1107 Token::VarLength(v) => write!(f, "${{#{}}}", v),
1108 Token::Int(n) => write!(f, "INT({})", n),
1109 Token::Float(n) => write!(f, "FLOAT({})", n),
1110 Token::Path(s) => write!(f, "PATH({})", s),
1111 Token::Ident(s) => write!(f, "IDENT({})", s),
1112 Token::NumberIdent(s) => write!(f, "NUMIDENT({})", s),
1113 Token::DashNumWord(s) => write!(f, "DASHNUM({})", s),
1114 Token::AtWord(s) => write!(f, "ATWORD({})", s),
1115 Token::DottedIdent(s) => write!(f, "DOTIDENT({})", s),
1116 Token::Comment => write!(f, "COMMENT"),
1117 Token::Newline => write!(f, "NEWLINE"),
1118 Token::LineContinuation => write!(f, "LINECONT"),
1119 Token::InvalidFloatNoLeading => write!(f, "INVALID_FLOAT_NO_LEADING"),
1121 Token::InvalidFloatNoTrailing => write!(f, "INVALID_FLOAT_NO_TRAILING"),
1122 Token::BacktickRejected => write!(f, "BACKTICK_REJECTED"),
1123 }
1124 }
1125}
1126
1127impl Token {
1128 pub fn is_keyword(&self) -> bool {
1133 matches!(
1134 self,
1135 Token::Set
1136 | Token::Local
1137 | Token::If
1138 | Token::Then
1139 | Token::Else
1140 | Token::Elif
1141 | Token::Fi
1142 | Token::For
1143 | Token::In
1144 | Token::Do
1145 | Token::Done
1146 | Token::While
1147 | Token::Case
1148 | Token::Esac
1149 | Token::Function
1150 | Token::Return
1151 | Token::Break
1152 | Token::Continue
1153 | Token::Exit
1154 | Token::True
1155 | Token::False
1156 )
1157 }
1158
1159 pub fn is_type(&self) -> bool {
1161 matches!(
1162 self,
1163 Token::TypeString
1164 | Token::TypeInt
1165 | Token::TypeFloat
1166 | Token::TypeBool
1167 )
1168 }
1169
1170 pub fn starts_statement(&self) -> bool {
1173 matches!(
1174 self,
1175 Token::Set
1176 | Token::Local
1177 | Token::Function
1178 | Token::If
1179 | Token::For
1180 | Token::While
1181 | Token::Case
1182 | Token::Ident(_)
1183 | Token::LBracket
1184 )
1185 }
1186
1187 pub fn is_value(&self) -> bool {
1189 matches!(
1190 self,
1191 Token::String(_)
1192 | Token::SingleString(_)
1193 | Token::HereDoc(_)
1194 | Token::Arithmetic(_)
1195 | Token::Int(_)
1196 | Token::Float(_)
1197 | Token::True
1198 | Token::False
1199 | Token::VarRef(_)
1200 | Token::SimpleVarRef(_)
1201 | Token::CmdSubstStart
1202 | Token::Path(_)
1203 | Token::GlobWord(_)
1204 | Token::LastExitCode
1205 | Token::CurrentPid
1206 )
1207 }
1208}
1209
1210#[derive(Debug, Clone)]
1230struct Replacement {
1231 orig_start: usize,
1232 orig_len: usize,
1233 new_start: usize,
1234 new_len: usize,
1235 kind: ReplacementKind,
1236}
1237
1238#[derive(Debug, Clone, PartialEq)]
1239enum ReplacementKind {
1240 Arith(usize),
1242 HeredocIntro(usize),
1244 Elision,
1246}
1247
1248fn map_position(p: usize, is_end: bool, replacements: &[Replacement]) -> usize {
1253 let mut delta: isize = 0;
1254 for r in replacements {
1255 let r_end = r.new_start + r.new_len;
1256 let past = if is_end {
1257 p >= r_end && p > r.new_start
1258 } else {
1259 p >= r_end
1260 };
1261 if past {
1262 delta += r.orig_len as isize - r.new_len as isize;
1263 } else if p > r.new_start {
1264 return r.orig_start + (p - r.new_start).min(r.orig_len);
1267 } else {
1268 break; }
1270 }
1271 ((p as isize) + delta).max(0) as usize
1272}
1273
1274fn map_span(span: &Span, replacements: &[Replacement]) -> Span {
1275 let start = map_position(span.start, false, replacements);
1276 let end = map_position(span.end, true, replacements).max(start);
1277 start..end
1278}
1279
1280#[derive(Debug, Clone)]
1287struct HeredocExtract {
1288 body: String,
1289 literal: bool,
1290 strip_tabs: bool,
1291 body_start_offset: usize,
1292}
1293
1294struct PendingHeredoc {
1298 delimiter: String,
1299 literal: bool,
1300 strip_tabs: bool,
1301 intro_span: Span,
1304}
1305
1306struct ScanOutput {
1309 text: String,
1310 arithmetics: Vec<(String, String)>,
1312 heredocs: Vec<HeredocExtract>,
1314 replacements: Vec<Replacement>,
1315}
1316
1317fn scan(source: &str) -> Result<ScanOutput, Spanned<LexerError>> {
1325 let chars: Vec<(usize, char)> = source.char_indices().collect();
1326 let n = chars.len();
1327 let total_len = source.len();
1328 let byte_at = |i: usize| -> usize {
1329 if i < n { chars[i].0 } else { total_len }
1330 };
1331
1332 let mut out = String::with_capacity(source.len());
1333 let mut arithmetics: Vec<(String, String)> = Vec::new();
1334 let mut heredocs: Vec<HeredocExtract> = Vec::new();
1335 let mut replacements: Vec<Replacement> = Vec::new();
1336 let mut pending: Vec<PendingHeredoc> = Vec::new();
1337
1338 let mut i = 0;
1339 let mut prev_char: Option<char> = None;
1342
1343 while i < n {
1344 let (pos, ch) = chars[i];
1345
1346 if ch == '\\' && i + 1 < n {
1351 out.push(ch);
1352 out.push(chars[i + 1].1);
1353 prev_char = Some(chars[i + 1].1);
1354 i += 2;
1355 continue;
1356 }
1357
1358 match ch {
1359 '\'' => {
1362 out.push(ch);
1363 i += 1;
1364 while i < n && chars[i].1 != '\'' {
1365 out.push(chars[i].1);
1366 i += 1;
1367 }
1368 if i < n {
1369 out.push('\''); i += 1;
1371 }
1372 prev_char = Some('\'');
1373 }
1374
1375 '"' => {
1378 out.push(ch);
1379 i += 1;
1380 while i < n {
1381 let (dpos, dch) = chars[i];
1382 if dch == '\\' && i + 1 < n {
1383 let next = chars[i + 1].1;
1384 if next == '"' || next == '\\' || next == '$' || next == '`' {
1385 out.push(dch);
1386 out.push(next);
1387 i += 2;
1388 continue;
1389 }
1390 }
1391 if dch == '"' {
1392 out.push(dch);
1393 i += 1;
1394 break;
1395 }
1396 if dch == '$'
1397 && i + 2 < n
1398 && chars[i + 1].1 == '('
1399 && chars[i + 2].1 == '('
1400 {
1401 extract_arithmetic(
1402 &chars,
1403 &mut i,
1404 dpos,
1405 total_len,
1406 &mut out,
1407 &mut arithmetics,
1408 &mut replacements,
1409 )?;
1410 continue;
1411 }
1412 out.push(dch);
1413 i += 1;
1414 }
1415 prev_char = Some('"');
1416 }
1417
1418 '#' if prev_char != Some('$') => {
1421 while i < n && chars[i].1 != '\n' && chars[i].1 != '\r' {
1422 out.push(chars[i].1);
1423 i += 1;
1424 }
1425 prev_char = Some('#');
1426 }
1427
1428 '<' if i + 1 < n && chars[i + 1].1 == '<' => {
1430 if i + 2 < n && chars[i + 2].1 == '<' {
1431 out.push_str("<<<");
1432 i += 3;
1433 prev_char = Some('<');
1434 continue;
1435 }
1436 let heredoc_index = heredocs.len() + pending.len();
1437 scan_heredoc_introducer(
1438 &chars,
1439 &mut i,
1440 pos,
1441 &mut out,
1442 &mut pending,
1443 &mut replacements,
1444 heredoc_index,
1445 );
1446 prev_char = Some('_'); }
1448
1449 '$' if i + 2 < n && chars[i + 1].1 == '(' && chars[i + 2].1 == '(' => {
1451 extract_arithmetic(
1452 &chars,
1453 &mut i,
1454 pos,
1455 total_len,
1456 &mut out,
1457 &mut arithmetics,
1458 &mut replacements,
1459 )?;
1460 prev_char = Some('_');
1461 }
1462 '$' if i + 1 < n && chars[i + 1].1 == '{' => {
1463 out.push('$');
1469 out.push('{');
1470 i += 2;
1471 let mut depth = 1usize;
1472 while i < n && depth > 0 {
1473 let (vpos, vch) = chars[i];
1474 if vch == '$'
1475 && i + 2 < n
1476 && chars[i + 1].1 == '('
1477 && chars[i + 2].1 == '('
1478 {
1479 return Err(Spanned::new(
1480 LexerError::ArithmeticInVarRef,
1481 vpos..(byte_at(i + 3)),
1482 ));
1483 }
1484 match vch {
1485 '{' => depth += 1,
1486 '}' => depth -= 1,
1487 _ => {}
1488 }
1489 out.push(vch);
1490 i += 1;
1491 }
1492 prev_char = Some('}');
1493 }
1494
1495 '\n' => {
1498 out.push('\n');
1499 i += 1;
1500 prev_char = Some('\n');
1501 if !pending.is_empty() {
1502 collect_heredoc_bodies(
1503 &chars,
1504 &mut i,
1505 total_len,
1506 out.len(),
1507 &mut pending,
1508 &mut heredocs,
1509 &mut replacements,
1510 )?;
1511 }
1512 }
1513
1514 '\r' if !pending.is_empty()
1522 && chars.get(i + 1).map(|c| c.1) != Some('\n') =>
1523 {
1524 out.push('\n');
1525 i += 1;
1526 prev_char = Some('\n');
1527 collect_heredoc_bodies(
1528 &chars,
1529 &mut i,
1530 total_len,
1531 out.len(),
1532 &mut pending,
1533 &mut heredocs,
1534 &mut replacements,
1535 )?;
1536 }
1537
1538 _ => {
1539 out.push(ch);
1540 i += 1;
1541 prev_char = Some(ch);
1542 }
1543 }
1544 }
1545
1546 if let Some(p) = pending.first() {
1549 return Err(Spanned::new(
1550 LexerError::UnterminatedHeredoc {
1551 delimiter: p.delimiter.clone(),
1552 },
1553 p.intro_span.clone(),
1554 ));
1555 }
1556
1557 Ok(ScanOutput {
1558 text: out,
1559 arithmetics,
1560 heredocs,
1561 replacements,
1562 })
1563}
1564
1565fn extract_arithmetic(
1570 chars: &[(usize, char)],
1571 i: &mut usize,
1572 start_pos: usize,
1573 total_len: usize,
1574 out: &mut String,
1575 arithmetics: &mut Vec<(String, String)>,
1576 replacements: &mut Vec<Replacement>,
1577) -> Result<(), Spanned<LexerError>> {
1578 let n = chars.len();
1579 *i += 3; let mut expr = String::new();
1582 let mut depth = 0usize;
1583 let mut closed = false;
1584
1585 while *i < n {
1586 let c = chars[*i].1;
1587 match c {
1588 '(' => {
1589 depth += 1;
1590 if depth > MAX_PAREN_DEPTH {
1591 return Err(Spanned::new(
1592 LexerError::NestingTooDeep,
1593 start_pos..chars[*i].0,
1594 ));
1595 }
1596 expr.push('(');
1597 *i += 1;
1598 }
1599 ')' => {
1600 if depth > 0 {
1601 depth -= 1;
1602 expr.push(')');
1603 *i += 1;
1604 } else if *i + 1 < n && chars[*i + 1].1 == ')' {
1605 *i += 2;
1606 closed = true;
1607 break;
1608 } else if *i + 1 == n {
1609 break;
1611 } else {
1612 expr.push(')');
1613 *i += 1;
1614 }
1615 }
1616 _ => {
1617 expr.push(c);
1618 *i += 1;
1619 }
1620 }
1621 }
1622
1623 if !closed {
1624 return Err(Spanned::new(
1627 LexerError::UnterminatedArithmetic,
1628 start_pos..total_len,
1629 ));
1630 }
1631
1632 let end_pos = if *i < n { chars[*i].0 } else { total_len };
1633 let marker = format!("__KAISH_ARITH_{}__", unique_marker_id());
1634 replacements.push(Replacement {
1635 orig_start: start_pos,
1636 orig_len: end_pos - start_pos,
1637 new_start: out.len(),
1638 new_len: marker.len(),
1639 kind: ReplacementKind::Arith(arithmetics.len()),
1640 });
1641 arithmetics.push((marker.clone(), expr));
1642 out.push_str(&marker);
1643 Ok(())
1644}
1645
1646fn scan_heredoc_introducer(
1653 chars: &[(usize, char)],
1654 i: &mut usize,
1655 intro_start: usize,
1656 out: &mut String,
1657 pending: &mut Vec<PendingHeredoc>,
1658 replacements: &mut Vec<Replacement>,
1659 heredoc_index: usize,
1660) {
1661 let n = chars.len();
1662 *i += 2; let strip_tabs = *i < n && chars[*i].1 == '-';
1665 if strip_tabs {
1666 *i += 1;
1667 }
1668
1669 while *i < n && (chars[*i].1 == ' ' || chars[*i].1 == '\t') {
1671 *i += 1;
1672 }
1673
1674 let mut delimiter = String::new();
1679 let mut literal = false;
1680 while *i < n {
1681 let c = chars[*i].1;
1682 match c {
1683 '\'' | '"' => {
1684 literal = true;
1685 let quote = c;
1686 *i += 1;
1687 while *i < n && chars[*i].1 != quote {
1688 delimiter.push(chars[*i].1);
1689 *i += 1;
1690 }
1691 if *i < n {
1692 *i += 1; }
1694 }
1695 c if c.is_whitespace() => break,
1696 c => {
1697 delimiter.push(c);
1698 *i += 1;
1699 }
1700 }
1701 }
1702 let word_end = if *i < n {
1703 chars[*i].0
1704 } else {
1705 chars
1706 .last()
1707 .map(|(pos, c)| pos + c.len_utf8())
1708 .unwrap_or(intro_start + 2)
1709 };
1710
1711 if delimiter.is_empty() {
1712 out.push_str("<<");
1714 if strip_tabs {
1715 out.push('-');
1716 }
1717 return;
1718 }
1719
1720 let marker = format!("__KAISH_HEREDOC_{}__", unique_marker_id());
1721 out.push_str("<<");
1722 replacements.push(Replacement {
1723 orig_start: intro_start + 2,
1727 orig_len: word_end - (intro_start + 2),
1728 new_start: out.len(),
1729 new_len: marker.len(),
1730 kind: ReplacementKind::HeredocIntro(heredoc_index),
1731 });
1732 out.push_str(&marker);
1733 pending.push(PendingHeredoc {
1734 delimiter,
1735 literal,
1736 strip_tabs,
1737 intro_span: intro_start..word_end,
1738 });
1739}
1740
1741fn collect_heredoc_bodies(
1747 chars: &[(usize, char)],
1748 i: &mut usize,
1749 total_len: usize,
1750 out_len: usize,
1751 pending: &mut Vec<PendingHeredoc>,
1752 heredocs: &mut Vec<HeredocExtract>,
1753 replacements: &mut Vec<Replacement>,
1754) -> Result<(), Spanned<LexerError>> {
1755 let n = chars.len();
1756
1757 for p in pending.drain(..) {
1758 let body_start = if *i < n { chars[*i].0 } else { total_len };
1759 let mut body = String::new();
1760 let mut found = false;
1761
1762 while !found {
1763 if *i >= n {
1764 return Err(Spanned::new(
1767 LexerError::UnterminatedHeredoc {
1768 delimiter: p.delimiter.clone(),
1769 },
1770 p.intro_span.clone(),
1771 ));
1772 }
1773
1774 let mut line = String::new();
1778 let mut terminator = "";
1779 let mut at_eof = false;
1780 loop {
1781 if *i >= n {
1782 at_eof = true;
1783 break;
1784 }
1785 let c = chars[*i].1;
1786 if c == '\n' {
1787 *i += 1;
1788 terminator = "\n";
1789 break;
1790 }
1791 if c == '\r' {
1792 *i += 1;
1793 if *i < n && chars[*i].1 == '\n' {
1794 *i += 1;
1795 terminator = "\r\n";
1796 } else {
1797 terminator = "\r";
1798 }
1799 break;
1800 }
1801 line.push(c);
1802 *i += 1;
1803 }
1804
1805 let compare = if p.strip_tabs {
1806 line.trim_start_matches('\t')
1807 } else {
1808 line.as_str()
1809 };
1810 if compare == p.delimiter {
1811 found = true;
1812 } else if at_eof {
1813 return Err(Spanned::new(
1817 LexerError::UnterminatedHeredoc {
1818 delimiter: p.delimiter.clone(),
1819 },
1820 p.intro_span.clone(),
1821 ));
1822 } else {
1823 body.push_str(&line);
1824 body.push_str(terminator);
1825 }
1826 }
1827
1828 let elide_end = if *i < n { chars[*i].0 } else { total_len };
1829 replacements.push(Replacement {
1830 orig_start: body_start,
1831 orig_len: elide_end - body_start,
1832 new_start: out_len,
1833 new_len: 0,
1834 kind: ReplacementKind::Elision,
1835 });
1836
1837 let body = if p.literal {
1845 body
1846 } else {
1847 rewrite_body_arithmetic(&body, &p)?
1848 };
1849
1850 heredocs.push(HeredocExtract {
1851 body,
1852 literal: p.literal,
1853 strip_tabs: p.strip_tabs,
1854 body_start_offset: body_start,
1855 });
1856 }
1857
1858 Ok(())
1859}
1860
1861fn rewrite_body_arithmetic(
1867 body: &str,
1868 p: &PendingHeredoc,
1869) -> Result<String, Spanned<LexerError>> {
1870 if !body.contains("$((") {
1871 return Ok(body.to_string());
1872 }
1873 let chars: Vec<char> = body.chars().collect();
1874 let n = chars.len();
1875 let mut out = String::with_capacity(body.len());
1876 let mut i = 0;
1877 while i < n {
1878 if chars[i] == '\\' && i + 1 < n {
1879 out.push(chars[i]);
1880 out.push(chars[i + 1]);
1881 i += 2;
1882 continue;
1883 }
1884 if chars[i] == '$' && i + 2 < n && chars[i + 1] == '(' && chars[i + 2] == '(' {
1885 i += 3;
1886 let mut expr = String::new();
1887 let mut depth = 0usize;
1888 let mut closed = false;
1889 while i < n {
1890 let c = chars[i];
1891 match c {
1892 '(' => {
1893 depth += 1;
1894 expr.push('(');
1895 i += 1;
1896 }
1897 ')' => {
1898 if depth > 0 {
1899 depth -= 1;
1900 expr.push(')');
1901 i += 1;
1902 } else if i + 1 < n && chars[i + 1] == ')' {
1903 i += 2;
1904 closed = true;
1905 break;
1906 } else {
1907 expr.push(')');
1908 i += 1;
1909 }
1910 }
1911 _ => {
1912 expr.push(c);
1913 i += 1;
1914 }
1915 }
1916 }
1917 if !closed {
1918 return Err(Spanned::new(
1919 LexerError::UnterminatedArithmetic,
1920 p.intro_span.clone(),
1921 ));
1922 }
1923 out.push_str(&format!("${{__ARITH:{}__}}", expr));
1924 continue;
1925 }
1926 out.push(chars[i]);
1927 i += 1;
1928 }
1929 Ok(out)
1930}
1931
1932fn resolve_markers(
1953 tokens: Vec<Spanned<Token>>,
1954 scan: &ScanOutput,
1955) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
1956 let markers: Vec<&Replacement> = scan
1957 .replacements
1958 .iter()
1959 .filter(|r| !matches!(r.kind, ReplacementKind::Elision))
1960 .collect();
1961
1962 let mut result = Vec::with_capacity(tokens.len());
1963 let mut mi = 0usize;
1964
1965 for spanned in tokens {
1966 let span = spanned.span.clone();
1967 while mi < markers.len() && markers[mi].new_start + markers[mi].new_len <= span.start {
1968 mi += 1;
1969 }
1970
1971 let mut contained = Vec::new();
1973 let mut mj = mi;
1974 while mj < markers.len() {
1975 let m = markers[mj];
1976 if m.new_start >= span.end {
1977 break;
1978 }
1979 if m.new_start >= span.start && m.new_start + m.new_len <= span.end {
1980 contained.push(m);
1981 }
1982 mj += 1;
1983 }
1984
1985 if contained.is_empty() {
1986 result.push(spanned);
1987 continue;
1988 }
1989
1990 match (&spanned.token, contained.as_slice()) {
1991 (Token::Ident(_), [m])
1993 if matches!(m.kind, ReplacementKind::Arith(_))
1994 && m.new_start == span.start
1995 && m.new_start + m.new_len == span.end =>
1996 {
1997 let ReplacementKind::Arith(idx) = m.kind else {
1998 unreachable!("guarded by matches! above")
1999 };
2000 result.push(Spanned::new(
2001 Token::Arithmetic(scan.arithmetics[idx].1.clone()),
2002 span,
2003 ));
2004 }
2005
2006 (Token::Ident(_), [m])
2009 if matches!(m.kind, ReplacementKind::HeredocIntro(_))
2010 && m.new_start == span.start
2011 && m.new_start + m.new_len == span.end =>
2012 {
2013 let ReplacementKind::HeredocIntro(idx) = m.kind else {
2014 unreachable!("guarded by matches! above")
2015 };
2016 let hd = &scan.heredocs[idx];
2017 result.push(Spanned::new(
2018 Token::HereDoc(HereDocData {
2019 content: hd.body.clone(),
2020 literal: hd.literal,
2021 strip_tabs: hd.strip_tabs,
2022 body_start_offset: hd.body_start_offset,
2023 }),
2024 span,
2025 ));
2026 }
2027
2028 (Token::String(s), ms) => {
2033 let mut content = s.clone();
2034 for m in ms {
2035 let ReplacementKind::Arith(idx) = m.kind else {
2036 unreachable!("heredoc marker inside string content")
2040 };
2041 let (marker, expr) = &scan.arithmetics[idx];
2042 content =
2043 content.replacen(marker, &format!("${{__ARITH:{}__}}", expr), 1);
2044 }
2045 result.push(Spanned::new(Token::String(content), span));
2046 }
2047
2048 _ => {
2054 let mut cursor = span.start;
2055 for m in &contained {
2056 if m.new_start > cursor {
2057 relex_fragment(
2058 &scan.text[cursor..m.new_start],
2059 cursor,
2060 &mut result,
2061 )?;
2062 }
2063 match m.kind {
2064 ReplacementKind::Arith(idx) => {
2065 result.push(Spanned::new(
2066 Token::Arithmetic(scan.arithmetics[idx].1.clone()),
2067 m.new_start..m.new_start + m.new_len,
2068 ));
2069 }
2070 ReplacementKind::HeredocIntro(_) | ReplacementKind::Elision => {
2071 unreachable!("heredoc marker glued into word token")
2075 }
2076 }
2077 cursor = m.new_start + m.new_len;
2078 }
2079 if cursor < span.end {
2080 relex_fragment(&scan.text[cursor..span.end], cursor, &mut result)?;
2081 }
2082 }
2083 }
2084
2085 mi = mj;
2086 }
2087
2088 Ok(result)
2089}
2090
2091fn relex_fragment(
2093 fragment: &str,
2094 base: usize,
2095 result: &mut Vec<Spanned<Token>>,
2096) -> Result<(), Vec<Spanned<LexerError>>> {
2097 let mut errors = Vec::new();
2098 for (tok, span) in Token::lexer(fragment).spanned() {
2099 let span = base + span.start..base + span.end;
2100 match tok {
2101 Ok(t) => result.push(Spanned::new(t, span)),
2102 Err(e) => errors.push(Spanned::new(e, span)),
2103 }
2104 }
2105 if errors.is_empty() { Ok(()) } else { Err(errors) }
2106}
2107
2108#[derive(Clone, Copy, Default)]
2118struct ValueContext {
2119 in_literal: bool,
2122 in_brace: bool,
2126 push_target: bool,
2131}
2132
2133#[derive(Debug, Clone, Copy, PartialEq)]
2144enum PushTarget {
2145 None,
2147 AwaitingRoot,
2150 Root(usize),
2154 RootSubscript(usize),
2156}
2157
2158#[derive(Debug, Clone, Copy, PartialEq)]
2164enum Frame {
2165 Test,
2167 List,
2169 Record,
2171 Subst,
2173 Paren,
2176}
2177
2178#[derive(Debug, Clone, Copy, PartialEq)]
2189enum StmtHead {
2190 Start,
2192 AfterLocal,
2194 Lvalue(usize),
2198 LvalueSubscript(usize),
2201 Value,
2204 Argv,
2206}
2207
2208fn is_statement_boundary(token: &Token) -> bool {
2215 matches!(
2219 token,
2220 Token::Newline
2221 | Token::Semi
2222 | Token::DoubleSemi
2223 | Token::And
2224 | Token::Or
2225 | Token::Pipe
2226 | Token::Amp
2227 | Token::If
2228 | Token::Then
2229 | Token::Elif
2230 | Token::Else
2231 | Token::Fi
2232 | Token::While
2233 | Token::Do
2234 | Token::Done
2235 | Token::For
2236 | Token::Case
2237 | Token::Esac
2238 | Token::In
2239 )
2240}
2241
2242fn compute_value_context(tokens: &[Spanned<Token>]) -> Vec<ValueContext> {
2243 let mut ctx = vec![ValueContext::default(); tokens.len()];
2244
2245 let mut frames: Vec<Frame> = Vec::new();
2249 let mut scopes: Vec<StmtHead> = vec![StmtHead::Start];
2250 let mut expect_value = false;
2251 let mut skip_paired_bracket = false;
2254
2255 let mut scope_floors: Vec<usize> = vec![0];
2258
2259 let mut push_target = PushTarget::None;
2265
2266 for i in 0..tokens.len() {
2267 let tok = &tokens[i].token;
2268 let span = &tokens[i].span;
2269
2270 let floor = *scope_floors.last().unwrap_or(&0);
2271 let top = frames.last().copied();
2272 let in_open_literal = frames.len() > floor
2273 && matches!(top, Some(Frame::List) | Some(Frame::Record));
2274
2275 ctx[i] = ValueContext {
2276 in_literal: expect_value || in_open_literal,
2277 in_brace: matches!(top, Some(Frame::Record)),
2278 push_target: false, };
2280
2281 let in_test = frames[floor..].contains(&Frame::Test);
2285
2286 let opens_value = expect_value;
2287 expect_value = false;
2288
2289 if skip_paired_bracket {
2290 skip_paired_bracket = false;
2291 continue;
2292 }
2293
2294 let stmt_head_is_start = matches!(scopes.last(), Some(StmtHead::Start));
2302 push_target = if is_statement_boundary(tok) {
2303 PushTarget::None
2304 } else {
2305 match (push_target, tok) {
2306 (PushTarget::None, Token::Ident(s)) if stmt_head_is_start && s == "push" => {
2307 PushTarget::AwaitingRoot
2308 }
2309 (PushTarget::AwaitingRoot, Token::Ident(_)) => PushTarget::Root(span.end),
2310 (PushTarget::Root(end), Token::LBracket) if span.start == end => {
2311 PushTarget::RootSubscript(1)
2312 }
2313 (PushTarget::RootSubscript(d), Token::LBracket) => {
2314 PushTarget::RootSubscript(d + 1)
2315 }
2316 (PushTarget::RootSubscript(d), Token::RBracket) => {
2317 if d == 1 {
2318 PushTarget::Root(span.end)
2319 } else {
2320 PushTarget::RootSubscript(d - 1)
2321 }
2322 }
2323 (PushTarget::RootSubscript(d), _) => PushTarget::RootSubscript(d),
2326 _ => PushTarget::None,
2327 }
2328 };
2329 ctx[i].push_target =
2330 matches!(push_target, PushTarget::Root(_) | PushTarget::RootSubscript(_));
2331
2332 match tok {
2333 Token::LBracket => {
2334 let next_adjacent_lbracket = tokens.get(i + 1).is_some_and(|t| {
2335 matches!(t.token, Token::LBracket) && t.span.start == span.end
2336 });
2337 let dfa = scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty"));
2338 if let StmtHead::Lvalue(end) = *dfa {
2339 if span.start == end {
2342 *dfa = StmtHead::LvalueSubscript(1);
2343 continue;
2344 }
2345 }
2346 if let StmtHead::LvalueSubscript(depth) = *dfa {
2347 *dfa = StmtHead::LvalueSubscript(depth + 1);
2348 continue;
2349 }
2350 if opens_value || in_open_literal {
2351 frames.push(Frame::List);
2352 } else if next_adjacent_lbracket {
2353 frames.push(Frame::Test);
2357 skip_paired_bracket = true;
2358 }
2359 }
2362 Token::RBracket => {
2363 let dfa = scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty"));
2364 if let StmtHead::LvalueSubscript(depth) = *dfa {
2365 *dfa = if depth == 1 {
2366 StmtHead::Lvalue(span.end)
2367 } else {
2368 StmtHead::LvalueSubscript(depth - 1)
2369 };
2370 continue;
2371 }
2372 let next_adjacent_rbracket = tokens.get(i + 1).is_some_and(|t| {
2373 matches!(t.token, Token::RBracket) && t.span.start == span.end
2374 });
2375 if frames.len() > floor && top == Some(Frame::List) {
2376 frames.pop();
2377 if frames.len() == floor {
2378 let dfa = scopes
2382 .last_mut()
2383 .unwrap_or_else(|| unreachable!("scopes never empty"));
2384 if *dfa == StmtHead::Value {
2385 *dfa = StmtHead::Start;
2386 }
2387 }
2388 } else if frames.len() > floor
2389 && top == Some(Frame::Test)
2390 && next_adjacent_rbracket
2391 {
2392 frames.pop();
2393 skip_paired_bracket = true;
2394 }
2395 }
2396 Token::LBrace => {
2397 if opens_value || in_open_literal {
2398 frames.push(Frame::Record);
2399 } else {
2400 *scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty")) =
2402 StmtHead::Start;
2403 }
2404 }
2405 Token::RBrace => {
2406 if frames.len() > floor && top == Some(Frame::Record) {
2407 frames.pop();
2408 if frames.len() == floor {
2409 let dfa = scopes
2410 .last_mut()
2411 .unwrap_or_else(|| unreachable!("scopes never empty"));
2412 if *dfa == StmtHead::Value {
2413 *dfa = StmtHead::Start;
2414 }
2415 }
2416 } else {
2417 *scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty")) =
2418 StmtHead::Start;
2419 }
2420 }
2421 Token::CmdSubstStart => {
2422 frames.push(Frame::Subst);
2423 scope_floors.push(frames.len());
2424 scopes.push(StmtHead::Start);
2425 }
2426 Token::LParen => {
2427 frames.push(Frame::Paren);
2428 }
2429 Token::RParen => {
2430 while let Some(f) = frames.last() {
2434 match f {
2435 Frame::Subst => {
2436 frames.pop();
2437 scope_floors.pop();
2438 scopes.pop();
2439 if scopes.is_empty() {
2440 scopes.push(StmtHead::Start);
2441 }
2442 if scope_floors.is_empty() {
2443 scope_floors.push(0);
2444 }
2445 let enclosing_floor = *scope_floors.last().unwrap_or(&0);
2449 if frames.len() == enclosing_floor {
2450 let dfa = scopes
2451 .last_mut()
2452 .unwrap_or_else(|| unreachable!("scopes never empty"));
2453 if *dfa == StmtHead::Value {
2454 *dfa = StmtHead::Start;
2455 }
2456 }
2457 break;
2458 }
2459 Frame::Paren => {
2460 frames.pop();
2461 break;
2462 }
2463 _ => {
2464 frames.pop();
2465 }
2466 }
2467 }
2468 }
2469 Token::Eq => {
2470 let dfa = scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty"));
2471 if matches!(*dfa, StmtHead::Lvalue(_)) && !in_test {
2472 expect_value = true;
2474 *dfa = StmtHead::Value;
2475 } else if matches!(*dfa, StmtHead::Value) {
2476 *dfa = StmtHead::Argv;
2479 }
2480 }
2482 Token::In if in_test => {
2483 expect_value = true;
2484 }
2485 t if is_statement_boundary(t) => {
2486 *scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty")) =
2491 StmtHead::Start;
2492 match t {
2493 Token::Newline => {
2494 while frames.len() > floor && frames.last() == Some(&Frame::Test) {
2495 frames.pop();
2496 }
2497 }
2498 Token::Semi
2499 | Token::DoubleSemi
2500 | Token::Pipe
2501 | Token::Amp
2502 | Token::And
2503 | Token::Or => {
2504 while frames.len() > floor
2505 && matches!(
2506 frames.last(),
2507 Some(Frame::Test) | Some(Frame::List) | Some(Frame::Record)
2508 )
2509 {
2510 frames.pop();
2511 }
2512 }
2513 _ => {}
2514 }
2515 }
2516 _ => {
2517 if !in_open_literal {
2520 let dfa =
2521 scopes.last_mut().unwrap_or_else(|| unreachable!("scopes never empty"));
2522 *dfa = match (*dfa, tok) {
2523 (StmtHead::Start, Token::Local) => StmtHead::AfterLocal,
2524 (StmtHead::Start, Token::Ident(_)) => StmtHead::Lvalue(span.end),
2525 (StmtHead::AfterLocal, Token::Ident(_)) => StmtHead::Lvalue(span.end),
2526 (StmtHead::LvalueSubscript(d), _) => StmtHead::LvalueSubscript(d),
2527 (StmtHead::Value, _) => StmtHead::Start,
2528 _ => StmtHead::Argv,
2529 };
2530 }
2531 }
2532 }
2533 }
2534
2535 ctx
2536}
2537
2538fn is_colon_mergeable(token: &Token) -> bool {
2552 matches!(
2553 token,
2554 Token::Ident(_)
2555 | Token::NumberIdent(_)
2556 | Token::DashNumWord(_)
2557 | Token::AtWord(_)
2558 | Token::DottedIdent(_)
2559 | Token::Colon
2560 | Token::Int(_)
2561 | Token::Path(_)
2562 | Token::Float(_)
2563 )
2564}
2565
2566fn merge_colon_adjacent(tokens: Vec<Spanned<Token>>, source: &str) -> Vec<Spanned<Token>> {
2577 if tokens.is_empty() {
2578 return tokens;
2579 }
2580
2581 let value_ctx = compute_value_context(&tokens);
2582 let mut result = Vec::with_capacity(tokens.len());
2583 let mut run: Vec<&Spanned<Token>> = Vec::new();
2584 let mut run_start = 0usize;
2585
2586 for (idx, token) in tokens.iter().enumerate() {
2587 if run.is_empty() {
2588 if is_colon_mergeable(&token.token) {
2589 run.push(token);
2590 run_start = idx;
2591 } else {
2592 result.push(token.clone());
2593 }
2594 continue;
2595 }
2596
2597 let Some(last) = run.last() else { unreachable!() };
2599 let adjacent = last.span.end == token.span.start;
2600
2601 if adjacent && is_colon_mergeable(&token.token) {
2602 run.push(token);
2603 } else {
2604 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace, source);
2605 if is_colon_mergeable(&token.token) {
2606 run.push(token);
2607 run_start = idx;
2608 } else {
2609 result.push(token.clone());
2610 }
2611 }
2612 }
2613
2614 flush_colon_run(&mut run, &mut result, value_ctx[run_start].in_brace, source);
2615
2616 result
2617}
2618
2619fn flush_colon_run(
2624 run: &mut Vec<&Spanned<Token>>,
2625 result: &mut Vec<Spanned<Token>>,
2626 suppress: bool,
2627 source: &str,
2628) {
2629 if run.is_empty() {
2630 return;
2631 }
2632
2633 let has_colon = run.iter().any(|t| matches!(t.token, Token::Colon));
2634
2635 if !suppress && run.len() >= 2 && has_colon {
2636 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2637 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2638 let text = source.get(start..end).unwrap_or_default().to_string();
2639 result.push(Spanned::new(Token::Ident(text), start..end));
2640 } else {
2641 for t in run.iter() {
2642 result.push((*t).clone());
2643 }
2644 }
2645
2646 run.clear();
2647}
2648
2649fn is_glob_mergeable(token: &Token) -> bool {
2651 matches!(
2652 token,
2653 Token::Star
2654 | Token::Question
2655 | Token::Dot
2656 | Token::DotDot
2657 | Token::Ident(_)
2658 | Token::NumberIdent(_)
2659 | Token::DashNumWord(_)
2660 | Token::AtWord(_)
2661 | Token::DottedIdent(_)
2662 | Token::Path(_)
2663 | Token::Int(_)
2664 | Token::LBracket
2665 | Token::RBracket
2666 | Token::Bang
2667 | Token::DotSlashPath(_)
2668 | Token::RelativePath(_)
2669 | Token::TildePath(_)
2670 | Token::Tilde
2671 | Token::LBrace
2672 | Token::RBrace
2673 | Token::Comma
2674 )
2675}
2676
2677fn merge_flag_metachar_adjacent(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
2690 if tokens.len() < 2 {
2691 return tokens;
2692 }
2693
2694 let mut result = Vec::with_capacity(tokens.len());
2695 let mut i = 0;
2696
2697 while i < tokens.len() {
2698 let token = &tokens[i];
2699
2700 if let Token::ShortFlag(flag_name) = &token.token {
2701 let mut fused = flag_name.clone();
2702 let mut end_span = token.span.end;
2703 let mut j = i + 1;
2704
2705 while let Some(next) = tokens.get(j) {
2706 if next.span.start == end_span {
2707 if let Token::Colon = &next.token {
2708 fused.push(':');
2709 end_span = next.span.end;
2710 j += 1;
2711 continue;
2712 }
2713 }
2714 break;
2715 }
2716
2717 if j > i + 1 {
2718 let span = token.span.start..end_span;
2719 result.push(Spanned::new(Token::ShortFlag(fused), span));
2720 i = j;
2721 continue;
2722 }
2723 }
2724
2725 result.push(token.clone());
2726 i += 1;
2727 }
2728
2729 result
2730}
2731
2732fn merge_glob_adjacent(tokens: Vec<Spanned<Token>>, source: &str) -> Vec<Spanned<Token>> {
2751 if tokens.is_empty() {
2752 return tokens;
2753 }
2754
2755 let value_ctx = compute_value_context(&tokens);
2756 let bracket_depth = compute_bracket_depth(&tokens);
2757 let mut result = Vec::with_capacity(tokens.len());
2758 let mut run: Vec<&Spanned<Token>> = Vec::new();
2759 let mut run_start = 0usize;
2760
2761 for (idx, token) in tokens.iter().enumerate() {
2762 if run.is_empty() {
2763 if is_glob_mergeable(&token.token) {
2764 run.push(token);
2765 run_start = idx;
2766 } else {
2767 result.push(token.clone());
2768 }
2769 continue;
2770 }
2771
2772 let Some(last) = run.last() else { unreachable!() };
2774 let adjacent = last.span.end == token.span.start;
2775
2776 if adjacent && is_glob_mergeable(&token.token) {
2777 run.push(token);
2778 } else {
2779 let followed_by_eq = matches!(token.token, Token::Eq);
2783 flush_glob_run(
2784 &mut run,
2785 &mut result,
2786 value_ctx[run_start].in_literal,
2787 followed_by_eq,
2788 value_ctx[run_start].push_target,
2789 bracket_depth[run_start],
2790 source,
2791 );
2792 if is_glob_mergeable(&token.token) {
2793 run.push(token);
2794 run_start = idx;
2795 } else {
2796 result.push(token.clone());
2797 }
2798 }
2799 }
2800
2801 flush_glob_run(
2805 &mut run,
2806 &mut result,
2807 value_ctx[run_start].in_literal,
2808 false,
2809 value_ctx[run_start].push_target,
2810 bracket_depth[run_start],
2811 source,
2812 );
2813
2814 result
2815}
2816
2817fn compute_bracket_depth(tokens: &[Spanned<Token>]) -> Vec<usize> {
2838 let mut depths = Vec::with_capacity(tokens.len());
2839 let mut depth: i32 = 0;
2840 for t in tokens {
2841 if is_statement_boundary(&t.token) {
2842 depth = 0;
2843 }
2844 depths.push(depth.max(0) as usize);
2845 match &t.token {
2846 Token::LBracket | Token::LBrace => depth += 1,
2847 Token::RBracket | Token::RBrace => depth = (depth - 1).max(0),
2848 _ => {}
2849 }
2850 }
2851 depths
2852}
2853
2854fn run_has_bare_comma(run: &[&Spanned<Token>], start_depth: usize) -> bool {
2865 let mut depth = start_depth as i32;
2866 let mut found = false;
2867 for t in run.iter() {
2868 match &t.token {
2869 Token::LBracket | Token::LBrace => depth += 1,
2870 Token::RBracket | Token::RBrace => depth = (depth - 1).max(0),
2871 Token::Comma if depth == 0 => found = true,
2872 _ => {}
2873 }
2874 }
2875 found
2876}
2877
2878fn flush_glob_run(
2914 run: &mut Vec<&Spanned<Token>>,
2915 result: &mut Vec<Spanned<Token>>,
2916 value_position_suppress: bool,
2917 followed_by_eq: bool,
2918 push_target: bool,
2919 bracket_depth_at_start: usize,
2920 source: &str,
2921) {
2922 if run.is_empty() {
2923 return;
2924 }
2925
2926 let has_bracket_pair = run.iter().any(|t| matches!(t.token, Token::LBracket))
2927 && run.iter().any(|t| matches!(t.token, Token::RBracket));
2928 let has_star_or_question = run
2929 .iter()
2930 .any(|t| matches!(t.token, Token::Star | Token::Question));
2931 let has_glob = has_star_or_question || has_bracket_pair;
2932
2933 let run_starts_with_ident = matches!(run.first().map(|t| &t.token), Some(Token::Ident(_)));
2939 let lvalue_suppress =
2940 followed_by_eq && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2941 let push_target_suppress =
2942 push_target && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2943 let suppress = (value_position_suppress && has_bracket_pair) || lvalue_suppress;
2944 let has_bare_comma =
2945 !value_position_suppress && run_has_bare_comma(run, bracket_depth_at_start);
2946
2947 if push_target_suppress && run.len() >= 2 {
2948 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2951 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2952 let text = source.get(start..end).unwrap_or_default().to_string();
2953 result.push(Spanned::new(Token::Ident(text), start..end));
2954 } else if !suppress && run.len() >= 2 && has_glob {
2955 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2956 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2957 let text = source.get(start..end).unwrap_or_default().to_string();
2958 result.push(Spanned::new(Token::GlobWord(text), start..end));
2959 } else if run.len() >= 2 && has_bare_comma {
2960 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2961 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2962 let text = source.get(start..end).unwrap_or_default().to_string();
2963 result.push(Spanned::new(Token::Ident(text), start..end));
2964 } else {
2965 for t in run.iter() {
2966 result.push((*t).clone());
2967 }
2968 }
2969
2970 run.clear();
2971}
2972
2973pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2986 tokenize_impl(source, false)
2987}
2988
2989pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2995 tokenize_impl(source, true)
2996}
2997
2998fn tokenize_impl(
2999 source: &str,
3000 keep_comments: bool,
3001) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
3002 let scan_output = scan(source).map_err(|e| vec![e])?;
3003
3004 debug_assert!(
3008 scan_output
3009 .replacements
3010 .windows(2)
3011 .all(|w| w[0].new_start <= w[1].new_start),
3012 "replacement table must be ordered by new_start"
3013 );
3014
3015 let mut tokens = Vec::new();
3016 let mut errors = Vec::new();
3017 for (result, span) in Token::lexer(&scan_output.text).spanned() {
3018 match result {
3019 Ok(token) => {
3020 if !keep_comments
3021 && matches!(token, Token::Comment | Token::LineContinuation)
3022 {
3023 continue;
3024 }
3025 tokens.push(Spanned::new(token, span));
3028 }
3029 Err(err) => {
3030 errors.push(Spanned::new(err, map_span(&span, &scan_output.replacements)));
3031 }
3032 }
3033 }
3034 if !errors.is_empty() {
3035 return Err(errors);
3036 }
3037
3038 let resolved = resolve_markers(tokens, &scan_output).map_err(|errs| {
3039 errs.into_iter()
3040 .map(|e| Spanned::new(e.token, map_span(&e.span, &scan_output.replacements)))
3041 .collect::<Vec<_>>()
3042 })?;
3043
3044 let mapped: Vec<Spanned<Token>> = resolved
3045 .into_iter()
3046 .map(|s| {
3047 let span = map_span(&s.span, &scan_output.replacements);
3048 Spanned::new(s.token, span)
3049 })
3050 .collect();
3051
3052 Ok(merge_glob_adjacent(
3053 merge_colon_adjacent(merge_flag_metachar_adjacent(mapped), source),
3054 source,
3055 ))
3056}
3057
3058pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
3060 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
3062 return Err(LexerError::UnterminatedString);
3063 }
3064
3065 let inner = &source[1..source.len() - 1];
3066 let mut result = String::with_capacity(inner.len());
3067 let mut chars = inner.chars().peekable();
3068
3069 while let Some(ch) = chars.next() {
3070 if ch == '\\' {
3071 match chars.next() {
3072 Some('n') => result.push('\n'),
3073 Some('t') => result.push('\t'),
3074 Some('r') => result.push('\r'),
3075 Some('\\') => result.push('\\'),
3076 Some('"') => result.push('"'),
3077 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
3080 Some('u') => {
3081 let mut hex = String::with_capacity(4);
3083 for _ in 0..4 {
3084 match chars.next() {
3085 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
3086 _ => return Err(LexerError::InvalidEscape),
3087 }
3088 }
3089 let codepoint = u32::from_str_radix(&hex, 16)
3090 .map_err(|_| LexerError::InvalidEscape)?;
3091 let ch = char::from_u32(codepoint)
3092 .ok_or(LexerError::InvalidEscape)?;
3093 result.push(ch);
3094 }
3095 Some(next) => {
3097 result.push('\\');
3098 result.push(next);
3099 }
3100 None => return Err(LexerError::InvalidEscape),
3101 }
3102 } else {
3103 result.push(ch);
3104 }
3105 }
3106
3107 Ok(result)
3108}
3109
3110pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
3121 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
3123 return Err(LexerError::UnterminatedVarRef);
3124 }
3125
3126 let inner = &source[2..source.len() - 1];
3127
3128 if inner == "?" {
3130 return Ok(vec!["?".to_string()]);
3131 }
3132
3133 let mut segments = Vec::new();
3134 let mut current = String::new();
3135 let mut chars = inner.chars().peekable();
3136
3137 while let Some(ch) = chars.next() {
3138 match ch {
3139 '.' => {
3140 if !current.is_empty() {
3141 segments.push(current.clone());
3142 current.clear();
3143 }
3144 }
3145 '[' => {
3146 if !current.is_empty() {
3147 segments.push(current.clone());
3148 current.clear();
3149 }
3150 let mut index = String::from("[");
3155 if let Some("e) = chars.peek() {
3156 if quote == '"' || quote == '\'' {
3157 if let Some(q) = chars.next() {
3158 index.push(q);
3159 }
3160 for c in chars.by_ref() {
3161 index.push(c);
3162 if c == quote {
3163 break;
3164 }
3165 }
3166 }
3167 }
3168 while let Some(&c) = chars.peek() {
3169 if let Some(c) = chars.next() {
3170 index.push(c);
3171 }
3172 if c == ']' {
3173 break;
3174 }
3175 }
3176 segments.push(index);
3177 }
3178 _ => {
3179 current.push(ch);
3180 }
3181 }
3182 }
3183
3184 if !current.is_empty() {
3185 segments.push(current);
3186 }
3187
3188 Ok(segments)
3189}
3190
3191pub fn parse_int(source: &str) -> Result<i64, LexerError> {
3193 source.parse().map_err(|_| LexerError::InvalidNumber)
3194}
3195
3196pub fn parse_float(source: &str) -> Result<f64, LexerError> {
3198 source.parse().map_err(|_| LexerError::InvalidNumber)
3199}
3200
3201#[cfg(test)]
3202#[allow(clippy::approx_constant)]
3203mod tests {
3204 use super::*;
3205
3206 fn lex(source: &str) -> Vec<Token> {
3207 tokenize(source)
3208 .expect("lexer should succeed")
3209 .into_iter()
3210 .map(|s| s.token)
3211 .collect()
3212 }
3213
3214 #[test]
3219 fn keywords() {
3220 assert_eq!(lex("set"), vec![Token::Set]);
3221 assert_eq!(lex("if"), vec![Token::If]);
3222 assert_eq!(lex("then"), vec![Token::Then]);
3223 assert_eq!(lex("else"), vec![Token::Else]);
3224 assert_eq!(lex("elif"), vec![Token::Elif]);
3225 assert_eq!(lex("fi"), vec![Token::Fi]);
3226 assert_eq!(lex("for"), vec![Token::For]);
3227 assert_eq!(lex("in"), vec![Token::In]);
3228 assert_eq!(lex("do"), vec![Token::Do]);
3229 assert_eq!(lex("done"), vec![Token::Done]);
3230 assert_eq!(lex("case"), vec![Token::Case]);
3231 assert_eq!(lex("esac"), vec![Token::Esac]);
3232 assert_eq!(lex("function"), vec![Token::Function]);
3233 assert_eq!(lex("true"), vec![Token::True]);
3234 assert_eq!(lex("false"), vec![Token::False]);
3235 }
3236
3237 #[test]
3238 fn double_semicolon() {
3239 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
3240 assert_eq!(lex("echo \"hi\";;"), vec![
3242 Token::Ident("echo".to_string()),
3243 Token::String("hi".to_string()),
3244 Token::DoubleSemi,
3245 ]);
3246 }
3247
3248 #[test]
3249 fn type_keywords() {
3250 assert_eq!(lex("string"), vec![Token::TypeString]);
3251 assert_eq!(lex("int"), vec![Token::TypeInt]);
3252 assert_eq!(lex("float"), vec![Token::TypeFloat]);
3253 assert_eq!(lex("bool"), vec![Token::TypeBool]);
3254 }
3255
3256 #[test]
3261 fn single_char_operators() {
3262 assert_eq!(lex("="), vec![Token::Eq]);
3263 assert_eq!(lex("|"), vec![Token::Pipe]);
3264 assert_eq!(lex("&"), vec![Token::Amp]);
3265 assert_eq!(lex(">"), vec![Token::Gt]);
3266 assert_eq!(lex("<"), vec![Token::Lt]);
3267 assert_eq!(lex(";"), vec![Token::Semi]);
3268 assert_eq!(lex(":"), vec![Token::Colon]);
3269 assert_eq!(lex(","), vec![Token::Comma]);
3270 assert_eq!(lex("."), vec![Token::Dot]);
3271 }
3272
3273 #[test]
3274 fn multi_char_operators() {
3275 assert_eq!(lex("&&"), vec![Token::And]);
3276 assert_eq!(lex("||"), vec![Token::Or]);
3277 assert_eq!(lex("=="), vec![Token::EqEq]);
3278 assert_eq!(lex("!="), vec![Token::NotEq]);
3279 assert_eq!(lex("=~"), vec![Token::Match]);
3280 assert_eq!(lex("!~"), vec![Token::NotMatch]);
3281 assert_eq!(lex(">="), vec![Token::GtEq]);
3282 assert_eq!(lex("<="), vec![Token::LtEq]);
3283 assert_eq!(lex(">>"), vec![Token::GtGt]);
3284 assert_eq!(lex("2>"), vec![Token::Stderr]);
3285 assert_eq!(lex("&>"), vec![Token::Both]);
3286 }
3287
3288 #[test]
3289 fn brackets() {
3290 assert_eq!(lex("{"), vec![Token::LBrace]);
3291 assert_eq!(lex("}"), vec![Token::RBrace]);
3292 assert_eq!(lex("["), vec![Token::LBracket]);
3293 assert_eq!(lex("]"), vec![Token::RBracket]);
3294 assert_eq!(lex("("), vec![Token::LParen]);
3295 assert_eq!(lex(")"), vec![Token::RParen]);
3296 }
3297
3298 #[test]
3303 fn integers() {
3304 assert_eq!(lex("0"), vec![Token::Int(0)]);
3305 assert_eq!(lex("42"), vec![Token::Int(42)]);
3306 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
3307 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
3308 }
3309
3310 #[test]
3311 fn floats() {
3312 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
3313 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
3314 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
3315 }
3316
3317 #[test]
3318 fn strings() {
3319 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
3320 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
3321 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
3323 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
3324 }
3325
3326 #[test]
3327 fn var_refs() {
3328 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
3329 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
3330 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
3331 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
3332 }
3333
3334 #[test]
3335 fn var_ref_nested_default_is_one_token() {
3336 assert_eq!(
3340 lex("${X:-${Y}}"),
3341 vec![Token::VarRef("${X:-${Y}}".to_string())]
3342 );
3343 assert_eq!(
3344 lex("${A:-${B:-${C}}}"),
3345 vec![Token::VarRef("${A:-${B:-${C}}}".to_string())]
3346 );
3347 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3349 }
3350
3351 #[test]
3352 fn var_ref_unterminated_and_empty_are_errors() {
3353 assert!(tokenize("${X:-${Y}").is_err(), "unbalanced nesting is loud");
3354 assert!(tokenize("${a{b}").is_err(), "extra open brace is loud");
3355 assert!(tokenize("${}").is_err(), "empty reference is loud");
3356 }
3357
3358 #[test]
3359 fn var_ref_closes_at_first_balanced_brace() {
3360 assert_eq!(
3363 lex("${a}b}"),
3364 vec![
3365 Token::VarRef("${a}".to_string()),
3366 Token::Ident("b".to_string()),
3367 Token::RBrace,
3368 ]
3369 );
3370 }
3371
3372 #[test]
3377 fn identifiers() {
3378 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
3379 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
3380 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
3381 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
3382 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
3383 }
3384
3385 #[test]
3386 fn keyword_prefix_identifiers() {
3387 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
3389 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
3390 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
3391 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
3392 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
3393 }
3394
3395 #[test]
3400 fn assignment() {
3401 assert_eq!(
3402 lex("set X = 5"),
3403 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3404 );
3405 }
3406
3407 #[test]
3408 fn command_simple() {
3409 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
3410 assert_eq!(
3411 lex(r#"echo "hello""#),
3412 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
3413 );
3414 }
3415
3416 #[test]
3417 fn command_with_args() {
3418 assert_eq!(
3419 lex("cmd arg1 arg2"),
3420 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
3421 );
3422 }
3423
3424 #[test]
3425 fn command_with_named_args() {
3426 assert_eq!(
3427 lex("cmd key=value"),
3428 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
3429 );
3430 }
3431
3432 #[test]
3433 fn pipeline() {
3434 assert_eq!(
3435 lex("a | b | c"),
3436 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
3437 );
3438 }
3439
3440 #[test]
3441 fn if_statement() {
3442 assert_eq!(
3443 lex("if true; then echo; fi"),
3444 vec![
3445 Token::If,
3446 Token::True,
3447 Token::Semi,
3448 Token::Then,
3449 Token::Ident("echo".to_string()),
3450 Token::Semi,
3451 Token::Fi
3452 ]
3453 );
3454 }
3455
3456 #[test]
3457 fn for_loop() {
3458 assert_eq!(
3459 lex("for X in items; do echo; done"),
3460 vec![
3461 Token::For,
3462 Token::Ident("X".to_string()),
3463 Token::In,
3464 Token::Ident("items".to_string()),
3465 Token::Semi,
3466 Token::Do,
3467 Token::Ident("echo".to_string()),
3468 Token::Semi,
3469 Token::Done
3470 ]
3471 );
3472 }
3473
3474 #[test]
3479 fn whitespace_ignored() {
3480 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
3481 }
3482
3483 #[test]
3484 fn newlines_preserved() {
3485 let tokens = lex("a\nb");
3486 assert_eq!(
3487 tokens,
3488 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
3489 );
3490 }
3491
3492 #[test]
3493 fn multiple_newlines() {
3494 let tokens = lex("a\n\n\nb");
3495 assert_eq!(
3496 tokens,
3497 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
3498 );
3499 }
3500
3501 #[test]
3506 fn comments_skipped() {
3507 assert_eq!(lex("# comment"), vec![]);
3508 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
3509 assert_eq!(
3510 lex("a # comment\nb"),
3511 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
3512 );
3513 }
3514
3515 #[test]
3516 fn comments_preserved_when_requested() {
3517 let tokens = tokenize_with_comments("a # comment")
3518 .expect("should succeed")
3519 .into_iter()
3520 .map(|s| s.token)
3521 .collect::<Vec<_>>();
3522 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
3523 }
3524
3525 #[test]
3530 fn parse_simple_string() {
3531 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
3532 }
3533
3534 #[test]
3535 fn parse_string_with_escapes() {
3536 assert_eq!(
3537 parse_string_literal(r#""hello\nworld""#).expect("ok"),
3538 "hello\nworld"
3539 );
3540 assert_eq!(
3541 parse_string_literal(r#""tab\there""#).expect("ok"),
3542 "tab\there"
3543 );
3544 assert_eq!(
3545 parse_string_literal(r#""quote\"here""#).expect("ok"),
3546 "quote\"here"
3547 );
3548 }
3549
3550 #[test]
3551 fn parse_string_with_unicode() {
3552 assert_eq!(
3553 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
3554 "emoji ❤"
3555 );
3556 }
3557
3558 #[test]
3559 fn parse_string_with_escaped_dollar() {
3560 assert_eq!(
3563 parse_string_literal(r#""\$VAR""#).expect("ok"),
3564 "__KAISH_ESCAPED_DOLLAR__VAR"
3565 );
3566 assert_eq!(
3567 parse_string_literal(r#""cost: \$100""#).expect("ok"),
3568 "cost: __KAISH_ESCAPED_DOLLAR__100"
3569 );
3570 }
3571
3572 #[test]
3577 fn parse_simple_var() {
3578 assert_eq!(
3579 parse_var_ref("${X}").expect("ok"),
3580 vec!["X"]
3581 );
3582 }
3583
3584 #[test]
3585 fn parse_var_with_field() {
3586 assert_eq!(
3587 parse_var_ref("${VAR.field}").expect("ok"),
3588 vec!["VAR", "field"]
3589 );
3590 }
3591
3592 #[test]
3593 fn parse_var_with_index() {
3594 assert_eq!(
3595 parse_var_ref("${VAR[0]}").expect("ok"),
3596 vec!["VAR", "[0]"]
3597 );
3598 }
3599
3600 #[test]
3601 fn parse_var_nested() {
3602 assert_eq!(
3603 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
3604 vec!["VAR", "field", "[0]", "nested"]
3605 );
3606 }
3607
3608 #[test]
3609 fn parse_last_result() {
3610 assert_eq!(
3611 parse_var_ref("${?}").expect("ok"),
3612 vec!["?"]
3613 );
3614 }
3615
3616 #[test]
3619 fn parse_var_quoted_subscript_with_embedded_bracket() {
3620 assert_eq!(
3621 parse_var_ref(r#"${r["weird]key"]}"#).expect("ok"),
3622 vec!["r", r#"["weird]key"]"#]
3623 );
3624 assert_eq!(
3625 parse_var_ref("${r['weird]key']}").expect("ok"),
3626 vec!["r", "['weird]key']"]
3627 );
3628 }
3629
3630 #[test]
3633 fn parse_var_quoted_subscript_without_embedded_bracket() {
3634 assert_eq!(
3635 parse_var_ref(r#"${r["normal"]}"#).expect("ok"),
3636 vec!["r", r#"["normal"]"#]
3637 );
3638 }
3639
3640 #[test]
3644 fn parse_var_quoted_subscript_with_embedded_bracket_then_more_path() {
3645 assert_eq!(
3646 parse_var_ref(r#"${r["weird]key"][0]}"#).expect("ok"),
3647 vec!["r", r#"["weird]key"]"#, "[0]"]
3648 );
3649 }
3650
3651 #[test]
3656 fn parse_integers() {
3657 assert_eq!(parse_int("0").expect("ok"), 0);
3658 assert_eq!(parse_int("42").expect("ok"), 42);
3659 assert_eq!(parse_int("-1").expect("ok"), -1);
3660 }
3661
3662 #[test]
3663 fn parse_floats() {
3664 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
3665 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
3666 }
3667
3668 #[test]
3673 fn empty_input() {
3674 assert_eq!(lex(""), vec![]);
3675 }
3676
3677 #[test]
3678 fn only_whitespace() {
3679 assert_eq!(lex(" \t\t "), vec![]);
3680 }
3681
3682 #[test]
3683 fn json_array() {
3684 assert_eq!(
3685 lex(r#"[1, 2, 3]"#),
3686 vec![
3687 Token::LBracket,
3688 Token::Int(1),
3689 Token::Comma,
3690 Token::Int(2),
3691 Token::Comma,
3692 Token::Int(3),
3693 Token::RBracket
3694 ]
3695 );
3696 }
3697
3698 #[test]
3699 fn json_object() {
3700 assert_eq!(
3701 lex(r#"{"key": "value"}"#),
3702 vec![
3703 Token::LBrace,
3704 Token::String("key".to_string()),
3705 Token::Colon,
3706 Token::String("value".to_string()),
3707 Token::RBrace
3708 ]
3709 );
3710 }
3711
3712 #[test]
3713 fn redirect_operators() {
3714 assert_eq!(
3715 lex("cmd > file"),
3716 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
3717 );
3718 assert_eq!(
3719 lex("cmd >> file"),
3720 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
3721 );
3722 assert_eq!(
3723 lex("cmd 2> err"),
3724 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
3725 );
3726 assert_eq!(
3727 lex("cmd &> all"),
3728 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
3729 );
3730 }
3731
3732 #[test]
3733 fn background_job() {
3734 assert_eq!(
3735 lex("cmd &"),
3736 vec![Token::Ident("cmd".to_string()), Token::Amp]
3737 );
3738 }
3739
3740 #[test]
3741 fn command_substitution() {
3742 assert_eq!(
3743 lex("$(cmd)"),
3744 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
3745 );
3746 assert_eq!(
3747 lex("$(cmd arg)"),
3748 vec![
3749 Token::CmdSubstStart,
3750 Token::Ident("cmd".to_string()),
3751 Token::Ident("arg".to_string()),
3752 Token::RParen
3753 ]
3754 );
3755 assert_eq!(
3756 lex("$(a | b)"),
3757 vec![
3758 Token::CmdSubstStart,
3759 Token::Ident("a".to_string()),
3760 Token::Pipe,
3761 Token::Ident("b".to_string()),
3762 Token::RParen
3763 ]
3764 );
3765 }
3766
3767 #[test]
3768 fn complex_pipeline() {
3769 assert_eq!(
3770 lex(r#"cat file | grep pattern="foo" | head count=10"#),
3771 vec![
3772 Token::Ident("cat".to_string()),
3773 Token::Ident("file".to_string()),
3774 Token::Pipe,
3775 Token::Ident("grep".to_string()),
3776 Token::Ident("pattern".to_string()),
3777 Token::Eq,
3778 Token::String("foo".to_string()),
3779 Token::Pipe,
3780 Token::Ident("head".to_string()),
3781 Token::Ident("count".to_string()),
3782 Token::Eq,
3783 Token::Int(10),
3784 ]
3785 );
3786 }
3787
3788 #[test]
3793 fn short_flag() {
3794 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3795 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
3796 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
3797 }
3798
3799 #[test]
3800 fn short_flag_combined() {
3801 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
3803 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
3804 }
3805
3806 #[test]
3807 fn job_spec_lexes_as_one_token() {
3808 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
3810 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
3811 assert_eq!(
3812 lex("wait %1 %2"),
3813 vec![
3814 Token::Ident("wait".to_string()),
3815 Token::JobSpec("%1".to_string()),
3816 Token::JobSpec("%2".to_string()),
3817 ]
3818 );
3819 }
3820
3821 #[test]
3822 fn short_flag_with_internal_hyphens_is_one_token() {
3823 assert_eq!(
3827 lex("-not-a-flag"),
3828 vec![Token::ShortFlag("not-a-flag".to_string())]
3829 );
3830 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3834 assert_eq!(lex("-"), vec![Token::MinusAlone]);
3835 }
3836
3837 #[test]
3838 fn long_flag() {
3839 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
3840 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
3841 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
3842 }
3843
3844 #[test]
3845 fn double_dash() {
3846 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3848 }
3849
3850 #[test]
3851 fn flags_vs_negative_numbers() {
3852 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3854 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3856 assert_eq!(
3859 lex("-1 a"),
3860 vec![Token::Int(-1), Token::Ident("a".to_string())]
3861 );
3862 }
3863
3864 #[test]
3865 fn command_with_flags() {
3866 assert_eq!(
3867 lex("ls -l"),
3868 vec![
3869 Token::Ident("ls".to_string()),
3870 Token::ShortFlag("l".to_string()),
3871 ]
3872 );
3873 assert_eq!(
3874 lex("git commit -m"),
3875 vec![
3876 Token::Ident("git".to_string()),
3877 Token::Ident("commit".to_string()),
3878 Token::ShortFlag("m".to_string()),
3879 ]
3880 );
3881 assert_eq!(
3882 lex("git push --force"),
3883 vec![
3884 Token::Ident("git".to_string()),
3885 Token::Ident("push".to_string()),
3886 Token::LongFlag("force".to_string()),
3887 ]
3888 );
3889 }
3890
3891 #[test]
3892 fn flag_with_value() {
3893 assert_eq!(
3894 lex(r#"git commit -m "message""#),
3895 vec![
3896 Token::Ident("git".to_string()),
3897 Token::Ident("commit".to_string()),
3898 Token::ShortFlag("m".to_string()),
3899 Token::String("message".to_string()),
3900 ]
3901 );
3902 assert_eq!(
3903 lex(r#"--message="hello""#),
3904 vec![
3905 Token::LongFlag("message".to_string()),
3906 Token::Eq,
3907 Token::String("hello".to_string()),
3908 ]
3909 );
3910 }
3911
3912 #[test]
3913 fn end_of_flags_marker() {
3914 assert_eq!(
3915 lex("git checkout -- file"),
3916 vec![
3917 Token::Ident("git".to_string()),
3918 Token::Ident("checkout".to_string()),
3919 Token::DoubleDash,
3920 Token::Ident("file".to_string()),
3921 ]
3922 );
3923 }
3924
3925 #[test]
3930 fn local_keyword() {
3931 assert_eq!(lex("local"), vec![Token::Local]);
3932 assert_eq!(
3933 lex("local X = 5"),
3934 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3935 );
3936 }
3937
3938 #[test]
3939 fn simple_var_ref() {
3940 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
3941 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
3942 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
3943 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
3944 }
3945
3946 #[test]
3947 fn simple_var_ref_in_command() {
3948 assert_eq!(
3949 lex("echo $NAME"),
3950 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
3951 );
3952 }
3953
3954 #[test]
3955 fn single_quoted_strings() {
3956 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
3957 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
3958 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
3959 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
3961 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
3962 }
3963
3964 #[test]
3965 fn test_brackets() {
3966 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
3968 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
3969 assert_eq!(
3970 lex("[[ -f file ]]"),
3971 vec![
3972 Token::LBracket,
3973 Token::LBracket,
3974 Token::ShortFlag("f".to_string()),
3975 Token::Ident("file".to_string()),
3976 Token::RBracket,
3977 Token::RBracket
3978 ]
3979 );
3980 }
3981
3982 #[test]
3983 fn test_expression_syntax() {
3984 assert_eq!(
3985 lex(r#"[[ $X == "value" ]]"#),
3986 vec![
3987 Token::LBracket,
3988 Token::LBracket,
3989 Token::SimpleVarRef("X".to_string()),
3990 Token::EqEq,
3991 Token::String("value".to_string()),
3992 Token::RBracket,
3993 Token::RBracket
3994 ]
3995 );
3996 }
3997
3998 #[test]
3999 fn bash_style_assignment() {
4000 assert_eq!(
4002 lex(r#"NAME="value""#),
4003 vec![
4004 Token::Ident("NAME".to_string()),
4005 Token::Eq,
4006 Token::String("value".to_string())
4007 ]
4008 );
4009 }
4010
4011 #[test]
4012 fn positional_params() {
4013 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
4014 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
4015 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
4016 assert_eq!(lex("$@"), vec![Token::AllArgs]);
4017 assert_eq!(lex("$#"), vec![Token::ArgCount]);
4018 }
4019
4020 #[test]
4021 fn positional_in_context() {
4022 assert_eq!(
4023 lex("echo $1 $2"),
4024 vec![
4025 Token::Ident("echo".to_string()),
4026 Token::Positional(1),
4027 Token::Positional(2),
4028 ]
4029 );
4030 }
4031
4032 #[test]
4033 fn var_length() {
4034 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
4035 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
4036 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
4037 }
4038
4039 #[test]
4040 fn var_length_with_subscript() {
4041 assert_eq!(lex("${#u[tags]}"), vec![Token::VarLength("u[tags]".to_string())]);
4044 assert_eq!(lex("${#a[0]}"), vec![Token::VarLength("a[0]".to_string())]);
4045 assert_eq!(lex("${#a[b][c]}"), vec![Token::VarLength("a[b][c]".to_string())]);
4046 assert_eq!(lex("${#r[$k]}"), vec![Token::VarLength("r[$k]".to_string())]);
4047 }
4048
4049 #[test]
4050 fn var_length_in_context() {
4051 assert_eq!(
4052 lex("echo ${#NAME}"),
4053 vec![
4054 Token::Ident("echo".to_string()),
4055 Token::VarLength("NAME".to_string()),
4056 ]
4057 );
4058 }
4059
4060 #[test]
4065 fn plus_flag() {
4066 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
4068 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
4069 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
4070 }
4071
4072 #[test]
4073 fn set_with_plus_flag() {
4074 assert_eq!(
4075 lex("set +e"),
4076 vec![
4077 Token::Set,
4078 Token::PlusFlag("e".to_string()),
4079 ]
4080 );
4081 }
4082
4083 #[test]
4084 fn set_with_multiple_flags() {
4085 assert_eq!(
4086 lex("set -e -u"),
4087 vec![
4088 Token::Set,
4089 Token::ShortFlag("e".to_string()),
4090 Token::ShortFlag("u".to_string()),
4091 ]
4092 );
4093 }
4094
4095 #[test]
4096 fn flags_vs_negative_numbers_edge_cases() {
4097 assert_eq!(
4099 lex("-1 a"),
4100 vec![Token::Int(-1), Token::Ident("a".to_string())]
4101 );
4102 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
4104 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
4106 }
4107
4108 #[test]
4109 fn single_dash_is_minus_alone() {
4110 let result = tokenize("-").expect("should lex");
4112 assert_eq!(result.len(), 1);
4113 assert!(matches!(result[0].token, Token::MinusAlone));
4114 }
4115
4116 #[test]
4117 fn plus_bare_for_date_format() {
4118 let result = tokenize("+%s").expect("should lex");
4120 assert_eq!(result.len(), 1);
4121 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
4122
4123 let result = tokenize("+%Y-%m-%d").expect("should lex");
4125 assert_eq!(result.len(), 1);
4126 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
4127 }
4128
4129 #[test]
4130 fn plus_flag_still_works() {
4131 let result = tokenize("+e").expect("should lex");
4133 assert_eq!(result.len(), 1);
4134 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
4135 }
4136
4137 #[test]
4138 fn while_keyword_vs_while_loop() {
4139 assert_eq!(lex("while"), vec![Token::While]);
4141 assert_eq!(
4143 lex("while true"),
4144 vec![Token::While, Token::True]
4145 );
4146 }
4147
4148 #[test]
4149 fn control_flow_keywords() {
4150 assert_eq!(lex("break"), vec![Token::Break]);
4151 assert_eq!(lex("continue"), vec![Token::Continue]);
4152 assert_eq!(lex("return"), vec![Token::Return]);
4153 assert_eq!(lex("exit"), vec![Token::Exit]);
4154 }
4155
4156 #[test]
4157 fn control_flow_with_numbers() {
4158 assert_eq!(
4159 lex("break 2"),
4160 vec![Token::Break, Token::Int(2)]
4161 );
4162 assert_eq!(
4163 lex("continue 3"),
4164 vec![Token::Continue, Token::Int(3)]
4165 );
4166 assert_eq!(
4167 lex("exit 1"),
4168 vec![Token::Exit, Token::Int(1)]
4169 );
4170 }
4171
4172 #[test]
4177 fn heredoc_simple() {
4178 let source = "cat <<EOF\nhello\nworld\nEOF";
4179 let tokens = lex(source);
4180 assert_eq!(tokens, vec![
4182 Token::Ident("cat".to_string()),
4183 Token::HereDocStart,
4184 Token::HereDoc(HereDocData {
4185 content: "hello\nworld\n".to_string(),
4186 literal: false,
4187 strip_tabs: false,
4188 body_start_offset: 10,
4189 }),
4190 Token::Newline,
4191 ]);
4192 }
4193
4194 #[test]
4195 fn heredoc_empty() {
4196 let source = "cat <<EOF\nEOF";
4197 let tokens = lex(source);
4198 assert_eq!(tokens, vec![
4199 Token::Ident("cat".to_string()),
4200 Token::HereDocStart,
4201 Token::HereDoc(HereDocData {
4202 content: "".to_string(),
4203 literal: false,
4204 strip_tabs: false,
4205 body_start_offset: 10,
4206 }),
4207 Token::Newline,
4208 ]);
4209 }
4210
4211 #[test]
4212 fn heredoc_with_special_chars() {
4213 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
4214 let tokens = lex(source);
4215 assert_eq!(tokens, vec![
4216 Token::Ident("cat".to_string()),
4217 Token::HereDocStart,
4218 Token::HereDoc(HereDocData {
4219 content: "$VAR and \"quoted\" 'single'\n".to_string(),
4220 literal: false,
4221 strip_tabs: false,
4222 body_start_offset: 10,
4223 }),
4224 Token::Newline,
4225 ]);
4226 }
4227
4228 #[test]
4229 fn heredoc_multiline() {
4230 let source = "cat <<END\nline1\nline2\nline3\nEND";
4231 let tokens = lex(source);
4232 assert_eq!(tokens, vec![
4233 Token::Ident("cat".to_string()),
4234 Token::HereDocStart,
4235 Token::HereDoc(HereDocData {
4236 content: "line1\nline2\nline3\n".to_string(),
4237 literal: false,
4238 strip_tabs: false,
4239 body_start_offset: 10,
4240 }),
4241 Token::Newline,
4242 ]);
4243 }
4244
4245 #[test]
4246 fn heredoc_in_command() {
4247 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
4248 let tokens = lex(source);
4249 assert_eq!(tokens, vec![
4250 Token::Ident("cat".to_string()),
4251 Token::HereDocStart,
4252 Token::HereDoc(HereDocData {
4253 content: "hello\n".to_string(),
4254 literal: false,
4255 strip_tabs: false,
4256 body_start_offset: 10,
4257 }),
4258 Token::Newline,
4259 Token::Ident("echo".to_string()),
4260 Token::Ident("goodbye".to_string()),
4261 ]);
4262 }
4263
4264 #[test]
4265 fn heredoc_strip_tabs() {
4266 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
4267 let tokens = lex(source);
4268 assert_eq!(tokens, vec![
4272 Token::Ident("cat".to_string()),
4273 Token::HereDocStart,
4274 Token::HereDoc(HereDocData {
4275 content: "\thello\n\tworld\n".to_string(),
4276 literal: false,
4277 strip_tabs: true,
4278 body_start_offset: 11,
4279 }),
4280 Token::Newline,
4281 ]);
4282 }
4283
4284 #[test]
4289 fn arithmetic_simple() {
4290 let source = "$((1 + 2))";
4291 let tokens = lex(source);
4292 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
4293 }
4294
4295 #[test]
4296 fn arithmetic_in_assignment() {
4297 let source = "X=$((5 * 3))";
4298 let tokens = lex(source);
4299 assert_eq!(tokens, vec![
4300 Token::Ident("X".to_string()),
4301 Token::Eq,
4302 Token::Arithmetic("5 * 3".to_string()),
4303 ]);
4304 }
4305
4306 #[test]
4307 fn arithmetic_with_nested_parens() {
4308 let source = "$((2 * (3 + 4)))";
4309 let tokens = lex(source);
4310 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
4311 }
4312
4313 #[test]
4314 fn arithmetic_with_variable() {
4315 let source = "$((X + 1))";
4316 let tokens = lex(source);
4317 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
4318 }
4319
4320 #[test]
4321 fn arithmetic_command_subst_not_confused() {
4322 let source = "$(echo hello)";
4324 let tokens = lex(source);
4325 assert_eq!(tokens, vec![
4326 Token::CmdSubstStart,
4327 Token::Ident("echo".to_string()),
4328 Token::Ident("hello".to_string()),
4329 Token::RParen,
4330 ]);
4331 }
4332
4333 #[test]
4334 fn arithmetic_nesting_limit() {
4335 let open_parens = "(".repeat(300);
4337 let close_parens = ")".repeat(300);
4338 let source = format!("$(({}1{}))", open_parens, close_parens);
4339 let result = tokenize(&source);
4340 assert!(result.is_err());
4341 let errors = result.unwrap_err();
4342 assert_eq!(errors.len(), 1);
4343 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
4344 }
4345
4346 #[test]
4347 fn arithmetic_nesting_within_limit() {
4348 let source = "$((((1 + 2) * 3)))";
4350 let tokens = lex(source);
4351 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
4352 }
4353
4354 #[test]
4366 fn arithmetic_after_apostrophe_in_comment() {
4367 let source = "# this doesn't work\necho $((1+2))";
4370 let tokens = lex(source);
4371 assert_eq!(tokens, vec![
4372 Token::Newline,
4373 Token::Ident("echo".to_string()),
4374 Token::Arithmetic("1+2".to_string()),
4375 ]);
4376 }
4377
4378 #[test]
4379 fn arithmetic_inside_comment_is_not_expanded() {
4380 let source = "# the $((y)) syntax explained\necho hello";
4382 let tokens = lex(source);
4383 assert_eq!(tokens, vec![
4384 Token::Newline,
4385 Token::Ident("echo".to_string()),
4386 Token::Ident("hello".to_string()),
4387 ]);
4388 }
4389
4390 #[test]
4391 fn backticked_arithmetic_in_comment_is_not_expanded() {
4392 let source = "# the `$((x))` syntax explained\necho $((3+4))";
4396 let tokens = lex(source);
4397 assert_eq!(tokens, vec![
4398 Token::Newline,
4399 Token::Ident("echo".to_string()),
4400 Token::Arithmetic("3+4".to_string()),
4401 ]);
4402 }
4403
4404 #[test]
4405 fn arithmetic_still_works_outside_comments() {
4406 let source = "X=$((1+2)); Y=$((3*4))";
4409 let tokens = lex(source);
4410 assert_eq!(tokens, vec![
4411 Token::Ident("X".to_string()),
4412 Token::Eq,
4413 Token::Arithmetic("1+2".to_string()),
4414 Token::Semi,
4415 Token::Ident("Y".to_string()),
4416 Token::Eq,
4417 Token::Arithmetic("3*4".to_string()),
4418 ]);
4419 }
4420
4421 #[test]
4422 fn arithmetic_inside_double_quotes_still_expands() {
4423 let source = "echo \"# $((1+2))\"";
4426 let tokens = lex(source);
4427 assert_eq!(tokens.len(), 2);
4432 assert!(matches!(tokens[0], Token::Ident(_)));
4433 assert!(matches!(tokens[1], Token::String(_)));
4434 }
4435
4436 #[test]
4449 fn backtick_in_source_is_rejected() {
4450 let result = tokenize("echo `date`");
4451 assert!(result.is_err());
4452 let errors = result.unwrap_err();
4453 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
4454 }
4455
4456 #[test]
4457 fn backtick_in_comment_is_just_comment_text() {
4458 let source = "# use `date` here\necho hi";
4461 let tokens = lex(source);
4462 assert_eq!(tokens, vec![
4463 Token::Newline,
4464 Token::Ident("echo".to_string()),
4465 Token::Ident("hi".to_string()),
4466 ]);
4467 }
4468
4469 #[test]
4470 fn backtick_in_single_quoted_string_is_literal() {
4471 let source = "echo '`date`'";
4474 let tokens = lex(source);
4475 assert_eq!(tokens, vec![
4476 Token::Ident("echo".to_string()),
4477 Token::SingleString("`date`".to_string()),
4478 ]);
4479 }
4480
4481 #[test]
4482 fn backtick_in_double_quoted_string_is_literal() {
4483 let source = "echo \"`date`\"";
4488 let tokens = lex(source);
4489 assert_eq!(tokens.len(), 2);
4490 assert!(matches!(tokens[0], Token::Ident(_)));
4491 match &tokens[1] {
4492 Token::String(s) => assert!(s.contains('`')),
4493 other => panic!("expected Token::String, got {:?}", other),
4494 }
4495 }
4496
4497 #[test]
4498 fn backtick_in_heredoc_body_is_preserved() {
4499 let source = "cat <<EOF\n`date`\nEOF\n";
4502 let tokens = lex(source);
4503 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
4504 assert!(heredoc.is_some(), "expected a HereDoc token");
4505 if let Some(Token::HereDoc(d)) = heredoc {
4506 assert!(d.content.contains('`'));
4507 }
4508 }
4509
4510 #[test]
4515 fn token_categories() {
4516 assert_eq!(Token::If.category(), TokenCategory::Keyword);
4518 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
4519 assert_eq!(Token::For.category(), TokenCategory::Keyword);
4520 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
4521 assert_eq!(Token::True.category(), TokenCategory::Keyword);
4522 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
4523
4524 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
4526 assert_eq!(Token::And.category(), TokenCategory::Operator);
4527 assert_eq!(Token::Or.category(), TokenCategory::Operator);
4528 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
4529 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
4530
4531 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
4533 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
4534 assert_eq!(
4535 Token::HereDoc(HereDocData {
4536 content: "test".to_string(),
4537 literal: false,
4538 strip_tabs: false,
4539 body_start_offset: 0,
4540 }).category(),
4541 TokenCategory::String,
4542 );
4543
4544 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
4546 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
4547 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
4548
4549 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
4551 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
4552 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
4553 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
4554 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
4555 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
4556 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
4557
4558 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
4560 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
4561 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
4562 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
4563
4564 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
4566 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
4567 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
4568 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
4569
4570 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
4572
4573 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
4575
4576 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
4578 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
4579 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
4580
4581 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
4583 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
4584 }
4585
4586 #[test]
4587 fn test_heredoc_piped_to_command() {
4588 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
4591 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
4592 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
4593 assert!(heredoc_pos.is_some(), "should have a heredoc token");
4594 assert!(pipe_pos.is_some(), "should have a pipe token");
4595 assert!(
4596 pipe_pos.unwrap() > heredoc_pos.unwrap(),
4597 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
4598 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
4599 );
4600 }
4601
4602 #[test]
4603 fn test_heredoc_standalone_still_works() {
4604 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
4606 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
4607 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
4608 }
4609
4610 #[test]
4611 fn test_heredoc_preserves_leading_empty_lines() {
4612 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
4614 let heredoc = tokens.iter().find_map(|t| {
4615 if let Token::HereDoc(data) = &t.token {
4616 Some(data.clone())
4617 } else {
4618 None
4619 }
4620 });
4621 assert!(heredoc.is_some(), "should have a heredoc token");
4622 let data = heredoc.unwrap();
4623 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
4624 assert_eq!(data.content, "\nhello\n");
4625 }
4626
4627 #[test]
4628 fn test_heredoc_quoted_delimiter_sets_literal() {
4629 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
4631 let heredoc = tokens.iter().find_map(|t| {
4632 if let Token::HereDoc(data) = &t.token {
4633 Some(data.clone())
4634 } else {
4635 None
4636 }
4637 });
4638 assert!(heredoc.is_some(), "should have a heredoc token");
4639 let data = heredoc.unwrap();
4640 assert!(data.literal, "quoted delimiter should set literal=true");
4641 assert_eq!(data.content, "hello $HOME\n");
4642 }
4643
4644 #[test]
4645 fn test_heredoc_unquoted_delimiter_not_literal() {
4646 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
4648 let heredoc = tokens.iter().find_map(|t| {
4649 if let Token::HereDoc(data) = &t.token {
4650 Some(data.clone())
4651 } else {
4652 None
4653 }
4654 });
4655 assert!(heredoc.is_some(), "should have a heredoc token");
4656 let data = heredoc.unwrap();
4657 assert!(!data.literal, "unquoted delimiter should have literal=false");
4658 }
4659
4660 #[test]
4665 fn colon_double_in_word() {
4666 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
4667 }
4668
4669 #[test]
4670 fn colon_single_in_word() {
4671 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
4672 }
4673
4674 #[test]
4675 fn colon_with_port() {
4676 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
4677 }
4678
4679 #[test]
4680 fn colon_standalone() {
4681 assert_eq!(lex(":"), vec![Token::Colon]);
4682 }
4683
4684 #[test]
4685 fn colon_spaced_no_merge() {
4686 assert_eq!(
4687 lex("foo : bar"),
4688 vec![
4689 Token::Ident("foo".into()),
4690 Token::Colon,
4691 Token::Ident("bar".into()),
4692 ]
4693 );
4694 }
4695
4696 #[test]
4697 fn colon_in_command_arg() {
4698 assert_eq!(
4699 lex("echo foo::bar"),
4700 vec![
4701 Token::Ident("echo".into()),
4702 Token::Ident("foo::bar".into()),
4703 ]
4704 );
4705 }
4706
4707 #[test]
4708 fn colon_trailing() {
4709 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
4711 }
4712
4713 #[test]
4714 fn colon_leading() {
4715 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
4717 }
4718
4719 #[test]
4720 fn colon_with_path() {
4721 assert_eq!(
4723 lex("/usr/bin:8080"),
4724 vec![Token::Ident("/usr/bin:8080".into())]
4725 );
4726 }
4727
4728 #[test]
4733 fn is_keyword_covers_control_flow() {
4734 for t in [
4735 Token::While,
4736 Token::Return,
4737 Token::Break,
4738 Token::Continue,
4739 Token::Exit,
4740 ] {
4741 assert!(t.is_keyword(), "{t:?} should be a keyword");
4742 }
4743 }
4744
4745 #[test]
4746 fn starts_statement_covers_while() {
4747 assert!(Token::While.starts_statement());
4748 }
4749
4750 #[test]
4751 fn is_keyword_rejects_operators() {
4752 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
4753 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
4754 }
4755 }
4756
4757 #[test]
4768 fn bare_comma_run_folds_to_ident() {
4769 assert_eq!(lex("1,3p"), vec![Token::Ident("1,3p".into())]);
4772 assert_eq!(lex("1,3"), vec![Token::Ident("1,3".into())]);
4773 assert_eq!(lex("2,2n"), vec![Token::Ident("2,2n".into())]);
4774 assert_eq!(lex("a,b"), vec![Token::Ident("a,b".into())]);
4775 assert_eq!(lex("1,2,3"), vec![Token::Ident("1,2,3".into())]);
4776 }
4777
4778 #[test]
4779 fn standalone_comma_stays_a_token() {
4780 assert_eq!(
4783 lex("cut -d , -f2"),
4784 vec![
4785 Token::Ident("cut".into()),
4786 Token::ShortFlag("d".into()),
4787 Token::Comma,
4788 Token::ShortFlag("f2".into()),
4789 ]
4790 );
4791 }
4792
4793 #[test]
4794 fn case_pattern_brace_comma_stays_significant() {
4795 assert_eq!(
4799 lex("{js,ts}"),
4800 vec![
4801 Token::LBrace,
4802 Token::Ident("js".into()),
4803 Token::Comma,
4804 Token::Ident("ts".into()),
4805 Token::RBrace,
4806 ]
4807 );
4808 }
4809
4810 #[test]
4811 fn glob_brace_expansion_with_star_still_fuses() {
4812 assert_eq!(lex("*.{js,ts}"), vec![Token::GlobWord("*.{js,ts}".into())]);
4816 assert_eq!(
4817 lex("src/*.{rs,toml}"),
4818 vec![Token::GlobWord("src/*.{rs,toml}".into())]
4819 );
4820 }
4821
4822 #[test]
4823 fn bracket_list_with_spaces_keeps_comma_significant() {
4824 assert_eq!(
4831 lex("[1, 2, 3]"),
4832 vec![
4833 Token::LBracket,
4834 Token::Int(1),
4835 Token::Comma,
4836 Token::Int(2),
4837 Token::Comma,
4838 Token::Int(3),
4839 Token::RBracket,
4840 ]
4841 );
4842 }
4843
4844 #[test]
4853 fn nested_list_of_lists_keeps_commas_significant() {
4854 assert_eq!(
4855 lex("x=[[1,2],[3,4]]"),
4856 vec![
4857 Token::Ident("x".into()),
4858 Token::Eq,
4859 Token::LBracket,
4860 Token::LBracket,
4861 Token::Int(1),
4862 Token::Comma,
4863 Token::Int(2),
4864 Token::RBracket,
4865 Token::Comma,
4866 Token::LBracket,
4867 Token::Int(3),
4868 Token::Comma,
4869 Token::Int(4),
4870 Token::RBracket,
4871 Token::RBracket,
4872 ]
4873 );
4874 }
4875
4876 #[test]
4877 fn nested_record_in_list_keeps_commas_significant() {
4878 assert_eq!(
4879 lex("x=[{a:1},{b:2}]"),
4880 vec![
4881 Token::Ident("x".into()),
4882 Token::Eq,
4883 Token::LBracket,
4884 Token::LBrace,
4885 Token::Ident("a".into()),
4886 Token::Colon,
4887 Token::Int(1),
4888 Token::RBrace,
4889 Token::Comma,
4890 Token::LBrace,
4891 Token::Ident("b".into()),
4892 Token::Colon,
4893 Token::Int(2),
4894 Token::RBrace,
4895 Token::RBracket,
4896 ]
4897 );
4898 }
4899
4900 #[test]
4901 fn stray_unclosed_bracket_does_not_wedge_past_the_line() {
4902 assert_eq!(
4909 lex("[dog\nsed -n 1,3p"),
4910 vec![
4911 Token::LBracket,
4912 Token::Ident("dog".into()),
4913 Token::Newline,
4914 Token::Ident("sed".into()),
4915 Token::ShortFlag("n".into()),
4916 Token::Ident("1,3p".into()),
4917 ]
4918 );
4919 }
4920
4921 #[test]
4922 fn stray_unmatched_closing_bracket_does_not_underflow() {
4923 assert_eq!(lex("]a,b"), vec![Token::Ident("]a,b".into())]);
4932 }
4933
4934 #[test]
4935 fn comma_in_double_quoted_string_is_string_content() {
4936 assert_eq!(lex(r#""a,b""#), vec![Token::String("a,b".into())]);
4939 }
4940
4941 #[test]
4942 fn comma_in_single_quoted_string_is_string_content() {
4943 assert_eq!(lex("'a,b'"), vec![Token::SingleString("a,b".into())]);
4944 }
4945
4946 #[test]
4947 fn comma_in_var_ref_braces_is_not_tokenized_separately() {
4948 assert_eq!(
4952 lex("${X:-1,3}"),
4953 vec![Token::VarRef("${X:-1,3}".into())]
4954 );
4955 }
4956
4957 #[test]
4958 fn comma_inside_cmd_subst_folds_like_top_level() {
4959 assert_eq!(
4963 lex("$(sed -n 1,3p)"),
4964 vec![
4965 Token::CmdSubstStart,
4966 Token::Ident("sed".into()),
4967 Token::ShortFlag("n".into()),
4968 Token::Ident("1,3p".into()),
4969 Token::RParen,
4970 ]
4971 );
4972 }
4973
4974 #[test]
4975 fn non_comma_glued_pasting_is_unaffected() {
4976 assert_eq!(
4982 lex("--flag$(echo x)"),
4983 vec![
4984 Token::LongFlag("flag".into()),
4985 Token::CmdSubstStart,
4986 Token::Ident("echo".into()),
4987 Token::Ident("x".into()),
4988 Token::RParen,
4989 ]
4990 );
4991 }
4992}