rush_sh/
script_engine.rs

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
9/// Check if a line contains a heredoc redirection using proper lexer-based detection
10/// Returns the delimiter if found, None otherwise
11pub fn line_contains_heredoc(line: &str, shell_state: &state::ShellState) -> Option<String> {
12    // Use the lexer to properly parse the line
13    match lexer::lex(line, shell_state) {
14        Ok(tokens) => {
15            // Look for a RedirHereDoc token
16            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
27/// Check if a line contains a specific keyword as a distinct token
28/// This handles comments and ensures the keyword is not part of another word
29pub 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            // Escaped characters are treated as part of the word
40            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; // Comment starts at word boundary
68                }
69                current_word.push(ch); // # inside word, treat as literal
70            }
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    // Check last word
91    current_word == keyword
92}
93
94/// Check if a line starts with a specific keyword
95pub fn starts_with_keyword(line: &str, keyword: &str) -> bool {
96    let mut chars = line.chars().peekable();
97    let mut current_word = String::new();
98
99    // Skip leading whitespace
100    while let Some(&ch) = chars.peek() {
101        if ch == ' ' || ch == '\t' {
102            chars.next();
103        } else {
104            break;
105        }
106    }
107
108    while let Some(ch) = chars.next() {
109        match ch {
110            ' ' | '\t' | '\n' | ';' | '|' | '&' | '(' | ')' | '{' | '}' => {
111                return current_word == keyword;
112            }
113            _ => current_word.push(ch),
114        }
115    }
116
117    current_word == keyword
118}
119
120pub fn execute_line(line: &str, shell_state: &mut state::ShellState) {
121    match lexer::lex(line, shell_state) {
122        Ok(tokens) => match lexer::expand_aliases(tokens, shell_state, &mut HashSet::new()) {
123            Ok(expanded_tokens) => match brace_expansion::expand_braces(expanded_tokens) {
124                Ok(brace_expanded_tokens) => match parser::parse(brace_expanded_tokens) {
125                    Ok(ast) => {
126                        let exit_code = executor::execute(ast, shell_state);
127                        shell_state.set_last_exit_code(exit_code);
128                    }
129                    Err(e) => {
130                        if shell_state.colors_enabled {
131                            eprintln!(
132                                "{}Parse error: {}\x1b[0m",
133                                shell_state.color_scheme.error, e
134                            );
135                        } else {
136                            eprintln!("Parse error: {}", e);
137                        }
138                        shell_state.set_last_exit_code(1);
139                    }
140                },
141                Err(e) => {
142                    if shell_state.colors_enabled {
143                        eprintln!(
144                            "{}Brace expansion error: {}\x1b[0m",
145                            shell_state.color_scheme.error, e
146                        );
147                    } else {
148                        eprintln!("Brace expansion error: {}", e);
149                    }
150                    shell_state.set_last_exit_code(1);
151                }
152            },
153            Err(e) => {
154                if shell_state.colors_enabled {
155                    eprintln!(
156                        "{}Alias expansion error: {}\x1b[0m",
157                        shell_state.color_scheme.error, e
158                    );
159                } else {
160                    eprintln!("Alias expansion error: {}", e);
161                }
162                shell_state.set_last_exit_code(1);
163            }
164        },
165        Err(e) => {
166            if shell_state.colors_enabled {
167                eprintln!("{}Lex error: {}\x1b[0m", shell_state.color_scheme.error, e);
168            } else {
169                eprintln!("Lex error: {}", e);
170            }
171            shell_state.set_last_exit_code(1);
172        }
173    }
174}
175
176pub fn execute_script(
177    content: &str,
178    shell_state: &mut state::ShellState,
179    shutdown_flag: Option<&AtomicBool>,
180) {
181    let mut current_block = String::new();
182    let mut in_if_block = false;
183    let mut if_depth = 0;
184    let mut in_case_block = false;
185    let mut in_function_block = false;
186    let mut in_group_block = false;
187    let mut brace_depth = 0;
188    let mut in_for_block = false;
189    let mut for_depth = 0;
190    let mut in_while_block = false;
191    let mut while_depth = 0;
192    let mut in_until_block = false;
193    let mut until_depth = 0;
194
195    // Track quote state across lines to handle multiline strings correctly
196    let mut in_double_quote = false;
197    let mut in_single_quote = false;
198
199    let lines: Vec<&str> = content.lines().collect();
200    let mut i = 0;
201
202    while i < lines.len() {
203        let line = lines[i];
204        // Process pending signals at the start of each line
205        state::process_pending_signals(shell_state);
206
207        // Check for shutdown signal
208        if let Some(flag) = shutdown_flag {
209            if flag.load(Ordering::Relaxed) {
210                eprintln!("Script interrupted by SIGTERM");
211                break;
212            }
213        }
214
215        // Check if exit was requested (e.g., from trap handler)
216        if shell_state.exit_requested {
217            break;
218        }
219
220        // Skip shebang lines
221        if line.starts_with("#!") {
222            i += 1;
223            continue;
224        }
225
226        // Update quote state based on this line
227        let mut chars = line.chars().peekable();
228        let mut escaped = false;
229
230        while let Some(ch) = chars.next() {
231            if escaped {
232                escaped = false;
233                continue;
234            }
235
236            if in_single_quote {
237                if ch == '\'' {
238                    in_single_quote = false;
239                }
240                continue;
241            }
242
243            if in_double_quote {
244                if ch == '"' {
245                    in_double_quote = false;
246                } else if ch == '\\' {
247                    escaped = true;
248                }
249                continue;
250            }
251
252            match ch {
253                '#' => break, // Comment starts
254                '\'' => in_single_quote = true,
255                '"' => in_double_quote = true,
256                '\\' => escaped = true,
257                _ => {}
258            }
259        }
260
261        let trimmed = line.trim();
262        if !in_double_quote && !in_single_quote && (trimmed.is_empty() || trimmed.starts_with("#"))
263        {
264            i += 1;
265            continue;
266        }
267
268        let keywords_active = !in_double_quote && !in_single_quote;
269
270        if keywords_active && !in_function_block {
271            if starts_with_keyword(line, "if") {
272                in_if_block = true;
273                if_depth += 1;
274            } else if starts_with_keyword(line, "case") {
275                in_case_block = true;
276            } else if starts_with_keyword(line, "for") {
277                in_for_block = true;
278                for_depth += 1;
279            } else if starts_with_keyword(line, "while") {
280                in_while_block = true;
281                while_depth += 1;
282            } else if starts_with_keyword(line, "until") {
283                in_until_block = true;
284                until_depth += 1;
285            } else if {
286                let trimmed = line.trim();
287                trimmed == "{" || trimmed.starts_with("{ ") || trimmed.starts_with("{\t")
288            } {
289                in_group_block = true;
290                brace_depth += line.matches('{').count() as i32;
291                brace_depth -= line.matches('}').count() as i32;
292            }
293        }
294
295        if keywords_active
296            && (line.contains("() {") || (trimmed.ends_with("()") && !in_function_block))
297        {
298            in_function_block = true;
299            brace_depth += line.matches('{').count() as i32;
300            brace_depth -= line.matches('}').count() as i32;
301        } else if in_function_block || in_group_block {
302            brace_depth += line.matches('{').count() as i32;
303            brace_depth -= line.matches('}').count() as i32;
304        }
305
306        if !current_block.is_empty() {
307            current_block.push('\n');
308        }
309        current_block.push_str(line);
310
311        if keywords_active {
312            if (in_function_block || in_group_block) && brace_depth == 0 {
313                in_function_block = false;
314                in_group_block = false;
315                execute_line(&current_block, shell_state);
316                current_block.clear();
317
318                if shell_state.exit_requested {
319                    break;
320                }
321            } else if in_if_block && contains_keyword(line, "fi") {
322                if_depth -= 1;
323                if if_depth == 0 {
324                    in_if_block = false;
325                    // Only execute if we're not inside a loop or other block
326                    if !in_for_block && !in_while_block && !in_until_block && !in_function_block && !in_group_block && !in_case_block {
327                        execute_line(&current_block, shell_state);
328                        current_block.clear();
329
330                        if shell_state.exit_requested {
331                            break;
332                        }
333                    }
334                }
335            } else if in_for_block && contains_keyword(line, "done") {
336                for_depth -= 1;
337                if for_depth == 0 {
338                    in_for_block = false;
339                    execute_line(&current_block, shell_state);
340                    current_block.clear();
341
342                    if shell_state.exit_requested {
343                        break;
344                    }
345                }
346            } else if in_while_block && contains_keyword(line, "done") {
347                while_depth -= 1;
348                if while_depth == 0 {
349                    in_while_block = false;
350                    execute_line(&current_block, shell_state);
351                    current_block.clear();
352
353                    if shell_state.exit_requested {
354                        break;
355                    }
356                }
357            } else if in_until_block && contains_keyword(line, "done") {
358                until_depth -= 1;
359                if until_depth == 0 {
360                    in_until_block = false;
361                    execute_line(&current_block, shell_state);
362                    current_block.clear();
363
364                    if shell_state.exit_requested {
365                        break;
366                    }
367                }
368            } else if in_case_block && contains_keyword(line, "esac") {
369                in_case_block = false;
370                execute_line(&current_block, shell_state);
371                current_block.clear();
372
373                if shell_state.exit_requested {
374                    break;
375                }
376            } else if !in_if_block
377                && !in_case_block
378                && !in_function_block
379                && !in_group_block
380                && !in_for_block
381                && !in_while_block
382                && !in_until_block
383            {
384                if let Some(delimiter) = line_contains_heredoc(&current_block, shell_state) {
385                    i += 1;
386                    let mut heredoc_content = String::new();
387                    while i < lines.len() {
388                        let content_line = lines[i];
389                        if content_line.trim() == delimiter.trim() {
390                            break;
391                        }
392                        if !heredoc_content.is_empty() {
393                            heredoc_content.push('\n');
394                        }
395                        heredoc_content.push_str(content_line);
396                        i += 1;
397                    }
398                    shell_state.pending_heredoc_content = Some(heredoc_content);
399                    execute_line(&current_block, shell_state);
400                    current_block.clear();
401                } else if !in_single_quote
402                    && !in_double_quote
403                    && (line.ends_with(';') || !line.trim_end().ends_with('\\'))
404                {
405                    execute_line(&current_block, shell_state);
406                    current_block.clear();
407                }
408            }
409        }
410        i += 1;
411    }
412}