bare_script/sync/builder.rs
1//! Synchronous command execution module.
2//!
3//! This module provides a type-safe interface for executing shell commands
4//! using the standard library's `std::process::Command`.
5
6use std::ffi::OsStr;
7use std::io;
8use std::process::{Command, ExitStatus, Stdio};
9
10use crate::error::{ScriptError, ScriptResult};
11use crate::output::Output;
12
13/// Command builder for synchronous execution.
14#[derive(Debug)]
15pub struct CommandBuilder {
16 cmd: Command,
17}
18
19impl CommandBuilder {
20 /// Creates a new command builder for the specified program.
21 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
22 Self {
23 cmd: Command::new(program),
24 }
25 }
26
27 /// Adds a single argument to the command.
28 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
29 let _ = self.cmd.arg(arg);
30 self
31 }
32
33 /// Adds multiple arguments to the command.
34 pub fn args<I, S>(mut self, args: I) -> Self
35 where
36 I: IntoIterator<Item = S>,
37 S: AsRef<OsStr>,
38 {
39 let _ = self.cmd.args(args);
40 self
41 }
42
43 /// Sets an environment variable for the command.
44 pub fn env<K, V>(mut self, key: K, value: V) -> Self
45 where
46 K: AsRef<OsStr>,
47 V: AsRef<OsStr>,
48 {
49 let _ = self.cmd.env(key, value);
50 self
51 }
52
53 /// Sets multiple environment variables for the command.
54 pub fn envs<I, K, V>(mut self, vars: I) -> Self
55 where
56 I: IntoIterator<Item = (K, V)>,
57 K: AsRef<OsStr>,
58 V: AsRef<OsStr>,
59 {
60 let _ = self.cmd.envs(vars);
61 self
62 }
63
64 /// Clears all environment variables inherited from the parent process.
65 pub fn env_clear(mut self) -> Self {
66 let _ = self.cmd.env_clear();
67 self
68 }
69
70 /// Removes an environment variable from the command's environment.
71 pub fn env_remove<K>(mut self, key: K) -> Self
72 where
73 K: AsRef<OsStr>,
74 {
75 let _ = self.cmd.env_remove(key);
76 self
77 }
78
79 /// Sets the working directory for the command.
80 pub fn current_dir<D>(mut self, dir: D) -> Self
81 where
82 D: AsRef<std::path::Path>,
83 {
84 let _ = self.cmd.current_dir(dir);
85 self
86 }
87
88 /// Configures the stdin handle for the command.
89 pub fn stdin<T: Into<Stdio>>(mut self, stdin: T) -> Self {
90 let _ = self.cmd.stdin(stdin);
91 self
92 }
93
94 /// Configures the stdout handle for the command.
95 pub fn stdout<T: Into<Stdio>>(mut self, stdout: T) -> Self {
96 let _ = self.cmd.stdout(stdout);
97 self
98 }
99
100 /// Configures the stderr handle for the command.
101 pub fn stderr<T: Into<Stdio>>(mut self, stderr: T) -> Self {
102 let _ = self.cmd.stderr(stderr);
103 self
104 }
105
106 /// Configures the command to capture both stdout and stderr.
107 pub fn capture_output(mut self) -> Self {
108 let _ = self.cmd.stdout(Stdio::piped());
109 let _ = self.cmd.stderr(Stdio::piped());
110 self
111 }
112
113 /// Configures the command to inherit stdin from the parent process.
114 pub fn inherit_stdin(mut self) -> Self {
115 let _ = self.cmd.stdin(Stdio::inherit());
116 self
117 }
118
119 /// Configures the command to inherit stdout from the parent process.
120 pub fn inherit_stdout(mut self) -> Self {
121 let _ = self.cmd.stdout(Stdio::inherit());
122 self
123 }
124
125 /// Configures the command to inherit stderr from the parent process.
126 pub fn inherit_stderr(mut self) -> Self {
127 let _ = self.cmd.stderr(Stdio::inherit());
128 self
129 }
130
131 /// Configures the command to read from null stdin.
132 pub fn null_stdin(mut self) -> Self {
133 let _ = self.cmd.stdin(Stdio::null());
134 self
135 }
136
137 /// Configures the command to write to null stdout.
138 pub fn null_stdout(mut self) -> Self {
139 let _ = self.cmd.stdout(Stdio::null());
140 self
141 }
142
143 /// Configures the command to write to null stderr.
144 pub fn null_stderr(mut self) -> Self {
145 let _ = self.cmd.stderr(Stdio::null());
146 self
147 }
148
149 /// Sets the process group ID of the child process.
150 ///
151 /// A `pgroup` of 0 will cause the child to be placed in a new process group.
152 ///
153 /// # Errors
154 ///
155 /// Returns an error if the process group could not be set.
156 #[cfg(unix)]
157 pub fn process_group(mut self, pgroup: i32) -> Self {
158 use std::os::unix::process::CommandExt;
159 self.cmd.process_group(pgroup);
160 self
161 }
162
163 /// Sets whether the child process should create a new session.
164 ///
165 /// If `create` is `true`, the child process will call `setsid` to create
166 /// a new session and become the session leader.
167 ///
168 /// # Errors
169 ///
170 /// Returns an error if `setsid` fails.
171 #[cfg(unix)]
172 pub fn setsid(mut self, create: bool) -> Self {
173 use std::os::unix::process::CommandExt;
174 self.cmd.setsid(create);
175 self
176 }
177
178 /// Sets the child process's user ID.
179 ///
180 /// This translates to a `setuid` call in the child process.
181 /// Failure in the `setuid` call will cause the spawn to fail.
182 ///
183 /// # Errors
184 ///
185 /// Returns an error if the UID could not be set.
186 #[cfg(unix)]
187 pub fn uid(mut self, id: u32) -> Self {
188 use std::os::unix::process::CommandExt;
189 self.cmd.uid(id);
190 self
191 }
192
193 /// Sets the child process's group ID.
194 ///
195 /// This translates to a `setgid` call in the child process.
196 /// Failure in the `setgid` call will cause the spawn to fail.
197 ///
198 /// # Errors
199 ///
200 /// Returns an error if the GID could not be set.
201 #[cfg(unix)]
202 pub fn gid(mut self, id: u32) -> Self {
203 use std::os::unix::process::CommandExt;
204 self.cmd.gid(id);
205 self
206 }
207
208 /// Sets the process creation flags (Windows only).
209 ///
210 /// Sets the process creation flags to be passed to `CreateProcess`.
211 /// These will always be ORed with `CREATE_UNICODE_ENVIRONMENT`.
212 ///
213 /// # Errors
214 ///
215 /// This method does not return errors directly, but the spawned process
216 /// may fail if the flags are invalid.
217 #[cfg(windows)]
218 pub fn creation_flags(mut self, flags: u32) -> Self {
219 use std::os::windows::process::CommandExt;
220 let _ = self.cmd.creation_flags(flags);
221 self
222 }
223
224 /// Sets platform-specific creation flags (no-op on non-Windows).
225 ///
226 /// Note: On Unix, use `process_group`, `setsid`, `uid`, or `gid` instead.
227 #[cfg(not(windows))]
228 pub fn creation_flags(self, _flags: u32) -> Self {
229 self
230 }
231
232 /// Spawns the command as a child process.
233 pub fn spawn(&mut self) -> io::Result<std::process::Child> {
234 self.cmd.spawn()
235 }
236
237 /// Executes the command and captures its output.
238 pub fn output(&mut self) -> io::Result<Output> {
239 let output = self.cmd.output()?;
240 Ok(Output::new(output.stdout, output.stderr, output.status))
241 }
242
243 /// Executes the command and captures its output, returning a ScriptResult.
244 pub fn execute(&mut self) -> ScriptResult<Output> {
245 self.output().map_err(ScriptError::IoError)
246 }
247
248 /// Waits for the command to complete and returns its exit status.
249 pub fn status(&mut self) -> io::Result<ExitStatus> {
250 self.cmd.status()
251 }
252
253 /// Executes the command with a timeout.
254 ///
255 /// If the command does not complete within the specified duration,
256 /// the child process is killed and a timeout error is returned.
257 ///
258 /// Note: The builder is consumed by this operation and cannot be reused.
259 ///
260 /// # Errors
261 ///
262 /// Returns [`ScriptError::Timeout`] if the command exceeds the timeout.
263 /// Returns [`ScriptError::IoError`] if the command fails to start or execute.
264 pub fn execute_with_timeout(self, timeout: std::time::Duration) -> ScriptResult<Output> {
265 use std::sync::mpsc;
266 use std::thread;
267
268 let (tx, rx) = mpsc::channel();
269
270 // Move the builder into the thread
271 let mut builder = self;
272 let handle = thread::spawn(move || {
273 let result = builder.cmd.output();
274 drop(tx.send(result));
275 });
276 drop(handle);
277
278 match rx.recv_timeout(timeout) {
279 Ok(Ok(output)) => Ok(Output::new(output.stdout, output.stderr, output.status)),
280 Ok(Err(e)) => Err(ScriptError::IoError(e)),
281 Err(mpsc::RecvTimeoutError::Timeout) => Err(ScriptError::Timeout(timeout)),
282 Err(mpsc::RecvTimeoutError::Disconnected) => Err(ScriptError::Other(
283 "Thread disconnected unexpectedly".into(),
284 )),
285 }
286 }
287}