diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 0b1ac3f87b..5a15543141 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -122,11 +122,13 @@ impl NuCompleter { for pipeline_element in pipeline.elements { match pipeline_element { PipelineElement::Expression(_, expr) - | PipelineElement::Redirection(_, _, expr) + | PipelineElement::Redirection(_, _, expr, _) | PipelineElement::And(_, expr) | PipelineElement::Or(_, expr) | PipelineElement::SameTargetRedirection { cmd: (_, expr), .. } - | PipelineElement::SeparateRedirection { out: (_, expr), .. } => { + | PipelineElement::SeparateRedirection { + out: (_, expr, _), .. + } => { let flattened: Vec<_> = flatten_expression(&working_set, &expr); let mut spans: Vec = vec![]; diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index babe1635da..3b7d9bdcb2 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -252,11 +252,11 @@ fn find_matching_block_end_in_block( for e in &p.elements { match e { PipelineElement::Expression(_, e) - | PipelineElement::Redirection(_, _, e) + | PipelineElement::Redirection(_, _, e, _) | PipelineElement::And(_, e) | PipelineElement::Or(_, e) | PipelineElement::SameTargetRedirection { cmd: (_, e), .. } - | PipelineElement::SeparateRedirection { out: (_, e), .. } => { + | PipelineElement::SeparateRedirection { out: (_, e, _), .. } => { if e.span.contains(global_cursor_offset) { if let Some(pos) = find_matching_block_end_in_expr( line, diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs index bad5bee609..4e9a5bda8e 100644 --- a/crates/nu-command/src/filesystem/save.rs +++ b/crates/nu-command/src/filesystem/save.rs @@ -1,7 +1,7 @@ use nu_engine::current_dir; use nu_engine::CallExt; use nu_path::expand_path_with; -use nu_protocol::ast::Call; +use nu_protocol::ast::{Call, Expr, Expression}; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ Category, Example, PipelineData, RawStream, ShellError, Signature, Span, Spanned, SyntaxShape, @@ -67,6 +67,24 @@ impl Command for Save { let append = call.has_flag("append"); let force = call.has_flag("force"); let progress = call.has_flag("progress"); + let out_append = if let Some(Expression { + expr: Expr::Bool(out_append), + .. + }) = call.get_parser_info("out-append") + { + *out_append + } else { + false + }; + let err_append = if let Some(Expression { + expr: Expr::Bool(err_append), + .. + }) = call.get_parser_info("err-append") + { + *err_append + } else { + false + }; let span = call.head; let cwd = current_dir(engine_state, stack)?; @@ -87,7 +105,7 @@ impl Command for Save { match input { PipelineData::ExternalStream { stdout: None, .. } => { // Open files to possibly truncate them - let _ = get_files(&path, stderr_path.as_ref(), append, force)?; + let _ = get_files(&path, stderr_path.as_ref(), append, false, false, force)?; Ok(PipelineData::empty()) } PipelineData::ExternalStream { @@ -95,7 +113,14 @@ impl Command for Save { stderr, .. } => { - let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?; + let (file, stderr_file) = get_files( + &path, + stderr_path.as_ref(), + append, + out_append, + err_append, + force, + )?; // delegate a thread to redirect stderr to result. let handler = stderr.map(|stderr_stream| match stderr_file { @@ -127,7 +152,14 @@ impl Command for Save { PipelineData::ListStream(ls, _) if raw || prepare_path(&path, append, force)?.0.extension().is_none() => { - let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; + let (mut file, _) = get_files( + &path, + stderr_path.as_ref(), + append, + out_append, + err_append, + force, + )?; for val in ls { file.write_all(&value_to_bytes(val)?) .map_err(|err| ShellError::IOError(err.to_string()))?; @@ -143,7 +175,14 @@ impl Command for Save { input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?; // Only open file after successful conversion - let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; + let (mut file, _) = get_files( + &path, + stderr_path.as_ref(), + append, + out_append, + err_append, + force, + )?; file.write_all(&bytes) .map_err(|err| ShellError::IOError(err.to_string()))?; @@ -319,17 +358,19 @@ fn get_files( path: &Spanned, stderr_path: Option<&Spanned>, append: bool, + out_append: bool, + err_append: bool, force: bool, ) -> Result<(File, Option), ShellError> { // First check both paths - let (path, path_span) = prepare_path(path, append, force)?; + let (path, path_span) = prepare_path(path, append || out_append, force)?; let stderr_path_and_span = stderr_path .as_ref() - .map(|stderr_path| prepare_path(stderr_path, append, force)) + .map(|stderr_path| prepare_path(stderr_path, append || err_append, force)) .transpose()?; // Only if both files can be used open and possibly truncate them - let file = open_file(path, path_span, append)?; + let file = open_file(path, path_span, append || out_append)?; let stderr_file = stderr_path_and_span .map(|(stderr_path, stderr_path_span)| { @@ -342,7 +383,7 @@ fn get_files( vec![], )) } else { - open_file(stderr_path, stderr_path_span, append) + open_file(stderr_path, stderr_path_span, append || err_append) } }) .transpose()?; diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs index 8570ab9004..90f0ecd41d 100644 --- a/crates/nu-command/src/formats/from/nuon.rs +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -124,7 +124,7 @@ impl Command for FromNuon { } else { match pipeline.elements.remove(0) { PipelineElement::Expression(_, expression) - | PipelineElement::Redirection(_, _, expression) + | PipelineElement::Redirection(_, _, expression, _) | PipelineElement::And(_, expression) | PipelineElement::Or(_, expression) | PipelineElement::SameTargetRedirection { @@ -132,7 +132,7 @@ impl Command for FromNuon { .. } | PipelineElement::SeparateRedirection { - out: (_, expression), + out: (_, expression, _), .. } => expression, } diff --git a/crates/nu-command/tests/commands/redirection.rs b/crates/nu-command/tests/commands/redirection.rs index f223d62f56..cae01dbbd9 100644 --- a/crates/nu-command/tests/commands/redirection.rs +++ b/crates/nu-command/tests/commands/redirection.rs @@ -12,6 +12,14 @@ fn redirect_err() { ); assert!(output.out.contains("asdfasdfasdf.txt")); + + // check append mode + let output = nu!( + cwd: dirs.test(), + "cat asdfasdfasdf.txt err>> a.txt; cat a.txt" + ); + let v: Vec<_> = output.out.match_indices("asdfasdfasdf.txt").collect(); + assert_eq!(v.len(), 2); }) } @@ -25,6 +33,12 @@ fn redirect_err() { ); assert!(output.out.contains("true")); + + let output = nu!( + cwd: dirs.test(), + "vol missingdrive err>> a; (open a | str stats).bytes >= 32" + ); + assert!(output.out.contains("true")); }) } @@ -39,6 +53,10 @@ fn redirect_outerr() { let output = nu!(cwd: dirs.test(), "cat a"); assert!(output.out.contains("asdfasdfasdf.txt")); + + let output = nu!(cwd: dirs.test(), "cat asdfasdfasdf.txt o+e>> a; cat a"); + let v: Vec<_> = output.out.match_indices("asdfasdfasdf.txt").collect(); + assert_eq!(v.len(), 2); }) } @@ -53,6 +71,13 @@ fn redirect_outerr() { let output = nu!(cwd: dirs.test(), "(open a | str stats).bytes >= 16"); assert!(output.out.contains("true")); + + nu!( + cwd: dirs.test(), + "vol missingdrive out+err>> a" + ); + let output = nu!(cwd: dirs.test(), "(open a | str stats).bytes >= 32"); + assert!(output.out.contains("true")); }) } @@ -65,6 +90,12 @@ fn redirect_out() { ); assert!(output.out.contains("hello")); + + let output = nu!( + cwd: dirs.test(), + "echo 'hello' out>> a; open a" + ); + assert!(output.out.contains("hellohello")); }) } @@ -124,6 +155,25 @@ fn separate_redirection() { let expected_err_file = dirs.test().join("err.txt"); let actual = file_contents(expected_err_file); assert!(actual.contains(expect_body)); + #[cfg(not(windows))] + { + sandbox.with_files(vec![FileWithContent("test.sh", script_body)]); + nu!( + cwd: dirs.test(), + "bash test.sh out>> out.txt err>> err.txt" + ); + // check for stdout redirection file. + let expected_out_file = dirs.test().join("out.txt"); + let actual = file_contents(expected_out_file); + let v: Vec<_> = actual.match_indices("message").collect(); + assert_eq!(v.len(), 2); + + // check for stderr redirection file. + let expected_err_file = dirs.test().join("err.txt"); + let actual = file_contents(expected_err_file); + let v: Vec<_> = actual.match_indices("message").collect(); + assert_eq!(v.len(), 2); + } }, ) } @@ -152,6 +202,21 @@ fn same_target_redirection_with_too_much_stderr_not_hang_nushell() { let expected_file = dirs.test().join("another_large_file.txt"); let actual = file_contents(expected_file); assert_eq!(actual, format!("{large_file_body}\n")); + + // not hangs in append mode either. + let cloned_body = large_file_body.clone(); + large_file_body.push_str(&format!("\n{cloned_body}")); + nu!( + cwd: dirs.test(), pipeline( + " + $env.LARGE = (open --raw a_large_file.txt); + nu --testbin echo_env_stderr LARGE out+err>> another_large_file.txt + " + ), + ); + let expected_file = dirs.test().join("another_large_file.txt"); + let actual = file_contents(expected_file); + assert_eq!(actual, format!("{large_file_body}\n")); }) } @@ -202,6 +267,16 @@ fn redirection_with_pipeline_works() { let expected_out_file = dirs.test().join("out.txt"); let actual = file_contents(expected_out_file); assert!(actual.contains(expect_body)); + + // check append mode works + nu!( + cwd: dirs.test(), + "bash test.sh o>> out.txt | describe" + ); + let expected_out_file = dirs.test().join("out.txt"); + let actual = file_contents(expected_out_file); + let v: Vec<_> = actual.match_indices("message").collect(); + assert_eq!(v.len(), 2); }, ) } @@ -224,6 +299,22 @@ fn redirect_support_variable() { let expected_out_file = dirs.test().join("tmp_file"); let actual = file_contents(expected_out_file); assert!(actual.contains("hello there")); + + // append mode support variable too. + let output = nu!( + cwd: dirs.test(), + "let x = 'tmp_file'; echo 'hello' out>> $x; open tmp_file" + ); + let v: Vec<_> = output.out.match_indices("hello").collect(); + assert_eq!(v.len(), 2); + + let output = nu!( + cwd: dirs.test(), + "let x = 'tmp_file'; echo 'hello' out+err>> $x; open tmp_file" + ); + // check for stdout redirection file. + let v: Vec<_> = output.out.match_indices("hello").collect(); + assert_eq!(v.len(), 3); }) } @@ -243,7 +334,7 @@ fn separate_redirection_support_variable() { sandbox.with_files(vec![FileWithContent("test.sh", script_body)]); nu!( cwd: dirs.test(), - r#"let o_f = "out.txt"; let e_f = "err.txt"; bash test.sh out> $o_f err> $e_f"# + r#"let o_f = "out2.txt"; let e_f = "err2.txt"; bash test.sh out> $o_f err> $e_f"# ); } #[cfg(windows)] @@ -251,18 +342,38 @@ fn separate_redirection_support_variable() { sandbox.with_files(vec![FileWithContent("test.bat", script_body)]); nu!( cwd: dirs.test(), - r#"let o_f = "out.txt"; let e_f = "err.txt"; cmd /D /c test.bat out> $o_f err> $e_f"# + r#"let o_f = "out2.txt"; let e_f = "err2.txt"; cmd /D /c test.bat out> $o_f err> $e_f"# ); } // check for stdout redirection file. - let expected_out_file = dirs.test().join("out.txt"); + let expected_out_file = dirs.test().join("out2.txt"); let actual = file_contents(expected_out_file); assert!(actual.contains(expect_body)); // check for stderr redirection file. - let expected_err_file = dirs.test().join("err.txt"); + let expected_err_file = dirs.test().join("err2.txt"); let actual = file_contents(expected_err_file); assert!(actual.contains(expect_body)); + + #[cfg(not(windows))] + { + sandbox.with_files(vec![FileWithContent("test.sh", script_body)]); + nu!( + cwd: dirs.test(), + r#"let o_f = "out2.txt"; let e_f = "err2.txt"; bash test.sh out>> $o_f err>> $e_f"# + ); + // check for stdout redirection file. + let expected_out_file = dirs.test().join("out2.txt"); + let actual = file_contents(expected_out_file); + let v: Vec<_> = actual.match_indices("message").collect(); + assert_eq!(v.len(), 2); + + // check for stderr redirection file. + let expected_err_file = dirs.test().join("err2.txt"); + let actual = file_contents(expected_err_file); + let v: Vec<_> = actual.match_indices("message").collect(); + assert_eq!(v.len(), 2); + } }, ) } diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 6bb4cc2130..fed9e451f5 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -7,7 +7,7 @@ use nu_protocol::{ }, engine::{Closure, EngineState, Stack}, DeclId, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range, Record, - ShellError, Span, Spanned, Unit, Value, VarId, ENV_VARIABLE_ID, + ShellError, Span, Spanned, Type, Unit, Value, VarId, ENV_VARIABLE_ID, }; use std::collections::HashMap; use std::thread::{self, JoinHandle}; @@ -754,7 +754,7 @@ fn eval_element_with_input( redirect_stdout, redirect_stderr, ), - PipelineElement::Redirection(span, redirection, expr) => { + PipelineElement::Redirection(span, redirection, expr, is_append_mode) => { match &expr.expr { Expr::String(_) | Expr::FullCellPath(_) @@ -793,7 +793,11 @@ fn eval_element_with_input( }; if let Some(save_command) = engine_state.find_decl(b"save", &[]) { - let save_call = gen_save_call(save_command, (*span, expr.clone()), None); + let save_call = gen_save_call( + save_command, + (*span, expr.clone(), *is_append_mode), + None, + ); match out_stream { None => { eval_call(engine_state, stack, &save_call, input).map(|_| { @@ -847,8 +851,8 @@ fn eval_element_with_input( } } PipelineElement::SeparateRedirection { - out: (out_span, out_expr), - err: (err_span, err_expr), + out: (out_span, out_expr, out_append_mode), + err: (err_span, err_expr, err_append_mode), } => match (&out_expr.expr, &err_expr.expr) { ( Expr::String(_) @@ -867,8 +871,8 @@ fn eval_element_with_input( }; let save_call = gen_save_call( save_command, - (*out_span, out_expr.clone()), - Some((*err_span, err_expr.clone())), + (*out_span, out_expr.clone(), *out_append_mode), + Some((*err_span, err_expr.clone(), *err_append_mode)), ); eval_call(engine_state, stack, &save_call, input).map(|_| { @@ -901,7 +905,7 @@ fn eval_element_with_input( }, PipelineElement::SameTargetRedirection { cmd: (cmd_span, cmd_exp), - redirection: (redirect_span, redirect_exp), + redirection: (redirect_span, redirect_exp, is_append_mode), } => { // general idea: eval cmd and call save command to redirect stdout to result. input = match &cmd_exp.expr { @@ -939,6 +943,7 @@ fn eval_element_with_input( *redirect_span, Redirection::Stdout, redirect_exp.clone(), + *is_append_mode, ), input, redirect_stdout, @@ -1021,8 +1026,8 @@ pub fn eval_block( let next_element = &elements[idx + 1]; if matches!( next_element, - PipelineElement::Redirection(_, Redirection::Stderr, _) - | PipelineElement::Redirection(_, Redirection::StdoutAndStderr, _) + PipelineElement::Redirection(_, Redirection::Stderr, _, _) + | PipelineElement::Redirection(_, Redirection::StdoutAndStderr, _, _) | PipelineElement::SeparateRedirection { .. } ) { redirect_stderr = true; @@ -1033,12 +1038,12 @@ pub fn eval_block( let next_element = &elements[idx + 1]; match next_element { // is next element a stdout relative redirection? - PipelineElement::Redirection(_, Redirection::Stdout, _) - | PipelineElement::Redirection(_, Redirection::StdoutAndStderr, _) + PipelineElement::Redirection(_, Redirection::Stdout, _, _) + | PipelineElement::Redirection(_, Redirection::StdoutAndStderr, _, _) | PipelineElement::SeparateRedirection { .. } | PipelineElement::Expression(..) => redirect_stdout = true, - PipelineElement::Redirection(_, Redirection::Stderr, _) => { + PipelineElement::Redirection(_, Redirection::Stderr, _, _) => { // a stderr redirection, but we still need to check for the next 2nd // element, to handle for the following case: // cat a.txt err> /dev/null | lines @@ -1199,10 +1204,19 @@ fn compute(size: i64, unit: Unit, span: Span) -> Result { fn gen_save_call( save_decl_id: DeclId, - out_info: (Span, Expression), - err_info: Option<(Span, Expression)>, + out_info: (Span, Expression, bool), + err_info: Option<(Span, Expression, bool)>, ) -> Call { - let (out_span, out_expr) = out_info; + let (out_span, out_expr, out_append_mode) = out_info; + let mut call = Call { + decl_id: save_decl_id, + head: out_span, + arguments: vec![], + redirect_stdout: false, + redirect_stderr: false, + parser_info: HashMap::new(), + }; + let mut args = vec![ Argument::Positional(out_expr), Argument::Named(( @@ -1222,7 +1236,18 @@ fn gen_save_call( None, )), ]; - if let Some((err_span, err_expr)) = err_info { + if out_append_mode { + call.set_parser_info( + "out-append".to_string(), + Expression { + expr: Expr::Bool(true), + span: out_span, + ty: Type::Bool, + custom_completion: None, + }, + ); + } + if let Some((err_span, err_expr, err_append_mode)) = err_info { args.push(Argument::Named(( Spanned { item: "stderr".into(), @@ -1230,17 +1255,22 @@ fn gen_save_call( }, None, Some(err_expr), - ))) + ))); + if err_append_mode { + call.set_parser_info( + "err-append".to_string(), + Expression { + expr: Expr::Bool(true), + span: err_span, + ty: Type::Bool, + custom_completion: None, + }, + ); + } } - Call { - decl_id: save_decl_id, - head: out_span, - arguments: args, - redirect_stdout: false, - redirect_stderr: false, - parser_info: HashMap::new(), - } + call.arguments.append(&mut args); + call } /// A job which saves `PipelineData` to a file in a child thread. diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index b2bf26f46b..a64d2c63b7 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -528,14 +528,14 @@ pub fn flatten_pipeline_element( flatten_expression(working_set, expr) } } - PipelineElement::Redirection(span, _, expr) => { + PipelineElement::Redirection(span, _, expr, _) => { let mut output = vec![(*span, FlatShape::Redirection)]; output.append(&mut flatten_expression(working_set, expr)); output } PipelineElement::SeparateRedirection { - out: (out_span, out_expr), - err: (err_span, err_expr), + out: (out_span, out_expr, _), + err: (err_span, err_expr, _), } => { let mut output = vec![(*out_span, FlatShape::Redirection)]; output.append(&mut flatten_expression(working_set, out_expr)); @@ -545,7 +545,7 @@ pub fn flatten_pipeline_element( } PipelineElement::SameTargetRedirection { cmd: (cmd_span, cmd_expr), - redirection: (redirect_span, redirect_expr), + redirection: (redirect_span, redirect_expr, _), } => { let mut output = if let Some(span) = cmd_span { let mut output = vec![(*span, FlatShape::Pipe)]; diff --git a/crates/nu-parser/src/lex.rs b/crates/nu-parser/src/lex.rs index 50f0909d3a..9a48f67f4f 100644 --- a/crates/nu-parser/src/lex.rs +++ b/crates/nu-parser/src/lex.rs @@ -8,8 +8,11 @@ pub enum TokenContents { PipePipe, Semicolon, OutGreaterThan, + OutGreaterGreaterThan, ErrGreaterThan, + ErrGreaterGreaterThan, OutErrGreaterThan, + OutErrGreaterGreaterThan, Eol, } @@ -260,14 +263,26 @@ pub fn lex_item( contents: TokenContents::OutGreaterThan, span, }, + b"out>>" | b"o>>" => Token { + contents: TokenContents::OutGreaterGreaterThan, + span, + }, b"err>" | b"e>" => Token { contents: TokenContents::ErrGreaterThan, span, }, + b"err>>" | b"e>>" => Token { + contents: TokenContents::ErrGreaterGreaterThan, + span, + }, b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token { contents: TokenContents::OutErrGreaterThan, span, }, + b"out+err>>" | b"err+out>>" | b"o+e>>" | b"e+o>>" => Token { + contents: TokenContents::OutErrGreaterGreaterThan, + span, + }, b"&&" => { err = Some(ParseError::ShellAndAnd(span)); Token { diff --git a/crates/nu-parser/src/lite_parser.rs b/crates/nu-parser/src/lite_parser.rs index 116993d50c..e5c22af912 100644 --- a/crates/nu-parser/src/lite_parser.rs +++ b/crates/nu-parser/src/lite_parser.rs @@ -37,16 +37,19 @@ impl LiteCommand { #[derive(Debug)] pub enum LiteElement { Command(Option, LiteCommand), - Redirection(Span, Redirection, 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 + // final bool field indicates if it's in append mode SeparateRedirection { - out: (Span, LiteCommand), - err: (Span, LiteCommand), + out: (Span, LiteCommand, bool), + err: (Span, LiteCommand, bool), }, // SameTargetRedirection variant can only be generated by Command with Redirection::OutAndErr + // redirection's final bool field indicates if it's in append mode SameTargetRedirection { cmd: (Option, LiteCommand), - redirection: (Span, LiteCommand), + redirection: (Span, LiteCommand, bool), }, } @@ -72,10 +75,10 @@ impl LitePipeline { self.commands.is_empty() } - pub fn exists(&self, new_target: Redirection) -> bool { + pub fn exists(&self, new_target: &Redirection) -> bool { for cmd in &self.commands { - if let LiteElement::Redirection(_, exists_target, _) = cmd { - if exists_target == &new_target { + if let LiteElement::Redirection(_, exists_target, _, _) = cmd { + if exists_target == new_target { return true; } } @@ -121,7 +124,12 @@ impl LiteBlock { if let LiteElement::Command(..) = cmd { cmd_index = Some(index); } - if let LiteElement::Redirection(_span, Redirection::StdoutAndStderr, _target_cmd) = cmd + if let LiteElement::Redirection( + _span, + Redirection::StdoutAndStderr, + _target_cmd, + _is_append_mode, + ) = cmd { outerr_index = Some(index); break; @@ -134,14 +142,14 @@ impl LiteBlock { // `outerr_redirect` and `cmd` should always be `LiteElement::Command` and `LiteElement::Redirection` if let ( LiteElement::Command(cmd_span, lite_cmd), - LiteElement::Redirection(span, _, outerr_cmd), + LiteElement::Redirection(span, _, outerr_cmd, is_append_mode), ) = (cmd, outerr_redirect) { pipeline.insert( cmd_index, LiteElement::SameTargetRedirection { cmd: (cmd_span, lite_cmd), - redirection: (span, outerr_cmd), + redirection: (span, outerr_cmd, is_append_mode), }, ) } @@ -154,7 +162,8 @@ impl LiteBlock { let mut stdout_index = None; let mut stderr_index = None; for (index, cmd) in pipeline.commands.iter().enumerate() { - if let LiteElement::Redirection(_span, redirection, _target_cmd) = cmd { + if let LiteElement::Redirection(_span, redirection, _target_cmd, _is_append_mode) = cmd + { match *redirection { Redirection::Stderr => stderr_index = Some(index), Redirection::Stdout => stdout_index = Some(index), @@ -178,8 +187,8 @@ impl LiteBlock { }; // `out_redirect` and `err_redirect` should always be `LiteElement::Redirection` if let ( - LiteElement::Redirection(out_span, _, out_command), - LiteElement::Redirection(err_span, _, err_command), + LiteElement::Redirection(out_span, _, out_command, out_append_mode), + LiteElement::Redirection(err_span, _, err_command, err_append_mode), ) = (out_redirect, err_redirect) { // using insert with specific index to keep original @@ -187,8 +196,8 @@ impl LiteBlock { pipeline.insert( new_indx, LiteElement::SeparateRedirection { - out: (out_span, out_command), - err: (err_span, err_command), + out: (out_span, out_command, out_append_mode), + err: (err_span, err_command, err_append_mode), }, ) } @@ -244,8 +253,11 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option) { last_token = TokenContents::Item; } TokenContents::OutGreaterThan + | TokenContents::OutGreaterGreaterThan | TokenContents::ErrGreaterThan - | TokenContents::OutErrGreaterThan => { + | TokenContents::ErrGreaterGreaterThan + | TokenContents::OutErrGreaterThan + | TokenContents::OutErrGreaterGreaterThan => { if let Some(err) = push_command_to( &mut curr_pipeline, curr_command, @@ -375,6 +387,18 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option) { } } +fn get_redirection(connector: TokenContents) -> Option<(Redirection, bool)> { + match connector { + TokenContents::OutGreaterThan => Some((Redirection::Stdout, false)), + TokenContents::OutGreaterGreaterThan => Some((Redirection::Stdout, true)), + TokenContents::ErrGreaterThan => Some((Redirection::Stderr, false)), + TokenContents::ErrGreaterGreaterThan => Some((Redirection::Stderr, true)), + TokenContents::OutErrGreaterThan => Some((Redirection::StdoutAndStderr, false)), + TokenContents::OutErrGreaterGreaterThan => Some((Redirection::StdoutAndStderr, true)), + _ => None, + } +} + /// push a `command` to `pipeline` /// /// It will return Some(err) if `command` is empty and we want to push a @@ -386,24 +410,11 @@ fn push_command_to( last_connector_span: Option, ) -> Option { if !command.is_empty() { - match last_connector { - TokenContents::OutGreaterThan => { + match get_redirection(last_connector) { + Some((redirect, is_append_mode)) => { let span = last_connector_span .expect("internal error: redirection missing span information"); - if pipeline.exists(Redirection::Stdout) { - return Some(ParseError::LabeledError( - "Redirection can be set only once".into(), - "try to remove one".into(), - span, - )); - } - - pipeline.push(LiteElement::Redirection(span, Redirection::Stdout, command)); - } - TokenContents::ErrGreaterThan => { - let span = last_connector_span - .expect("internal error: redirection missing span information"); - if pipeline.exists(Redirection::Stderr) { + if pipeline.exists(&redirect) { return Some(ParseError::LabeledError( "Redirection can be set only once".into(), "try to remove one".into(), @@ -413,32 +424,20 @@ fn push_command_to( pipeline.push(LiteElement::Redirection( last_connector_span .expect("internal error: redirection missing span information"), - Redirection::Stderr, + redirect, command, - )); - } - TokenContents::OutErrGreaterThan => { - pipeline.push(LiteElement::Redirection( - last_connector_span - .expect("internal error: redirection missing span information"), - Redirection::StdoutAndStderr, - command, - )); - } - _ => { - pipeline.push(LiteElement::Command(last_connector_span, command)); + is_append_mode, + )) } + None => pipeline.push(LiteElement::Command(last_connector_span, command)), } None + } else if get_redirection(last_connector).is_some() { + Some(ParseError::Expected( + "redirection target", + last_connector_span.expect("internal error: redirection missing span information"), + )) } else { - match last_connector { - TokenContents::OutGreaterThan - | TokenContents::ErrGreaterThan - | TokenContents::OutErrGreaterThan => Some(ParseError::Expected( - "redirection target", - last_connector_span.expect("internal error: redirection missing span information"), - )), - _ => None, - } + None } } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 0697c90764..b93d3b0f2c 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -1846,11 +1846,12 @@ pub fn parse_module_block( } } } - LiteElement::Redirection(_, _, command) => { + LiteElement::Redirection(_, _, command, _) => { block.pipelines.push(garbage_pipeline(&command.parts)) } LiteElement::SeparateRedirection { - out: (_, command), .. + out: (_, command, _), + .. } => block.pipelines.push(garbage_pipeline(&command.parts)), LiteElement::SameTargetRedirection { cmd: (_, command), .. diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index cf30f46c24..47964c4754 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1219,8 +1219,11 @@ fn parse_binary_with_base( TokenContents::Pipe | TokenContents::PipePipe | TokenContents::OutGreaterThan + | TokenContents::OutGreaterGreaterThan | TokenContents::ErrGreaterThan - | TokenContents::OutErrGreaterThan => { + | TokenContents::ErrGreaterGreaterThan + | TokenContents::OutErrGreaterThan + | TokenContents::OutErrGreaterGreaterThan => { working_set.error(ParseError::Expected("binary", span)); return garbage(span); } @@ -5362,14 +5365,14 @@ pub fn parse_pipeline( PipelineElement::Expression(*span, expr) } - LiteElement::Redirection(span, redirection, command) => { + LiteElement::Redirection(span, redirection, command, is_append_mode) => { let expr = parse_value(working_set, command.parts[0], &SyntaxShape::Any); - PipelineElement::Redirection(*span, redirection.clone(), expr) + PipelineElement::Redirection(*span, redirection.clone(), expr, *is_append_mode) } LiteElement::SeparateRedirection { - out: (out_span, out_command), - err: (err_span, err_command), + out: (out_span, out_command, out_append_mode), + err: (err_span, err_command, err_append_mode), } => { trace!("parsing: pipeline element: separate redirection"); let out_expr = @@ -5379,13 +5382,13 @@ pub fn parse_pipeline( parse_value(working_set, err_command.parts[0], &SyntaxShape::Any); PipelineElement::SeparateRedirection { - out: (*out_span, out_expr), - err: (*err_span, err_expr), + out: (*out_span, out_expr, *out_append_mode), + err: (*err_span, err_expr, *err_append_mode), } } LiteElement::SameTargetRedirection { cmd: (cmd_span, command), - redirection: (redirect_span, redirect_command), + redirection: (redirect_span, redirect_command, is_append_mode), } => { trace!("parsing: pipeline element: same target redirection"); let expr = parse_expression(working_set, &command.parts, is_subexpression); @@ -5393,7 +5396,7 @@ pub fn parse_pipeline( parse_value(working_set, redirect_command.parts[0], &SyntaxShape::Any); PipelineElement::SameTargetRedirection { cmd: (*cmd_span, expr), - redirection: (*redirect_span, redirect_expr), + redirection: (*redirect_span, redirect_expr, *is_append_mode), } } }) @@ -5417,9 +5420,10 @@ pub fn parse_pipeline( } else { match &pipeline.commands[0] { LiteElement::Command(_, command) - | LiteElement::Redirection(_, _, command) + | LiteElement::Redirection(_, _, command, _) | LiteElement::SeparateRedirection { - out: (_, command), .. + out: (_, command, _), + .. } => { let mut pipeline = parse_builtin_commands(working_set, command, is_subexpression); @@ -5477,7 +5481,7 @@ pub fn parse_pipeline( } LiteElement::SameTargetRedirection { cmd: (span, command), - redirection: (redirect_span, redirect_cmd), + redirection: (redirect_span, redirect_cmd, is_append_mode), } => { trace!("parsing: pipeline element: same target redirection"); let expr = parse_expression(working_set, &command.parts, is_subexpression); @@ -5488,7 +5492,7 @@ pub fn parse_pipeline( Pipeline { elements: vec![PipelineElement::SameTargetRedirection { cmd: (*span, expr), - redirection: (*redirect_span, redirect_expr), + redirection: (*redirect_span, redirect_expr, *is_append_mode), }], } } @@ -5520,9 +5524,10 @@ pub fn parse_block( if pipeline.commands.len() == 1 { match &pipeline.commands[0] { LiteElement::Command(_, command) - | LiteElement::Redirection(_, _, command) + | LiteElement::Redirection(_, _, command, _) | LiteElement::SeparateRedirection { - out: (_, command), .. + out: (_, command, _), + .. } | LiteElement::SameTargetRedirection { cmd: (_, command), .. @@ -5612,14 +5617,14 @@ pub fn discover_captures_in_pipeline_element( ) -> Result<(), ParseError> { match element { PipelineElement::Expression(_, expression) - | PipelineElement::Redirection(_, _, expression) + | PipelineElement::Redirection(_, _, expression, _) | PipelineElement::And(_, expression) | PipelineElement::Or(_, expression) => { discover_captures_in_expr(working_set, expression, seen, seen_blocks, output) } PipelineElement::SeparateRedirection { - out: (_, out_expr), - err: (_, err_expr), + out: (_, out_expr, _), + err: (_, err_expr, _), } => { discover_captures_in_expr(working_set, out_expr, seen, seen_blocks, output)?; discover_captures_in_expr(working_set, err_expr, seen, seen_blocks, output)?; @@ -5627,7 +5632,7 @@ pub fn discover_captures_in_pipeline_element( } PipelineElement::SameTargetRedirection { cmd: (_, cmd_expr), - redirection: (_, redirect_expr), + redirection: (_, redirect_expr, _), } => { discover_captures_in_expr(working_set, cmd_expr, seen, seen_blocks, output)?; discover_captures_in_expr(working_set, redirect_expr, seen, seen_blocks, output)?; @@ -5934,28 +5939,38 @@ fn wrap_element_with_collect( PipelineElement::Expression(span, expression) => { PipelineElement::Expression(*span, wrap_expr_with_collect(working_set, expression)) } - PipelineElement::Redirection(span, redirection, expression) => { + PipelineElement::Redirection(span, redirection, expression, is_append_mode) => { PipelineElement::Redirection( *span, redirection.clone(), wrap_expr_with_collect(working_set, expression), + *is_append_mode, ) } PipelineElement::SeparateRedirection { - out: (out_span, out_exp), - err: (err_span, err_exp), + out: (out_span, out_exp, out_append_mode), + err: (err_span, err_exp, err_append_mode), } => PipelineElement::SeparateRedirection { - out: (*out_span, wrap_expr_with_collect(working_set, out_exp)), - err: (*err_span, wrap_expr_with_collect(working_set, err_exp)), + out: ( + *out_span, + wrap_expr_with_collect(working_set, out_exp), + *out_append_mode, + ), + err: ( + *err_span, + wrap_expr_with_collect(working_set, err_exp), + *err_append_mode, + ), }, PipelineElement::SameTargetRedirection { cmd: (cmd_span, cmd_exp), - redirection: (redirect_span, redirect_exp), + redirection: (redirect_span, redirect_exp, is_append_mode), } => PipelineElement::SameTargetRedirection { cmd: (*cmd_span, wrap_expr_with_collect(working_set, cmd_exp)), redirection: ( *redirect_span, wrap_expr_with_collect(working_set, redirect_exp), + *is_append_mode, ), }, PipelineElement::And(span, expression) => { diff --git a/crates/nu-protocol/src/ast/block.rs b/crates/nu-protocol/src/ast/block.rs index 0840cf642c..21b3e77fa7 100644 --- a/crates/nu-protocol/src/ast/block.rs +++ b/crates/nu-protocol/src/ast/block.rs @@ -71,7 +71,7 @@ impl Block { if let Some(last) = last.elements.last() { match last { PipelineElement::Expression(_, expr) => expr.ty.clone(), - PipelineElement::Redirection(_, _, _) => Type::Any, + PipelineElement::Redirection(_, _, _, _) => Type::Any, PipelineElement::SeparateRedirection { .. } => Type::Any, PipelineElement::SameTargetRedirection { .. } => Type::Any, PipelineElement::And(_, expr) => expr.ty.clone(), diff --git a/crates/nu-protocol/src/ast/pipeline.rs b/crates/nu-protocol/src/ast/pipeline.rs index b3a589b1d2..0abb18f107 100644 --- a/crates/nu-protocol/src/ast/pipeline.rs +++ b/crates/nu-protocol/src/ast/pipeline.rs @@ -13,14 +13,17 @@ pub enum Redirection { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PipelineElement { Expression(Option, Expression), - Redirection(Span, Redirection, Expression), + // final field indicates if it's in append mode + Redirection(Span, Redirection, Expression, bool), + // final bool field indicates if it's in append mode SeparateRedirection { - out: (Span, Expression), - err: (Span, Expression), + out: (Span, Expression, bool), + err: (Span, Expression, bool), }, + // redirection's final bool field indicates if it's in append mode SameTargetRedirection { cmd: (Option, Expression), - redirection: (Span, Expression), + redirection: (Span, Expression, bool), }, And(Span, Expression), Or(Span, Expression), @@ -30,9 +33,9 @@ impl PipelineElement { pub fn expression(&self) -> &Expression { match self { PipelineElement::Expression(_, expression) => expression, - PipelineElement::Redirection(_, _, expression) => expression, + PipelineElement::Redirection(_, _, expression, _) => expression, PipelineElement::SeparateRedirection { - out: (_, expression), + out: (_, expression, _), .. } => expression, PipelineElement::SameTargetRedirection { @@ -52,9 +55,9 @@ impl PipelineElement { .. } => expression.span, PipelineElement::Expression(Some(span), expression) - | PipelineElement::Redirection(span, _, expression) + | PipelineElement::Redirection(span, _, expression, _) | PipelineElement::SeparateRedirection { - out: (span, expression), + out: (span, expression, _), .. } | PipelineElement::And(span, expression) @@ -71,7 +74,7 @@ impl PipelineElement { pub fn has_in_variable(&self, working_set: &StateWorkingSet) -> bool { match self { PipelineElement::Expression(_, expression) - | PipelineElement::Redirection(_, _, expression) + | PipelineElement::Redirection(_, _, expression, _) | PipelineElement::And(_, expression) | PipelineElement::Or(_, expression) | PipelineElement::SameTargetRedirection { @@ -79,8 +82,8 @@ impl PipelineElement { .. } => expression.has_in_variable(working_set), PipelineElement::SeparateRedirection { - out: (_, out_expr), - err: (_, err_expr), + out: (_, out_expr, _), + err: (_, err_expr, _), } => out_expr.has_in_variable(working_set) || err_expr.has_in_variable(working_set), } } @@ -88,7 +91,7 @@ impl PipelineElement { pub fn replace_in_variable(&mut self, working_set: &mut StateWorkingSet, new_var_id: VarId) { match self { PipelineElement::Expression(_, expression) - | PipelineElement::Redirection(_, _, expression) + | PipelineElement::Redirection(_, _, expression, _) | PipelineElement::And(_, expression) | PipelineElement::Or(_, expression) | PipelineElement::SameTargetRedirection { @@ -96,8 +99,8 @@ impl PipelineElement { .. } => expression.replace_in_variable(working_set, new_var_id), PipelineElement::SeparateRedirection { - out: (_, out_expr), - err: (_, err_expr), + out: (_, out_expr, _), + err: (_, err_expr, _), } => { if out_expr.has_in_variable(working_set) { out_expr.replace_in_variable(working_set, new_var_id) @@ -117,7 +120,7 @@ impl PipelineElement { ) { match self { PipelineElement::Expression(_, expression) - | PipelineElement::Redirection(_, _, expression) + | PipelineElement::Redirection(_, _, expression, _) | PipelineElement::And(_, expression) | PipelineElement::Or(_, expression) | PipelineElement::SameTargetRedirection { @@ -125,7 +128,7 @@ impl PipelineElement { .. } | PipelineElement::SeparateRedirection { - out: (_, expression), + out: (_, expression, _), .. } => expression.replace_span(working_set, replaced, new_span), }