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