1pub mod tasks;
3use super::encode::auto_decode;
4use crate::AnyRes;
5use serde::{de, Deserialize, Serialize};
6use std::{
7 collections::HashMap,
8 ffi::OsStr,
9 path::{Path, PathBuf},
10 process::{Command, ExitStatus, Stdio},
11};
12use strum::*;
13type DWORD = u32;
14
15#[cfg(feature = "regex")]
17pub mod rlog;
18pub const NUMA_NO_PREFERRED_NODE: DWORD = 0x0;
20pub const CREATE_NO_WINDOW: DWORD = 0x08000000;
22pub const CREATE_NEW_PROCESS_GROUP: DWORD = 0x00000200;
24pub const DEBUG_PROCESS: DWORD = 0x00000001;
26pub const DEBUG_ONLY_THIS_PROCESS: DWORD = 0x00000002;
28pub const CREATE_SUSPENDED: DWORD = 0x00000004;
30pub const DETACHED_PROCESS: DWORD = 0x00000008;
32pub const CREATE_NEW_CONSOLE: DWORD = 0x00000010;
34pub const NORMAL_PRIORITY_CLASS: DWORD = 0x00000020;
36pub const IDLE_PRIORITY_CLASS: DWORD = 0x00000040;
38pub const HIGH_PRIORITY_CLASS: DWORD = 0x00000080;
40pub const REALTIME_PRIORITY_CLASS: DWORD = 0x00000100;
42pub const CREATE_UNICODE_ENVIRONMENT: DWORD = 0x00000400;
44pub const CREATE_SEPARATE_WOW_VDM: DWORD = 0x00000800;
46pub const CREATE_SHARED_WOW_VDM: DWORD = 0x00001000;
48pub const CREATE_FORCEDOS: DWORD = 0x00002000;
50pub const BELOW_NORMAL_PRIORITY_CLASS: DWORD = 0x00004000;
52pub const ABOVE_NORMAL_PRIORITY_CLASS: DWORD = 0x00008000;
54pub const INHERIT_PARENT_AFFINITY: DWORD = 0x00010000;
56pub const INHERIT_CALLER_PRIORITY: DWORD = 0x00020000;
58pub const CREATE_PROTECTED_PROCESS: DWORD = 0x00040000;
60pub const EXTENDED_STARTUPINFO_PRESENT: DWORD = 0x00080000;
62pub const PROCESS_MODE_BACKGROUND_BEGIN: DWORD = 0x00100000;
64pub const PROCESS_MODE_BACKGROUND_END: DWORD = 0x00200000;
66pub const CREATE_BREAKAWAY_FROM_JOB: DWORD = 0x01000000;
68pub const CREATE_PRESERVE_CODE_AUTHZ_LEVEL: DWORD = 0x02000000;
70pub const CREATE_DEFAULT_ERROR_MODE: DWORD = 0x04000000;
72pub const PROFILE_USER: DWORD = 0x10000000;
74pub const PROFILE_KERNEL: DWORD = 0x20000000;
76pub const PROFILE_SERVER: DWORD = 0x40000000;
78pub const CREATE_IGNORE_SYSTEM_DEFAULT: DWORD = 0x80000000;
80
81#[derive(Debug, Clone)]
84pub struct CmdOutput {
85 pub stdout: String,
87 pub status: ExitStatus,
89 pub stderr: Vec<u8>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CmdResult<T> {
98 pub content: String,
100 pub status: bool,
102 pub opts: T,
104}
105
106impl<T> CmdResult<T> {
107 pub fn set_opts(mut self, opts: T) -> Self {
109 self.opts = opts;
110 self
111 }
112
113 pub fn merge(mut self, target: Self) -> Self {
115 self.content = target.content;
116 self.status = target.status;
117 self
118 }
119
120 pub fn set_status(mut self, state: bool) -> Self {
122 self.status = state;
123 self
124 }
125
126 pub fn set_content(mut self, content: String) -> Self {
128 self.content = content;
129 self
130 }
131
132 pub fn opts(&self) -> &T {
134 &self.opts
135 }
136}
137
138impl<'a, T: de::Deserialize<'a>> CmdResult<T> {
139 pub fn from_str(value: &'a str) -> crate::AnyResult<Self> {
141 let s = value.trim().trim_start_matches("R<").trim_end_matches(">R");
142 Ok(serde_json::from_str(s)?)
143 }
144}
145
146impl<T: Serialize> CmdResult<T> {
147 pub fn to_str(&self) -> crate::AnyResult<String> {
149 Ok(format!("R<{}>R", serde_json::to_string(self)?))
150 }
151
152 pub fn to_string_pretty(&self) -> crate::AnyResult<String> {
154 Ok(format!("R<{}>R", serde_json::to_string_pretty(self)?))
155 }
156}
157
158#[cfg(feature = "fs")]
160pub fn shell_open(target: impl AsRef<str>) -> crate::AnyResult<()> {
161 let pathname = crate::fs::convert_path(target.as_ref());
162 #[cfg(target_os = "macos")]
163 Cmd::new("open").args(&["-R", &pathname]).spawn()?;
164 #[cfg(target_os = "windows")]
165 Cmd::new("explorer").arg(pathname).spawn()?;
166 #[cfg(target_os = "linux")]
167 Cmd::new("xdg-open").arg(pathname).spawn()?;
168 Ok(())
169}
170
171#[cfg(all(feature = "fs", feature = "tokio"))]
173pub async fn a_shell_open(target: impl AsRef<str>) -> crate::AnyResult<()> {
174 let pathname = crate::fs::convert_path(target.as_ref());
175 #[cfg(target_os = "macos")]
176 Cmd::new("open").args(&["-R", &pathname]).a_spawn()?.wait().await?;
177 #[cfg(target_os = "windows")]
178 Cmd::new("explorer").arg(pathname).a_spawn()?.wait().await?;
179 #[cfg(target_os = "linux")]
180 Cmd::new("xdg-open").arg(pathname).a_spawn()?.wait().await?;
181 Ok(())
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Cmd {
204 exe: String,
205 args: Vec<String>,
206 cwd: Option<PathBuf>,
207 flags: DWORD,
208 env: Option<HashMap<String, String>>,
209 exe_type: ExeType,
210}
211impl Cmd {
212 pub fn get_exe_path(&self) -> PathBuf {
214 let cwd = self.cwd.clone().unwrap_or(std::env::current_dir().unwrap_or_default());
215 cwd.join(&self.exe)
216 }
217 pub fn check_exe_path(&self) -> crate::Result<PathBuf> {
219 let path = self.get_exe_path();
220 if !path.exists() {
221 return Err(format!("File not found: {}", path.display()).into());
222 }
223 Ok(path)
224 }
225 pub fn split_args(args: &str, key: char) -> Vec<String> {
227 let mut result = Vec::with_capacity(args.split_whitespace().count());
228 let mut start = 0;
229 let mut in_quotes = None;
230 let chars: Vec<_> = args.chars().collect();
231 for (i, &c) in chars.iter().enumerate() {
232 match c {
233 '"' | '\'' => {
234 if let Some(quote) = in_quotes {
235 if quote == c {
236 in_quotes = None;
237 if start < i {
238 result.push(chars[start..i].iter().collect());
239 start = i + 1;
240 }
241 }
242 } else {
243 in_quotes = Some(c);
244 start = i + 1;
245 }
246 }
247 _ if c == key && in_quotes.is_none() => {
248 if start < i {
249 result.push(chars[start..i].iter().collect());
250 }
251 start = i + 1;
252 }
253 _ if i == chars.len() - 1 => {
254 if start <= i {
255 result.push(chars[start..=i].iter().collect());
256 }
257 }
258 _ => {}
259 }
260 }
261
262 result
263 }
264}
265impl Cmd {
266 pub fn get_flags(&self) -> DWORD {
268 self.flags
269 }
270 pub fn get_env(&self) -> Option<&HashMap<String, String>> {
272 self.env.as_ref()
273 }
274 pub fn get_cwd(&self) -> Option<&PathBuf> {
276 self.cwd.as_ref()
277 }
278 pub fn get_exe(&self) -> &str {
280 &self.exe
281 }
282 pub fn get_args(&self) -> &Vec<String> {
284 &self.args
285 }
286 pub fn get_exe_type(&self) -> &ExeType {
288 &self.exe_type
289 }
290}
291impl Cmd {
292 #[cfg(debug_assertions)]
302 pub const DEFAULT_PROCESS_FLAGS: DWORD =
303 CREATE_UNICODE_ENVIRONMENT | NORMAL_PRIORITY_CLASS | INHERIT_PARENT_AFFINITY | INHERIT_CALLER_PRIORITY | CREATE_PRESERVE_CODE_AUTHZ_LEVEL;
304 #[cfg(not(debug_assertions))]
305 pub const DEFAULT_PROCESS_FLAGS: DWORD = CREATE_UNICODE_ENVIRONMENT
306 | NORMAL_PRIORITY_CLASS
307 | CREATE_NO_WINDOW
308 | INHERIT_PARENT_AFFINITY
309 | INHERIT_CALLER_PRIORITY
310 | CREATE_PRESERVE_CODE_AUTHZ_LEVEL;
311 pub fn new<S: AsRef<OsStr>>(exe: S) -> Self {
330 Self {
331 exe: exe.as_ref().to_string_lossy().into_owned(),
332 args: Vec::new(),
333 cwd: None,
334 flags: Self::DEFAULT_PROCESS_FLAGS,
335 env: None,
336 exe_type: ExeType::default(),
337 }
338 }
339 pub fn cwd(mut self, env_path: impl AsRef<std::path::Path>) -> Self {
341 let path = env_path.as_ref().to_path_buf();
342 self.cwd = Some(path.clone());
343
344 let env = self.env.get_or_insert_with(|| std::env::vars().collect());
346
347 Self::update_path_env(env, path);
349 self
350 }
351
352 fn update_path_env(env: &mut HashMap<String, String>, new_path: PathBuf) {
354 env.entry("Path".to_string()).and_modify(|path_str| {
356 let mut paths = std::env::split_paths(path_str).collect::<Vec<_>>();
357 paths.push(new_path.clone());
359
360 if let Ok(new_path_str) = std::env::join_paths(paths) {
362 if let Ok(path_string) = new_path_str.into_string() {
363 *path_str = path_string;
364 }
365 }
366 });
367 }
368 pub fn arg(mut self, arg: impl Into<String>) -> Self {
370 let arg = arg.into();
371 if !arg.is_empty() {
372 self.args.push(arg);
373 }
374 self
375 }
376 pub fn flags(mut self, flags: DWORD) -> Self {
378 self.flags = flags;
379 self
380 }
381 pub fn args<I, S>(mut self, args: I) -> Self
383 where
384 I: IntoIterator<Item = S>,
385 S: AsRef<OsStr>,
386 {
387 self.args.extend(args.into_iter().map(|s| s.as_ref().to_string_lossy().into_owned()));
388 self
389 }
390 pub fn set_args(&mut self, args: Vec<String>) -> &mut Self {
392 if !args.is_empty() {
393 self.args = args;
394 }
395 self
396 }
397
398 pub fn env<K, V>(mut self, key: K, val: V) -> Self
400 where
401 K: Into<String>,
402 V: Into<String>,
403 {
404 self.env.get_or_insert_with(HashMap::new).insert(key.into(), val.into());
405 self
406 }
407
408 pub fn envs<I, K, V>(mut self, vars: I) -> Self
410 where
411 I: IntoIterator<Item = (K, V)>,
412 K: Into<String>,
413 V: Into<String>,
414 {
415 let env = self.env.get_or_insert_with(HashMap::new);
416 for (key, val) in vars {
417 env.insert(key.into(), val.into());
418 }
419 self
420 }
421 pub fn set_type(mut self, exe_type: ExeType) -> Self {
423 self.exe_type = exe_type;
424 self
425 }
426
427 fn prepare_command(&self) -> crate::Result<Command> {
429 self.prepare_generic_command(|cmd| Box::new(Command::new(cmd)))
430 }
431
432 #[cfg(feature = "tokio")]
434 fn prepare_tokio_command(&self) -> crate::Result<tokio::process::Command> {
435 self.prepare_generic_command(|cmd| Box::new(tokio::process::Command::new(cmd)))
436 }
437
438 fn prepare_generic_command<C>(&self, new_command: impl Fn(&str) -> Box<C>) -> crate::Result<C>
440 where
441 C: CommandTrait,
442 {
443 let exe_type = match self.exe_type {
444 ExeType::AutoShell => match ExeType::from_target(&self.exe) {
445 v => v,
446 },
447 other => other,
448 };
449 let mut cmd = match exe_type {
450 ExeType::AutoShell => return Err("AutoShell 无法执行".into()),
451 ExeType::PowerShell => new_command("powershell").args(["-NoProfile", "-Command", &self.exe]),
452 ExeType::Shell => new_command("sh").args(["-c", &self.exe]),
453 ExeType::Cmd => new_command("cmd").args(["/C", &self.exe]),
454 ExeType::Ps1Script => new_command("powershell").args(["-ExecutionPolicy", "Bypass", "-File", &self.exe]),
455 ExeType::Vbs => new_command("cscript").args(["/Nologo"]).arg(&self.exe),
456 ExeType::PythonScript => new_command("python").arg(&self.exe),
457 ExeType::MacOSApp => new_command("open").arg(&self.get_exe_path()),
458 ExeType::AndroidApk => new_command("adb").args(["shell", "am", "start", "-n", &self.exe]),
459 ExeType::IosApp => new_command("xcrun").args(["simctl", "launch", "booted", &self.exe]),
460 ExeType::Unknown => *new_command(&self.exe),
461 _ => *new_command(&self.check_exe_path()?.to_string_lossy()),
462 };
463 if !self.args.is_empty() {
465 cmd = cmd.args(&self.args);
466 }
467 if let Some(ref env) = self.env {
469 cmd = cmd.envs(env);
470 } else {
471 cmd = cmd.envs(std::env::vars());
472 }
473
474 if let Some(ref cwd) = self.cwd {
476 cmd = cmd.current_dir(cwd);
477 }
478
479 cmd = cmd.creation_flags(self.flags);
480 cmd = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
482 Ok(cmd)
483 }
484
485 pub fn output(&self) -> crate::Result<CmdOutput> {
487 let output = self.prepare_command()?.output().any()?;
488 let stdout = auto_decode(&output.stdout).unwrap_or_else(|_| String::from_utf8_lossy(&output.stdout).to_string());
489
490 Ok(CmdOutput {
491 stdout,
492 status: output.status,
493 stderr: output.stderr,
494 })
495 }
496
497 #[cfg(feature = "tokio")]
499 pub async fn a_output(&self) -> crate::Result<CmdOutput> {
500 let output = self.prepare_tokio_command()?.output().await.any()?;
501 let stdout = auto_decode(&output.stdout).unwrap_or_else(|_| String::from_utf8_lossy(&output.stdout).to_string());
502
503 Ok(CmdOutput {
504 stdout,
505 status: output.status,
506 stderr: output.stderr,
507 })
508 }
509
510 pub fn spawn(&self) -> crate::Result<std::process::Child> {
512 self.prepare_command()?.spawn().any()
513 }
514
515 #[cfg(feature = "tokio")]
517 pub fn a_spawn(&self) -> crate::Result<tokio::process::Child> {
518 self.prepare_tokio_command()?.spawn().any()
519 }
520}
521
522pub trait CommandTrait<Target = Command> {
526 fn arg<S: AsRef<OsStr>>(self, arg: S) -> Self;
528
529 fn args<I, S>(self, args: I) -> Self
531 where
532 I: IntoIterator<Item = S>,
533 S: AsRef<OsStr>;
534
535 fn envs<I, K, V>(self, vars: I) -> Self
537 where
538 I: IntoIterator<Item = (K, V)>,
539 K: AsRef<OsStr>,
540 V: AsRef<OsStr>;
541 fn current_dir<P: AsRef<std::path::Path>>(self, dir: P) -> Self;
543
544 fn stdin<T: Into<Stdio>>(self, cfg: T) -> Self;
546
547 fn stdout<T: Into<Stdio>>(self, cfg: T) -> Self;
549
550 fn stderr<T: Into<Stdio>>(self, cfg: T) -> Self;
552
553 fn creation_flags(self, flags: u32) -> Self;
555}
556
557impl CommandTrait for Command {
559 fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
560 Command::arg(&mut self, arg);
561 self
562 }
563 fn args<I, S>(mut self, args: I) -> Self
564 where
565 I: IntoIterator<Item = S>,
566 S: AsRef<OsStr>,
567 {
568 Command::args(&mut self, args);
569 self
570 }
571 fn envs<I, K, V>(mut self, vars: I) -> Self
572 where
573 I: IntoIterator<Item = (K, V)>,
574 K: AsRef<OsStr>,
575 V: AsRef<OsStr>,
576 {
577 Command::env_clear(&mut self).envs(vars);
578 self
579 }
580 fn current_dir<P: AsRef<std::path::Path>>(mut self, dir: P) -> Self {
581 Command::current_dir(&mut self, dir);
582 self
583 }
584 fn stdin<T: Into<Stdio>>(mut self, cfg: T) -> Self {
585 Command::stdin(&mut self, cfg);
586 self
587 }
588 fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
589 Command::stdout(&mut self, cfg);
590 self
591 }
592 fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
593 Command::stderr(&mut self, cfg);
594 self
595 }
596
597 fn creation_flags(mut self, flags: u32) -> Self {
598 #[cfg(target_os = "windows")]
599 {
600 std::os::windows::process::CommandExt::creation_flags(&mut self, flags);
602 }
603 self
604 }
605}
606
607#[cfg(feature = "tokio")]
609impl CommandTrait for tokio::process::Command {
610 fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
611 tokio::process::Command::arg(&mut self, arg);
612 self
613 }
614 fn args<I, S>(mut self, args: I) -> Self
615 where
616 I: IntoIterator<Item = S>,
617 S: AsRef<OsStr>,
618 {
619 tokio::process::Command::args(&mut self, args);
620 self
621 }
622 fn envs<I, K, V>(mut self, vars: I) -> Self
623 where
624 I: IntoIterator<Item = (K, V)>,
625 K: AsRef<OsStr>,
626 V: AsRef<OsStr>,
627 {
628 tokio::process::Command::env_clear(&mut self).envs(vars);
629 self
630 }
631 fn current_dir<P: AsRef<std::path::Path>>(mut self, dir: P) -> Self {
632 tokio::process::Command::current_dir(&mut self, dir);
633 self
634 }
635 fn stdin<T: Into<Stdio>>(mut self, cfg: T) -> Self {
636 tokio::process::Command::stdin(&mut self, cfg);
637 self
638 }
639 fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
640 tokio::process::Command::stdout(&mut self, cfg);
641 self
642 }
643 fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
644 tokio::process::Command::stderr(&mut self, cfg);
645 self
646 }
647 fn creation_flags(mut self, flags: u32) -> Self {
648 #[cfg(target_os = "windows")]
649 tokio::process::Command::creation_flags(&mut self, flags);
650 self
651 }
652}
653
654#[allow(missing_docs)]
656#[derive(Default, Clone, Copy, Debug, Display, PartialEq, EnumString, VariantArray, Deserialize, Serialize)]
657#[repr(i32)]
658pub enum ExeType {
659 #[default]
660 #[strum(to_string = "Auto")]
661 AutoShell,
662 #[strum(to_string = "PS1")]
663 PowerShell,
664 #[strum(to_string = "SH")]
665 Shell,
666 #[strum(to_string = "CMD")]
667 Cmd,
668 #[strum(to_string = ".exe")]
669 WindowsExe,
670 #[strum(to_string = ".sh")]
671 ShellScript,
672 #[strum(to_string = ".ps1")]
673 Ps1Script,
674 #[strum(to_string = ".bat")]
675 Bat,
676 #[strum(to_string = ".vbs")]
677 Vbs,
678 #[strum(to_string = ".py")]
679 PythonScript,
680 #[strum(to_string = ".cmd")]
681 CmdScript,
682 #[strum(to_string = ".app")]
683 MacOSApp,
684 #[strum(to_string = ".LinuxEXE")]
685 LinuxExe,
686 #[strum(to_string = ".apk")]
687 AndroidApk,
688 #[strum(to_string = ".ipa")]
689 IosApp,
690 #[strum(to_string = ".so")]
691 So,
692 #[strum(to_string = ".dll")]
693 Dll,
694 #[strum(to_string = "Unknown")]
695 Unknown,
696}
697impl ExeType {
698 pub fn from_target(p: impl AsRef<Path>) -> Self {
700 match p.as_ref().extension().and_then(|x| x.to_str()).unwrap_or_default().to_lowercase().as_str() {
701 "exe" => ExeType::WindowsExe,
702 "bat" => ExeType::Bat,
703 "cmd" => ExeType::CmdScript,
704 "vbs" => ExeType::Vbs,
705 "ps1" => ExeType::Ps1Script,
706 "sh" => ExeType::ShellScript,
707 "app" => ExeType::MacOSApp,
708 "apk" => ExeType::AndroidApk,
709 "ipa" => ExeType::IosApp,
710 "py" => ExeType::PythonScript,
711 "so" => ExeType::So,
712 "dll" => ExeType::Dll,
713 _ => ExeType::Unknown,
714 }
715 }
716 pub fn to_extension(&self) -> &'static str {
718 match self {
719 ExeType::WindowsExe => "exe",
720 ExeType::Bat => "bat",
721 ExeType::CmdScript => "cmd",
722 ExeType::Vbs => "vbs",
723 ExeType::Ps1Script => "ps1",
724 ExeType::ShellScript => "sh",
725 ExeType::MacOSApp => "app",
726 ExeType::AndroidApk => "apk",
727 ExeType::IosApp => "ipa",
728 ExeType::PythonScript => "py",
729 ExeType::So => "so",
730 ExeType::Dll => "dll",
731 _ => "",
732 }
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 #[cfg(feature = "tokio")]
739 mod a_async {
740 use crate::{
741 cmd::{Cmd, ExeType},
742 fs::a_temp_file,
743 };
744 #[tokio::test]
745 #[cfg(not(target_os = "windows"))]
746 fn test_shell_open_unix() {
747 assert!(a_shell_open("/").await.is_ok());
748 }
749 #[tokio::test]
750 #[cfg(target_os = "windows")]
751 async fn test_shell_open_windows() {
752 use crate::cmd::a_shell_open;
753
754 assert!(a_shell_open("C:\\").await.is_ok());
755 }
756 #[tokio::test]
757 async fn test_cmd_bat() {
758 let temp_dir = a_temp_file("test_cmd_bat", "test.bat", "echo test").await.unwrap();
759 let cwd = temp_dir.parent().unwrap();
760 let output = Cmd::new("test.bat").cwd(cwd).a_output().await.unwrap();
761 assert!(output.stdout.contains("test"));
762 }
763
764 #[tokio::test]
765 async fn test_cmd_type() {
766 assert!(Cmd::new("echo Hello from cmd").set_type(ExeType::Cmd).output().is_ok());
767 assert!(Cmd::new("echo Hello from cmd").set_type(ExeType::AutoShell).output().is_ok());
768 assert!(Cmd::new("echo").args(["Hello", "from", "cmd"]).set_type(ExeType::IosApp).output().is_err());
769 assert!(Cmd::new("echo Hello from cmd").set_type(ExeType::WindowsExe).output().is_err());
770 }
771 #[tokio::test]
772 async fn test_cmd_zh() {
773 let output = Cmd::new("echo 你好Rust").a_output().await.unwrap();
774 assert_eq!(output.stdout, "你好Rust");
775 }
776 }
777 mod sync {
778 use crate::{
779 cmd::{shell_open, Cmd, CmdResult},
780 fs::temp_file,
781 };
782 use serde::{Deserialize, Serialize};
783 #[test]
784 #[cfg(target_os = "windows")]
785 fn test_shell_open_windows() {
786 assert!(shell_open("C:\\").is_ok());
787 }
788 #[test]
789 #[cfg(not(target_os = "windows"))]
790 fn test_shell_open_unix() {
791 assert!(shell_open("/").is_ok());
792 }
793 #[test]
794 fn test_cmd() {
795 let output = Cmd::new("echo").args(["\"Hello from cmd\""]).output().unwrap();
796 assert_eq!(output.stdout, "Hello from cmd");
797 }
798
799 #[test]
800 fn test_cmd_result_serialization() {
801 #[derive(Debug, Serialize, Deserialize)]
802 struct TestOpts {
803 value: String,
804 }
805
806 let result = CmdResult {
807 content: "Test content".to_string(),
808 status: true,
809 opts: TestOpts { value: "test".to_string() },
810 };
811
812 let serialized = result.to_str().unwrap();
813 assert!(serialized.starts_with("R<") && serialized.ends_with(">R"));
814
815 let deserialized: CmdResult<TestOpts> = CmdResult::from_str(&serialized).unwrap();
816 assert_eq!(deserialized.content, "Test content");
817 assert_eq!(deserialized.status, true);
818 assert_eq!(deserialized.opts.value, "test");
819 }
820 #[test]
821 fn test_cmd_bat() {
822 let temp_dir = temp_file("test_cmd_bat", "test.bat", "echo test").unwrap();
823 let cwd = temp_dir.parent().unwrap();
824 let output = Cmd::new("test.bat").cwd(cwd).output().unwrap();
825 assert!(output.stdout.contains("test"));
826 }
827 }
828}