command_error/
child_ext.rs

1use std::borrow::Borrow;
2use std::fmt::Debug;
3use std::fmt::Display;
4use std::process::Child;
5use std::process::ExitStatus;
6use std::process::Output;
7
8use utf8_command::Utf8Output;
9
10use crate::ChildContext;
11#[cfg(doc)]
12use crate::CommandExt;
13
14use crate::Error;
15use crate::ExecError;
16use crate::OutputContext;
17use crate::OutputConversionError;
18use crate::OutputLike;
19use crate::TryWaitContext;
20use crate::WaitError;
21
22/// Checked methods for [`Child`] processes.
23///
24/// This trait is largely the same as [`CommandExt`], with the difference that the
25/// [`ChildExt::output_checked`] methods take `self` as an owned parameter and the
26/// [`CommandExt::output_checked`] methods take `self` as a mutable reference.
27///
28/// Additionally, methods that return an [`ExitStatus`] are named
29/// [`wait_checked`][`ChildExt::wait_checked`] instead of
30/// [`status_checked`][`CommandExt::status_checked`], to match the method names on [`Child`].
31pub trait ChildExt: Sized {
32    /// The error type returned from methods on this trait.
33    type Error: From<Error>;
34
35    /// Wait for the process to complete, capturing its output. `succeeded` is called and returned
36    /// to determine if the command succeeded.
37    ///
38    /// See [`CommandExt::output_checked_as`] for more information.
39    #[track_caller]
40    fn output_checked_as<O, R, E>(
41        self,
42        succeeded: impl Fn(OutputContext<O>) -> Result<R, E>,
43    ) -> Result<R, E>
44    where
45        O: Debug,
46        O: OutputLike,
47        O: 'static,
48        O: TryFrom<Output>,
49        <O as TryFrom<Output>>::Error: Display + Send + Sync,
50        E: From<Self::Error>;
51
52    /// Wait for the process to complete, capturing its output. `succeeded` is called and used to
53    /// determine if the command succeeded and (optionally) to add an additional message to the error returned.
54    ///
55    /// See [`CommandExt::output_checked_with`] and [`Child::wait_with_output`] for more information.
56    #[track_caller]
57    fn output_checked_with<O, E>(
58        self,
59        succeeded: impl Fn(&O) -> Result<(), Option<E>>,
60    ) -> Result<O, Self::Error>
61    where
62        O: Debug,
63        O: OutputLike,
64        O: TryFrom<Output>,
65        <O as TryFrom<Output>>::Error: Display + Send + Sync,
66        O: Send + Sync + 'static,
67        E: Debug,
68        E: Display,
69        E: Send + Sync + 'static,
70    {
71        self.output_checked_as(|context| match succeeded(context.output()) {
72            Ok(()) => Ok(context.into_output()),
73            Err(user_error) => Err(context.maybe_error_msg(user_error).into()),
74        })
75    }
76
77    /// Wait for the process to complete, capturing its output. If the command exits with a
78    /// non-zero exit code, an error is raised.
79    ///
80    /// See [`CommandExt::output_checked`] and [`Child::wait_with_output`] for more information.
81    #[track_caller]
82    fn output_checked(self) -> Result<Output, Self::Error> {
83        self.output_checked_with(|output: &Output| {
84            if output.status.success() {
85                Ok(())
86            } else {
87                Err(None::<String>)
88            }
89        })
90    }
91
92    /// Wait for the process to exit, capturing its output and decoding it as UTF-8. If the command
93    /// exits with a non-zero exit code, an error is raised.
94    ///
95    /// See [`CommandExt::output_checked_utf8`] and [`Child::wait_with_output`] for more information.
96    #[track_caller]
97    fn output_checked_utf8(self) -> Result<Utf8Output, Self::Error> {
98        self.output_checked_with_utf8(|output| {
99            if output.status.success() {
100                Ok(())
101            } else {
102                Err(None::<String>)
103            }
104        })
105    }
106
107    /// Wait for the process to exit, capturing its output and decoding it as UTF-8. `succeeded` is
108    /// called and used to determine if the command succeeded and (optionally) to add an additional
109    /// message to the error returned.
110    ///
111    /// See [`CommandExt::output_checked_with_utf8`] and [`Child::wait_with_output`] for more information.
112    #[track_caller]
113    fn output_checked_with_utf8<E>(
114        self,
115        succeeded: impl Fn(&Utf8Output) -> Result<(), Option<E>>,
116    ) -> Result<Utf8Output, Self::Error>
117    where
118        E: Display,
119        E: Debug,
120        E: Send + Sync + 'static,
121    {
122        self.output_checked_with(succeeded)
123    }
124
125    /// Check if the process has exited.
126    ///
127    /// The `succeeded` closure is called and returned to determine the result.
128    ///
129    /// Errors while attempting to retrieve the process's exit status are returned as
130    /// [`WaitError`]s.
131    ///
132    /// See [`Child::try_wait`] for more information.
133    #[track_caller]
134    fn try_wait_checked_as<R, E>(
135        &mut self,
136        succeeded: impl Fn(TryWaitContext) -> Result<R, E>,
137    ) -> Result<R, E>
138    where
139        E: From<Self::Error>;
140
141    /// Check if the process has exited and, if it failed, return an error.
142    ///
143    /// Errors while attempting to retrieve the process's exit status are transformed into
144    /// [`WaitError`]s.
145    ///
146    /// See [`Child::try_wait`] for more information.
147    #[track_caller]
148    fn try_wait_checked(&mut self) -> Result<Option<ExitStatus>, Self::Error> {
149        self.try_wait_checked_as(|context| match context.into_output_context() {
150            Some(context) => {
151                if context.status().success() {
152                    Ok(Some(context.status()))
153                } else {
154                    Err(context.error().into())
155                }
156            }
157            None => Ok(None),
158        })
159    }
160
161    /// Wait for the process to exit. `succeeded` is called and returned to determine
162    /// if the command succeeded.
163    ///
164    /// See [`CommandExt::status_checked_as`] and [`Child::wait`] for more information.
165    #[track_caller]
166    fn wait_checked_as<R, E>(
167        &mut self,
168        succeeded: impl Fn(OutputContext<ExitStatus>) -> Result<R, E>,
169    ) -> Result<R, E>
170    where
171        E: From<Self::Error>;
172
173    /// Wait for the process to exit. `succeeded` is called and used to determine
174    /// if the command succeeded and (optionally) to add an additional message to the error
175    /// returned.
176    ///
177    /// See [`CommandExt::status_checked_with`] and [`Child::wait`] for more information.
178    #[track_caller]
179    fn wait_checked_with<E>(
180        &mut self,
181        succeeded: impl Fn(ExitStatus) -> Result<(), Option<E>>,
182    ) -> Result<ExitStatus, Self::Error>
183    where
184        E: Debug,
185        E: Display,
186        E: Send + Sync + 'static,
187    {
188        self.wait_checked_as(|context| match succeeded(context.status()) {
189            Ok(()) => Ok(context.status()),
190            Err(user_error) => Err(context.maybe_error_msg(user_error).into()),
191        })
192    }
193
194    /// Wait for the process to exit. If the command exits with a non-zero status
195    /// code, an error is raised containing information about the command that was run.
196    ///
197    /// See [`CommandExt::status_checked`] and [`Child::wait`] for more information.
198    #[track_caller]
199    fn wait_checked(&mut self) -> Result<ExitStatus, Self::Error> {
200        self.wait_checked_with(|status| {
201            if status.success() {
202                Ok(())
203            } else {
204                Err(None::<String>)
205            }
206        })
207    }
208
209    /// Log the command that will be run.
210    ///
211    /// With the `tracing` feature enabled, this will emit a debug-level log with message
212    /// `Executing command` and a `command` field containing the displayed command (by default,
213    /// shell-quoted).
214    fn log(&self) -> Result<(), Self::Error>;
215}
216
217impl ChildExt for ChildContext<Child> {
218    type Error = Error;
219
220    fn output_checked_as<O, R, E>(
221        self,
222        succeeded: impl Fn(OutputContext<O>) -> Result<R, E>,
223    ) -> Result<R, E>
224    where
225        O: Debug,
226        O: OutputLike,
227        O: 'static,
228        O: TryFrom<Output>,
229        <O as TryFrom<Output>>::Error: Display + Send + Sync,
230        E: From<Self::Error>,
231    {
232        self.log()?;
233        let command = dyn_clone::clone_box(self.command.borrow());
234        match self.child.wait_with_output() {
235            Ok(output) => match output.try_into() {
236                Ok(output) => succeeded(OutputContext { output, command }),
237                Err(error) => Err(Error::from(OutputConversionError {
238                    command,
239                    inner: Box::new(error),
240                })
241                .into()),
242            },
243            Err(inner) => Err(Error::from(ExecError { command, inner }).into()),
244        }
245    }
246
247    fn try_wait_checked_as<R, E>(
248        &mut self,
249        succeeded: impl Fn(TryWaitContext) -> Result<R, E>,
250    ) -> Result<R, E>
251    where
252        E: From<Self::Error>,
253    {
254        let command = dyn_clone::clone_box(self.command.borrow());
255        match self.child.try_wait() {
256            Ok(status) => succeeded(TryWaitContext { status, command }),
257            Err(inner) => Err(Error::from(WaitError { inner, command }).into()),
258        }
259    }
260
261    fn wait_checked_as<R, E>(
262        &mut self,
263        succeeded: impl Fn(OutputContext<ExitStatus>) -> Result<R, E>,
264    ) -> Result<R, E>
265    where
266        E: From<Self::Error>,
267    {
268        self.log()?;
269        let command = dyn_clone::clone_box(self.command.borrow());
270        match self.child.wait() {
271            Ok(status) => succeeded(OutputContext {
272                output: status,
273                command,
274            }),
275            Err(inner) => Err(Error::from(ExecError { command, inner }).into()),
276        }
277    }
278
279    fn log(&self) -> Result<(), Self::Error> {
280        #[cfg(feature = "tracing")]
281        {
282            tracing::debug!(command = %self.command, "Executing command");
283        }
284        Ok(())
285    }
286}