From 88980bc95be4351d753f91decfd39e77519b19e3 Mon Sep 17 00:00:00 2001 From: Jonathas-Conceicao Date: Thu, 30 Apr 2020 19:33:34 -0300 Subject: [PATCH] Fix badges Signed-off-by: Jonathas-Conceicao --- README.md | 2 +- README.tpl | 2 +- src/#lib.rs# | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/#lib.rs# diff --git a/README.md b/README.md index d60996b..1811ec2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Coverage Status](https://coveralls.io/repos/github/OSSystems/compress-tools-rs/badge.svg?branch=master)](https://coveralls.io/github/OSSystems/compress-tools-rs?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/OSSystems/easy-process-rs/badge.svg?branch=master)](https://coveralls.io/github/OSSystems/easy-process-rs?branch=master) [![Documentation](https://docs.rs/compress-tools/badge.svg)](https://docs.rs/compress-tools) # easy_process diff --git a/README.tpl b/README.tpl index 47261d1..0e5b5a5 100644 --- a/README.tpl +++ b/README.tpl @@ -1,4 +1,4 @@ -[![Coverage Status](https://coveralls.io/repos/github/OSSystems/compress-tools-rs/badge.svg?branch=master)](https://coveralls.io/github/OSSystems/compress-tools-rs?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/OSSystems/easy-process-rs/badge.svg?branch=master)](https://coveralls.io/github/OSSystems/easy-process-rs?branch=master) [![Documentation](https://docs.rs/compress-tools/badge.svg)](https://docs.rs/compress-tools) # {{crate}} diff --git a/src/#lib.rs# b/src/#lib.rs# new file mode 100644 index 0000000..e9b67da --- /dev/null +++ b/src/#lib.rs# @@ -0,0 +1,260 @@ +// Copyright (C) 2018 O.S. Systems Sofware LTDA +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications, + warnings +)] + +//! Allow running external commands and properly handle its success +//! and failures. +//! +//! | Platform | Build Status | +//! | -------- | ------------ | +//! | Linux | [![build status](https://github.com/OSSystems/easy-process-rs/workflows/CI%20(Linux)/badge.svg)](https://github.com/OSSystems/easy-process-rs/actions) | +//! | macOS | [![build status](https://github.com/OSSystems/easy-process-rs/workflows/CI%20(macOS)/badge.svg)](https://github.com/OSSystems/easy-process-rs/actions) | +//! | Windows | [![build status](https://github.com/OSSystems/easy-process-rs/workflows/CI%20(Windows)/badge.svg)](https://github.com/OSSystems/easy-process-rs/actions) | +//! +//! --- +//! +//! This creates provides a `run` function that does inline parsing of +//! literal command line strings (handling escape codes and splitting +//! at whitespace) and checks the `ExitStatus` of the command. If it +//! didn't succeed they will return a `Err(...)` instead of a +//! `Ok(...)`. +//! +//! Note that the provided functions do return their own `Output` +//! struct instead of [`std::process::Output`]. +//! +//! # Example +//! ```no_run +//! # fn run() -> Result<(), easy_process::Error> { +//! use easy_process; +//! +//! // stdout +//! let output = easy_process::run(r#"sh -c 'echo "1 2 3 4"'"#)?; +//! assert_eq!(&output.stdout, "1 2 3 4\n"); +//! +//! // stderr +//! let output = easy_process::run(r#"sh -c 'echo "1 2 3 4" >&2'"#)?; +//! assert_eq!(&output.stderr, "1 2 3 4\n"); +//! # Ok(()) +//! # } +//! # run(); +//! ``` +//! +//! [`std::process::Output`]: https://doc.rust-lang.org/std/process/struct.Output.html +//! +//! Commands on windows are also supported in the same way: +//! +//! ```no_run +//! # fn run() -> Result<(), easy_process::Error> { +//! let output = easy_process::run(r#"powershell /C 'echo "1 2 3 4"'"#)?; +//! assert_eq!(&output.stdout, "1 2 3 4\r\n"); +//! # Ok(()) +//! # } +//! # run(); +//! ``` + +use cmdline_words_parser::parse_posix; +use derive_more::{Display, Error, From}; +use std::{ + io, + process::{ChildStdin, ExitStatus, Stdio}, +}; + +#[derive(Debug, Default)] +/// Holds the output for a giving `easy_process::run` +pub struct Output { + /// The stdout output of the process + pub stdout: String, + /// The stderr output of the process + pub stderr: String, +} + +/// Error variant for `easy_process::run`. +#[derive(Display, Error, From, Debug)] +pub enum Error { + /// I/O error + #[display(fmt = "unexpected I/O Error: {}", _0)] + Io(io::Error), + /// Process error. It holds two parts: first argument is the exit + /// code and the second is the output (stdout and stderr). + #[display( + fmt = "status: {:?} stdout: {:?} stderr: {:?}", + "_0.code()", + "_1.stdout", + "_1.stderr" + )] + Failure(ExitStatus, Output), +} + +/// Result alias with crate's Error value +pub type Result = std::result::Result; + +impl From for Error { + fn from(error: checked_command::Error) -> Self { + match error { + checked_command::Error::Io(e) => Error::Io(e), + checked_command::Error::Failure(ex, err) => Error::Failure( + ex, + match err { + Some(e) => Output { + stdout: String::from_utf8_lossy(&e.stdout).to_string(), + stderr: String::from_utf8_lossy(&e.stderr).to_string(), + }, + None => Output::default(), + }, + ), + } + } +} + +/// Runs the given command +/// +/// # Arguments +/// +/// `cmd` - A string slice containing the command to be run. +/// +/// # Errors +/// +/// if the exit status is not successful or a `io::Error` was returned. +pub fn run(cmd: &str) -> Result { + let mut cmd = setup_process(cmd); + + let o = cmd.output()?; + Ok(Output { + stdout: String::from_utf8_lossy(&o.stdout).to_string(), + stderr: String::from_utf8_lossy(&o.stderr).to_string(), + }) +} + +/// Runs command with access to it's stdin. +/// +/// Spawns the given command then run it's piped stdin through the given +/// closure. The closure's Result Error type is used as the function's +/// result so the users can use their locally defined error types and +/// [easy_process::Error](Error) itself can also be used. +/// +/// # Examples +/// ```no_run +/// let output = easy_process::run_with_stdin("rev", |stdin| { +/// std::io::Write::write_all(stdin, b"Hello, world!")?; +/// easy_process::Result::Ok(()) +/// }) +/// .unwrap(); +/// assert_eq!("!dlrow ,olleH", &output.stdout); +/// ``` +pub fn run_with_stdin(cmd: &str, f: F) -> std::result::Result +where + F: FnOnce(&mut ChildStdin) -> std::result::Result<(), E>, + E: From, +{ + let mut cmd = setup_process(cmd); + // both pipes must be set in order to obtain the output later + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()); + let mut child = cmd.spawn().map_err(Error::from).unwrap(); + let stdin = child.stdin().as_mut().unwrap(); + + f(stdin)?; + + let o = child.wait_with_output().map_err(Error::from).unwrap(); + Ok(Output { + stdout: String::from_utf8_lossy(&o.stdout).to_string(), + stderr: String::from_utf8_lossy(&o.stderr).to_string(), + }) +} + +fn setup_process(cmd: &str) -> checked_command::CheckedCommand { + let mut cmd = cmd.to_string(); + let mut args = parse_posix(&mut cmd); + + let mut p = checked_command::CheckedCommand::new(args.next().unwrap()); + p.args(args); + p +} + +#[cfg(all(test, not(windows)))] +mod tests { + use super::*; + + #[test] + fn failing_command() { + // failing command with exit status 1 + match run(r#"sh -c 'echo "error" >&2; exit 1'"#) { + Ok(_) => panic!("call should have failed"), + Err(Error::Io(io_err)) => panic!("unexpected I/O Error: {:?}", io_err), + Err(Error::Failure(ex, output)) => { + assert_eq!(ex.code().unwrap(), 1); + assert_eq!(&output.stderr, "error\n"); + } + } + } + + #[test] + fn success_command() { + // failing command with exit status 1 + match run(r#"sh -c 'echo "ok" && exit 0'"#) { + Ok(output) => assert_eq!(&output.stdout, "ok\n"), + Err(e) => panic!("unexpected error: {:?}", e), + } + } + + #[test] + fn piped_input() { + let output = run_with_stdin("rev", |stdin| { + io::Write::write_all(stdin, b"Hello, world!")?; + Result::Ok(()) + }) + .unwrap(); + // Older versions of rev will add an new line terminator + // so we test only the start of the stdout + assert!(&output.stdout.starts_with("!dlrow ,olleH")); + } +} + +#[cfg(all(test, windows))] +mod tests { + use super::*; + + #[test] + fn failing_command() { + // failing command with exit status 1 + match run(r#"powershell /C '[Console]::Error.WriteLine("Error"); exit(1)'"#) { + Ok(_) => panic!("call should have failed"), + Err(Error::Io(io_err)) => panic!("unexpected I/O Error: {:?}", io_err), + Err(Error::Failure(ex, output)) => { + assert_eq!(ex.code().unwrap(), 1); + assert_eq!(&output.stderr, "Error\r\n"); + } + } + } + + #[test] + fn success_command() { + // failing command with exit status 1 + match run(r#"powershell /C "echo 1 2 3 4""#) { + Ok(output) => assert_eq!(&output.stdout, "1\r\n2\r\n3\r\n4\r\n"), + Err(e) => panic!("unexpected error: {:?}", e), + } + } + + #[test] + fn piped_input() { + let output = run_with_stdin("cat", |stdin| { + io::Write::write_all(stdin, b"echo 4 3 2 1")?; + Result::Ok(()) + }) + .unwrap(); + assert_eq!("1 2 3 4\r\n", &output.stdout); + } +}