clitest_lib/
script.rs

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