Fix try printing when it is not the last pipeline element (#13992)

# Description

Fixes #13991. This was done by more clearly separating the case when a
pipeline is drained vs when it is being written (to a file).

I also added an `OutDest::Print` case which might not be strictly
necessary, but is a helpful addition.

# User-Facing Changes

Bug fix.

# Tests + Formatting

Added a test.

# After Submitting

There are still a few redirection bugs that I found, but they require
larger code changes, so I'll leave them until after the release.
This commit is contained in:
Ian Manske
2024-10-11 23:37:10 -07:00
committed by GitHub
parent 0e3a8c552c
commit de08b68ba8
14 changed files with 127 additions and 194 deletions

View File

@ -62,8 +62,8 @@ pub(crate) struct StackOutDest {
impl StackOutDest {
pub(crate) fn new() -> Self {
Self {
pipe_stdout: None,
pipe_stderr: None,
pipe_stdout: Some(OutDest::Print),
pipe_stderr: Some(OutDest::Print),
stdout: OutDest::Inherit,
stderr: OutDest::Inherit,
parent_stdout: None,

View File

@ -92,8 +92,8 @@ impl<'a> fmt::Display for FmtInstruction<'a> {
Instruction::Drain { src } => {
write!(f, "{:WIDTH$} {src}", "drain")
}
Instruction::WriteToOutDests { src } => {
write!(f, "{:WIDTH$} {src}", "write-to-out-dests")
Instruction::DrainIfEnd { src } => {
write!(f, "{:WIDTH$} {src}", "drain-if-end")
}
Instruction::LoadVariable { dst, var_id } => {
let var = FmtVar::new(self.engine_state, *var_id);
@ -312,6 +312,7 @@ impl fmt::Display for RedirectMode {
RedirectMode::Value => write!(f, "value"),
RedirectMode::Null => write!(f, "null"),
RedirectMode::Inherit => write!(f, "inherit"),
RedirectMode::Print => write!(f, "print"),
RedirectMode::File { file_num } => write!(f, "file({file_num})"),
RedirectMode::Caller => write!(f, "caller"),
}

View File

@ -123,9 +123,9 @@ pub enum Instruction {
/// code, and invokes any available error handler with Empty, or if not available, returns an
/// exit-code-only stream, leaving the block.
Drain { src: RegId },
/// Write to the output destinations in the stack
/// Drain the value/stream in a register and discard only if this is the last pipeline element.
// TODO: see if it's possible to remove this
WriteToOutDests { src: RegId },
DrainIfEnd { src: RegId },
/// Load the value of a variable into the `dst` register
LoadVariable { dst: RegId, var_id: VarId },
/// Store the value of a variable from the `src` register
@ -290,7 +290,7 @@ impl Instruction {
Instruction::Span { src_dst } => Some(src_dst),
Instruction::Drop { .. } => None,
Instruction::Drain { .. } => None,
Instruction::WriteToOutDests { .. } => None,
Instruction::DrainIfEnd { .. } => None,
Instruction::LoadVariable { dst, .. } => Some(dst),
Instruction::StoreVariable { .. } => None,
Instruction::DropVariable { .. } => None,
@ -450,6 +450,7 @@ pub enum RedirectMode {
Value,
Null,
Inherit,
Print,
/// Use the given numbered file.
File {
file_num: u32,

View File

@ -1,7 +1,7 @@
//! Module managing the streaming of raw bytes between pipeline elements
use crate::{
process::{ChildPipe, ChildProcess},
ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Signals, Span, Type, Value,
ErrSpan, IntoSpanned, PipelineData, ShellError, Signals, Span, Type, Value,
};
use serde::{Deserialize, Serialize};
#[cfg(unix)]
@ -13,7 +13,6 @@ use std::{
fs::File,
io::{self, BufRead, BufReader, Cursor, ErrorKind, Read, Write},
process::Stdio,
thread,
};
/// The source of bytes for a [`ByteStream`].
@ -600,80 +599,6 @@ impl ByteStream {
}
Ok(())
}
pub(crate) fn write_to_out_dests(
self,
stdout: &OutDest,
stderr: &OutDest,
) -> Result<(), ShellError> {
let span = self.span;
let signals = &self.signals;
match self.stream {
ByteStreamSource::Read(read) => {
write_to_out_dest(read, stdout, true, span, signals)?;
}
ByteStreamSource::File(file) => match stdout {
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value | OutDest::Null => {}
OutDest::Inherit => {
copy_with_signals(file, io::stdout(), span, signals)?;
}
OutDest::File(f) => {
copy_with_signals(file, f.as_ref(), span, signals)?;
}
},
ByteStreamSource::Child(mut child) => {
match (child.stdout.take(), child.stderr.take()) {
(Some(out), Some(err)) => {
// To avoid deadlocks, we must spawn a separate thread to wait on stderr.
thread::scope(|s| {
let err_thread = thread::Builder::new()
.name("stderr writer".into())
.spawn_scoped(s, || match err {
ChildPipe::Pipe(pipe) => {
write_to_out_dest(pipe, stderr, false, span, signals)
}
ChildPipe::Tee(tee) => {
write_to_out_dest(tee, stderr, false, span, signals)
}
})
.err_span(span);
match out {
ChildPipe::Pipe(pipe) => {
write_to_out_dest(pipe, stdout, true, span, signals)
}
ChildPipe::Tee(tee) => {
write_to_out_dest(tee, stdout, true, span, signals)
}
}?;
if let Ok(result) = err_thread?.join() {
result?;
} else {
// thread panicked, which should not happen
debug_assert!(false)
}
Ok::<_, ShellError>(())
})?;
}
(Some(out), None) => {
// single output stream, we can consume directly
write_to_out_dest(out, stdout, true, span, signals)?;
}
(None, Some(err)) => {
// single output stream, we can consume directly
write_to_out_dest(err, stderr, false, span, signals)?;
}
(None, None) => {}
}
child.wait()?;
}
}
Ok(())
}
}
impl From<ByteStream> for PipelineData {
@ -962,23 +887,6 @@ fn trim_end_newline(string: &mut String) {
}
}
fn write_to_out_dest(
read: impl Read,
stream: &OutDest,
stdout: bool,
span: Span,
signals: &Signals,
) -> Result<(), ShellError> {
match stream {
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => return Ok(()),
OutDest::Null => copy_with_signals(read, io::sink(), span, signals),
OutDest::Inherit if stdout => copy_with_signals(read, io::stdout(), span, signals),
OutDest::Inherit => copy_with_signals(read, io::stderr(), span, signals),
OutDest::File(file) => copy_with_signals(read, file.as_ref(), span, signals),
}?;
Ok(())
}
#[cfg(unix)]
pub(crate) fn convert_file<T: From<OwnedFd>>(file: impl Into<OwnedFd>) -> T {
file.into().into()

View File

@ -25,10 +25,16 @@ pub enum OutDest {
///
/// This will forward output to the null device for the platform.
Null,
/// Output to nushell's stdout or stderr.
/// Output to nushell's stdout or stderr (only for external commands).
///
/// This causes external commands to inherit nushell's stdout or stderr.
/// This causes external commands to inherit nushell's stdout or stderr. This also causes
/// [`ListStream`](crate::ListStream)s to be drained, but not to be printed.
Inherit,
/// Print to nushell's stdout or stderr.
///
/// This is just like `Inherit`, except that [`ListStream`](crate::ListStream)s and
/// [`Value`](crate::Value)s are also printed.
Print,
/// Redirect output to a file.
File(Arc<File>), // Arc<File>, since we sometimes need to clone `OutDest` into iterators, etc.
}
@ -52,7 +58,7 @@ impl TryFrom<&OutDest> for Stdio {
match out_dest {
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => Ok(Self::piped()),
OutDest::Null => Ok(Self::null()),
OutDest::Inherit => Ok(Self::inherit()),
OutDest::Print | OutDest::Inherit => Ok(Self::inherit()),
OutDest::File(file) => Ok(file.try_clone()?.into()),
}
}

View File

@ -165,66 +165,71 @@ impl PipelineData {
}
}
/// Writes all values or redirects all output to the current [`OutDest`]s in `stack`.
/// Drain and write this [`PipelineData`] to `dest`.
///
/// For [`OutDest::Pipe`] and [`OutDest::PipeSeparate`], this will return the `PipelineData` as
/// is without consuming input and without writing anything.
/// Values are converted to bytes and separated by newlines if this is a `ListStream`.
pub fn write_to(self, mut dest: impl Write) -> Result<(), ShellError> {
match self {
PipelineData::Empty => Ok(()),
PipelineData::Value(value, ..) => {
let bytes = value_to_bytes(value)?;
dest.write_all(&bytes)?;
dest.flush()?;
Ok(())
}
PipelineData::ListStream(stream, ..) => {
for value in stream {
let bytes = value_to_bytes(value)?;
dest.write_all(&bytes)?;
dest.write_all(b"\n")?;
}
dest.flush()?;
Ok(())
}
PipelineData::ByteStream(stream, ..) => stream.write_to(dest),
}
}
/// Drain this [`PipelineData`] according to the current stdout [`OutDest`]s in `stack`.
///
/// For the other [`OutDest`]s, the given `PipelineData` will be completely consumed
/// and `PipelineData::Empty` will be returned (assuming no errors).
pub fn write_to_out_dests(
/// For [`OutDest::Pipe`] and [`OutDest::PipeSeparate`], this will return the [`PipelineData`]
/// as is. For [`OutDest::Value`], this will collect into a value and return it. For
/// [`OutDest::Print`], the [`PipelineData`] is drained and printed. Otherwise, the
/// [`PipelineData`] is drained, but only printed if it is the output of an external command.
pub fn drain_to_out_dests(
self,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<PipelineData, ShellError> {
match (self, stack.stdout()) {
(PipelineData::Empty, ..) => {}
(data, OutDest::Pipe | OutDest::PipeSeparate) => return Ok(data),
(data, OutDest::Value) => {
let metadata = data.metadata();
let span = data.span().unwrap_or(Span::unknown());
return data
.into_value(span)
.map(|val| PipelineData::Value(val, metadata));
) -> Result<Self, ShellError> {
match stack.pipe_stdout().unwrap_or(&OutDest::Inherit) {
OutDest::Print => {
self.print(engine_state, stack, false, false)?;
Ok(Self::Empty)
}
(PipelineData::ByteStream(stream, ..), stdout) => {
stream.write_to_out_dests(stdout, stack.stderr())?;
OutDest::Pipe | OutDest::PipeSeparate => Ok(self),
OutDest::Value => {
let metadata = self.metadata();
let span = self.span().unwrap_or(Span::unknown());
self.into_value(span).map(|val| Self::Value(val, metadata))
}
(PipelineData::Value(..), OutDest::Null) => {}
(PipelineData::ListStream(stream, ..), OutDest::Null) => {
// we need to drain the stream in case there are external commands in the pipeline
stream.drain()?;
OutDest::File(file) => {
self.write_to(file.as_ref())?;
Ok(Self::Empty)
}
(PipelineData::Value(value, ..), OutDest::File(file)) => {
let bytes = value_to_bytes(value)?;
let mut file = file.as_ref();
file.write_all(&bytes)?;
file.flush()?;
}
(PipelineData::ListStream(stream, ..), OutDest::File(file)) => {
let mut file = file.as_ref();
// use BufWriter here?
for value in stream {
let bytes = value_to_bytes(value)?;
file.write_all(&bytes)?;
file.write_all(b"\n")?;
}
file.flush()?;
}
(data @ (PipelineData::Value(..) | PipelineData::ListStream(..)), OutDest::Inherit) => {
data.print(engine_state, stack, false, false)?;
OutDest::Null | OutDest::Inherit => {
self.drain()?;
Ok(Self::Empty)
}
}
Ok(PipelineData::Empty)
}
pub fn drain(self) -> Result<(), ShellError> {
match self {
PipelineData::Empty => Ok(()),
PipelineData::Value(Value::Error { error, .. }, ..) => Err(*error),
PipelineData::Value(..) => Ok(()),
PipelineData::ListStream(stream, ..) => stream.drain(),
PipelineData::ByteStream(stream, ..) => stream.drain(),
Self::Empty => Ok(()),
Self::Value(Value::Error { error, .. }, ..) => Err(*error),
Self::Value(..) => Ok(()),
Self::ListStream(stream, ..) => stream.drain(),
Self::ByteStream(stream, ..) => stream.drain(),
}
}