diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs index 5e5d0ddf..f2ebb604 100644 --- a/brush-core/src/interp.rs +++ b/brush-core/src/interp.rs @@ -307,8 +307,9 @@ impl Execute for ast::Pipeline { let mut spawn_results = VecDeque::new(); for (current_pipeline_index, command) in self.seq.iter().enumerate() { - // If there's only one command in the pipeline, then we run directly in the current shell. - // Otherwise, we spawn a separate subshell for each command in the pipeline. + // If there's only one command in the pipeline, then we run directly in the current + // shell. Otherwise, we spawn a separate subshell for each command in the + // pipeline. let spawn_result = if pipeline_len > 1 { let mut subshell = shell.clone(); let mut pipeline_context = PipelineExecutionContext { @@ -354,8 +355,9 @@ impl Execute for ast::Pipeline { let mut child_future = Box::pin(child.wait_with_output()); - // Wait for the process to exit or for a relevant signal, whichever happens first. - // TODO: Figure out how to detect a SIGSTOP'd process. + // Wait for the process to exit or for a relevant signal, whichever happens + // first. TODO: Figure out how to detect a SIGSTOP'd + // process. let wait_result = if stopped.is_empty() { loop { tokio::select! { @@ -879,8 +881,9 @@ impl ExecuteInPipeline for ast::SimpleCommand { basic_expand_assignment(context.shell, assignment).await?; args.push(CommandArg::Assignment(expanded)); } else { - // This *looks* like an assignment, but it's really a string we should fully - // treat as a regular looking argument. + // This *looks* like an assignment, but it's really a string we should + // fully treat as a regular looking + // argument. let mut next_args = expansion::full_expand_and_split_word(context.shell, word) .await? @@ -914,8 +917,9 @@ impl ExecuteInPipeline for ast::SimpleCommand { next_args = alias_pieces; } - // Check if we're going to be invoking a special declaration builtin. That will - // change how we parse and process args. + // Check if we're going to be invoking a special declaration builtin. + // That will change how we parse and process + // args. if context .shell .builtins @@ -1002,7 +1006,7 @@ impl ExecuteInPipeline for ast::SimpleCommand { // Execute. let execution_result = - commands::execute(cmd_context, args, true /*use functions?*/).await; + commands::execute(cmd_context, args, true /* use functions? */).await; // Pop off that ephemeral environment scope. context.shell.env.pop_scope(EnvironmentScope::Command)?; @@ -1103,7 +1107,8 @@ async fn apply_assignment( required_scope: Option, creation_scope: EnvironmentScope, ) -> Result<(), error::Error> { - // Figure out if we are trying to assign to a variable or assign to an element of an existing array. + // Figure out if we are trying to assign to a variable or assign to an element of an existing + // array. let mut array_index; let variable_name = match &assignment.name { ast::AssignmentName::VariableName(name) => { @@ -1263,7 +1268,7 @@ pub(crate) async fn setup_redirect<'a>( redirect: &ast::IoRedirect, ) -> Result, error::Error> { match redirect { - ast::IoRedirect::OutputAndError(f) => { + ast::IoRedirect::OutputAndError(f, append) => { let mut expanded_file_path = expansion::full_expand_and_split_word(shell, f).await?; if expanded_file_path.len() != 1 { return Err(error::Error::InvalidRedirection); @@ -1274,7 +1279,8 @@ pub(crate) async fn setup_redirect<'a>( let opened_file = std::fs::File::options() .create(true) .write(true) - .truncate(true) + .truncate(!*append) + .append(*append) .open(expanded_file_path.as_str()) .map_err(|err| error::Error::RedirectionFailure(expanded_file_path, err))?; diff --git a/brush-parser/src/ast.rs b/brush-parser/src/ast.rs index 163efaf7..aa2cdc24 100644 --- a/brush-parser/src/ast.rs +++ b/brush-parser/src/ast.rs @@ -703,8 +703,8 @@ pub enum IoRedirect { HereDocument(Option, IoHereDocument), /// Redirection from a here-string. HereString(Option, Word), - /// Redirection of both standard output and standard error. - OutputAndError(Word), + /// Redirection of both standard output and standard error (with optional append). + OutputAndError(Word, bool), } impl Display for IoRedirect { @@ -717,8 +717,12 @@ impl Display for IoRedirect { write!(f, "{} {}", kind, target)?; } - IoRedirect::OutputAndError(target) => { - write!(f, "&> {}", target)?; + IoRedirect::OutputAndError(target, append) => { + write!(f, "&>")?; + if *append { + write!(f, ">")?; + } + write!(f, " {}", target)?; } IoRedirect::HereDocument( fd_num, @@ -934,11 +938,14 @@ pub enum UnaryPredicate { FileExistsAndIsWritable, /// Computes if the operand is a path to an existing file that is executable. FileExistsAndIsExecutable, - /// Computes if the operand is a path to an existing file owned by the current context's effective group ID. + /// Computes if the operand is a path to an existing file owned by the current context's + /// effective group ID. FileExistsAndOwnedByEffectiveGroupId, - /// Computes if the operand is a path to an existing file that has been modified since last being read. + /// Computes if the operand is a path to an existing file that has been modified since last + /// being read. FileExistsAndModifiedSinceLastRead, - /// Computes if the operand is a path to an existing file owned by the current context's effective user ID. + /// Computes if the operand is a path to an existing file owned by the current context's + /// effective user ID. FileExistsAndOwnedByEffectiveUserId, /// Computes if the operand is a path to an existing socket file. FileExistsAndIsSocket, diff --git a/brush-parser/src/parser.rs b/brush-parser/src/parser.rs index fec115ff..165b516b 100644 --- a/brush-parser/src/parser.rs +++ b/brush-parser/src/parser.rs @@ -580,7 +580,8 @@ peg::parser! { let (kind, target) = f; ast::IoRedirect::File(n, kind, target) } / - non_posix_extensions_enabled() specific_operator("&>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target)) } / + non_posix_extensions_enabled() specific_operator("&>>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), true) } / + non_posix_extensions_enabled() specific_operator("&>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), false) } / non_posix_extensions_enabled() n:io_number()? specific_operator("<<<") w:word() { ast::IoRedirect::HereString(n, ast::Word::from(w)) } / n:io_number()? h:io_here() { ast::IoRedirect::HereDocument(n, h) } / expected!("I/O redirect") diff --git a/brush-parser/src/tokenizer.rs b/brush-parser/src/tokenizer.rs index 1388a6ec..da35c56f 100644 --- a/brush-parser/src/tokenizer.rs +++ b/brush-parser/src/tokenizer.rs @@ -927,7 +927,7 @@ impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> { fn is_operator(&self, s: &str) -> bool { // Handle non-POSIX operators. - if !self.options.posix_mode && matches!(s, "<<<" | "&>") { + if !self.options.posix_mode && matches!(s, "<<<" | "&>" | "&>>") { return true; } diff --git a/brush-shell/tests/cases/redirection.yaml b/brush-shell/tests/cases/redirection.yaml index c2f67913..ad6e85fa 100644 --- a/brush-shell/tests/cases/redirection.yaml +++ b/brush-shell/tests/cases/redirection.yaml @@ -40,6 +40,7 @@ cases: - name: "Redirect stdout and stderr" stdin: | ls -d . non-existent-dir &>/dev/null + ls -d . non-existent-dir &>>/dev/null - name: "Process substitution: input + output" known_failure: true