2019-04-11 15:23:14 -07:00
|
|
|
use crate::common::*;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2018-12-08 14:29:41 -08:00
|
|
|
use std::process::{Command, ExitStatus, Stdio};
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
|
|
|
|
/// otherwise return an `RuntimeError::UnknownFailure`
|
|
|
|
fn error_from_signal(
|
2018-12-08 14:29:41 -08:00
|
|
|
recipe: &str,
|
2017-11-16 23:30:08 -08:00
|
|
|
line_number: Option<usize>,
|
2018-12-08 14:29:41 -08:00
|
|
|
exit_status: ExitStatus,
|
2017-11-16 23:30:08 -08:00
|
|
|
) -> RuntimeError {
|
|
|
|
match Platform::signal_from_exit_status(exit_status) {
|
2018-12-08 14:29:41 -08:00
|
|
|
Some(signal) => RuntimeError::Signal {
|
|
|
|
recipe,
|
|
|
|
line_number,
|
|
|
|
signal,
|
|
|
|
},
|
|
|
|
None => RuntimeError::Unknown {
|
|
|
|
recipe,
|
|
|
|
line_number,
|
|
|
|
},
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
/// A recipe, e.g. `foo: bar baz`
|
2017-11-16 23:30:08 -08:00
|
|
|
#[derive(PartialEq, Debug)]
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) struct Recipe<'a> {
|
2019-11-07 10:55:15 -08:00
|
|
|
pub(crate) dependencies: Vec<Name<'a>>,
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) doc: Option<&'a str>,
|
2019-11-07 10:55:15 -08:00
|
|
|
pub(crate) body: Vec<Line<'a>>,
|
|
|
|
pub(crate) name: Name<'a>,
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) parameters: Vec<Parameter<'a>>,
|
|
|
|
pub(crate) private: bool,
|
|
|
|
pub(crate) quiet: bool,
|
|
|
|
pub(crate) shebang: bool,
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Recipe<'a> {
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {
|
2019-04-11 12:30:29 -07:00
|
|
|
self.min_arguments()..=self.max_arguments()
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) fn min_arguments(&self) -> usize {
|
2018-12-08 14:29:41 -08:00
|
|
|
self
|
|
|
|
.parameters
|
|
|
|
.iter()
|
|
|
|
.filter(|p| p.default.is_none())
|
|
|
|
.count()
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) fn max_arguments(&self) -> usize {
|
2017-11-16 23:30:08 -08:00
|
|
|
if self.parameters.iter().any(|p| p.variadic) {
|
|
|
|
usize::MAX - 1
|
|
|
|
} else {
|
|
|
|
self.parameters.len()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
pub(crate) fn name(&self) -> &'a str {
|
|
|
|
self.name.lexeme()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn line_number(&self) -> usize {
|
|
|
|
self.name.line
|
|
|
|
}
|
|
|
|
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) fn run(
|
2017-11-16 23:30:08 -08:00
|
|
|
&self,
|
2018-12-08 14:29:41 -08:00
|
|
|
context: &RecipeContext<'a>,
|
|
|
|
arguments: &[&'a str],
|
2019-04-11 15:23:14 -07:00
|
|
|
dotenv: &BTreeMap<String, String>,
|
2017-11-17 17:28:06 -08:00
|
|
|
) -> RunResult<'a, ()> {
|
2019-10-07 02:06:45 -07:00
|
|
|
let config = &context.config;
|
2018-08-27 18:36:40 -07:00
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.verbosity.loquacious() {
|
|
|
|
let color = config.color.stderr().banner();
|
2018-12-08 14:29:41 -08:00
|
|
|
eprintln!(
|
|
|
|
"{}===> Running recipe `{}`...{}",
|
|
|
|
color.prefix(),
|
|
|
|
self.name,
|
|
|
|
color.suffix()
|
|
|
|
);
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-04-11 15:23:14 -07:00
|
|
|
let mut argument_map = BTreeMap::new();
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2019-04-11 23:58:08 -07:00
|
|
|
let mut evaluator = AssignmentEvaluator {
|
|
|
|
assignments: &empty(),
|
|
|
|
evaluated: empty(),
|
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
to be implicitly inherited by subprocesses, we now use
`Command::current_dir` to set it explicitly. This feels much better,
since we aren't dependent on the implicit state of the process's
current directory.
- Subcommand execution is much improved.
- Added a ton of tests for config parsing, config execution, working
dir, and search dir.
- Error messages are improved. Many more will be colored.
- The Config is now onwed, instead of borrowing from the arguments and
the `clap::ArgMatches` object. This is a huge ergonomic improvement,
especially in tests, and I don't think anyone will notice.
- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
matching git, which I think is what most people will expect.
- Added a cute `tmptree!{}` macro, for creating temporary directories
populated with directories and files for tests.
- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00
|
|
|
working_directory: context.working_directory,
|
2019-04-11 23:58:08 -07:00
|
|
|
scope: &context.scope,
|
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
to be implicitly inherited by subprocesses, we now use
`Command::current_dir` to set it explicitly. This feels much better,
since we aren't dependent on the implicit state of the process's
current directory.
- Subcommand execution is much improved.
- Added a ton of tests for config parsing, config execution, working
dir, and search dir.
- Error messages are improved. Many more will be colored.
- The Config is now onwed, instead of borrowing from the arguments and
the `clap::ArgMatches` object. This is a huge ergonomic improvement,
especially in tests, and I don't think anyone will notice.
- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
matching git, which I think is what most people will expect.
- Added a cute `tmptree!{}` macro, for creating temporary directories
populated with directories and files for tests.
- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00
|
|
|
config,
|
2019-04-11 23:58:08 -07:00
|
|
|
dotenv,
|
|
|
|
};
|
|
|
|
|
2017-11-16 23:30:08 -08:00
|
|
|
let mut rest = arguments;
|
|
|
|
for parameter in &self.parameters {
|
|
|
|
let value = if rest.is_empty() {
|
|
|
|
match parameter.default {
|
2019-04-11 23:58:08 -07:00
|
|
|
Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
|
2018-12-08 14:29:41 -08:00
|
|
|
None => {
|
|
|
|
return Err(RuntimeError::Internal {
|
|
|
|
message: "missing parameter without default".to_string(),
|
2019-04-11 12:30:29 -07:00
|
|
|
});
|
2018-12-08 14:29:41 -08:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
} else if parameter.variadic {
|
|
|
|
let value = Cow::Owned(rest.to_vec().join(" "));
|
|
|
|
rest = &[];
|
|
|
|
value
|
|
|
|
} else {
|
|
|
|
let value = Cow::Borrowed(rest[0]);
|
|
|
|
rest = &rest[1..];
|
|
|
|
value
|
|
|
|
};
|
2019-11-07 10:55:15 -08:00
|
|
|
argument_map.insert(parameter.name.lexeme(), value);
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.shebang {
|
|
|
|
let mut evaluated_lines = vec![];
|
2019-11-07 10:55:15 -08:00
|
|
|
for line in &self.body {
|
|
|
|
evaluated_lines.push(evaluator.evaluate_line(&line.fragments, &argument_map)?);
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.dry_run || self.quiet {
|
2017-11-16 23:30:08 -08:00
|
|
|
for line in &evaluated_lines {
|
|
|
|
eprintln!("{}", line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.dry_run {
|
2017-11-16 23:30:08 -08:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2019-07-13 01:55:06 -07:00
|
|
|
let tmp = tempfile::Builder::new()
|
|
|
|
.prefix("just")
|
|
|
|
.tempdir()
|
|
|
|
.map_err(|error| RuntimeError::TmpdirIoError {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2019-07-13 01:55:06 -07:00
|
|
|
io_error: error,
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
let mut path = tmp.path().to_path_buf();
|
2019-11-07 10:55:15 -08:00
|
|
|
path.push(self.name());
|
2017-11-16 23:30:08 -08:00
|
|
|
{
|
2018-12-08 14:29:41 -08:00
|
|
|
let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
io_error: error,
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
let mut text = String::new();
|
|
|
|
// add the shebang
|
|
|
|
text += &evaluated_lines[0];
|
|
|
|
text += "\n";
|
|
|
|
// add blank lines so that lines in the generated script
|
|
|
|
// have the same line number as the corresponding lines
|
|
|
|
// in the justfile
|
2019-11-07 10:55:15 -08:00
|
|
|
for _ in 1..(self.line_number() + 2) {
|
2017-11-16 23:30:08 -08:00
|
|
|
text += "\n"
|
|
|
|
}
|
|
|
|
for line in &evaluated_lines[1..] {
|
|
|
|
text += line;
|
|
|
|
text += "\n";
|
|
|
|
}
|
2018-08-31 00:04:06 -07:00
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.verbosity.grandiloquent() {
|
|
|
|
eprintln!("{}", config.color.doc().stderr().paint(&text));
|
2018-08-31 00:04:06 -07:00
|
|
|
}
|
|
|
|
|
2017-11-16 23:30:08 -08:00
|
|
|
f.write_all(text.as_bytes())
|
2018-12-08 14:29:41 -08:00
|
|
|
.map_err(|error| RuntimeError::TmpdirIoError {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
io_error: error,
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// make the script executable
|
2018-12-08 14:29:41 -08:00
|
|
|
Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
io_error: error,
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2018-12-08 14:29:41 -08:00
|
|
|
let shebang_line = evaluated_lines
|
|
|
|
.first()
|
2017-11-16 23:30:08 -08:00
|
|
|
.ok_or_else(|| RuntimeError::Internal {
|
2018-12-08 14:29:41 -08:00
|
|
|
message: "evaluated_lines was empty".to_string(),
|
2017-11-16 23:30:08 -08:00
|
|
|
})?;
|
|
|
|
|
2018-12-08 14:29:41 -08:00
|
|
|
let Shebang {
|
|
|
|
interpreter,
|
|
|
|
argument,
|
|
|
|
} = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
|
|
|
|
message: format!("bad shebang line: {}", shebang_line),
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
// create a command to run the script
|
2018-12-08 14:29:41 -08:00
|
|
|
let mut command =
|
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
to be implicitly inherited by subprocesses, we now use
`Command::current_dir` to set it explicitly. This feels much better,
since we aren't dependent on the implicit state of the process's
current directory.
- Subcommand execution is much improved.
- Added a ton of tests for config parsing, config execution, working
dir, and search dir.
- Error messages are improved. Many more will be colored.
- The Config is now onwed, instead of borrowing from the arguments and
the `clap::ArgMatches` object. This is a huge ergonomic improvement,
especially in tests, and I don't think anyone will notice.
- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
matching git, which I think is what most people will expect.
- Added a cute `tmptree!{}` macro, for creating temporary directories
populated with directories and files for tests.
- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00
|
|
|
Platform::make_shebang_command(&path, context.working_directory, interpreter, argument)
|
|
|
|
.map_err(|output_error| RuntimeError::Cygpath {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
output_error,
|
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
to be implicitly inherited by subprocesses, we now use
`Command::current_dir` to set it explicitly. This feels much better,
since we aren't dependent on the implicit state of the process's
current directory.
- Subcommand execution is much improved.
- Added a ton of tests for config parsing, config execution, working
dir, and search dir.
- Error messages are improved. Many more will be colored.
- The Config is now onwed, instead of borrowing from the arguments and
the `clap::ArgMatches` object. This is a huge ergonomic improvement,
especially in tests, and I don't think anyone will notice.
- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
matching git, which I think is what most people will expect.
- Added a cute `tmptree!{}` macro, for creating temporary directories
populated with directories and files for tests.
- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
command.export_environment_variables(&context.scope, dotenv)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
// run it!
|
2018-08-27 16:03:52 -07:00
|
|
|
match InterruptHandler::guard(|| command.status()) {
|
2018-12-08 14:29:41 -08:00
|
|
|
Ok(exit_status) => {
|
|
|
|
if let Some(code) = exit_status.code() {
|
|
|
|
if code != 0 {
|
|
|
|
return Err(RuntimeError::Code {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
line_number: None,
|
|
|
|
code,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2019-11-07 10:55:15 -08:00
|
|
|
return Err(error_from_signal(self.name(), None, exit_status));
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
2018-12-08 14:29:41 -08:00
|
|
|
}
|
|
|
|
Err(io_error) => {
|
|
|
|
return Err(RuntimeError::Shebang {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
command: interpreter.to_string(),
|
|
|
|
argument: argument.map(String::from),
|
|
|
|
io_error,
|
2019-04-11 12:30:29 -07:00
|
|
|
});
|
2018-12-08 14:29:41 -08:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
};
|
|
|
|
} else {
|
2019-11-07 10:55:15 -08:00
|
|
|
let mut lines = self.body.iter().peekable();
|
|
|
|
let mut line_number = self.line_number() + 1;
|
2017-11-16 23:30:08 -08:00
|
|
|
loop {
|
|
|
|
if lines.peek().is_none() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let mut evaluated = String::new();
|
|
|
|
loop {
|
|
|
|
if lines.peek().is_none() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let line = lines.next().unwrap();
|
|
|
|
line_number += 1;
|
2019-11-07 10:55:15 -08:00
|
|
|
evaluated += &evaluator.evaluate_line(&line.fragments, &argument_map)?;
|
|
|
|
if line.is_continuation() {
|
2017-11-16 23:30:08 -08:00
|
|
|
evaluated.pop();
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let mut command = evaluated.as_str();
|
|
|
|
let quiet_command = command.starts_with('@');
|
|
|
|
if quiet_command {
|
|
|
|
command = &command[1..];
|
|
|
|
}
|
|
|
|
|
|
|
|
if command == "" {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.dry_run
|
|
|
|
|| config.verbosity.loquacious()
|
|
|
|
|| !((quiet_command ^ self.quiet) || config.quiet)
|
2018-08-31 00:04:06 -07:00
|
|
|
{
|
2019-10-07 02:06:45 -07:00
|
|
|
let color = if config.highlight {
|
|
|
|
config.color.command()
|
2017-11-16 23:30:08 -08:00
|
|
|
} else {
|
2019-10-07 02:06:45 -07:00
|
|
|
config.color
|
2017-11-16 23:30:08 -08:00
|
|
|
};
|
|
|
|
eprintln!("{}", color.stderr().paint(command));
|
|
|
|
}
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.dry_run {
|
2017-11-16 23:30:08 -08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
to be implicitly inherited by subprocesses, we now use
`Command::current_dir` to set it explicitly. This feels much better,
since we aren't dependent on the implicit state of the process's
current directory.
- Subcommand execution is much improved.
- Added a ton of tests for config parsing, config execution, working
dir, and search dir.
- Error messages are improved. Many more will be colored.
- The Config is now onwed, instead of borrowing from the arguments and
the `clap::ArgMatches` object. This is a huge ergonomic improvement,
especially in tests, and I don't think anyone will notice.
- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
matching git, which I think is what most people will expect.
- Added a cute `tmptree!{}` macro, for creating temporary directories
populated with directories and files for tests.
- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00
|
|
|
let mut cmd = Command::new(&config.shell);
|
|
|
|
|
|
|
|
cmd.current_dir(context.working_directory);
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
cmd.arg("-cu").arg(command);
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
if config.quiet {
|
2017-11-16 23:30:08 -08:00
|
|
|
cmd.stderr(Stdio::null());
|
|
|
|
cmd.stdout(Stdio::null());
|
|
|
|
}
|
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
cmd.export_environment_variables(&context.scope, dotenv)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2018-08-27 16:03:52 -07:00
|
|
|
match InterruptHandler::guard(|| cmd.status()) {
|
2018-12-08 14:29:41 -08:00
|
|
|
Ok(exit_status) => {
|
|
|
|
if let Some(code) = exit_status.code() {
|
|
|
|
if code != 0 {
|
|
|
|
return Err(RuntimeError::Code {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
line_number: Some(line_number),
|
|
|
|
code,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2019-11-07 10:55:15 -08:00
|
|
|
return Err(error_from_signal(
|
|
|
|
self.name(),
|
|
|
|
Some(line_number),
|
|
|
|
exit_status,
|
|
|
|
));
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
2018-12-08 14:29:41 -08:00
|
|
|
}
|
|
|
|
Err(io_error) => {
|
|
|
|
return Err(RuntimeError::IoError {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2018-12-08 14:29:41 -08:00
|
|
|
io_error,
|
2019-04-11 12:30:29 -07:00
|
|
|
});
|
2018-12-08 14:29:41 -08:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
impl<'src> Keyed<'src> for Recipe<'src> {
|
|
|
|
fn key(&self) -> &'src str {
|
|
|
|
self.name.lexeme()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-16 23:30:08 -08:00
|
|
|
impl<'a> Display for Recipe<'a> {
|
2019-04-11 15:23:14 -07:00
|
|
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
2017-11-16 23:30:08 -08:00
|
|
|
if let Some(doc) = self.doc {
|
|
|
|
writeln!(f, "# {}", doc)?;
|
|
|
|
}
|
2019-04-15 22:40:02 -07:00
|
|
|
|
|
|
|
if self.quiet {
|
|
|
|
write!(f, "@{}", self.name)?;
|
|
|
|
} else {
|
|
|
|
write!(f, "{}", self.name)?;
|
|
|
|
}
|
|
|
|
|
2017-11-16 23:30:08 -08:00
|
|
|
for parameter in &self.parameters {
|
|
|
|
write!(f, " {}", parameter)?;
|
|
|
|
}
|
|
|
|
write!(f, ":")?;
|
|
|
|
for dependency in &self.dependencies {
|
|
|
|
write!(f, " {}", dependency)?;
|
|
|
|
}
|
|
|
|
|
2019-11-07 10:55:15 -08:00
|
|
|
for (i, line) in self.body.iter().enumerate() {
|
2017-11-16 23:30:08 -08:00
|
|
|
if i == 0 {
|
2018-08-27 18:36:40 -07:00
|
|
|
writeln!(f)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
2019-11-07 10:55:15 -08:00
|
|
|
for (j, fragment) in line.fragments.iter().enumerate() {
|
2017-11-16 23:30:08 -08:00
|
|
|
if j == 0 {
|
|
|
|
write!(f, " ")?;
|
|
|
|
}
|
2019-11-07 10:55:15 -08:00
|
|
|
match fragment {
|
|
|
|
Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
|
|
|
|
Fragment::Interpolation { expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
2019-11-07 10:55:15 -08:00
|
|
|
if i + 1 < self.body.len() {
|
2018-08-27 18:36:40 -07:00
|
|
|
writeln!(f)?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|