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