1use kdl::{KdlDocument, KdlEntry, KdlNode};
2use serde::Serialize;
3use std::fmt::Display;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use crate::error::UsageErr;
8use crate::spec::builder::SpecArgBuilder;
9use crate::spec::context::ParsingContext;
10use crate::spec::helpers::NodeHelper;
11use crate::spec::is_false;
12use crate::{string, SpecChoices};
13
14#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, strum::EnumString, strum::Display)]
15#[strum(serialize_all = "snake_case")]
16pub enum SpecDoubleDashChoices {
17 Automatic,
19 #[default]
21 Optional,
22 Required,
24 Preserve,
26}
27
28#[derive(Debug, Default, Clone, Serialize)]
45pub struct SpecArg {
46 pub name: String,
48 pub usage: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub help: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub help_long: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub help_md: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub help_first_line: Option<String>,
62 pub required: bool,
64 pub double_dash: SpecDoubleDashChoices,
66 #[serde(skip_serializing_if = "is_false")]
68 pub var: bool,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub var_min: Option<usize>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub var_max: Option<usize>,
75 pub hide: bool,
77 #[serde(skip_serializing_if = "Vec::is_empty")]
79 pub default: Vec<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub choices: Option<SpecChoices>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub env: Option<String>,
86}
87
88impl SpecArg {
89 pub fn builder() -> SpecArgBuilder {
91 SpecArgBuilder::new()
92 }
93
94 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
95 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
96 for (k, v) in node.props() {
97 match k {
98 "help" => arg.help = Some(v.ensure_string()?),
99 "long_help" => arg.help_long = Some(v.ensure_string()?),
100 "help_long" => arg.help_long = Some(v.ensure_string()?),
101 "help_md" => arg.help_md = Some(v.ensure_string()?),
102 "required" => arg.required = v.ensure_bool()?,
103 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
104 "var" => arg.var = v.ensure_bool()?,
105 "hide" => arg.hide = v.ensure_bool()?,
106 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
107 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
108 "default" => arg.default = vec![v.ensure_string()?],
109 "env" => arg.env = v.ensure_string().map(Some)?,
110 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
111 }
112 }
113 if !arg.default.is_empty() {
114 arg.required = false;
115 }
116 for child in node.children() {
117 match child.name() {
118 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
119 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
120 "default" => {
121 let children = child.children();
125 if children.is_empty() {
126 arg.default = vec![child.arg(0)?.ensure_string()?];
128 } else {
129 arg.default = children.iter().map(|c| c.name().to_string()).collect();
132 }
133 }
134 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
135 }
136 }
137 arg.usage = arg.usage();
138 if let Some(help) = &arg.help {
139 arg.help_first_line = Some(string::first_line(help));
140 }
141 Ok(arg)
142 }
143}
144
145impl SpecArg {
146 pub fn usage(&self) -> String {
147 let name = if self.double_dash == SpecDoubleDashChoices::Required {
148 format!("-- {}", self.name)
149 } else {
150 self.name.clone()
151 };
152 let mut name = if self.required {
153 format!("<{name}>")
154 } else {
155 format!("[{name}]")
156 };
157 if self.var {
158 name = format!("{name}…");
159 }
160 name
161 }
162}
163
164impl From<&SpecArg> for KdlNode {
165 fn from(arg: &SpecArg) -> Self {
166 let mut node = KdlNode::new("arg");
167 node.push(KdlEntry::new(arg.usage()));
168 if let Some(desc) = &arg.help {
169 node.push(KdlEntry::new_prop("help", desc.clone()));
170 }
171 if let Some(desc) = &arg.help_long {
172 node.push(KdlEntry::new_prop("help_long", desc.clone()));
173 }
174 if let Some(desc) = &arg.help_md {
175 node.push(KdlEntry::new_prop("help_md", desc.clone()));
176 }
177 if !arg.required {
178 node.push(KdlEntry::new_prop("required", false));
179 }
180 if arg.double_dash == SpecDoubleDashChoices::Automatic
181 || arg.double_dash == SpecDoubleDashChoices::Preserve
182 {
183 node.push(KdlEntry::new_prop(
184 "double_dash",
185 arg.double_dash.to_string(),
186 ));
187 }
188 if arg.var {
189 node.push(KdlEntry::new_prop("var", true));
190 }
191 if let Some(min) = arg.var_min {
192 node.push(KdlEntry::new_prop("var_min", min as i128));
193 }
194 if let Some(max) = arg.var_max {
195 node.push(KdlEntry::new_prop("var_max", max as i128));
196 }
197 if arg.hide {
198 node.push(KdlEntry::new_prop("hide", true));
199 }
200 if !arg.default.is_empty() {
202 if arg.default.len() == 1 {
203 node.push(KdlEntry::new_prop("default", arg.default[0].clone()));
205 } else {
206 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
208 let mut default_node = KdlNode::new("default");
209 let default_children = default_node
210 .children_mut()
211 .get_or_insert_with(KdlDocument::new);
212 for val in &arg.default {
213 default_children
214 .nodes_mut()
215 .push(KdlNode::new(val.as_str()));
216 }
217 children.nodes_mut().push(default_node);
218 }
219 }
220 if let Some(env) = &arg.env {
221 node.push(KdlEntry::new_prop("env", env.clone()));
222 }
223 if let Some(choices) = &arg.choices {
224 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
225 children.nodes_mut().push(choices.into());
226 }
227 node
228 }
229}
230
231impl From<&str> for SpecArg {
232 fn from(input: &str) -> Self {
233 let mut arg = SpecArg {
234 name: input.to_string(),
235 required: true,
236 ..Default::default()
237 };
238 if let Some(name) = arg
240 .name
241 .strip_suffix("...")
242 .or_else(|| arg.name.strip_suffix("…"))
243 {
244 arg.var = true;
245 arg.name = name.to_string();
246 }
247 let first = arg.name.chars().next().unwrap_or_default();
248 let last = arg.name.chars().last().unwrap_or_default();
249 match (first, last) {
250 ('[', ']') => {
251 arg.name = arg.name[1..arg.name.len() - 1].to_string();
252 arg.required = false;
253 }
254 ('<', '>') => {
255 arg.name = arg.name[1..arg.name.len() - 1].to_string();
256 }
257 _ => {}
258 }
259 if let Some(name) = arg.name.strip_prefix("-- ") {
260 arg.double_dash = SpecDoubleDashChoices::Required;
261 arg.name = name.to_string();
262 }
263 arg
264 }
265}
266impl FromStr for SpecArg {
267 type Err = UsageErr;
268 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
269 Ok(input.into())
270 }
271}
272
273#[cfg(feature = "clap")]
274impl From<&clap::Arg> for SpecArg {
275 fn from(arg: &clap::Arg) -> Self {
276 let required = arg.is_required_set();
277 let help = arg.get_help().map(|s| s.to_string());
278 let help_long = arg.get_long_help().map(|s| s.to_string());
279 let help_first_line = help.as_ref().map(|s| string::first_line(s));
280 let hide = arg.is_hide_set();
281 let var = matches!(
282 arg.get_action(),
283 clap::ArgAction::Count | clap::ArgAction::Append
284 );
285 let choices = arg
286 .get_possible_values()
287 .iter()
288 .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
289 .collect::<Vec<_>>();
290 let mut arg = Self {
291 name: arg
292 .get_value_names()
293 .unwrap_or_default()
294 .first()
295 .cloned()
296 .unwrap_or_default()
297 .to_string(),
298 usage: "".into(),
299 required,
300 double_dash: if arg.is_last_set() {
301 SpecDoubleDashChoices::Required
302 } else if arg.is_trailing_var_arg_set() {
303 SpecDoubleDashChoices::Automatic
304 } else {
305 SpecDoubleDashChoices::Optional
306 },
307 help,
308 help_long,
309 help_md: None,
310 help_first_line,
311 var,
312 var_max: None,
313 var_min: None,
314 hide,
315 default: arg
316 .get_default_values()
317 .iter()
318 .map(|v| v.to_string_lossy().to_string())
319 .collect(),
320 choices: None,
321 env: None,
322 };
323 if !choices.is_empty() {
324 arg.choices = Some(SpecChoices { choices });
325 }
326
327 arg
328 }
329}
330
331impl Display for SpecArg {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 write!(f, "{}", self.usage())
334 }
335}
336impl PartialEq for SpecArg {
337 fn eq(&self, other: &Self) -> bool {
338 self.name == other.name
339 }
340}
341impl Eq for SpecArg {}
342impl Hash for SpecArg {
343 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
344 self.name.hash(state);
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use crate::Spec;
351 use insta::assert_snapshot;
352
353 #[test]
354 fn test_arg_with_env() {
355 let spec = Spec::parse(
356 &Default::default(),
357 r#"
358arg "<input>" env="MY_INPUT" help="Input file"
359arg "<output>" env="MY_OUTPUT"
360 "#,
361 )
362 .unwrap();
363
364 assert_snapshot!(spec, @r#"
365 arg <input> help="Input file" env=MY_INPUT
366 arg <output> env=MY_OUTPUT
367 "#);
368
369 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
370 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
371
372 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
373 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
374 }
375
376 #[test]
377 fn test_arg_with_env_child_node() {
378 let spec = Spec::parse(
379 &Default::default(),
380 r#"
381arg "<input>" help="Input file" {
382 env "MY_INPUT"
383}
384arg "<output>" {
385 env "MY_OUTPUT"
386}
387 "#,
388 )
389 .unwrap();
390
391 assert_snapshot!(spec, @r#"
392 arg <input> help="Input file" env=MY_INPUT
393 arg <output> env=MY_OUTPUT
394 "#);
395
396 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
397 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
398
399 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
400 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
401 }
402
403 #[test]
404 fn test_arg_variadic_syntax() {
405 use crate::SpecArg;
406
407 let arg: SpecArg = "<files>...".into();
409 assert_eq!(arg.name, "files");
410 assert!(arg.var);
411 assert!(arg.required);
412
413 let arg: SpecArg = "[files]...".into();
415 assert_eq!(arg.name, "files");
416 assert!(arg.var);
417 assert!(!arg.required);
418
419 let arg: SpecArg = "<files>…".into();
421 assert_eq!(arg.name, "files");
422 assert!(arg.var);
423
424 let arg: SpecArg = "[files]…".into();
425 assert_eq!(arg.name, "files");
426 assert!(arg.var);
427 assert!(!arg.required);
428 }
429}