├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── buffer.rs ├── gag.rs ├── hold.rs ├── lib.rs └── redirect.rs └── tests └── test_redirect.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | - beta 5 | - stable 6 | os: 7 | - linux 8 | - osx 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gag" 3 | version = "1.0.0" 4 | authors = ["Steven Allen "] 5 | description = "Gag, redirect, or hold stdout/stderr output. Currently only *nix operating systems are supported." 6 | 7 | documentation = "https://docs.rs/gag/" 8 | repository = "https://github.com/Stebalien/gag-rs" 9 | keywords = ["stdout", "stderr", "stdio", "redirect"] 10 | edition = "2018" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | tempfile = "3.0" 15 | filedescriptor = "0.8.0" 16 | 17 | [dev-dependencies] 18 | lazy_static = "1" 19 | dirs = "3.0.2" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Steven Allen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redirect and/or gag stdout/stderr. 2 | 3 | [![Build Status](https://travis-ci.org/Stebalien/gag-rs.svg?branch=master)](https://travis-ci.org/Stebalien/gag-rs) 4 | 5 | Documentation (with examples): https://docs.rs/gag/ 6 | 7 | # Limitations 8 | 9 | * Won't work if something else has called `std::io::set_print` (currently 10 | unstable). Unfortunately, this function doesn't actually redirect the stdio 11 | file descriptor, it just replaces the `std::io::stdout` writer. 12 | * Won't work in rust test cases. The rust test cases use `std::io::set_print` to 13 | redirect stdout. You can get around this though by using the `--nocapture` argument 14 | when running your tests. 15 | 16 | # TODO: 17 | 18 | * General: 19 | * Better error handling? 20 | * Redirect: 21 | * Be generic over references. That is, accept both a reference to an AsRawFd or 22 | an AsRawFd. Unfortunately, I don't know if this is even possible. Borrow 23 | doesn't work because I really want the following constraint: 24 | `impl Redirect where F: BorrowMut, T: AsMut` so I can write 25 | `file.borrow_mut().as_mut()` but that would be ambiguous... 26 | * Buffer: 27 | * Deallocate the buffer as it is read (FALLOC_FL_PUNCH_HOLE) if possible. 28 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, Read}; 3 | 4 | use crate::redirect::Redirect; 5 | use tempfile::NamedTempFile; 6 | 7 | /// Buffer output in an in-memory buffer. 8 | pub struct BufferRedirect { 9 | #[allow(dead_code)] 10 | redir: Redirect, 11 | outer: File, 12 | } 13 | 14 | /// An in-memory read-only buffer into which BufferRedirect buffers output. 15 | pub struct Buffer(File); 16 | 17 | impl Read for Buffer { 18 | #[inline(always)] 19 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 20 | self.0.read(buf) 21 | } 22 | } 23 | 24 | impl BufferRedirect { 25 | /// Buffer stdout. 26 | pub fn stdout() -> io::Result { 27 | let tempfile = NamedTempFile::new()?; 28 | let inner = tempfile.reopen()?; 29 | let outer = tempfile.reopen()?; 30 | let redir = Redirect::stdout(inner)?; 31 | Ok(BufferRedirect { redir, outer }) 32 | } 33 | /// Buffer stderr. 34 | pub fn stderr() -> io::Result { 35 | let tempfile = NamedTempFile::new()?; 36 | let inner = tempfile.reopen()?; 37 | let outer = tempfile.reopen()?; 38 | let redir = Redirect::stderr(inner)?; 39 | Ok(BufferRedirect { redir, outer }) 40 | } 41 | 42 | /// Extract the inner buffer and stop redirecting output. 43 | pub fn into_inner(self) -> Buffer { 44 | Buffer(self.outer) 45 | } 46 | } 47 | 48 | impl Read for BufferRedirect { 49 | #[inline(always)] 50 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 51 | self.outer.read(buf) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/gag.rs: -------------------------------------------------------------------------------- 1 | use crate::redirect::Redirect; 2 | use std::fs::File; 3 | use std::fs::OpenOptions; 4 | use std::io; 5 | 6 | // Helper function for opening /dev/null in unix or NUL on windows 7 | fn null() -> io::Result { 8 | #[cfg(windows)] 9 | return OpenOptions::new().write(true).open("NUL"); 10 | 11 | #[cfg(unix)] 12 | return OpenOptions::new().write(true).open("/dev/null"); 13 | } 14 | 15 | /// Discard output until dropped. 16 | pub struct Gag(Redirect); 17 | 18 | impl Gag { 19 | /// Discard stdout until dropped. 20 | pub fn stdout() -> io::Result { 21 | Ok(Gag(Redirect::stdout(null()?)?)) 22 | } 23 | /// Discard stderr until dropped. 24 | pub fn stderr() -> io::Result { 25 | Ok(Gag(Redirect::stderr(null()?)?)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/hold.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Write}; 2 | use crate::BufferRedirect; 3 | 4 | /// Hold output until dropped. On drop, the held output is sent to the stdout/stderr. 5 | /// 6 | /// Note: This will ignore IO errors when printing held output. 7 | pub struct Hold { 8 | buf_redir: Option, 9 | is_stdout: bool, 10 | } 11 | 12 | impl Hold { 13 | /// Hold stderr output. 14 | pub fn stderr() -> io::Result { 15 | Ok(Hold { 16 | buf_redir: Some(BufferRedirect::stderr()?), 17 | is_stdout: false, 18 | }) 19 | } 20 | 21 | /// Hold stdout output. 22 | pub fn stdout() -> io::Result { 23 | Ok(Hold { 24 | buf_redir: Some(BufferRedirect::stdout()?), 25 | is_stdout: true, 26 | }) 27 | } 28 | } 29 | 30 | impl Drop for Hold { 31 | fn drop(&mut self) { 32 | fn read_into(mut from: R, mut to: W) { 33 | // TODO: use sendfile? 34 | let mut buf = [0u8; 4096]; 35 | loop { 36 | // Ignore errors 37 | match from.read(&mut buf) { 38 | Ok(0) => break, 39 | Ok(size) => { 40 | if to.write_all(&buf[..size]).is_err() { 41 | break; 42 | } 43 | } 44 | Err(_) => break, 45 | } 46 | } 47 | // Just in case... 48 | let _ = to.flush(); 49 | } 50 | 51 | let from = self.buf_redir.take().unwrap().into_inner(); 52 | // Ignore errors. 53 | if self.is_stdout { 54 | let stdout = io::stdout(); 55 | read_into(from, stdout.lock()); 56 | } else { 57 | let stderr = io::stderr(); 58 | read_into(from, stderr.lock()); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Temporarily redirect stdout/stderr. 2 | //! 3 | //! For example, one can temporarily throw away stdout output: 4 | //! 5 | //! ``` 6 | //! use gag::Gag; 7 | //! println!("Hello world!"); 8 | //! { 9 | //! let print_gag = Gag::stdout().unwrap(); 10 | //! println!("No one will see this!"); 11 | //! println!("Or this!"); 12 | //! } 13 | //! println!("But they will see this!"); 14 | //! ``` 15 | //! 16 | //! You can also temporarily un-gag by dropping the gag: 17 | //! 18 | //! ``` 19 | //! use gag::Gag; 20 | //! let mut print_gag = Gag::stdout().unwrap(); 21 | //! println!("No one will see this!"); 22 | //! if true { 23 | //! drop(print_gag); 24 | //! println!("They will see this..."); 25 | //! print_gag = Gag::stdout().unwrap(); 26 | //! println!("Not this..."); 27 | //! } 28 | //! println!("Nor this."); 29 | //! ``` 30 | //! 31 | //! However, you can't open multiple Gags/Redirects/Holds for the same output at once: 32 | //! 33 | //! ``` 34 | //! use gag::Gag; 35 | //! // Multiple stdout gags 36 | //! let gag_a = Gag::stdout().unwrap(); 37 | //! let gag_b_result = Gag::stdout(); 38 | //! assert!(gag_b_result.is_err()); 39 | //! assert_eq!(gag_b_result.err().expect("Expected an error").kind(), 40 | //! std::io::ErrorKind::AlreadyExists); 41 | //! 42 | //! // However, you can still gag stderr: 43 | //! let gag_c = Gag::stderr().unwrap(); 44 | //! ``` 45 | //! 46 | //! If you don't want to throw away stdout, you can write it to a file: 47 | //! 48 | //! ``` 49 | //! # extern crate dirs; 50 | //! use std::fs::OpenOptions; 51 | //! use std::io::{Read, Write, Seek, SeekFrom}; 52 | //! use gag::Redirect; 53 | //! use dirs::data_local_dir; 54 | //! 55 | //! fn get_temp_filepath() -> String { 56 | //! #[cfg(windows)] 57 | //! return data_local_dir() 58 | //! .unwrap() 59 | //! .join("Temp") 60 | //! .join("my_log.log") 61 | //! .to_string_lossy() 62 | //! .into(); 63 | //! 64 | //! #[cfg(unix)] 65 | //! return "/tmp/my_log.log".into(); 66 | //! } 67 | //! 68 | //! println!("Displayed"); 69 | //! 70 | //! // Open a log 71 | //! let log = OpenOptions::new() 72 | //! .truncate(true) 73 | //! .read(true) 74 | //! .create(true) 75 | //! .write(true) 76 | //! .open(get_temp_filepath()) 77 | //! .unwrap(); 78 | //! 79 | //! let print_redirect = Redirect::stdout(log).unwrap(); 80 | //! println!("Hidden"); 81 | //! 82 | //! // Extract redirect 83 | //! let mut log = print_redirect.into_inner(); 84 | //! println!("Displayed"); 85 | //! 86 | //! let mut buf = String::new(); 87 | //! log.seek(SeekFrom::Start(0)).unwrap(); 88 | //! log.read_to_string(&mut buf).unwrap(); 89 | //! assert_eq!(&buf[..], "Hidden\n"); 90 | //! 91 | //! ``` 92 | //! 93 | //! Alternatively, you can buffer stdout to a temporary file. On linux 3.11+, this file is 94 | //! guarenteed to be stored in-memory. 95 | //! 96 | //! ``` 97 | //! use std::io::Read; 98 | //! use gag::BufferRedirect; 99 | //! 100 | //! let mut buf = BufferRedirect::stdout().unwrap(); 101 | //! println!("Hello world!"); 102 | //! 103 | //! let mut output = String::new(); 104 | //! buf.read_to_string(&mut output).unwrap(); 105 | //! 106 | //! assert_eq!(&output[..], "Hello world!\n"); 107 | //! ``` 108 | //! 109 | //! Finally, if you just want to temporarily hold std output, you can use `Hold` to hold the output 110 | //! until dropped: 111 | //! 112 | //! ``` 113 | //! use gag::Hold; 114 | //! 115 | //! let hold = Hold::stdout().unwrap(); 116 | //! println!("first"); 117 | //! println!("second"); 118 | //! drop(hold); // printing happens here! 119 | //! ``` 120 | extern crate filedescriptor; 121 | extern crate tempfile; 122 | 123 | mod buffer; 124 | mod gag; 125 | mod hold; 126 | mod redirect; 127 | 128 | pub use crate::buffer::{Buffer, BufferRedirect}; 129 | pub use crate::gag::Gag; 130 | pub use crate::hold::Hold; 131 | pub use crate::redirect::{Redirect, RedirectError}; 132 | -------------------------------------------------------------------------------- /src/redirect.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::io; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | use filedescriptor::{AsRawFileDescriptor, FileDescriptor, StdioDescriptor}; 6 | 7 | static REDIRECT_FLAGS: [AtomicBool; 3] = [ 8 | AtomicBool::new(false), 9 | AtomicBool::new(false), 10 | AtomicBool::new(false), 11 | ]; 12 | 13 | pub struct RedirectError { 14 | pub error: io::Error, 15 | pub file: F, 16 | } 17 | 18 | impl From> for io::Error { 19 | fn from(err: RedirectError) -> io::Error { 20 | err.error 21 | } 22 | } 23 | 24 | impl ::std::error::Error for RedirectError { 25 | fn description(&self) -> &str { 26 | #[allow(deprecated)] // for backwards compat. 27 | self.error.description() 28 | } 29 | fn source(&self) -> Option<&(dyn std::error::Error+'static)> { 30 | Some(&self.error) 31 | } 32 | } 33 | 34 | impl ::std::fmt::Display for RedirectError { 35 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 36 | self.error.fmt(fmt) 37 | } 38 | } 39 | 40 | impl ::std::fmt::Debug for RedirectError { 41 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 42 | self.error.fmt(fmt) 43 | } 44 | } 45 | 46 | /// Redirect stderr/stdout to a file. 47 | pub struct Redirect { 48 | #[allow(dead_code)] 49 | fds: RedirectFds, 50 | file: F, 51 | } 52 | 53 | // Separate struct so we can destruct the redirect and drop the file descriptors. 54 | struct RedirectFds { 55 | std_fd: FileDescriptor, 56 | stdio: StdioDescriptor, 57 | } 58 | 59 | impl RedirectFds { 60 | fn make(file: &F, stdio: StdioDescriptor) -> io::Result { 61 | if REDIRECT_FLAGS[stdio as usize].fetch_or(true, Ordering::Relaxed) { 62 | return Err(io::Error::new( 63 | io::ErrorKind::AlreadyExists, 64 | "Redirect already exists.", 65 | )); 66 | } 67 | 68 | let std_fd = FileDescriptor::redirect_stdio(file, stdio) 69 | .map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string()))?; 70 | 71 | // Dropping this will redirect stdio back to original std_fd 72 | Ok(RedirectFds { std_fd, stdio }) 73 | } 74 | } 75 | 76 | impl Drop for RedirectFds { 77 | fn drop(&mut self) { 78 | let _ = FileDescriptor::redirect_stdio(&self.std_fd, self.stdio); 79 | REDIRECT_FLAGS[self.stdio as usize].store(false, Ordering::Relaxed); 80 | } 81 | } 82 | 83 | impl Redirect 84 | where 85 | F: AsRawFileDescriptor, 86 | { 87 | fn make(file: F, stdio: StdioDescriptor) -> Result> { 88 | let fds = match RedirectFds::make(&file, stdio) { 89 | Ok(fds) => fds, 90 | Err(error) => return Err(RedirectError { error, file }), 91 | }; 92 | Ok(Redirect { fds, file }) 93 | } 94 | /// Redirect stdout to `file`. 95 | pub fn stdout(file: F) -> Result> { 96 | Redirect::make(file, StdioDescriptor::Stdout) 97 | } 98 | /// Redirect stderr to `file`. 99 | pub fn stderr(file: F) -> Result> { 100 | Redirect::make(file, StdioDescriptor::Stderr) 101 | } 102 | 103 | /// Extract inner file object. 104 | pub fn into_inner(self) -> F { 105 | self.file 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/test_redirect.rs: -------------------------------------------------------------------------------- 1 | extern crate gag; 2 | #[macro_use] 3 | extern crate lazy_static; 4 | 5 | use gag::{BufferRedirect, Hold}; 6 | use std::io::{Read, Write}; 7 | use std::sync::Mutex; 8 | 9 | lazy_static! { 10 | static ref STDERR_MUTEX: Mutex<()> = Mutex::new(()); 11 | } 12 | 13 | // Catch the cases not covered by the doc tests. 14 | 15 | #[test] 16 | fn test_buffer_stderr() { 17 | let _l = STDERR_MUTEX.lock().unwrap(); 18 | 19 | let mut buf = BufferRedirect::stderr().unwrap(); 20 | println!("Don't capture"); 21 | ::std::io::stderr().write_all(b"Hello world!\n").unwrap(); 22 | 23 | let mut output = String::new(); 24 | buf.read_to_string(&mut output).unwrap(); 25 | 26 | assert_eq!(&output[..], "Hello world!\n"); 27 | } 28 | 29 | #[test] 30 | fn test_gag_stderr_twice() { 31 | let _l = STDERR_MUTEX.lock().unwrap(); 32 | 33 | let hold = Hold::stderr(); 34 | let hold2 = Hold::stderr(); 35 | assert!(hold.is_ok()); 36 | assert!(hold2.is_err()); 37 | } 38 | --------------------------------------------------------------------------------