pub_just/
justfile.rs

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}