1use std::{
2 collections::{HashMap, VecDeque},
3 path::Path,
4 sync::{Arc, atomic::AtomicBool},
5 thread::ScopedJoinHandle,
6 time::{Duration, Instant},
7};
8
9use grok::Grok;
10use keepcalm::SharedMut;
11use serde::{Serialize, ser::SerializeMap};
12use termcolor::{Color, ColorChoice, WriteColor};
13
14use crate::{
15 command::{CommandLine, CommandResult},
16 failure::{OutputPatternMatchFailure, format_match_trace_tree},
17 util::{NicePathBuf, NiceTempDir},
18};
19use crate::{cwrite, cwriteln, cwriteln_rule};
20use crate::{output::*, util::ShellBit};
21
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
23
24#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
25pub struct ScriptLocation {
26 pub file: ScriptFile,
27 pub line: usize,
28}
29
30impl ScriptLocation {
31 pub fn new(file: ScriptFile, line: usize) -> Self {
32 Self { file, line }
33 }
34}
35
36impl std::fmt::Display for ScriptLocation {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}:{}", self.file, self.line)
39 }
40}
41
42#[derive(
43 derive_more::Debug, derive_more::Display, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash,
44)]
45#[display("{}", file)]
46pub struct ScriptFile {
47 pub base_line: usize,
48 pub file: Arc<NicePathBuf>,
49}
50
51impl ScriptFile {
52 pub fn new(file: impl AsRef<Path>) -> Self {
53 Self {
54 base_line: 0,
55 file: Arc::new(NicePathBuf::new(file)),
56 }
57 }
58 pub fn new_with_line(file: impl AsRef<Path>, line: usize) -> Self {
59 Self {
60 base_line: line,
61 file: Arc::new(NicePathBuf::new(file)),
62 }
63 }
64}
65
66impl<T: AsRef<Path>> From<T> for ScriptFile {
67 fn from(file: T) -> Self {
68 Self::new(file)
69 }
70}
71
72#[derive(Clone, derive_more::Debug, Serialize)]
73pub struct Script {
74 pub commands: Arc<Vec<ScriptBlock>>,
75 pub includes: Arc<HashMap<String, Script>>,
76 pub file: ScriptFile,
77}
78
79#[derive(Debug, Clone, Default)]
80pub struct ScriptRunArgs {
81 pub delay_steps: Option<u64>,
82 pub ignore_exit_codes: bool,
83 pub ignore_matches: bool,
84 pub simplified_output: bool,
85 pub show_line_numbers: bool,
86 pub runner: Option<String>,
87 pub quiet: bool,
88 pub verbose: bool,
89 pub global_timeout: Option<Duration>,
90 pub no_color: bool,
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct ScriptEnv {
95 env_vars: HashMap<String, String>,
96}
97
98impl ScriptEnv {
99 pub fn set_defaults(&mut self, pwd: impl AsRef<Path>) {
100 macro_rules! target {
101 ($env:ident, $var:ident, [$($vals:expr),*]) => {
102 $(
103 if cfg!($var = $vals) {
104 self.env_vars.insert(stringify!($env).to_string(), $vals.to_string());
105 }
106 )*
107 };
108 }
109
110 target!(
111 TARGET_OS,
112 target_os,
113 [
114 "windows",
115 "linux",
116 "macos",
117 "ios",
118 "android",
119 "freebsd",
120 "netbsd",
121 "openbsd",
122 "dragonfly",
123 "haiku",
124 "aix"
125 ]
126 );
127 target!(TARGET_FAMILY, target_family, ["windows", "unix", "wasm"]);
128 target!(
129 TARGET_ARCH,
130 target_arch,
131 [
132 "aarch64",
133 "amdgpu",
134 "arm",
135 "arm64ec",
136 "avr",
137 "bpf",
138 "csky",
139 "hexagon",
140 "loongarch32",
141 "loongarch64",
142 "m68k",
143 "mips",
144 "mips32r6",
145 "mips64",
146 "mips64r6",
147 "msp430",
148 "nvptx64",
149 "powerpc",
150 "powerpc64",
151 "riscv32",
152 "riscv64",
153 "s390x",
154 "sparc",
155 "sparc64",
156 "wasm32",
157 "wasm64",
158 "x86",
159 "x86_64",
160 "xtensa"
161 ]
162 );
163
164 let pwd = if pwd.as_ref().as_os_str().is_empty() {
166 Path::new(".")
167 } else {
168 pwd.as_ref()
169 };
170
171 let pwd = NicePathBuf::from(pwd).env_string();
173 self.env_vars.insert("PWD".to_string(), pwd);
174 self.env_vars
176 .insert("INITIAL_PWD".to_string(), self.env_vars["PWD"].clone());
177 }
178
179 pub fn pwd(&self) -> NicePathBuf {
180 self.env_vars
181 .get("PWD")
182 .cloned()
183 .map(NicePathBuf::from)
184 .unwrap_or_else(NicePathBuf::cwd)
185 }
186
187 pub fn get_env(&self, name: &str) -> Option<&str> {
188 self.env_vars.get(name).map(|s| s.as_str())
189 }
190
191 pub fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) {
192 let name = name.into();
193 if name == "PWD" {
194 self.set_pwd(value.into());
195 } else {
196 self.env_vars.insert(name, value.into());
197 }
198 }
199
200 pub fn set_pwd(&mut self, pwd: impl Into<NicePathBuf>) {
201 let pwd = pwd.into().env_string();
202 self.env_vars.insert("PWD".to_string(), pwd);
203 }
204
205 pub fn expand(&self, value: &ShellBit) -> Result<String, ScriptRunError> {
206 match value {
207 ShellBit::Literal(s) => Ok(s.clone()),
208 ShellBit::Quoted(s) => self.expand_str(s),
209 }
210 }
211
212 pub fn expand_str(&self, value: impl AsRef<str>) -> Result<String, ScriptRunError> {
214 enum State {
215 Normal,
216 EscapeNext,
217 InCurly,
218 Dollar,
219 InDollar,
220 }
221
222 let value = value.as_ref();
223
224 let mut state = State::Normal;
229 let mut variable = String::new();
230 let mut expanded = String::new();
231
232 for c in value.chars() {
233 match state {
234 State::Normal => {
235 if c == '$' {
236 state = State::Dollar;
237 continue;
238 }
239 if c == '\\' {
240 state = State::EscapeNext;
241 continue;
242 }
243 expanded.push(c);
244 }
245 State::EscapeNext => {
246 expanded.push(c);
247 state = State::Normal;
248 }
249 State::InCurly => {
250 if c == '}' {
251 if let Some(value) = self.get_env(&std::mem::take(&mut variable)) {
252 expanded.push_str(value);
253 } else {
254 return Err(ScriptRunError::ExpansionError(format!(
255 "undefined variable in ${{...}}: {variable:?} (in {value:?})"
256 )));
257 }
258 state = State::Normal;
259 } else {
260 variable.push(c);
261 }
262 }
263 State::Dollar => {
264 if c.is_alphanumeric() || c == '_' {
265 state = State::InDollar;
266 variable.push(c);
267 } else if c == '{' {
268 state = State::InCurly;
269 } else {
270 return Err(ScriptRunError::ExpansionError(format!(
271 "invalid variable: {c:?} (in {value:?})"
272 )));
273 }
274 }
275 State::InDollar => {
276 if c.is_alphanumeric() || c == '_' {
277 variable.push(c);
278 } else {
279 if let Some(value) = self.get_env(&std::mem::take(&mut variable)) {
280 expanded.push_str(value);
281 } else {
282 return Err(ScriptRunError::ExpansionError(format!(
283 "undefined variable in $...: {variable:?} (in {value:?})"
284 )));
285 }
286 expanded.push(c);
287 state = State::Normal;
288 }
289 }
290 }
291 }
292 match state {
293 State::InDollar => {
294 if let Some(value) = self.get_env(&variable) {
295 expanded.push_str(value);
296 } else {
297 return Err(ScriptRunError::ExpansionError(format!(
298 "undefined variable: {variable}"
299 )));
300 }
301 }
302 State::Dollar => {
303 return Err(ScriptRunError::ExpansionError(
304 "incomplete variable".to_string(),
305 ));
306 }
307 State::InCurly => {
308 return Err(ScriptRunError::ExpansionError(format!(
309 "unclosed variable: {variable}"
310 )));
311 }
312 State::Normal => {}
313 State::EscapeNext => {
314 return Err(ScriptRunError::ExpansionError(
315 "unclosed backslash".to_string(),
316 ));
317 }
318 }
319 Ok(expanded)
320 }
321
322 pub fn env_vars(&self) -> &HashMap<String, String> {
323 &self.env_vars
324 }
325}
326
327#[derive(derive_more::Debug, Clone)]
328pub struct ScriptOutput {
329 #[debug(skip)]
330 stream: SharedMut<Box<dyn WriteColorAny>>,
331}
332
333trait WriteColorAny: WriteColor + Send + Sync + std::any::Any + 'static + std::fmt::Debug {
334 fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String>;
336 fn clone_buffer(&self) -> Result<termcolor::Buffer, String>;
337}
338
339impl WriteColorAny for termcolor::StandardStream {
340 fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String> {
341 Err("not a buffer".to_string())
342 }
343 fn clone_buffer(&self) -> Result<termcolor::Buffer, String> {
344 Err("not a buffer".to_string())
345 }
346}
347
348impl WriteColorAny for termcolor::Buffer {
349 fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String> {
350 Ok(*self)
351 }
352 fn clone_buffer(&self) -> Result<termcolor::Buffer, String> {
353 Ok(self.clone())
354 }
355}
356
357impl ScriptOutput {
358 pub fn no_color() -> Self {
359 let stm = termcolor::StandardStream::stdout(ColorChoice::Never);
360 Self {
361 stream: SharedMut::new(Box::new(stm) as _),
362 }
363 }
364
365 pub fn quiet(no_color: bool) -> Self {
366 let stm = if no_color {
367 termcolor::Buffer::no_color()
368 } else {
369 termcolor::Buffer::ansi()
370 };
371 Self {
372 stream: SharedMut::new(Box::new(stm) as _),
373 }
374 }
375
376 pub fn take_buffer(self) -> String {
377 let stream = match SharedMut::try_unwrap(self.stream) {
378 Ok(stream) => stream.take_buffer().expect("wrong stream type"),
379 Err(shared) => shared.read().clone_buffer().expect("wrong stream type"),
380 };
381 String::from_utf8_lossy(&stream.into_inner()).to_string()
382 }
383}
384
385impl Default for ScriptOutput {
386 fn default() -> Self {
387 let stm = termcolor::StandardStream::stdout(ColorChoice::Auto);
388 Self {
389 stream: SharedMut::new(Box::new(stm) as _),
390 }
391 }
392}
393
394impl std::io::Write for ScriptOutputLock<'_> {
395 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
396 self.stream.write(buf)
397 }
398 fn flush(&mut self) -> std::io::Result<()> {
399 self.stream.flush()
400 }
401}
402
403impl termcolor::WriteColor for ScriptOutputLock<'_> {
404 fn supports_color(&self) -> bool {
405 self.stream.supports_color()
406 }
407 fn set_color(&mut self, spec: &termcolor::ColorSpec) -> std::io::Result<()> {
408 self.stream.set_color(spec)
409 }
410 fn reset(&mut self) -> std::io::Result<()> {
411 self.stream.reset()
412 }
413 fn is_synchronous(&self) -> bool {
414 self.stream.is_synchronous()
415 }
416 fn set_hyperlink(&mut self, _link: &termcolor::HyperlinkSpec) -> std::io::Result<()> {
417 self.stream.set_hyperlink(_link)
418 }
419 fn supports_hyperlinks(&self) -> bool {
420 self.stream.supports_hyperlinks()
421 }
422}
423
424struct ScriptOutputLock<'a> {
425 stream: keepcalm::SharedWriteLock<'a, Box<dyn WriteColorAny>>,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429enum ScriptMode {
430 Normal,
431 Deferred,
432 Background,
433}
434
435#[derive(derive_more::Debug)]
436pub struct ScriptRunContext {
437 pub args: ScriptRunArgs,
438 pub grok: Grok,
439 timeout: Duration,
440 env: ScriptEnv,
441 includes: Arc<HashMap<String, Script>>,
442 background: ScriptMode,
443 #[debug(skip)]
444 kill: ScriptKillReceiver,
445 #[debug(skip)]
446 kill_sender: ScriptKillSender,
447 output: ScriptOutput,
448
449 global_ignore: OutputPatterns,
450 global_reject: OutputPatterns,
451}
452
453impl Default for ScriptRunContext {
454 fn default() -> Self {
455 let kill = Arc::new(AtomicBool::new(false));
456 Self {
457 args: ScriptRunArgs::default(),
458 grok: Grok::with_default_patterns(),
459 timeout: DEFAULT_TIMEOUT,
460 env: ScriptEnv::default(),
461 background: ScriptMode::Normal,
462 includes: Arc::new(HashMap::new()),
463 kill: ScriptKillReceiver::new(kill.clone()),
464 kill_sender: ScriptKillSender::new(kill.clone()),
465 output: ScriptOutput::default(),
466 global_ignore: OutputPatterns::default(),
467 global_reject: OutputPatterns::default(),
468 }
469 }
470}
471
472impl ScriptRunContext {
473 pub fn new_background(&self) -> Self {
474 let kill = Arc::new(AtomicBool::new(false));
475 Self {
476 args: self.args.clone(),
477 grok: self.grok.clone(),
478 timeout: Duration::MAX,
480 env: self.env.clone(),
481 background: ScriptMode::Background,
482 kill: ScriptKillReceiver::new(kill.clone()),
483 kill_sender: ScriptKillSender::new(kill.clone()),
484 includes: self.includes.clone(),
485 output: if self.args.verbose {
486 self.output.clone()
487 } else {
488 ScriptOutput::quiet(self.args.no_color)
489 },
490 global_ignore: self.global_ignore.clone(),
491 global_reject: self.global_reject.clone(),
492 }
493 }
494
495 pub fn new_deferred(&self) -> Self {
496 Self {
497 args: self.args.clone(),
498 grok: self.grok.clone(),
499 timeout: self.timeout,
500 env: self.env.clone(),
501 background: ScriptMode::Deferred,
502 kill: self.kill.clone(),
503 kill_sender: self.kill_sender.clone(),
504 includes: self.includes.clone(),
505 output: self.output.clone(),
506 global_ignore: self.global_ignore.clone(),
507 global_reject: self.global_reject.clone(),
508 }
509 }
510
511 pub fn pwd(&self) -> NicePathBuf {
512 self.env.pwd()
513 }
514
515 pub fn get_env(&self, name: &str) -> Option<&str> {
516 self.env.get_env(name)
517 }
518
519 pub fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) {
520 self.env.set_env(name, value);
521 }
522
523 pub fn set_pwd(&mut self, pwd: impl Into<NicePathBuf>) {
524 self.env.set_pwd(pwd);
525 }
526
527 pub fn take_output(self) -> String {
528 self.output.take_buffer()
529 }
530
531 fn expand(&self, value: &ShellBit) -> Result<String, ScriptRunError> {
532 self.env.expand(value)
533 }
534
535 pub fn stream(&self) -> impl termcolor::WriteColor + use<'_> {
537 ScriptOutputLock {
538 stream: self.output.stream.write(),
539 }
540 }
541}
542
543#[derive(Clone)]
544pub struct ScriptKillReceiver {
545 kill_receiver: Arc<AtomicBool>,
546}
547
548impl ScriptKillReceiver {
549 pub fn new(kill_receiver: Arc<AtomicBool>) -> Self {
550 Self { kill_receiver }
551 }
552
553 pub fn is_killed(&self) -> bool {
554 self.kill_receiver.load(std::sync::atomic::Ordering::SeqCst)
555 }
556
557 pub fn run_with<T>(&self, kill: impl FnOnce() + Send, wait: impl FnOnce() -> T) -> T {
558 std::thread::scope(|s| {
559 let done = Arc::new(AtomicBool::new(false));
560 let done_clone = done.clone();
561 let t = s.spawn(move || {
562 while !done_clone.load(std::sync::atomic::Ordering::SeqCst) {
563 if self.is_killed() {
564 kill();
565 break;
566 }
567 std::thread::sleep(Duration::from_millis(10));
568 }
569 });
570 let res = wait();
571 done.store(true, std::sync::atomic::Ordering::SeqCst);
572 t.join().unwrap();
573 res
574 })
575 }
576}
577
578#[derive(Clone)]
579pub struct ScriptKillSender {
580 kill_sender: Arc<AtomicBool>,
581}
582
583impl ScriptKillSender {
584 pub fn new(kill_sender: Arc<AtomicBool>) -> Self {
585 Self { kill_sender }
586 }
587
588 pub fn kill(&self) {
589 self.kill_sender
590 .store(true, std::sync::atomic::Ordering::SeqCst);
591 }
592}
593
594impl ScriptRunContext {
595 pub fn new(args: ScriptRunArgs, script_path: impl AsRef<Path>, output: ScriptOutput) -> Self {
596 let mut env = ScriptEnv::default();
597
598 let script_parent = script_path.as_ref().parent().unwrap_or(Path::new(""));
600 env.set_defaults(script_parent);
601
602 let kill = Arc::new(AtomicBool::new(false));
603
604 Self {
605 timeout: args.global_timeout.unwrap_or(DEFAULT_TIMEOUT),
606 args,
607 env,
608 grok: Grok::with_default_patterns(),
609 includes: Arc::new(HashMap::new()),
610 background: ScriptMode::Normal,
611 kill: ScriptKillReceiver::new(kill.clone()),
612 kill_sender: ScriptKillSender::new(kill.clone()),
613 output,
614 global_ignore: OutputPatterns::default(),
615 global_reject: OutputPatterns::default(),
616 }
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
621pub struct ScriptLine {
622 pub location: ScriptLocation,
623 text: String,
624}
625
626impl ScriptLine {
627 pub fn new(file: ScriptFile, line: usize, text: impl AsRef<str>) -> Self {
628 Self {
629 location: ScriptLocation::new(file, line),
630 text: text.as_ref().to_string(),
631 }
632 }
633
634 pub fn parse(file: ScriptFile, text: impl AsRef<str>) -> Vec<Self> {
635 text.as_ref()
636 .lines()
637 .enumerate()
638 .map(|(line, text)| Self {
639 location: ScriptLocation::new(file.clone(), line + file.base_line + 1),
640 text: text.to_string(),
641 })
642 .collect()
643 }
644
645 pub fn starts_with(&self, text: &str) -> bool {
646 self.text.trim().starts_with(text)
647 }
648
649 pub fn first_char(&self) -> Option<char> {
650 self.text.trim().chars().next()
651 }
652
653 pub fn text(&self) -> &str {
654 self.text.trim()
655 }
656
657 pub fn text_untrimmed(&self) -> &str {
658 &self.text
659 }
660
661 pub fn is_empty(&self) -> bool {
662 self.text.trim().is_empty()
663 }
664
665 pub fn strip_prefix(&self, prefix: &str) -> Option<&str> {
666 self.text.strip_prefix(prefix)
667 }
668}
669
670#[derive(Debug, thiserror::Error, derive_more::Display)]
671#[display("{error} at {location}{}", associated_data.as_deref().map_or("".to_string(), |d| format!(": {d}")))]
672pub struct ScriptError {
673 pub error: ScriptErrorType,
674 pub location: ScriptLocation,
675 pub associated_data: Option<String>,
676}
677
678impl ScriptError {
679 pub fn new(error: ScriptErrorType, location: ScriptLocation) -> Self {
680 if std::env::var("PANIC_ON_ERROR").is_ok() {
681 panic!("ScriptError: {error} at {location}");
682 }
683 Self {
684 error,
685 location,
686 associated_data: None,
687 }
688 }
689
690 pub fn new_with_data(
691 error: ScriptErrorType,
692 location: ScriptLocation,
693 associated_data: String,
694 ) -> Self {
695 if std::env::var("PANIC_ON_ERROR").is_ok() {
696 panic!("ScriptError: {error} at {location}: {associated_data}");
697 }
698 Self {
699 error,
700 location,
701 associated_data: Some(associated_data),
702 }
703 }
704}
705
706#[derive(Debug, thiserror::Error, Eq, PartialEq)]
707pub enum ScriptErrorType {
708 #[error("background process not allowed")]
709 BackgroundProcessNotAllowed,
710 #[error("unclosed quote")]
711 UnclosedQuote,
712 #[error("unclosed backslash")]
713 UnclosedBackslash,
714 #[error("illegal shell command format")]
715 IllegalShellCommand,
716 #[error("unsupported redirection")]
717 UnsupportedRedirection,
718 #[error("invalid pattern definition")]
719 InvalidPatternDefinition,
720 #[error("invalid pattern")]
721 InvalidPattern,
722 #[error("invalid meta command")]
723 InvalidMetaCommand,
724 #[error("invalid pattern at global level (only reject or ignore allowed here)")]
725 InvalidGlobalPattern,
726 #[error("invalid block type")]
727 InvalidBlockType,
728 #[error("invalid block arguments")]
729 InvalidBlockArgs,
730 #[error("unsupported command position")]
731 UnsupportedCommandPosition,
732 #[error("invalid trailing pattern after *")]
733 InvalidAnyPattern,
734 #[error("invalid exit status")]
735 InvalidExitStatus,
736 #[error("invalid set variable")]
737 InvalidSetVariable,
738 #[error(
739 "invalid version header, expected `#!/usr/bin/env crok --v0`"
740 )]
741 InvalidVersion,
742 #[error("invalid internal command")]
743 InvalidInternalCommand,
744 #[error("missing command lines")]
745 MissingCommandLines,
746 #[error(
747 "block end without matching block start, too many closing braces or braces not properly nested"
748 )]
749 InvalidBlockEnd,
750 #[error("invalid if condition")]
751 InvalidIfCondition,
752 #[error("expected block or semi-colon (did you forget to add ';' at the end of this line?)")]
753 ExpectedBlockOrSemi,
754}
755
756#[derive(Debug, thiserror::Error)]
757pub enum ScriptRunError {
758 #[error("{0}")]
759 Pattern(#[from] OutputPatternMatchFailure),
760 #[error("{0}")]
761 PatternPrepareError(#[from] OutputPatternPrepareError),
762 #[error("{0} at line {1}")]
763 Exit(CommandResult, ScriptLocation),
764 #[error("included file not found: {0}")]
765 IncludedFileNotFound(String),
766 #[error("expected failure, but passed at line {0}")]
767 ExpectedFailure(ScriptLocation),
768 #[error("{0}")]
769 ExpansionError(String),
770 #[error("{0}")]
771 IO(#[from] std::io::Error),
772 #[error("killed")]
773 Killed,
774 #[error("background process took too long to finish")]
775 BackgroundProcessTookTooLong,
776 #[error("retry took too long to finish")]
777 RetryTookTooLong,
778 #[error("exiting script")]
780 ExitScript,
781}
782
783impl ScriptRunError {
784 #[expect(unused)]
785 pub fn short(&self) -> String {
786 match self {
787 Self::Pattern(_) => "Pattern".to_string(),
788 Self::PatternPrepareError(e) => format!("PatternPrepareError({e:?})"),
789 Self::Exit(status, _) => format!("Exit({status})"),
790 Self::ExpectedFailure(_) => "ExpectedFailure".to_string(),
791 Self::IO(e) => format!("IO({:?})", e.kind()),
792 Self::Killed => "Killed".to_string(),
793 Self::BackgroundProcessTookTooLong => "BackgroundProcessTookTooLong".to_string(),
794 Self::ExpansionError(e) => "ExpansionError".to_string(),
795 Self::RetryTookTooLong => "RetryTookTooLong".to_string(),
796 Self::ExitScript => unreachable!(),
797 Self::IncludedFileNotFound(path) => format!("IncludedFileNotFound({path})"),
798 }
799 }
800}
801
802impl Script {
803 pub fn new(file: ScriptFile) -> Self {
804 Self {
805 commands: Arc::new(vec![]),
806 includes: Arc::new(HashMap::new()),
807 file,
808 }
809 }
810
811 pub fn includes(&self) -> Vec<(ScriptLocation, String)> {
813 self.commands
814 .iter()
815 .flat_map(|block| block.includes())
816 .collect()
817 }
818
819 pub fn run(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
820 let old_includes = context.includes.clone();
821 context.includes = self.includes.clone();
822 let res = ScriptBlock::run_blocks(context, &self.commands);
823 context.includes = old_includes;
824 let v = match res {
825 Ok(v) => v,
826 Err(ScriptRunError::ExitScript) => return Ok(()),
828 Err(e) => return Err(e),
829 };
830 assert!(v.is_empty(), "script did not run to completion: {v:?}");
831 Ok(())
832 }
833
834 pub fn run_with_args(
835 &self,
836 args: ScriptRunArgs,
837 output: ScriptOutput,
838 ) -> Result<(), ScriptRunError> {
839 let start = Instant::now();
840 let script_path = &*self.file.file;
841 let mut context = ScriptRunContext::new(args, script_path, output);
842
843 cwrite!(context.stream(), "Running ");
845 cwrite!(context.stream(), fg = Color::Cyan, "{}", script_path);
846 cwriteln!(context.stream(), " ...");
847 cwriteln!(context.stream());
848
849 let result = self.run(&mut context);
850
851 if let Err(ref e) = result {
853 cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
854 cwrite!(context.stream(), fg = Color::Red, "FAILED");
855 if !context.args.simplified_output {
856 cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
857 } else {
858 cwriteln!(context.stream());
859 }
860 cwrite!(context.stream(), fg = Color::Red, "Error: ");
861 cwriteln!(context.stream(), "{}", e);
862 cwriteln!(context.stream());
863 } else {
864 cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
865 cwrite!(context.stream(), fg = Color::Green, "PASSED");
866 if !context.args.simplified_output {
867 cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
868 } else {
869 cwriteln!(context.stream());
870 }
871 }
872
873 result
874 }
875}
876
877#[derive(Debug, Default, Serialize)]
878pub enum CommandExit {
879 #[default]
880 Success,
881 Failure(i32),
882 Timeout,
883 Any,
884 AnyFailure,
885}
886
887impl CommandExit {
888 pub fn matches(&self, status: CommandResult) -> bool {
889 match (self, status) {
890 (CommandExit::Success, CommandResult::Exit(status, _)) => status.success(),
891 (CommandExit::Failure(code), CommandResult::Exit(status, _)) => {
892 *code == status.code().unwrap_or(-1)
893 }
894 (CommandExit::Timeout, CommandResult::TimedOut) => true,
895 (CommandExit::Any, _) => true,
896 (CommandExit::AnyFailure, CommandResult::Exit(status, _)) => !status.success(),
897 (CommandExit::AnyFailure, _) => true,
898 _ => false,
899 }
900 }
901
902 pub fn is_success(&self) -> bool {
903 matches!(self, CommandExit::Success)
904 }
905}
906
907#[derive(derive_more::Debug)]
908#[allow(clippy::large_enum_variant)]
909pub enum ScriptBlock {
910 Command(ScriptCommand),
911 InternalCommand(ScriptLocation, InternalCommand),
912 Background(Vec<ScriptBlock>),
913 Defer(Vec<ScriptBlock>),
914 If(IfCondition, Vec<ScriptBlock>),
915 For(ForCondition, Vec<ScriptBlock>),
916 Retry(Vec<ScriptBlock>),
917 GlobalIgnore(OutputPatterns),
918 GlobalReject(OutputPatterns),
919}
920
921impl ScriptBlock {
922 pub fn includes(&self) -> Vec<(ScriptLocation, String)> {
923 match self {
924 ScriptBlock::Command(..) => vec![],
925 ScriptBlock::InternalCommand(location, InternalCommand::Include(path)) => {
926 vec![(location.clone(), path.clone())]
927 }
928 ScriptBlock::InternalCommand(..) => vec![],
929 ScriptBlock::Background(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
930 ScriptBlock::Defer(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
931 ScriptBlock::If(_, blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
932 ScriptBlock::For(_, blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
933 ScriptBlock::Retry(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
934 ScriptBlock::GlobalIgnore(_) => vec![],
935 ScriptBlock::GlobalReject(_) => vec![],
936 }
937 }
938
939 #[allow(clippy::type_complexity)]
940 pub fn run_blocks(
941 context: &mut ScriptRunContext,
942 blocks: &[ScriptBlock],
943 ) -> Result<Vec<ScriptResult>, ScriptRunError> {
944 enum Deferred<'a> {
945 Scripts(&'a [ScriptBlock]),
946 Internal(
947 Box<
948 dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError>
949 + Send
950 + Sync
951 + 'a,
952 >,
953 ),
954 Background(
955 ScopedJoinHandle<'a, Result<Vec<ScriptResult>, ScriptRunError>>,
956 ScriptKillSender,
957 ),
958 }
959
960 let mut results = Vec::new();
961 std::thread::scope(|s| {
962 let mut defer_blocks = VecDeque::new();
963 let mut pending_error = None;
964 for block in blocks {
965 if context.kill.is_killed() {
966 return Err(ScriptRunError::Killed);
967 }
968 match block {
969 ScriptBlock::Background(blocks) => {
970 let mut context = context.new_background();
971 let kill_sender = context.kill_sender.clone();
972 let handle = s.spawn(move || Self::run_blocks(&mut context, blocks));
973 defer_blocks.push_front(Deferred::Background(handle, kill_sender));
974 }
975 ScriptBlock::Defer(blocks) => {
976 defer_blocks.push_front(Deferred::Scripts(blocks));
979 }
980 ScriptBlock::InternalCommand(_, command) => {
981 if context.background == ScriptMode::Deferred {
982 cwrite!(context.stream(), dimmed = true, "(deferred) ");
983 }
984 if let Some(f) = command.run(context)? {
985 defer_blocks.push_front(Deferred::Internal(f));
986 }
987 }
988 _ => match block.run(context) {
989 Ok(res) => results.extend(res),
990 Err(e) => {
991 pending_error = Some(e);
992 break;
993 }
994 },
995 }
996 }
997 for block in defer_blocks {
998 match block {
999 Deferred::Scripts(blocks) => {
1000 let mut context = context.new_deferred();
1001 ScriptBlock::run_blocks(&mut context, blocks)?;
1002 }
1003 Deferred::Internal(block) => {
1004 cwrite!(context.stream(), dimmed = true, "(cleanup) ");
1005 block(context)?;
1006 }
1007 Deferred::Background(handle, kill_sender) => {
1008 kill_sender.kill();
1009 let start = std::time::Instant::now();
1010 let mut warned = false;
1011
1012 let timeout = context.timeout;
1013 let warn_at = timeout * 8 / 10;
1014
1015 let results = loop {
1016 if handle.is_finished() {
1017 break handle.join().unwrap()?;
1018 }
1019 std::thread::sleep(std::time::Duration::from_millis(10));
1020 if !warned && start.elapsed() > warn_at {
1021 cwriteln!(
1022 context.stream(),
1023 fg = Color::Yellow,
1024 "Background process is taking too long to finish."
1025 );
1026 warned = true;
1027 }
1028 if start.elapsed() > timeout {
1029 cwriteln!(
1030 context.stream(),
1031 fg = Color::Red,
1032 "Background process took too long to finish."
1033 );
1034 return Err(ScriptRunError::BackgroundProcessTookTooLong);
1035 }
1036 };
1037 for result in results {
1038 cwrite!(context.stream(), dimmed = true, "(background) ");
1039 for line in result.command.command.split('\n') {
1040 cwriteln!(context.stream(), fg = Color::Green, "{}", line);
1041 }
1042 if context.args.simplified_output {
1043 cwriteln!(context.stream(), dimmed = true, "---");
1044 } else {
1045 cwriteln_rule!(
1046 context.stream(),
1047 fg = Color::Cyan,
1048 "{}",
1049 result.command.location
1050 );
1051 }
1052 for line in &result.output {
1053 cwriteln!(context.stream(), "{}", line);
1054 }
1055 if result.output.is_empty() {
1056 cwriteln!(context.stream(), dimmed = true, "(no output)");
1057 }
1058 if context.args.simplified_output {
1059 cwriteln!(context.stream(), dimmed = true, "---");
1060 } else {
1061 cwriteln_rule!(context.stream());
1062 }
1063 result.evaluate(context)?;
1064 }
1065 }
1066 }
1067 }
1068 if let Some(error) = pending_error {
1069 return Err(error);
1070 }
1071 Ok(results)
1072 })
1073 }
1074
1075 pub fn run(&self, context: &mut ScriptRunContext) -> Result<Vec<ScriptResult>, ScriptRunError> {
1076 let pwd = context.pwd();
1077 let res = pwd.exists();
1078 if !matches!(res, Ok(true)) {
1079 cwriteln!(
1080 context.stream(),
1081 fg = Color::Red,
1082 "$PWD {pwd:?} doesn't exist. Run `cd $INITIAL_PWD` to fix.",
1083 );
1084 return Err(ScriptRunError::IO(std::io::Error::new(
1085 std::io::ErrorKind::NotFound,
1086 format!("PWD does not exist: {pwd:?}"),
1087 )));
1088 }
1089
1090 match self {
1091 ScriptBlock::Command(command) => {
1092 if context.background == ScriptMode::Deferred {
1093 cwrite!(context.stream(), dimmed = true, "(deferred) ");
1094 }
1095 let result = command.run(context)?;
1096 if context.background != ScriptMode::Background {
1097 result.evaluate(context)?;
1098 Ok(vec![])
1099 } else {
1100 Ok(vec![result])
1101 }
1102 }
1103 ScriptBlock::If(condition, blocks) => {
1104 let condition = condition.expand(context)?;
1105 if condition.matches(context) {
1106 Self::run_blocks(context, blocks)
1107 } else {
1108 Ok(vec![])
1109 }
1110 }
1111 ScriptBlock::For(ForCondition::Env(env, values), blocks) => {
1112 let mut results = Vec::new();
1113 for value in values {
1114 context.set_env(env, context.expand(value)?);
1115 results.extend(Self::run_blocks(context, blocks)?);
1116 }
1117 Ok(results)
1118 }
1119 ScriptBlock::Retry(blocks) => {
1120 let start = Instant::now();
1121 let mut backoff = Duration::from_millis(100);
1122
1123 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1124 cwriteln!(context.stream(), "running...");
1125
1126 loop {
1127 let mut nested_context = context.new_background();
1128 if let Ok(results) = Self::run_blocks(&mut nested_context, blocks) {
1129 let mut all_ok = true;
1130 for result in results {
1131 if result.evaluate(&mut nested_context).is_err() {
1132 all_ok = false;
1133 break;
1134 }
1135 }
1136 if all_ok {
1137 let output = nested_context.take_output();
1138 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1139 cwriteln!(context.stream(), "success");
1140 cwriteln!(context.stream());
1141 cwriteln!(context.stream(), "{output}");
1142 return Ok(vec![]);
1143 }
1144 }
1145
1146 if start.elapsed() > context.timeout {
1147 let output = nested_context.take_output();
1148 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1149 cwriteln!(context.stream(), fg = Color::Red, "timed out");
1150 cwriteln!(context.stream());
1151 cwriteln!(context.stream(), "{output}");
1152 cwriteln_rule!(context.stream());
1153 return Err(ScriptRunError::RetryTookTooLong);
1154 }
1155 std::thread::sleep(backoff);
1156 backoff *= 2;
1157 }
1158 }
1159 ScriptBlock::GlobalIgnore(patterns) => {
1160 for pattern in patterns.iter() {
1161 pattern.prepare(&context.grok)?;
1162 }
1163 context.global_ignore.extend(patterns);
1164 Ok(vec![])
1165 }
1166 ScriptBlock::GlobalReject(patterns) => {
1167 for pattern in patterns.iter() {
1168 pattern.prepare(&context.grok)?;
1169 }
1170 context.global_reject.extend(patterns);
1171 Ok(vec![])
1172 }
1173 _ => unreachable!("Unexpected block type: {self:?}"),
1174 }
1175 }
1176}
1177
1178impl Serialize for ScriptBlock {
1179 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1180 where
1181 S: serde::Serializer,
1182 {
1183 match self {
1184 ScriptBlock::Command(command) => command.serialize(serializer),
1185 ScriptBlock::InternalCommand(_, command) => command.serialize(serializer),
1186 ScriptBlock::Background(blocks) => {
1187 let mut ser = serializer.serialize_map(Some(1))?;
1188 ser.serialize_entry("background", blocks)?;
1189 ser.end()
1190 }
1191 ScriptBlock::Defer(blocks) => {
1192 let mut ser = serializer.serialize_map(Some(1))?;
1193 ser.serialize_entry("defer", blocks)?;
1194 ser.end()
1195 }
1196 ScriptBlock::If(condition, blocks) => {
1197 let mut ser = serializer.serialize_map(Some(2))?;
1198 ser.serialize_entry("if", condition)?;
1199 ser.serialize_entry("blocks", blocks)?;
1200 ser.end()
1201 }
1202 ScriptBlock::For(condition, blocks) => {
1203 let mut ser = serializer.serialize_map(Some(2))?;
1204 ser.serialize_entry("for", condition)?;
1205 ser.serialize_entry("blocks", blocks)?;
1206 ser.end()
1207 }
1208 ScriptBlock::Retry(blocks) => {
1209 let mut ser = serializer.serialize_map(Some(1))?;
1210 ser.serialize_entry("retry", blocks)?;
1211 ser.end()
1212 }
1213 ScriptBlock::GlobalIgnore(patterns) => {
1214 let mut ser = serializer.serialize_map(Some(1))?;
1215 ser.serialize_entry("ignore", patterns)?;
1216 ser.end()
1217 }
1218 ScriptBlock::GlobalReject(patterns) => {
1219 let mut ser = serializer.serialize_map(Some(1))?;
1220 ser.serialize_entry("reject", patterns)?;
1221 ser.end()
1222 }
1223 }
1224 }
1225}
1226
1227#[derive(Debug, Clone, Serialize)]
1228pub enum InternalCommand {
1229 UsingTempdir,
1230 UsingDir(ShellBit, bool),
1231 ChangeDir(ShellBit),
1232 Set(String, ShellBit),
1233 Include(String),
1234 ExitScript,
1235 Pattern(String, String),
1236}
1237
1238impl InternalCommand {
1239 #[allow(clippy::type_complexity)]
1240 pub fn run(
1241 &self,
1242 context: &mut ScriptRunContext,
1243 ) -> Result<
1244 Option<Box<dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError> + Send + Sync>>,
1245 ScriptRunError,
1246 > {
1247 match self.clone() {
1248 InternalCommand::Include(path) => {
1249 let Some(script) = context.includes.get(&path) else {
1250 return Err(ScriptRunError::IncludedFileNotFound(path));
1251 };
1252 script.clone().run(context)?;
1253 Ok(None)
1254 }
1255 InternalCommand::Pattern(name, pattern) => {
1256 context.grok.add_pattern(name, pattern);
1257 Ok(None)
1258 }
1259 InternalCommand::UsingTempdir => {
1260 let current_pwd = context.pwd();
1261 let tempdir = NiceTempDir::new();
1262 cwrite!(context.stream(), fg = Color::Yellow, "using tempdir: ");
1263 cwriteln!(context.stream(), "{}", tempdir);
1264 cwriteln!(context.stream());
1265 context.set_pwd(&tempdir);
1266 let pwd = context.pwd();
1267 if !pwd.exists()? {
1268 return Err(ScriptRunError::IO(std::io::Error::new(
1269 std::io::ErrorKind::NotFound,
1270 format!("newly created tempdir does not exist: {pwd:?}"),
1271 )));
1272 }
1273 Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1274 cwriteln!(
1275 context.stream(),
1276 fg = Color::Yellow,
1277 "removing {} && cd {}",
1278 tempdir,
1279 current_pwd
1280 );
1281 cwriteln!(context.stream());
1282 if !tempdir.exists()? {
1283 cwriteln!(
1284 context.stream(),
1285 fg = Color::Red,
1286 "tempdir does not exist: {tempdir}"
1287 );
1288 }
1289 if let Err(e) = tempdir.remove_dir_all() {
1290 cwriteln!(
1291 context.stream(),
1292 fg = Color::Red,
1293 "error removing tempdir: {e:?}"
1294 );
1295 }
1296 Ok::<_, ScriptRunError>(())
1297 })))
1298 }
1299 InternalCommand::UsingDir(dir, new) => {
1300 let current_pwd = context.pwd();
1301 let dir = context.expand(&dir)?;
1302 let new_pwd = current_pwd.join(dir);
1303 if new {
1304 cwrite!(context.stream(), fg = Color::Yellow, "using new dir: ");
1305 } else {
1306 cwrite!(context.stream(), fg = Color::Yellow, "using dir: ");
1307 }
1308 cwriteln!(context.stream(), "{}", new_pwd);
1309 cwriteln!(context.stream());
1310
1311 if new {
1312 new_pwd.create_dir_all()?;
1313 } else if !new_pwd.exists()? {
1314 return Err(ScriptRunError::IO(std::io::Error::new(
1315 std::io::ErrorKind::NotFound,
1316 "directory does not exist",
1317 )));
1318 }
1319 context.set_pwd(&new_pwd);
1320 Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1321 if new {
1322 cwriteln!(
1323 context.stream(),
1324 fg = Color::Yellow,
1325 "removing {} && cd {}",
1326 new_pwd,
1327 current_pwd
1328 );
1329 cwriteln!(context.stream());
1330 } else {
1331 cwriteln!(context.stream(), fg = Color::Yellow, "cd {}", current_pwd);
1332 cwriteln!(context.stream());
1333 }
1334 if new {
1335 new_pwd.remove_dir_all()?;
1336 }
1337 context.set_pwd(current_pwd);
1338 Ok::<_, ScriptRunError>(())
1339 })))
1340 }
1341 InternalCommand::ChangeDir(dir) => {
1342 let dir = context.expand(&dir)?;
1343
1344 cwriteln!(context.stream(), fg = Color::Yellow, "cd {dir}");
1345 cwriteln!(context.stream());
1346 let current_pwd = context.pwd();
1347 let new_pwd = current_pwd.join(dir);
1348 if !new_pwd.exists()? {
1349 return Err(ScriptRunError::IO(std::io::Error::new(
1350 std::io::ErrorKind::NotFound,
1351 format!("directory does not exist: {new_pwd}"),
1352 )));
1353 }
1354 context.set_pwd(new_pwd);
1355 Ok(None)
1356 }
1357 InternalCommand::Set(name, value) => {
1358 let value = context.expand(&value)?;
1359
1360 context.set_env(&name, &value);
1361 let new_value = context.get_env(&name).unwrap_or_default();
1362 if new_value != value {
1363 cwriteln!(
1364 context.stream(),
1365 fg = Color::Yellow,
1366 "set {name} {value} (-> {new_value})"
1367 );
1368 } else {
1369 cwriteln!(context.stream(), fg = Color::Yellow, "set {name} {value}");
1370 }
1371 cwriteln!(context.stream());
1372
1373 Ok(None)
1374 }
1375 InternalCommand::ExitScript => {
1376 cwriteln!(context.stream(), fg = Color::Yellow, "exiting script");
1377 cwriteln!(context.stream());
1378 Err(ScriptRunError::ExitScript)
1379 }
1380 }
1381 }
1382}
1383
1384#[derive(Debug, Clone)]
1385pub enum IfCondition {
1386 True,
1387 False,
1388 EnvEq(bool, String, ShellBit),
1389}
1390
1391impl IfCondition {
1392 pub fn matches(&self, context: &ScriptRunContext) -> bool {
1393 match self {
1394 IfCondition::True => true,
1395 IfCondition::False => false,
1396 IfCondition::EnvEq(negated, name, expected) => {
1397 let value = context.get_env(name).unwrap_or_default();
1398 (expected == value) ^ negated
1399 }
1400 }
1401 }
1402
1403 pub fn expand(&self, context: &ScriptRunContext) -> Result<IfCondition, ScriptRunError> {
1404 match self {
1405 IfCondition::True => Ok(IfCondition::True),
1406 IfCondition::False => Ok(IfCondition::False),
1407 IfCondition::EnvEq(negated, name, expected) => {
1408 let value = context.expand(expected)?;
1409 Ok(IfCondition::EnvEq(
1410 *negated,
1411 name.clone(),
1412 ShellBit::Literal(value),
1413 ))
1414 }
1415 }
1416 }
1417}
1418
1419impl Serialize for IfCondition {
1420 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1421 where
1422 S: serde::Serializer,
1423 {
1424 match self {
1425 IfCondition::True => "true".serialize(serializer),
1426 IfCondition::False => "false".serialize(serializer),
1427 IfCondition::EnvEq(negated, name, value) => {
1428 let mut ser = serializer.serialize_map(Some(3))?;
1429 ser.serialize_entry("op", if *negated { "!=" } else { "==" })?;
1430 ser.serialize_entry("env", name)?;
1431 ser.serialize_entry("value", value)?;
1432 ser.end()
1433 }
1434 }
1435 }
1436}
1437
1438#[derive(Debug)]
1439pub enum ForCondition {
1440 Env(String, Vec<ShellBit>),
1441}
1442
1443impl Serialize for ForCondition {
1444 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1445 where
1446 S: serde::Serializer,
1447 {
1448 match self {
1449 ForCondition::Env(name, values) => {
1450 let mut ser = serializer.serialize_map(Some(2))?;
1451 ser.serialize_entry("env", name)?;
1452 ser.serialize_entry("values", values)?;
1453 ser.end()
1454 }
1455 }
1456 }
1457}
1458
1459fn is_bool_false(b: &bool) -> bool {
1460 !b
1461}
1462
1463#[derive(Debug, Serialize)]
1464pub struct ScriptCommand {
1465 pub command: CommandLine,
1466 pub pattern: OutputPattern,
1467
1468 #[serde(skip_serializing_if = "CommandExit::is_success")]
1469 pub exit: CommandExit,
1470
1471 #[serde(skip_serializing_if = "is_bool_false")]
1472 pub expect_failure: bool,
1473
1474 #[serde(skip_serializing_if = "Option::is_none")]
1476 pub set_var: Option<String>,
1477
1478 pub set_vars: HashMap<String, ShellBit>,
1480
1481 #[serde(skip_serializing_if = "Option::is_none")]
1483 pub timeout: Option<Duration>,
1484
1485 pub expect: HashMap<String, ShellBit>,
1487}
1488
1489impl ScriptCommand {
1490 pub fn new(command: CommandLine) -> Self {
1491 let location = command.location.clone();
1492 Self {
1493 command,
1494 pattern: OutputPattern {
1495 pattern: OutputPatternType::None,
1496 ignore: Default::default(),
1497 reject: Default::default(),
1498 location,
1499 },
1500 exit: Default::default(),
1501 timeout: None,
1502 expect_failure: false,
1503 set_var: None,
1504 set_vars: Default::default(),
1505 expect: Default::default(),
1506 }
1507 }
1508
1509 pub fn run(&self, context: &mut ScriptRunContext) -> Result<ScriptResult, ScriptRunError> {
1510 let command = &self.command;
1511 let args = &context.args;
1512 let start = Instant::now();
1513
1514 if let Some(delay) = args.delay_steps {
1515 std::thread::sleep(std::time::Duration::from_millis(delay));
1516 }
1517
1518 for line in command.command.split('\n') {
1519 cwriteln!(context.stream(), fg = Color::Green, "{}", line);
1520 }
1521 if args.simplified_output {
1522 cwriteln!(context.stream(), dimmed = true, "---");
1523 } else {
1524 cwriteln_rule!(context.stream(), fg = Color::Cyan, "{}", command.location);
1525 }
1526 let (output, status) = command.run(
1527 &mut context.stream(),
1528 context.args.show_line_numbers,
1529 context.args.runner.clone(),
1530 self.timeout.unwrap_or(context.timeout),
1531 context.env.env_vars(),
1532 &context.kill,
1533 &context.kill_sender,
1534 )?;
1535
1536 let exit_result = if !self.exit.matches(status) {
1537 ExitResult::Mismatch(status)
1538 } else {
1539 ExitResult::Matches(status)
1540 };
1541
1542 if let Some(set_var) = &self.set_var {
1544 context.set_env(set_var, output.to_string().trim());
1545 }
1546
1547 let match_context = OutputMatchContext::new(context);
1548 for (key, value) in &self.expect {
1549 match_context.expect(key, context.expand(value)?);
1550 }
1551 self.pattern.prepare(&context.grok)?;
1552 let prepared_output = output
1553 .with_ignore(&context.global_ignore)
1554 .with_reject(&context.global_reject);
1555 let pattern_result = match self.pattern.matches(match_context.clone(), prepared_output) {
1556 Ok(_) => {
1557 let mut env = context.env.clone();
1558 for (key, value) in match_context.expects() {
1559 env.set_env(key, value);
1560 }
1561 for (key, value) in &self.set_vars {
1562 context.set_env(key, env.expand(value)?);
1563 }
1564
1565 if self.expect_failure {
1566 PatternResult::ExpectedFailure
1567 } else {
1568 PatternResult::Matches
1569 }
1570 }
1571 Err(e) => {
1572 if self.expect_failure {
1573 PatternResult::MatchesFailure
1574 } else {
1575 let trace = format_match_trace_tree(&match_context.traces());
1576 PatternResult::Mismatch(e, trace)
1577 }
1578 }
1579 };
1580
1581 if output.is_empty() {
1582 cwriteln!(context.stream(), dimmed = true, "(no output)");
1583 }
1584
1585 if context.args.simplified_output {
1586 cwriteln!(context.stream(), dimmed = true, "---");
1587 } else {
1588 cwriteln_rule!(context.stream());
1589 }
1590
1591 Ok(ScriptResult {
1592 command: command.clone(),
1593 pattern: pattern_result,
1594 exit: exit_result,
1595 elapsed: start.elapsed(),
1596 output,
1597 })
1598 }
1599}
1600
1601#[derive(derive_more::Debug)]
1602pub struct ScriptResult {
1603 pub command: CommandLine,
1604 pub pattern: PatternResult,
1605 pub exit: ExitResult,
1606 pub elapsed: Duration,
1607 #[debug(skip)]
1608 pub output: Lines,
1609}
1610
1611impl ScriptResult {
1612 pub fn evaluate(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
1613 let args = &context.args;
1614 let (success, failure, warning, arrow) = if *crate::term::IS_UTF8 {
1615 ("✅", "❌", "⚠️", "→")
1616 } else {
1617 ("[*]", "[X]", "[!]", "->")
1618 };
1619
1620 if let ExitResult::Mismatch(status) = self.exit {
1621 if args.ignore_exit_codes {
1622 cwriteln!(
1623 context.stream(),
1624 fg = Color::Yellow,
1625 "{warning} Ignored incorrect exit code: {status}"
1626 );
1627 cwriteln!(context.stream());
1628 } else {
1629 cwriteln!(
1630 context.stream(),
1631 fg = Color::Red,
1632 "{failure} FAIL: {status}"
1633 );
1634 cwriteln!(
1635 context.stream(),
1636 dimmed = true,
1637 " {arrow} {}",
1638 self.command.command
1639 );
1640 cwriteln!(context.stream());
1641 return Err(ScriptRunError::Exit(status, self.command.location.clone()));
1642 }
1643 }
1644
1645 if let PatternResult::Mismatch(e, trace) = &self.pattern {
1646 if args.ignore_matches {
1647 cwriteln!(
1648 context.stream(),
1649 fg = Color::Yellow,
1650 "{warning} Ignored error: {e} (ignoring mismatches)"
1651 );
1652 cwriteln!(context.stream());
1653 } else {
1654 cwriteln!(context.stream(), fg = Color::Red, "ERROR: {e}");
1655 cwriteln!(context.stream(), dimmed = true, "{trace}");
1656 cwriteln!(context.stream(), fg = Color::Red, "{failure} FAIL");
1657 cwriteln!(context.stream());
1658 return Err(ScriptRunError::Pattern(e.clone()));
1659 }
1660 }
1661
1662 if let PatternResult::ExpectedFailure = self.pattern {
1663 if args.ignore_matches {
1664 cwriteln!(
1665 context.stream(),
1666 fg = Color::Yellow,
1667 "{warning} Should not have matched! (ignoring mismatches)"
1668 );
1669 cwriteln!(context.stream());
1670 } else {
1671 cwriteln!(
1672 context.stream(),
1673 fg = Color::Red,
1674 "{failure} FAIL (output shouldn't match)"
1675 );
1676 cwriteln!(
1677 context.stream(),
1678 dimmed = true,
1679 " {arrow} {}",
1680 self.command.command
1681 );
1682 cwriteln!(context.stream());
1683 return Err(ScriptRunError::ExpectedFailure(
1684 self.command.location.clone(),
1685 ));
1686 }
1687 }
1688
1689 if let ExitResult::Matches(status) = self.exit {
1690 if status.success() {
1691 cwrite!(context.stream(), fg = Color::Green, "{success} OK");
1692 if !context.args.simplified_output {
1693 cwriteln!(
1694 context.stream(),
1695 dimmed = true,
1696 " ({:.2}s)",
1697 self.elapsed.as_secs_f32()
1698 );
1699 } else {
1700 cwriteln!(context.stream());
1701 }
1702 } else {
1703 cwrite!(
1704 context.stream(),
1705 fg = Color::Green,
1706 "{success} OK ({status})"
1707 );
1708 if !context.args.simplified_output {
1709 cwriteln!(
1710 context.stream(),
1711 dimmed = true,
1712 " ({:.2}s)",
1713 self.elapsed.as_secs_f32()
1714 );
1715 } else {
1716 cwriteln!(context.stream());
1717 }
1718 }
1719 cwriteln!(context.stream());
1720 }
1721
1722 Ok(())
1723 }
1724}
1725
1726#[derive(Debug)]
1727pub enum PatternResult {
1728 Matches,
1729 MatchesFailure,
1730 ExpectedFailure,
1731 Mismatch(OutputPatternMatchFailure, String),
1732}
1733
1734#[derive(Debug)]
1735pub enum ExitResult {
1736 Matches(CommandResult),
1737 Mismatch(CommandResult),
1738 TimedOut,
1739}
1740
1741#[cfg(test)]
1742mod tests {
1743 use crate::parser::v0::parse_script;
1744
1745 use super::*;
1746 use std::error::Error;
1747
1748 #[test]
1749 fn test_script() -> Result<(), Box<dyn Error>> {
1750 let script = r#"
1751pattern VERSION \d+\.\d+\.\d+;
1752
1753$ something --version || echo 1
1754? Something %{VERSION}
1755
1756$ something --help
1757? Usage: something [OPTIONS]
1758repeat {
1759 choice {
1760? %{DATA} %{GREEDYDATA}
1761? %{DATA}=%{DATA} %{GREEDYDATA}
1762 }
1763}
1764"#;
1765
1766 let script = parse_script(ScriptFile::new("test.cli"), script)?;
1767 assert_eq!(script.commands.len(), 3);
1768 eprintln!("{script:?}");
1769 Ok(())
1770 }
1771
1772 #[test]
1773 fn test_bad_script() -> Result<(), Box<dyn Error>> {
1774 let script = r#"
1775$ (cmd; cmd)
1776$ cmd &
1777 "#;
1778
1779 assert!(matches!(
1780 parse_script(ScriptFile::new("test.cli"), script),
1781 Err(ScriptError {
1782 error: ScriptErrorType::BackgroundProcessNotAllowed,
1783 ..
1784 })
1785 ));
1786 Ok(())
1787 }
1788
1789 #[test]
1790 fn test_script_run_context_expand() {
1791 let mut context = ScriptEnv::default();
1792 context.set_env("A", "1");
1793 context.set_env("B", "2");
1794 context.set_env("C", "3");
1795 assert_eq!(context.expand_str("$A").unwrap(), "1".to_string());
1796 assert_eq!(context.expand_str("$A $B ").unwrap(), "1 2 ".to_string());
1797 assert_eq!(
1798 context.expand_str("${A} ${B} ").unwrap(),
1799 "1 2 ".to_string()
1800 );
1801 assert_eq!(context.expand_str(r#"\$A"#).unwrap(), "$A".to_string());
1802 assert_eq!(context.expand_str(r#"\${A}"#).unwrap(), "${A}".to_string());
1803 assert_eq!(context.expand_str(r#"\\$A"#).unwrap(), r#"\1"#);
1804 assert_eq!(context.expand_str(r#"\\${A}"#).unwrap(), r#"\1"#);
1805 context.set_env("TEMP_DIR", "/tmp");
1806 assert_eq!(context.expand_str("$TEMP_DIR").unwrap(), "/tmp".to_string());
1807 assert_eq!(
1808 context.expand_str("${TEMP_DIR}").unwrap(),
1809 "/tmp".to_string()
1810 );
1811 }
1812}