just_run/lib.rs
1//! # just-run
2//!
3//! `just-run` is a simple convenience crate for executing system commands with the expectation
4//! of successful termination and UTF-8 encoded output. It aims to handle the most common case without
5//! extensive configuration or edge case coverage.
6//!
7//! ## Usage
8//! ```rust
9//! use just_run::{run, Success};
10//!
11//! let Success { stdout, stderr } = run("echo", ["Hello world!"]).expect("Command failed");
12//! println!("{stdout}");
13//! ```
14//!
15//! For more advanced use cases, consider checking out the [`duct`](https://crates.io/crates/duct) crate or
16//! using the standard library tools directly.
17//!
18//! ## Future
19//! This crate focuses on convenience. It might expand to include async command execution or other
20//! features in the future, but it is not meant to cover all use cases.
21
22#![cfg_attr(docsrs, feature(doc_auto_cfg))]
23
24/// Represents the successful UTF-8 encoded output of a command execution.
25///
26/// This struct is generally created by calling [`run`]. Please see the documentation of [`run`] for more details.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Success {
29 /// The standard output generated by the command.
30 pub stdout: String,
31
32 /// The standard error output generated by the command.
33 pub stderr: String,
34}
35
36/// Executes a system command with the specified arguments.
37///
38/// Executes the command as a child process, waiting for it to finish and
39/// collecting all of its output.
40///
41/// stdout and stderr are captured (and used to provide the resulting output).
42/// Stdin is not inherited from the parent and any attempt by the child process
43/// to read from the stdin stream will result in the stream immediately closing.
44///
45/// # Returns
46/// Returns the stdout and stderr output of the command if it terminates successfully
47/// and the output could be decoded as UTF-8.
48///
49/// # Example
50/// ```rust
51/// use just_run::{run, Success};
52///
53/// let Success { stdout, stderr } = match run("echo", ["Hello, World!"]) {
54/// Ok(success) => success,
55/// Err(err) => panic!("Error: {err}"),
56/// };
57/// ```
58///
59/// This function is intended for straightforward command execution scenarios. For
60/// more complex use cases or advanced features, consider using alternative crates
61/// or standard library functionality.
62pub fn run<Prog, Args, Arg>(program: Prog, args: Args) -> Result<Success, Error>
63where
64 Prog: AsRef<std::ffi::OsStr>,
65 Args: IntoIterator<Item = Arg>,
66 Arg: AsRef<std::ffi::OsStr>,
67{
68 let mut command = std::process::Command::new(program);
69 command.args(args);
70
71 execute(&mut command)
72}
73
74fn execute(cmd: &mut std::process::Command) -> Result<Success, Error> {
75 let std::process::Output { status, stdout, stderr } = cmd.output()?;
76 if !status.success() {
77 return Err(Error::UnsuccessfulTermination { status, stdout, stderr });
78 }
79
80 let stdout = String::from_utf8(stdout).map_err(Error::StdoutNotUtf8)?;
81 let stderr = String::from_utf8(stderr).map_err(Error::StderrNotUtf8)?;
82
83 Ok(Success { stdout, stderr })
84}
85
86/// Represents the errors that can occur during command execution.
87/// # Variants
88/// - `UnsuccessfulTermination { status, stdout, stderr }`: /// - `StdoutNotUtf8(std::string::FromUtf8Error)`: /// - `StderrNotUtf8(std::string::FromUtf8Error)`: Indicates that the `stderr` output could not be
89/// decoded as valid UTF-8.
90#[derive(Debug)]
91pub enum Error {
92 /// Input/output error encountered during command execution.
93 Io(std::io::Error),
94
95 /// Indicates that the command terminated unsuccessfully.
96 ///
97 /// Includes the exit status, and the raw captured `stdout` and `stderr`
98 /// output byte vectors.
99 UnsuccessfulTermination {
100 /// Command exit status.
101 status: std::process::ExitStatus,
102
103 /// Raw captured stdout byte vector.
104 stdout: Vec<u8>,
105
106 /// Raw captured stderr byte vector.
107 stderr: Vec<u8>,
108 },
109
110 /// Indicates that the `stdout` output could not be decoded as valid UTF-8.
111 StdoutNotUtf8(std::string::FromUtf8Error),
112
113 /// Indicates that the `stderr` output could not be decoded as valid UTF-8.
114 StderrNotUtf8(std::string::FromUtf8Error),
115}
116
117impl std::error::Error for Error {
118 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119 match self {
120 Error::Io(src) => Some(src),
121 Error::UnsuccessfulTermination { .. } => None,
122 Error::StdoutNotUtf8(src) => Some(src),
123 Error::StderrNotUtf8(src) => Some(src),
124 }
125 }
126}
127
128impl From<std::io::Error> for Error {
129 fn from(err: std::io::Error) -> Self {
130 Error::Io(err)
131 }
132}
133
134impl std::fmt::Display for Error {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 match self {
137 Error::Io(_) => write!(f, "IO error"),
138 Error::UnsuccessfulTermination { status, stdout: _, stderr } => {
139 let stderr = String::from_utf8_lossy(stderr);
140 let mut stderr_tail = stderr.lines().rev().take(10).collect::<Vec<_>>();
141 stderr_tail.reverse();
142 writeln!(f, "Unsuccessful termination with exit status {status} and stderr (last 10 lines):")?;
143 writeln!(f, "{}", stderr_tail.join("\n"))?;
144 Ok(())
145 }
146 Error::StdoutNotUtf8(_) => write!(f, "stdout is not valid UTF-8"),
147 Error::StderrNotUtf8(_) => write!(f, "stderr is not valid UTF-8"),
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use std::os::unix::process::ExitStatusExt;
156
157 /// Helper function to generate fake command execution output with a specified number of lines.
158 fn generate_lines(prefix: &str, lines: usize) -> Vec<u8> {
159 (1..=lines).map(|i| format!("{prefix} line {}\n", i)).collect::<String>().into_bytes()
160 }
161
162 #[test]
163 fn test_unsuccessful_termination_full_stderr() {
164 // Simulate a command with 12 lines of stderr output
165 let stderr = generate_lines("stderr", 12);
166 let error = Error::UnsuccessfulTermination {
167 status: std::process::ExitStatus::from_raw(1),
168 stdout: Vec::new(),
169 stderr: stderr.clone(),
170 };
171
172 let error_message = error.to_string();
173
174 let expected = indoc::indoc! {r#"
175 Unsuccessful termination with exit status signal: 1 (SIGHUP) and stderr (last 10 lines):
176 stderr line 3
177 stderr line 4
178 stderr line 5
179 stderr line 6
180 stderr line 7
181 stderr line 8
182 stderr line 9
183 stderr line 10
184 stderr line 11
185 stderr line 12
186 "#};
187 assert_eq!(error_message, expected);
188 }
189
190 #[test]
191 fn test_unsuccessful_termination_short_stderr() {
192 // Simulate a command with 4 lines of stderr output
193 let stderr = generate_lines("stderr", 4);
194 let error = Error::UnsuccessfulTermination {
195 status: std::process::ExitStatus::from_raw(1),
196 stdout: Vec::new(),
197 stderr: stderr.clone(),
198 };
199
200 let error_message = error.to_string();
201
202 let expected = indoc::indoc! {r#"
203 Unsuccessful termination with exit status signal: 1 (SIGHUP) and stderr (last 10 lines):
204 stderr line 1
205 stderr line 2
206 stderr line 3
207 stderr line 4
208 "#};
209 assert_eq!(error_message, expected);
210 }
211}