Skip to main content

frost_exec/
env.rs

1//! Shell environment — variables, functions, aliases, and process state.
2
3use std::collections::HashMap;
4use std::ffi::{CString, OsStr};
5use std::os::unix::ffi::OsStrExt;
6
7use frost_builtins::ShellEnvironment;
8use frost_parser::ast::FunctionDef;
9
10// ── Shell variable ───────────────────────────────────────────────────
11
12/// A shell variable with metadata.
13#[derive(Debug, Clone)]
14pub struct ShellVar {
15    /// The variable's value.
16    pub value: String,
17    /// Whether the variable is exported to child processes.
18    pub export: bool,
19    /// Whether the variable is read-only.
20    pub readonly: bool,
21}
22
23impl ShellVar {
24    /// Create a new mutable, unexported variable.
25    pub fn new(value: impl Into<String>) -> Self {
26        Self {
27            value: value.into(),
28            export: false,
29            readonly: false,
30        }
31    }
32
33    /// Create a new exported variable.
34    pub fn exported(value: impl Into<String>) -> Self {
35        Self {
36            value: value.into(),
37            export: true,
38            readonly: false,
39        }
40    }
41}
42
43// ── Shell environment ────────────────────────────────────────────────
44
45/// The full shell environment state.
46#[derive(Debug, Clone)]
47pub struct ShellEnv {
48    /// Named variables.
49    pub variables: HashMap<String, ShellVar>,
50    /// Shell functions (stored as AST nodes).
51    pub functions: HashMap<String, FunctionDef>,
52    /// Aliases: name -> replacement text.
53    pub aliases: HashMap<String, String>,
54    /// Exit status of the last command (`$?`).
55    pub exit_status: i32,
56    /// PID of the shell process (`$$`).
57    pub pid: u32,
58    /// Parent PID (`$PPID`).
59    pub ppid: u32,
60    /// Positional parameters (`$1`, `$2`, ...).
61    pub positional_params: Vec<String>,
62}
63
64impl ShellEnv {
65    /// Create a new environment, inheriting the OS environment variables
66    /// and capturing the current PID/PPID.
67    pub fn new() -> Self {
68        let mut variables = HashMap::new();
69
70        // Import process environment.
71        for (key, value) in std::env::vars() {
72            variables.insert(
73                key,
74                ShellVar {
75                    value,
76                    export: true,
77                    readonly: false,
78                },
79            );
80        }
81
82        let pid = std::process::id();
83        let ppid = nix::unistd::getppid().as_raw() as u32;
84
85        Self {
86            variables,
87            functions: HashMap::new(),
88            aliases: HashMap::new(),
89            exit_status: 0,
90            pid,
91            ppid,
92            positional_params: Vec::new(),
93        }
94    }
95
96    /// Get a variable's value, if it exists.
97    pub fn get_var(&self, name: &str) -> Option<&str> {
98        self.variables.get(name).map(|v| v.value.as_str())
99    }
100
101    /// Set a variable. Fails silently if the variable is read-only.
102    pub fn set_var(&mut self, name: &str, value: &str) {
103        if let Some(existing) = self.variables.get(name) {
104            if existing.readonly {
105                eprintln!("frost: {name}: readonly variable");
106                return;
107            }
108        }
109        self.variables
110            .entry(name.to_owned())
111            .and_modify(|v| v.value = value.to_owned())
112            .or_insert_with(|| ShellVar::new(value));
113    }
114
115    /// Mark a variable as exported.
116    pub fn export_var(&mut self, name: &str) {
117        if let Some(var) = self.variables.get_mut(name) {
118            var.export = true;
119        } else {
120            // Export an empty variable if it doesn't exist yet.
121            self.variables
122                .insert(name.to_owned(), ShellVar::exported(""));
123        }
124    }
125
126    /// Remove a variable. Fails silently if it is read-only.
127    pub fn unset_var(&mut self, name: &str) {
128        if let Some(var) = self.variables.get(name) {
129            if var.readonly {
130                eprintln!("frost: {name}: readonly variable");
131                return;
132            }
133        }
134        self.variables.remove(name);
135    }
136
137    /// Build the `envp` array for `execve(2)`.
138    ///
139    /// Returns a list of `CString`s in `KEY=VALUE` format for every
140    /// exported variable.
141    pub fn to_env_vec(&self) -> Vec<CString> {
142        self.variables
143            .iter()
144            .filter(|(_, v)| v.export)
145            .filter_map(|(k, v)| {
146                let entry = format!("{k}={}", v.value);
147                CString::new(entry).ok()
148            })
149            .collect()
150    }
151
152    /// Build an `argv` from an `OsStr` slice (convenience for fork/exec).
153    pub fn to_argv(words: &[impl AsRef<OsStr>]) -> Vec<CString> {
154        words
155            .iter()
156            .filter_map(|w| CString::new(w.as_ref().as_bytes()).ok())
157            .collect()
158    }
159}
160
161impl Default for ShellEnv {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167// ── ShellEnvironment trait impl ──────────────────────────────────────
168
169/// Implements the `frost_builtins::ShellEnvironment` trait so that
170/// built-in commands can interact with the shell state.
171impl ShellEnvironment for ShellEnv {
172    fn get_var(&self, name: &str) -> Option<&str> {
173        self.get_var(name)
174    }
175
176    fn set_var(&mut self, name: &str, value: &str) {
177        self.set_var(name, value);
178    }
179
180    fn export_var(&mut self, name: &str) {
181        self.export_var(name);
182    }
183
184    fn unset_var(&mut self, name: &str) {
185        self.unset_var(name);
186    }
187
188    fn exit_status(&self) -> i32 {
189        self.exit_status
190    }
191
192    fn set_exit_status(&mut self, status: i32) {
193        self.exit_status = status;
194    }
195
196    fn chdir(&mut self, path: &str) -> Result<(), String> {
197        std::env::set_current_dir(path).map_err(|e| e.to_string())?;
198        self.set_var("PWD", path);
199        Ok(())
200    }
201
202    fn home_dir(&self) -> Option<&str> {
203        self.get_var("HOME")
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use pretty_assertions::assert_eq;
211
212    #[test]
213    fn set_and_get_var() {
214        let mut env = ShellEnv::new();
215        env.set_var("FOO", "bar");
216        assert_eq!(env.get_var("FOO"), Some("bar"));
217    }
218
219    #[test]
220    fn export_var_appears_in_env_vec() {
221        let mut env = ShellEnv::new();
222        // Clear inherited env for a clean test.
223        env.variables.clear();
224        env.set_var("MY_VAR", "hello");
225        env.export_var("MY_VAR");
226        let vec = env.to_env_vec();
227        let entry = CString::new("MY_VAR=hello").unwrap();
228        assert!(vec.contains(&entry));
229    }
230
231    #[test]
232    fn unexported_var_not_in_env_vec() {
233        let mut env = ShellEnv::new();
234        env.variables.clear();
235        env.set_var("HIDDEN", "secret");
236        let vec = env.to_env_vec();
237        assert!(vec.is_empty());
238    }
239
240    #[test]
241    fn readonly_var_cannot_be_set() {
242        let mut env = ShellEnv::new();
243        env.variables.insert(
244            "RO".into(),
245            ShellVar {
246                value: "locked".into(),
247                export: false,
248                readonly: true,
249            },
250        );
251        env.set_var("RO", "changed");
252        assert_eq!(env.get_var("RO"), Some("locked"));
253    }
254
255    #[test]
256    fn readonly_var_cannot_be_unset() {
257        let mut env = ShellEnv::new();
258        env.variables.insert(
259            "RO".into(),
260            ShellVar {
261                value: "locked".into(),
262                export: false,
263                readonly: true,
264            },
265        );
266        env.unset_var("RO");
267        assert_eq!(env.get_var("RO"), Some("locked"));
268    }
269
270    #[test]
271    fn unset_var_removes_it() {
272        let mut env = ShellEnv::new();
273        env.set_var("GONE", "bye");
274        env.unset_var("GONE");
275        assert_eq!(env.get_var("GONE"), None);
276    }
277
278    #[test]
279    fn positional_params() {
280        let mut env = ShellEnv::new();
281        env.positional_params = vec!["a".into(), "b".into(), "c".into()];
282        assert_eq!(env.positional_params.len(), 3);
283        assert_eq!(env.positional_params[0], "a");
284    }
285}