repo_description: 🤖 Just a command runner
code: use {super::*, serde::Serialize};
  
  type Scopes<'src, 'run> = BTreeMap<
    Modulepath,
    (
      &'run Justfile<'src>,
      &'run Scope<'src, 'run>,
      &'run BTreeMap<String, String>,
    ),
  >;
  
  #[derive(Debug, PartialEq, Serialize)]
  pub(crate) struct Justfile<'src> {
    pub(crate) aliases: Table<'src, Alias<'src>>,
    pub(crate) assignments: Table<'src, Assignment<'src>>,
    #[serde(rename = "first", serialize_with = "keyed::serialize_option")]
    pub(crate) default: Option<Arc<Recipe<'src>>>,
    pub(crate) doc: Option<String>,
    #[serde(skip)]
    pub(crate) functions: Table<'src, FunctionDefinition<'src>>,
    pub(crate) groups: Vec<StringLiteral<'src>>,
    #[serde(skip)]
    pub(crate) loaded: Vec<PathBuf>,
    pub(crate) module_path: Modulepath,
    pub(crate) modules: Table<'src, Self>,
    #[serde(skip)]
    pub(crate) name: Option<Name<'src>>,
    #[serde(skip)]
    pub(crate) private: bool,
    pub(crate) recipes: Table<'src, Arc<Recipe<'src>>>,
    pub(crate) settings: Settings,
    pub(crate) source: PathBuf,
    pub(crate) unexports: HashSet<String>,
    #[serde(skip)]
    pub(crate) unstable_features: BTreeSet<UnstableFeature>,
    pub(crate) warnings: Vec<Warning>,
    #[serde(skip)]
    pub(crate) working_directory: PathBuf,
  }
  
  impl<'src> Justfile<'src> {
    fn find_suggestion(
      input: &str,
      candidates: impl Iterator<Item = Suggestion<'src>>,
    ) -> Option<Suggestion<'src>> {
      candidates
        .map(|suggestion| (edit_distance(input, suggestion.name), suggestion))
        .filter(|(distance, _suggestion)| *distance < 3)
        .min_by_key(|(distance, _suggestion)| *distance)
        .map(|(_distance, suggestion)| suggestion)
    }
  
    pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
      Self::find_suggestion(
        input,
        self
          .recipes
          .values()
          .filter(|recipe| recipe.is_public())
          .map(|recipe| Suggestion {
            name: recipe.name(),
            target: None,
          })
          .chain(
            self
              .aliases
              .values()
              .filter(|alias| alias.is_public())
              .map(|alias| Suggestion {
                name: alias.name.lexeme(),
                target: Some(alias.target.name.lexeme()),
              }),
          ),
      )
    }
  
    pub(crate) fn suggest_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
      Self::find_suggestion(
        input,
        self
          .modules
          .keys()
          .map(|name| Suggestion { name, target: None }),
      )
    }
  
    pub(crate) fn suggest_variable_or_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
      Self::find_suggestion(
        input,
        self
          .assignments
          .keys()
          .chain(self.modules.keys())
          .map(|name| Suggestion { name, target: None }),
      )
    }
  
    fn evaluate_scopes<'run>(
      &'run self,
      config: &'run Config,
      dotenv_arena: &'run Arena<BTreeMap<String, String>>,
      overrides: &'run HashMap<Number, String>,
      parent_dotenv: Option<&'run BTreeMap<String, String>>,
      root: &'run Scope<'src, 'run>,
      scope_arena: &'run Arena<Scope<'src, 'run>>,
      scopes: &mut Scopes<'src, 'run>,
      search: &'run Search,
      variable_references: Option<&HashSet<Number>>,
    ) -> RunResult<'src> {
      let dotenv = if config.load_dotenv {
        let working_directory = if self.is_submodule() {
          &self.working_directory
        } else {
          &search.working_directory
        };
        load_dotenv(config, &self.settings, working_directory)?
      } else {
        BTreeMap::new()
      };
  
      let dotenv = if let Some(parent_dotenv) = parent_dotenv {
        parent_dotenv
          .iter()
          .map(|(key, value)| (key.clone(), value.clone()))
          .chain(dotenv)
          .collect()
      } else {
        dotenv
      };
  
      let dotenv = dotenv_arena.alloc(dotenv);
  
      let scope = Evaluator::evaluate_assignments(
        config,
        dotenv,
        self,
        overrides,
        root,
        search,
        variable_references,
      )?;
  
      let scope = scope_arena.alloc(scope);
      scopes.insert(self.module_path.clone(), (self, scope, dotenv));
  
      for module in self.modules.values() {
        module.evaluate_scopes(
          config,
          dotenv_arena,
          overrides,
          Some(dotenv),
          scope,
          scope_arena,
          scopes,
          search,
          variable_references,
        )?;
      }
  
      Ok(())
    }
  
    pub(crate) fn run(
      &self,
      config: &Config,
      search: &Search,
      arguments: &[String],
      overrides: &HashMap<Number, String>,
    ) -> RunResult<'src> {
      let root = Scope::root();
      let dotenv_arena = Arena::new();
      let scope_arena = Arena::new();
      let mut scopes = BTreeMap::new();
  
      match &config.subcommand {
        Subcommand::Choose { .. } | Subcommand::Run { .. } => {
          let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
  
          let invocations = InvocationParser::parse_invocations(self, &arguments)?;
  
          if config.one && invocations.len() > 1 {
            return Err(Error::ExcessInvocations {
              invocations: invocations.len(),
            });
          }
  
          let variable_references = if self.settings.lazy {
            let mut variable_references = HashSet::new();
  
            let mut stack = Vec::new();
  
            for invocation in &invocations {
              stack.push(invocation.recipe);
            }
  
            while let Some(recipe) = stack.pop() {
              variable_references.extend(&recipe.variable_references);
              for dependency in &recipe.dependencies {
                stack.push(&dependency.recipe);
              }
            }
  
            Some(variable_references)
          } else {
            None
          };
  
          self.evaluate_scopes(
            config,
            &dotenv_arena,
            overrides,
            None,
            &root,
            &scope_arena,
            &mut scopes,
            search,
            variable_references.as_ref(),
          )?;
  
          let ran = Ran::default();
          for invocation in invocations {
            Self::run_recipe(
              &invocation.arguments,
              config,
              false,
              &ran,
              invocation.recipe,
              &scopes,
              search,
            )?;
          }
  
          Ok(())
        }
        Subcommand::Command {
          binary, arguments, ..
        } => {
          let mut command = if config.shell_command {
            let mut command = self.settings.shell_command(config);
            command.arg(binary);
            command
          } else {
            Command::resolve(binary)
          };
  
          command
            .args(arguments)
            .current_dir(&search.working_directory);
  
          self.evaluate_scopes(
            config,
            &dotenv_arena,
            overrides,
            None,
            &root,
            &scope_arena,
            &mut scopes,
            search,
            Some(HashSet::new()).as_ref(),
          )?;
  
          let (_module, scope, dotenv) = scopes.get(&self.module_path).unwrap();
          let scope = scope.child();
  
          command.export(&self.settings, dotenv, &scope, &self.unexports);
  
          let (result, caught) = command.status_guard();
  
          let status = result.map_err(|io_error| Error::CommandInvoke {
            binary: binary.clone(),
            arguments: arguments.clone(),
            io_error,
          })?;
  
          if !status.success() {
            return Err(Error::CommandStatus {
              binary: binary.clone(),
              arguments: arguments.clone(),
              status,
            });
          }
  
          if let Some(signal) = caught {
            return Err(Error::Interrupted { signal });
          }
  
          Ok(())
        }
        Subcommand::Evaluate { format, path } => {
          let (module, variable, variable_references) = self.evaluation_target(path)?;
  
          self.evaluate_scopes(
            config,
            &dotenv_arena,
            overrides,
            None,
            &root,
            &scope_arena,
            &mut scopes,
            search,
            Some(&variable_references),
          )?;
  
          let scope = scopes.get(&module.module_path).unwrap().1;
  
          if let Some(variable) = variable {
            print!("{}", scope.value(variable).unwrap());
          } else {
            let width = scope.names().fold(0, |max, name| name.len().max(max));
  
            for binding in scope.bindings() {
              if !binding.private {
                match format {
                  EvaluateFormat::Just => {
                    println!("{0:1$} := \"{2}\"", binding.name, width, binding.value);
                  }
                  EvaluateFormat::Shell => {
                    if binding.export || module.settings.export {
                      print!("export ");
                    }
                    print!("{}=\"", binding.name.lexeme().replace('-', "_"));
                    for c in binding.value.chars() {
                      if matches!(c, '!' | '"' | '$' | '\\' | '`') {
                        print!("\\");
                      }
                      print!("{c}");
                    }
                    println!("\"");
                  }
                }
              }
            }
          }
  
          Ok(())
        }
        _ => unreachable!(),
      }
    }
  
    pub(crate) fn evaluation_target<'a>(
      &'a self,
      path: &'a Modulepath,
    ) -> RunResult<'src, (&'a Justfile<'a>, Option<&'a str>, HashSet<Number>)> {
      let mut current = self;
  
      let mut variable = None;
  
      for (i, component) in path.components.iter().enumerate() {
        let last = i + 1 == path.components.len();
  
        if last && current.assignments.contains_key(component) {
          variable = Some(component.as_ref());
          break;
        }
  
        if let Some(module) = current.modules.get(component) {
          current = module;
        } else if last {
          return Err(Error::EvalUnknownSubmoduleOrVariable {
            suggestion: current.suggest_variable_or_submodule(component),
            component: component.into(),
          });
        } else {
          return Err(Error::EvalUnknownSubmodule {
            suggestion: current.suggest_submodule(component),
            component: component.into(),
          });
        }
      }
  
      let variable_references = if let Some(variable) = variable {
        HashSet::from([current.assignments.get(variable).unwrap().number])
      } else {
        current
          .assignments
          .values()
          .filter(|assignment| !assignment.private)
          .map(|assignment| assignment.number)
          .collect()
      };
  
      Ok((current, variable, variable_references))
    }
  
    pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
      if let Some(&unstable_feature) = self.unstable_features.iter().next() {
        config.require_unstable(self, unstable_feature)?;
      }
  
      for module in self.modules.values() {
        module.check_unstable(config)?;
      }
  
      Ok(())
    }
  
    pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
      self.aliases.get(name)
    }
  
    pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
      self
        .recipes
        .get(name)
        .map(Arc::as_ref)
        .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
    }
  
    pub(crate) fn is_submodule(&self) -> bool {
      self.name.is_some()
    }
  
    pub(crate) fn name(&self) -> &'src str {
      self.name.map(|name| name.lexeme()).unwrap_or_default()
    }
  
    fn run_recipe(
      arguments: &[Vec<String>],
      config: &Config,
      is_dependency: bool,
      ran: &Ran,
      recipe: &Recipe<'src>,
      scopes: &Scopes<'src, '_>,
      search: &Search,
    ) -> RunResult<'src> {
      let mutex = ran.mutex(recipe, arguments);
  
      let mut guard = mutex.lock().unwrap();
  
      if *guard {
        return Ok(());
      }
  
      let (module, scope, dotenv) = scopes
        .get(recipe.module_path())
        .expect("failed to retrieve scope for module");
  
      let context = ExecutionContext {
        config,
        dotenv,
        module,
        search,
      };
  
      let (outer, positional) = Evaluator::evaluate_parameters(
        arguments,
        &context,
        is_dependency,
        &recipe.parameters,
        recipe,
        scope,
      )?;
  
      let scope = outer.child();
  
      let mut evaluator = Evaluator::new(&context, BTreeMap::new(), true, &scope);
  
      if !config.yes && !recipe.confirm(&mut evaluator)? {
        return Err(Error::NotConfirmed {
          recipe: recipe.name(),
        });
      }
  
      Self::run_dependencies(
        config,
        &context,
        recipe.priors(),
        &mut evaluator,
        ran,
        recipe,
        scopes,
        search,
      )?;
  
      recipe.run(&context, &scope, &positional, is_dependency)?;
  
      Self::run_dependencies(
        config,
        &context,
        recipe.subsequents(),
        &mut evaluator,
        &Ran::default(),
        recipe,
        scopes,
        search,
      )?;
  
      *guard = true;
  
      Ok(())
    }
  
    fn run_dependencies<'run>(
      config: &Config,
      context: &ExecutionContext<'src, 'run>,
      dependencies: &[Dependency<'src>],
      evaluator: &mut Evaluator<'src, 'run>,
      ran: &Ran,
      recipe: &Recipe<'src>,
      scopes: &Scopes<'src, 'run>,
      search: &Search,
    ) -> RunResult<'src> {
      if context.config.no_dependencies {
        return Ok(());
      }
  
      let mut evaluated = Vec::new();
      for Dependency { recipe, arguments } in dependencies {
        let mut grouped = Vec::new();
        for group in arguments {
          let evaluated_group = group
            .iter()
            .map(|argument| evaluator.evaluate_expression(argument))
            .collect::<RunResult<Vec<String>>>()?;
          grouped.push(evaluated_group);
        }
        evaluated.push((recipe, grouped));
      }
  
      if recipe.is_parallel() {
        thread::scope::<_, RunResult>(|thread_scope| {
          let mut handles = Vec::new();
          for (recipe, arguments) in evaluated {
            handles.push(thread_scope.spawn(move || {
              Self::run_recipe(&arguments, config, true, ran, recipe, scopes, search)
            }));
          }
          for handle in handles {
            handle
              .join()
              .map_err(|_| Error::internal("parallel dependency thread panicked"))??;
          }
          Ok(())
        })?;
      } else {
        for (recipe, arguments) in evaluated {
          Self::run_recipe(&arguments, config, true, ran, recipe, scopes, search)?;
        }
      }
  
      Ok(())
    }
  
    pub(crate) fn public_modules(&self, config: &Config) -> Vec<&Justfile> {
      let mut modules = self
        .modules
        .values()
        .filter(|module| !module.private)
        .collect::<Vec<&Justfile>>();
  
      if config.unsorted {
        modules.sort_by_key(|module| {
          module
            .name
            .map(|name| name.token.offset)
            .unwrap_or_default()
        });
      }
  
      modules
    }
  
    pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {
      let mut recipes = self
        .recipes
        .values()
        .map(AsRef::as_ref)
        .filter(|recipe| recipe.is_public())
        .collect::<Vec<&Recipe>>();
  
      if config.unsorted {
        recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
      }
  
      recipes
    }
  
    pub(crate) fn public_recipes_recursive(&self, config: &Config) -> Vec<&Recipe> {
      let mut recipes = Vec::new();
  
      let mut stack = vec![self];
      while let Some(current) = stack.pop() {
        for recipe in current.public_recipes(config) {
          recipes.push(recipe);
        }
  
        for module in current.public_modules(config).into_iter().rev() {
          stack.push(module);
        }
      }
  
      recipes
    }
  
    pub(crate) fn public_aliases_recursive(&self, config: &Config) -> Vec<(&Alias, &Modulepath)> {
      let mut aliases = Vec::new();
  
      let mut stack = vec![self];
      while let Some(current) = stack.pop() {
        for alias in current.aliases.values() {
          if alias.is_public() {
            aliases.push((alias, &current.module_path));
          }
        }
  
        for module in current.public_modules(config).into_iter().rev() {
          stack.push(module);
        }
      }
  
      aliases
    }
  
    pub(crate) fn groups(&self) -> Vec<&str> {
      self
        .groups
        .iter()
        .map(|group| group.cooked.as_str())
        .collect()
    }
  
    pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {
      let mut groups = Vec::new();
  
      for recipe in self.recipes.values() {
        if recipe.is_public() {
          for group in recipe.groups() {
            groups.push((recipe.import_offsets.as_slice(), recipe.name.offset, group));
          }
        }
      }
  
      for submodule in self.public_modules(config) {
        for group in submodule.groups() {
          groups.push((&[], submodule.name.unwrap().offset, group.to_string()));
        }
      }
  
      if config.unsorted {
        groups.sort();
      } else {
        groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));
      }
  
      let mut seen = HashSet::new();
  
      groups.retain(|(_, _, group)| seen.insert(group.clone()));
  
      groups.into_iter().map(|(_, _, group)| group).collect()
    }
  }
  
  impl ColorDisplay for Justfile<'_> {
    fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
      let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
      for (name, assignment) in &self.assignments {
        if assignment.export {
          write!(f, "export ")?;
        }
        write!(f, "{name} := {}", assignment.value)?;
        items -= 1;
        if items != 0 {
          write!(f, "\n\n")?;
        }
      }
      for alias in self.aliases.values() {
        write!(f, "{alias}")?;
        items -= 1;
        if items != 0 {
          write!(f, "\n\n")?;
        }
      }
      for recipe in self.recipes.values() {
        write!(f, "{}", recipe.color_display(color))?;
        items -= 1;
        if items != 0 {
          write!(f, "\n\n")?;
        }
      }
      Ok(())
    }
  }
  
  impl<'src> Keyed<'src> for Justfile<'src> {
    fn key(&self) -> &'src str {
      self.name()
    }
  }
  
  #[cfg(test)]
  mod tests {
    use {super::*, Error::*, testing::compile};
  
    run_error! {
      name: unknown_recipe_no_suggestion,
      src: "a:\nb:\nc:",
      args: ["a", "xyz", "y", "z"],
      error: UnknownRecipe {
        recipe,
        suggestion,
      },
      check: {
        assert_eq!(recipe, "xyz");
        assert_eq!(suggestion, None);
      }
    }
  
    run_error! {
      name: unknown_recipe_with_suggestion,
      src: "a:\nb:\nc:",
      args: ["a", "x", "y", "z"],
      error: UnknownRecipe {
        recipe,
        suggestion,
      },
      check: {
        assert_eq!(recipe, "x");
        assert_eq!(suggestion, Some(Suggestion {
          name: "a",
          target: None,
        }));
      }
    }
  
    run_error! {
      name: unknown_recipe_show_alias_suggestion,
      src: "
        foo:
          echo foo
  
        alias z := foo
      ",
      args: ["zz"],
      error: UnknownRecipe {
        recipe,
        suggestion,
      },
      check: {
        assert_eq!(recipe, "zz");
        assert_eq!(suggestion, Some(Suggestion {
          name: "z",
          target: Some("foo"),
        }
      ));
      }
    }
  
    run_error! {
      name: code_error,
      src: "
        fail:
          @exit 100
      ",
      args: ["fail"],
      error: Code {
        recipe,
        line_number,
        code,
        print_message,
      },
      check: {
        assert_eq!(recipe, "fail");
        assert_eq!(code, 100);
        assert_eq!(line_number, Some(2));
        assert!(print_message);
      }
    }
  
    run_error! {
      name: run_args,
      src: "
        a return code:
          @x() { {{return}} {{code + '0'}}; }; x
      ",
      args: ["a", "return", "15"],
      error: Code {
        recipe,
        line_number,
        code,
        print_message,
      },
      check: {
        assert_eq!(recipe, "a");
        assert_eq!(code, 150);
        assert_eq!(line_number, Some(2));
        assert!(print_message);
      }
    }
  
    run_error! {
      name: missing_some_arguments,
      src: "a b c d:",
      args: ["a", "b", "c"],
      error: PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
      },
      check: {
        assert_eq!(recipe.name(), "a");
        assert_eq!(found, 2);
        assert_eq!(min, 3);
        assert_eq!(max, 3);
      }
    }
  
    run_error! {
      name: missing_some_arguments_variadic,
      src: "a b c +d:",
      args: ["a", "B", "C"],
      error: PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
      },
      check: {
        assert_eq!(recipe.name(), "a");
        assert_eq!(found, 2);
        assert_eq!(min, 3);
        assert_eq!(max, usize::MAX - 1);
      }
    }
  
    run_error! {
      name: missing_all_arguments,
      src: "a b c d:\n echo {{b}}{{c}}{{d}}",
      args: ["a"],
      error: PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
      },
      check: {
        assert_eq!(recipe.name(), "a");
        assert_eq!(found, 0);
        assert_eq!(min, 3);
        assert_eq!(max, 3);
      }
    }
  
    run_error! {
      name: missing_some_defaults,
      src: "a b c d='hello':",
      args: ["a", "b"],
      error: PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
      },
      check: {
        assert_eq!(recipe.name(), "a");
        assert_eq!(found, 1);
        assert_eq!(min, 2);
        assert_eq!(max, 3);
      }
    }
  
    run_error! {
      name: missing_all_defaults,
      src: "a b c='r' d='h':",
      args: ["a"],
      error: PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
      },
      check: {
        assert_eq!(recipe.name(), "a");
        assert_eq!(found, 0);
        assert_eq!(min, 1);
        assert_eq!(max, 3);
      }
    }
  
    run_error! {
      name: export_failure,
      src: "
        export foo := 'a'
        baz := 'c'
        export bar := 'b'
        export abc := foo + bar + baz
  
        wut:
          echo $foo $bar $baz
      ",
      args: ["--quiet", "wut"],
      error: Code {
        recipe,
        line_number,
        print_message,
        ..
      },
      check: {
        assert_eq!(recipe, "wut");
        assert_eq!(line_number, Some(7));
        assert!(print_message);
      }
    }
  
    fn case(input: &str, expected: &str) {
      let justfile = compile(input);
      let actual = format!("{}", justfile.color_display(Color::never()));
      assert_eq!(actual, expected);
      let reparsed = compile(&actual);
      let redumped = format!("{}", reparsed.color_display(Color::never()));
      assert_eq!(redumped, actual);
    }
  
    #[test]
    fn parse_empty() {
      case(
        "
  
  # hello
  
  
      ",
        "",
      );
    }
  
    #[test]
    fn parse_string_default() {
      case(
        r#"
  
  foo a="b\t":
  
  
    "#,
        r#"foo a="b\t":"#,
      );
    }
  
    #[test]
    fn parse_multiple() {
      case(
        "
  a:
  b:
  ", "a:
  
  b:",
      );
    }
  
    #[test]
    fn parse_variadic() {
      case(
        "
  
  foo +a:
  
  
    ",
        "foo +a:",
      );
    }
  
    #[test]
    fn parse_variadic_string_default() {
      case(
        "
  
  foo +a='Hello':
  
  
    ",
        "foo +a='Hello':",
      );
    }
  
    #[test]
    fn parse_raw_string_default() {
      case(
        r"
  
  foo a='b\t':
  
  
    ",
        r"foo a='b\t':",
      );
    }
  
    #[test]
    fn parse_export() {
      case(
        "
  export a := 'hello'
  
    ",
        "export a := 'hello'",
      );
    }
  
    #[test]
    fn parse_alias_after_target() {
      case(
        "
  foo:
    echo a
  alias f := foo
  ",
        "alias f := foo
  
  foo:
      echo a",
      );
    }
  
    #[test]
    fn parse_alias_before_target() {
      case(
        "
  alias f := foo
  foo:
    echo a
  ",
        "alias f := foo
  
  foo:
      echo a",
      );
    }
  
    #[test]
    fn parse_alias_with_comment() {
      case(
        "
  alias f := foo # comment
  foo:
    echo a
  ",
        "alias f := foo
  
  foo:
      echo a",
      );
    }
  
    #[test]
    fn parse_complex() {
      case(
        "
  x:
  y:
  z:
  foo := \"xx\"
  bar := foo
  goodbye := \"y\"
  hello a b    c   : x y    z #hello
    #! blah
    #blarg
    {{ foo + bar}}abc{{ goodbye\t  + \"x\" }}xyz
    1
    2
    3
  ",
        "bar := foo
  
  foo := \"xx\"
  
  goodbye := \"y\"
  
  hello a b c: x y z
      #! blah
      #blarg
      {{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
      1
      2
      3
  
  x:
  
  y:
  
  z:",
      );
    }
  
    #[test]
    fn parse_shebang() {
      case(
        "
  practicum := 'hello'
  install:
  \t#!/bin/sh
  \tif [[ -f {{practicum}} ]]; then
  \t\treturn
  \tfi
  ",
        "practicum := 'hello'
  
  install:
      #!/bin/sh
      if [[ -f {{ practicum }} ]]; then
      \treturn
      fi",
      );
    }
  
    #[test]
    fn parse_simple_shebang() {
      case("a:\n #!\n  print(1)", "a:\n    #!\n     print(1)");
    }
  
    #[test]
    fn parse_assignments() {
      case(
        "a := '0'
  c := a + b + a + b
  b := '1'
  ",
        "a := '0'
  
  b := '1'
  
  c := a + b + a + b",
      );
    }
  
    #[test]
    fn parse_assignment_backticks() {
      case(
        "a := `echo hello`
  c := a + b + a + b
  b := `echo goodbye`",
        "a := `echo hello`
  
  b := `echo goodbye`
  
  c := a + b + a + b",
      );
    }
  
    #[test]
    fn parse_interpolation_backticks() {
      case(
        "a:
    echo {{  `echo hello` + 'blarg'   }} {{   `echo bob`   }}",
        "a:
      echo {{ `echo hello` + 'blarg' }} {{ `echo bob` }}",
      );
    }
  
    #[test]
    fn eof_test() {
      case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
    }
  
    #[test]
    fn string_quote_escape() {
      case(r#"a := "hello\"""#, r#"a := "hello\"""#);
    }
  
    #[test]
    fn string_escapes() {
      case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
    }
  
    #[test]
    fn parameters() {
      case(
        "a b c:
    {{b}} {{c}}",
        "a b c:
      {{ b }} {{ c }}",
      );
    }
  
    #[test]
    fn unary_functions() {
      case(
        "
  x := arch()
  
  a:
    {{os()}} {{os_family()}} {{num_cpus()}}",
        "x := arch()
  
  a:
      {{ os() }} {{ os_family() }} {{ num_cpus() }}",
      );
    }
  
    #[test]
    fn env_functions() {
      case(
        "
  x := env_var('foo',)
  
  a:
    {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var('baz'))}}",
        "x := env_var('foo')
  
  a:
      {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var('baz')) }}",
      );
    }
  
    #[test]
    fn parameter_default_string() {
      case(
        "
  f x='abc':
  ",
        "f x='abc':",
      );
    }
  
    #[test]
    fn parameter_default_raw_string() {
      case(
        "
  f x='abc':
  ",
        "f x='abc':",
      );
    }
  
    #[test]
    fn parameter_default_backtick() {
      case(
        "
  f x=`echo hello`:
  ",
        "f x=`echo hello`:",
      );
    }
  
    #[test]
    fn parameter_default_concatenation_string() {
      case(
        "
  f x=(`echo hello` + 'foo'):
  ",
        "f x=(`echo hello` + 'foo'):",
      );
    }
  
    #[test]
    fn parameter_default_concatenation_variable() {
      case(
        "
  x := '10'
  f y=(`echo hello` + x) +z='foo':
  ",
        "x := '10'
  
  f y=(`echo hello` + x) +z='foo':",
      );
    }
  
    #[test]
    fn parameter_default_multiple() {
      case(
        "
  x := '10'
  f y=(`echo hello` + x) +z=('foo' + 'bar'):
  ",
        "x := '10'
  
  f y=(`echo hello` + x) +z=('foo' + 'bar'):",
      );
    }
  
    #[test]
    fn concatenation_in_group() {
      case("x := ('0' + '1')", "x := ('0' + '1')");
    }
  
    #[test]
    fn string_in_group() {
      case("x := ('0'   )", "x := ('0')");
    }
  
    #[rustfmt::skip]
    #[test]
    fn escaped_dos_newlines() {
      case("@spam:\r
  \t{ \\\r
  \t\tfiglet test; \\\r
  \t\tcargo build --color always 2>&1; \\\r
  \t\tcargo test  --color always -- --color always 2>&1; \\\r
  \t} | less\r
  ",
  "@spam:
      { \\
      \tfiglet test; \\
      \tcargo build --color always 2>&1; \\
      \tcargo test  --color always -- --color always 2>&1; \\
      } | less");
    }
  }
  