1use 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#[derive(Debug, Clone)]
14pub struct ShellVar {
15 pub value: String,
17 pub export: bool,
19 pub readonly: bool,
21}
22
23impl ShellVar {
24 pub fn new(value: impl Into<String>) -> Self {
26 Self {
27 value: value.into(),
28 export: false,
29 readonly: false,
30 }
31 }
32
33 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#[derive(Debug, Clone)]
47pub struct ShellEnv {
48 pub variables: HashMap<String, ShellVar>,
50 pub functions: HashMap<String, FunctionDef>,
52 pub aliases: HashMap<String, String>,
54 pub exit_status: i32,
56 pub pid: u32,
58 pub ppid: u32,
60 pub positional_params: Vec<String>,
62}
63
64impl ShellEnv {
65 pub fn new() -> Self {
68 let mut variables = HashMap::new();
69
70 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 pub fn get_var(&self, name: &str) -> Option<&str> {
98 self.variables.get(name).map(|v| v.value.as_str())
99 }
100
101 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 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 self.variables
122 .insert(name.to_owned(), ShellVar::exported(""));
123 }
124 }
125
126 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 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 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
167impl 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 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}