attempt to use a file stack for redirection, but realized that both out and err can be separate so need to be able to reference both

This commit is contained in:
Devyn Cairns 2024-07-07 23:08:16 -07:00
parent 689cc49663
commit 2807717245
6 changed files with 179 additions and 85 deletions

View File

@ -1,5 +1,5 @@
use nu_protocol::{
ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal},
CompileError, IntoSpanned, RegId, Span, Spanned,
};
@ -168,10 +168,11 @@ impl BlockBuilder {
Instruction::PushFlag { name: _ } => Ok(()),
Instruction::PushNamed { name: _, src } => allocate(&[*src], &[]),
Instruction::PushParserInfo { name: _, info: _ } => Ok(()),
Instruction::RedirectOut { mode } | Instruction::RedirectErr { mode } => match mode {
RedirectMode::File { path, .. } => allocate(&[*path], &[]),
_ => Ok(()),
},
Instruction::RedirectOut { mode: _ } => Ok(()),
Instruction::RedirectErr { mode: _ } => Ok(()),
Instruction::OpenFile { path, append: _ } => allocate(&[*path], &[]),
Instruction::WriteFile { src } => allocate(&[*src], &[]),
Instruction::CloseFile => Ok(()),
Instruction::Call {
decl_id: _,
src_dst,

View File

@ -154,21 +154,23 @@ fn compile_pipeline(
},
};
let out_mode = spec_redirect_modes.out.or(next_redirect_modes.out);
let err_mode = spec_redirect_modes.err.or(next_redirect_modes.err);
let redirect_modes = RedirectModes {
out: spec_redirect_modes.out.or(next_redirect_modes.out),
err: spec_redirect_modes.err.or(next_redirect_modes.err),
};
compile_expression(
working_set,
builder,
&element.expr,
RedirectModes {
out: out_mode,
err: err_mode,
},
redirect_modes.clone(),
in_reg,
out_reg,
)?;
// Clean up the redirection
finish_redirection(builder, redirect_modes, out_reg)?;
// The next pipeline element takes input from this output
in_reg = Some(out_reg);
}

View File

