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 mut 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 while let Some(ch) = chars.next() {
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 while let Some(ch) = chars.next() {
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 if flag.load(Ordering::Relaxed) {
255 eprintln!("Script interrupted by SIGTERM");
256 break;
257 }
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 mut chars = line.chars().peekable();
273 let mut escaped = false;
274
275 while let Some(ch) = chars.next() {
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 if {
331 let trimmed = line.trim();
332 trimmed == "{" || trimmed.starts_with("{ ") || trimmed.starts_with("{\t")
333 } {
334 in_group_block = true;
335 brace_depth += line.matches('{').count() as i32;
336 brace_depth -= line.matches('}').count() as i32;
337 }
338 }
339
340 if keywords_active
341 && (line.contains("() {") || (trimmed.ends_with("()") && !in_function_block))
342 {
343 in_function_block = true;
344 brace_depth += line.matches('{').count() as i32;
345 brace_depth -= line.matches('}').count() as i32;
346 } else if in_function_block || in_group_block {
347 brace_depth += line.matches('{').count() as i32;
348 brace_depth -= line.matches('}').count() as i32;
349 }
350
351 if !current_block.is_empty() {
352 current_block.push('\n');
353 }
354 current_block.push_str(line);
355
356 if keywords_active {
357 if (in_function_block || in_group_block) && brace_depth == 0 {
358 in_function_block = false;
359 in_group_block = false;
360 execute_line(¤t_block, shell_state);
361 current_block.clear();
362
363 if shell_state.exit_requested {
364 break;
365 }
366 } else if in_if_block && contains_keyword(line, "fi") {
367 if_depth -= 1;
368 if if_depth == 0 {
369 in_if_block = false;
370 if !in_for_block && !in_while_block && !in_until_block && !in_function_block && !in_group_block && !in_case_block {
372 execute_line(¤t_block, shell_state);
373 current_block.clear();
374
375 if shell_state.exit_requested {
376 break;
377 }
378 }
379 }
380 } else if in_for_block && contains_keyword(line, "done") {
381 for_depth -= 1;
382 if for_depth == 0 {
383 in_for_block = false;
384 execute_line(¤t_block, shell_state);
385 current_block.clear();
386
387 if shell_state.exit_requested {
388 break;
389 }
390 }
391 } else if in_while_block && contains_keyword(line, "done") {
392 while_depth -= 1;
393 if while_depth == 0 {
394 in_while_block = false;
395 execute_line(¤t_block, shell_state);
396 current_block.clear();
397
398 if shell_state.exit_requested {
399 break;
400 }
401 }
402 } else if in_until_block && contains_keyword(line, "done") {
403 until_depth -= 1;
404 if until_depth == 0 {
405 in_until_block = false;
406 execute_line(¤t_block, shell_state);
407 current_block.clear();
408
409 if shell_state.exit_requested {
410 break;
411 }
412 }
413 } else if in_case_block && contains_keyword(line, "esac") {
414 in_case_block = false;
415 execute_line(¤t_block, shell_state);
416 current_block.clear();
417
418 if shell_state.exit_requested {
419 break;
420 }
421 } else if !in_if_block
422 && !in_case_block
423 && !in_function_block
424 && !in_group_block
425 && !in_for_block
426 && !in_while_block
427 && !in_until_block
428 {
429 if let Some(delimiter) = line_contains_heredoc(¤t_block, shell_state) {
430 i += 1;
431 let mut heredoc_content = String::new();
432 while i < lines.len() {
433 let content_line = lines[i];
434 if content_line.trim() == delimiter.trim() {
435 break;
436 }
437 if !heredoc_content.is_empty() {
438 heredoc_content.push('\n');
439 }
440 heredoc_content.push_str(content_line);
441 i += 1;
442 }
443 shell_state.pending_heredoc_content = Some(heredoc_content);
444 execute_line(¤t_block, shell_state);
445 current_block.clear();
446 } else if !in_single_quote
447 && !in_double_quote
448 && (line.ends_with(';') || !line.trim_end().ends_with('\\'))
449 {
450 execute_line(¤t_block, shell_state);
451 current_block.clear();
452 }
453 }
454 }
455 i += 1;
456 }
457}