1use {super::*, serde::Serialize};
2
3#[derive(Debug)]
4struct Invocation<'src: 'run, 'run> {
5 arguments: Vec<&'run str>,
6 module: &'run Justfile<'src>,
7 recipe: &'run Recipe<'src>,
8 scope: &'run Scope<'src, 'run>,
9}
10
11#[derive(Debug, PartialEq, Serialize)]
12pub struct Justfile<'src> {
13 pub aliases: Table<'src, Alias<'src>>,
14 pub assignments: Table<'src, Assignment<'src>>,
15 pub doc: Option<String>,
16 #[serde(rename = "first", serialize_with = "keyed::serialize_option")]
17 pub default: Option<Rc<Recipe<'src>>>,
18 #[serde(skip)]
19 pub loaded: Vec<PathBuf>,
20 pub groups: Vec<String>,
21 pub modules: Table<'src, Justfile<'src>>,
22 #[serde(skip)]
23 pub name: Option<Name<'src>>,
24 pub recipes: Table<'src, Rc<Recipe<'src>>>,
25 pub settings: Settings<'src>,
26 #[serde(skip)]
27 pub source: PathBuf,
28 pub unexports: HashSet<String>,
29 #[serde(skip)]
30 pub unstable_features: BTreeSet<UnstableFeature>,
31 pub warnings: Vec<Warning>,
32 #[serde(skip)]
33 pub working_directory: PathBuf,
34}
35
36impl<'src> Justfile<'src> {
37 fn find_suggestion(
38 input: &str,
39 candidates: impl Iterator<Item = Suggestion<'src>>,
40 ) -> Option<Suggestion<'src>> {
41 candidates
42 .map(|suggestion| (edit_distance(input, suggestion.name), suggestion))
43 .filter(|(distance, _suggestion)| *distance < 3)
44 .min_by_key(|(distance, _suggestion)| *distance)
45 .map(|(_distance, suggestion)| suggestion)
46 }
47
48 pub fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
49 Self::find_suggestion(
50 input,
51 self
52 .recipes
53 .keys()
54 .map(|name| Suggestion { name, target: None })
55 .chain(self.aliases.iter().map(|(name, alias)| Suggestion {
56 name,
57 target: Some(alias.target.name.lexeme()),
58 })),
59 )
60 }
61
62 pub fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
63 Self::find_suggestion(
64 input,
65 self
66 .assignments
67 .keys()
68 .map(|name| Suggestion { name, target: None }),
69 )
70 }
71
72 pub fn run(
73 &self,
74 config: &Config,
75 search: &Search,
76 overrides: &BTreeMap<String, String>,
77 arguments: &[String],
78 ) -> RunResult<'src> {
79 let unknown_overrides = overrides
80 .keys()
81 .filter(|name| !self.assignments.contains_key(name.as_str()))
82 .cloned()
83 .collect::<Vec<String>>();
84
85 if !unknown_overrides.is_empty() {
86 return Err(Error::UnknownOverrides {
87 overrides: unknown_overrides,
88 });
89 }
90
91 let dotenv = if config.load_dotenv {
92 load_dotenv(config, &self.settings, &search.working_directory)?
93 } else {
94 BTreeMap::new()
95 };
96
97 let root = Scope::root();
98
99 let scope = Evaluator::evaluate_assignments(config, &dotenv, self, overrides, &root, search)?;
100
101 match &config.subcommand {
102 Subcommand::Command {
103 binary, arguments, ..
104 } => {
105 let mut command = if config.shell_command {
106 let mut command = self.settings.shell_command(config);
107 command.arg(binary);
108 command
109 } else {
110 Command::new(binary)
111 };
112
113 command.args(arguments);
114
115 command.current_dir(&search.working_directory);
116
117 let scope = scope.child();
118
119 command.export(&self.settings, &dotenv, &scope, &self.unexports);
120
121 let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
122 Error::CommandInvoke {
123 binary: binary.clone(),
124 arguments: arguments.clone(),
125 io_error,
126 }
127 })?;
128
129 if !status.success() {
130 return Err(Error::CommandStatus {
131 binary: binary.clone(),
132 arguments: arguments.clone(),
133 status,
134 });
135 };
136
137 return Ok(());
138 }
139 Subcommand::Evaluate { variable, .. } => {
140 if let Some(variable) = variable {
141 if let Some(value) = scope.value(variable) {
142 print!("{value}");
143 } else {
144 return Err(Error::EvalUnknownVariable {
145 suggestion: self.suggest_variable(variable),
146 variable: variable.clone(),
147 });
148 }
149 } else {
150 let width = scope.names().fold(0, |max, name| name.len().max(max));
151
152 for binding in scope.bindings() {
153 if !binding.private {
154 println!(
155 "{0:1$} := \"{2}\"",
156 binding.name.lexeme(),
157 width,
158 binding.value
159 );
160 }
161 }
162 }
163
164 return Ok(());
165 }
166 _ => {}
167 }
168
169 let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
170
171 let groups = ArgumentParser::parse_arguments(self, &arguments)?;
172
173 let arena: Arena<Scope> = Arena::new();
174 let mut invocations = Vec::<Invocation>::new();
175 let mut scopes = BTreeMap::new();
176
177 for group in &groups {
178 invocations.push(self.invocation(
179 &arena,
180 &group.arguments,
181 config,
182 &dotenv,
183 &scope,
184 &group.path,
185 0,
186 &mut scopes,
187 search,
188 )?);
189 }
190
191 if config.one && invocations.len() > 1 {
192 return Err(Error::ExcessInvocations {
193 invocations: invocations.len(),
194 });
195 }
196
197 let mut ran = Ran::default();
198 for invocation in invocations {
199 let context = ExecutionContext {
200 config,
201 dotenv: &dotenv,
202 module: invocation.module,
203 scope: invocation.scope,
204 search,
205 };
206
207 Self::run_recipe(
208 &invocation
209 .arguments
210 .iter()
211 .copied()
212 .map(str::to_string)
213 .collect::<Vec<String>>(),
214 &context,
215 &mut ran,
216 invocation.recipe,
217 false,
218 )?;
219 }
220
221 Ok(())
222 }
223
224 pub fn check_unstable(&self, config: &Config) -> RunResult<'src> {
225 if let Some(&unstable_feature) = self.unstable_features.iter().next() {
226 config.require_unstable(self, unstable_feature)?;
227 }
228
229 for module in self.modules.values() {
230 module.check_unstable(config)?;
231 }
232
233 Ok(())
234 }
235
236 pub fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
237 self.aliases.get(name)
238 }
239
240 pub fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
241 self
242 .recipes
243 .get(name)
244 .map(Rc::as_ref)
245 .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
246 }
247
248 fn invocation<'run>(
249 &'run self,
250 arena: &'run Arena<Scope<'src, 'run>>,
251 arguments: &[&'run str],
252 config: &'run Config,
253 dotenv: &'run BTreeMap<String, String>,
254 parent: &'run Scope<'src, 'run>,
255 path: &'run [String],
256 position: usize,
257 scopes: &mut BTreeMap<&'run [String], &'run Scope<'src, 'run>>,
258 search: &'run Search,
259 ) -> RunResult<'src, Invocation<'src, 'run>> {
260 if position + 1 == path.len() {
261 let recipe = self.get_recipe(&path[position]).unwrap();
262 Ok(Invocation {
263 arguments: arguments.into(),
264 module: self,
265 recipe,
266 scope: parent,
267 })
268 } else {
269 let module = self.modules.get(&path[position]).unwrap();
270
271 let scope = if let Some(scope) = scopes.get(&path[..position]) {
272 scope
273 } else {
274 let scope = Evaluator::evaluate_assignments(
275 config,
276 dotenv,
277 module,
278 &BTreeMap::new(),
279 parent,
280 search,
281 )?;
282 let scope = arena.alloc(scope);
283 scopes.insert(path, scope);
284 scopes.get(path).unwrap()
285 };
286
287 module.invocation(
288 arena,
289 arguments,
290 config,
291 dotenv,
292 scope,
293 path,
294 position + 1,
295 scopes,
296 search,
297 )
298 }
299 }
300
301 pub fn is_submodule(&self) -> bool {
302 self.name.is_some()
303 }
304
305 pub fn name(&self) -> &'src str {
306 self.name.map(|name| name.lexeme()).unwrap_or_default()
307 }
308
309 fn run_recipe(
310 arguments: &[String],
311 context: &ExecutionContext<'src, '_>,
312 ran: &mut Ran<'src>,
313 recipe: &Recipe<'src>,
314 is_dependency: bool,
315 ) -> RunResult<'src> {
316 if ran.has_run(&recipe.namepath, arguments) {
317 return Ok(());
318 }
319
320 if !context.config.yes && !recipe.confirm()? {
321 return Err(Error::NotConfirmed {
322 recipe: recipe.name(),
323 });
324 }
325
326 let (outer, positional) =
327 Evaluator::evaluate_parameters(context, is_dependency, arguments, &recipe.parameters)?;
328
329 let scope = outer.child();
330
331 let mut evaluator = Evaluator::new(context, true, &scope);
332
333 if !context.config.no_dependencies {
334 for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
335 let arguments = arguments
336 .iter()
337 .map(|argument| evaluator.evaluate_expression(argument))
338 .collect::<RunResult<Vec<String>>>()?;
339
340 Self::run_recipe(&arguments, context, ran, recipe, true)?;
341 }
342 }
343
344 recipe.run(context, &scope, &positional, is_dependency)?;
345
346 if !context.config.no_dependencies {
347 let mut ran = Ran::default();
348
349 for Dependency { recipe, arguments } in recipe.subsequents() {
350 let mut evaluated = Vec::new();
351
352 for argument in arguments {
353 evaluated.push(evaluator.evaluate_expression(argument)?);
354 }
355
356 Self::run_recipe(&evaluated, context, &mut ran, recipe, true)?;
357 }
358 }
359
360 ran.ran(&recipe.namepath, arguments.to_vec());
361
362 Ok(())
363 }
364
365 pub fn modules(&self, config: &Config) -> Vec<&Justfile> {
366 let mut modules = self.modules.values().collect::<Vec<&Justfile>>();
367
368 if config.unsorted {
369 modules.sort_by_key(|module| {
370 module
371 .name
372 .map(|name| name.token.offset)
373 .unwrap_or_default()
374 });
375 }
376
377 modules
378 }
379
380 pub fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {
381 let mut recipes = self
382 .recipes
383 .values()
384 .map(AsRef::as_ref)
385 .filter(|recipe| recipe.is_public())
386 .collect::<Vec<&Recipe>>();
387
388 if config.unsorted {
389 recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
390 }
391
392 recipes
393 }
394
395 pub fn groups(&self) -> &[String] {
396 &self.groups
397 }
398
399 pub fn public_groups(&self, config: &Config) -> Vec<String> {
400 let mut groups = Vec::new();
401
402 for recipe in self.recipes.values() {
403 if recipe.is_public() {
404 for group in recipe.groups() {
405 groups.push((recipe.import_offsets.as_slice(), recipe.name.offset, group));
406 }
407 }
408 }
409
410 for submodule in self.modules.values() {
411 for group in submodule.groups() {
412 groups.push((&[], submodule.name.unwrap().offset, group.to_string()));
413 }
414 }
415
416 if config.unsorted {
417 groups.sort();
418 } else {
419 groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));
420 }
421
422 let mut seen = HashSet::new();
423
424 groups.retain(|(_, _, group)| seen.insert(group.clone()));
425
426 groups.into_iter().map(|(_, _, group)| group).collect()
427 }
428}
429
430impl<'src> ColorDisplay for Justfile<'src> {
431 fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
432 let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
433 for (name, assignment) in &self.assignments {
434 if assignment.export {
435 write!(f, "export ")?;
436 }
437 write!(f, "{name} := {}", assignment.value)?;
438 items -= 1;
439 if items != 0 {
440 write!(f, "\n\n")?;
441 }
442 }
443 for alias in self.aliases.values() {
444 write!(f, "{alias}")?;
445 items -= 1;
446 if items != 0 {
447 write!(f, "\n\n")?;
448 }
449 }
450 for recipe in self.recipes.values() {
451 write!(f, "{}", recipe.color_display(color))?;
452 items -= 1;
453 if items != 0 {
454 write!(f, "\n\n")?;
455 }
456 }
457 Ok(())
458 }
459}
460
461impl<'src> Keyed<'src> for Justfile<'src> {
462 fn key(&self) -> &'src str {
463 self.name()
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 use testing::compile;
472 use Error::*;
473
474 run_error! {
475 name: unknown_recipe_no_suggestion,
476 src: "a:\nb:\nc:",
477 args: ["a", "xyz", "y", "z"],
478 error: UnknownRecipe {
479 recipe,
480 suggestion,
481 },
482 check: {
483 assert_eq!(recipe, "xyz");
484 assert_eq!(suggestion, None);
485 }
486 }
487
488 run_error! {
489 name: unknown_recipe_with_suggestion,
490 src: "a:\nb:\nc:",
491 args: ["a", "x", "y", "z"],
492 error: UnknownRecipe {
493 recipe,
494 suggestion,
495 },
496 check: {
497 assert_eq!(recipe, "x");
498 assert_eq!(suggestion, Some(Suggestion {
499 name: "a",
500 target: None,
501 }));
502 }
503 }
504
505 run_error! {
506 name: unknown_recipe_show_alias_suggestion,
507 src: "
508 foo:
509 echo foo
510
511 alias z := foo
512 ",
513 args: ["zz"],
514 error: UnknownRecipe {
515 recipe,
516 suggestion,
517 },
518 check: {
519 assert_eq!(recipe, "zz");
520 assert_eq!(suggestion, Some(Suggestion {
521 name: "z",
522 target: Some("foo"),
523 }
524 ));
525 }
526 }
527
528 run_error! {
529 name: code_error,
530 src: "
531 fail:
532 @exit 100
533 ",
534 args: ["fail"],
535 error: Code {
536 recipe,
537 line_number,
538 code,
539 print_message,
540 },
541 check: {
542 assert_eq!(recipe, "fail");
543 assert_eq!(code, 100);
544 assert_eq!(line_number, Some(2));
545 assert!(print_message);
546 }
547 }
548
549 run_error! {
550 name: run_args,
551 src: r#"
552 a return code:
553 @x() { {{return}} {{code + "0"}}; }; x
554 "#,
555 args: ["a", "return", "15"],
556 error: Code {
557 recipe,
558 line_number,
559 code,
560 print_message,
561 },
562 check: {
563 assert_eq!(recipe, "a");
564 assert_eq!(code, 150);
565 assert_eq!(line_number, Some(2));
566 assert!(print_message);
567 }
568 }
569
570 run_error! {
571 name: missing_some_arguments,
572 src: "a b c d:",
573 args: ["a", "b", "c"],
574 error: ArgumentCountMismatch {
575 recipe,
576 parameters,
577 found,
578 min,
579 max,
580 },
581 check: {
582 let param_names = parameters
583 .iter()
584 .map(|p| p.name.lexeme())
585 .collect::<Vec<&str>>();
586 assert_eq!(recipe, "a");
587 assert_eq!(param_names, ["b", "c", "d"]);
588 assert_eq!(found, 2);
589 assert_eq!(min, 3);
590 assert_eq!(max, 3);
591 }
592 }
593
594 run_error! {
595 name: missing_some_arguments_variadic,
596 src: "a b c +d:",
597 args: ["a", "B", "C"],
598 error: ArgumentCountMismatch {
599 recipe,
600 parameters,
601 found,
602 min,
603 max,
604 },
605 check: {
606 let param_names = parameters
607 .iter()
608 .map(|p| p.name.lexeme())
609 .collect::<Vec<&str>>();
610 assert_eq!(recipe, "a");
611 assert_eq!(param_names, ["b", "c", "d"]);
612 assert_eq!(found, 2);
613 assert_eq!(min, 3);
614 assert_eq!(max, usize::MAX - 1);
615 }
616 }
617
618 run_error! {
619 name: missing_all_arguments,
620 src: "a b c d:\n echo {{b}}{{c}}{{d}}",
621 args: ["a"],
622 error: ArgumentCountMismatch {
623 recipe,
624 parameters,
625 found,
626 min,
627 max,
628 },
629 check: {
630 let param_names = parameters
631 .iter()
632 .map(|p| p.name.lexeme())
633 .collect::<Vec<&str>>();
634 assert_eq!(recipe, "a");
635 assert_eq!(param_names, ["b", "c", "d"]);
636 assert_eq!(found, 0);
637 assert_eq!(min, 3);
638 assert_eq!(max, 3);
639 }
640 }
641
642 run_error! {
643 name: missing_some_defaults,
644 src: "a b c d='hello':",
645 args: ["a", "b"],
646 error: ArgumentCountMismatch {
647 recipe,
648 parameters,
649 found,
650 min,
651 max,
652 },
653 check: {
654 let param_names = parameters
655 .iter()
656 .map(|p| p.name.lexeme())
657 .collect::<Vec<&str>>();
658 assert_eq!(recipe, "a");
659 assert_eq!(param_names, ["b", "c", "d"]);
660 assert_eq!(found, 1);
661 assert_eq!(min, 2);
662 assert_eq!(max, 3);
663 }
664 }
665
666 run_error! {
667 name: missing_all_defaults,
668 src: "a b c='r' d='h':",
669 args: ["a"],
670 error: ArgumentCountMismatch {
671 recipe,
672 parameters,
673 found,
674 min,
675 max,
676 },
677 check: {
678 let param_names = parameters
679 .iter()
680 .map(|p| p.name.lexeme())
681 .collect::<Vec<&str>>();
682 assert_eq!(recipe, "a");
683 assert_eq!(param_names, ["b", "c", "d"]);
684 assert_eq!(found, 0);
685 assert_eq!(min, 1);
686 assert_eq!(max, 3);
687 }
688 }
689
690 run_error! {
691 name: unknown_overrides,
692 src: "
693 a:
694 echo {{`f() { return 100; }; f`}}
695 ",
696 args: ["foo=bar", "baz=bob", "a"],
697 error: UnknownOverrides { overrides },
698 check: {
699 assert_eq!(overrides, &["baz", "foo"]);
700 }
701 }
702
703 run_error! {
704 name: export_failure,
705 src: r#"
706 export foo := "a"
707 baz := "c"
708 export bar := "b"
709 export abc := foo + bar + baz
710
711 wut:
712 echo $foo $bar $baz
713 "#,
714 args: ["--quiet", "wut"],
715 error: Code {
716 recipe,
717 line_number,
718 print_message,
719 ..
720 },
721 check: {
722 assert_eq!(recipe, "wut");
723 assert_eq!(line_number, Some(7));
724 assert!(print_message);
725 }
726 }
727
728 fn case(input: &str, expected: &str) {
729 let justfile = compile(input);
730 let actual = format!("{}", justfile.color_display(Color::never()));
731 assert_eq!(actual, expected);
732 println!("Re-parsing...");
733 let reparsed = compile(&actual);
734 let redumped = format!("{}", reparsed.color_display(Color::never()));
735 assert_eq!(redumped, actual);
736 }
737
738 #[test]
739 fn parse_empty() {
740 case(
741 "
742
743# hello
744
745
746 ",
747 "",
748 );
749 }
750
751 #[test]
752 fn parse_string_default() {
753 case(
754 r#"
755
756foo a="b\t":
757
758
759 "#,
760 r#"foo a="b\t":"#,
761 );
762 }
763
764 #[test]
765 fn parse_multiple() {
766 case(
767 r"
768a:
769b:
770", r"a:
771
772b:",
773 );
774 }
775
776 #[test]
777 fn parse_variadic() {
778 case(
779 r"
780
781foo +a:
782
783
784 ",
785 r"foo +a:",
786 );
787 }
788
789 #[test]
790 fn parse_variadic_string_default() {
791 case(
792 r#"
793
794foo +a="Hello":
795
796
797 "#,
798 r#"foo +a="Hello":"#,
799 );
800 }
801
802 #[test]
803 fn parse_raw_string_default() {
804 case(
805 r"
806
807foo a='b\t':
808
809
810 ",
811 r"foo a='b\t':",
812 );
813 }
814
815 #[test]
816 fn parse_export() {
817 case(
818 r#"
819export a := "hello"
820
821 "#,
822 r#"export a := "hello""#,
823 );
824 }
825
826 #[test]
827 fn parse_alias_after_target() {
828 case(
829 r"
830foo:
831 echo a
832alias f := foo
833",
834 r"alias f := foo
835
836foo:
837 echo a",
838 );
839 }
840
841 #[test]
842 fn parse_alias_before_target() {
843 case(
844 r"
845alias f := foo
846foo:
847 echo a
848",
849 r"alias f := foo
850
851foo:
852 echo a",
853 );
854 }
855
856 #[test]
857 fn parse_alias_with_comment() {
858 case(
859 r"
860alias f := foo #comment
861foo:
862 echo a
863",
864 r"alias f := foo
865
866foo:
867 echo a",
868 );
869 }
870
871 #[test]
872 fn parse_complex() {
873 case(
874 "
875x:
876y:
877z:
878foo := \"xx\"
879bar := foo
880goodbye := \"y\"
881hello a b c : x y z #hello
882 #! blah
883 #blarg
884 {{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
885 1
886 2
887 3
888",
889 "bar := foo
890
891foo := \"xx\"
892
893goodbye := \"y\"
894
895hello a b c: x y z
896 #! blah
897 #blarg
898 {{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
899 1
900 2
901 3
902
903x:
904
905y:
906
907z:",
908 );
909 }
910
911 #[test]
912 fn parse_shebang() {
913 case(
914 "
915practicum := 'hello'
916install:
917\t#!/bin/sh
918\tif [[ -f {{practicum}} ]]; then
919\t\treturn
920\tfi
921",
922 "practicum := 'hello'
923
924install:
925 #!/bin/sh
926 if [[ -f {{ practicum }} ]]; then
927 \treturn
928 fi",
929 );
930 }
931
932 #[test]
933 fn parse_simple_shebang() {
934 case("a:\n #!\n print(1)", "a:\n #!\n print(1)");
935 }
936
937 #[test]
938 fn parse_assignments() {
939 case(
940 r#"a := "0"
941c := a + b + a + b
942b := "1"
943"#,
944 r#"a := "0"
945
946b := "1"
947
948c := a + b + a + b"#,
949 );
950 }
951
952 #[test]
953 fn parse_assignment_backticks() {
954 case(
955 "a := `echo hello`
956c := a + b + a + b
957b := `echo goodbye`",
958 "a := `echo hello`
959
960b := `echo goodbye`
961
962c := a + b + a + b",
963 );
964 }
965
966 #[test]
967 fn parse_interpolation_backticks() {
968 case(
969 r#"a:
970 echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
971 r#"a:
972 echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
973 );
974 }
975
976 #[test]
977 fn eof_test() {
978 case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
979 }
980
981 #[test]
982 fn string_quote_escape() {
983 case(r#"a := "hello\"""#, r#"a := "hello\"""#);
984 }
985
986 #[test]
987 fn string_escapes() {
988 case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
989 }
990
991 #[test]
992 fn parameters() {
993 case(
994 "a b c:
995 {{b}} {{c}}",
996 "a b c:
997 {{ b }} {{ c }}",
998 );
999 }
1000
1001 #[test]
1002 fn unary_functions() {
1003 case(
1004 "
1005x := arch()
1006
1007a:
1008 {{os()}} {{os_family()}} {{num_cpus()}}",
1009 "x := arch()
1010
1011a:
1012 {{ os() }} {{ os_family() }} {{ num_cpus() }}",
1013 );
1014 }
1015
1016 #[test]
1017 fn env_functions() {
1018 case(
1019 r#"
1020x := env_var('foo',)
1021
1022a:
1023 {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
1024 r#"x := env_var('foo')
1025
1026a:
1027 {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#,
1028 );
1029 }
1030
1031 #[test]
1032 fn parameter_default_string() {
1033 case(
1034 r#"
1035f x="abc":
1036"#,
1037 r#"f x="abc":"#,
1038 );
1039 }
1040
1041 #[test]
1042 fn parameter_default_raw_string() {
1043 case(
1044 r"
1045f x='abc':
1046",
1047 r"f x='abc':",
1048 );
1049 }
1050
1051 #[test]
1052 fn parameter_default_backtick() {
1053 case(
1054 r"
1055f x=`echo hello`:
1056",
1057 r"f x=`echo hello`:",
1058 );
1059 }
1060
1061 #[test]
1062 fn parameter_default_concatenation_string() {
1063 case(
1064 r#"
1065f x=(`echo hello` + "foo"):
1066"#,
1067 r#"f x=(`echo hello` + "foo"):"#,
1068 );
1069 }
1070
1071 #[test]
1072 fn parameter_default_concatenation_variable() {
1073 case(
1074 r#"
1075x := "10"
1076f y=(`echo hello` + x) +z="foo":
1077"#,
1078 r#"x := "10"
1079
1080f y=(`echo hello` + x) +z="foo":"#,
1081 );
1082 }
1083
1084 #[test]
1085 fn parameter_default_multiple() {
1086 case(
1087 r#"
1088x := "10"
1089f y=(`echo hello` + x) +z=("foo" + "bar"):
1090"#,
1091 r#"x := "10"
1092
1093f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
1094 );
1095 }
1096
1097 #[test]
1098 fn concatenation_in_group() {
1099 case("x := ('0' + '1')", "x := ('0' + '1')");
1100 }
1101
1102 #[test]
1103 fn string_in_group() {
1104 case("x := ('0' )", "x := ('0')");
1105 }
1106
1107 #[rustfmt::skip]
1108 #[test]
1109 fn escaped_dos_newlines() {
1110 case("@spam:\r
1111\t{ \\\r
1112\t\tfiglet test; \\\r
1113\t\tcargo build --color always 2>&1; \\\r
1114\t\tcargo test --color always -- --color always 2>&1; \\\r
1115\t} | less\r
1116",
1117"@spam:
1118 { \\
1119 \tfiglet test; \\
1120 \tcargo build --color always 2>&1; \\
1121 \tcargo test --color always -- --color always 2>&1; \\
1122 } | less");
1123 }
1124}