e_utils/system/cmd/
mod.rs

1/// 多任务
2pub 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/// 日志R<...>R解析
16#[cfg(feature = "regex")]
17pub mod rlog;
18/// 无首选 NUMA 节点
19pub const NUMA_NO_PREFERRED_NODE: DWORD = 0x0;
20/// Windows 创建无窗口进程的标志
21pub const CREATE_NO_WINDOW: DWORD = 0x08000000;
22/// Windows 创建新进程组的标志
23pub const CREATE_NEW_PROCESS_GROUP: DWORD = 0x00000200;
24/// 调试新进程。调试器将接收所有调试事件,包括来自此进程创建的所有子进程的事件
25pub const DEBUG_PROCESS: DWORD = 0x00000001;
26/// 调试此进程。调试器不会接收此进程创建的任何子进程的调试事件
27pub const DEBUG_ONLY_THIS_PROCESS: DWORD = 0x00000002;
28/// 进程的主线程以挂起状态创建,直到调用 ResumeThread 函数
29pub const CREATE_SUSPENDED: DWORD = 0x00000004;
30/// 对于控制台进程,新进程没有访问其父进程控制台的权限
31pub const DETACHED_PROCESS: DWORD = 0x00000008;
32/// 新进程有一个新的控制台,而不是继承其父进程的控制台
33pub const CREATE_NEW_CONSOLE: DWORD = 0x00000010;
34/// 进程具有正常优先级类
35pub const NORMAL_PRIORITY_CLASS: DWORD = 0x00000020;
36/// 进程具有空闲优先级类
37pub const IDLE_PRIORITY_CLASS: DWORD = 0x00000040;
38/// 进程具有高优先级类
39pub const HIGH_PRIORITY_CLASS: DWORD = 0x00000080;
40/// 进程具有实时优先级类
41pub const REALTIME_PRIORITY_CLASS: DWORD = 0x00000100;
42/// 如果在 lpEnvironment 参数中指定了环境块,则它使用 Unicode 字符
43pub const CREATE_UNICODE_ENVIRONMENT: DWORD = 0x00000400;
44/// 新进程在单独的 Windows VDM 中运行 16 位应用程序
45pub const CREATE_SEPARATE_WOW_VDM: DWORD = 0x00000800;
46/// 新进程与其他应用程序共享 Windows VDM
47pub const CREATE_SHARED_WOW_VDM: DWORD = 0x00001000;
48/// 强制在单独的 VDM 中运行
49pub const CREATE_FORCEDOS: DWORD = 0x00002000;
50/// 进程具有低于正常优先级的优先级类
51pub const BELOW_NORMAL_PRIORITY_CLASS: DWORD = 0x00004000;
52/// 进程具有高于正常优先级的优先级类
53pub const ABOVE_NORMAL_PRIORITY_CLASS: DWORD = 0x00008000;
54/// 进程继承其父进程的处理器关联性
55pub const INHERIT_PARENT_AFFINITY: DWORD = 0x00010000;
56/// 进程继承其调用者的优先级
57pub const INHERIT_CALLER_PRIORITY: DWORD = 0x00020000;
58/// 进程是受保护的进程
59pub const CREATE_PROTECTED_PROCESS: DWORD = 0x00040000;
60/// 进程创建时使用扩展的启动信息
61pub const EXTENDED_STARTUPINFO_PRESENT: DWORD = 0x00080000;
62/// 开始后台模式,这可能会降低进程的内存和 I/O 优先级
63pub const PROCESS_MODE_BACKGROUND_BEGIN: DWORD = 0x00100000;
64/// 结束后台模式,恢复正常优先级
65pub const PROCESS_MODE_BACKGROUND_END: DWORD = 0x00200000;
66/// 进程不受其父作业的限制
67pub const CREATE_BREAKAWAY_FROM_JOB: DWORD = 0x01000000;
68/// 保留进程的代码授权级别
69pub const CREATE_PRESERVE_CODE_AUTHZ_LEVEL: DWORD = 0x02000000;
70/// 进程不继承其父进程的错误模式
71pub const CREATE_DEFAULT_ERROR_MODE: DWORD = 0x04000000;
72/// 为用户启用分析
73pub const PROFILE_USER: DWORD = 0x10000000;
74/// 为内核启用分析
75pub const PROFILE_KERNEL: DWORD = 0x20000000;
76/// 为服务器启用分析
77pub const PROFILE_SERVER: DWORD = 0x40000000;
78/// 忽略系统默认优先级和调度量程
79pub const CREATE_IGNORE_SYSTEM_DEFAULT: DWORD = 0x80000000;
80
81/// 表示命令执行结果的结构体
82/// 这个结构体包含了命令执行后的标准输出、退出状态和标准错误输出。
83#[derive(Debug, Clone)]
84pub struct CmdOutput {
85  /// 命令的标准输出,已经被解码为字符串
86  pub stdout: String,
87  /// 命令的退出状态
88  pub status: ExitStatus,
89  /// 命令的标准错误输出,保持为原始字节
90  pub stderr: Vec<u8>,
91}
92
93/// 通用命令结果结构体,可序列化和反序列化
94/// 这个结构体用于表示一个通用的命令执行结果,包含内容、状态和自定义选项。
95/// 它可以被序列化和反序列化,方便在不同的上下文中传递和存储。
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CmdResult<T> {
98  /// 命令执行的结果内容
99  pub content: String,
100  /// 命令执行的状态,true 表示成功,false 表示失败
101  pub status: bool,
102  /// 与命令相关的自定义选项,类型为泛型 T
103  pub opts: T,
104}
105
106impl<T> CmdResult<T> {
107  /// 设置选项
108  pub fn set_opts(mut self, opts: T) -> Self {
109    self.opts = opts;
110    self
111  }
112
113  /// 合并另一个 CmdResult
114  pub fn merge(mut self, target: Self) -> Self {
115    self.content = target.content;
116    self.status = target.status;
117    self
118  }
119
120  /// 设置状态
121  pub fn set_status(mut self, state: bool) -> Self {
122    self.status = state;
123    self
124  }
125
126  /// 设置内容
127  pub fn set_content(mut self, content: String) -> Self {
128    self.content = content;
129    self
130  }
131
132  /// 获取选项引用
133  pub fn opts(&self) -> &T {
134    &self.opts
135  }
136}
137
138impl<'a, T: de::Deserialize<'a>> CmdResult<T> {
139  /// 从字符串解析 CmdResult
140  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  /// 将 CmdResult 转换为字符串
148  pub fn to_str(&self) -> crate::AnyResult<String> {
149    Ok(format!("R<{}>R", serde_json::to_string(self)?))
150  }
151
152  /// 将 CmdResult 转换为格式化的字符串
153  pub fn to_string_pretty(&self) -> crate::AnyResult<String> {
154    Ok(format!("R<{}>R", serde_json::to_string_pretty(self)?))
155  }
156}
157
158/// 打开文件或目录的 shell 命令
159#[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/// 异步打开文件或目录的 shell 命令
172#[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/// 命令结构体,用于构建和执行系统命令
185/// ```rust
186/// use e_utils::{shell_open, Cmd};
187/// fn test_cmd() {
188///   let output = Cmd::new("echo Hello from cmd").output().unwrap();
189///   assert_eq!(output.stdout, "Hello from cmd");
190///   assert!(Cmd::new("echo Hello from cmd")
191///     .output()
192///     .is_err());
193/// }
194/// fn test_shell_open_windows() {
195///   assert!(shell_open("C:\\").is_ok());
196/// }
197/// fn main() {
198///   test_cmd();
199///   test_shell_open_windows();
200/// }
201/// ```
202#[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  /// 获取自动识别的exe路径
213  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  /// 检查exe路径是否存在
218  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  /// Args处理
226  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  /// 获取flags
267  pub fn get_flags(&self) -> DWORD {
268    self.flags
269  }
270  /// 获取环境变量
271  pub fn get_env(&self) -> Option<&HashMap<String, String>> {
272    self.env.as_ref()
273  }
274  /// 获取工作目录
275  pub fn get_cwd(&self) -> Option<&PathBuf> {
276    self.cwd.as_ref()
277  }
278  /// 获取exe
279  pub fn get_exe(&self) -> &str {
280    &self.exe
281  }
282  /// 获取args
283  pub fn get_args(&self) -> &Vec<String> {
284    &self.args
285  }
286  /// 获取exe类型
287  pub fn get_exe_type(&self) -> &ExeType {
288    &self.exe_type
289  }
290}
291impl Cmd {
292  /// 默认的进程创建标志
293  ///
294  /// 包含以下标志位:
295  /// - CREATE_UNICODE_ENVIRONMENT: 使用 Unicode 环境变量
296  /// - CREATE_NEW_PROCESS_GROUP: 创建新的进程组
297  /// - NORMAL_PRIORITY_CLASS: 使用正常优先级
298  /// - INHERIT_PARENT_AFFINITY: 继承父进程的处理器关联性
299  /// - INHERIT_CALLER_PRIORITY: 继承调用者的优先级
300  /// - CREATE_PRESERVE_CODE_AUTHZ_LEVEL: 保留代码授权级别
301  #[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  /// 命令结构体,用于构建和执行系统命令
312  /// ```rust
313  /// use e_utils::{shell_open, Cmd};
314  /// fn test_cmd() {
315  ///   let output = Cmd::new("echo Hello from cmd").output().unwrap();
316  ///   assert_eq!(output.stdout, "Hello from cmd");
317  ///   assert!(Cmd::new("echo Hello from cmd")
318  ///     .output()
319  ///     .is_err());
320  /// }
321  /// fn test_shell_open_windows() {
322  ///   assert!(shell_open("C:\\").is_ok());
323  /// }
324  /// fn main() {
325  ///   test_cmd();
326  ///   test_shell_open_windows();
327  /// }
328  /// ```
329  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  /// 添加工作目录并更新 PATH 环境变量
340  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    // 获取或创建环境变量 map
345    let env = self.env.get_or_insert_with(|| std::env::vars().collect());
346
347    // 更新 PATH 环境变量
348    Self::update_path_env(env, path);
349    self
350  }
351
352  /// 更新环境变量中的 PATH
353  fn update_path_env(env: &mut HashMap<String, String>, new_path: PathBuf) {
354    // 使用 entry API 处理 Path 环境变量
355    env.entry("Path".to_string()).and_modify(|path_str| {
356      let mut paths = std::env::split_paths(path_str).collect::<Vec<_>>();
357      // 添加新路径
358      paths.push(new_path.clone());
359
360      // 尝试合并路径并更新环境变量
361      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  /// 添加单个参数
369  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  /// 配置flags
377  pub fn flags(mut self, flags: DWORD) -> Self {
378    self.flags = flags;
379    self
380  }
381  /// 添加多个参数
382  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  /// 添加多个参数
391  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  /// 设置单个环境变量
399  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  /// 设置多个环境变量
409  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  /// 设置是否使用 cmd.exe 或 sh
422  pub fn set_type(mut self, exe_type: ExeType) -> Self {
423    self.exe_type = exe_type;
424    self
425  }
426
427  /// 准备标准 Command
428  fn prepare_command(&self) -> crate::Result<Command> {
429    self.prepare_generic_command(|cmd| Box::new(Command::new(cmd)))
430  }
431
432  /// 准备 Tokio Command
433  #[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  /// 通用命令准备函数
439  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    // 如果参数不为空,则将exe作为参数
464    if !self.args.is_empty() {
465      cmd = cmd.args(&self.args);
466    }
467    // 设置环境变量
468    if let Some(ref env) = self.env {
469      cmd = cmd.envs(env);
470    } else {
471      cmd = cmd.envs(std::env::vars());
472    }
473
474    // 设置工作目录
475    if let Some(ref cwd) = self.cwd {
476      cmd = cmd.current_dir(cwd);
477    }
478
479    cmd = cmd.creation_flags(self.flags);
480    // 配置标准输入输出
481    cmd = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
482    Ok(cmd)
483  }
484
485  /// 执行命令并等待结果
486  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  /// 异步执行命令并等待结果
498  #[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  /// 启动命令但不等待结果
511  pub fn spawn(&self) -> crate::Result<std::process::Child> {
512    self.prepare_command()?.spawn().any()
513  }
514
515  /// 异步启动命令但不等待结果
516  #[cfg(feature = "tokio")]
517  pub fn a_spawn(&self) -> crate::Result<tokio::process::Child> {
518    self.prepare_tokio_command()?.spawn().any()
519  }
520}
521
522/// 统一 Command 和 tokio::process::Command 接口的 trait
523/// 这个 trait 定义了一组通用的命令配置方法,使得 std::process::Command 和 tokio::process::Command
524/// 可以使用相同的接口进行操作。
525pub trait CommandTrait<Target = Command> {
526  /// 添加单个命令行参数
527  fn arg<S: AsRef<OsStr>>(self, arg: S) -> Self;
528
529  /// 添加多个命令行参数
530  fn args<I, S>(self, args: I) -> Self
531  where
532    I: IntoIterator<Item = S>,
533    S: AsRef<OsStr>;
534
535  /// 设置环境变量
536  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  /// 设置命令的工作目录
542  fn current_dir<P: AsRef<std::path::Path>>(self, dir: P) -> Self;
543
544  /// 配置命令的标准输入
545  fn stdin<T: Into<Stdio>>(self, cfg: T) -> Self;
546
547  /// 配置命令的标准输出
548  fn stdout<T: Into<Stdio>>(self, cfg: T) -> Self;
549
550  /// 配置命令的标准错误输出
551  fn stderr<T: Into<Stdio>>(self, cfg: T) -> Self;
552
553  /// 设置进程创建标志(主要用于 Windows 系统)
554  fn creation_flags(self, flags: u32) -> Self;
555}
556
557// 为 std::process::Command 实现 CommandTrait
558impl 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      // 在非 Windows 系统上不执行任何操作
601      std::os::windows::process::CommandExt::creation_flags(&mut self, flags);
602    }
603    self
604  }
605}
606
607// 为 tokio::process::Command 实现 CommandTrait
608#[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/// 应用程序类型枚举
655#[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  /// 从目标路径推断可执行文件类型
699  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  /// 获取可执行文件类型的扩展名
717  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}