tatara_lisp_script/stdlib/
cli.rs1use std::sync::Arc;
28
29use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
30
31use crate::script_ctx::ScriptCtx;
32use crate::stdlib::env::str_arg;
33
34pub fn install(interp: &mut Interpreter<ScriptCtx>) {
35 interp.register_fn(
36 "parse-args",
37 Arity::Exact(1),
38 |args: &[Value], ctx: &mut ScriptCtx, sp| {
39 let spec = parse_spec(&args[0], sp)?;
40 let argv = ctx.argv.clone();
41 let result =
42 do_parse(&spec, &argv).map_err(|e| EvalError::native_fn("parse-args", e, sp))?;
43 Ok(result)
44 },
45 );
46
47 interp.register_fn(
48 "print-usage",
49 Arity::Range(1, 2),
50 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
51 let spec = parse_spec(&args[0], sp)?;
52 let prog = args
53 .get(1)
54 .map(|v| str_arg(v, "print-usage", sp))
55 .transpose()?
56 .unwrap_or_else(|| Arc::from("tatara-script"));
57 eprintln!("usage: {prog} [flags]");
58 for opt in &spec {
59 let short = opt
60 .short
61 .as_ref()
62 .map(|c| format!("-{c}, "))
63 .unwrap_or_default();
64 let kind_hint = match opt.kind {
65 Kind::Flag => "".to_string(),
66 Kind::String => " <str>".to_string(),
67 Kind::Int => " <int>".to_string(),
68 };
69 let req = if opt.required { " (required)" } else { "" };
70 let help = opt.help.as_deref().unwrap_or("");
71 eprintln!(" {}--{}{}{} {}", short, opt.name, kind_hint, req, help);
72 }
73 Ok(Value::Nil)
74 },
75 );
76}
77
78enum Kind {
79 Flag,
80 String,
81 Int,
82}
83
84struct Spec {
85 name: String,
86 short: Option<String>,
87 kind: Kind,
88 required: bool,
89 default: Option<Value>,
90 help: Option<String>,
91}
92
93fn parse_spec(v: &Value, sp: tatara_lisp::Span) -> Result<Vec<Spec>, EvalError> {
94 let Value::List(items) = v else {
95 return Err(EvalError::native_fn(
96 "parse-args",
97 "spec must be a list",
98 sp,
99 ));
100 };
101 items
102 .iter()
103 .map(|entry| parse_spec_entry(entry, sp))
104 .collect()
105}
106
107fn parse_spec_entry(v: &Value, sp: tatara_lisp::Span) -> Result<Spec, EvalError> {
108 let Value::List(pairs) = v else {
109 return Err(EvalError::native_fn(
110 "parse-args",
111 "spec entry must be a plist like (:name \"template\" :kind :string)",
112 sp,
113 ));
114 };
115 let mut spec = Spec {
116 name: String::new(),
117 short: None,
118 kind: Kind::Flag,
119 required: false,
120 default: None,
121 help: None,
122 };
123 let mut i = 0;
124 while i + 1 < pairs.len() {
125 let Value::Keyword(k) = &pairs[i] else {
126 return Err(EvalError::native_fn(
127 "parse-args",
128 "spec entry keys must be keywords (:name etc)",
129 sp,
130 ));
131 };
132 let key: &str = k.as_ref();
133 let val = &pairs[i + 1];
134 match key {
135 "name" => match val {
136 Value::Str(s) => spec.name = s.as_ref().to_owned(),
137 _ => {
138 return Err(EvalError::native_fn(
139 "parse-args",
140 ":name must be string",
141 sp,
142 ))
143 }
144 },
145 "short" => match val {
146 Value::Str(s) => spec.short = Some(s.as_ref().to_owned()),
147 _ => {
148 return Err(EvalError::native_fn(
149 "parse-args",
150 ":short must be string",
151 sp,
152 ))
153 }
154 },
155 "kind" => match val {
156 Value::Keyword(k) => {
157 spec.kind = match k.as_ref() {
158 "flag" => Kind::Flag,
159 "string" => Kind::String,
160 "int" => Kind::Int,
161 other => {
162 return Err(EvalError::native_fn(
163 "parse-args",
164 format!("unknown :kind {other}"),
165 sp,
166 ))
167 }
168 }
169 }
170 _ => {
171 return Err(EvalError::native_fn(
172 "parse-args",
173 ":kind must be keyword",
174 sp,
175 ))
176 }
177 },
178 "required" => {
179 spec.required = matches!(val, Value::Bool(true));
180 }
181 "default" => {
182 spec.default = Some(val.clone());
183 }
184 "help" => match val {
185 Value::Str(s) => spec.help = Some(s.as_ref().to_owned()),
186 _ => {}
187 },
188 other => {
189 return Err(EvalError::native_fn(
190 "parse-args",
191 format!("unknown spec key :{other}"),
192 sp,
193 ))
194 }
195 }
196 i += 2;
197 }
198 if spec.name.is_empty() {
199 return Err(EvalError::native_fn("parse-args", ":name required", sp));
200 }
201 Ok(spec)
202}
203
204fn do_parse(spec: &[Spec], argv: &[String]) -> Result<Value, String> {
205 use std::collections::HashMap;
206 let mut matched: HashMap<String, Value> = HashMap::new();
207 let mut positional: Vec<Value> = Vec::new();
208
209 let by_long: HashMap<&str, &Spec> = spec.iter().map(|s| (s.name.as_str(), s)).collect();
211 let by_short: HashMap<&str, &Spec> = spec
212 .iter()
213 .filter_map(|s| s.short.as_deref().map(|k| (k, s)))
214 .collect();
215
216 let mut it = argv.iter();
217 while let Some(arg) = it.next() {
218 if let Some(name) = arg.strip_prefix("--") {
219 let entry = by_long
220 .get(name)
221 .ok_or_else(|| format!("unknown flag --{name}"))?;
222 consume_flag(entry, &mut it, &mut matched)?;
223 } else if let Some(name) = arg.strip_prefix('-') {
224 if name.is_empty() {
225 positional.push(Value::Str(Arc::from(arg.as_str())));
226 continue;
227 }
228 let entry = by_short
229 .get(name)
230 .ok_or_else(|| format!("unknown flag -{name}"))?;
231 consume_flag(entry, &mut it, &mut matched)?;
232 } else {
233 positional.push(Value::Str(Arc::from(arg.as_str())));
234 }
235 }
236
237 for entry in spec {
239 if matched.contains_key(&entry.name) {
240 continue;
241 }
242 if let Some(d) = &entry.default {
243 matched.insert(entry.name.clone(), d.clone());
244 } else if entry.required {
245 return Err(format!("missing required flag --{}", entry.name));
246 } else if matches!(entry.kind, Kind::Flag) {
247 matched.insert(entry.name.clone(), Value::Bool(false));
248 }
249 }
250
251 let mut out: Vec<Value> = matched
253 .into_iter()
254 .map(|(k, v)| Value::list(vec![Value::Str(Arc::from(k.as_str())), v]))
255 .collect();
256 out.push(Value::list(vec![
258 Value::Str(Arc::from("")),
259 Value::list(positional),
260 ]));
261 Ok(Value::list(out))
262}
263
264fn consume_flag<'a>(
265 entry: &Spec,
266 it: &mut std::slice::Iter<'a, String>,
267 matched: &mut std::collections::HashMap<String, Value>,
268) -> Result<(), String> {
269 let v = match entry.kind {
270 Kind::Flag => Value::Bool(true),
271 Kind::String => {
272 let raw = it
273 .next()
274 .ok_or_else(|| format!("--{} expects a value", entry.name))?;
275 Value::Str(Arc::from(raw.as_str()))
276 }
277 Kind::Int => {
278 let raw = it
279 .next()
280 .ok_or_else(|| format!("--{} expects an integer", entry.name))?;
281 let n: i64 = raw
282 .parse()
283 .map_err(|e| format!("--{} bad int: {e}", entry.name))?;
284 Value::Int(n)
285 }
286 };
287 matched.insert(entry.name.clone(), v);
288 Ok(())
289}