checked_command/
ext.rs

1use std::fmt::Debug;
2use std::io::Error as IoError;
3use std::io::Result as IoResult;
4use std::process::Output as StdOutput;
5use std::process::{Child, Command, ExitStatus};
6
7#[cfg(use_std_output)]
8pub type Output = StdOutput;
9
10#[cfg(not(use_std_output))]
11#[derive(Debug, Clone, Eq, PartialEq)]
12/// custom output type which, diferently to `std::process::Output` is not
13/// closely coupled with `ExitStatus` (it does not have a `status` field)
14pub struct Output {
15    /// the collected output of stdout as bytes
16    pub stdout: Vec<u8>,
17    /// the collected output of stderr as bytes
18    pub stderr: Vec<u8>,
19}
20
21#[cfg(not(use_std_output))]
22impl From<StdOutput> for Output {
23    fn from(out: StdOutput) -> Output {
24        Output {
25            stdout: out.stdout,
26            stderr: out.stderr,
27        }
28    }
29}
30
31quick_error! {
32
33    /// error type representing either a `io::Error` or a
34    /// failure caused by a non-successful exit status i.e.
35    /// without exit code or a exit code not equal zero.
36    #[derive(Debug)]
37    pub enum Error {
38        /// a `io::Error` occurred when handling the action
39        Io(err: IoError) {
40            from()
41            description(err.description())
42            cause(err)
43        }
44        /// Process exited with a non-zero exit code
45        Failure(ex: ExitStatus, output: Option<Output>) {
46            description("command failed with nonzero exit code")
47            display("command failed with exit code {}", ex.code()
48            .map(|code|code.to_string())
49            .unwrap_or_else(||"<None> possible terminated by signal".into()))
50        }
51    }
52}
53
54/// Extension to `std::process::Command` adding versions of the output/status
55/// functions which also fail/error with a non-success exit status
56pub trait CommandExt {
57    /// Behaves like `std::process::Command::output` but also checks the
58    /// exit status for success. The `Output` produced in case of a
59    /// command returning a failing exit status is included into the
60    /// error.
61    ///
62    /// # Error
63    ///
64    /// if the exit status is not successful or a `io::Error` was returned
65    /// from `Command::output`
66    ///
67    fn checked_output(&mut self) -> Result<Output, Error>;
68
69    /// Behaves like `std::process::Command::status` but also checks the
70    /// exit status for success, returning a error if it did not succeed
71    ///
72    /// # Error
73    ///
74    /// if the exit status is not successful or a `io::Error` was returned
75    /// from `Command::output`
76    ///
77    fn checked_status(&mut self) -> Result<(), Error>;
78}
79
80/// Extension to `std::process::Child` adding versions of the wait_with_output/wait
81/// functions which also fail/error with a non-success exit status
82pub trait ChildExt {
83    /// Behaves like `std::process::Child::wait_with_output` but also
84    /// checks the exit status for success. The `Output` produced in
85    /// case of a command returning a failing exit status is included
86    /// into the error.
87    ///
88    /// # Error
89    ///
90    /// if the exit status is not successful or a `io::Error` was returned
91    /// from `Child::wait_with_output`
92    ///
93    fn checked_wait_with_output(self) -> Result<Output, Error>;
94
95    /// Behaves like `std::process::Child::wait` but also checks the
96    /// exit status for success.
97    ///
98    /// # Error
99    ///
100    /// if the exit status is not successful or a `io::Error` was returned
101    /// from `Child::checked_wait`
102    ///
103    fn checked_wait(&mut self) -> Result<(), Error>;
104
105    /// Behaves like `std::process::child::try_wait` but also checks the
106    /// exit status for success.
107    ///
108    /// This means `Ok(true)` is returned if the process exited and
109    /// did with a successful exit status. If it did not exit jet `Ok(false)` is
110    /// returned. If it exited but had a non successful exit status `Err(StatusError)`
111    /// with the `StatusError::Failure` variant is returned.
112    ///
113    /// # Error
114    ///
115    /// if the exit status can be retrived but is not successful or
116    /// a `io::Error` was returned from `Child::checked_try_wait`
117    ///
118    /// # Example
119    ///
120    /// ```no_run
121    /// use std::process::Command;
122    /// use checked_command::{ChildExt, Error};
123    ///
124    ///
125    /// let mut child = Command::new("ls").spawn().unwrap();
126    ///
127    /// match child.checked_try_wait() {
128    ///     Ok(true) => println!("exited with successful status (== 0)"),
129    ///     Ok(false) => {
130    ///         println!("command still running, now waiting");
131    ///         let res = child.checked_wait();
132    ///         println!("command finished");
133    ///         println!("result: {:?}", res);
134    ///     }
135    ///     Err(Error::Io(e)) => println!("I/O error when attempting to wait for `ls` {}", e),
136    ///     Err(Error::Failure(exit_status, _)) => {
137    ///         println!("ls failed with exit code {:?}", exit_status.code())
138    ///     }
139    /// }
140    /// ```
141    #[cfg(feature = "process_try_wait")]
142    fn checked_try_wait(&mut self) -> Result<bool, Error>;
143}
144
145impl CommandExt for Command {
146    fn checked_output(&mut self) -> Result<Output, Error> {
147        convert_result(self.output())
148    }
149    fn checked_status(&mut self) -> Result<(), Error> {
150        convert_result(self.status())
151    }
152}
153
154impl ChildExt for Child {
155    fn checked_wait_with_output(self) -> Result<Output, Error> {
156        convert_result(self.wait_with_output())
157    }
158    fn checked_wait(&mut self) -> Result<(), Error> {
159        convert_result(self.wait())
160    }
161
162    #[cfg(feature = "process_try_wait")]
163    fn checked_try_wait(&mut self) -> Result<bool, Error> {
164        convert_result(self.try_wait())
165    }
166}
167
168/// internal trait to zero-cost abstract
169/// over handling `IoResult<Output>`` or
170/// `IoResult<ExitStatus>``. It's a bit
171/// over engineered but through zero-cost
172/// abstraction (Type Extensions+Inlining,
173/// ExitStatus is Copy) it should not
174/// introduce any overhead at runtime
175trait OutputOrExitStatus: Sized {
176    type Out;
177    fn use_ok_result(&self) -> bool;
178    fn create_error(self) -> Error;
179    fn convert(self) -> Self::Out;
180}
181
182#[cfg(feature = "process_try_wait")]
183impl OutputOrExitStatus for Option<ExitStatus> {
184    type Out = bool;
185
186    #[inline]
187    fn use_ok_result(&self) -> bool {
188        self.is_none() || self.unwrap().success()
189    }
190
191    #[inline]
192    fn create_error(self) -> Error {
193        // we can call unwrap as a None option won't lead to this branch
194        // as it is only called if there is a exit status (but possible no
195        // exit code nevertheless)
196        Error::Failure(self.unwrap(), None)
197    }
198
199    #[inline]
200    fn convert(self) -> bool {
201        self.is_some()
202    }
203}
204
205impl OutputOrExitStatus for ExitStatus {
206    type Out = ();
207
208    #[inline]
209    fn use_ok_result(&self) -> bool {
210        self.success()
211    }
212
213    #[inline]
214    fn create_error(self) -> Error {
215        Error::Failure(self, None)
216    }
217
218    #[inline]
219    fn convert(self) -> () {
220        ()
221    }
222}
223
224impl OutputOrExitStatus for StdOutput {
225    type Out = Output;
226
227    #[inline]
228    fn use_ok_result(&self) -> bool {
229        self.status.success()
230    }
231
232    #[inline]
233    fn create_error(self) -> Error {
234        // because of the abstraction we got a Option but we can relay on
235        // it to always be `Some(Output)` as long as this function is
236        // not exported
237        Error::Failure(self.status, Some(self.into()))
238    }
239
240    #[inline]
241    fn convert(self) -> Output {
242        self.into()
243    }
244}
245
246/// works with both Output and `ExitStatus`
247/// **without** introducing any clones or similar
248/// which would not have been needed for
249/// specialized methods
250fn convert_result<T>(result: IoResult<T>) -> Result<T::Out, Error>
251where
252    T: OutputOrExitStatus + Debug,
253{
254    match result {
255        Ok(think) => {
256            if think.use_ok_result() {
257                Ok(think.convert())
258            } else {
259                Err(think.create_error())
260            }
261        }
262        Err(io_error) => Err(io_error.into()),
263    }
264}
265
266#[cfg(test)]
267mod tests {
268
269    // this crate on itself is doesn't care about unix/windows,
270    // through the `from_raw` method is only aviable in the
271    // unix specific `ExitStatusExt`, therefore tests are
272    // only available on unix for now
273    #[cfg(unix)]
274    mod using_unix_exit_code_ext {
275
276        use super::super::{convert_result, Error, Output};
277        use std::error::Error as StdError;
278        use std::fmt;
279        use std::fmt::Write;
280        use std::io;
281        use std::os::unix::process::ExitStatusExt;
282        use std::process::ExitStatus;
283        use std::process::Output as StdOutput;
284
285        /// I will use this as a way to compare ExitStatus instances which do not
286        /// implement PartialEq. Note that this is quite brittle, through ok for
287        /// this contexts
288        pub fn assert_debugstr_eq<Type: fmt::Debug>(a: Type, b: Type) {
289            let mut buffer_a = String::new();
290            write!(&mut buffer_a, "{:?}", a).expect("debug fmt (a) failed");
291            let mut buffer_b = String::new();
292            write!(&mut buffer_b, "{:?}", b).expect("debug fmt (b) failed");
293
294            assert_eq!(buffer_a, buffer_b);
295        }
296
297        fn ok_exit_status() -> ExitStatus {
298            ExitStatus::from_raw(0)
299        }
300
301        #[cfg(not(target_os = "haiku"))]
302        fn fail_exit_status() -> ExitStatus {
303            ExitStatus::from_raw(2 << 8)
304        }
305
306        #[cfg(target_os = "haiku")]
307        fn fail_exit_status() -> ExitStatus {
308            ExitStatus::from_raw(2)
309        }
310
311        #[cfg(not(target_os = "haiku"))]
312        fn fail_exit_status_none() -> ExitStatus {
313            ExitStatus::from_raw(2)
314        }
315
316        #[cfg(target_os = "haiku")]
317        fn fail_exit_status_none() -> ExitStatus {
318            ExitStatus::from_raw(2 << 8)
319        }
320
321        fn create_output(ex: ExitStatus) -> StdOutput {
322            StdOutput {
323                status: ex,
324                stderr: vec![1, 2, 3],
325                stdout: vec![1, 2, 3],
326            }
327        }
328
329        #[test]
330        fn conv_result_status_ok() {
331            let res = convert_result(Ok(ok_exit_status()));
332            assert_debugstr_eq(Ok(()), res);
333        }
334
335        #[test]
336        fn conv_result_status_fail() {
337            let fail_status = fail_exit_status();
338            let res = convert_result(Ok(fail_status));
339            assert_debugstr_eq(Err(Error::Failure(fail_status, None)), res);
340        }
341
342        #[test]
343        fn conv_result_status_io_error() {
344            let ioerr = io::Error::new(io::ErrorKind::Other, "bla");
345            let ioerr2 = io::Error::new(io::ErrorKind::Other, "bla");
346            let res: Result<(), Error> = convert_result::<ExitStatus>(Err(ioerr));
347            assert_debugstr_eq(Err(Error::Io(ioerr2)), res)
348        }
349
350        #[test]
351        fn conv_result_output_ok() {
352            let out = create_output(ok_exit_status());
353            let out2 = out.clone();
354            assert_debugstr_eq(Ok(out2.into()), convert_result(Ok(out)));
355        }
356
357        #[test]
358        fn conv_result_output_fail() {
359            let fail_status = fail_exit_status();
360            let out = create_output(fail_status);
361            let out2 = out.clone();
362            assert_debugstr_eq(
363                Err(Error::Failure(fail_status, Some(out2.into()))),
364                convert_result(Ok(out)),
365            )
366        }
367
368        #[test]
369        fn conv_result_output_io_error() {
370            let ioerr = io::Error::new(io::ErrorKind::Other, "bla");
371            let ioerr2 = io::Error::new(io::ErrorKind::Other, "bla");
372            let res: Result<Output, Error> = convert_result::<StdOutput>(Err(ioerr));
373            assert_debugstr_eq(Err(Error::Io(ioerr2)), res)
374        }
375
376        #[cfg(feature = "process_try_wait")]
377        #[test]
378        fn conv_result_not_ready() {
379            match convert_result(Ok(None)) {
380                Ok(false) => {}
381                e => panic!("expected `Ok(false)` got `{:?}`", e),
382            }
383        }
384
385        #[cfg(feature = "process_try_wait")]
386        #[test]
387        fn conv_result_ready_ok() {
388            match convert_result(Ok(Some(ok_exit_status()))) {
389                Ok(true) => {}
390                e => panic!("expected `Ok(true)` got `{:?}`", e),
391            }
392        }
393
394        #[cfg(feature = "process_try_wait")]
395        #[test]
396        fn conv_result_ready_failure() {
397            let fail_status = fail_exit_status();
398            let res = convert_result(Ok(Some(fail_status)));
399            assert_debugstr_eq(Err(Error::Failure(fail_status, None)), res);
400        }
401
402        #[ignore = "broken due to deprecation of description changing io::Error::cause"]
403        #[allow(deprecated)]
404        #[test]
405        fn error_io_cause() {
406            let ioerr = || io::Error::new(io::ErrorKind::Other, "bla");
407            let err = Error::Io(ioerr());
408            assert_debugstr_eq(&ioerr() as &dyn StdError, err.cause().unwrap());
409        }
410
411        #[ignore = "broken due to deprecation of description changing io::Error::description"]
412        #[allow(deprecated)]
413        #[test]
414        fn error_io_description() {
415            let ioerr = io::Error::new(io::ErrorKind::Other, "bla");
416            let desc: String = ioerr.description().into();
417            let got: String = Error::Io(ioerr).description().into();
418            assert_eq!(desc, got);
419        }
420
421        #[test]
422        fn error_failure_display() {
423            let err = Error::Failure(fail_exit_status(), None);
424            assert_eq!(format!("{}", err), "command failed with exit code 2");
425        }
426
427        #[test]
428        fn error_failure_no_code_display() {
429            let err = Error::Failure(fail_exit_status_none(), None);
430            assert_eq!(
431                format!("{}", err),
432                "command failed with exit code <None> possible terminated by signal"
433            );
434        }
435
436        #[test]
437        fn from_raw_ok() {
438            let ex1 = ok_exit_status();
439            assert_eq!(true, ex1.success());
440            assert_eq!(Some(0), ex1.code());
441        }
442
443        #[test]
444        fn from_raw_fail() {
445            let ex1 = fail_exit_status();
446            assert_eq!(false, ex1.success());
447            assert_eq!(Some(2), ex1.code());
448        }
449
450        #[test]
451        fn from_raw_fail_none() {
452            let ex1 = fail_exit_status_none();
453            assert_eq!(false, ex1.success());
454            assert_eq!(None, ex1.code());
455        }
456    }
457}