1use super::*;
2
3fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: ExitStatus) -> Error {
6 match Platform::signal_from_exit_status(exit_status) {
7 Some(signal) => Error::Signal {
8 recipe,
9 line_number,
10 signal,
11 },
12 None => Error::Unknown {
13 recipe,
14 line_number,
15 },
16 }
17}
18
19#[derive(PartialEq, Debug, Clone, Serialize)]
21pub struct Recipe<'src, D = Dependency<'src>> {
22 pub attributes: BTreeSet<Attribute<'src>>,
23 pub body: Vec<Line<'src>>,
24 pub dependencies: Vec<D>,
25 pub doc: Option<String>,
26 #[serde(skip)]
27 pub file_depth: u32,
28 #[serde(skip)]
29 pub import_offsets: Vec<usize>,
30 pub name: Name<'src>,
31 pub namepath: Namepath<'src>,
32 pub parameters: Vec<Parameter<'src>>,
33 pub priors: usize,
34 pub private: bool,
35 pub quiet: bool,
36 pub shebang: bool,
37}
38
39impl<'src, D> Recipe<'src, D> {
40 pub fn argument_range(&self) -> RangeInclusive<usize> {
41 self.min_arguments()..=self.max_arguments()
42 }
43
44 pub fn min_arguments(&self) -> usize {
45 self
46 .parameters
47 .iter()
48 .filter(|p| p.default.is_none() && p.kind != ParameterKind::Star)
49 .count()
50 }
51
52 pub fn max_arguments(&self) -> usize {
53 if self.parameters.iter().any(|p| p.kind.is_variadic()) {
54 usize::MAX - 1
55 } else {
56 self.parameters.len()
57 }
58 }
59
60 pub fn name(&self) -> &'src str {
61 self.name.lexeme()
62 }
63
64 pub fn line_number(&self) -> usize {
65 self.name.line
66 }
67
68 pub fn confirm(&self) -> RunResult<'src, bool> {
69 for attribute in &self.attributes {
70 if let Attribute::Confirm(prompt) = attribute {
71 if let Some(prompt) = prompt {
72 eprint!("{} ", prompt.cooked);
73 } else {
74 eprint!("Run recipe `{}`? ", self.name);
75 }
76 let mut line = String::new();
77 std::io::stdin()
78 .read_line(&mut line)
79 .map_err(|io_error| Error::GetConfirmation { io_error })?;
80 let line = line.trim().to_lowercase();
81 return Ok(line == "y" || line == "yes");
82 }
83 }
84 Ok(true)
85 }
86
87 pub fn check_can_be_default_recipe(&self) -> RunResult<'src, ()> {
88 let min_arguments = self.min_arguments();
89 if min_arguments > 0 {
90 return Err(Error::DefaultRecipeRequiresArguments {
91 recipe: self.name.lexeme(),
92 min_arguments,
93 });
94 }
95
96 Ok(())
97 }
98
99 pub fn is_public(&self) -> bool {
100 !self.private && !self.attributes.contains(&Attribute::Private)
101 }
102
103 pub fn is_script(&self) -> bool {
104 self.shebang
105 }
106
107 pub fn takes_positional_arguments(&self, settings: &Settings) -> bool {
108 settings.positional_arguments || self.attributes.contains(&Attribute::PositionalArguments)
109 }
110
111 pub fn change_directory(&self) -> bool {
112 !self.attributes.contains(&Attribute::NoCd)
113 }
114
115 pub fn enabled(&self) -> bool {
116 let windows = self.attributes.contains(&Attribute::Windows);
117 let linux = self.attributes.contains(&Attribute::Linux);
118 let macos = self.attributes.contains(&Attribute::Macos);
119 let unix = self.attributes.contains(&Attribute::Unix);
120
121 (!windows && !linux && !macos && !unix)
122 || (cfg!(target_os = "windows") && windows)
123 || (cfg!(target_os = "linux") && (linux || unix))
124 || (cfg!(target_os = "macos") && (macos || unix))
125 || (cfg!(windows) && windows)
126 || (cfg!(unix) && unix)
127 }
128
129 fn print_exit_message(&self) -> bool {
130 !self.attributes.contains(&Attribute::NoExitMessage)
131 }
132
133 fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {
134 if self.change_directory() {
135 Some(context.working_directory())
136 } else {
137 None
138 }
139 }
140
141 fn no_quiet(&self) -> bool {
142 self.attributes.contains(&Attribute::NoQuiet)
143 }
144
145 pub fn run<'run>(
146 &self,
147 context: &ExecutionContext<'src, 'run>,
148 scope: &Scope<'src, 'run>,
149 positional: &[String],
150 is_dependency: bool,
151 ) -> RunResult<'src, ()> {
152 let config = &context.config;
153
154 let color = config.color.stderr().banner();
155 let prefix = color.prefix();
156 let suffix = color.suffix();
157
158 if config.verbosity.loquacious() {
159 eprintln!("{prefix}===> Running recipe `{}`...{suffix}", self.name);
160 }
161
162 if config.explain {
163 if let Some(doc) = self.doc() {
164 eprintln!("{prefix}#### {doc}{suffix}");
165 }
166 }
167
168 let evaluator = Evaluator::new(context, is_dependency, scope);
169
170 if self.is_script() {
171 self.run_script(context, scope, positional, config, evaluator)
172 } else {
173 self.run_linewise(context, scope, positional, config, evaluator)
174 }
175 }
176
177 fn run_linewise<'run>(
178 &self,
179 context: &ExecutionContext<'src, 'run>,
180 scope: &Scope<'src, 'run>,
181 positional: &[String],
182 config: &Config,
183 mut evaluator: Evaluator<'src, 'run>,
184 ) -> RunResult<'src, ()> {
185 let mut lines = self.body.iter().peekable();
186 let mut line_number = self.line_number() + 1;
187 loop {
188 if lines.peek().is_none() {
189 return Ok(());
190 }
191 let mut evaluated = String::new();
192 let mut continued = false;
193 let quiet_line = lines.peek().map_or(false, |line| line.is_quiet());
194 let infallible_line = lines.peek().map_or(false, |line| line.is_infallible());
195
196 let comment_line = context.module.settings.ignore_comments
197 && lines.peek().map_or(false, |line| line.is_comment());
198
199 loop {
200 if lines.peek().is_none() {
201 break;
202 }
203 let line = lines.next().unwrap();
204 line_number += 1;
205 if !comment_line {
206 evaluated += &evaluator.evaluate_line(line, continued)?;
207 }
208 if line.is_continuation() && !comment_line {
209 continued = true;
210 evaluated.pop();
211 } else {
212 break;
213 }
214 }
215
216 if comment_line {
217 continue;
218 }
219
220 let mut command = evaluated.as_str();
221
222 let sigils = usize::from(infallible_line) + usize::from(quiet_line);
223
224 command = &command[sigils..];
225
226 if command.is_empty() {
227 continue;
228 }
229
230 if config.dry_run
231 || config.verbosity.loquacious()
232 || !((quiet_line ^ self.quiet)
233 || (context.module.settings.quiet && !self.no_quiet())
234 || config.verbosity.quiet())
235 {
236 let color = config
237 .highlight
238 .then(|| config.color.command(config.command_color))
239 .unwrap_or(config.color)
240 .stderr();
241
242 if config.timestamp {
243 eprint!(
244 "[{}] ",
245 color.paint(
246 &chrono::Local::now()
247 .format(&config.timestamp_format)
248 .to_string()
249 ),
250 );
251 }
252
253 eprintln!("{}", color.paint(command));
254 }
255
256 if config.dry_run {
257 continue;
258 }
259
260 let mut cmd = context.module.settings.shell_command(config);
261
262 if let Some(working_directory) = self.working_directory(context) {
263 cmd.current_dir(working_directory);
264 }
265
266 cmd.arg(command);
267
268 if self.takes_positional_arguments(&context.module.settings) {
269 cmd.arg(self.name.lexeme());
270 cmd.args(positional);
271 }
272
273 if config.verbosity.quiet() {
274 cmd.stderr(Stdio::null());
275 cmd.stdout(Stdio::null());
276 }
277
278 cmd.export(
279 &context.module.settings,
280 context.dotenv,
281 scope,
282 &context.module.unexports,
283 );
284
285 match InterruptHandler::guard(|| cmd.status()) {
286 Ok(exit_status) => {
287 if let Some(code) = exit_status.code() {
288 if code != 0 && !infallible_line {
289 return Err(Error::Code {
290 recipe: self.name(),
291 line_number: Some(line_number),
292 code,
293 print_message: self.print_exit_message(),
294 });
295 }
296 } else {
297 return Err(error_from_signal(
298 self.name(),
299 Some(line_number),
300 exit_status,
301 ));
302 }
303 }
304 Err(io_error) => {
305 return Err(Error::Io {
306 recipe: self.name(),
307 io_error,
308 });
309 }
310 };
311 }
312 }
313
314 pub fn run_script<'run>(
315 &self,
316 context: &ExecutionContext<'src, 'run>,
317 scope: &Scope<'src, 'run>,
318 positional: &[String],
319 config: &Config,
320 mut evaluator: Evaluator<'src, 'run>,
321 ) -> RunResult<'src, ()> {
322 let mut evaluated_lines = Vec::new();
323 for line in &self.body {
324 evaluated_lines.push(evaluator.evaluate_line(line, false)?);
325 }
326
327 if config.verbosity.loud() && (config.dry_run || self.quiet) {
328 for line in &evaluated_lines {
329 eprintln!(
330 "{}",
331 config
332 .color
333 .command(config.command_color)
334 .stderr()
335 .paint(line)
336 );
337 }
338 }
339
340 if config.dry_run {
341 return Ok(());
342 }
343
344 let executor = if let Some(Attribute::Script(interpreter)) = self
345 .attributes
346 .iter()
347 .find(|attribute| matches!(attribute, Attribute::Script(_)))
348 {
349 Executor::Command(
350 interpreter
351 .as_ref()
352 .or(context.module.settings.script_interpreter.as_ref())
353 .unwrap_or_else(|| Interpreter::default_script_interpreter()),
354 )
355 } else {
356 let line = evaluated_lines
357 .first()
358 .ok_or_else(|| Error::internal("evaluated_lines was empty"))?;
359
360 let shebang =
361 Shebang::new(line).ok_or_else(|| Error::internal(format!("bad shebang line: {line}")))?;
362
363 Executor::Shebang(shebang)
364 };
365
366 let mut tempdir_builder = tempfile::Builder::new();
367 tempdir_builder.prefix("just-");
368 let tempdir = match &context.module.settings.tempdir {
369 Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)),
370 None => {
371 if let Some(runtime_dir) = dirs::runtime_dir() {
372 let path = runtime_dir.join("just");
373 fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {
374 io_error,
375 path: path.clone(),
376 })?;
377 tempdir_builder.tempdir_in(path)
378 } else {
379 tempdir_builder.tempdir()
380 }
381 }
382 }
383 .map_err(|error| Error::TempdirIo {
384 recipe: self.name(),
385 io_error: error,
386 })?;
387 let mut path = tempdir.path().to_path_buf();
388
389 let extension = self.attributes.iter().find_map(|attribute| {
390 if let Attribute::Extension(extension) = attribute {
391 Some(extension.cooked.as_str())
392 } else {
393 None
394 }
395 });
396
397 path.push(executor.script_filename(self.name(), extension));
398
399 let script = executor.script(self, &evaluated_lines);
400
401 if config.verbosity.grandiloquent() {
402 eprintln!("{}", config.color.doc().stderr().paint(&script));
403 }
404
405 fs::write(&path, script).map_err(|error| Error::TempdirIo {
406 recipe: self.name(),
407 io_error: error,
408 })?;
409
410 let mut command = executor.command(
411 &path,
412 self.name(),
413 self.working_directory(context).as_deref(),
414 )?;
415
416 if self.takes_positional_arguments(&context.module.settings) {
417 command.args(positional);
418 }
419
420 command.export(
421 &context.module.settings,
422 context.dotenv,
423 scope,
424 &context.module.unexports,
425 );
426
427 match InterruptHandler::guard(|| command.status()) {
429 Ok(exit_status) => exit_status.code().map_or_else(
430 || Err(error_from_signal(self.name(), None, exit_status)),
431 |code| {
432 if code == 0 {
433 Ok(())
434 } else {
435 Err(Error::Code {
436 recipe: self.name(),
437 line_number: None,
438 code,
439 print_message: self.print_exit_message(),
440 })
441 }
442 },
443 ),
444 Err(io_error) => Err(executor.error(io_error, self.name())),
445 }
446 }
447
448 pub fn groups(&self) -> BTreeSet<String> {
449 self
450 .attributes
451 .iter()
452 .filter_map(|attribute| {
453 if let Attribute::Group(group) = attribute {
454 Some(group.cooked.clone())
455 } else {
456 None
457 }
458 })
459 .collect()
460 }
461
462 pub fn doc(&self) -> Option<&str> {
463 for attribute in &self.attributes {
464 if let Attribute::Doc(doc) = attribute {
465 return doc.as_ref().map(|s| s.cooked.as_ref());
466 }
467 }
468
469 self.doc.as_deref()
470 }
471
472 pub fn subsequents(&self) -> impl Iterator<Item = &D> {
473 self.dependencies.iter().skip(self.priors)
474 }
475}
476
477impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
478 fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
479 if !self
480 .attributes
481 .iter()
482 .any(|attribute| matches!(attribute, Attribute::Doc(_)))
483 {
484 if let Some(doc) = &self.doc {
485 writeln!(f, "# {doc}")?;
486 }
487 }
488
489 for attribute in &self.attributes {
490 writeln!(f, "[{attribute}]")?;
491 }
492
493 if self.quiet {
494 write!(f, "@{}", self.name)?;
495 } else {
496 write!(f, "{}", self.name)?;
497 }
498
499 for parameter in &self.parameters {
500 write!(f, " {}", parameter.color_display(color))?;
501 }
502 write!(f, ":")?;
503
504 for (i, dependency) in self.dependencies.iter().enumerate() {
505 if i == self.priors {
506 write!(f, " &&")?;
507 }
508
509 write!(f, " {dependency}")?;
510 }
511
512 for (i, line) in self.body.iter().enumerate() {
513 if i == 0 {
514 writeln!(f)?;
515 }
516 for (j, fragment) in line.fragments.iter().enumerate() {
517 if j == 0 {
518 write!(f, " ")?;
519 }
520 match fragment {
521 Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
522 Fragment::Interpolation { expression, .. } => write!(f, "{{{{ {expression} }}}}")?,
523 }
524 }
525 if i + 1 < self.body.len() {
526 writeln!(f)?;
527 }
528 }
529 Ok(())
530 }
531}
532
533impl<'src, D> Keyed<'src> for Recipe<'src, D> {
534 fn key(&self) -> &'src str {
535 self.name.lexeme()
536 }
537}