Skip to content

Commit b137220

Browse files
authored
Implement child extension traits (#11)
Closes #3
1 parent 589f52e commit b137220

File tree

12 files changed

+688
-104
lines changed

12 files changed

+688
-104
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ push = false # Don't do `git push`.
2424
publish = false # Don't do `cargo publish`.
2525

2626
[dependencies]
27+
dyn-clone = "1.0.17"
2728
shell-words = "1"
2829
tracing = { version = "0", optional = true }
2930
utf8-command = "1"

src/child_context.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::borrow::Borrow;
2+
use std::fmt::Debug;
3+
4+
#[cfg(doc)]
5+
use std::process::Child;
6+
#[cfg(doc)]
7+
use std::process::Command;
8+
9+
#[cfg(doc)]
10+
use crate::ChildExt;
11+
use crate::CommandDisplay;
12+
#[cfg(doc)]
13+
use crate::OutputContext;
14+
15+
/// A [`Child`] process combined with context about the [`Command`] that produced it.
16+
///
17+
/// The context information stored in this type is used to produce diagnostics in [`ChildExt`].
18+
///
19+
/// See: [`OutputContext`].
20+
pub struct ChildContext<C> {
21+
pub(crate) child: C,
22+
pub(crate) command: Box<dyn CommandDisplay>,
23+
}
24+
25+
impl<C> ChildContext<C> {
26+
/// Get the child process.
27+
pub fn into_child(self) -> C {
28+
self.child
29+
}
30+
31+
/// Get a reference to the child process.
32+
pub fn child(&self) -> &C {
33+
&self.child
34+
}
35+
36+
/// Get a mutable reference to the child process.
37+
pub fn child_mut(&mut self) -> &mut C {
38+
&mut self.child
39+
}
40+
41+
/// Get a reference to the command which produced this child process.
42+
pub fn command(&self) -> &dyn CommandDisplay {
43+
self.command.borrow()
44+
}
45+
}
46+
47+
impl<C> Debug for ChildContext<C>
48+
where
49+
C: Debug,
50+
{
51+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52+
f.debug_struct("ChildContext")
53+
.field("child", &self.child)
54+
.field("command", &self.command.to_string())
55+
.finish()
56+
}
57+
}

src/child_ext.rs

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
use std::borrow::Borrow;
2+
use std::fmt::Debug;
3+
use std::fmt::Display;
4+
use std::process::Child;
5+
use std::process::ExitStatus;
6+
use std::process::Output;
7+
8+
use utf8_command::Utf8Output;
9+
10+
use crate::ChildContext;
11+
#[cfg(doc)]
12+
use crate::CommandExt;
13+
14+
use crate::Error;
15+
use crate::ExecError;
16+
use crate::OutputContext;
17+
use crate::OutputConversionError;
18+
use crate::OutputLike;
19+
use crate::TryWaitContext;
20+
use 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`].
31+
pub 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,
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,
66+
O: 'static,
67+
E: Debug,
68+
E: Display,
69+
E: '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: '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: '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+
217+
impl 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,
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+
}

0 commit comments

Comments
 (0)