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