ps_parser/parser/
variables.rs

1mod variable;
2use std::collections::HashMap;
3
4use thiserror_no_std::Error;
5pub(super) use variable::{Scope, VarName, VarProp, Variable};
6
7use crate::parser::Val;
8
9#[derive(Error, Debug, PartialEq, Clone)]
10pub enum VariableError {
11    #[error("Variable \"{0}\" is not defined")]
12    NotDefined(String),
13    #[error("Cannot overwrite variable \"{0}\" because it is read-only or constant.")]
14    ReadOnly(String),
15}
16
17pub type VariableResult<T> = core::result::Result<T, VariableError>;
18
19#[derive(Default, Clone)]
20pub struct Variables {
21    map: HashMap<VarName, Variable>,
22    force_var_eval: bool,
23    //special variables
24    // status: bool, // $?
25    // first_token: Option<String>,
26    // last_token: Option<String>,
27    // current_pipeline: Option<String>,
28}
29
30impl Variables {
31    fn const_variables() -> HashMap<VarName, Variable> {
32        HashMap::from([
33            (
34                VarName::new(Scope::Global, "true".to_ascii_lowercase()),
35                Variable::new(VarProp::ReadOnly, Val::Bool(true)),
36            ),
37            (
38                VarName::new(Scope::Global, "false".to_ascii_lowercase()),
39                Variable::new(VarProp::ReadOnly, Val::Bool(false)),
40            ),
41            (
42                VarName::new(Scope::Global, "null".to_ascii_lowercase()),
43                Variable::new(VarProp::ReadOnly, Val::Null),
44            ),
45        ])
46    }
47
48    pub(crate) fn set_ps_item(&mut self, ps_item: Val) {
49        let _ = self.set(
50            &VarName::new(Scope::Special, "$PSItem".into()),
51            ps_item.clone(),
52        );
53        let _ = self.set(&VarName::new(Scope::Special, "$_".into()), ps_item);
54    }
55
56    pub(crate) fn reset_ps_item(&mut self) {
57        let _ = self.set(&VarName::new(Scope::Special, "$PSItem".into()), Val::Null);
58        let _ = self.set(&VarName::new(Scope::Special, "$_".into()), Val::Null);
59    }
60
61    pub fn set_status(&mut self, b: bool) {
62        let _ = self.set(&VarName::new(Scope::Special, "$?".into()), Val::Bool(b));
63    }
64
65    pub fn status(&mut self) -> bool {
66        let Some(Val::Bool(b)) = self.get(&VarName::new(Scope::Special, "$?".into())) else {
67            return false;
68        };
69        b
70    }
71
72    pub fn load_from_file(
73        &mut self,
74        path: &std::path::Path,
75    ) -> Result<(), Box<dyn std::error::Error>> {
76        let mut config_parser = configparser::ini::Ini::new();
77        let map = config_parser.load(path)?;
78        self.load(map)
79    }
80
81    pub fn load_from_string(&mut self, ini_string: &str) -> Result<(), Box<dyn std::error::Error>> {
82        let mut config_parser = configparser::ini::Ini::new();
83        let map = config_parser.read(ini_string.into())?;
84        self.load(map)
85    }
86
87    fn load(
88        &mut self,
89        conf_map: HashMap<String, HashMap<String, Option<String>>>,
90    ) -> Result<(), Box<dyn std::error::Error>> {
91        for (section_name, properties) in conf_map {
92            for (key, value) in properties {
93                let Some(value) = value else {
94                    continue;
95                };
96
97                let var_name = match section_name.as_str() {
98                    "global" => VarName::new(Scope::Global, key.to_lowercase()),
99                    "local" => VarName::new(Scope::Local, key.to_lowercase()),
100                    _ => {
101                        continue;
102                    }
103                };
104
105                // Try to parse the value as different types
106                let parsed_value = if let Ok(bool_val) = value.parse::<bool>() {
107                    Val::Bool(bool_val)
108                } else if let Ok(int_val) = value.parse::<i64>() {
109                    Val::Int(int_val)
110                } else if let Ok(float_val) = value.parse::<f64>() {
111                    Val::Float(float_val)
112                } else if value.is_empty() {
113                    Val::Null
114                } else {
115                    Val::String(value.clone().into())
116                };
117
118                // Insert the variable (overwrite if it exists and is not read-only)
119                if let Some(variable) = self.map.get(&var_name) {
120                    if variable.prop == VarProp::ReadOnly {
121                        log::warn!("Skipping read-only variable: {:?}", var_name);
122                        continue;
123                    }
124                }
125
126                self.map
127                    .insert(var_name, Variable::new(VarProp::ReadWrite, parsed_value));
128            }
129        }
130        Ok(())
131    }
132
133    /// Creates a new empty Variables container.
134    ///
135    /// # Arguments
136    ///
137    /// * initializes the container with PowerShell built-in variables like
138    ///   `$true`, `$false`, `$null`, and `$?`. If `false`,
139    ///
140    /// # Returns
141    ///
142    /// A new `Variables` instance.
143    ///
144    /// # Examples
145    ///
146    /// ```rust
147    /// use ps_parser::Variables;
148    ///
149    /// // Create with built-in variables
150    /// let vars_with_builtins = Variables::new();
151    ///
152    /// // Create empty
153    /// let empty_vars = Variables::new();
154    /// ```
155    pub fn new() -> Variables {
156        let map = Self::const_variables();
157
158        Self {
159            map,
160            force_var_eval: false,
161        }
162    }
163
164    /// Creates a new Variables container with forced evaluation enabled.
165    ///
166    /// This constructor creates a Variables instance that will return
167    /// `Val::Null` for undefined variables instead of returning `None`.
168    /// This is useful for PowerShell script evaluation where undefined
169    /// variables should be treated as `$null` rather than causing errors.
170    ///
171    /// # Returns
172    ///
173    /// A new `Variables` instance with forced evaluation enabled and built-in
174    /// variables initialized.
175    ///
176    /// # Examples
177    ///
178    /// ```rust
179    /// use ps_parser::{Variables, PowerShellSession};
180    ///
181    /// // Create with forced evaluation
182    /// let vars = Variables::force_eval();
183    /// let mut session = PowerShellSession::new().with_variables(vars);
184    ///
185    /// // Undefined variables will evaluate to $null instead of causing errors
186    /// let result = session.safe_eval("$undefined_variable").unwrap();
187    /// assert_eq!(result, "");  // $null displays as empty string
188    /// ```
189    ///
190    /// # Behavior Difference
191    ///
192    /// - `Variables::new()`: Returns `None` for undefined variables
193    /// - `Variables::force_eval()`: Returns `Val::Null` for undefined variables
194    ///
195    /// This is particularly useful when parsing PowerShell scripts that may
196    /// reference variables that haven't been explicitly defined, allowing
197    /// the script to continue execution rather than failing.
198    pub fn force_eval() -> Self {
199        let map = Self::const_variables();
200
201        Self {
202            map,
203            force_var_eval: true,
204        }
205    }
206
207    /// Loads all environment variables into a Variables container.
208    ///
209    /// This method reads all environment variables from the system and stores
210    /// them in the `env` scope, making them accessible as
211    /// `$env:VARIABLE_NAME` in PowerShell scripts.
212    ///
213    /// # Returns
214    ///
215    /// A new `Variables` instance containing all environment variables.
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use ps_parser::{Variables, PowerShellSession};
221    ///
222    /// let env_vars = Variables::env();
223    /// let mut session = PowerShellSession::new().with_variables(env_vars);
224    ///
225    /// // Access environment variables
226    /// let path = session.safe_eval("$env:PATH").unwrap();
227    /// let username = session.safe_eval("$env:USERNAME").unwrap();
228    /// ```
229    pub fn env() -> Variables {
230        let mut map = Self::const_variables();
231
232        // Load all environment variables
233        for (key, value) in std::env::vars() {
234            // Store environment variables with Env scope so they can be accessed via
235            // $env:variable_name
236            map.insert(
237                VarName::new(Scope::Env, key.to_lowercase()),
238                Variable::new(VarProp::ReadWrite, Val::String(value.into())),
239            );
240        }
241
242        Self {
243            map,
244            force_var_eval: true,
245        }
246    }
247
248    /// Loads variables from an INI configuration file.
249    ///
250    /// This method parses an INI file and loads its key-value pairs as
251    /// PowerShell variables. Variables are organized by INI sections, with
252    /// the `[global]` section creating global variables and other sections
253    /// creating scoped variables.
254    ///
255    /// # Arguments
256    ///
257    /// * `path` - A reference to the path of the INI file to load.
258    ///
259    /// # Returns
260    ///
261    /// * `Result<Variables, VariableError>` - A Variables instance with the
262    ///   loaded data, or an error if the file cannot be read or parsed.
263    ///
264    /// # Examples
265    ///
266    /// ```rust
267    /// use ps_parser::{Variables, PowerShellSession};
268    /// use std::path::Path;
269    ///
270    /// // Load from INI file
271    /// let variables = Variables::from_ini_string("[global]\nname = John Doe\n[local]\nlocal_var = \"local_value\"").unwrap();
272    /// let mut session = PowerShellSession::new().with_variables(variables);
273    ///
274    /// // Access loaded variables
275    /// let name = session.safe_eval("$global:name").unwrap();
276    /// let local_var = session.safe_eval("$local:local_var").unwrap();
277    /// ```
278    ///
279    /// # INI Format
280    ///
281    /// ```ini
282    /// # Global variables (accessible as $global:key)
283    /// [global]
284    /// name = John Doe
285    /// version = 1.0
286    ///
287    /// # Local scope variables (accessible as $local:key)
288    /// [local]
289    /// temp_dir = /tmp
290    /// debug = true
291    /// ```
292    pub fn from_ini_string(ini_string: &str) -> Result<Self, Box<dyn std::error::Error>> {
293        let mut variables = Self::new();
294        variables.load_from_string(ini_string)?;
295        Ok(variables)
296    }
297
298    /// Create a new Variables instance with variables loaded from an INI file
299    pub fn from_ini_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
300        let mut variables = Self::new();
301        variables.load_from_file(path)?;
302        Ok(variables)
303    }
304
305    /// Sets the value of a variable in the specified scope.
306    ///
307    /// # Arguments
308    ///
309    /// * `var_name` - The variable name and scope information.
310    /// * `val` - The value to assign to the variable.
311    ///
312    /// # Returns
313    ///
314    /// * `Result<(), VariableError>` - Success or an error if the variable is
315    ///   read-only.
316    pub(crate) fn set(&mut self, var_name: &VarName, val: Val) -> VariableResult<()> {
317        if let Some(variable) = self.map.get_mut(var_name) {
318            if let VarProp::ReadOnly = variable.prop {
319                log::error!("You couldn't modify a read-only variable");
320                Err(VariableError::ReadOnly(var_name.name.to_string()))
321            } else {
322                variable.value = val;
323                Ok(())
324            }
325        } else {
326            self.map
327                .insert(var_name.clone(), Variable::new(VarProp::ReadWrite, val));
328            Ok(())
329        }
330    }
331
332    /// Retrieves the value of a variable from the appropriate scope.
333    ///
334    /// # Arguments
335    ///
336    /// * `var_name` - The variable name and scope information.
337    ///
338    /// # Returns
339    ///
340    /// * `VariableResult<Val>` - The variable's value, or an error if not
341    ///   found.
342    pub(crate) fn get(&self, var_name: &VarName) -> Option<Val> {
343        //todo: handle special variables and scopes
344
345        let mut var = self.map.get(var_name).map(|v| v.value.clone());
346        if self.force_var_eval && var.is_none() {
347            var = Some(Val::Null);
348        }
349
350        var
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::Variables;
357    use crate::{PowerShellSession, PsValue};
358
359    #[test]
360    fn test_builtin_variables() {
361        let mut p = PowerShellSession::new();
362        assert_eq!(p.safe_eval(r#" $true "#).unwrap().as_str(), "True");
363        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
364        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
365    }
366
367    #[test]
368    fn test_env_variables() {
369        let v = Variables::env();
370        let mut p = PowerShellSession::new().with_variables(v);
371        assert_eq!(
372            p.safe_eval(r#" $env:path "#).unwrap().as_str(),
373            std::env::var("PATH").unwrap()
374        );
375        assert_eq!(
376            p.safe_eval(r#" $env:username "#).unwrap().as_str(),
377            std::env::var("USERNAME").unwrap()
378        );
379        assert_eq!(
380            p.safe_eval(r#" $env:tEMp "#).unwrap().as_str(),
381            std::env::var("TEMP").unwrap()
382        );
383        assert_eq!(
384            p.safe_eval(r#" $env:tMp "#).unwrap().as_str(),
385            std::env::var("TMP").unwrap()
386        );
387        assert_eq!(
388            p.safe_eval(r#" $env:cOmputername "#).unwrap().as_str(),
389            std::env::var("COMPUTERNAME").unwrap()
390        );
391        assert_eq!(
392            p.safe_eval(r#" $env:programfiles "#).unwrap().as_str(),
393            std::env::var("PROGRAMFILES").unwrap()
394        );
395        assert_eq!(
396            p.safe_eval(r#" $env:temp "#).unwrap().as_str(),
397            std::env::var("TEMP").unwrap()
398        );
399        assert_eq!(
400            p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
401                .unwrap()
402                .as_str(),
403            std::env::var("ProgramFiles(x86)").unwrap()
404        );
405
406        p.safe_eval(r#" $global:program = $env:programfiles + "\program" "#)
407            .unwrap();
408        assert_eq!(
409            p.safe_eval(r#" $global:program "#).unwrap().as_str(),
410            format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
411        );
412        assert_eq!(
413            p.safe_eval(r#" $program "#).unwrap().as_str(),
414            format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
415        );
416
417        p.safe_eval(r#" ${Env:ProgramFiles(x86):adsf} = 5 "#)
418            .unwrap();
419        assert_eq!(
420            p.safe_eval(r#" ${Env:ProgramFiles(x86):adsf} "#)
421                .unwrap()
422                .as_str(),
423            5.to_string()
424        );
425        assert_eq!(
426            p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
427                .unwrap()
428                .as_str(),
429            std::env::var("ProgramFiles(x86)").unwrap()
430        );
431    }
432
433    #[test]
434    fn special_last_error() {
435        let input = r#"3+"01234 ?";$a=5;$a;$?"#;
436
437        let mut p = PowerShellSession::new();
438        assert_eq!(p.safe_eval(input).unwrap().as_str(), "True");
439
440        let input = r#"3+"01234 ?";$?"#;
441        assert_eq!(p.safe_eval(input).unwrap().as_str(), "False");
442    }
443
444    #[test]
445    fn test_from_ini() {
446        let input = r#"[global]
447name = radek
448age = 30
449is_admin = true
450height = 5.9
451empty_value =
452
453[local]
454local_var = "local_value"
455        "#;
456        let mut variables = Variables::new();
457        variables.load_from_string(input).unwrap();
458        let mut p = PowerShellSession::new().with_variables(variables);
459
460        assert_eq!(
461            p.parse_input(r#" $global:name "#).unwrap().result(),
462            PsValue::String("radek".into())
463        );
464        assert_eq!(
465            p.parse_input(r#" $global:age "#).unwrap().result(),
466            PsValue::Int(30)
467        );
468        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
469        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
470        assert_eq!(
471            p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
472            "\"local_value\""
473        );
474    }
475
476    #[test]
477    fn test_from_ini_string() {
478        let input = r#"[global]
479name = radek
480age = 30
481is_admin = true
482height = 5.9
483empty_value =
484
485[local]
486local_var = "local_value"
487        "#;
488
489        let variables = Variables::from_ini_string(input).unwrap();
490        let mut p = PowerShellSession::new().with_variables(variables);
491
492        assert_eq!(
493            p.parse_input(r#" $global:name "#).unwrap().result(),
494            PsValue::String("radek".into())
495        );
496        assert_eq!(
497            p.parse_input(r#" $global:age "#).unwrap().result(),
498            PsValue::Int(30)
499        );
500        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
501        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
502        assert_eq!(
503            p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
504            "\"local_value\""
505        );
506    }
507}