mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02:00
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:
@ -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));
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user