unistore_process/
command.rs1use crate::child::Child;
6use crate::error::ProcessError;
7use crate::output::ProcessOutput;
8use std::collections::HashMap;
9use std::path::PathBuf;
10use tokio::process::Command as TokioCommand;
11
12pub struct Command {
14 program: String,
15 args: Vec<String>,
16 envs: HashMap<String, String>,
17 env_clear: bool,
18 current_dir: Option<PathBuf>,
19 stdin_piped: bool,
20 stdout_piped: bool,
21 stderr_piped: bool,
22}
23
24impl Command {
25 pub fn new(program: impl Into<String>) -> Self {
27 Self {
28 program: program.into(),
29 args: Vec::new(),
30 envs: HashMap::new(),
31 env_clear: false,
32 current_dir: None,
33 stdin_piped: false,
34 stdout_piped: true,
35 stderr_piped: true,
36 }
37 }
38
39 pub fn builder(program: impl Into<String>) -> CommandBuilder {
41 CommandBuilder::new(program)
42 }
43
44 pub fn arg(mut self, arg: impl Into<String>) -> Self {
46 self.args.push(arg.into());
47 self
48 }
49
50 pub fn args<I, S>(mut self, args: I) -> Self
52 where
53 I: IntoIterator<Item = S>,
54 S: Into<String>,
55 {
56 self.args.extend(args.into_iter().map(|s| s.into()));
57 self
58 }
59
60 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
62 self.envs.insert(key.into(), value.into());
63 self
64 }
65
66 pub fn envs<I, K, V>(mut self, vars: I) -> Self
68 where
69 I: IntoIterator<Item = (K, V)>,
70 K: Into<String>,
71 V: Into<String>,
72 {
73 self.envs
74 .extend(vars.into_iter().map(|(k, v)| (k.into(), v.into())));
75 self
76 }
77
78 pub fn env_clear(mut self) -> Self {
80 self.env_clear = true;
81 self
82 }
83
84 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
86 self.current_dir = Some(dir.into());
87 self
88 }
89
90 pub fn stdin_piped(mut self) -> Self {
92 self.stdin_piped = true;
93 self
94 }
95
96 pub fn stdout_inherit(mut self) -> Self {
98 self.stdout_piped = false;
99 self
100 }
101
102 pub fn stderr_inherit(mut self) -> Self {
104 self.stderr_piped = false;
105 self
106 }
107
108 fn build_command(&self) -> TokioCommand {
110 let mut cmd = TokioCommand::new(&self.program);
111
112 cmd.args(&self.args);
113
114 if self.env_clear {
115 cmd.env_clear();
116 }
117
118 for (key, value) in &self.envs {
119 cmd.env(key, value);
120 }
121
122 if let Some(ref dir) = self.current_dir {
123 cmd.current_dir(dir);
124 }
125
126 use std::process::Stdio;
127
128 cmd.stdin(if self.stdin_piped {
129 Stdio::piped()
130 } else {
131 Stdio::null()
132 });
133
134 cmd.stdout(if self.stdout_piped {
135 Stdio::piped()
136 } else {
137 Stdio::inherit()
138 });
139
140 cmd.stderr(if self.stderr_piped {
141 Stdio::piped()
142 } else {
143 Stdio::inherit()
144 });
145
146 cmd
147 }
148
149 pub async fn spawn(self) -> Result<Child, ProcessError> {
151 let mut cmd = self.build_command();
152 let child = cmd.spawn()?;
153 Child::new(child)
154 }
155
156 pub async fn output(self) -> Result<ProcessOutput, ProcessError> {
158 let mut cmd = self.build_command();
159 let output = cmd.output().await?;
160 Ok(ProcessOutput::new(
161 output.status.into(),
162 output.stdout,
163 output.stderr,
164 ))
165 }
166
167 pub async fn status(self) -> Result<crate::child::ExitStatus, ProcessError> {
169 let mut cmd = self.build_command();
170 let status = cmd.status().await?;
171 Ok(status.into())
172 }
173}
174
175#[derive(Default)]
177pub struct CommandBuilder {
178 command: Option<Command>,
179}
180
181impl CommandBuilder {
182 pub fn new(program: impl Into<String>) -> Self {
184 Self {
185 command: Some(Command::new(program)),
186 }
187 }
188
189 pub fn arg(mut self, arg: impl Into<String>) -> Self {
191 if let Some(cmd) = self.command.take() {
192 self.command = Some(cmd.arg(arg));
193 }
194 self
195 }
196
197 pub fn args<I, S>(mut self, args: I) -> Self
199 where
200 I: IntoIterator<Item = S>,
201 S: Into<String>,
202 {
203 if let Some(cmd) = self.command.take() {
204 self.command = Some(cmd.args(args));
205 }
206 self
207 }
208
209 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
211 if let Some(cmd) = self.command.take() {
212 self.command = Some(cmd.env(key, value));
213 }
214 self
215 }
216
217 pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
219 if let Some(cmd) = self.command.take() {
220 self.command = Some(cmd.current_dir(dir));
221 }
222 self
223 }
224
225 pub fn stdin_piped(mut self) -> Self {
227 if let Some(cmd) = self.command.take() {
228 self.command = Some(cmd.stdin_piped());
229 }
230 self
231 }
232
233 pub fn build(self) -> Command {
235 self.command.unwrap_or_else(|| Command::new(""))
236 }
237
238 pub async fn spawn(self) -> Result<Child, ProcessError> {
240 self.build().spawn().await
241 }
242
243 pub async fn output(self) -> Result<ProcessOutput, ProcessError> {
245 self.build().output().await
246 }
247}
248
249#[cfg(windows)]
253pub async fn shell(command: &str) -> Result<ProcessOutput, ProcessError> {
254 Command::new("cmd")
255 .args(["/C", command])
256 .output()
257 .await
258}
259
260#[cfg(unix)]
262pub async fn shell(command: &str) -> Result<ProcessOutput, ProcessError> {
263 Command::new("sh")
264 .args(["-c", command])
265 .output()
266 .await
267}
268
269pub async fn exec(program: &str, args: &[&str]) -> Result<String, ProcessError> {
271 let output = Command::new(program)
272 .args(args.iter().map(|s| s.to_string()))
273 .output()
274 .await?;
275
276 output.ensure_success()?;
277 output.stdout_string()
278}
279
280pub async fn which(program: &str) -> Option<PathBuf> {
282 #[cfg(windows)]
283 let result = Command::new("where")
284 .arg(program)
285 .output()
286 .await
287 .ok();
288
289 #[cfg(unix)]
290 let result = Command::new("which")
291 .arg(program)
292 .output()
293 .await
294 .ok();
295
296 result.and_then(|output| {
297 if output.success() {
298 output
299 .stdout_lossy()
300 .lines()
301 .next()
302 .map(|s| PathBuf::from(s.trim()))
303 } else {
304 None
305 }
306 })
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[tokio::test]
314 async fn test_command_builder() {
315 let cmd = Command::builder("echo")
316 .arg("hello")
317 .arg("world")
318 .build();
319
320 assert_eq!(cmd.program, "echo");
321 assert_eq!(cmd.args, vec!["hello", "world"]);
322 }
323
324 #[tokio::test]
325 async fn test_env() {
326 let cmd = Command::new("env")
327 .env("MY_VAR", "my_value");
328
329 assert_eq!(cmd.envs.get("MY_VAR"), Some(&"my_value".to_string()));
330 }
331
332 #[tokio::test]
333 async fn test_shell_command() {
334 #[cfg(windows)]
335 let output = shell("echo hello").await.unwrap();
336
337 #[cfg(unix)]
338 let output = shell("echo hello").await.unwrap();
339
340 assert!(output.success());
341 assert!(output.stdout_lossy().contains("hello"));
342 }
343
344 #[tokio::test]
345 async fn test_which() {
346 #[cfg(windows)]
348 let result = which("cmd").await;
349
350 #[cfg(unix)]
351 let result = which("sh").await;
352
353 assert!(result.is_some());
354 }
355
356 #[tokio::test]
357 async fn test_nonexistent_command() {
358 let result = Command::new("nonexistent_command_12345")
359 .output()
360 .await;
361
362 assert!(result.is_err());
363 }
364}