Support redirect stderr and stdout+stderr with a pipe (#11708)

# Description
Close: #9673
Close: #8277
Close: #10944

This pr introduces the following syntax:
1. `e>|`, pipe stderr to next command. Example: `$env.FOO=bar nu
--testbin echo_env_stderr FOO e>| str length`
2. `o+e>|` and `e+o>|`, pipe both stdout and stderr to next command,
example: `$env.FOO=bar nu --testbin echo_env_mixed out-err FOO FOO e+o>|
str length`

Note: it only works for external commands. ~There is no different for
internal commands, that is, the following three commands do the same
things:~ Edit: it raises errors if we want to pipes for internal
commands
``` 
❯ ls e>| str length
Error:   × `e>|` only works with external streams
   ╭─[entry #1:1:1]
 1 │ ls e>| str length
   ·    ─┬─
   ·     ╰── `e>|` only works on external streams
   ╰────

❯ ls e+o>| str length
Error:   × `o+e>|` only works with external streams
   ╭─[entry #2:1:1]
 1 │ ls e+o>| str length
   ·    ──┬──
   ·      ╰── `o+e>|` only works on external streams
   ╰────
```

This can help us to avoid some strange issues like the following:

`$env.FOO=bar (nu --testbin echo_env_stderr FOO) e>| str length`

Which is hard to understand and hard to explain to users.

# User-Facing Changes
Nan

# Tests + Formatting
To be done

# After Submitting
Maybe update documentation about these syntax.
This commit is contained in:
Wind
2024-02-09 01:30:46 +08:00
committed by GitHub
parent e7f1bf8535
commit 58c6fea60b
15 changed files with 520 additions and 106 deletions

View File

@ -560,7 +560,9 @@ pub fn flatten_pipeline_element(
pipeline_element: &PipelineElement,
) -> Vec<(Span, FlatShape)> {
match pipeline_element {
PipelineElement::Expression(span, expr) => {
PipelineElement::Expression(span, expr)
| PipelineElement::ErrPipedExpression(span, expr)
| PipelineElement::OutErrPipedExpression(span, expr) => {
if let Some(span) = span {
let mut output = vec![(*span, FlatShape::Pipe)];
output.append(&mut flatten_expression(working_set, expr));

View File

@ -6,6 +6,8 @@ pub enum TokenContents {
Comment,
Pipe,
PipePipe,
ErrGreaterPipe,
OutErrGreaterPipe,
Semicolon,
OutGreaterThan,
OutGreaterGreaterThan,
@ -485,8 +487,14 @@ fn lex_internal(
// If the next character is non-newline whitespace, skip it.
curr_offset += 1;
} else {
// Otherwise, try to consume an unclassified token.
let token = try_lex_special_piped_item(input, &mut curr_offset, span_offset);
if let Some(token) = token {
output.push(token);
is_complete = false;
continue;
}
// Otherwise, try to consume an unclassified token.
let (token, err) = lex_item(
input,
&mut curr_offset,
@ -504,3 +512,49 @@ fn lex_internal(
}
(output, error)
}
/// trying to lex for the following item:
/// e>|, e+o>|, o+e>|
///
/// It returns Some(token) if we find the item, or else return None.
fn try_lex_special_piped_item(
input: &[u8],
curr_offset: &mut usize,
span_offset: usize,
) -> Option<Token> {
let c = input[*curr_offset];
let e_pipe_len = 3;
let eo_pipe_len = 5;
let offset = *curr_offset;
if c == b'e' {
// expect `e>|`
if (offset + e_pipe_len <= input.len()) && (&input[offset..offset + e_pipe_len] == b"e>|") {
*curr_offset += e_pipe_len;
return Some(Token::new(
TokenContents::ErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + e_pipe_len),
));
}
if (offset + eo_pipe_len <= input.len())
&& (&input[offset..offset + eo_pipe_len] == b"e+o>|")
{
*curr_offset += eo_pipe_len;
return Some(Token::new(
TokenContents::OutErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + eo_pipe_len),
));
}
} else if c == b'o' {
// it can be the following case: `o+e>|`
if (offset + eo_pipe_len <= input.len())
&& (&input[offset..offset + eo_pipe_len] == b"o+e>|")
{
*curr_offset += eo_pipe_len;
return Some(Token::new(
TokenContents::OutErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + eo_pipe_len),
));
}
}
None
}

View File

@ -37,6 +37,12 @@ impl LiteCommand {
#[derive(Debug)]
pub enum LiteElement {
Command(Option<Span>, LiteCommand),
// Similar to LiteElement::Command, except the previous command's output is stderr piped.
// e.g: `e>| cmd`
ErrPipedCommand(Option<Span>, LiteCommand),
// Similar to LiteElement::Command, except the previous command's output is stderr + stdout piped.
// e.g: `o+e>| cmd`
OutErrPipedCommand(Option<Span>, LiteCommand),
// final field indicates if it's in append mode
Redirection(Span, Redirection, LiteCommand, bool),
// SeparateRedirection variant can only be generated by two different Redirection variant
@ -272,7 +278,9 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option<ParseError>) {
last_connector = token.contents;
last_connector_span = Some(token.span);
}
TokenContents::Pipe => {
pipe_token @ (TokenContents::Pipe
| TokenContents::ErrGreaterPipe
| TokenContents::OutErrGreaterPipe) => {
if let Some(err) = push_command_to(
&mut curr_pipeline,
curr_command,
@ -283,8 +291,8 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option<ParseError>) {
}
curr_command = LiteCommand::new();
last_token = TokenContents::Pipe;
last_connector = TokenContents::Pipe;
last_token = *pipe_token;
last_connector = *pipe_token;
last_connector_span = Some(token.span);
}
TokenContents::Eol => {
@ -429,7 +437,35 @@ fn push_command_to(
is_append_mode,
))
}
None => pipeline.push(LiteElement::Command(last_connector_span, command)),
None => {
if last_connector == TokenContents::ErrGreaterPipe {
pipeline.push(LiteElement::ErrPipedCommand(last_connector_span, command))
} else if last_connector == TokenContents::OutErrGreaterPipe {
// Don't allow o+e>| along with redirection.
for cmd in &pipeline.commands {
if matches!(
cmd,
LiteElement::Redirection { .. }
| LiteElement::SameTargetRedirection { .. }
| LiteElement::SeparateRedirection { .. }
) {
return Some(ParseError::LabeledError(
"`o+e>|` pipe is not allowed to use with redirection".into(),
"try to use different type of pipe, or remove redirection".into(),
last_connector_span
.expect("internal error: outerr pipe missing span information"),
));
}
}
pipeline.push(LiteElement::OutErrPipedCommand(
last_connector_span,
command,
))
} else {
pipeline.push(LiteElement::Command(last_connector_span, command))
}
}
}
None
} else if get_redirection(last_connector).is_some() {

View File

@ -1702,7 +1702,9 @@ pub fn parse_module_block(
for pipeline in output.block.iter() {
if pipeline.commands.len() == 1 {
match &pipeline.commands[0] {
LiteElement::Command(_, command) => {
LiteElement::Command(_, command)
| LiteElement::ErrPipedCommand(_, command)
| LiteElement::OutErrPipedCommand(_, command) => {
let name = working_set.get_span_contents(command.parts[0]);
match name {

View File

@ -1299,7 +1299,9 @@ fn parse_binary_with_base(
}
TokenContents::Pipe
| TokenContents::PipePipe
| TokenContents::ErrGreaterPipe
| TokenContents::OutGreaterThan
| TokenContents::OutErrGreaterPipe
| TokenContents::OutGreaterGreaterThan
| TokenContents::ErrGreaterThan
| TokenContents::ErrGreaterGreaterThan
@ -5479,7 +5481,9 @@ pub fn parse_pipeline(
for command in &pipeline.commands[1..] {
match command {
LiteElement::Command(Some(pipe_span), command) => {
LiteElement::Command(Some(pipe_span), command)
| LiteElement::ErrPipedCommand(Some(pipe_span), command)
| LiteElement::OutErrPipedCommand(Some(pipe_span), command) => {
new_command.parts.push(*pipe_span);
new_command.comments.extend_from_slice(&command.comments);
@ -5584,6 +5588,18 @@ pub fn parse_pipeline(
PipelineElement::Expression(*span, expr)
}
LiteElement::ErrPipedCommand(span, command) => {
trace!("parsing: pipeline element: err piped command");
let expr = parse_expression(working_set, &command.parts, is_subexpression);
PipelineElement::ErrPipedExpression(*span, expr)
}
LiteElement::OutErrPipedCommand(span, command) => {
trace!("parsing: pipeline element: err piped command");
let expr = parse_expression(working_set, &command.parts, is_subexpression);
PipelineElement::OutErrPipedExpression(*span, expr)
}
LiteElement::Redirection(span, redirection, command, is_append_mode) => {
let expr = parse_value(working_set, command.parts[0], &SyntaxShape::Any);
@ -5639,6 +5655,8 @@ pub fn parse_pipeline(
} else {
match &pipeline.commands[0] {
LiteElement::Command(_, command)
| LiteElement::ErrPipedCommand(_, command)
| LiteElement::OutErrPipedCommand(_, command)
| LiteElement::Redirection(_, _, command, _)
| LiteElement::SeparateRedirection {
out: (_, command, _),
@ -5743,6 +5761,8 @@ pub fn parse_block(
if pipeline.commands.len() == 1 {
match &pipeline.commands[0] {
LiteElement::Command(_, command)
| LiteElement::ErrPipedCommand(_, command)
| LiteElement::OutErrPipedCommand(_, command)
| LiteElement::Redirection(_, _, command, _)
| LiteElement::SeparateRedirection {
out: (_, command, _),
@ -5836,6 +5856,8 @@ pub fn discover_captures_in_pipeline_element(
) -> Result<(), ParseError> {
match element {
PipelineElement::Expression(_, expression)
| PipelineElement::ErrPipedExpression(_, expression)
| PipelineElement::OutErrPipedExpression(_, expression)
| PipelineElement::Redirection(_, _, expression, _)
| PipelineElement::And(_, expression)
| PipelineElement::Or(_, expression) => {
@ -6181,6 +6203,18 @@ fn wrap_element_with_collect(
PipelineElement::Expression(span, expression) => {
PipelineElement::Expression(*span, wrap_expr_with_collect(working_set, expression))
}
PipelineElement::ErrPipedExpression(span, expression) => {
PipelineElement::ErrPipedExpression(
*span,
wrap_expr_with_collect(working_set, expression),
)
}
PipelineElement::OutErrPipedExpression(span, expression) => {
PipelineElement::OutErrPipedExpression(
*span,
wrap_expr_with_collect(working_set, expression),
)
}
PipelineElement::Redirection(span, redirection, expression, is_append_mode) => {
PipelineElement::Redirection(
*span,