1use crate::brace_expansion;
2use crate::executor;
3use crate::lexer;
4use crate::parser;
5use crate::state;
6use std::collections::HashSet;
7use std::sync::atomic::{AtomicBool, Ordering};
8
9pub fn line_contains_heredoc(line: &str, shell_state: &state::ShellState) -> Option<String> {
12 match lexer::lex(line, shell_state) {
14 Ok(tokens) => {
15 for token in tokens {
17 if let lexer::Token::RedirHereDoc(delimiter, _quoted) = token {
18 return Some(delimiter);
19 }
20 }
21 None
22 }
23 Err(_) => None,
24 }
25}
26
27pub fn contains_keyword(line: &str, keyword: &str) -> bool {
30 let chars = line.chars().peekable();
31 let mut in_single_quote = false;
32 let mut in_double_quote = false;
33 let mut escaped = false;
34 let mut current_word = String::new();
35
36 for ch in chars {
37 if escaped {
38 escaped = false;
39 current_word.push(ch);
41 continue;
42 }
43
44 if in_single_quote {
45 if ch == '\'' {
46 in_single_quote = false;
47 } else {
48 current_word.push(ch);
49 }
50 continue;
51 }
52
53 if in_double_quote {
54 if ch == '"' {
55 in_double_quote = false;
56 } else if ch == '\\' {
57 escaped = true;
58 } else {
59 current_word.push(ch);
60 }
61 continue;
62 }
63
64 match ch {
65 '#' => {
66 if current_word.is_empty() {
67 return false; }
69 current_word.push(ch); }
71 '\'' => {
72 in_single_quote = true;
73 current_word.push(ch);
74 }
75 '"' => {
76 in_double_quote = true;
77 current_word.push(ch);
78 }
79 '\\' => escaped = true,
80 ' ' | '\t' | '\n' | ';' | '|' | '&' | '(' | ')' | '{' | '}' => {
81 if current_word == keyword {
82 return true;
83 }
84 current_word.clear();
85 }
86 _ => current_word.push(ch),
87 }
88 }
89
90 current_word == keyword
92}
93
94pub fn starts_with_keyword(line: &str, keyword: &str) -> bool {
106 let mut chars = line.chars().peekable();
107 let mut current_word = String::new();
108
109 while let Some(&ch) = chars.peek() {
111 if ch == ' ' || ch == '\t' {
112 chars.next();
113 } else {
114 break;
115 }
116 }
117
118 for ch in chars {
119 match ch {
120 ' ' | '\t' | '\n' | ';' | '|' | '&' | '(' | ')' | '{' | '}' => {
121 return current_word == keyword;
122 }
123 _ => current_word.push(ch),
124 }
125 }
126
127 current_word == keyword
128}
129
130pub fn execute_line(line: &str, shell_state: &mut state::ShellState) {
151 if shell_state.options.verbose {
153 if shell_state.colors_enabled {
154 eprintln!("{}{}\x1b[0m", shell_state.color_scheme.builtin, line);
155 } else {
156 eprintln!("{}", line);
157 }
158 }
159
160 match lexer::lex(line, shell_state) {
161 Ok(tokens) => match lexer::expand_aliases(tokens, shell_state, &mut HashSet::new()) {
162 Ok(expanded_tokens) => match brace_expansion::expand_braces(expanded_tokens) {
163 Ok(brace_expanded_tokens) => match parser::parse(brace_expanded_tokens) {
164 Ok(ast) => {
165 let exit_code = executor::execute(ast, shell_state);
166 shell_state.set_last_exit_code(exit_code);
167 }
168 Err(e) => {
169 if shell_state.colors_enabled {
170 eprintln!(
171 "{}Parse error: {}\x1b[0m",
172 shell_state.color_scheme.error, e
173 );
174 } else {
175 eprintln!("Parse error: {}", e);
176 }
177 shell_state.set_last_exit_code(1);
178 }
179 },
180 Err(e) => {
181 if shell_state.colors_enabled {
182 eprintln!(
183 "{}Brace expansion error: {}\x1b[0m",
184 shell_state.color_scheme.error, e
185 );
186 } else {
187 eprintln!("Brace expansion error: {}", e);
188 }
189 shell_state.set_last_exit_code(1);
190 }
191 },
192 Err(e) => {
193 if shell_state.colors_enabled {
194 eprintln!(
195 "{}Alias expansion error: {}\x1b[0m",
196 shell_state.color_scheme.error, e
197 );
198 } else {
199 eprintln!("Alias expansion error: {}", e);
200 }
201 shell_state.set_last_exit_code(1);
202 }
203 },
204 Err(e) => {
205 if shell_state.colors_enabled {
206 eprintln!("{}Lex error: {}\x1b[0m", shell_state.color_scheme.error, e);
207 } else {
208 eprintln!("Lex error: {}", e);
209 }
210 shell_state.set_last_exit_code(1);
211
212 if e.contains("unbound variable") {
214 shell_state.exit_requested = true;
215 shell_state.exit_code = 1;
216 }
217 }
218 }
219}
220
221pub fn execute_script(
222 content: &str,
223 shell_state: &mut state::ShellState,
224 shutdown_flag: Option<&AtomicBool>,
225) {
226 let mut current_block = String::new();
227 let mut in_if_block = false;
228 let mut if_depth = 0;
229 let mut in_case_block = false;
230 let mut in_function_block = false;
231 let mut in_group_block = false;
232 let mut brace_depth = 0;
233 let mut in_for_block = false;
234 let mut for_depth = 0;
235 let mut in_while_block = false;
236 let mut while_depth = 0;
237 let mut in_until_block = false;
238 let mut until_depth = 0;
239
240 let mut in_double_quote = false;
242 let mut in_single_quote = false;
243
244 let lines: Vec<&str> = content.lines().collect();
245 let mut i = 0;
246
247 while i < lines.len() {
248 let line = lines[i];
249 state::process_pending_signals(shell_state);
251
252 if let Some(flag) = shutdown_flag
254 && flag.load(Ordering::Relaxed)
255 {
256 eprintln!("Script interrupted by SIGTERM");
257 break;
258 }
259
260 if shell_state.exit_requested {
262 break;
263 }
264
265 if line.starts_with("#!") {
267 i += 1;
268 continue;
269 }
270
271 let chars = line.chars().peekable();
273 let mut escaped = false;
274
275 for ch in chars {
276 if escaped {
277 escaped = false;
278 continue;
279 }
280
281 if in_single_quote {
282 if ch == '\'' {
283 in_single_quote = false;
284 }
285 continue;
286 }
287
288 if in_double_quote {
289 if ch == '"' {
290 in_double_quote = false;
291 } else if ch == '\\' {
292 escaped = true;
293 }
294 continue;
295 }
296
297 match ch {
298 '#' => break, '\'' => in_single_quote = true,
300 '"' => in_double_quote = true,
301 '\\' => escaped = true,
302 _ => {}
303 }
304 }
305
306 let trimmed = line.trim();
307 if !in_double_quote && !in_single_quote && (trimmed.is_empty() || trimmed.starts_with("#"))
308 {
309 i += 1;
310 continue;
311 }
312
313 let keywords_active = !in_double_quote && !in_single_quote;
314
315 if keywords_active && !in_function_block {
316 if starts_with_keyword(line, "if") {
317 in_if_block = true;
318 if_depth += 1;
319 } else if starts_with_keyword(line, "case") {
320 in_case_block = true;
321 } else if starts_with_keyword(line, "for") {
322 in_for_block = true;
323 for_depth += 1;
324 } else if starts_with_keyword(line, "while") {
325 in_while_block = true;
326 while_depth += 1;
327 } else if starts_with_keyword(line, "until") {
328 in_until_block = true;
329 until_depth += 1;
330 } else {
331 let is_group_start = {
332 let trimmed = line.trim();
333 trimmed == "{" || trimmed.starts_with("{ ") || trimmed.starts_with("{\t")
334 };
335 if is_group_start {
336 in_group_block = true;
337 brace_depth += line.matches('{').count() as i32;
338 brace_depth -= line.matches('}').count() as i32;
339 }
340 }
341 }
342
343 if keywords_active
344 && (line.contains("() {") || (trimmed.ends_with("()") && !in_function_block))
345 {
346 in_function_block = true;
347 brace_depth += line.matches('{').count() as i32;
348 brace_depth -= line.matches('}').count() as i32;
349 } else if in_function_block || in_group_block {
350 brace_depth += line.matches('{').count() as i32;
351 brace_depth -= line.matches('}').count() as i32;
352 }
353
354 if !current_block.is_empty() {
355 current_block.push('\n');
356 }
357 current_block.push_str(line);
358
359 if keywords_active {
360 if (in_function_block || in_group_block) && brace_depth == 0 {
361 in_function_block = false;
362 in_group_block = false;
363 execute_line(¤t_block, shell_state);
364 current_block.clear();
365
366 if shell_state.exit_requested {
367 break;
368 }
369 } else if in_if_block && contains_keyword(line, "fi") {
370 if_depth -= 1;
371 if if_depth == 0 {
372 in_if_block = false;
373 if !in_for_block
375 && !in_while_block
376 && !in_until_block
377 && !in_function_block
378 && !in_group_block
379 && !in_case_block
380 {
381 execute_line(¤t_block, shell_state);
382 current_block.clear();
383
384 if shell_state.exit_requested {
385 break;
386 }
387 }
388 }
389 } else if in_for_block && contains_keyword(line, "done") {
390 for_depth -= 1;
391 if for_depth == 0 {
392 in_for_block = false;
393 execute_line(¤t_block, shell_state);
394 current_block.clear();
395
396 if shell_state.exit_requested {
397 break;
398 }
399 }
400 } else if in_while_block && contains_keyword(line, "done") {
401 while_depth -= 1;
402 if while_depth == 0 {
403 in_while_block = false;
404 execute_line(¤t_block, shell_state);
405 current_block.clear();
406
407 if shell_state.exit_requested {
408 break;
409 }
410 }
411 } else if in_until_block && contains_keyword(line, "done") {
412 until_depth -= 1;
413 if until_depth == 0 {
414 in_until_block = false;
415 execute_line(¤t_block, shell_state);
416 current_block.clear();
417
418 if shell_state.exit_requested {
419 break;
420 }
421 }
422 } else if in_case_block && contains_keyword(line, "esac") {
423 in_case_block = false;
424 execute_line(¤t_block, shell_state);
425 current_block.clear();
426
427 if shell_state.exit_requested {
428 break;
429 }
430 } else if !in_if_block
431 && !in_case_block
432 && !in_function_block
433 && !in_group_block
434 && !in_for_block
435 && !in_while_block
436 && !in_until_block
437 {
438 if let Some(delimiter) = line_contains_heredoc(¤t_block, shell_state) {
439 i += 1;
440 let mut heredoc_content = String::new();
441 while i < lines.len() {
442 let content_line = lines[i];
443 if content_line.trim() == delimiter.trim() {
444 break;
445 }
446 if !heredoc_content.is_empty() {
447 heredoc_content.push('\n');
448 }
449 heredoc_content.push_str(content_line);
450 i += 1;
451 }
452 shell_state.pending_heredoc_content = Some(heredoc_content);
453 execute_line(¤t_block, shell_state);
454 current_block.clear();
455 } else if !in_single_quote
456 && !in_double_quote
457 && (line.ends_with(';') || !line.trim_end().ends_with('\\'))
458 {
459 execute_line(¤t_block, shell_state);
460 current_block.clear();
461 }
462 }
463 }
464 i += 1;
465 }
466}