1use super::*;
2
3#[allow(clippy::doc_markdown)]
4pub struct ArgumentParser<'src: 'run, 'run> {
21 arguments: &'run [&'run str],
22 next: usize,
23 root: &'run Justfile<'src>,
24}
25
26#[derive(Debug, PartialEq)]
27pub struct ArgumentGroup<'run> {
28 pub arguments: Vec<&'run str>,
29 pub path: Vec<String>,
30}
31
32impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
33 pub fn parse_arguments(
34 root: &'run Justfile<'src>,
35 arguments: &'run [&'run str],
36 ) -> RunResult<'src, Vec<ArgumentGroup<'run>>> {
37 let mut groups = Vec::new();
38
39 let mut invocation_parser = Self {
40 arguments,
41 next: 0,
42 root,
43 };
44
45 loop {
46 groups.push(invocation_parser.parse_group()?);
47
48 if invocation_parser.next == arguments.len() {
49 break;
50 }
51 }
52
53 Ok(groups)
54 }
55
56 fn parse_group(&mut self) -> RunResult<'src, ArgumentGroup<'run>> {
57 let (recipe, path) = if let Some(next) = self.next() {
58 if next.contains(':') {
59 let module_path =
60 ModulePath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {
61 recipe: next.into(),
62 suggestion: None,
63 })?;
64 let (recipe, path, _) = self.resolve_recipe(true, &module_path.path)?;
65 self.next += 1;
66 (recipe, path)
67 } else {
68 let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
69 self.next += consumed;
70 (recipe, path)
71 }
72 } else {
73 let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
74 assert_eq!(consumed, 0);
75 (recipe, path)
76 };
77
78 let rest = self.rest();
79
80 let argument_range = recipe.argument_range();
81 let argument_count = cmp::min(rest.len(), recipe.max_arguments());
82 if !argument_range.range_contains(&argument_count) {
83 return Err(Error::ArgumentCountMismatch {
84 recipe: recipe.name(),
85 parameters: recipe.parameters.clone(),
86 found: rest.len(),
87 min: recipe.min_arguments(),
88 max: recipe.max_arguments(),
89 });
90 }
91
92 let arguments = rest[..argument_count].to_vec();
93
94 self.next += argument_count;
95
96 Ok(ArgumentGroup { arguments, path })
97 }
98
99 fn resolve_recipe(
100 &self,
101 module_path: bool,
102 args: &[impl AsRef<str>],
103 ) -> RunResult<'src, (&'run Recipe<'src>, Vec<String>, usize)> {
104 let mut current = self.root;
105 let mut path = Vec::new();
106
107 for (i, arg) in args.iter().enumerate() {
108 let arg = arg.as_ref();
109
110 path.push(arg.to_string());
111
112 if let Some(module) = current.modules.get(arg) {
113 current = module;
114 } else if let Some(recipe) = current.get_recipe(arg) {
115 if module_path && i + 1 < args.len() {
116 return Err(Error::ExpectedSubmoduleButFoundRecipe {
117 path: if module_path {
118 path.join("::")
119 } else {
120 path.join(" ")
121 },
122 });
123 }
124 return Ok((recipe, path, i + 1));
125 } else {
126 if module_path && i + 1 < args.len() {
127 return Err(Error::UnknownSubmodule {
128 path: path.join("::"),
129 });
130 }
131
132 return Err(Error::UnknownRecipe {
133 recipe: if module_path {
134 path.join("::")
135 } else {
136 path.join(" ")
137 },
138 suggestion: current.suggest_recipe(arg),
139 });
140 }
141 }
142
143 if let Some(recipe) = ¤t.default {
144 recipe.check_can_be_default_recipe()?;
145 path.push(recipe.name().into());
146 Ok((recipe, path, args.len()))
147 } else if current.recipes.is_empty() {
148 Err(Error::NoRecipes)
149 } else {
150 Err(Error::NoDefaultRecipe)
151 }
152 }
153
154 fn next(&self) -> Option<&'run str> {
155 self.arguments.get(self.next).copied()
156 }
157
158 fn rest(&self) -> &[&'run str] {
159 &self.arguments[self.next..]
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use {super::*, tempfile::TempDir};
166
167 trait TempDirExt {
168 fn write(&self, path: &str, content: &str);
169 }
170
171 impl TempDirExt for TempDir {
172 fn write(&self, path: &str, content: &str) {
173 let path = self.path().join(path);
174 fs::create_dir_all(path.parent().unwrap()).unwrap();
175 fs::write(path, content).unwrap();
176 }
177 }
178
179 #[test]
180 fn single_no_arguments() {
181 let justfile = testing::compile("foo:");
182
183 assert_eq!(
184 ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap(),
185 vec![ArgumentGroup {
186 path: vec!["foo".into()],
187 arguments: Vec::new()
188 }],
189 );
190 }
191
192 #[test]
193 fn single_with_argument() {
194 let justfile = testing::compile("foo bar:");
195
196 assert_eq!(
197 ArgumentParser::parse_arguments(&justfile, &["foo", "baz"]).unwrap(),
198 vec![ArgumentGroup {
199 path: vec!["foo".into()],
200 arguments: vec!["baz"],
201 }],
202 );
203 }
204
205 #[test]
206 fn single_argument_count_mismatch() {
207 let justfile = testing::compile("foo bar:");
208
209 assert_matches!(
210 ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap_err(),
211 Error::ArgumentCountMismatch {
212 recipe: "foo",
213 found: 0,
214 min: 1,
215 max: 1,
216 ..
217 },
218 );
219 }
220
221 #[test]
222 fn single_unknown() {
223 let justfile = testing::compile("foo:");
224
225 assert_matches!(
226 ArgumentParser::parse_arguments(&justfile, &["bar"]).unwrap_err(),
227 Error::UnknownRecipe {
228 recipe,
229 suggestion: None
230 } if recipe == "bar",
231 );
232 }
233
234 #[test]
235 fn multiple_unknown() {
236 let justfile = testing::compile("foo:");
237
238 assert_matches!(
239 ArgumentParser::parse_arguments(&justfile, &["bar", "baz"]).unwrap_err(),
240 Error::UnknownRecipe {
241 recipe,
242 suggestion: None
243 } if recipe == "bar",
244 );
245 }
246
247 #[test]
248 fn recipe_in_submodule() {
249 let loader = Loader::new();
250 let tempdir = tempfile::tempdir().unwrap();
251 let path = tempdir.path().join("justfile");
252 fs::write(&path, "mod foo").unwrap();
253 fs::create_dir(tempdir.path().join("foo")).unwrap();
254 fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
255 let compilation = Compiler::compile(&loader, &path).unwrap();
256
257 assert_eq!(
258 ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(),
259 vec![ArgumentGroup {
260 path: vec!["foo".into(), "bar".into()],
261 arguments: Vec::new()
262 }],
263 );
264 }
265
266 #[test]
267 fn recipe_in_submodule_unknown() {
268 let loader = Loader::new();
269 let tempdir = tempfile::tempdir().unwrap();
270 let path = tempdir.path().join("justfile");
271 fs::write(&path, "mod foo").unwrap();
272 fs::create_dir(tempdir.path().join("foo")).unwrap();
273 fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
274 let compilation = Compiler::compile(&loader, &path).unwrap();
275
276 assert_matches!(
277 ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(),
278 Error::UnknownRecipe {
279 recipe,
280 suggestion: None
281 } if recipe == "foo zzz",
282 );
283 }
284
285 #[test]
286 fn recipe_in_submodule_path_unknown() {
287 let tempdir = tempfile::tempdir().unwrap();
288 tempdir.write("justfile", "mod foo");
289 tempdir.write("foo.just", "bar:");
290
291 let loader = Loader::new();
292 let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
293
294 assert_matches!(
295 ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(),
296 Error::UnknownRecipe {
297 recipe,
298 suggestion: None
299 } if recipe == "foo::zzz",
300 );
301 }
302
303 #[test]
304 fn module_path_not_consumed() {
305 let tempdir = tempfile::tempdir().unwrap();
306 tempdir.write("justfile", "mod foo");
307 tempdir.write("foo.just", "bar:");
308
309 let loader = Loader::new();
310 let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
311
312 assert_matches!(
313 ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(),
314 Error::ExpectedSubmoduleButFoundRecipe {
315 path,
316 } if path == "foo::bar",
317 );
318 }
319
320 #[test]
321 fn no_recipes() {
322 let tempdir = tempfile::tempdir().unwrap();
323 tempdir.write("justfile", "");
324
325 let loader = Loader::new();
326 let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
327
328 assert_matches!(
329 ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
330 Error::NoRecipes,
331 );
332 }
333
334 #[test]
335 fn default_recipe_requires_arguments() {
336 let tempdir = tempfile::tempdir().unwrap();
337 tempdir.write("justfile", "foo bar:");
338
339 let loader = Loader::new();
340 let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
341
342 assert_matches!(
343 ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
344 Error::DefaultRecipeRequiresArguments {
345 recipe: "foo",
346 min_arguments: 1,
347 },
348 );
349 }
350
351 #[test]
352 fn no_default_recipe() {
353 let tempdir = tempfile::tempdir().unwrap();
354 tempdir.write("justfile", "import 'foo.just'");
355 tempdir.write("foo.just", "bar:");
356
357 let loader = Loader::new();
358 let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
359
360 assert_matches!(
361 ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
362 Error::NoDefaultRecipe,
363 );
364 }
365
366 #[test]
367 fn complex_grouping() {
368 let justfile = testing::compile(
369 "
370FOO A B='blarg':
371 echo foo: {{A}} {{B}}
372
373BAR X:
374 echo bar: {{X}}
375
376BAZ +Z:
377 echo baz: {{Z}}
378",
379 );
380
381 assert_eq!(
382 ArgumentParser::parse_arguments(
383 &justfile,
384 &["BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"]
385 )
386 .unwrap(),
387 vec![
388 ArgumentGroup {
389 path: vec!["BAR".into()],
390 arguments: vec!["0"],
391 },
392 ArgumentGroup {
393 path: vec!["FOO".into()],
394 arguments: vec!["1", "2"],
395 },
396 ArgumentGroup {
397 path: vec!["BAZ".into()],
398 arguments: vec!["3", "4", "5"],
399 },
400 ],
401 );
402 }
403}