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