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