ps_parser/parser/
variables.rs

1mod function;
2mod scopes;
3mod variable;
4
5use std::collections::HashMap;
6
7pub(super) use function::FunctionMap;
8use phf::phf_map;
9pub(super) use scopes::SessionScope;
10use thiserror_no_std::Error;
11pub(super) use variable::{Scope, VarName};
12
13use crate::parser::{Val, value::ScriptBlock};
14#[derive(Error, Debug, PartialEq, Clone)]
15pub enum VariableError {
16    #[error("Variable \"{0}\" is not defined")]
17    NotDefined(String),
18    #[error("Cannot overwrite variable \"{0}\" because it is read-only or constant.")]
19    ReadOnly(String),
20}
21
22pub type VariableResult<T> = core::result::Result<T, VariableError>;
23pub type VariableMap = HashMap<String, Val>;
24
25#[derive(Clone, Default)]
26pub struct Variables {
27    env: VariableMap,
28    global_scope: VariableMap,
29    script_scope: VariableMap,
30    scope_sessions_stack: Vec<VariableMap>,
31    state: State,
32    force_var_eval: bool,
33    values_persist: bool,
34    global_functions: FunctionMap,
35    script_functions: FunctionMap,
36    //special variables
37    // status: bool, // $?
38    // first_token: Option<String>,
39    // last_token: Option<String>,
40    // current_pipeline: Option<String>,
41}
42
43#[derive(Default, Clone)]
44enum State {
45    #[default]
46    Script,
47    Stack(u32),
48}
49
50impl Variables {
51    const PREDEFINED_VARIABLES: phf::Map<&'static str, Val> = phf_map! {
52        "true" => Val::Bool(true),
53        "false" => Val::Bool(false),
54        "null" => Val::Null,
55    };
56
57    pub(crate) fn set_ps_item(&mut self, ps_item: Val) {
58        let _ = self.set(
59            &VarName::new_with_scope(Scope::Special, "$PSItem".into()),
60            ps_item.clone(),
61        );
62        let _ = self.set(
63            &VarName::new_with_scope(Scope::Special, "$_".into()),
64            ps_item,
65        );
66    }
67
68    pub(crate) fn reset_ps_item(&mut self) {
69        let _ = self.set(
70            &VarName::new_with_scope(Scope::Special, "$PSItem".into()),
71            Val::Null,
72        );
73        let _ = self.set(
74            &VarName::new_with_scope(Scope::Special, "$_".into()),
75            Val::Null,
76        );
77    }
78
79    pub fn set_status(&mut self, b: bool) {
80        let _ = self.set(
81            &VarName::new_with_scope(Scope::Special, "$?".into()),
82            Val::Bool(b),
83        );
84    }
85
86    pub fn status(&mut self) -> bool {
87        let Some(Val::Bool(b)) = self.get(&VarName::new_with_scope(Scope::Special, "$?".into()))
88        else {
89            return false;
90        };
91        b
92    }
93
94    pub fn load_from_file(
95        &mut self,
96        path: &std::path::Path,
97    ) -> Result<(), Box<dyn std::error::Error>> {
98        let mut config_parser = configparser::ini::Ini::new();
99        let map = config_parser.load(path)?;
100        self.load(map)
101    }
102
103    pub fn load_from_string(&mut self, ini_string: &str) -> Result<(), Box<dyn std::error::Error>> {
104        let mut config_parser = configparser::ini::Ini::new();
105        let map = config_parser.read(ini_string.into())?;
106        self.load(map)
107    }
108
109    pub fn init(&mut self) {
110        if !self.values_persist {
111            self.script_scope.clear();
112        }
113        self.scope_sessions_stack.clear();
114        self.state = State::Script;
115    }
116
117    fn load(
118        &mut self,
119        conf_map: HashMap<String, HashMap<String, Option<String>>>,
120    ) -> Result<(), Box<dyn std::error::Error>> {
121        for (section_name, properties) in conf_map {
122            for (key, value) in properties {
123                let Some(value) = value else {
124                    continue;
125                };
126
127                let var_name = match section_name.as_str() {
128                    "global" => VarName::new_with_scope(Scope::Global, key.to_lowercase()),
129                    "script" => VarName::new_with_scope(Scope::Script, key.to_lowercase()),
130                    "env" => VarName::new_with_scope(Scope::Env, key.to_lowercase()),
131                    _ => {
132                        continue;
133                    }
134                };
135
136                // Try to parse the value as different types
137                let parsed_value = if let Ok(bool_val) = value.parse::<bool>() {
138                    Val::Bool(bool_val)
139                } else if let Ok(int_val) = value.parse::<i64>() {
140                    Val::Int(int_val)
141                } else if let Ok(float_val) = value.parse::<f64>() {
142                    Val::Float(float_val)
143                } else if value.is_empty() {
144                    Val::Null
145                } else {
146                    Val::String(value.clone().into())
147                };
148
149                // Insert the variable (overwrite if it exists and is not read-only)
150                if let Err(err) = self.set(&var_name, parsed_value.clone()) {
151                    log::error!("Failed to set variable {:?}: {}", var_name, err);
152                }
153            }
154        }
155        Ok(())
156    }
157
158    pub(crate) fn script_scope(&self) -> VariableMap {
159        self.script_scope.clone()
160    }
161
162    pub(crate) fn get_env(&self) -> VariableMap {
163        self.env.clone()
164    }
165
166    pub(crate) fn get_global(&self) -> VariableMap {
167        self.global_scope.clone()
168    }
169
170    pub(crate) fn add_script_function(&mut self, name: String, func: ScriptBlock) {
171        self.script_functions.insert(name, func);
172    }
173
174    pub(crate) fn add_global_function(&mut self, name: String, func: ScriptBlock) {
175        self.global_functions.insert(name, func);
176    }
177
178    pub(crate) fn clear_script_functions(&mut self) {
179        self.script_functions.clear();
180    }
181
182    /// Creates a new empty Variables container.
183    ///
184    /// # Arguments
185    ///
186    /// * initializes the container with PowerShell built-in variables like
187    ///   `$true`, `$false`, `$null`, and `$?`. If `false`,
188    ///
189    /// # Returns
190    ///
191    /// A new `Variables` instance.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use ps_parser::Variables;
197    ///
198    /// // Create with built-in variables
199    /// let vars_with_builtins = Variables::new();
200    ///
201    /// // Create empty
202    /// let empty_vars = Variables::new();
203    /// ```
204    pub fn new() -> Variables {
205        Default::default()
206    }
207
208    /// Creates a new Variables container with forced evaluation enabled.
209    ///
210    /// This constructor creates a Variables instance that will return
211    /// `Val::Null` for undefined variables instead of returning `None`.
212    /// This is useful for PowerShell script evaluation where undefined
213    /// variables should be treated as `$null` rather than causing errors.
214    ///
215    /// # Returns
216    ///
217    /// A new `Variables` instance with forced evaluation enabled and built-in
218    /// variables initialized.
219    ///
220    /// # Examples
221    ///
222    /// ```rust
223    /// use ps_parser::{Variables, PowerShellSession};
224    ///
225    /// // Create with forced evaluation
226    /// let vars = Variables::force_eval();
227    /// let mut session = PowerShellSession::new().with_variables(vars);
228    ///
229    /// // Undefined variables will evaluate to $null instead of causing errors
230    /// let result = session.safe_eval("$undefined_variable").unwrap();
231    /// assert_eq!(result, "");  // $null displays as empty string
232    /// ```
233    ///
234    /// # Behavior Difference
235    ///
236    /// - `Variables::new()`: Returns `None` for undefined variables
237    /// - `Variables::force_eval()`: Returns `Val::Null` for undefined variables
238    ///
239    /// This is particularly useful when parsing PowerShell scripts that may
240    /// reference variables that haven't been explicitly defined, allowing
241    /// the script to continue execution rather than failing.
242    pub fn force_eval() -> Self {
243        Self {
244            force_var_eval: true,
245            ..Default::default()
246        }
247    }
248
249    // not exported in this version
250    #[allow(dead_code)]
251    pub(crate) fn values_persist(mut self) -> Self {
252        self.values_persist = true;
253        self
254    }
255
256    /// Loads all environment variables into a Variables container.
257    ///
258    /// This method reads all environment variables from the system and stores
259    /// them in the `env` scope, making them accessible as
260    /// `$env:VARIABLE_NAME` in PowerShell scripts.
261    ///
262    /// # Returns
263    ///
264    /// A new `Variables` instance containing all environment variables.
265    ///
266    /// # Examples
267    ///
268    /// ```rust
269    /// use ps_parser::{Variables, PowerShellSession};
270    ///
271    /// let env_vars = Variables::env();
272    /// let mut session = PowerShellSession::new().with_variables(env_vars);
273    ///
274    /// // Access environment variables
275    /// let path = session.safe_eval("$env:PATH").unwrap();
276    /// let username = session.safe_eval("$env:USERNAME").unwrap();
277    /// ```
278    pub fn env() -> Variables {
279        let mut vars = Variables::new();
280
281        // Load all environment variables
282        for (key, value) in std::env::vars() {
283            // Store environment variables with Env scope so they can be accessed via
284            // $env:variable_name
285            vars.env
286                .insert(key.to_lowercase(), Val::String(value.into()));
287        }
288        vars
289    }
290
291    /// Loads variables from an INI configuration file.
292    ///
293    /// This method parses an INI file and loads its key-value pairs as
294    /// PowerShell variables. Variables are organized by INI sections, with
295    /// the `[global]` section creating global variables and other sections
296    /// creating scoped variables.
297    ///
298    /// # Arguments
299    ///
300    /// * `path` - A reference to the path of the INI file to load.
301    ///
302    /// # Returns
303    ///
304    /// * `Result<Variables, VariableError>` - A Variables instance with the
305    ///   loaded data, or an error if the file cannot be read or parsed.
306    ///
307    /// # Examples
308    ///
309    /// ```rust
310    /// use ps_parser::{Variables, PowerShellSession};
311    /// use std::path::Path;
312    ///
313    /// // Load from INI file
314    /// let variables = Variables::from_ini_string("[global]\nname = John Doe\n[local]\nlocal_var = \"local_value\"").unwrap();
315    /// let mut session = PowerShellSession::new().with_variables(variables);
316    ///
317    /// // Access loaded variables
318    /// let name = session.safe_eval("$global:name").unwrap();
319    /// let local_var = session.safe_eval("$local:local_var").unwrap();
320    /// ```
321    ///
322    /// # INI Format
323    ///
324    /// ```ini
325    /// # Global variables (accessible as $global:key)
326    /// [global]
327    /// name = John Doe
328    /// version = 1.0
329    ///
330    /// # Local scope variables (accessible as $local:key)
331    /// [local]
332    /// temp_dir = /tmp
333    /// debug = true
334    /// ```
335    pub fn from_ini_string(ini_string: &str) -> Result<Self, Box<dyn std::error::Error>> {
336        let mut variables = Self::new();
337        variables.load_from_string(ini_string)?;
338        Ok(variables)
339    }
340
341    /// Create a new Variables instance with variables loaded from an INI file
342    pub fn from_ini_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
343        let mut variables = Self::new();
344        variables.load_from_file(path)?;
345        Ok(variables)
346    }
347
348    fn const_map_from_scope(&self, scope: &Scope) -> &VariableMap {
349        match scope {
350            Scope::Global => &self.global_scope,
351            Scope::Script => &self.script_scope,
352            Scope::Env => &self.env,
353            Scope::Local => match self.state {
354                State::Script => &self.script_scope,
355                State::Stack(depth) => {
356                    if depth < self.scope_sessions_stack.len() as u32 {
357                        &self.scope_sessions_stack[depth as usize]
358                    } else {
359                        &self.script_scope
360                    }
361                }
362            },
363            Scope::Special => {
364                &self.global_scope //todo!(),
365            }
366        }
367    }
368
369    fn local_scope(&mut self) -> &mut VariableMap {
370        match self.state {
371            State::Script => &mut self.script_scope,
372            State::Stack(depth) => {
373                if depth < self.scope_sessions_stack.len() as u32 {
374                    &mut self.scope_sessions_stack[depth as usize]
375                } else {
376                    &mut self.script_scope
377                }
378            }
379        }
380    }
381    fn map_from_scope(&mut self, scope: &Scope) -> &mut VariableMap {
382        match scope {
383            Scope::Global => &mut self.global_scope,
384            Scope::Script => &mut self.script_scope,
385            Scope::Env => &mut self.env,
386            Scope::Local => self.local_scope(),
387            Scope::Special => {
388                &mut self.global_scope //todo!(),
389            }
390        }
391    }
392
393    /// Sets the value of a variable in the specified scope.
394    ///
395    /// # Arguments
396    ///
397    /// * `var_name` - The variable name and scope information.
398    /// * `val` - The value to assign to the variable.
399    ///
400    /// # Returns
401    ///
402    /// * `Result<(), VariableError>` - Success or an error if the variable is
403    ///   read-only.
404    pub(crate) fn set(&mut self, var_name: &VarName, val: Val) -> VariableResult<()> {
405        let var = self.find_mut_variable_in_scopes(var_name)?;
406
407        if let Some(variable) = var {
408            *variable = val;
409        } else {
410            let map = self.map_from_scope(&var_name.scope.clone().unwrap_or(Scope::Local));
411            map.insert(var_name.name.to_ascii_lowercase(), val);
412        }
413
414        Ok(())
415    }
416
417    pub(crate) fn set_local(&mut self, name: &str, val: Val) -> VariableResult<()> {
418        let var_name = VarName::new_with_scope(Scope::Local, name.to_ascii_lowercase());
419        self.set(&var_name, val)
420    }
421
422    fn find_mut_variable_in_scopes(
423        &mut self,
424        var_name: &VarName,
425    ) -> VariableResult<Option<&mut Val>> {
426        let name = var_name.name.to_ascii_lowercase();
427        let name_str = name.as_str();
428
429        if let Some(scope) = &var_name.scope {
430            let map = self.map_from_scope(scope);
431            Ok(map.get_mut(name_str))
432        } else {
433            if Self::PREDEFINED_VARIABLES.contains_key(name_str) {
434                return Err(VariableError::ReadOnly(name.clone()));
435            }
436
437            // No scope specified, check local scopes first, then globals
438            for local_scope in self.scope_sessions_stack.iter_mut().rev() {
439                if local_scope.contains_key(name_str) {
440                    return Ok(local_scope.get_mut(name_str));
441                }
442            }
443
444            if self.script_scope.contains_key(name_str) {
445                return Ok(self.script_scope.get_mut(name_str));
446            }
447
448            if self.global_scope.contains_key(name_str) {
449                return Ok(self.global_scope.get_mut(name_str));
450            }
451
452            Ok(None)
453        }
454    }
455
456    /// Retrieves the value of a variable from the appropriate scope.
457    ///
458    /// # Arguments
459    ///
460    /// * `var_name` - The variable name and scope information.
461    ///
462    /// # Returns
463    ///
464    /// * `VariableResult<Val>` - The variable's value, or an error if not
465    ///   found.
466    pub(crate) fn get(&self, var_name: &VarName) -> Option<Val> {
467        let var = self.find_variable_in_scopes(var_name);
468
469        if self.force_var_eval && var.is_none() {
470            Some(Val::Null)
471        } else {
472            var.cloned()
473        }
474    }
475
476    fn find_variable_in_scopes(&self, var_name: &VarName) -> Option<&Val> {
477        let name = var_name.name.to_ascii_lowercase();
478        let name_str = name.as_str();
479
480        if let Some(scope) = &var_name.scope {
481            let map = self.const_map_from_scope(scope);
482            map.get(name_str)
483        } else {
484            if Self::PREDEFINED_VARIABLES.contains_key(name_str) {
485                return Self::PREDEFINED_VARIABLES.get(name_str);
486            }
487
488            // No scope specified, check local scopes first, then globals
489            for local_scope in self.scope_sessions_stack.iter().rev() {
490                if local_scope.contains_key(name_str) {
491                    return local_scope.get(name_str);
492                }
493            }
494
495            if self.script_scope.contains_key(name_str) {
496                return self.script_scope.get(name_str);
497            }
498
499            if self.global_scope.contains_key(name_str) {
500                return self.global_scope.get(name_str);
501            }
502
503            None
504        }
505    }
506
507    pub(crate) fn push_scope_session(&mut self) {
508        let current_map = self.local_scope();
509        let new_map = current_map.clone();
510
511        self.scope_sessions_stack.push(new_map);
512        self.state = State::Stack(self.scope_sessions_stack.len() as u32 - 1);
513    }
514
515    pub(crate) fn pop_scope_session(&mut self) {
516        match self.scope_sessions_stack.len() {
517            0 => {} /* unreachable */
518            1 => {
519                self.scope_sessions_stack.pop();
520                self.state = State::Script;
521            }
522            _ => {
523                self.scope_sessions_stack.pop();
524                self.state = State::Stack(self.scope_sessions_stack.len() as u32 - 1);
525            }
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::Variables;
533    use crate::{PowerShellSession, PsValue};
534
535    #[test]
536    fn test_builtin_variables() {
537        let mut p = PowerShellSession::new();
538        assert_eq!(p.safe_eval(r#" $true "#).unwrap().as_str(), "True");
539        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
540        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
541    }
542
543    #[test]
544    fn test_env_variables() {
545        let v = Variables::env();
546        let mut p = PowerShellSession::new().with_variables(v);
547        assert_eq!(
548            p.safe_eval(r#" $env:path "#).unwrap().as_str(),
549            std::env::var("PATH").unwrap()
550        );
551        assert_eq!(
552            p.safe_eval(r#" $env:username "#).unwrap().as_str(),
553            std::env::var("USERNAME").unwrap()
554        );
555        assert_eq!(
556            p.safe_eval(r#" $env:tEMp "#).unwrap().as_str(),
557            std::env::var("TEMP").unwrap()
558        );
559        assert_eq!(
560            p.safe_eval(r#" $env:tMp "#).unwrap().as_str(),
561            std::env::var("TMP").unwrap()
562        );
563        assert_eq!(
564            p.safe_eval(r#" $env:cOmputername "#).unwrap().as_str(),
565            std::env::var("COMPUTERNAME").unwrap()
566        );
567        assert_eq!(
568            p.safe_eval(r#" $env:programfiles "#).unwrap().as_str(),
569            std::env::var("PROGRAMFILES").unwrap()
570        );
571        assert_eq!(
572            p.safe_eval(r#" $env:temp "#).unwrap().as_str(),
573            std::env::var("TEMP").unwrap()
574        );
575        assert_eq!(
576            p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
577                .unwrap()
578                .as_str(),
579            std::env::var("ProgramFiles(x86)").unwrap()
580        );
581        let env_variables = p.env_variables();
582        assert_eq!(
583            env_variables.get("path").unwrap().to_string(),
584            std::env::var("PATH").unwrap()
585        );
586        assert_eq!(
587            env_variables.get("tmp").unwrap().to_string(),
588            std::env::var("TMP").unwrap()
589        );
590        assert_eq!(
591            env_variables.get("temp").unwrap().to_string(),
592            std::env::var("TMP").unwrap()
593        );
594        assert_eq!(
595            env_variables.get("appdata").unwrap().to_string(),
596            std::env::var("APPDATA").unwrap()
597        );
598        assert_eq!(
599            env_variables.get("username").unwrap().to_string(),
600            std::env::var("USERNAME").unwrap()
601        );
602        assert_eq!(
603            env_variables.get("programfiles").unwrap().to_string(),
604            std::env::var("PROGRAMFILES").unwrap()
605        );
606        assert_eq!(
607            env_variables.get("programfiles(x86)").unwrap().to_string(),
608            std::env::var("PROGRAMFILES(x86)").unwrap()
609        );
610    }
611
612    #[test]
613    fn test_global_variables() {
614        let v = Variables::env();
615        let mut p = PowerShellSession::new().with_variables(v);
616
617        p.parse_input(r#" $global:var_int = 5 "#).unwrap();
618        p.parse_input(r#" $global:var_string = "global";$script:var_string = "script";$local:var_string = "local" "#).unwrap();
619
620        assert_eq!(
621            p.parse_input(r#" $var_int "#).unwrap().result(),
622            PsValue::Int(5)
623        );
624        assert_eq!(
625            p.parse_input(r#" $var_string "#).unwrap().result(),
626            PsValue::String("global".into())
627        );
628
629        let global_variables = p.session_variables();
630        assert_eq!(global_variables.get("var_int").unwrap(), &PsValue::Int(5));
631        assert_eq!(
632            global_variables.get("var_string").unwrap(),
633            &PsValue::String("global".into())
634        );
635    }
636
637    #[test]
638    fn test_script_variables() {
639        let v = Variables::env();
640        let mut p = PowerShellSession::new().with_variables(v);
641
642        let script_res = p
643            .parse_input(r#" $script:var_int = 5;$var_string = "assdfa" "#)
644            .unwrap();
645        let script_variables = script_res.script_variables();
646        assert_eq!(script_variables.get("var_int"), Some(&PsValue::Int(5)));
647        assert_eq!(
648            script_variables.get("var_string"),
649            Some(&PsValue::String("assdfa".into()))
650        );
651    }
652
653    #[test]
654    fn test_env_special_cases() {
655        let v = Variables::env();
656        let mut p = PowerShellSession::new().with_variables(v);
657        p.safe_eval(r#" $global:program = $env:programfiles + "\program" "#)
658            .unwrap();
659        assert_eq!(
660            p.safe_eval(r#" $global:program "#).unwrap().as_str(),
661            format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
662        );
663        assert_eq!(
664            p.safe_eval(r#" $program "#).unwrap().as_str(),
665            format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
666        );
667
668        assert_eq!(
669            p.safe_eval(r#" ${Env:ProgramFiles(x86):adsf} = 5;${Env:ProgramFiles(x86):adsf} "#)
670                .unwrap()
671                .as_str(),
672            5.to_string()
673        );
674        assert_eq!(
675            p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
676                .unwrap()
677                .as_str(),
678            std::env::var("ProgramFiles(x86)").unwrap()
679        );
680    }
681
682    #[test]
683    fn special_last_error() {
684        let input = r#"3+"01234 ?";$a=5;$a;$?"#;
685
686        let mut p = PowerShellSession::new();
687        assert_eq!(p.safe_eval(input).unwrap().as_str(), "True");
688
689        let input = r#"3+"01234 ?";$?"#;
690        assert_eq!(p.safe_eval(input).unwrap().as_str(), "False");
691    }
692
693    #[test]
694    fn test_from_ini() {
695        let input = r#"[global]
696name = radek
697age = 30
698is_admin = true
699height = 5.9
700empty_value =
701
702[script]
703local_var = "local_value"
704        "#;
705        let mut variables = Variables::new().values_persist();
706        variables.load_from_string(input).unwrap();
707        let mut p = PowerShellSession::new().with_variables(variables);
708
709        assert_eq!(
710            p.parse_input(r#" $global:name "#).unwrap().result(),
711            PsValue::String("radek".into())
712        );
713        assert_eq!(
714            p.parse_input(r#" $global:age "#).unwrap().result(),
715            PsValue::Int(30)
716        );
717        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
718        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
719        assert_eq!(
720            p.safe_eval(r#" $script:local_var "#).unwrap().as_str(),
721            "\"local_value\""
722        );
723        assert_eq!(
724            p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
725            "\"local_value\""
726        );
727    }
728
729    #[test]
730    fn test_from_ini_string() {
731        let input = r#"[global]
732name = radek
733age = 30
734is_admin = true
735height = 5.9
736empty_value =
737
738[script]
739local_var = "local_value"
740        "#;
741
742        let variables = Variables::from_ini_string(input).unwrap().values_persist();
743        let mut p = PowerShellSession::new().with_variables(variables);
744        assert_eq!(
745            p.parse_input(r#" $global:name "#).unwrap().result(),
746            PsValue::String("radek".into())
747        );
748        assert_eq!(
749            p.parse_input(r#" $global:age "#).unwrap().result(),
750            PsValue::Int(30)
751        );
752        assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
753        assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
754        assert_eq!(
755            p.safe_eval(r#" $script:local_var "#).unwrap().as_str(),
756            "\"local_value\""
757        );
758        assert_eq!(
759            p.safe_eval(r#" $local_var "#).unwrap().as_str(),
760            "\"local_value\""
761        );
762        assert_eq!(
763            p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
764            "\"local_value\""
765        );
766    }
767}