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