1use std::collections::HashMap;
8
9#[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 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 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 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 pub fn exit_function(&mut self) {
90 self.level = (self.level - 1).max(0);
91 self.fun = None;
92 }
93
94 pub fn enter_subshell(&mut self) {
96 self.subshell += 1;
97 }
98
99 pub fn exit_subshell(&mut self) {
101 self.subshell = (self.subshell - 1).max(0);
102 }
103
104 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 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#[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
154pub 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}