Skip to main content

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}