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 {
558 let thread = OpenThread(THREAD_SUSPEND_RESUME, 0, thread_entry.thread_id);
559 if thread.is_null() {
560 return Err(std::io::Error::last_os_error().into());
561 }
562 ResumeThread(thread);
563 CloseHandle(thread);
564 }
565 }
566 }
567
568 let job = Mutex::new(Some(job));
569 let output = Mutex::new(output);
570 self.run_with(
571 || {
572 _ = job.lock().unwrap().take();
573 _ = output.lock().unwrap().kill();
574 },
575 || {
576 let start = std::time::Instant::now();
577 let mut warned = false;
578 loop {
579 let res = output.lock().unwrap().try_wait()?;
580 if let Some(status) = res {
581 return Ok::<_, std::io::Error>(status);
582 }
583 if start.elapsed() > warn_time {
584 if !warned {
585 let child = output.lock().unwrap().id();
586 eprintln!("Process #{child} taking too long to finish.");
587 warned = true;
588 }
589 }
590 std::thread::sleep(Duration::from_millis(10));
591 }
592 },
593 )
594 }
595
596 #[cfg(unix)]
597 pub fn run_cmd(
598 &self,
599 output: std::process::Child,
600 warn_time: Duration,
601 ) -> std::io::Result<ExitStatus> {
602 let output = Mutex::new(output);
603 self.run_with(
604 || {
605 use signal_child::{signal, signal::*};
606 let id = output.lock().unwrap().id() as i32;
607 _ = signal(-id, SIGINT);
608 std::thread::sleep(Duration::from_millis(10));
609 _ = signal(-id, SIGTERM);
610 },
611 || {
612 let start = std::time::Instant::now();
613 let mut warned = false;
614 loop {
615 let res = output.lock().unwrap().try_wait()?;
616 if let Some(status) = res {
617 return Ok::<_, std::io::Error>(status);
618 }
619 if start.elapsed() > warn_time && !warned {
620 let child = output.lock().unwrap().id();
621 eprintln!("Process #{child} taking too long to finish.");
622 warned = true;
623 }
624 std::thread::sleep(Duration::from_millis(10));
625 }
626 },
627 )
628 }
629}
630
631#[derive(Clone)]
632pub struct ScriptKillSender {
633 kill_sender: Arc<AtomicBool>,
634}
635
636impl ScriptKillSender {
637 pub fn new(kill_sender: Arc<AtomicBool>) -> Self {
638 Self { kill_sender }
639 }
640
641 pub fn kill(&self) {
642 self.kill_sender
643 .store(true, std::sync::atomic::Ordering::SeqCst);
644 }
645}
646
647impl ScriptRunContext {
648 pub fn new(args: ScriptRunArgs, script_path: impl AsRef<Path>, output: ScriptOutput) -> Self {
649 let mut env = ScriptEnv::default();
650 env.set_defaults(script_path.as_ref().parent().unwrap());
651
652 let kill = Arc::new(AtomicBool::new(false));
653
654 Self {
655 timeout: args.global_timeout.unwrap_or(DEFAULT_TIMEOUT),
656 args,
657 env,
658 grok: Grok::with_default_patterns(),
659 includes: Arc::new(HashMap::new()),
660 background: ScriptMode::Normal,
661 kill: ScriptKillReceiver::new(kill.clone()),
662 kill_sender: ScriptKillSender::new(kill.clone()),
663 output,
664 global_ignore: OutputPatterns::default(),
665 global_reject: OutputPatterns::default(),
666 }
667 }
668}
669
670#[derive(Clone, Debug, PartialEq, Eq)]
671pub struct ScriptLine {
672 pub location: ScriptLocation,
673 text: String,
674}
675
676impl ScriptLine {
677 pub fn new(file: ScriptFile, line: usize, text: impl AsRef<str>) -> Self {
678 Self {
679 location: ScriptLocation::new(file, line),
680 text: text.as_ref().to_string(),
681 }
682 }
683
684 pub fn parse(file: ScriptFile, text: impl AsRef<str>) -> Vec<Self> {
685 text.as_ref()
686 .lines()
687 .enumerate()
688 .map(|(line, text)| Self {
689 location: ScriptLocation::new(file.clone(), line + 1),
690 text: text.to_string(),
691 })
692 .collect()
693 }
694
695 pub fn starts_with(&self, text: &str) -> bool {
696 self.text.trim().starts_with(text)
697 }
698
699 pub fn first_char(&self) -> Option<char> {
700 self.text.trim().chars().next()
701 }
702
703 pub fn text(&self) -> &str {
704 self.text.trim()
705 }
706
707 pub fn text_untrimmed(&self) -> &str {
708 &self.text
709 }
710
711 pub fn is_empty(&self) -> bool {
712 self.text.trim().is_empty()
713 }
714
715 pub fn strip_prefix(&self, prefix: &str) -> Option<&str> {
716 self.text.strip_prefix(prefix)
717 }
718}
719
720#[derive(Debug, thiserror::Error, derive_more::Display)]
721#[display("{error} at {location}{}", associated_data.as_deref().map_or("".to_string(), |d| format!(": {d}")))]
722pub struct ScriptError {
723 pub error: ScriptErrorType,
724 pub location: ScriptLocation,
725 pub associated_data: Option<String>,
726}
727
728impl ScriptError {
729 pub fn new(error: ScriptErrorType, location: ScriptLocation) -> Self {
730 if std::env::var("PANIC_ON_ERROR").is_ok() {
731 panic!("ScriptError: {error} at {location}");
732 }
733 Self {
734 error,
735 location,
736 associated_data: None,
737 }
738 }
739
740 pub fn new_with_data(
741 error: ScriptErrorType,
742 location: ScriptLocation,
743 associated_data: String,
744 ) -> Self {
745 if std::env::var("PANIC_ON_ERROR").is_ok() {
746 panic!("ScriptError: {error} at {location}: {associated_data}");
747 }
748 Self {
749 error,
750 location,
751 associated_data: Some(associated_data),
752 }
753 }
754}
755
756#[derive(Debug, thiserror::Error, Eq, PartialEq)]
757pub enum ScriptErrorType {
758 #[error("background process not allowed")]
759 BackgroundProcessNotAllowed,
760 #[error("unclosed quote")]
761 UnclosedQuote,
762 #[error("unclosed backslash")]
763 UnclosedBackslash,
764 #[error("illegal shell command format")]
765 IllegalShellCommand,
766 #[error("unsupported redirection")]
767 UnsupportedRedirection,
768 #[error("invalid pattern definition")]
769 InvalidPatternDefinition,
770 #[error("invalid pattern")]
771 InvalidPattern,
772 #[error("invalid meta command")]
773 InvalidMetaCommand,
774 #[error("invalid pattern at global level (only reject or ignore allowed here)")]
775 InvalidGlobalPattern,
776 #[error("invalid block type")]
777 InvalidBlockType,
778 #[error("invalid block arguments")]
779 InvalidBlockArgs,
780 #[error("unsupported command position")]
781 UnsupportedCommandPosition,
782 #[error("invalid trailing pattern after *")]
783 InvalidAnyPattern,
784 #[error("invalid exit status")]
785 InvalidExitStatus,
786 #[error("invalid set variable")]
787 InvalidSetVariable,
788 #[error("invalid version header, expected `#!/usr/bin/env clitest --v0`")]
789 InvalidVersion,
790 #[error("invalid internal command")]
791 InvalidInternalCommand,
792 #[error("missing command lines")]
793 MissingCommandLines,
794 #[error(
795 "block end without matching block start, too many closing braces or braces not properly nested"
796 )]
797 InvalidBlockEnd,
798 #[error("invalid if condition")]
799 InvalidIfCondition,
800 #[error("expected block or semi-colon (did you forget to add ';' at the end of this line?)")]
801 ExpectedBlockOrSemi,
802}
803
804#[derive(Debug, thiserror::Error)]
805pub enum ScriptRunError {
806 #[error("{0}")]
807 Pattern(#[from] OutputPatternMatchFailure),
808 #[error("{0}")]
809 PatternPrepareError(#[from] OutputPatternPrepareError),
810 #[error("{0} at line {1}")]
811 Exit(CommandResult, ScriptLocation),
812 #[error("included file not found: {0}")]
813 IncludedFileNotFound(String),
814 #[error("expected failure, but passed at line {0}")]
815 ExpectedFailure(ScriptLocation),
816 #[error("{0}")]
817 ExpansionError(String),
818 #[error("{0}")]
819 IO(#[from] std::io::Error),
820 #[error("killed")]
821 Killed,
822 #[error("background process took too long to finish")]
823 BackgroundProcessTookTooLong,
824 #[error("retry took too long to finish")]
825 RetryTookTooLong,
826 #[error("exiting script")]
828 ExitScript,
829}
830
831impl ScriptRunError {
832 #[expect(unused)]
833 pub fn short(&self) -> String {
834 match self {
835 Self::Pattern(_) => "Pattern".to_string(),
836 Self::PatternPrepareError(e) => format!("PatternPrepareError({e:?})"),
837 Self::Exit(status, _) => format!("Exit({status})"),
838 Self::ExpectedFailure(_) => "ExpectedFailure".to_string(),
839 Self::IO(e) => format!("IO({:?})", e.kind()),
840 Self::Killed => "Killed".to_string(),
841 Self::BackgroundProcessTookTooLong => "BackgroundProcessTookTooLong".to_string(),
842 Self::ExpansionError(e) => "ExpansionError".to_string(),
843 Self::RetryTookTooLong => "RetryTookTooLong".to_string(),
844 Self::ExitScript => unreachable!(),
845 Self::IncludedFileNotFound(path) => format!("IncludedFileNotFound({path})"),
846 }
847 }
848}
849
850impl Script {
851 pub fn new(file: ScriptFile) -> Self {
852 Self {
853 commands: Arc::new(vec![]),
854 includes: Arc::new(HashMap::new()),
855 file,
856 }
857 }
858
859 pub fn includes(&self) -> Vec<(ScriptLocation, String)> {
861 self.commands
862 .iter()
863 .flat_map(|block| block.includes())
864 .collect()
865 }
866
867 pub fn run(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
868 let old_includes = context.includes.clone();
869 context.includes = self.includes.clone();
870 let res = ScriptBlock::run_blocks(context, &self.commands);
871 context.includes = old_includes;
872 let v = match res {
873 Ok(v) => v,
874 Err(ScriptRunError::ExitScript) => return Ok(()),
876 Err(e) => return Err(e),
877 };
878 assert!(v.is_empty(), "script did not run to completion: {v:?}");
879 Ok(())
880 }
881
882 pub fn run_with_args(
883 &self,
884 args: ScriptRunArgs,
885 output: ScriptOutput,
886 ) -> Result<(), ScriptRunError> {
887 let start = Instant::now();
888 let script_path = &*self.file.file;
889 let mut context = ScriptRunContext::new(args, script_path, output);
890
891 cwrite!(context.stream(), "Running ");
893 cwrite!(context.stream(), fg = Color::Cyan, "{}", script_path);
894 cwriteln!(context.stream(), " ...");
895 cwriteln!(context.stream());
896
897 let result = self.run(&mut context);
898
899 if let Err(ref e) = result {
901 cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
902 cwrite!(context.stream(), fg = Color::Red, "FAILED");
903 if !context.args.simplified_output {
904 cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
905 } else {
906 cwriteln!(context.stream());
907 }
908 cwrite!(context.stream(), fg = Color::Red, "Error: ");
909 cwriteln!(context.stream(), "{}", e);
910 cwriteln!(context.stream());
911 } else {
912 cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
913 cwrite!(context.stream(), fg = Color::Green, "PASSED");
914 if !context.args.simplified_output {
915 cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
916 } else {
917 cwriteln!(context.stream());
918 }
919 }
920
921 result
922 }
923}
924
925#[derive(Debug, Default, Serialize)]
926pub enum CommandExit {
927 #[default]
928 Success,
929 Failure(i32),
930 Timeout,
931 Any,
932 AnyFailure,
933}
934
935impl CommandExit {
936 pub fn matches(&self, status: CommandResult) -> bool {
937 match (self, status) {
938 (CommandExit::Success, CommandResult::Exit(status)) => status.success(),
939 (CommandExit::Failure(code), CommandResult::Exit(status)) => {
940 *code == status.code().unwrap_or(-1)
941 }
942 (CommandExit::Timeout, CommandResult::TimedOut) => true,
943 (CommandExit::Any, _) => true,
944 (CommandExit::AnyFailure, CommandResult::Exit(status)) => !status.success(),
945 (CommandExit::AnyFailure, _) => true,
946 _ => false,
947 }
948 }
949
950 pub fn is_success(&self) -> bool {
951 matches!(self, CommandExit::Success)
952 }
953}
954
955#[derive(derive_more::Debug)]
956#[allow(clippy::large_enum_variant)]
957pub enum ScriptBlock {
958 Command(ScriptCommand),
959 InternalCommand(ScriptLocation, InternalCommand),
960 Background(Vec<ScriptBlock>),
961 Defer(Vec<ScriptBlock>),
962 If(IfCondition, Vec<ScriptBlock>),
963 For(ForCondition, Vec<ScriptBlock>),
964 Retry(Vec<ScriptBlock>),
965 GlobalIgnore(OutputPatterns),
966 GlobalReject(OutputPatterns),
967}
968
969impl ScriptBlock {
970 pub fn includes(&self) -> Vec<(ScriptLocation, String)> {
971 match self {
972 ScriptBlock::Command(..) => vec![],
973 ScriptBlock::InternalCommand(location, InternalCommand::Include(path)) => {
974 vec![(location.clone(), path.clone())]
975 }
976 ScriptBlock::InternalCommand(..) => vec![],
977 ScriptBlock::Background(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
978 ScriptBlock::Defer(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
979 ScriptBlock::If(_, blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
980 ScriptBlock::For(_, blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
981 ScriptBlock::Retry(blocks) => blocks.iter().flat_map(|b| b.includes()).collect(),
982 ScriptBlock::GlobalIgnore(_) => vec![],
983 ScriptBlock::GlobalReject(_) => vec![],
984 }
985 }
986
987 #[allow(clippy::type_complexity)]
988 pub fn run_blocks(
989 context: &mut ScriptRunContext,
990 blocks: &[ScriptBlock],
991 ) -> Result<Vec<ScriptResult>, ScriptRunError> {
992 enum Deferred<'a> {
993 Scripts(&'a [ScriptBlock]),
994 Internal(
995 Box<
996 dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError>
997 + Send
998 + Sync
999 + 'a,
1000 >,
1001 ),
1002 Background(
1003 ScopedJoinHandle<'a, Result<Vec<ScriptResult>, ScriptRunError>>,
1004 ScriptKillSender,
1005 ),
1006 }
1007
1008 let mut results = Vec::new();
1009 std::thread::scope(|s| {
1010 let mut defer_blocks = VecDeque::new();
1011 let mut pending_error = None;
1012 for block in blocks {
1013 if context.kill.is_killed() {
1014 return Err(ScriptRunError::Killed);
1015 }
1016 match block {
1017 ScriptBlock::Background(blocks) => {
1018 let mut context = context.new_background();
1019 let kill_sender = context.kill_sender.clone();
1020 let handle = s.spawn(move || Self::run_blocks(&mut context, blocks));
1021 defer_blocks.push_front(Deferred::Background(handle, kill_sender));
1022 }
1023 ScriptBlock::Defer(blocks) => {
1024 defer_blocks.push_front(Deferred::Scripts(blocks));
1027 }
1028 ScriptBlock::InternalCommand(_, command) => {
1029 if context.background == ScriptMode::Deferred {
1030 cwrite!(context.stream(), dimmed = true, "(deferred) ");
1031 }
1032 if let Some(f) = command.run(context)? {
1033 defer_blocks.push_front(Deferred::Internal(f));
1034 }
1035 }
1036 _ => match block.run(context) {
1037 Ok(res) => results.extend(res),
1038 Err(e) => {
1039 pending_error = Some(e);
1040 break;
1041 }
1042 },
1043 }
1044 }
1045 for block in defer_blocks {
1046 match block {
1047 Deferred::Scripts(blocks) => {
1048 let mut context = context.new_deferred();
1049 ScriptBlock::run_blocks(&mut context, blocks)?;
1050 }
1051 Deferred::Internal(block) => {
1052 cwrite!(context.stream(), dimmed = true, "(cleanup) ");
1053 block(context)?;
1054 }
1055 Deferred::Background(handle, kill_sender) => {
1056 kill_sender.kill();
1057 let start = std::time::Instant::now();
1058 let mut warned = false;
1059
1060 let timeout = context.timeout;
1061 let warn_at = timeout * 8 / 10;
1062
1063 let results = loop {
1064 if handle.is_finished() {
1065 break handle.join().unwrap()?;
1066 }
1067 std::thread::sleep(std::time::Duration::from_millis(10));
1068 if !warned && start.elapsed() > warn_at {
1069 cwriteln!(
1070 context.stream(),
1071 fg = Color::Yellow,
1072 "Background process is taking too long to finish."
1073 );
1074 warned = true;
1075 }
1076 if start.elapsed() > timeout {
1077 cwriteln!(
1078 context.stream(),
1079 fg = Color::Red,
1080 "Background process took too long to finish."
1081 );
1082 return Err(ScriptRunError::BackgroundProcessTookTooLong);
1083 }
1084 };
1085 for result in results {
1086 cwrite!(context.stream(), dimmed = true, "(background) ");
1087 for line in result.command.command.split('\n') {
1088 cwriteln!(context.stream(), fg = Color::Green, "{}", line);
1089 }
1090 if context.args.simplified_output {
1091 cwriteln!(context.stream(), dimmed = true, "---");
1092 } else {
1093 cwriteln_rule!(
1094 context.stream(),
1095 fg = Color::Cyan,
1096 "{}",
1097 result.command.location
1098 );
1099 }
1100 for line in &result.output {
1101 cwriteln!(context.stream(), "{}", line);
1102 }
1103 if result.output.is_empty() {
1104 cwriteln!(context.stream(), dimmed = true, "(no output)");
1105 }
1106 if context.args.simplified_output {
1107 cwriteln!(context.stream(), dimmed = true, "---");
1108 } else {
1109 cwriteln_rule!(context.stream());
1110 }
1111 result.evaluate(context)?;
1112 }
1113 }
1114 }
1115 }
1116 if let Some(error) = pending_error {
1117 return Err(error);
1118 }
1119 Ok(results)
1120 })
1121 }
1122
1123 pub fn run(&self, context: &mut ScriptRunContext) -> Result<Vec<ScriptResult>, ScriptRunError> {
1124 let pwd = context.pwd();
1125 let res = pwd.exists();
1126 if !matches!(res, Ok(true)) {
1127 cwriteln!(
1128 context.stream(),
1129 fg = Color::Red,
1130 "$PWD {pwd:?} doesn't exist. Run `cd $INITIAL_PWD` to fix.",
1131 );
1132 return Err(ScriptRunError::IO(std::io::Error::new(
1133 std::io::ErrorKind::NotFound,
1134 format!("PWD does not exist: {pwd:?}"),
1135 )));
1136 }
1137
1138 match self {
1139 ScriptBlock::Command(command) => {
1140 if context.background == ScriptMode::Deferred {
1141 cwrite!(context.stream(), dimmed = true, "(deferred) ");
1142 }
1143 let result = command.run(context)?;
1144 if context.background != ScriptMode::Background {
1145 result.evaluate(context)?;
1146 Ok(vec![])
1147 } else {
1148 Ok(vec![result])
1149 }
1150 }
1151 ScriptBlock::If(condition, blocks) => {
1152 let condition = condition.expand(context)?;
1153 if condition.matches(context) {
1154 Self::run_blocks(context, blocks)
1155 } else {
1156 Ok(vec![])
1157 }
1158 }
1159 ScriptBlock::For(ForCondition::Env(env, values), blocks) => {
1160 let mut results = Vec::new();
1161 for value in values {
1162 context.set_env(env, context.expand(value)?);
1163 results.extend(Self::run_blocks(context, blocks)?);
1164 }
1165 Ok(results)
1166 }
1167 ScriptBlock::Retry(blocks) => {
1168 let start = Instant::now();
1169 let mut backoff = Duration::from_millis(100);
1170
1171 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1172 cwriteln!(context.stream(), "running...");
1173
1174 loop {
1175 let mut nested_context = context.new_background();
1176 if let Ok(results) = Self::run_blocks(&mut nested_context, blocks) {
1177 let mut all_ok = true;
1178 for result in results {
1179 if result.evaluate(&mut nested_context).is_err() {
1180 all_ok = false;
1181 break;
1182 }
1183 }
1184 if all_ok {
1185 let output = nested_context.take_output();
1186 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1187 cwriteln!(context.stream(), "success");
1188 cwriteln!(context.stream());
1189 cwriteln!(context.stream(), "{output}");
1190 return Ok(vec![]);
1191 }
1192 }
1193
1194 if start.elapsed() > context.timeout {
1195 let output = nested_context.take_output();
1196 cwrite!(context.stream(), fg = Color::Green, "retry: ");
1197 cwriteln!(context.stream(), fg = Color::Red, "timed out");
1198 cwriteln!(context.stream());
1199 cwriteln!(context.stream(), "{output}");
1200 cwriteln_rule!(context.stream());
1201 return Err(ScriptRunError::RetryTookTooLong);
1202 }
1203 std::thread::sleep(backoff);
1204 backoff *= 2;
1205 }
1206 }
1207 ScriptBlock::GlobalIgnore(patterns) => {
1208 for pattern in patterns.iter() {
1209 pattern.prepare(&context.grok)?;
1210 }
1211 context.global_ignore.extend(patterns);
1212 Ok(vec![])
1213 }
1214 ScriptBlock::GlobalReject(patterns) => {
1215 for pattern in patterns.iter() {
1216 pattern.prepare(&context.grok)?;
1217 }
1218 context.global_reject.extend(patterns);
1219 Ok(vec![])
1220 }
1221 _ => unreachable!("Unexpected block type: {self:?}"),
1222 }
1223 }
1224}
1225
1226impl Serialize for ScriptBlock {
1227 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1228 where
1229 S: serde::Serializer,
1230 {
1231 match self {
1232 ScriptBlock::Command(command) => command.serialize(serializer),
1233 ScriptBlock::InternalCommand(_, command) => command.serialize(serializer),
1234 ScriptBlock::Background(blocks) => {
1235 let mut ser = serializer.serialize_map(Some(1))?;
1236 ser.serialize_entry("background", blocks)?;
1237 ser.end()
1238 }
1239 ScriptBlock::Defer(blocks) => {
1240 let mut ser = serializer.serialize_map(Some(1))?;
1241 ser.serialize_entry("defer", blocks)?;
1242 ser.end()
1243 }
1244 ScriptBlock::If(condition, blocks) => {
1245 let mut ser = serializer.serialize_map(Some(2))?;
1246 ser.serialize_entry("if", condition)?;
1247 ser.serialize_entry("blocks", blocks)?;
1248 ser.end()
1249 }
1250 ScriptBlock::For(condition, blocks) => {
1251 let mut ser = serializer.serialize_map(Some(2))?;
1252 ser.serialize_entry("for", condition)?;
1253 ser.serialize_entry("blocks", blocks)?;
1254 ser.end()
1255 }
1256 ScriptBlock::Retry(blocks) => {
1257 let mut ser = serializer.serialize_map(Some(1))?;
1258 ser.serialize_entry("retry", blocks)?;
1259 ser.end()
1260 }
1261 ScriptBlock::GlobalIgnore(patterns) => {
1262 let mut ser = serializer.serialize_map(Some(1))?;
1263 ser.serialize_entry("ignore", patterns)?;
1264 ser.end()
1265 }
1266 ScriptBlock::GlobalReject(patterns) => {
1267 let mut ser = serializer.serialize_map(Some(1))?;
1268 ser.serialize_entry("reject", patterns)?;
1269 ser.end()
1270 }
1271 }
1272 }
1273}
1274
1275#[derive(Debug, Clone, Serialize)]
1276pub enum InternalCommand {
1277 UsingTempdir,
1278 UsingDir(ShellBit, bool),
1279 ChangeDir(ShellBit),
1280 Set(String, ShellBit),
1281 Include(String),
1282 ExitScript,
1283 Pattern(String, String),
1284}
1285
1286impl InternalCommand {
1287 #[allow(clippy::type_complexity)]
1288 pub fn run(
1289 &self,
1290 context: &mut ScriptRunContext,
1291 ) -> Result<
1292 Option<Box<dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError> + Send + Sync>>,
1293 ScriptRunError,
1294 > {
1295 match self.clone() {
1296 InternalCommand::Include(path) => {
1297 let Some(script) = context.includes.get(&path) else {
1298 return Err(ScriptRunError::IncludedFileNotFound(path));
1299 };
1300 script.clone().run(context)?;
1301 Ok(None)
1302 }
1303 InternalCommand::Pattern(name, pattern) => {
1304 context.grok.add_pattern(name, pattern);
1305 Ok(None)
1306 }
1307 InternalCommand::UsingTempdir => {
1308 let current_pwd = context.pwd();
1309 let tempdir = NiceTempDir::new();
1310 cwrite!(context.stream(), fg = Color::Yellow, "using tempdir: ");
1311 cwriteln!(context.stream(), "{}", tempdir);
1312 cwriteln!(context.stream());
1313 context.set_pwd(&tempdir);
1314 let pwd = context.pwd();
1315 if !pwd.exists()? {
1316 return Err(ScriptRunError::IO(std::io::Error::new(
1317 std::io::ErrorKind::NotFound,
1318 format!("newly created tempdir does not exist: {pwd:?}"),
1319 )));
1320 }
1321 Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1322 cwriteln!(
1323 context.stream(),
1324 fg = Color::Yellow,
1325 "removing {} && cd {}",
1326 tempdir,
1327 current_pwd
1328 );
1329 cwriteln!(context.stream());
1330 if !tempdir.exists()? {
1331 cwriteln!(
1332 context.stream(),
1333 fg = Color::Red,
1334 "tempdir does not exist: {tempdir}"
1335 );
1336 }
1337 if let Err(e) = tempdir.remove_dir_all() {
1338 cwriteln!(
1339 context.stream(),
1340 fg = Color::Red,
1341 "error removing tempdir: {e:?}"
1342 );
1343 }
1344 Ok::<_, ScriptRunError>(())
1345 })))
1346 }
1347 InternalCommand::UsingDir(dir, new) => {
1348 let current_pwd = context.pwd();
1349 let dir = context.expand(&dir)?;
1350 let new_pwd = current_pwd.join(dir);
1351 if new {
1352 cwrite!(context.stream(), fg = Color::Yellow, "using new dir: ");
1353 } else {
1354 cwrite!(context.stream(), fg = Color::Yellow, "using dir: ");
1355 }
1356 cwriteln!(context.stream(), "{}", new_pwd);
1357 cwriteln!(context.stream());
1358
1359 if new {
1360 new_pwd.create_dir_all()?;
1361 } else if !new_pwd.exists()? {
1362 return Err(ScriptRunError::IO(std::io::Error::new(
1363 std::io::ErrorKind::NotFound,
1364 "directory does not exist",
1365 )));
1366 }
1367 context.set_pwd(&new_pwd);
1368 Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1369 if new {
1370 cwriteln!(
1371 context.stream(),
1372 fg = Color::Yellow,
1373 "removing {} && cd {}",
1374 new_pwd,
1375 current_pwd
1376 );
1377 cwriteln!(context.stream());
1378 } else {
1379 cwriteln!(context.stream(), fg = Color::Yellow, "cd {}", current_pwd);
1380 cwriteln!(context.stream());
1381 }
1382 if new {
1383 new_pwd.remove_dir_all()?;
1384 }
1385 context.set_pwd(current_pwd);
1386 Ok::<_, ScriptRunError>(())
1387 })))
1388 }
1389 InternalCommand::ChangeDir(dir) => {
1390 let dir = context.expand(&dir)?;
1391
1392 cwriteln!(context.stream(), fg = Color::Yellow, "cd {dir}");
1393 cwriteln!(context.stream());
1394 let current_pwd = context.pwd();
1395 let new_pwd = current_pwd.join(dir);
1396 context.set_pwd(new_pwd);
1397 Ok(None)
1398 }
1399 InternalCommand::Set(name, value) => {
1400 let value = context.expand(&value)?;
1401
1402 context.set_env(&name, &value);
1403 let new_value = context.get_env(&name).unwrap_or_default();
1404 if new_value != value {
1405 cwriteln!(
1406 context.stream(),
1407 fg = Color::Yellow,
1408 "set {name} {value} (-> {new_value})"
1409 );
1410 } else {
1411 cwriteln!(context.stream(), fg = Color::Yellow, "set {name} {value}");
1412 }
1413 cwriteln!(context.stream());
1414
1415 Ok(None)
1416 }
1417 InternalCommand::ExitScript => {
1418 cwriteln!(context.stream(), fg = Color::Yellow, "exiting script");
1419 cwriteln!(context.stream());
1420 Err(ScriptRunError::ExitScript)
1421 }
1422 }
1423 }
1424}
1425
1426#[derive(Debug, Clone)]
1427pub enum IfCondition {
1428 True,
1429 False,
1430 EnvEq(bool, String, ShellBit),
1431}
1432
1433impl IfCondition {
1434 pub fn matches(&self, context: &ScriptRunContext) -> bool {
1435 match self {
1436 IfCondition::True => true,
1437 IfCondition::False => false,
1438 IfCondition::EnvEq(negated, name, expected) => {
1439 let value = context.get_env(name).unwrap_or_default();
1440 (expected == value) ^ negated
1441 }
1442 }
1443 }
1444
1445 pub fn expand(&self, context: &ScriptRunContext) -> Result<IfCondition, ScriptRunError> {
1446 match self {
1447 IfCondition::True => Ok(IfCondition::True),
1448 IfCondition::False => Ok(IfCondition::False),
1449 IfCondition::EnvEq(negated, name, expected) => {
1450 let value = context.expand(expected)?;
1451 Ok(IfCondition::EnvEq(
1452 *negated,
1453 name.clone(),
1454 ShellBit::Literal(value),
1455 ))
1456 }
1457 }
1458 }
1459}
1460
1461impl Serialize for IfCondition {
1462 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1463 where
1464 S: serde::Serializer,
1465 {
1466 match self {
1467 IfCondition::True => "true".serialize(serializer),
1468 IfCondition::False => "false".serialize(serializer),
1469 IfCondition::EnvEq(negated, name, value) => {
1470 let mut ser = serializer.serialize_map(Some(3))?;
1471 ser.serialize_entry("op", if *negated { "!=" } else { "==" })?;
1472 ser.serialize_entry("env", name)?;
1473 ser.serialize_entry("value", value)?;
1474 ser.end()
1475 }
1476 }
1477 }
1478}
1479
1480#[derive(Debug)]
1481pub enum ForCondition {
1482 Env(String, Vec<ShellBit>),
1483}
1484
1485impl Serialize for ForCondition {
1486 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1487 where
1488 S: serde::Serializer,
1489 {
1490 match self {
1491 ForCondition::Env(name, values) => {
1492 let mut ser = serializer.serialize_map(Some(2))?;
1493 ser.serialize_entry("env", name)?;
1494 ser.serialize_entry("values", values)?;
1495 ser.end()
1496 }
1497 }
1498 }
1499}
1500
1501fn is_bool_false(b: &bool) -> bool {
1502 !b
1503}
1504
1505#[derive(Debug, Serialize)]
1506pub struct ScriptCommand {
1507 pub command: CommandLine,
1508 pub pattern: OutputPattern,
1509
1510 #[serde(skip_serializing_if = "CommandExit::is_success")]
1511 pub exit: CommandExit,
1512
1513 #[serde(skip_serializing_if = "is_bool_false")]
1514 pub expect_failure: bool,
1515
1516 #[serde(skip_serializing_if = "Option::is_none")]
1518 pub set_var: Option<String>,
1519
1520 pub set_vars: HashMap<String, ShellBit>,
1522
1523 #[serde(skip_serializing_if = "Option::is_none")]
1525 pub timeout: Option<Duration>,
1526
1527 pub expect: HashMap<String, ShellBit>,
1529}
1530
1531impl ScriptCommand {
1532 pub fn new(command: CommandLine) -> Self {
1533 let location = command.location.clone();
1534 Self {
1535 command,
1536 pattern: OutputPattern {
1537 pattern: OutputPatternType::None,
1538 ignore: Default::default(),
1539 reject: Default::default(),
1540 location,
1541 },
1542 exit: Default::default(),
1543 timeout: None,
1544 expect_failure: false,
1545 set_var: None,
1546 set_vars: Default::default(),
1547 expect: Default::default(),
1548 }
1549 }
1550
1551 pub fn run(&self, context: &mut ScriptRunContext) -> Result<ScriptResult, ScriptRunError> {
1552 let command = &self.command;
1553 let args = &context.args;
1554 let start = Instant::now();
1555
1556 if let Some(delay) = args.delay_steps {
1557 std::thread::sleep(std::time::Duration::from_millis(delay));
1558 }
1559
1560 for line in command.command.split('\n') {
1561 cwriteln!(context.stream(), fg = Color::Green, "{}", line);
1562 }
1563 if args.simplified_output {
1564 cwriteln!(context.stream(), dimmed = true, "---");
1565 } else {
1566 cwriteln_rule!(context.stream(), fg = Color::Cyan, "{}", command.location);
1567 }
1568 let (output, status) = command.run(
1569 &mut context.stream(),
1570 context.args.show_line_numbers,
1571 context.args.runner.clone(),
1572 self.timeout.unwrap_or(context.timeout),
1573 context.env.env_vars(),
1574 &context.kill,
1575 &context.kill_sender,
1576 )?;
1577
1578 let exit_result = if !self.exit.matches(status) {
1579 ExitResult::Mismatch(status)
1580 } else {
1581 ExitResult::Matches(status)
1582 };
1583
1584 if let Some(set_var) = &self.set_var {
1586 context.set_env(set_var, output.to_string().trim());
1587 }
1588
1589 let match_context = OutputMatchContext::new(context);
1590 for (key, value) in &self.expect {
1591 match_context.expect(key, context.expand(value)?);
1592 }
1593 self.pattern.prepare(&context.grok)?;
1594 let prepared_output = output
1595 .with_ignore(&context.global_ignore)
1596 .with_reject(&context.global_reject);
1597 let pattern_result = match self.pattern.matches(match_context.clone(), prepared_output) {
1598 Ok(_) => {
1599 let mut env = context.env.clone();
1600 for (key, value) in match_context.expects() {
1601 env.set_env(key, value);
1602 }
1603 for (key, value) in &self.set_vars {
1604 context.set_env(key, env.expand(value)?);
1605 }
1606
1607 if self.expect_failure {
1608 PatternResult::ExpectedFailure
1609 } else {
1610 PatternResult::Matches
1611 }
1612 }
1613 Err(e) => {
1614 if self.expect_failure {
1615 PatternResult::MatchesFailure
1616 } else {
1617 let mut trace = String::new();
1618 for line in match_context.traces() {
1619 trace.push_str(&format!("{line}\n"));
1620 }
1621 PatternResult::Mismatch(e, trace)
1622 }
1623 }
1624 };
1625
1626 if output.is_empty() {
1627 cwriteln!(context.stream(), dimmed = true, "(no output)");
1628 }
1629
1630 if context.args.simplified_output {
1631 cwriteln!(context.stream(), dimmed = true, "---");
1632 } else {
1633 cwriteln_rule!(context.stream());
1634 }
1635
1636 Ok(ScriptResult {
1637 command: command.clone(),
1638 pattern: pattern_result,
1639 exit: exit_result,
1640 elapsed: start.elapsed(),
1641 output,
1642 })
1643 }
1644}
1645
1646#[derive(derive_more::Debug)]
1647pub struct ScriptResult {
1648 pub command: CommandLine,
1649 pub pattern: PatternResult,
1650 pub exit: ExitResult,
1651 pub elapsed: Duration,
1652 #[debug(skip)]
1653 pub output: Lines,
1654}
1655
1656impl ScriptResult {
1657 pub fn evaluate(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
1658 let args = &context.args;
1659 let (success, failure, warning, arrow) = if *crate::term::IS_UTF8 {
1660 ("✅", "❌", "⚠️", "→")
1661 } else {
1662 ("[*]", "[X]", "[!]", "->")
1663 };
1664
1665 if let ExitResult::Mismatch(status) = self.exit {
1666 if args.ignore_exit_codes {
1667 cwriteln!(
1668 context.stream(),
1669 fg = Color::Yellow,
1670 "{warning} Ignored incorrect exit code: {status}"
1671 );
1672 cwriteln!(context.stream());
1673 } else {
1674 cwriteln!(
1675 context.stream(),
1676 fg = Color::Red,
1677 "{failure} FAIL: {status}"
1678 );
1679 cwriteln!(
1680 context.stream(),
1681 dimmed = true,
1682 " {arrow} {}",
1683 self.command.command
1684 );
1685 cwriteln!(context.stream());
1686 return Err(ScriptRunError::Exit(status, self.command.location.clone()));
1687 }
1688 }
1689
1690 if let PatternResult::Mismatch(e, trace) = &self.pattern {
1691 if args.ignore_matches {
1692 cwriteln!(
1693 context.stream(),
1694 fg = Color::Yellow,
1695 "{warning} Ignored error: {e} (ignoring mismatches)"
1696 );
1697 cwriteln!(context.stream());
1698 } else {
1699 cwriteln!(context.stream(), fg = Color::Red, "ERROR: {e}");
1700 cwriteln!(context.stream(), dimmed = true, "{trace}");
1701 cwriteln!(context.stream(), fg = Color::Red, "{failure} FAIL");
1702 cwriteln!(context.stream());
1703 return Err(ScriptRunError::Pattern(e.clone()));
1704 }
1705 }
1706
1707 if let PatternResult::ExpectedFailure = self.pattern {
1708 if args.ignore_matches {
1709 cwriteln!(
1710 context.stream(),
1711 fg = Color::Yellow,
1712 "{warning} Should not have matched! (ignoring mismatches)"
1713 );
1714 cwriteln!(context.stream());
1715 } else {
1716 cwriteln!(
1717 context.stream(),
1718 fg = Color::Red,
1719 "{failure} FAIL (output shouldn't match)"
1720 );
1721 cwriteln!(
1722 context.stream(),
1723 dimmed = true,
1724 " {arrow} {}",
1725 self.command.command
1726 );
1727 cwriteln!(context.stream());
1728 return Err(ScriptRunError::ExpectedFailure(self.command.location.clone()));
1729 }
1730 }
1731
1732 if let ExitResult::Matches(status) = self.exit {
1733 if status.success() {
1734 cwrite!(context.stream(), fg = Color::Green, "{success} OK");
1735 if !context.args.simplified_output {
1736 cwriteln!(
1737 context.stream(),
1738 dimmed = true,
1739 " ({:.2}s)",
1740 self.elapsed.as_secs_f32()
1741 );
1742 } else {
1743 cwriteln!(context.stream());
1744 }
1745 } else {
1746 cwrite!(
1747 context.stream(),
1748 fg = Color::Green,
1749 "{success} OK ({status})"
1750 );
1751 if !context.args.simplified_output {
1752 cwriteln!(
1753 context.stream(),
1754 dimmed = true,
1755 " ({:.2}s)",
1756 self.elapsed.as_secs_f32()
1757 );
1758 } else {
1759 cwriteln!(context.stream());
1760 }
1761 }
1762 cwriteln!(context.stream());
1763 }
1764
1765 Ok(())
1766 }
1767}
1768
1769#[derive(Debug)]
1770pub enum PatternResult {
1771 Matches,
1772 MatchesFailure,
1773 ExpectedFailure,
1774 Mismatch(OutputPatternMatchFailure, String),
1775}
1776
1777#[derive(Debug)]
1778pub enum ExitResult {
1779 Matches(CommandResult),
1780 Mismatch(CommandResult),
1781 TimedOut,
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786 use crate::parser::v0::parse_script;
1787
1788 use super::*;
1789 use std::error::Error;
1790
1791 #[test]
1792 fn test_script() -> Result<(), Box<dyn Error>> {
1793 let script = r#"
1794pattern VERSION \d+\.\d+\.\d+;
1795
1796$ something --version || echo 1
1797? Something %{VERSION}
1798
1799$ something --help
1800? Usage: something [OPTIONS]
1801repeat {
1802 choice {
1803? %{DATA} %{GREEDYDATA}
1804? %{DATA}=%{DATA} %{GREEDYDATA}
1805 }
1806}
1807"#;
1808
1809 let script = parse_script(ScriptFile::new("test.cli"), script)?;
1810 assert_eq!(script.commands.len(), 3);
1811 eprintln!("{script:?}");
1812 Ok(())
1813 }
1814
1815 #[test]
1816 fn test_bad_script() -> Result<(), Box<dyn Error>> {
1817 let script = r#"
1818$ (cmd; cmd)
1819$ cmd &
1820 "#;
1821
1822 assert!(matches!(
1823 parse_script(ScriptFile::new("test.cli"), script),
1824 Err(ScriptError {
1825 error: ScriptErrorType::BackgroundProcessNotAllowed,
1826 ..
1827 })
1828 ));
1829 Ok(())
1830 }
1831
1832 #[test]
1833 fn test_script_run_context_expand() {
1834 let mut context = ScriptEnv::default();
1835 context.set_env("A", "1");
1836 context.set_env("B", "2");
1837 context.set_env("C", "3");
1838 assert_eq!(context.expand_str("$A").unwrap(), "1".to_string());
1839 assert_eq!(context.expand_str("$A $B ").unwrap(), "1 2 ".to_string());
1840 assert_eq!(
1841 context.expand_str("${A} ${B} ").unwrap(),
1842 "1 2 ".to_string()
1843 );
1844 assert_eq!(context.expand_str(r#"\$A"#).unwrap(), "$A".to_string());
1845 assert_eq!(context.expand_str(r#"\${A}"#).unwrap(), "${A}".to_string());
1846 assert_eq!(context.expand_str(r#"\\$A"#).unwrap(), r#"\1"#);
1847 assert_eq!(context.expand_str(r#"\\${A}"#).unwrap(), r#"\1"#);
1848 context.set_env("TEMP_DIR", "/tmp");
1849 assert_eq!(context.expand_str("$TEMP_DIR").unwrap(), "/tmp".to_string());
1850 assert_eq!(
1851 context.expand_str("${TEMP_DIR}").unwrap(),
1852 "/tmp".to_string()
1853 );
1854 }
1855}