Add Stack::stdout_file and Stack::stderr_file to capture stdout/-err of external commands (#12857)

# Description
In this PR I added two new methods to `Stack`, `stdout_file` and
`stderr_file`. These two modify the inner `StackOutDest` and set a
`File` into the `stdout` and `stderr` respectively. Different to the
`push_redirection` methods, these do not require to hold a guard up all
the time but require ownership of the stack.

This is primarly useful for applications that use `nu` as a language but
not the `nushell`.

This PR replaces my first attempt #12851 to add a way to capture
stdout/-err of external commands. Capturing the stdout without having to
write into a file is possible with crates like
[`os_pipe`](https://docs.rs/os_pipe), an example for this is given in
the doc comment of the `stdout_file` command and can be executed as a
doctest (although it doesn't validate that you actually got any data).

This implementation takes `File` as input to make it easier to implement
on different operating systems without having to worry about
`OwnedHandle` or `OwnedFd`. Also this doesn't expose any use `os_pipe`
to not leak its types into this API, making it depend on it.

As in my previous attempt, @IanManske guided me here.

# User-Facing Changes
This change has no effect on `nushell` and therefore no user-facing
changes.

# Tests + Formatting
This only exposes a new way of using already existing code and has
therefore no further testing. The doctest succeeds on my machine at
least (x86 Windows, 64 Bit).

# After Submitting
All the required documentation is already part of this PR.
This commit is contained in:
Piepmatz 2024-05-13 20:48:38 +02:00 committed by GitHub
parent 905ec88091
commit aaf973bbba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 55 additions and 1 deletions

1
Cargo.lock generated
View file

@ -3259,6 +3259,7 @@ dependencies = [
"nu-test-support", "nu-test-support",
"nu-utils", "nu-utils",
"num-format", "num-format",
"os_pipe",
"pretty_assertions", "pretty_assertions",
"rmp-serde", "rmp-serde",
"rstest", "rstest",

View file

@ -119,7 +119,7 @@ num-traits = "0.2"
omnipath = "0.1" omnipath = "0.1"
once_cell = "1.18" once_cell = "1.18"
open = "5.1" open = "5.1"
os_pipe = "1.1" os_pipe = { version = "1.1", features = ["io_safety"] }
pathdiff = "0.2" pathdiff = "0.2"
percent-encoding = "2" percent-encoding = "2"
pretty_assertions = "1.4" pretty_assertions = "1.4"

View file

@ -47,6 +47,7 @@ nu-test-support = { path = "../nu-test-support", version = "0.93.1" }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
rstest = { workspace = true } rstest = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
os_pipe = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View file

@ -7,6 +7,7 @@ use crate::{
}; };
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fs::File,
sync::Arc, sync::Arc,
}; };
@ -593,6 +594,57 @@ impl Stack {
self self
} }
/// Replaces the default stdout of the stack with a given file.
///
/// This method configures the default stdout to redirect to a specified file.
/// It is primarily useful for applications using `nu` as a language, where the stdout of
/// external commands that are not explicitly piped can be redirected to a file.
///
/// # Using Pipes
///
/// For use in third-party applications pipes might be very useful as they allow using the
/// stdout of external commands for different uses.
/// For example the [`os_pipe`](https://docs.rs/os_pipe) crate provides a elegant way to to
/// access the stdout.
///
/// ```
/// # use std::{fs::File, io::{self, Read}, thread, error};
/// # use nu_protocol::engine::Stack;
/// #
/// let (mut reader, writer) = os_pipe::pipe().unwrap();
/// // Use a thread to avoid blocking the execution of the called command.
/// let reader = thread::spawn(move || {
/// let mut buf: Vec<u8> = Vec::new();
/// reader.read_to_end(&mut buf)?;
/// Ok::<_, io::Error>(buf)
/// });
///
/// #[cfg(windows)]
/// let file = std::os::windows::io::OwnedHandle::from(writer).into();
/// #[cfg(unix)]
/// let file = std::os::unix::io::OwnedFd::from(writer).into();
///
/// let stack = Stack::new().stdout_file(file);
///
/// // Execute some nu code.
///
/// drop(stack); // drop the stack so that the writer will be dropped too
/// let buf = reader.join().unwrap().unwrap();
/// // Do with your buffer whatever you want.
/// ```
pub fn stdout_file(mut self, file: File) -> Self {
self.out_dest.stdout = OutDest::File(Arc::new(file));
self
}
/// Replaces the default stderr of the stack with a given file.
///
/// For more info, see [`stdout_file`](Self::stdout_file).
pub fn stderr_file(mut self, file: File) -> Self {
self.out_dest.stderr = OutDest::File(Arc::new(file));
self
}
/// Set the PWD environment variable to `path`. /// Set the PWD environment variable to `path`.
/// ///
/// This method accepts `path` with trailing slashes, but they're removed /// This method accepts `path` with trailing slashes, but they're removed