Skip to main content

strat9_kernel/shell/
scripting.rs

1//! Minimal shell scripting: variable expansion, `if`, `for`, `while`.
2//!
3//! # Supported constructs
4//!
5//! - **Variables**: `set VAR=VALUE`, `$VAR` expansion, `$?` for last exit code.
6//! - **For loops**: `for VAR in A B C ; do COMMAND ; done`
7//! - **While loops**: `while COMMAND ; do BODY ; done`
8//! - **If/then**: `if COMMAND ; then BODY ; fi` or `if COMMAND ; then A ; else B ; fi`
9
10use crate::sync::SpinLock;
11use alloc::{
12    collections::BTreeMap,
13    string::{String, ToString},
14    vec::Vec,
15};
16
17// SpinLock over BTreeMap<String, String>: administrative scripting surface,
18// not on any system hot path.  String allocation under this lock is acceptable.
19// Tracked as low-priority debt in ticket #49.
20static SHELL_VARS: SpinLock<BTreeMap<String, String>> = SpinLock::new(BTreeMap::new());
21static LAST_EXIT: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(0);
22
23/// Set the exit code of the last executed command.
24pub fn set_last_exit(code: i32) {
25    LAST_EXIT.store(code, core::sync::atomic::Ordering::Relaxed);
26}
27
28/// Get the exit code of the last executed command.
29pub fn last_exit() -> i32 {
30    LAST_EXIT.load(core::sync::atomic::Ordering::Relaxed)
31}
32
33/// Set a shell variable.
34pub fn set_var(key: &str, val: &str) {
35    SHELL_VARS
36        .lock()
37        .insert(String::from(key), String::from(val));
38}
39
40/// Get a shell variable.
41pub fn get_var(key: &str) -> Option<String> {
42    SHELL_VARS.lock().get(key).cloned()
43}
44
45/// Remove a shell variable.
46pub fn unset_var(key: &str) {
47    SHELL_VARS.lock().remove(key);
48}
49
50/// Expand `$VAR` and `$?` references in a string.
51pub fn expand_vars(input: &str) -> String {
52    let mut result = String::new();
53    let bytes = input.as_bytes();
54    let mut i = 0;
55
56    while i < bytes.len() {
57        if bytes[i] == b'$' {
58            i += 1;
59            if i < bytes.len() && bytes[i] == b'?' {
60                result.push_str(&alloc::format!("{}", last_exit()));
61                i += 1;
62            } else {
63                let start = i;
64                while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
65                    i += 1;
66                }
67                if i == start {
68                    // Keep literal '$' when no variable name follows.
69                    result.push('$');
70                    continue;
71                }
72                let var_name = core::str::from_utf8(&bytes[start..i]).unwrap_or("");
73                if let Some(val) = get_var(var_name) {
74                    result.push_str(&val);
75                } else if let Some(env_val) = super::commands::util::shell_getenv(var_name) {
76                    result.push_str(&env_val);
77                }
78            }
79        } else {
80            result.push(bytes[i] as char);
81            i += 1;
82        }
83    }
84
85    result
86}
87
88/// Script construct types recognized by the parser.
89pub enum ScriptConstruct {
90    /// Simple command line (possibly with pipes).
91    Simple(String),
92    /// `set VAR=VALUE`
93    SetVar { key: String, val: String },
94    /// `unset VAR`
95    UnsetVar(String),
96    /// `for VAR in ITEMS ; do BODY ; done`
97    ForLoop {
98        var: String,
99        items: Vec<String>,
100        body: Vec<String>,
101    },
102    /// `while COND ; do BODY ; done`
103    WhileLoop { cond: String, body: Vec<String> },
104    /// `if COND ; then THEN_BODY [; else ELSE_BODY] ; fi`
105    IfElse {
106        cond: String,
107        then_body: Vec<String>,
108        else_body: Vec<String>,
109    },
110}
111
112/// Parse a full line into a script construct.
113///
114/// The parser splits on `;` to find keywords. For simple commands
115/// (no scripting keywords), returns [`ScriptConstruct::Simple`].
116pub fn parse_script(line: &str) -> ScriptConstruct {
117    let trimmed = line.trim();
118
119    if trimmed.starts_with("set ") {
120        let rest = &trimmed[4..];
121        if let Some(eq) = rest.find('=') {
122            return ScriptConstruct::SetVar {
123                key: String::from(rest[..eq].trim()),
124                val: String::from(rest[eq + 1..].trim()),
125            };
126        }
127        return ScriptConstruct::Simple(String::from(trimmed));
128    }
129
130    if trimmed.starts_with("unset ") {
131        return ScriptConstruct::UnsetVar(String::from(trimmed[6..].trim()));
132    }
133
134    let parts: Vec<&str> = trimmed.split(';').map(|s| s.trim()).collect();
135
136    if parts.first().map(|p| p.starts_with("for ")) == Some(true) {
137        return parse_for_loop(&parts);
138    }
139    if parts.first().map(|p| p.starts_with("while ")) == Some(true) {
140        return parse_while_loop(&parts);
141    }
142    if parts.first().map(|p| p.starts_with("if ")) == Some(true) {
143        return parse_if_else(&parts);
144    }
145
146    ScriptConstruct::Simple(String::from(trimmed))
147}
148
149/// `for VAR in A B C ; do cmd1 ; cmd2 ; done`
150fn parse_for_loop(parts: &[&str]) -> ScriptConstruct {
151    if parts.is_empty() {
152        return ScriptConstruct::Simple(String::new());
153    }
154    let header = parts[0];
155    let tokens: Vec<&str> = header.split_whitespace().collect();
156    if tokens.len() < 4 || tokens[0] != "for" || tokens[2] != "in" {
157        return ScriptConstruct::Simple(String::from(header));
158    }
159
160    let var = tokens.get(1).unwrap_or(&"_").to_string();
161    let items: Vec<String> = tokens
162        .iter()
163        .skip(3) // skip "for VAR in"
164        .map(|s| String::from(*s))
165        .collect();
166
167    let Some(do_idx) = parts.iter().position(|p| *p == "do") else {
168        return ScriptConstruct::Simple(String::from(header));
169    };
170    let Some(done_idx) = parts.iter().position(|p| *p == "done") else {
171        return ScriptConstruct::Simple(String::from(header));
172    };
173    if done_idx <= do_idx {
174        return ScriptConstruct::Simple(String::from(header));
175    }
176
177    let body: Vec<String> = parts[do_idx + 1..done_idx]
178        .iter()
179        .filter(|s| !s.is_empty())
180        .map(|s| String::from(*s))
181        .collect();
182
183    ScriptConstruct::ForLoop {
184        var: String::from(var),
185        items,
186        body,
187    }
188}
189
190/// `while cond ; do body ; done`
191fn parse_while_loop(parts: &[&str]) -> ScriptConstruct {
192    if parts.is_empty() {
193        return ScriptConstruct::Simple(String::new());
194    }
195    let header = parts[0];
196    let Some(cond) = header.strip_prefix("while ") else {
197        return ScriptConstruct::Simple(String::from(header));
198    };
199    let Some(do_idx) = parts.iter().position(|p| *p == "do") else {
200        return ScriptConstruct::Simple(String::from(header));
201    };
202    let Some(done_idx) = parts.iter().position(|p| *p == "done") else {
203        return ScriptConstruct::Simple(String::from(header));
204    };
205    if done_idx <= do_idx {
206        return ScriptConstruct::Simple(String::from(header));
207    }
208
209    let body: Vec<String> = parts[do_idx + 1..done_idx]
210        .iter()
211        .filter(|s| !s.is_empty())
212        .map(|s| String::from(*s))
213        .collect();
214
215    ScriptConstruct::WhileLoop {
216        cond: String::from(cond),
217        body,
218    }
219}
220
221/// `if cond ; then body ; [else body ;] fi`
222fn parse_if_else(parts: &[&str]) -> ScriptConstruct {
223    if parts.is_empty() {
224        return ScriptConstruct::Simple(String::new());
225    }
226    let header = parts[0];
227    let Some(cond) = header.strip_prefix("if ") else {
228        return ScriptConstruct::Simple(String::from(header));
229    };
230    let Some(then_idx) = parts.iter().position(|p| *p == "then") else {
231        return ScriptConstruct::Simple(String::from(header));
232    };
233    let else_idx = parts.iter().position(|p| *p == "else");
234    let Some(fi_idx) = parts.iter().position(|p| *p == "fi") else {
235        return ScriptConstruct::Simple(String::from(header));
236    };
237    if fi_idx <= then_idx {
238        return ScriptConstruct::Simple(String::from(header));
239    }
240
241    let then_end = else_idx.unwrap_or(fi_idx);
242    let then_body: Vec<String> = parts[then_idx + 1..then_end]
243        .iter()
244        .filter(|s| !s.is_empty())
245        .map(|s| String::from(*s))
246        .collect();
247
248    let else_body: Vec<String> = if let Some(ei) = else_idx {
249        parts[ei + 1..fi_idx]
250            .iter()
251            .filter(|s| !s.is_empty())
252            .map(|s| String::from(*s))
253            .collect()
254    } else {
255        Vec::new()
256    };
257
258    ScriptConstruct::IfElse {
259        cond: String::from(cond),
260        then_body,
261        else_body,
262    }
263}