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 mut result = Vec::with_capacity(tokens.len());
2757 let mut run: Vec<&Spanned<Token>> = Vec::new();
2758 let mut run_start = 0usize;
2759
2760 for (idx, token) in tokens.iter().enumerate() {
2761 if run.is_empty() {
2762 if is_glob_mergeable(&token.token) {
2763 run.push(token);
2764 run_start = idx;
2765 } else {
2766 result.push(token.clone());
2767 }
2768 continue;
2769 }
2770
2771 let Some(last) = run.last() else { unreachable!() };
2773 let adjacent = last.span.end == token.span.start;
2774
2775 if adjacent && is_glob_mergeable(&token.token) {
2776 run.push(token);
2777 } else {
2778 let followed_by_eq = matches!(token.token, Token::Eq);
2782 flush_glob_run(
2783 &mut run,
2784 &mut result,
2785 value_ctx[run_start].in_literal,
2786 followed_by_eq,
2787 value_ctx[run_start].push_target,
2788 source,
2789 );
2790 if is_glob_mergeable(&token.token) {
2791 run.push(token);
2792 run_start = idx;
2793 } else {
2794 result.push(token.clone());
2795 }
2796 }
2797 }
2798
2799 flush_glob_run(
2803 &mut run,
2804 &mut result,
2805 value_ctx[run_start].in_literal,
2806 false,
2807 value_ctx[run_start].push_target,
2808 source,
2809 );
2810
2811 result
2812}
2813
2814fn flush_glob_run(
2834 run: &mut Vec<&Spanned<Token>>,
2835 result: &mut Vec<Spanned<Token>>,
2836 value_position_suppress: bool,
2837 followed_by_eq: bool,
2838 push_target: bool,
2839 source: &str,
2840) {
2841 if run.is_empty() {
2842 return;
2843 }
2844
2845 let has_bracket_pair = run.iter().any(|t| matches!(t.token, Token::LBracket))
2846 && run.iter().any(|t| matches!(t.token, Token::RBracket));
2847 let has_star_or_question = run
2848 .iter()
2849 .any(|t| matches!(t.token, Token::Star | Token::Question));
2850 let has_glob = has_star_or_question || has_bracket_pair;
2851
2852 let run_starts_with_ident = matches!(run.first().map(|t| &t.token), Some(Token::Ident(_)));
2858 let lvalue_suppress =
2859 followed_by_eq && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2860 let push_target_suppress =
2861 push_target && has_bracket_pair && !has_star_or_question && run_starts_with_ident;
2862 let suppress = (value_position_suppress && has_bracket_pair) || lvalue_suppress;
2863
2864 if push_target_suppress && run.len() >= 2 {
2865 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2868 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2869 let text = source.get(start..end).unwrap_or_default().to_string();
2870 result.push(Spanned::new(Token::Ident(text), start..end));
2871 } else if !suppress && run.len() >= 2 && has_glob {
2872 let start = run.first().map(|t| t.span.start).unwrap_or(0);
2873 let end = run.last().map(|t| t.span.end).unwrap_or(0);
2874 let text = source.get(start..end).unwrap_or_default().to_string();
2875 result.push(Spanned::new(Token::GlobWord(text), start..end));
2876 } else {
2877 for t in run.iter() {
2878 result.push((*t).clone());
2879 }
2880 }
2881
2882 run.clear();
2883}
2884
2885pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2898 tokenize_impl(source, false)
2899}
2900
2901pub fn tokenize_with_comments(source: &str) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2907 tokenize_impl(source, true)
2908}
2909
2910fn tokenize_impl(
2911 source: &str,
2912 keep_comments: bool,
2913) -> Result<Vec<Spanned<Token>>, Vec<Spanned<LexerError>>> {
2914 let scan_output = scan(source).map_err(|e| vec![e])?;
2915
2916 debug_assert!(
2920 scan_output
2921 .replacements
2922 .windows(2)
2923 .all(|w| w[0].new_start <= w[1].new_start),
2924 "replacement table must be ordered by new_start"
2925 );
2926
2927 let mut tokens = Vec::new();
2928 let mut errors = Vec::new();
2929 for (result, span) in Token::lexer(&scan_output.text).spanned() {
2930 match result {
2931 Ok(token) => {
2932 if !keep_comments
2933 && matches!(token, Token::Comment | Token::LineContinuation)
2934 {
2935 continue;
2936 }
2937 tokens.push(Spanned::new(token, span));
2940 }
2941 Err(err) => {
2942 errors.push(Spanned::new(err, map_span(&span, &scan_output.replacements)));
2943 }
2944 }
2945 }
2946 if !errors.is_empty() {
2947 return Err(errors);
2948 }
2949
2950 let resolved = resolve_markers(tokens, &scan_output).map_err(|errs| {
2951 errs.into_iter()
2952 .map(|e| Spanned::new(e.token, map_span(&e.span, &scan_output.replacements)))
2953 .collect::<Vec<_>>()
2954 })?;
2955
2956 let mapped: Vec<Spanned<Token>> = resolved
2957 .into_iter()
2958 .map(|s| {
2959 let span = map_span(&s.span, &scan_output.replacements);
2960 Spanned::new(s.token, span)
2961 })
2962 .collect();
2963
2964 Ok(merge_glob_adjacent(
2965 merge_colon_adjacent(merge_flag_metachar_adjacent(mapped), source),
2966 source,
2967 ))
2968}
2969
2970pub fn parse_string_literal(source: &str) -> Result<String, LexerError> {
2972 if source.len() < 2 || !source.starts_with('"') || !source.ends_with('"') {
2974 return Err(LexerError::UnterminatedString);
2975 }
2976
2977 let inner = &source[1..source.len() - 1];
2978 let mut result = String::with_capacity(inner.len());
2979 let mut chars = inner.chars().peekable();
2980
2981 while let Some(ch) = chars.next() {
2982 if ch == '\\' {
2983 match chars.next() {
2984 Some('n') => result.push('\n'),
2985 Some('t') => result.push('\t'),
2986 Some('r') => result.push('\r'),
2987 Some('\\') => result.push('\\'),
2988 Some('"') => result.push('"'),
2989 Some('$') => result.push_str("__KAISH_ESCAPED_DOLLAR__"),
2992 Some('u') => {
2993 let mut hex = String::with_capacity(4);
2995 for _ in 0..4 {
2996 match chars.next() {
2997 Some(h) if h.is_ascii_hexdigit() => hex.push(h),
2998 _ => return Err(LexerError::InvalidEscape),
2999 }
3000 }
3001 let codepoint = u32::from_str_radix(&hex, 16)
3002 .map_err(|_| LexerError::InvalidEscape)?;
3003 let ch = char::from_u32(codepoint)
3004 .ok_or(LexerError::InvalidEscape)?;
3005 result.push(ch);
3006 }
3007 Some(next) => {
3009 result.push('\\');
3010 result.push(next);
3011 }
3012 None => return Err(LexerError::InvalidEscape),
3013 }
3014 } else {
3015 result.push(ch);
3016 }
3017 }
3018
3019 Ok(result)
3020}
3021
3022pub fn parse_var_ref(source: &str) -> Result<Vec<String>, LexerError> {
3033 if source.len() < 4 || !source.starts_with("${") || !source.ends_with('}') {
3035 return Err(LexerError::UnterminatedVarRef);
3036 }
3037
3038 let inner = &source[2..source.len() - 1];
3039
3040 if inner == "?" {
3042 return Ok(vec!["?".to_string()]);
3043 }
3044
3045 let mut segments = Vec::new();
3046 let mut current = String::new();
3047 let mut chars = inner.chars().peekable();
3048
3049 while let Some(ch) = chars.next() {
3050 match ch {
3051 '.' => {
3052 if !current.is_empty() {
3053 segments.push(current.clone());
3054 current.clear();
3055 }
3056 }
3057 '[' => {
3058 if !current.is_empty() {
3059 segments.push(current.clone());
3060 current.clear();
3061 }
3062 let mut index = String::from("[");
3067 if let Some("e) = chars.peek() {
3068 if quote == '"' || quote == '\'' {
3069 if let Some(q) = chars.next() {
3070 index.push(q);
3071 }
3072 for c in chars.by_ref() {
3073 index.push(c);
3074 if c == quote {
3075 break;
3076 }
3077 }
3078 }
3079 }
3080 while let Some(&c) = chars.peek() {
3081 if let Some(c) = chars.next() {
3082 index.push(c);
3083 }
3084 if c == ']' {
3085 break;
3086 }
3087 }
3088 segments.push(index);
3089 }
3090 _ => {
3091 current.push(ch);
3092 }
3093 }
3094 }
3095
3096 if !current.is_empty() {
3097 segments.push(current);
3098 }
3099
3100 Ok(segments)
3101}
3102
3103pub fn parse_int(source: &str) -> Result<i64, LexerError> {
3105 source.parse().map_err(|_| LexerError::InvalidNumber)
3106}
3107
3108pub fn parse_float(source: &str) -> Result<f64, LexerError> {
3110 source.parse().map_err(|_| LexerError::InvalidNumber)
3111}
3112
3113#[cfg(test)]
3114#[allow(clippy::approx_constant)]
3115mod tests {
3116 use super::*;
3117
3118 fn lex(source: &str) -> Vec<Token> {
3119 tokenize(source)
3120 .expect("lexer should succeed")
3121 .into_iter()
3122 .map(|s| s.token)
3123 .collect()
3124 }
3125
3126 #[test]
3131 fn keywords() {
3132 assert_eq!(lex("set"), vec![Token::Set]);
3133 assert_eq!(lex("if"), vec![Token::If]);
3134 assert_eq!(lex("then"), vec![Token::Then]);
3135 assert_eq!(lex("else"), vec![Token::Else]);
3136 assert_eq!(lex("elif"), vec![Token::Elif]);
3137 assert_eq!(lex("fi"), vec![Token::Fi]);
3138 assert_eq!(lex("for"), vec![Token::For]);
3139 assert_eq!(lex("in"), vec![Token::In]);
3140 assert_eq!(lex("do"), vec![Token::Do]);
3141 assert_eq!(lex("done"), vec![Token::Done]);
3142 assert_eq!(lex("case"), vec![Token::Case]);
3143 assert_eq!(lex("esac"), vec![Token::Esac]);
3144 assert_eq!(lex("function"), vec![Token::Function]);
3145 assert_eq!(lex("true"), vec![Token::True]);
3146 assert_eq!(lex("false"), vec![Token::False]);
3147 }
3148
3149 #[test]
3150 fn double_semicolon() {
3151 assert_eq!(lex(";;"), vec![Token::DoubleSemi]);
3152 assert_eq!(lex("echo \"hi\";;"), vec![
3154 Token::Ident("echo".to_string()),
3155 Token::String("hi".to_string()),
3156 Token::DoubleSemi,
3157 ]);
3158 }
3159
3160 #[test]
3161 fn type_keywords() {
3162 assert_eq!(lex("string"), vec![Token::TypeString]);
3163 assert_eq!(lex("int"), vec![Token::TypeInt]);
3164 assert_eq!(lex("float"), vec![Token::TypeFloat]);
3165 assert_eq!(lex("bool"), vec![Token::TypeBool]);
3166 }
3167
3168 #[test]
3173 fn single_char_operators() {
3174 assert_eq!(lex("="), vec![Token::Eq]);
3175 assert_eq!(lex("|"), vec![Token::Pipe]);
3176 assert_eq!(lex("&"), vec![Token::Amp]);
3177 assert_eq!(lex(">"), vec![Token::Gt]);
3178 assert_eq!(lex("<"), vec![Token::Lt]);
3179 assert_eq!(lex(";"), vec![Token::Semi]);
3180 assert_eq!(lex(":"), vec![Token::Colon]);
3181 assert_eq!(lex(","), vec![Token::Comma]);
3182 assert_eq!(lex("."), vec![Token::Dot]);
3183 }
3184
3185 #[test]
3186 fn multi_char_operators() {
3187 assert_eq!(lex("&&"), vec![Token::And]);
3188 assert_eq!(lex("||"), vec![Token::Or]);
3189 assert_eq!(lex("=="), vec![Token::EqEq]);
3190 assert_eq!(lex("!="), vec![Token::NotEq]);
3191 assert_eq!(lex("=~"), vec![Token::Match]);
3192 assert_eq!(lex("!~"), vec![Token::NotMatch]);
3193 assert_eq!(lex(">="), vec![Token::GtEq]);
3194 assert_eq!(lex("<="), vec![Token::LtEq]);
3195 assert_eq!(lex(">>"), vec![Token::GtGt]);
3196 assert_eq!(lex("2>"), vec![Token::Stderr]);
3197 assert_eq!(lex("&>"), vec![Token::Both]);
3198 }
3199
3200 #[test]
3201 fn brackets() {
3202 assert_eq!(lex("{"), vec![Token::LBrace]);
3203 assert_eq!(lex("}"), vec![Token::RBrace]);
3204 assert_eq!(lex("["), vec![Token::LBracket]);
3205 assert_eq!(lex("]"), vec![Token::RBracket]);
3206 assert_eq!(lex("("), vec![Token::LParen]);
3207 assert_eq!(lex(")"), vec![Token::RParen]);
3208 }
3209
3210 #[test]
3215 fn integers() {
3216 assert_eq!(lex("0"), vec![Token::Int(0)]);
3217 assert_eq!(lex("42"), vec![Token::Int(42)]);
3218 assert_eq!(lex("-1"), vec![Token::Int(-1)]);
3219 assert_eq!(lex("999999"), vec![Token::Int(999999)]);
3220 }
3221
3222 #[test]
3223 fn floats() {
3224 assert_eq!(lex("3.14"), vec![Token::Float(3.14)]);
3225 assert_eq!(lex("-0.5"), vec![Token::Float(-0.5)]);
3226 assert_eq!(lex("123.456"), vec![Token::Float(123.456)]);
3227 }
3228
3229 #[test]
3230 fn strings() {
3231 assert_eq!(lex(r#""hello""#), vec![Token::String("hello".to_string())]);
3232 assert_eq!(lex(r#""hello world""#), vec![Token::String("hello world".to_string())]);
3233 assert_eq!(lex(r#""""#), vec![Token::String("".to_string())]); assert_eq!(lex(r#""with \"quotes\"""#), vec![Token::String("with \"quotes\"".to_string())]);
3235 assert_eq!(lex(r#""with\nnewline""#), vec![Token::String("with\nnewline".to_string())]);
3236 }
3237
3238 #[test]
3239 fn var_refs() {
3240 assert_eq!(lex("${X}"), vec![Token::VarRef("${X}".to_string())]);
3241 assert_eq!(lex("${VAR}"), vec![Token::VarRef("${VAR}".to_string())]);
3242 assert_eq!(lex("${VAR.field}"), vec![Token::VarRef("${VAR.field}".to_string())]);
3243 assert_eq!(lex("${VAR[0]}"), vec![Token::VarRef("${VAR[0]}".to_string())]);
3244 }
3245
3246 #[test]
3247 fn var_ref_nested_default_is_one_token() {
3248 assert_eq!(
3252 lex("${X:-${Y}}"),
3253 vec![Token::VarRef("${X:-${Y}}".to_string())]
3254 );
3255 assert_eq!(
3256 lex("${A:-${B:-${C}}}"),
3257 vec![Token::VarRef("${A:-${B:-${C}}}".to_string())]
3258 );
3259 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3261 }
3262
3263 #[test]
3264 fn var_ref_unterminated_and_empty_are_errors() {
3265 assert!(tokenize("${X:-${Y}").is_err(), "unbalanced nesting is loud");
3266 assert!(tokenize("${a{b}").is_err(), "extra open brace is loud");
3267 assert!(tokenize("${}").is_err(), "empty reference is loud");
3268 }
3269
3270 #[test]
3271 fn var_ref_closes_at_first_balanced_brace() {
3272 assert_eq!(
3275 lex("${a}b}"),
3276 vec![
3277 Token::VarRef("${a}".to_string()),
3278 Token::Ident("b".to_string()),
3279 Token::RBrace,
3280 ]
3281 );
3282 }
3283
3284 #[test]
3289 fn identifiers() {
3290 assert_eq!(lex("foo"), vec![Token::Ident("foo".to_string())]);
3291 assert_eq!(lex("foo_bar"), vec![Token::Ident("foo_bar".to_string())]);
3292 assert_eq!(lex("foo-bar"), vec![Token::Ident("foo-bar".to_string())]);
3293 assert_eq!(lex("_private"), vec![Token::Ident("_private".to_string())]);
3294 assert_eq!(lex("cmd123"), vec![Token::Ident("cmd123".to_string())]);
3295 }
3296
3297 #[test]
3298 fn keyword_prefix_identifiers() {
3299 assert_eq!(lex("setup"), vec![Token::Ident("setup".to_string())]);
3301 assert_eq!(lex("kaish-tools"), vec![Token::Ident("kaish-tools".to_string())]);
3302 assert_eq!(lex("iffy"), vec![Token::Ident("iffy".to_string())]);
3303 assert_eq!(lex("forked"), vec![Token::Ident("forked".to_string())]);
3304 assert_eq!(lex("done-with-it"), vec![Token::Ident("done-with-it".to_string())]);
3305 }
3306
3307 #[test]
3312 fn assignment() {
3313 assert_eq!(
3314 lex("set X = 5"),
3315 vec![Token::Set, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3316 );
3317 }
3318
3319 #[test]
3320 fn command_simple() {
3321 assert_eq!(lex("echo"), vec![Token::Ident("echo".to_string())]);
3322 assert_eq!(
3323 lex(r#"echo "hello""#),
3324 vec![Token::Ident("echo".to_string()), Token::String("hello".to_string())]
3325 );
3326 }
3327
3328 #[test]
3329 fn command_with_args() {
3330 assert_eq!(
3331 lex("cmd arg1 arg2"),
3332 vec![Token::Ident("cmd".to_string()), Token::Ident("arg1".to_string()), Token::Ident("arg2".to_string())]
3333 );
3334 }
3335
3336 #[test]
3337 fn command_with_named_args() {
3338 assert_eq!(
3339 lex("cmd key=value"),
3340 vec![Token::Ident("cmd".to_string()), Token::Ident("key".to_string()), Token::Eq, Token::Ident("value".to_string())]
3341 );
3342 }
3343
3344 #[test]
3345 fn pipeline() {
3346 assert_eq!(
3347 lex("a | b | c"),
3348 vec![Token::Ident("a".to_string()), Token::Pipe, Token::Ident("b".to_string()), Token::Pipe, Token::Ident("c".to_string())]
3349 );
3350 }
3351
3352 #[test]
3353 fn if_statement() {
3354 assert_eq!(
3355 lex("if true; then echo; fi"),
3356 vec![
3357 Token::If,
3358 Token::True,
3359 Token::Semi,
3360 Token::Then,
3361 Token::Ident("echo".to_string()),
3362 Token::Semi,
3363 Token::Fi
3364 ]
3365 );
3366 }
3367
3368 #[test]
3369 fn for_loop() {
3370 assert_eq!(
3371 lex("for X in items; do echo; done"),
3372 vec![
3373 Token::For,
3374 Token::Ident("X".to_string()),
3375 Token::In,
3376 Token::Ident("items".to_string()),
3377 Token::Semi,
3378 Token::Do,
3379 Token::Ident("echo".to_string()),
3380 Token::Semi,
3381 Token::Done
3382 ]
3383 );
3384 }
3385
3386 #[test]
3391 fn whitespace_ignored() {
3392 assert_eq!(lex(" set X = 5 "), lex("set X = 5"));
3393 }
3394
3395 #[test]
3396 fn newlines_preserved() {
3397 let tokens = lex("a\nb");
3398 assert_eq!(
3399 tokens,
3400 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
3401 );
3402 }
3403
3404 #[test]
3405 fn multiple_newlines() {
3406 let tokens = lex("a\n\n\nb");
3407 assert_eq!(
3408 tokens,
3409 vec![Token::Ident("a".to_string()), Token::Newline, Token::Newline, Token::Newline, Token::Ident("b".to_string())]
3410 );
3411 }
3412
3413 #[test]
3418 fn comments_skipped() {
3419 assert_eq!(lex("# comment"), vec![]);
3420 assert_eq!(lex("a # comment"), vec![Token::Ident("a".to_string())]);
3421 assert_eq!(
3422 lex("a # comment\nb"),
3423 vec![Token::Ident("a".to_string()), Token::Newline, Token::Ident("b".to_string())]
3424 );
3425 }
3426
3427 #[test]
3428 fn comments_preserved_when_requested() {
3429 let tokens = tokenize_with_comments("a # comment")
3430 .expect("should succeed")
3431 .into_iter()
3432 .map(|s| s.token)
3433 .collect::<Vec<_>>();
3434 assert_eq!(tokens, vec![Token::Ident("a".to_string()), Token::Comment]);
3435 }
3436
3437 #[test]
3442 fn parse_simple_string() {
3443 assert_eq!(parse_string_literal(r#""hello""#).expect("ok"), "hello");
3444 }
3445
3446 #[test]
3447 fn parse_string_with_escapes() {
3448 assert_eq!(
3449 parse_string_literal(r#""hello\nworld""#).expect("ok"),
3450 "hello\nworld"
3451 );
3452 assert_eq!(
3453 parse_string_literal(r#""tab\there""#).expect("ok"),
3454 "tab\there"
3455 );
3456 assert_eq!(
3457 parse_string_literal(r#""quote\"here""#).expect("ok"),
3458 "quote\"here"
3459 );
3460 }
3461
3462 #[test]
3463 fn parse_string_with_unicode() {
3464 assert_eq!(
3465 parse_string_literal(r#""emoji \u2764""#).expect("ok"),
3466 "emoji ❤"
3467 );
3468 }
3469
3470 #[test]
3471 fn parse_string_with_escaped_dollar() {
3472 assert_eq!(
3475 parse_string_literal(r#""\$VAR""#).expect("ok"),
3476 "__KAISH_ESCAPED_DOLLAR__VAR"
3477 );
3478 assert_eq!(
3479 parse_string_literal(r#""cost: \$100""#).expect("ok"),
3480 "cost: __KAISH_ESCAPED_DOLLAR__100"
3481 );
3482 }
3483
3484 #[test]
3489 fn parse_simple_var() {
3490 assert_eq!(
3491 parse_var_ref("${X}").expect("ok"),
3492 vec!["X"]
3493 );
3494 }
3495
3496 #[test]
3497 fn parse_var_with_field() {
3498 assert_eq!(
3499 parse_var_ref("${VAR.field}").expect("ok"),
3500 vec!["VAR", "field"]
3501 );
3502 }
3503
3504 #[test]
3505 fn parse_var_with_index() {
3506 assert_eq!(
3507 parse_var_ref("${VAR[0]}").expect("ok"),
3508 vec!["VAR", "[0]"]
3509 );
3510 }
3511
3512 #[test]
3513 fn parse_var_nested() {
3514 assert_eq!(
3515 parse_var_ref("${VAR.field[0].nested}").expect("ok"),
3516 vec!["VAR", "field", "[0]", "nested"]
3517 );
3518 }
3519
3520 #[test]
3521 fn parse_last_result() {
3522 assert_eq!(
3523 parse_var_ref("${?}").expect("ok"),
3524 vec!["?"]
3525 );
3526 }
3527
3528 #[test]
3531 fn parse_var_quoted_subscript_with_embedded_bracket() {
3532 assert_eq!(
3533 parse_var_ref(r#"${r["weird]key"]}"#).expect("ok"),
3534 vec!["r", r#"["weird]key"]"#]
3535 );
3536 assert_eq!(
3537 parse_var_ref("${r['weird]key']}").expect("ok"),
3538 vec!["r", "['weird]key']"]
3539 );
3540 }
3541
3542 #[test]
3545 fn parse_var_quoted_subscript_without_embedded_bracket() {
3546 assert_eq!(
3547 parse_var_ref(r#"${r["normal"]}"#).expect("ok"),
3548 vec!["r", r#"["normal"]"#]
3549 );
3550 }
3551
3552 #[test]
3556 fn parse_var_quoted_subscript_with_embedded_bracket_then_more_path() {
3557 assert_eq!(
3558 parse_var_ref(r#"${r["weird]key"][0]}"#).expect("ok"),
3559 vec!["r", r#"["weird]key"]"#, "[0]"]
3560 );
3561 }
3562
3563 #[test]
3568 fn parse_integers() {
3569 assert_eq!(parse_int("0").expect("ok"), 0);
3570 assert_eq!(parse_int("42").expect("ok"), 42);
3571 assert_eq!(parse_int("-1").expect("ok"), -1);
3572 }
3573
3574 #[test]
3575 fn parse_floats() {
3576 assert!((parse_float("3.14").expect("ok") - 3.14).abs() < f64::EPSILON);
3577 assert!((parse_float("-0.5").expect("ok") - (-0.5)).abs() < f64::EPSILON);
3578 }
3579
3580 #[test]
3585 fn empty_input() {
3586 assert_eq!(lex(""), vec![]);
3587 }
3588
3589 #[test]
3590 fn only_whitespace() {
3591 assert_eq!(lex(" \t\t "), vec![]);
3592 }
3593
3594 #[test]
3595 fn json_array() {
3596 assert_eq!(
3597 lex(r#"[1, 2, 3]"#),
3598 vec![
3599 Token::LBracket,
3600 Token::Int(1),
3601 Token::Comma,
3602 Token::Int(2),
3603 Token::Comma,
3604 Token::Int(3),
3605 Token::RBracket
3606 ]
3607 );
3608 }
3609
3610 #[test]
3611 fn json_object() {
3612 assert_eq!(
3613 lex(r#"{"key": "value"}"#),
3614 vec![
3615 Token::LBrace,
3616 Token::String("key".to_string()),
3617 Token::Colon,
3618 Token::String("value".to_string()),
3619 Token::RBrace
3620 ]
3621 );
3622 }
3623
3624 #[test]
3625 fn redirect_operators() {
3626 assert_eq!(
3627 lex("cmd > file"),
3628 vec![Token::Ident("cmd".to_string()), Token::Gt, Token::Ident("file".to_string())]
3629 );
3630 assert_eq!(
3631 lex("cmd >> file"),
3632 vec![Token::Ident("cmd".to_string()), Token::GtGt, Token::Ident("file".to_string())]
3633 );
3634 assert_eq!(
3635 lex("cmd 2> err"),
3636 vec![Token::Ident("cmd".to_string()), Token::Stderr, Token::Ident("err".to_string())]
3637 );
3638 assert_eq!(
3639 lex("cmd &> all"),
3640 vec![Token::Ident("cmd".to_string()), Token::Both, Token::Ident("all".to_string())]
3641 );
3642 }
3643
3644 #[test]
3645 fn background_job() {
3646 assert_eq!(
3647 lex("cmd &"),
3648 vec![Token::Ident("cmd".to_string()), Token::Amp]
3649 );
3650 }
3651
3652 #[test]
3653 fn command_substitution() {
3654 assert_eq!(
3655 lex("$(cmd)"),
3656 vec![Token::CmdSubstStart, Token::Ident("cmd".to_string()), Token::RParen]
3657 );
3658 assert_eq!(
3659 lex("$(cmd arg)"),
3660 vec![
3661 Token::CmdSubstStart,
3662 Token::Ident("cmd".to_string()),
3663 Token::Ident("arg".to_string()),
3664 Token::RParen
3665 ]
3666 );
3667 assert_eq!(
3668 lex("$(a | b)"),
3669 vec![
3670 Token::CmdSubstStart,
3671 Token::Ident("a".to_string()),
3672 Token::Pipe,
3673 Token::Ident("b".to_string()),
3674 Token::RParen
3675 ]
3676 );
3677 }
3678
3679 #[test]
3680 fn complex_pipeline() {
3681 assert_eq!(
3682 lex(r#"cat file | grep pattern="foo" | head count=10"#),
3683 vec![
3684 Token::Ident("cat".to_string()),
3685 Token::Ident("file".to_string()),
3686 Token::Pipe,
3687 Token::Ident("grep".to_string()),
3688 Token::Ident("pattern".to_string()),
3689 Token::Eq,
3690 Token::String("foo".to_string()),
3691 Token::Pipe,
3692 Token::Ident("head".to_string()),
3693 Token::Ident("count".to_string()),
3694 Token::Eq,
3695 Token::Int(10),
3696 ]
3697 );
3698 }
3699
3700 #[test]
3705 fn short_flag() {
3706 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3707 assert_eq!(lex("-a"), vec![Token::ShortFlag("a".to_string())]);
3708 assert_eq!(lex("-v"), vec![Token::ShortFlag("v".to_string())]);
3709 }
3710
3711 #[test]
3712 fn short_flag_combined() {
3713 assert_eq!(lex("-la"), vec![Token::ShortFlag("la".to_string())]);
3715 assert_eq!(lex("-vvv"), vec![Token::ShortFlag("vvv".to_string())]);
3716 }
3717
3718 #[test]
3719 fn job_spec_lexes_as_one_token() {
3720 assert_eq!(lex("%1"), vec![Token::JobSpec("%1".to_string())]);
3722 assert_eq!(lex("%12"), vec![Token::JobSpec("%12".to_string())]);
3723 assert_eq!(
3724 lex("wait %1 %2"),
3725 vec![
3726 Token::Ident("wait".to_string()),
3727 Token::JobSpec("%1".to_string()),
3728 Token::JobSpec("%2".to_string()),
3729 ]
3730 );
3731 }
3732
3733 #[test]
3734 fn short_flag_with_internal_hyphens_is_one_token() {
3735 assert_eq!(
3739 lex("-not-a-flag"),
3740 vec![Token::ShortFlag("not-a-flag".to_string())]
3741 );
3742 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3746 assert_eq!(lex("-"), vec![Token::MinusAlone]);
3747 }
3748
3749 #[test]
3750 fn long_flag() {
3751 assert_eq!(lex("--force"), vec![Token::LongFlag("force".to_string())]);
3752 assert_eq!(lex("--verbose"), vec![Token::LongFlag("verbose".to_string())]);
3753 assert_eq!(lex("--foo-bar"), vec![Token::LongFlag("foo-bar".to_string())]);
3754 }
3755
3756 #[test]
3757 fn double_dash() {
3758 assert_eq!(lex("--"), vec![Token::DoubleDash]);
3760 }
3761
3762 #[test]
3763 fn flags_vs_negative_numbers() {
3764 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
3766 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
3768 assert_eq!(
3771 lex("-1 a"),
3772 vec![Token::Int(-1), Token::Ident("a".to_string())]
3773 );
3774 }
3775
3776 #[test]
3777 fn command_with_flags() {
3778 assert_eq!(
3779 lex("ls -l"),
3780 vec![
3781 Token::Ident("ls".to_string()),
3782 Token::ShortFlag("l".to_string()),
3783 ]
3784 );
3785 assert_eq!(
3786 lex("git commit -m"),
3787 vec![
3788 Token::Ident("git".to_string()),
3789 Token::Ident("commit".to_string()),
3790 Token::ShortFlag("m".to_string()),
3791 ]
3792 );
3793 assert_eq!(
3794 lex("git push --force"),
3795 vec![
3796 Token::Ident("git".to_string()),
3797 Token::Ident("push".to_string()),
3798 Token::LongFlag("force".to_string()),
3799 ]
3800 );
3801 }
3802
3803 #[test]
3804 fn flag_with_value() {
3805 assert_eq!(
3806 lex(r#"git commit -m "message""#),
3807 vec![
3808 Token::Ident("git".to_string()),
3809 Token::Ident("commit".to_string()),
3810 Token::ShortFlag("m".to_string()),
3811 Token::String("message".to_string()),
3812 ]
3813 );
3814 assert_eq!(
3815 lex(r#"--message="hello""#),
3816 vec![
3817 Token::LongFlag("message".to_string()),
3818 Token::Eq,
3819 Token::String("hello".to_string()),
3820 ]
3821 );
3822 }
3823
3824 #[test]
3825 fn end_of_flags_marker() {
3826 assert_eq!(
3827 lex("git checkout -- file"),
3828 vec![
3829 Token::Ident("git".to_string()),
3830 Token::Ident("checkout".to_string()),
3831 Token::DoubleDash,
3832 Token::Ident("file".to_string()),
3833 ]
3834 );
3835 }
3836
3837 #[test]
3842 fn local_keyword() {
3843 assert_eq!(lex("local"), vec![Token::Local]);
3844 assert_eq!(
3845 lex("local X = 5"),
3846 vec![Token::Local, Token::Ident("X".to_string()), Token::Eq, Token::Int(5)]
3847 );
3848 }
3849
3850 #[test]
3851 fn simple_var_ref() {
3852 assert_eq!(lex("$X"), vec![Token::SimpleVarRef("X".to_string())]);
3853 assert_eq!(lex("$foo"), vec![Token::SimpleVarRef("foo".to_string())]);
3854 assert_eq!(lex("$foo_bar"), vec![Token::SimpleVarRef("foo_bar".to_string())]);
3855 assert_eq!(lex("$_private"), vec![Token::SimpleVarRef("_private".to_string())]);
3856 }
3857
3858 #[test]
3859 fn simple_var_ref_in_command() {
3860 assert_eq!(
3861 lex("echo $NAME"),
3862 vec![Token::Ident("echo".to_string()), Token::SimpleVarRef("NAME".to_string())]
3863 );
3864 }
3865
3866 #[test]
3867 fn single_quoted_strings() {
3868 assert_eq!(lex("'hello'"), vec![Token::SingleString("hello".to_string())]);
3869 assert_eq!(lex("'hello world'"), vec![Token::SingleString("hello world".to_string())]);
3870 assert_eq!(lex("''"), vec![Token::SingleString("".to_string())]);
3871 assert_eq!(lex(r"'no $VAR here'"), vec![Token::SingleString("no $VAR here".to_string())]);
3873 assert_eq!(lex(r"'backslash \n stays'"), vec![Token::SingleString(r"backslash \n stays".to_string())]);
3874 }
3875
3876 #[test]
3877 fn test_brackets() {
3878 assert_eq!(lex("[["), vec![Token::LBracket, Token::LBracket]);
3880 assert_eq!(lex("]]"), vec![Token::RBracket, Token::RBracket]);
3881 assert_eq!(
3882 lex("[[ -f file ]]"),
3883 vec![
3884 Token::LBracket,
3885 Token::LBracket,
3886 Token::ShortFlag("f".to_string()),
3887 Token::Ident("file".to_string()),
3888 Token::RBracket,
3889 Token::RBracket
3890 ]
3891 );
3892 }
3893
3894 #[test]
3895 fn test_expression_syntax() {
3896 assert_eq!(
3897 lex(r#"[[ $X == "value" ]]"#),
3898 vec![
3899 Token::LBracket,
3900 Token::LBracket,
3901 Token::SimpleVarRef("X".to_string()),
3902 Token::EqEq,
3903 Token::String("value".to_string()),
3904 Token::RBracket,
3905 Token::RBracket
3906 ]
3907 );
3908 }
3909
3910 #[test]
3911 fn bash_style_assignment() {
3912 assert_eq!(
3914 lex(r#"NAME="value""#),
3915 vec![
3916 Token::Ident("NAME".to_string()),
3917 Token::Eq,
3918 Token::String("value".to_string())
3919 ]
3920 );
3921 }
3922
3923 #[test]
3924 fn positional_params() {
3925 assert_eq!(lex("$0"), vec![Token::Positional(0)]);
3926 assert_eq!(lex("$1"), vec![Token::Positional(1)]);
3927 assert_eq!(lex("$9"), vec![Token::Positional(9)]);
3928 assert_eq!(lex("$@"), vec![Token::AllArgs]);
3929 assert_eq!(lex("$#"), vec![Token::ArgCount]);
3930 }
3931
3932 #[test]
3933 fn positional_in_context() {
3934 assert_eq!(
3935 lex("echo $1 $2"),
3936 vec![
3937 Token::Ident("echo".to_string()),
3938 Token::Positional(1),
3939 Token::Positional(2),
3940 ]
3941 );
3942 }
3943
3944 #[test]
3945 fn var_length() {
3946 assert_eq!(lex("${#X}"), vec![Token::VarLength("X".to_string())]);
3947 assert_eq!(lex("${#NAME}"), vec![Token::VarLength("NAME".to_string())]);
3948 assert_eq!(lex("${#foo_bar}"), vec![Token::VarLength("foo_bar".to_string())]);
3949 }
3950
3951 #[test]
3952 fn var_length_with_subscript() {
3953 assert_eq!(lex("${#u[tags]}"), vec![Token::VarLength("u[tags]".to_string())]);
3956 assert_eq!(lex("${#a[0]}"), vec![Token::VarLength("a[0]".to_string())]);
3957 assert_eq!(lex("${#a[b][c]}"), vec![Token::VarLength("a[b][c]".to_string())]);
3958 assert_eq!(lex("${#r[$k]}"), vec![Token::VarLength("r[$k]".to_string())]);
3959 }
3960
3961 #[test]
3962 fn var_length_in_context() {
3963 assert_eq!(
3964 lex("echo ${#NAME}"),
3965 vec![
3966 Token::Ident("echo".to_string()),
3967 Token::VarLength("NAME".to_string()),
3968 ]
3969 );
3970 }
3971
3972 #[test]
3977 fn plus_flag() {
3978 assert_eq!(lex("+e"), vec![Token::PlusFlag("e".to_string())]);
3980 assert_eq!(lex("+x"), vec![Token::PlusFlag("x".to_string())]);
3981 assert_eq!(lex("+ex"), vec![Token::PlusFlag("ex".to_string())]);
3982 }
3983
3984 #[test]
3985 fn set_with_plus_flag() {
3986 assert_eq!(
3987 lex("set +e"),
3988 vec![
3989 Token::Set,
3990 Token::PlusFlag("e".to_string()),
3991 ]
3992 );
3993 }
3994
3995 #[test]
3996 fn set_with_multiple_flags() {
3997 assert_eq!(
3998 lex("set -e -u"),
3999 vec![
4000 Token::Set,
4001 Token::ShortFlag("e".to_string()),
4002 Token::ShortFlag("u".to_string()),
4003 ]
4004 );
4005 }
4006
4007 #[test]
4008 fn flags_vs_negative_numbers_edge_cases() {
4009 assert_eq!(
4011 lex("-1 a"),
4012 vec![Token::Int(-1), Token::Ident("a".to_string())]
4013 );
4014 assert_eq!(lex("-l"), vec![Token::ShortFlag("l".to_string())]);
4016 assert_eq!(lex("-123"), vec![Token::Int(-123)]);
4018 }
4019
4020 #[test]
4021 fn single_dash_is_minus_alone() {
4022 let result = tokenize("-").expect("should lex");
4024 assert_eq!(result.len(), 1);
4025 assert!(matches!(result[0].token, Token::MinusAlone));
4026 }
4027
4028 #[test]
4029 fn plus_bare_for_date_format() {
4030 let result = tokenize("+%s").expect("should lex");
4032 assert_eq!(result.len(), 1);
4033 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%s"));
4034
4035 let result = tokenize("+%Y-%m-%d").expect("should lex");
4037 assert_eq!(result.len(), 1);
4038 assert!(matches!(result[0].token, Token::PlusBare(ref s) if s == "+%Y-%m-%d"));
4039 }
4040
4041 #[test]
4042 fn plus_flag_still_works() {
4043 let result = tokenize("+e").expect("should lex");
4045 assert_eq!(result.len(), 1);
4046 assert!(matches!(result[0].token, Token::PlusFlag(ref s) if s == "e"));
4047 }
4048
4049 #[test]
4050 fn while_keyword_vs_while_loop() {
4051 assert_eq!(lex("while"), vec![Token::While]);
4053 assert_eq!(
4055 lex("while true"),
4056 vec![Token::While, Token::True]
4057 );
4058 }
4059
4060 #[test]
4061 fn control_flow_keywords() {
4062 assert_eq!(lex("break"), vec![Token::Break]);
4063 assert_eq!(lex("continue"), vec![Token::Continue]);
4064 assert_eq!(lex("return"), vec![Token::Return]);
4065 assert_eq!(lex("exit"), vec![Token::Exit]);
4066 }
4067
4068 #[test]
4069 fn control_flow_with_numbers() {
4070 assert_eq!(
4071 lex("break 2"),
4072 vec![Token::Break, Token::Int(2)]
4073 );
4074 assert_eq!(
4075 lex("continue 3"),
4076 vec![Token::Continue, Token::Int(3)]
4077 );
4078 assert_eq!(
4079 lex("exit 1"),
4080 vec![Token::Exit, Token::Int(1)]
4081 );
4082 }
4083
4084 #[test]
4089 fn heredoc_simple() {
4090 let source = "cat <<EOF\nhello\nworld\nEOF";
4091 let tokens = lex(source);
4092 assert_eq!(tokens, vec![
4094 Token::Ident("cat".to_string()),
4095 Token::HereDocStart,
4096 Token::HereDoc(HereDocData {
4097 content: "hello\nworld\n".to_string(),
4098 literal: false,
4099 strip_tabs: false,
4100 body_start_offset: 10,
4101 }),
4102 Token::Newline,
4103 ]);
4104 }
4105
4106 #[test]
4107 fn heredoc_empty() {
4108 let source = "cat <<EOF\nEOF";
4109 let tokens = lex(source);
4110 assert_eq!(tokens, vec![
4111 Token::Ident("cat".to_string()),
4112 Token::HereDocStart,
4113 Token::HereDoc(HereDocData {
4114 content: "".to_string(),
4115 literal: false,
4116 strip_tabs: false,
4117 body_start_offset: 10,
4118 }),
4119 Token::Newline,
4120 ]);
4121 }
4122
4123 #[test]
4124 fn heredoc_with_special_chars() {
4125 let source = "cat <<EOF\n$VAR and \"quoted\" 'single'\nEOF";
4126 let tokens = lex(source);
4127 assert_eq!(tokens, vec![
4128 Token::Ident("cat".to_string()),
4129 Token::HereDocStart,
4130 Token::HereDoc(HereDocData {
4131 content: "$VAR and \"quoted\" 'single'\n".to_string(),
4132 literal: false,
4133 strip_tabs: false,
4134 body_start_offset: 10,
4135 }),
4136 Token::Newline,
4137 ]);
4138 }
4139
4140 #[test]
4141 fn heredoc_multiline() {
4142 let source = "cat <<END\nline1\nline2\nline3\nEND";
4143 let tokens = lex(source);
4144 assert_eq!(tokens, vec![
4145 Token::Ident("cat".to_string()),
4146 Token::HereDocStart,
4147 Token::HereDoc(HereDocData {
4148 content: "line1\nline2\nline3\n".to_string(),
4149 literal: false,
4150 strip_tabs: false,
4151 body_start_offset: 10,
4152 }),
4153 Token::Newline,
4154 ]);
4155 }
4156
4157 #[test]
4158 fn heredoc_in_command() {
4159 let source = "cat <<EOF\nhello\nEOF\necho goodbye";
4160 let tokens = lex(source);
4161 assert_eq!(tokens, vec![
4162 Token::Ident("cat".to_string()),
4163 Token::HereDocStart,
4164 Token::HereDoc(HereDocData {
4165 content: "hello\n".to_string(),
4166 literal: false,
4167 strip_tabs: false,
4168 body_start_offset: 10,
4169 }),
4170 Token::Newline,
4171 Token::Ident("echo".to_string()),
4172 Token::Ident("goodbye".to_string()),
4173 ]);
4174 }
4175
4176 #[test]
4177 fn heredoc_strip_tabs() {
4178 let source = "cat <<-EOF\n\thello\n\tworld\n\tEOF";
4179 let tokens = lex(source);
4180 assert_eq!(tokens, vec![
4184 Token::Ident("cat".to_string()),
4185 Token::HereDocStart,
4186 Token::HereDoc(HereDocData {
4187 content: "\thello\n\tworld\n".to_string(),
4188 literal: false,
4189 strip_tabs: true,
4190 body_start_offset: 11,
4191 }),
4192 Token::Newline,
4193 ]);
4194 }
4195
4196 #[test]
4201 fn arithmetic_simple() {
4202 let source = "$((1 + 2))";
4203 let tokens = lex(source);
4204 assert_eq!(tokens, vec![Token::Arithmetic("1 + 2".to_string())]);
4205 }
4206
4207 #[test]
4208 fn arithmetic_in_assignment() {
4209 let source = "X=$((5 * 3))";
4210 let tokens = lex(source);
4211 assert_eq!(tokens, vec![
4212 Token::Ident("X".to_string()),
4213 Token::Eq,
4214 Token::Arithmetic("5 * 3".to_string()),
4215 ]);
4216 }
4217
4218 #[test]
4219 fn arithmetic_with_nested_parens() {
4220 let source = "$((2 * (3 + 4)))";
4221 let tokens = lex(source);
4222 assert_eq!(tokens, vec![Token::Arithmetic("2 * (3 + 4)".to_string())]);
4223 }
4224
4225 #[test]
4226 fn arithmetic_with_variable() {
4227 let source = "$((X + 1))";
4228 let tokens = lex(source);
4229 assert_eq!(tokens, vec![Token::Arithmetic("X + 1".to_string())]);
4230 }
4231
4232 #[test]
4233 fn arithmetic_command_subst_not_confused() {
4234 let source = "$(echo hello)";
4236 let tokens = lex(source);
4237 assert_eq!(tokens, vec![
4238 Token::CmdSubstStart,
4239 Token::Ident("echo".to_string()),
4240 Token::Ident("hello".to_string()),
4241 Token::RParen,
4242 ]);
4243 }
4244
4245 #[test]
4246 fn arithmetic_nesting_limit() {
4247 let open_parens = "(".repeat(300);
4249 let close_parens = ")".repeat(300);
4250 let source = format!("$(({}1{}))", open_parens, close_parens);
4251 let result = tokenize(&source);
4252 assert!(result.is_err());
4253 let errors = result.unwrap_err();
4254 assert_eq!(errors.len(), 1);
4255 assert_eq!(errors[0].token, LexerError::NestingTooDeep);
4256 }
4257
4258 #[test]
4259 fn arithmetic_nesting_within_limit() {
4260 let source = "$((((1 + 2) * 3)))";
4262 let tokens = lex(source);
4263 assert_eq!(tokens, vec![Token::Arithmetic("((1 + 2) * 3)".to_string())]);
4264 }
4265
4266 #[test]
4278 fn arithmetic_after_apostrophe_in_comment() {
4279 let source = "# this doesn't work\necho $((1+2))";
4282 let tokens = lex(source);
4283 assert_eq!(tokens, vec![
4284 Token::Newline,
4285 Token::Ident("echo".to_string()),
4286 Token::Arithmetic("1+2".to_string()),
4287 ]);
4288 }
4289
4290 #[test]
4291 fn arithmetic_inside_comment_is_not_expanded() {
4292 let source = "# the $((y)) syntax explained\necho hello";
4294 let tokens = lex(source);
4295 assert_eq!(tokens, vec![
4296 Token::Newline,
4297 Token::Ident("echo".to_string()),
4298 Token::Ident("hello".to_string()),
4299 ]);
4300 }
4301
4302 #[test]
4303 fn backticked_arithmetic_in_comment_is_not_expanded() {
4304 let source = "# the `$((x))` syntax explained\necho $((3+4))";
4308 let tokens = lex(source);
4309 assert_eq!(tokens, vec![
4310 Token::Newline,
4311 Token::Ident("echo".to_string()),
4312 Token::Arithmetic("3+4".to_string()),
4313 ]);
4314 }
4315
4316 #[test]
4317 fn arithmetic_still_works_outside_comments() {
4318 let source = "X=$((1+2)); Y=$((3*4))";
4321 let tokens = lex(source);
4322 assert_eq!(tokens, vec![
4323 Token::Ident("X".to_string()),
4324 Token::Eq,
4325 Token::Arithmetic("1+2".to_string()),
4326 Token::Semi,
4327 Token::Ident("Y".to_string()),
4328 Token::Eq,
4329 Token::Arithmetic("3*4".to_string()),
4330 ]);
4331 }
4332
4333 #[test]
4334 fn arithmetic_inside_double_quotes_still_expands() {
4335 let source = "echo \"# $((1+2))\"";
4338 let tokens = lex(source);
4339 assert_eq!(tokens.len(), 2);
4344 assert!(matches!(tokens[0], Token::Ident(_)));
4345 assert!(matches!(tokens[1], Token::String(_)));
4346 }
4347
4348 #[test]
4361 fn backtick_in_source_is_rejected() {
4362 let result = tokenize("echo `date`");
4363 assert!(result.is_err());
4364 let errors = result.unwrap_err();
4365 assert!(errors.iter().any(|e| e.token == LexerError::BackticksNotSupported));
4366 }
4367
4368 #[test]
4369 fn backtick_in_comment_is_just_comment_text() {
4370 let source = "# use `date` here\necho hi";
4373 let tokens = lex(source);
4374 assert_eq!(tokens, vec![
4375 Token::Newline,
4376 Token::Ident("echo".to_string()),
4377 Token::Ident("hi".to_string()),
4378 ]);
4379 }
4380
4381 #[test]
4382 fn backtick_in_single_quoted_string_is_literal() {
4383 let source = "echo '`date`'";
4386 let tokens = lex(source);
4387 assert_eq!(tokens, vec![
4388 Token::Ident("echo".to_string()),
4389 Token::SingleString("`date`".to_string()),
4390 ]);
4391 }
4392
4393 #[test]
4394 fn backtick_in_double_quoted_string_is_literal() {
4395 let source = "echo \"`date`\"";
4400 let tokens = lex(source);
4401 assert_eq!(tokens.len(), 2);
4402 assert!(matches!(tokens[0], Token::Ident(_)));
4403 match &tokens[1] {
4404 Token::String(s) => assert!(s.contains('`')),
4405 other => panic!("expected Token::String, got {:?}", other),
4406 }
4407 }
4408
4409 #[test]
4410 fn backtick_in_heredoc_body_is_preserved() {
4411 let source = "cat <<EOF\n`date`\nEOF\n";
4414 let tokens = lex(source);
4415 let heredoc = tokens.iter().find(|t| matches!(t, Token::HereDoc(_)));
4416 assert!(heredoc.is_some(), "expected a HereDoc token");
4417 if let Some(Token::HereDoc(d)) = heredoc {
4418 assert!(d.content.contains('`'));
4419 }
4420 }
4421
4422 #[test]
4427 fn token_categories() {
4428 assert_eq!(Token::If.category(), TokenCategory::Keyword);
4430 assert_eq!(Token::Then.category(), TokenCategory::Keyword);
4431 assert_eq!(Token::For.category(), TokenCategory::Keyword);
4432 assert_eq!(Token::Function.category(), TokenCategory::Keyword);
4433 assert_eq!(Token::True.category(), TokenCategory::Keyword);
4434 assert_eq!(Token::TypeString.category(), TokenCategory::Keyword);
4435
4436 assert_eq!(Token::Pipe.category(), TokenCategory::Operator);
4438 assert_eq!(Token::And.category(), TokenCategory::Operator);
4439 assert_eq!(Token::Or.category(), TokenCategory::Operator);
4440 assert_eq!(Token::StderrToStdout.category(), TokenCategory::Operator);
4441 assert_eq!(Token::GtGt.category(), TokenCategory::Operator);
4442
4443 assert_eq!(Token::String("test".to_string()).category(), TokenCategory::String);
4445 assert_eq!(Token::SingleString("test".to_string()).category(), TokenCategory::String);
4446 assert_eq!(
4447 Token::HereDoc(HereDocData {
4448 content: "test".to_string(),
4449 literal: false,
4450 strip_tabs: false,
4451 body_start_offset: 0,
4452 }).category(),
4453 TokenCategory::String,
4454 );
4455
4456 assert_eq!(Token::Int(42).category(), TokenCategory::Number);
4458 assert_eq!(Token::Float(3.14).category(), TokenCategory::Number);
4459 assert_eq!(Token::Arithmetic("1+2".to_string()).category(), TokenCategory::Number);
4460
4461 assert_eq!(Token::SimpleVarRef("X".to_string()).category(), TokenCategory::Variable);
4463 assert_eq!(Token::VarRef("${X}".to_string()).category(), TokenCategory::Variable);
4464 assert_eq!(Token::Positional(1).category(), TokenCategory::Variable);
4465 assert_eq!(Token::AllArgs.category(), TokenCategory::Variable);
4466 assert_eq!(Token::ArgCount.category(), TokenCategory::Variable);
4467 assert_eq!(Token::LastExitCode.category(), TokenCategory::Variable);
4468 assert_eq!(Token::CurrentPid.category(), TokenCategory::Variable);
4469
4470 assert_eq!(Token::ShortFlag("l".to_string()).category(), TokenCategory::Flag);
4472 assert_eq!(Token::LongFlag("verbose".to_string()).category(), TokenCategory::Flag);
4473 assert_eq!(Token::PlusFlag("e".to_string()).category(), TokenCategory::Flag);
4474 assert_eq!(Token::DoubleDash.category(), TokenCategory::Flag);
4475
4476 assert_eq!(Token::Semi.category(), TokenCategory::Punctuation);
4478 assert_eq!(Token::LParen.category(), TokenCategory::Punctuation);
4479 assert_eq!(Token::LBracket.category(), TokenCategory::Punctuation);
4480 assert_eq!(Token::Newline.category(), TokenCategory::Punctuation);
4481
4482 assert_eq!(Token::Comment.category(), TokenCategory::Comment);
4484
4485 assert_eq!(Token::Path("/tmp/file".to_string()).category(), TokenCategory::Path);
4487
4488 assert_eq!(Token::Ident("echo".to_string()).category(), TokenCategory::Command);
4490 assert_eq!(Token::NumberIdent("019dda1c".to_string()).category(), TokenCategory::Command);
4491 assert_eq!(Token::DottedIdent(".gitignore".to_string()).category(), TokenCategory::Command);
4492
4493 assert_eq!(Token::InvalidFloatNoLeading.category(), TokenCategory::Error);
4495 assert_eq!(Token::InvalidFloatNoTrailing.category(), TokenCategory::Error);
4496 }
4497
4498 #[test]
4499 fn test_heredoc_piped_to_command() {
4500 let tokens = tokenize("cat <<EOF | jq\n{\"key\": \"val\"}\nEOF").unwrap();
4503 let heredoc_pos = tokens.iter().position(|t| matches!(t.token, Token::HereDoc(_)));
4504 let pipe_pos = tokens.iter().position(|t| matches!(t.token, Token::Pipe));
4505 assert!(heredoc_pos.is_some(), "should have a heredoc token");
4506 assert!(pipe_pos.is_some(), "should have a pipe token");
4507 assert!(
4508 pipe_pos.unwrap() > heredoc_pos.unwrap(),
4509 "Pipe must come after heredoc, got heredoc at {}, pipe at {}. Tokens: {:?}",
4510 heredoc_pos.unwrap(), pipe_pos.unwrap(), tokens,
4511 );
4512 }
4513
4514 #[test]
4515 fn test_heredoc_standalone_still_works() {
4516 let tokens = tokenize("cat <<EOF\nhello\nEOF").unwrap();
4518 assert!(tokens.iter().any(|t| matches!(t.token, Token::HereDoc(_))));
4519 assert!(!tokens.iter().any(|t| matches!(t.token, Token::Pipe)));
4520 }
4521
4522 #[test]
4523 fn test_heredoc_preserves_leading_empty_lines() {
4524 let tokens = tokenize("cat <<EOF\n\nhello\nEOF").unwrap();
4526 let heredoc = tokens.iter().find_map(|t| {
4527 if let Token::HereDoc(data) = &t.token {
4528 Some(data.clone())
4529 } else {
4530 None
4531 }
4532 });
4533 assert!(heredoc.is_some(), "should have a heredoc token");
4534 let data = heredoc.unwrap();
4535 assert!(data.content.starts_with('\n'), "leading empty line must be preserved, got: {:?}", data.content);
4536 assert_eq!(data.content, "\nhello\n");
4537 }
4538
4539 #[test]
4540 fn test_heredoc_quoted_delimiter_sets_literal() {
4541 let tokens = tokenize("cat <<'EOF'\nhello $HOME\nEOF").unwrap();
4543 let heredoc = tokens.iter().find_map(|t| {
4544 if let Token::HereDoc(data) = &t.token {
4545 Some(data.clone())
4546 } else {
4547 None
4548 }
4549 });
4550 assert!(heredoc.is_some(), "should have a heredoc token");
4551 let data = heredoc.unwrap();
4552 assert!(data.literal, "quoted delimiter should set literal=true");
4553 assert_eq!(data.content, "hello $HOME\n");
4554 }
4555
4556 #[test]
4557 fn test_heredoc_unquoted_delimiter_not_literal() {
4558 let tokens = tokenize("cat <<EOF\nhello $HOME\nEOF").unwrap();
4560 let heredoc = tokens.iter().find_map(|t| {
4561 if let Token::HereDoc(data) = &t.token {
4562 Some(data.clone())
4563 } else {
4564 None
4565 }
4566 });
4567 assert!(heredoc.is_some(), "should have a heredoc token");
4568 let data = heredoc.unwrap();
4569 assert!(!data.literal, "unquoted delimiter should have literal=false");
4570 }
4571
4572 #[test]
4577 fn colon_double_in_word() {
4578 assert_eq!(lex("foo::bar"), vec![Token::Ident("foo::bar".into())]);
4579 }
4580
4581 #[test]
4582 fn colon_single_in_word() {
4583 assert_eq!(lex("a:b:c"), vec![Token::Ident("a:b:c".into())]);
4584 }
4585
4586 #[test]
4587 fn colon_with_port() {
4588 assert_eq!(lex("host:8080"), vec![Token::Ident("host:8080".into())]);
4589 }
4590
4591 #[test]
4592 fn colon_standalone() {
4593 assert_eq!(lex(":"), vec![Token::Colon]);
4594 }
4595
4596 #[test]
4597 fn colon_spaced_no_merge() {
4598 assert_eq!(
4599 lex("foo : bar"),
4600 vec![
4601 Token::Ident("foo".into()),
4602 Token::Colon,
4603 Token::Ident("bar".into()),
4604 ]
4605 );
4606 }
4607
4608 #[test]
4609 fn colon_in_command_arg() {
4610 assert_eq!(
4611 lex("echo foo::bar"),
4612 vec![
4613 Token::Ident("echo".into()),
4614 Token::Ident("foo::bar".into()),
4615 ]
4616 );
4617 }
4618
4619 #[test]
4620 fn colon_trailing() {
4621 assert_eq!(lex("foo:"), vec![Token::Ident("foo:".into())]);
4623 }
4624
4625 #[test]
4626 fn colon_leading() {
4627 assert_eq!(lex(":foo"), vec![Token::Ident(":foo".into())]);
4629 }
4630
4631 #[test]
4632 fn colon_with_path() {
4633 assert_eq!(
4635 lex("/usr/bin:8080"),
4636 vec![Token::Ident("/usr/bin:8080".into())]
4637 );
4638 }
4639
4640 #[test]
4645 fn is_keyword_covers_control_flow() {
4646 for t in [
4647 Token::While,
4648 Token::Return,
4649 Token::Break,
4650 Token::Continue,
4651 Token::Exit,
4652 ] {
4653 assert!(t.is_keyword(), "{t:?} should be a keyword");
4654 }
4655 }
4656
4657 #[test]
4658 fn starts_statement_covers_while() {
4659 assert!(Token::While.starts_statement());
4660 }
4661
4662 #[test]
4663 fn is_keyword_rejects_operators() {
4664 for t in [Token::Pipe, Token::Amp, Token::Eq, Token::LBrace] {
4665 assert!(!t.is_keyword(), "{t:?} should not be a keyword");
4666 }
4667 }
4668}