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
193    // Track quote state across lines to handle multiline strings correctly
194    let mut in_double_quote = false;
195    let mut in_single_quote = false;
196
197    let lines: Vec<&str> = content.lines().collect();
198    let mut i = 0;
199
200    while i < lines.len() {
201        let line = lines[i];
202        // Process pending signals at the start of each line
203        state::process_pending_signals(shell_state);
204
205        // Check for shutdown signal
206        if let Some(flag) = shutdown_flag {
207            if flag.load(Ordering::Relaxed) {
208                eprintln!("Script interrupted by SIGTERM");
209                break;
210            }
211        }
212
213        // Check if exit was requested (e.g., from trap handler)
214        if shell_state.exit_requested {
215            break;
216        }
217
218        // Skip shebang lines
219        if line.starts_with("#!") {
220            i += 1;
221            continue;
222        }
223
224        // Update quote state based on this line
225        let mut chars = line.chars().peekable();
226        let mut escaped = false;
227
228        while let Some(ch) = chars.next() {
229            if escaped {
230                escaped = false;
231                continue;
232            }
233
234            if in_single_quote {
235                if ch == '\'' {
236                    in_single_quote = false;
237                }
238                continue;
239            }
240
241            if in_double_quote {
242                if ch == '"' {
243                    in_double_quote = false;
244                } else if ch == '\\' {
245                    escaped = true;
246                }
247                continue;
248            }
249
250            match ch {
251                '#' => break, // Comment starts
252                '\'' => in_single_quote = true,
253                '"' => in_double_quote = true,
254                '\\' => escaped = true,
255                _ => {}
256            }
257        }
258
259        let trimmed = line.trim();
260        if !in_double_quote && !in_single_quote && (trimmed.is_empty() || trimmed.starts_with("#"))
261        {
262            i += 1;
263            continue;
264        }
265
266        let keywords_active = !in_double_quote && !in_single_quote;
267
268        if keywords_active && !in_function_block {
269            if starts_with_keyword(line, "if") {
270                in_if_block = true;
271                if_depth += 1;
272            } else if starts_with_keyword(line, "case") {
273                in_case_block = true;
274            } else if starts_with_keyword(line, "for") {
275                in_for_block = true;
276                for_depth += 1;
277            } else if starts_with_keyword(line, "while") {
278                in_while_block = true;
279                while_depth += 1;
280            } else if {
281                let trimmed = line.trim();
282                trimmed == "{" || trimmed.starts_with("{ ") || trimmed.starts_with("{\t")
283            } {
284                in_group_block = true;
285                brace_depth += line.matches('{').count() as i32;
286                brace_depth -= line.matches('}').count() as i32;
287            }
288        }
289
290        if keywords_active
291            && (line.contains("() {") || (trimmed.ends_with("()") && !in_function_block))
292        {
293            in_function_block = true;
294            brace_depth += line.matches('{').count() as i32;
295            brace_depth -= line.matches('}').count() as i32;
296        } else if in_function_block || in_group_block {
297            brace_depth += line.matches('{').count() as i32;
298            brace_depth -= line.matches('}').count() as i32;
299        }
300
301        if !current_block.is_empty() {
302            current_block.push('\n');
303        }
304        current_block.push_str(line);
305
306        if keywords_active {
307            if (in_function_block || in_group_block) && brace_depth == 0 {
308                in_function_block = false;
309                in_group_block = false;
310                execute_line(&current_block, shell_state);
311                current_block.clear();
312
313                if shell_state.exit_requested {
314                    break;
315                }
316            } else if in_if_block && contains_keyword(line, "fi") {
317                if_depth -= 1;
318                if if_depth == 0 {
319                    in_if_block = false;
320                    execute_line(&current_block, shell_state);
321                    current_block.clear();
322
323                    if shell_state.exit_requested {
324                        break;
325                    }
326                }
327            } else if in_for_block && contains_keyword(line, "done") {
328                for_depth -= 1;
329                if for_depth == 0 {
330                    in_for_block = false;
331                    execute_line(&current_block, shell_state);
332                    current_block.clear();
333
334                    if shell_state.exit_requested {
335                        break;
336                    }
337                }
338            } else if in_while_block && contains_keyword(line, "done") {
339                while_depth -= 1;
340                if while_depth == 0 {
341                    in_while_block = false;
342                    execute_line(&current_block, shell_state);
343                    current_block.clear();
344
345                    if shell_state.exit_requested {
346                        break;
347                    }
348                }
349            } else if in_case_block && contains_keyword(line, "esac") {
350                in_case_block = false;
351                execute_line(&current_block, shell_state);
352                current_block.clear();
353
354                if shell_state.exit_requested {
355                    break;
356                }
357            } else if !in_if_block
358                && !in_case_block
359                && !in_function_block
360                && !in_group_block
361                && !in_for_block
362                && !in_while_block
363            {
364                if let Some(delimiter) = line_contains_heredoc(&current_block, shell_state) {
365                    i += 1;
366                    let mut heredoc_content = String::new();
367                    while i < lines.len() {
368                        let content_line = lines[i];
369                        if content_line.trim() == delimiter.trim() {
370                            break;
371                        }
372                        if !heredoc_content.is_empty() {
373                            heredoc_content.push('\n');
374                        }
375                        heredoc_content.push_str(content_line);
376                        i += 1;
377                    }
378                    shell_state.pending_heredoc_content = Some(heredoc_content);
379                    execute_line(&current_block, shell_state);
380                    current_block.clear();
381                } else if !in_single_quote
382                    && !in_double_quote
383                    && (line.ends_with(';') || !line.trim_end().ends_with('\\'))
384                {
385                    execute_line(&current_block, shell_state);
386                    current_block.clear();
387                }
388            }
389        }
390        i += 1;
391    }
392}