Merge nucleic/keen-glass-marten-ddlf into dev
This commit is contained in:
@@ -13,6 +13,8 @@ DerivedData/
|
|||||||
# untracked, they dirty the autoship merge target and block integration.
|
# untracked, they dirty the autoship merge target and block integration.
|
||||||
test-transcripts/
|
test-transcripts/
|
||||||
nucleic-*.md
|
nucleic-*.md
|
||||||
|
# Python bytecode from the shell/corpus harnesses (replay.py imported by overhead.py).
|
||||||
|
__pycache__/
|
||||||
# Bundled Linux kernel staged by scripts/fetch-kernel.sh — large binary, not tracked.
|
# Bundled Linux kernel staged by scripts/fetch-kernel.sh — large binary, not tracked.
|
||||||
# (Resources/ otherwise holds tracked items like AppIcon.icns.)
|
# (Resources/ otherwise holds tracked items like AppIcon.icns.)
|
||||||
Resources/vmlinux-arm64
|
Resources/vmlinux-arm64
|
||||||
|
|||||||
@@ -10,6 +10,34 @@ stays open until fixed in the fork (or upstream) and re-verified by the corpus.
|
|||||||
|
|
||||||
## Closed (recent)
|
## Closed (recent)
|
||||||
|
|
||||||
|
### D3 — errexit re-triggered by a compound command's aggregated status (fixed in fork)
|
||||||
|
|
||||||
|
- **Found**: narOS N2 first agent-tier CI build (run 29669891162): with
|
||||||
|
`/bin/sh → nash` baked into naros-base, trixie's `tzdata` postinst exited 1
|
||||||
|
under dpkg configure, cascading into unconfigured python3/nodejs and failing
|
||||||
|
the whole `naros-agent` image build — exactly the loud-in-CI dogfood failure
|
||||||
|
mode the agent Dockerfile courts on purpose.
|
||||||
|
- **Repro**: `set -e; if true; then false && true; fi; echo hi` → bash prints
|
||||||
|
`hi` (exit 0), nash exited 1. In tzdata's postinst the trigger was
|
||||||
|
`which restorecon >/dev/null 2>&1 && restorecon …` (restorecon absent) as
|
||||||
|
the last statement of an `if` body under `set -e`.
|
||||||
|
- **Root cause**: brush applies errexit at `Pipeline::execute`, and an
|
||||||
|
`if`/brace-group/`case`/`for` compound is itself a (single-command) pipeline
|
||||||
|
— so a failure that was already exempt *inside* the compound (short-circuited
|
||||||
|
AND-OR list, `!`-negated pipeline) re-triggered errexit on the compound's
|
||||||
|
aggregated status. bash never re-adjudicates a grouping/looping compound's
|
||||||
|
status; it does re-trigger for simple commands (incl. function calls),
|
||||||
|
subshells, `[[ ]]`, and `(( ))`.
|
||||||
|
- **Fix**: `errexit_applies_to_pipeline` in `brush-core/src/interp.rs` — the
|
||||||
|
pipeline-level errexit/ERR-trap application now fires only for multi-command
|
||||||
|
pipelines, simple commands, subshells, extended tests, and arithmetic
|
||||||
|
commands. Verified against bash on a 15-case matrix (if/brace/case/for/while
|
||||||
|
bodies, function calls, subshells, cmdsub assignment, `!`, `||`, pipe-to-cat,
|
||||||
|
`[[ ]]`, compound redirect failures) — all matching; trixie tzdata postinst
|
||||||
|
green under nash; corpus 101/101 = 100%; candidate for upstreaming.
|
||||||
|
- **Regression tests**: corpus `errexit-if-andlist`, `errexit-brace-andlist`,
|
||||||
|
`errexit-if-bang`, `errexit-fn-status`.
|
||||||
|
|
||||||
### D2 — non-ASCII bytes re-encoded through `read`/`echo` under C/empty locale (fixed in fork)
|
### D2 — non-ASCII bytes re-encoded through `read`/`echo` under C/empty locale (fixed in fork)
|
||||||
|
|
||||||
- **Found**: narOS N0 rootfs validation (first real-world install run under the
|
- **Found**: narOS N0 rootfs validation (first real-world install run under the
|
||||||
|
|||||||
@@ -95,3 +95,7 @@
|
|||||||
{"id": "utf8-read-passthru", "cmd": "printf 'F\\305\\221tan\\303\\272s\\303\\255tv\\303\\241ny\\n' | { read x; printf '%s\\n' \"$x\"; }"}
|
{"id": "utf8-read-passthru", "cmd": "printf 'F\\305\\221tan\\303\\272s\\303\\255tv\\303\\241ny\\n' | { read x; printf '%s\\n' \"$x\"; }"}
|
||||||
{"id": "utf8-while-read-file", "cmd": "printf 'caf\\303\\251.crt\\nna\\303\\257ve.pem\\n' > names.txt; while read n; do echo \"got:$n\"; done < names.txt"}
|
{"id": "utf8-while-read-file", "cmd": "printf 'caf\\303\\251.crt\\nna\\303\\257ve.pem\\n' > names.txt; while read n; do echo \"got:$n\"; done < names.txt"}
|
||||||
{"id": "utf8-cmdsub-roundtrip", "cmd": "x=$(printf '\\303\\251l\\303\\251gant'); echo \"$x\""}
|
{"id": "utf8-cmdsub-roundtrip", "cmd": "x=$(printf '\\303\\251l\\303\\251gant'); echo \"$x\""}
|
||||||
|
{"id": "errexit-if-andlist", "cmd": "set -e; if true; then false && true; fi; echo survived"}
|
||||||
|
{"id": "errexit-brace-andlist", "cmd": "set -e; { false && true; }; echo grouped-ok"}
|
||||||
|
{"id": "errexit-if-bang", "cmd": "set -e; if true; then ! true; fi; echo bang-ok"}
|
||||||
|
{"id": "errexit-fn-status", "cmd": "set -e; f() { false && true; }; f && echo unreachable || echo fn-failed"}
|
||||||
|
|||||||
+33
-2
@@ -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]
|
#[async_trait::async_trait]
|
||||||
impl Execute for ast::Pipeline {
|
impl Execute for ast::Pipeline {
|
||||||
async fn execute(
|
async fn execute(
|
||||||
@@ -401,11 +422,21 @@ impl Execute for ast::Pipeline {
|
|||||||
// Update exit status.
|
// Update exit status.
|
||||||
shell.set_last_exit_status(result.exit_code.into());
|
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.
|
// 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
|
// We reuse `suppress_errexit` here because bash suppresses the ERR trap in
|
||||||
// exactly the same contexts it suppresses errexit (conditionals, `!`-prefixed
|
// exactly the same contexts it suppresses errexit (conditionals, `!`-prefixed
|
||||||
// pipelines, etc.).
|
// 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) {
|
if shell.traps().handles(crate::traps::TrapSignal::Err) {
|
||||||
shell
|
shell
|
||||||
.invoke_trap_handler(crate::traps::TrapSignal::Err, ¶ms)
|
.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)
|
// 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);
|
shell.apply_errexit_if_enabled(&mut result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user