diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs index 80727dd..f0545e6 100644 --- a/brush-core/src/interp.rs +++ b/brush-core/src/interp.rs @@ -363,6 +363,27 @@ impl Execute for ast::AndOrList { } } +/// Whether a failing status from this pipeline re-triggers errexit / the ERR trap. +/// Mirrors bash: true for multi-command pipelines and for single commands that are +/// simple commands (incl. function calls), subshells, extended tests (`[[ ]]`), or +/// arithmetic commands (`(( ))`). False for the looping/grouping compounds (brace +/// group, if, case, for, while/until, …) whose inner commands have already had their +/// own errexit adjudication — their aggregated status must not re-trigger it. +fn errexit_applies_to_pipeline(pipeline: &ast::Pipeline) -> bool { + if pipeline.seq.len() != 1 { + return true; + } + match &pipeline.seq[0] { + ast::Command::Simple(_) | ast::Command::Function(_) | ast::Command::ExtendedTest(..) => { + true + } + ast::Command::Compound(compound, _) => matches!( + compound, + ast::CompoundCommand::Subshell(_) | ast::CompoundCommand::Arithmetic(_) + ), + } +} + #[async_trait::async_trait] impl Execute for ast::Pipeline { async fn execute( @@ -401,11 +422,21 @@ impl Execute for ast::Pipeline { // Update exit status. shell.set_last_exit_status(result.exit_code.into()); + // bash applies errexit (and the ERR trap) to a failing pipeline's status only + // when that status comes from a command whose failure wasn't already + // adjudicated inside a compound body: simple commands (incl. function calls), + // subshells, extended tests, arithmetic commands, and real multi-command + // pipelines. A brace group / if / case / for / while whose body ends with a + // short-circuited AND-OR list (or a `!`-negated pipeline) must NOT re-trigger + // errexit on its aggregated status (divergence D3, shell/corpus/DIVERGENCES.md; + // observed via tzdata's postinst `which restorecon && restorecon` under set -e). + let errexit_eligible = errexit_applies_to_pipeline(self); + // Fire the ERR trap if the pipeline failed in a non-conditional context. // We reuse `suppress_errexit` here because bash suppresses the ERR trap in // exactly the same contexts it suppresses errexit (conditionals, `!`-prefixed // pipelines, etc.). - if !result.is_success() && !params.suppress_errexit && !self.bang { + if !result.is_success() && !params.suppress_errexit && !self.bang && errexit_eligible { if shell.traps().handles(crate::traps::TrapSignal::Err) { shell .invoke_trap_handler(crate::traps::TrapSignal::Err, ¶ms) @@ -414,7 +445,7 @@ impl Execute for ast::Pipeline { } // Apply errexit if not suppressed (and not negated) - if !params.suppress_errexit && !self.bang { + if !params.suppress_errexit && !self.bang && errexit_eligible { shell.apply_errexit_if_enabled(&mut result); }