2022-12-27 20:16:18 -08:00
|
|
|
use {
|
|
|
|
super::*,
|
|
|
|
std::process::{ExitStatus, Stdio},
|
|
|
|
};
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2021-07-26 01:26:06 -07:00
|
|
|
/// Return a `Error::Signal` if the process was terminated by a signal,
|
|
|
|
/// otherwise return an `Error::UnknownFailure`
|
|
|
|
fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: ExitStatus) -> Error {
|
2017-11-16 23:30:08 -08:00
|
|
|
match Platform::signal_from_exit_status(exit_status) {
|
2021-07-26 01:26:06 -07:00
|
|
|
Some(signal) => Error::Signal {
|
2018-12-08 14:29:41 -08:00
|
|
|
recipe,
|
|
|
|
line_number,
|
|
|
|
signal,
|
|
|
|
},
|
2021-07-26 01:26:06 -07:00
|
|
|
None => Error::Unknown {
|
2018-12-08 14:29:41 -08:00
|
|
|
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`
|
2021-11-17 00:07:48 -08:00
|
|
|
#[derive(PartialEq, Debug, Clone, Serialize)]
|
2019-12-07 03:09:21 -08:00
|
|
|
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
|
2022-10-31 00:52:03 -07:00
|
|
|
pub(crate) attributes: BTreeSet<Attribute>,
|
2021-09-16 06:44:40 -07:00
|
|
|
pub(crate) body: Vec<Line<'src>>,
|
2019-11-21 06:23:32 -08:00
|
|
|
pub(crate) dependencies: Vec<D>,
|
2021-09-16 06:44:40 -07:00
|
|
|
pub(crate) doc: Option<&'src str>,
|
|
|
|
pub(crate) name: Name<'src>,
|
|
|
|
pub(crate) parameters: Vec<Parameter<'src>>,
|
2021-11-17 00:07:48 -08:00
|
|
|
pub(crate) priors: usize,
|
2021-09-16 06:44:40 -07:00
|
|
|
pub(crate) private: bool,
|
|
|
|
pub(crate) quiet: bool,
|
|
|
|
pub(crate) shebang: bool,
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-12-07 03:09:21 -08:00
|
|
|
impl<'src, D> Recipe<'src, D> {
|
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()
|
2020-06-13 01:49:13 -07:00
|
|
|
.filter(|p| p.default.is_none() && p.kind != ParameterKind::Star)
|
2018-12-08 14:29:41 -08:00
|
|
|
.count()
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2019-09-21 15:35:03 -07:00
|
|
|
pub(crate) fn max_arguments(&self) -> usize {
|
2020-06-13 01:49:13 -07:00
|
|
|
if self.parameters.iter().any(|p| p.kind.is_variadic()) {
|
2020-01-15 02:16:13 -08:00
|
|
|
usize::max_value() - 1
|
2017-11-16 23:30:08 -08:00
|
|
|
} else {
|
|
|
|
self.parameters.len()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-07 03:09:21 -08:00
|
|
|
pub(crate) fn name(&self) -> &'src str {
|
2019-11-07 10:55:15 -08:00
|
|
|
self.name.lexeme()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn line_number(&self) -> usize {
|
|
|
|
self.name.line
|
|
|
|
}
|
|
|
|
|
2020-08-21 15:13:54 -07:00
|
|
|
pub(crate) fn public(&self) -> bool {
|
2022-11-22 16:25:57 -08:00
|
|
|
!self.private && !self.attributes.contains(&Attribute::Private)
|
2020-08-21 15:13:54 -07:00
|
|
|
}
|
|
|
|
|
2022-11-02 23:37:35 -07:00
|
|
|
pub(crate) fn change_directory(&self) -> bool {
|
|
|
|
!self.attributes.contains(&Attribute::NoCd)
|
|
|
|
}
|
|
|
|
|
2022-10-31 00:52:03 -07:00
|
|
|
pub(crate) fn enabled(&self) -> bool {
|
|
|
|
let windows = self.attributes.contains(&Attribute::Windows);
|
|
|
|
let linux = self.attributes.contains(&Attribute::Linux);
|
|
|
|
let macos = self.attributes.contains(&Attribute::Macos);
|
|
|
|
let unix = self.attributes.contains(&Attribute::Unix);
|
|
|
|
|
|
|
|
(!windows && !linux && !macos && !unix)
|
|
|
|
|| (cfg!(target_os = "windows") && windows)
|
|
|
|
|| (cfg!(target_os = "linux") && (linux || unix))
|
|
|
|
|| (cfg!(target_os = "macos") && (macos || unix))
|
|
|
|
|| (cfg!(windows) && windows)
|
|
|
|
|| (cfg!(unix) && unix)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn print_exit_message(&self) -> bool {
|
|
|
|
!self.attributes.contains(&Attribute::NoExitMessage)
|
|
|
|
}
|
|
|
|
|
2019-12-07 03:09:21 -08:00
|
|
|
pub(crate) fn run<'run>(
|
2017-11-16 23:30:08 -08:00
|
|
|
&self,
|
2019-12-07 03:09:21 -08:00
|
|
|
context: &RecipeContext<'src, 'run>,
|
2019-04-11 15:23:14 -07:00
|
|
|
dotenv: &BTreeMap<String, String>,
|
2019-12-07 04:03:03 -08:00
|
|
|
scope: Scope<'src, 'run>,
|
2019-12-25 06:12:06 -08:00
|
|
|
search: &'run Search,
|
2021-05-02 03:25:43 -07:00
|
|
|
positional: &[String],
|
2019-12-07 03:09:21 -08:00
|
|
|
) -> RunResult<'src, ()> {
|
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
|
|
|
}
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
let evaluator =
|
2019-12-25 06:12:06 -08:00
|
|
|
Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
|
2017-11-16 23:30:08 -08:00
|
|
|
|
|
|
|
if self.shebang {
|
2022-07-20 18:46:52 -07:00
|
|
|
self.run_shebang(context, dotenv, &scope, positional, config, evaluator)
|
|
|
|
} else {
|
|
|
|
self.run_linewise(context, dotenv, &scope, positional, config, evaluator)
|
|
|
|
}
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-10-04 17:32:30 -07:00
|
|
|
fn run_linewise<'run>(
|
2022-07-20 18:46:52 -07:00
|
|
|
&self,
|
|
|
|
context: &RecipeContext<'src, 'run>,
|
|
|
|
dotenv: &BTreeMap<String, String>,
|
|
|
|
scope: &Scope<'src, 'run>,
|
|
|
|
positional: &[String],
|
|
|
|
config: &Config,
|
|
|
|
mut evaluator: Evaluator<'src, 'run>,
|
|
|
|
) -> RunResult<'src, ()> {
|
|
|
|
let mut lines = self.body.iter().peekable();
|
|
|
|
let mut line_number = self.line_number() + 1;
|
|
|
|
loop {
|
|
|
|
if lines.peek().is_none() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
let mut evaluated = String::new();
|
|
|
|
let mut continued = false;
|
|
|
|
let quiet_command = lines.peek().map_or(false, |line| line.is_quiet());
|
|
|
|
let infallible_command = lines.peek().map_or(false, |line| line.is_infallible());
|
2022-10-04 17:32:30 -07:00
|
|
|
|
|
|
|
let comment_line =
|
|
|
|
context.settings.ignore_comments && lines.peek().map_or(false, |line| line.is_comment());
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
loop {
|
|
|
|
if lines.peek().is_none() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let line = lines.next().unwrap();
|
|
|
|
line_number += 1;
|
2022-10-04 22:33:19 -07:00
|
|
|
if !comment_line {
|
|
|
|
evaluated += &evaluator.evaluate_line(line, continued)?;
|
|
|
|
}
|
2022-10-04 17:32:30 -07:00
|
|
|
if line.is_continuation() && !comment_line {
|
2022-07-20 18:46:52 -07:00
|
|
|
continued = true;
|
|
|
|
evaluated.pop();
|
|
|
|
} else {
|
|
|
|
break;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
2022-10-04 22:33:19 -07:00
|
|
|
|
|
|
|
if comment_line {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
let mut command = evaluated.as_str();
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if quiet_command {
|
|
|
|
command = &command[1..];
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if infallible_command {
|
|
|
|
command = &command[1..];
|
|
|
|
}
|
2021-05-17 20:44:12 -07:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if command.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
2021-05-17 20:44:12 -07:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.dry_run
|
|
|
|
|| config.verbosity.loquacious()
|
|
|
|
|| !((quiet_command ^ self.quiet) || config.verbosity.quiet())
|
2017-11-16 23:30:08 -08:00
|
|
|
{
|
2022-07-20 18:46:52 -07:00
|
|
|
let color = if config.highlight {
|
|
|
|
config.color.command()
|
2021-05-17 20:44:12 -07:00
|
|
|
} else {
|
2022-07-20 18:46:52 -07:00
|
|
|
config.color
|
|
|
|
};
|
|
|
|
eprintln!("{}", color.stderr().paint(command));
|
|
|
|
}
|
2018-08-31 00:04:06 -07:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.dry_run {
|
|
|
|
continue;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
let mut cmd = context.settings.shell_command(config);
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-11-02 23:37:35 -07:00
|
|
|
if self.change_directory() {
|
|
|
|
cmd.current_dir(&context.search.working_directory);
|
|
|
|
}
|
2022-07-20 18:46:52 -07:00
|
|
|
|
|
|
|
cmd.arg(command);
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2021-04-24 18:29:58 -07:00
|
|
|
if context.settings.positional_arguments {
|
2022-07-20 18:46:52 -07:00
|
|
|
cmd.arg(self.name.lexeme());
|
|
|
|
cmd.args(positional);
|
2021-04-24 18:29:58 -07:00
|
|
|
}
|
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.verbosity.quiet() {
|
|
|
|
cmd.stderr(Stdio::null());
|
|
|
|
cmd.stdout(Stdio::null());
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd.export(context.settings, dotenv, scope);
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
match InterruptHandler::guard(|| cmd.status()) {
|
2021-09-16 06:44:40 -07:00
|
|
|
Ok(exit_status) => {
|
2018-12-08 14:29:41 -08:00
|
|
|
if let Some(code) = exit_status.code() {
|
2022-07-20 18:46:52 -07:00
|
|
|
if code != 0 && !infallible_command {
|
2021-07-26 01:26:06 -07:00
|
|
|
return Err(Error::Code {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2022-07-20 18:46:52 -07:00
|
|
|
line_number: Some(line_number),
|
2018-12-08 14:29:41 -08:00
|
|
|
code,
|
2022-10-31 00:52:03 -07:00
|
|
|
print_message: self.print_exit_message(),
|
2018-12-08 14:29:41 -08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2022-07-20 18:46:52 -07:00
|
|
|
return Err(error_from_signal(
|
|
|
|
self.name(),
|
|
|
|
Some(line_number),
|
|
|
|
exit_status,
|
|
|
|
));
|
2021-09-16 06:44:40 -07:00
|
|
|
}
|
|
|
|
}
|
2018-12-08 14:29:41 -08:00
|
|
|
Err(io_error) => {
|
2022-07-20 18:46:52 -07:00
|
|
|
return Err(Error::Io {
|
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
|
|
|
});
|
2021-09-16 06:44:40 -07:00
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
};
|
2022-07-20 18:46:52 -07:00
|
|
|
}
|
|
|
|
}
|
2022-01-02 16:51:22 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
pub(crate) fn run_shebang<'run>(
|
|
|
|
&self,
|
|
|
|
context: &RecipeContext<'src, 'run>,
|
|
|
|
dotenv: &BTreeMap<String, String>,
|
|
|
|
scope: &Scope<'src, 'run>,
|
|
|
|
positional: &[String],
|
|
|
|
config: &Config,
|
|
|
|
mut evaluator: Evaluator<'src, 'run>,
|
|
|
|
) -> RunResult<'src, ()> {
|
|
|
|
let mut evaluated_lines = vec![];
|
|
|
|
for line in &self.body {
|
|
|
|
evaluated_lines.push(evaluator.evaluate_line(line, false)?);
|
|
|
|
}
|
2022-01-02 16:51:22 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.verbosity.loud() && (config.dry_run || self.quiet) {
|
|
|
|
for line in &evaluated_lines {
|
2022-12-15 16:53:21 -08:00
|
|
|
eprintln!("{line}");
|
2022-07-20 18:46:52 -07:00
|
|
|
}
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.dry_run {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
let shebang_line = evaluated_lines.first().ok_or_else(|| Error::Internal {
|
|
|
|
message: "evaluated_lines was empty".to_owned(),
|
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
let shebang = Shebang::new(shebang_line).ok_or_else(|| Error::Internal {
|
2022-12-15 16:53:21 -08:00
|
|
|
message: format!("bad shebang line: {shebang_line}"),
|
2022-07-20 18:46:52 -07:00
|
|
|
})?;
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-10-25 16:57:20 -07:00
|
|
|
let mut tempdir_builder = tempfile::Builder::new();
|
|
|
|
tempdir_builder.prefix("just");
|
|
|
|
let tempdir = match &context.settings.tempdir {
|
|
|
|
Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)),
|
|
|
|
None => tempdir_builder.tempdir(),
|
|
|
|
}
|
|
|
|
.map_err(|error| Error::TmpdirIo {
|
|
|
|
recipe: self.name(),
|
|
|
|
io_error: error,
|
|
|
|
})?;
|
|
|
|
let mut path = tempdir.path().to_path_buf();
|
2022-07-20 18:46:52 -07:00
|
|
|
path.push(shebang.script_filename(self.name()));
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
{
|
|
|
|
let mut f = fs::File::create(&path).map_err(|error| Error::TmpdirIo {
|
|
|
|
recipe: self.name(),
|
|
|
|
io_error: error,
|
|
|
|
})?;
|
|
|
|
let mut text = String::new();
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if shebang.include_shebang_line() {
|
|
|
|
text += &evaluated_lines[0];
|
|
|
|
} else {
|
|
|
|
text += "\n";
|
|
|
|
}
|
2021-04-24 18:29:58 -07:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
text += "\n";
|
|
|
|
// add blank lines so that lines in the generated script have the same line
|
|
|
|
// number as the corresponding lines in the justfile
|
|
|
|
for _ in 1..(self.line_number() + 2) {
|
|
|
|
text += "\n";
|
|
|
|
}
|
|
|
|
for line in &evaluated_lines[1..] {
|
|
|
|
text += line;
|
|
|
|
text += "\n";
|
|
|
|
}
|
2017-11-16 23:30:08 -08:00
|
|
|
|
2022-07-20 18:46:52 -07:00
|
|
|
if config.verbosity.grandiloquent() {
|
|
|
|
eprintln!("{}", config.color.doc().stderr().paint(&text));
|
|
|
|
}
|
|
|
|
|
|
|
|
f.write_all(text.as_bytes())
|
|
|
|
.map_err(|error| Error::TmpdirIo {
|
|
|
|
recipe: self.name(),
|
|
|
|
io_error: error,
|
|
|
|
})?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// make the script executable
|
|
|
|
Platform::set_execute_permission(&path).map_err(|error| Error::TmpdirIo {
|
|
|
|
recipe: self.name(),
|
|
|
|
io_error: error,
|
|
|
|
})?;
|
|
|
|
|
|
|
|
// create a command to run the script
|
2022-11-02 23:37:35 -07:00
|
|
|
let mut command = Platform::make_shebang_command(
|
|
|
|
&path,
|
|
|
|
if self.change_directory() {
|
|
|
|
Some(&context.search.working_directory)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
shebang,
|
|
|
|
)
|
|
|
|
.map_err(|output_error| Error::Cygpath {
|
|
|
|
recipe: self.name(),
|
|
|
|
output_error,
|
|
|
|
})?;
|
2022-07-20 18:46:52 -07:00
|
|
|
|
|
|
|
if context.settings.positional_arguments {
|
|
|
|
command.args(positional);
|
|
|
|
}
|
|
|
|
|
|
|
|
command.export(context.settings, dotenv, scope);
|
|
|
|
|
|
|
|
// run it!
|
|
|
|
match InterruptHandler::guard(|| command.status()) {
|
|
|
|
Ok(exit_status) => exit_status.code().map_or_else(
|
|
|
|
|| Err(error_from_signal(self.name(), None, exit_status)),
|
|
|
|
|code| {
|
|
|
|
if code == 0 {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err(Error::Code {
|
2019-11-07 10:55:15 -08:00
|
|
|
recipe: self.name(),
|
2022-07-20 18:46:52 -07:00
|
|
|
line_number: None,
|
|
|
|
code,
|
2022-10-31 00:52:03 -07:00
|
|
|
print_message: self.print_exit_message(),
|
2022-07-20 18:46:52 -07:00
|
|
|
})
|
2021-09-16 06:44:40 -07:00
|
|
|
}
|
2022-07-20 18:46:52 -07:00
|
|
|
},
|
|
|
|
),
|
|
|
|
Err(io_error) => Err(Error::Shebang {
|
|
|
|
recipe: self.name(),
|
|
|
|
command: shebang.interpreter.to_owned(),
|
|
|
|
argument: shebang.argument.map(String::from),
|
|
|
|
io_error,
|
|
|
|
}),
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-28 18:06:57 -07:00
|
|
|
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
|
|
|
fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
|
2017-11-16 23:30:08 -08:00
|
|
|
if let Some(doc) = self.doc {
|
2022-12-15 16:53:21 -08:00
|
|
|
writeln!(f, "# {doc}")?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
2019-04-15 22:40:02 -07:00
|
|
|
|
2022-10-31 00:52:03 -07:00
|
|
|
for attribute in &self.attributes {
|
|
|
|
writeln!(f, "[{}]", attribute.to_str())?;
|
|
|
|
}
|
|
|
|
|
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 {
|
2021-07-28 18:06:57 -07:00
|
|
|
write!(f, " {}", parameter.color_display(color))?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
write!(f, ":")?;
|
2021-07-22 00:20:25 -07:00
|
|
|
|
|
|
|
for (i, dependency) in self.dependencies.iter().enumerate() {
|
|
|
|
if i == self.priors {
|
|
|
|
write!(f, " &&")?;
|
|
|
|
}
|
|
|
|
|
2022-12-15 16:53:21 -08:00
|
|
|
write!(f, " {dependency}")?;
|
2017-11-16 23:30:08 -08:00
|
|
|
}
|
|
|
|
|
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())?,
|
2022-12-15 16:53:21 -08:00
|
|
|
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(())
|
|
|
|
}
|
|
|
|
}
|
2021-11-17 00:07:48 -08:00
|
|
|
|
|
|
|
impl<'src, D> Keyed<'src> for Recipe<'src, D> {
|
|
|
|
fn key(&self) -> &'src str {
|
|
|
|
self.name.lexeme()
|
|
|
|
}
|
|
|
|
}
|