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 shell_state.current_line_number = 1;
228
229 let mut current_block = String::new();
230 let mut in_if_block = false;
231 let mut if_depth = 0;
232 let mut in_case_block = false;
233 let mut in_function_block = false;
234 let mut in_group_block = false;
235 let mut brace_depth = 0;
236 let mut in_for_block = false;
237 let mut for_depth = 0;
238 let mut in_while_block = false;
239 let mut while_depth = 0;
240 let mut in_until_block = false;
241 let mut until_depth = 0;
242
243 let mut in_double_quote = false;
245 let mut in_single_quote = false;
246
247 let lines: Vec<&str> = content.lines().collect();
248 let mut i = 0;
249
250 while i < lines.len() {
251 let line = lines[i];
252
253 shell_state.current_line_number = i + 1;
255
256 state::process_pending_signals(shell_state);
258
259 if let Some(flag) = shutdown_flag
261 && flag.load(Ordering::Relaxed)
262 {
263 eprintln!("Script interrupted by SIGTERM");
264 break;
265 }
266
267 if shell_state.exit_requested {
269 break;
270 }
271
272 if line.starts_with("#!") {
274 i += 1;
275 continue;
276 }
277
278 let chars = line.chars().peekable();
280 let mut escaped = false;
281
282 for ch in chars {
283 if escaped {
284 escaped = false;
285 continue;
286 }
287
288 if in_single_quote {
289 if ch == '\'' {
290 in_single_quote = false;
291 }
292 continue;
293 }
294
295 if in_double_quote {
296 if ch == '"' {
297 in_double_quote = false;
298 } else if ch == '\\' {
299 escaped = true;
300 }
301 continue;
302 }
303
304 match ch {
305 '#' => break, '\'' => in_single_quote = true,
307 '"' => in_double_quote = true,
308 '\\' => escaped = true,
309 _ => {}
310 }
311 }
312
313 let trimmed = line.trim();
314 if !in_double_quote && !in_single_quote && (trimmed.is_empty() || trimmed.starts_with("#"))
315 {
316 i += 1;
317 continue;
318 }
319
320 let keywords_active = !in_double_quote && !in_single_quote;
321
322 if keywords_active && !in_function_block {
323 if starts_with_keyword(line, "if") {
324 in_if_block = true;
325 if_depth += 1;
326 } else if starts_with_keyword(line, "case") {
327 in_case_block = true;
328 } else if starts_with_keyword(line, "for") {
329 in_for_block = true;
330 for_depth += 1;
331 } else if starts_with_keyword(line, "while") {
332 in_while_block = true;
333 while_depth += 1;
334 } else if starts_with_keyword(line, "until") {
335 in_until_block = true;
336 until_depth += 1;
337 } else {
338 let is_group_start = {
339 let trimmed = line.trim();
340 trimmed == "{" || trimmed.starts_with("{ ") || trimmed.starts_with("{\t")
341 };
342 if is_group_start {
343 in_group_block = true;
344 brace_depth += line.matches('{').count() as i32;
345 brace_depth -= line.matches('}').count() as i32;
346 }
347 }
348 }
349
350 if keywords_active
351 && (line.contains("() {") || (trimmed.ends_with("()") && !in_function_block))
352 {
353 in_function_block = true;
354 brace_depth += line.matches('{').count() as i32;
355 brace_depth -= line.matches('}').count() as i32;
356 } else if in_function_block || in_group_block {
357 brace_depth += line.matches('{').count() as i32;
358 brace_depth -= line.matches('}').count() as i32;
359 }
360
361 if !current_block.is_empty() {
362 current_block.push('\n');
363 }
364 current_block.push_str(line);
365
366 if keywords_active {
367 if (in_function_block || in_group_block) && brace_depth == 0 {
368 in_function_block = false;
369 in_group_block = false;
370 execute_line(¤t_block, shell_state);
371 current_block.clear();
372
373 if shell_state.exit_requested {
374 break;
375 }
376 } else if in_if_block && contains_keyword(line, "fi") {
377 if_depth -= 1;
378 if if_depth == 0 {
379 in_if_block = false;
380 if !in_for_block
382 && !in_while_block
383 && !in_until_block
384 && !in_function_block
385 && !in_group_block
386 && !in_case_block
387 {
388 execute_line(¤t_block, shell_state);
389 current_block.clear();
390
391 if shell_state.exit_requested {
392 break;
393 }
394 }
395 }
396 } else if in_for_block && contains_keyword(line, "done") {
397 for_depth -= 1;
398 if for_depth == 0 {
399 in_for_block = false;
400 execute_line(¤t_block, shell_state);
401 current_block.clear();
402
403 if shell_state.exit_requested {
404 break;
405 }
406 }
407 } else if in_while_block && contains_keyword(line, "done") {
408 while_depth -= 1;
409 if while_depth == 0 {
410 in_while_block = false;
411 execute_line(¤t_block, shell_state);
412 current_block.clear();
413
414 if shell_state.exit_requested {
415 break;
416 }
417 }
418 } else if in_until_block && contains_keyword(line, "done") {
419 until_depth -= 1;
420 if until_depth == 0 {
421 in_until_block = false;
422 execute_line(¤t_block, shell_state);
423 current_block.clear();
424
425 if shell_state.exit_requested {
426 break;
427 }
428 }
429 } else if in_case_block && contains_keyword(line, "esac") {
430 in_case_block = false;
431 execute_line(¤t_block, shell_state);
432 current_block.clear();
433
434 if shell_state.exit_requested {
435 break;
436 }
437 } else if !in_if_block
438 && !in_case_block
439 && !in_function_block
440 && !in_group_block
441 && !in_for_block
442 && !in_while_block
443 && !in_until_block
444 {
445 if let Some(delimiter) = line_contains_heredoc(¤t_block, shell_state) {
446 i += 1;
447 let mut heredoc_content = String::new();
448 while i < lines.len() {
449 let content_line = lines[i];
450 if content_line.trim() == delimiter.trim() {
451 break;
452 }
453 if !heredoc_content.is_empty() {
454 heredoc_content.push('\n');
455 }
456 heredoc_content.push_str(content_line);
457 i += 1;
458 }
459 shell_state.pending_heredoc_content = Some(heredoc_content);
460 execute_line(¤t_block, shell_state);
461 current_block.clear();
462 } else if !in_single_quote
463 && !in_double_quote
464 && (line.ends_with(';') || !line.trim_end().ends_with('\\'))
465 {
466 execute_line(¤t_block, shell_state);
467 current_block.clear();
468 }
469 }
470 }
471 i += 1;
472 }
473}