Skip to main content

zsh/
ksh93.rs

1//! Ksh93 compatibility module - port of Modules/ksh93.c
2//!
3//! Provides ksh93 compatibility features including:
4//! - nameref builtin
5//! - .sh.* special parameters
6
7use std::collections::HashMap;
8
9/// Ksh93 special parameters (.sh.*)
10#[derive(Debug, Default)]
11pub struct Ksh93Params {
12    pub file: Option<String>,
13    pub lineno: i64,
14    pub fun: Option<String>,
15    pub level: i64,
16    pub subshell: i64,
17    pub version: String,
18    pub name: Option<String>,
19    pub subscript: Option<String>,
20    pub edchar: Option<String>,
21    pub edmode: String,
22    pub edcol: Option<i64>,
23    pub edtext: Option<String>,
24    pub command: Option<String>,
25    pub value: Option<String>,
26    pub match_arr: Vec<String>,
27}
28
29impl Ksh93Params {
30    pub fn new() -> Self {
31        Self {
32            version: env!("CARGO_PKG_VERSION").to_string(),
33            ..Default::default()
34        }
35    }
36
37    /// Get a parameter by name
38    pub fn get(&self, name: &str) -> Option<String> {
39        match name {
40            ".sh.file" => self.file.clone(),
41            ".sh.lineno" => Some(self.lineno.to_string()),
42            ".sh.fun" => self.fun.clone(),
43            ".sh.level" => Some(self.level.to_string()),
44            ".sh.subshell" => Some(self.subshell.to_string()),
45            ".sh.version" => Some(self.version.clone()),
46            ".sh.name" => self.name.clone(),
47            ".sh.subscript" => self.subscript.clone(),
48            ".sh.edchar" => self.edchar.clone(),
49            ".sh.edmode" => Some(self.edmode.clone()),
50            ".sh.edcol" => self.edcol.map(|n| n.to_string()),
51            ".sh.edtext" => self.edtext.clone(),
52            ".sh.command" => self.command.clone(),
53            ".sh.value" => self.value.clone(),
54            ".sh.match" => {
55                if self.match_arr.is_empty() {
56                    None
57                } else {
58                    Some(self.match_arr.join(" "))
59                }
60            }
61            _ => None,
62        }
63    }
64
65    /// Set a parameter by name
66    pub fn set(&mut self, name: &str, value: &str) -> bool {
67        match name {
68            ".sh.edchar" => {
69                self.edchar = Some(value.to_string());
70                true
71            }
72            ".sh.value" => {
73                self.value = Some(value.to_string());
74                true
75            }
76            _ => false,
77        }
78    }
79
80    /// Update function context
81    pub fn enter_function(&mut self, name: &str, file: Option<&str>, lineno: i64) {
82        self.level += 1;
83        self.fun = Some(name.to_string());
84        self.file = file.map(|s| s.to_string());
85        self.lineno = lineno;
86    }
87
88    /// Exit function context
89    pub fn exit_function(&mut self) {
90        self.level = (self.level - 1).max(0);
91        self.fun = None;
92    }
93
94    /// Enter subshell
95    pub fn enter_subshell(&mut self) {
96        self.subshell += 1;
97    }
98
99    /// Exit subshell
100    pub fn exit_subshell(&mut self) {
101        self.subshell = (self.subshell - 1).max(0);
102    }
103
104    /// Set match array
105    pub fn set_match(&mut self, full: Option<&str>, captures: &[Option<String>]) {
106        self.match_arr.clear();
107        if let Some(m) = full {
108            self.match_arr.push(m.to_string());
109        }
110        for cap in captures {
111            if let Some(c) = cap {
112                self.match_arr.push(c.clone());
113            }
114        }
115    }
116
117    /// Get all parameters as hash
118    pub fn to_hash(&self) -> HashMap<String, String> {
119        let mut map = HashMap::new();
120        for name in &[
121            ".sh.file",
122            ".sh.lineno",
123            ".sh.fun",
124            ".sh.level",
125            ".sh.subshell",
126            ".sh.version",
127            ".sh.name",
128            ".sh.subscript",
129            ".sh.edchar",
130            ".sh.edmode",
131            ".sh.edcol",
132            ".sh.edtext",
133            ".sh.command",
134            ".sh.value",
135            ".sh.match",
136        ] {
137            if let Some(v) = self.get(name) {
138                map.insert(name.to_string(), v);
139            }
140        }
141        map
142    }
143}
144
145/// Nameref options
146#[derive(Debug, Default, Clone)]
147pub struct NamerefOptions {
148    pub global: bool,
149    pub print: bool,
150    pub readonly: bool,
151    pub unset: bool,
152}
153
154/// Execute nameref builtin
155pub fn builtin_nameref(args: &[&str], options: &NamerefOptions) -> (i32, String) {
156    if args.is_empty() {
157        if options.print {
158            return (0, String::new());
159        }
160        return (1, "nameref: variable name required\n".to_string());
161    }
162
163    let name = args[0];
164
165    if !is_valid_identifier(name) {
166        return (1, format!("nameref: {}: invalid variable name\n", name));
167    }
168
169    if args.len() < 2 {
170        if options.unset {
171            return (0, String::new());
172        }
173        return (1, format!("nameref: {}: reference target required\n", name));
174    }
175
176    let target = args[1];
177
178    if !is_valid_identifier(target) {
179        return (
180            1,
181            format!("nameref: {}: invalid reference target\n", target),
182        );
183    }
184
185    (0, String::new())
186}
187
188fn is_valid_identifier(s: &str) -> bool {
189    if s.is_empty() {
190        return false;
191    }
192
193    let mut chars = s.chars();
194    let first = chars.next().unwrap();
195
196    if !first.is_alphabetic() && first != '_' {
197        return false;
198    }
199
200    chars.all(|c| c.is_alphanumeric() || c == '_')
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_ksh93_params_new() {
209        let params = Ksh93Params::new();
210        assert!(!params.version.is_empty());
211        assert_eq!(params.level, 0);
212    }
213
214    #[test]
215    fn test_ksh93_params_get() {
216        let params = Ksh93Params::new();
217        assert!(params.get(".sh.version").is_some());
218        assert!(params.get(".sh.invalid").is_none());
219    }
220
221    #[test]
222    fn test_ksh93_params_enter_function() {
223        let mut params = Ksh93Params::new();
224        params.enter_function("test", Some("test.zsh"), 10);
225        assert_eq!(params.level, 1);
226        assert_eq!(params.fun, Some("test".to_string()));
227        assert_eq!(params.lineno, 10);
228    }
229
230    #[test]
231    fn test_ksh93_params_exit_function() {
232        let mut params = Ksh93Params::new();
233        params.enter_function("test", None, 1);
234        params.exit_function();
235        assert_eq!(params.level, 0);
236        assert!(params.fun.is_none());
237    }
238
239    #[test]
240    fn test_ksh93_params_subshell() {
241        let mut params = Ksh93Params::new();
242        params.enter_subshell();
243        assert_eq!(params.subshell, 1);
244        params.exit_subshell();
245        assert_eq!(params.subshell, 0);
246    }
247
248    #[test]
249    fn test_ksh93_params_set_match() {
250        let mut params = Ksh93Params::new();
251        params.set_match(
252            Some("hello"),
253            &[Some("h".to_string()), Some("ello".to_string())],
254        );
255        assert_eq!(params.match_arr.len(), 3);
256    }
257
258    #[test]
259    fn test_is_valid_identifier() {
260        assert!(is_valid_identifier("foo"));
261        assert!(is_valid_identifier("_bar"));
262        assert!(is_valid_identifier("foo123"));
263        assert!(!is_valid_identifier(""));
264        assert!(!is_valid_identifier("123"));
265        assert!(!is_valid_identifier("foo-bar"));
266    }
267
268    #[test]
269    fn test_builtin_nameref_no_args() {
270        let options = NamerefOptions::default();
271        let (status, _) = builtin_nameref(&[], &options);
272        assert_eq!(status, 1);
273    }
274
275    #[test]
276    fn test_builtin_nameref_no_target() {
277        let options = NamerefOptions::default();
278        let (status, _) = builtin_nameref(&["foo"], &options);
279        assert_eq!(status, 1);
280    }
281
282    #[test]
283    fn test_builtin_nameref_valid() {
284        let options = NamerefOptions::default();
285        let (status, _) = builtin_nameref(&["foo", "bar"], &options);
286        assert_eq!(status, 0);
287    }
288
289    #[test]
290    fn test_builtin_nameref_invalid_name() {
291        let options = NamerefOptions::default();
292        let (status, _) = builtin_nameref(&["123", "bar"], &options);
293        assert_eq!(status, 1);
294    }
295}