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