@ -1,8 +1,8 @@
use nu_protocol::{
ast::{Expression, RedirectionTarget},
engine::StateWorkingSet,
ir::RedirectMode,
IntoSpanned, OutDest, Span, Spanned,
ir::{Instruction, RedirectMode},
IntoSpanned, OutDest, RegId, Span, Spanned,
};
use super::{compile_expression, BlockBuilder, CompileError};
@ -64,11 +64,14 @@ pub(crate) fn redirection_target_to_mode(
None,
path_reg,
)?;
RedirectMode::File {
path: path_reg,
append: *append,
}
.into_spanned(*redir_span)
builder.push(
Instruction::OpenFile {
path: path_reg,
append: *append,
}
.into_spanned(*redir_span),
)?;
RedirectMode::File.into_spanned(*redir_span)
}
RedirectionTarget::Pipe { span } => (if separate {
RedirectMode::Capture
@ -97,6 +100,37 @@ pub(crate) fn redirect_modes_of_expression(
})
}
/// Finish the redirection for an expression, writing to and closing files as necessary
pub(crate) fn finish_redirection(
builder: &mut BlockBuilder,
modes: RedirectModes,
out_reg: RegId,
) -> Result<(), CompileError> {
match modes.out {
Some(Spanned {
item: RedirectMode::File,
span,
}) => {
builder.push(Instruction::WriteFile { src: out_reg }.into_spanned(span))?;
builder.load_empty(out_reg)?;
builder.push(Instruction::CloseFile.into_spanned(span))?;
}
_ => (),
}
match modes.err {
Some(Spanned {
item: RedirectMode::File,
span,
}) => {
builder.push(Instruction::CloseFile.into_spanned(span))?;
}
_ => (),
}
Ok(())
}
pub(crate) fn out_dest_to_redirect_mode(out_dest: OutDest) -> Result<RedirectMode, CompileError> {
match out_dest {
OutDest::Pipe => Ok(RedirectMode::Pipe),

View File

@ -6,9 +6,9 @@ use nu_protocol::{
debugger::DebugContext,
engine::{Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack},
ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
record, DeclId, Flag, IntoPipelineData, IntoSpanned, ListStream, OutDest, PipelineData,
PositionalArg, Range, Record, RegId, ShellError, Signature, Span, Spanned, Value, VarId,
ENV_VARIABLE_ID,
record, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, ListStream, OutDest,
PipelineData, PositionalArg, Range, Record, RegId, ShellError, Signature, Span, Spanned, Value,
VarId, ENV_VARIABLE_ID,
};
use nu_utils::IgnoreCaseExt;
@ -55,6 +55,7 @@ pub fn eval_ir_block<D: DebugContext>(
error_handler_base,
redirect_out: None,
redirect_err: None,
file_stack: vec![],
matches: vec![],
registers: &mut registers[..],
},
@ -95,6 +96,8 @@ struct EvalContext<'a> {
redirect_out: Option<Redirection>,
/// State set by redirect-err
redirect_err: Option<Redirection>,
/// Files used for redirection
file_stack: Vec<Arc<File>>,
/// Scratch space to use for `match`
matches: Vec<(VarId, Value)>,
registers: &'a mut [PipelineData],
@ -115,6 +118,28 @@ impl<'a> EvalContext<'a> {
std::mem::replace(&mut self.registers[reg_id.0 as usize], PipelineData::Empty)
}
/// Clone data from a register. Must be collected first.
fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result<PipelineData, ShellError> {
match &self.registers[reg_id.0 as usize] {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Value(val, meta) => Ok(PipelineData::Value(val.clone(), meta.clone())),
_ => Err(ShellError::IrEvalError {
msg: "Must collect to value before using instruction that clones from a register"
.into(),
span: Some(error_span),
}),
}
}
/// Clone a value from a register. Must be collected first.
fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
match self.clone_reg(reg_id, fallback_span)? {
PipelineData::Empty => Ok(Value::nothing(fallback_span)),
PipelineData::Value(val, _) => Ok(val),
_ => unreachable!("clone_reg should never return stream data"),
}
}
/// Take and implicitly collect a register to a value
fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
let data = self.take_reg(reg_id);
@ -234,22 +259,8 @@ fn eval_instruction<D: DebugContext>(
Ok(Continue)
}
Instruction::Clone { dst, src } => {
let data1 = ctx.take_reg(*src);
let data2 = match &data1 {
PipelineData::Empty => PipelineData::Empty,
PipelineData::Value(val, meta) => PipelineData::Value(val.clone(), meta.clone()),
_ => {
return Err(ShellError::GenericError {
error: "IR error: must collect before clone if a stream is expected".into(),
msg: "error occurred here".into(),
span: Some(*span),
help: Some("this is a compiler bug".into()),
inner: vec![],
})
}
};
ctx.put_reg(*src, data1);
ctx.put_reg(*dst, data2);
let data = ctx.clone_reg(*src, *span)?;
ctx.put_reg(*dst, data);
Ok(Continue)
}
Instruction::Collect { src_dst } => {
@ -378,6 +389,38 @@ fn eval_instruction<D: DebugContext>(
ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?;
Ok(Continue)
}
Instruction::OpenFile { path, append } => {
let path = ctx.collect_reg(*path, *span)?;
let file = open_file(ctx, &path, *append)?;
ctx.file_stack.push(file);
Ok(Continue)
}
Instruction::WriteFile { src } => {
let src = ctx.take_reg(*src);
let file = ctx
.file_stack
.last()
.cloned()
.ok_or_else(|| ShellError::IrEvalError {
msg: "Tried to write file without opening a file first".into(),
span: Some(*span),
})?;
let mut stack = ctx
.stack
.push_redirection(Some(Redirection::File(file)), None);
src.write_to_out_dests(ctx.engine_state, &mut stack)?;
Ok(Continue)
}
Instruction::CloseFile => {
if ctx.file_stack.pop().is_some() {
Ok(Continue)
} else {
Err(ShellError::IrEvalError {
msg: "Tried to close file without opening a file first".into(),
span: Some(*span),
})
}
}
Instruction::Call { decl_id, src_dst } => {
let input = ctx.take_reg(*src_dst);
let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
@ -498,25 +541,17 @@ fn eval_instruction<D: DebugContext>(
}
}
Instruction::CloneCellPath { dst, src, path } => {
let data = ctx.take_reg(*src);
let value = ctx.clone_reg_value(*src, *span)?;
let path = ctx.take_reg(*path);
if let PipelineData::Value(value, _) = &data {
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
// TODO: make follow_cell_path() not have to take ownership, probably using Cow
let value = value.clone().follow_cell_path(&path.members, true)?;
ctx.put_reg(*src, data);
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
} else {
Err(ShellError::TypeMismatch {
err_message: "cell path".into(),
span: path.span().unwrap_or(*span),
})
}
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
// TODO: make follow_cell_path() not have to take ownership, probably using Cow
let value = value.follow_cell_path(&path.members, true)?;
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
} else {
Err(ShellError::IrEvalError {
msg: "must collect value before clone-cell-path".into(),
span: Some(*span),
Err(ShellError::TypeMismatch {
err_message: "cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
@ -577,13 +612,7 @@ fn eval_instruction<D: DebugContext>(
src,
index,
} => {
let data = ctx.take_reg(*src);
let PipelineData::Value(value, metadata) = data else {
return Err(ShellError::IrEvalError {
msg: "must collect value before match".into(),
span: Some(*span),
});
};
let value = ctx.clone_reg_value(*src, *span)?;
ctx.matches.clear();
if pattern.match_value(&value, &mut ctx.matches) {
// Match succeeded: set variables and branch
@ -594,7 +623,6 @@ fn eval_instruction<D: DebugContext>(
} else {
// Failed to match, put back original value
ctx.matches.clear();
ctx.put_reg(*src, PipelineData::Value(value, metadata));
Ok(Continue)
}
}
@ -1096,6 +1124,23 @@ enum RedirectionStream {
Err,
}
/// Open a file for redirection
fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result<Arc<File>, ShellError> {
let path_expanded =
expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true);
let mut options = File::options();
if append {
options.append(true);
} else {
options.write(true).truncate(true);
}
let file = options
.create(true)
.open(path_expanded)
.err_span(path.span())?;
Ok(Arc::new(file))
}
/// Set up a [`Redirection`] from a [`RedirectMode`]
fn eval_redirection(
ctx: &mut EvalContext<'_>,
@ -1108,21 +1153,16 @@ fn eval_redirection(
RedirectMode::Capture => Ok(Some(Redirection::Pipe(OutDest::Capture))),
RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))),
RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))),
RedirectMode::File { path, append } => {
let path = ctx.collect_reg(*path, span)?;
let path_expanded =
expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true);
let mut options = File::options();
if *append {
options.append(true);
} else {
options.write(true).truncate(true);
}
let file = options
.create(true)
.open(path_expanded)
.map_err(|err| err.into_spanned(span))?;
Ok(Some(Redirection::File(file.into())))
RedirectMode::File => {
let file = ctx
.file_stack
.last()
.cloned()
.ok_or_else(|| ShellError::IrEvalError {
msg: "Tried to redirect to file without opening a file first".into(),
span: Some(span),
})?;
Ok(Some(Redirection::File(file)))
}
RedirectMode::Caller => Ok(match which {
RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe),

View File

@ -119,6 +119,15 @@ impl<'a> fmt::Display for FmtInstruction<'a> {
Instruction::RedirectErr { mode } => {
write!(f, "{:WIDTH$} {mode}", "redirect-err")
}
Instruction::OpenFile { path, append } => {
write!(f, "{:WIDTH$} {path}, append = {append:?}", "open-file")
}
Instruction::WriteFile { src } => {
write!(f, "{:WIDTH$} {src}", "write-file")
}
Instruction::CloseFile => {
write!(f, "{:WIDTH$}", "close-file")
}
Instruction::Call { decl_id, src_dst } => {
let decl = FmtDecl::new(self.engine_state, *decl_id);
write!(f, "{:WIDTH$} {decl}, {src_dst}", "call")
@ -253,7 +262,7 @@ impl fmt::Display for RedirectMode {
RedirectMode::Capture => write!(f, "capture"),
RedirectMode::Null => write!(f, "null"),
RedirectMode::Inherit => write!(f, "inherit"),
RedirectMode::File { path, append } => write!(f, "file({path}, append={append})"),
RedirectMode::File => write!(f, "file"),
RedirectMode::Caller => write!(f, "caller"),
}
}

View File

@ -134,10 +134,21 @@ pub enum Instruction {
name: DataSlice,
info: Box<Expression>,
},
/// Set the redirection for stdout for the next call (only)
/// Set the redirection for stdout for the next call (only).
///
/// The register for a file redirection is not consumed.
RedirectOut { mode: RedirectMode },
/// Set the redirection for stderr for the next call (only)
/// Set the redirection for stderr for the next call (only).
///
/// The register for a file redirection is not consumed.
RedirectErr { mode: RedirectMode },
/// Open a file for redirection, pushing it onto the file stack.
OpenFile { path: RegId, append: bool },
/// Write data from the register to a file. This is done to finish a file redirection, in case
/// an internal command or expression was evaluated rather than an external one.
WriteFile { src: RegId },
/// Pop a file used for redirection from the file stack.
CloseFile,
/// Make a call. The input is taken from `src_dst`, and the output is placed in `src_dst`,
/// overwriting it. The argument stack is used implicitly and cleared when the call ends.
Call { decl_id: DeclId, src_dst: RegId },
@ -295,11 +306,8 @@ pub enum RedirectMode {
Capture,
Null,
Inherit,
/// File path to be used in register.
File {
path: RegId,
append: bool,
},
/// Use the file on the top of the file stack.
File,
/// Use the redirection mode requested by the caller, for a pre-return call.
Caller,
}