From 864553617fc628d4411cd6cbfc17ccdc8236cf28 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 8 Oct 2026 12:45:54 +0000 Subject: [PATCH] fix(interpreter): close Oils tilde, patsub, brace, loop and paren-ambiguity gaps Tilde expansion follows bash's rules: only unquoted prefixes ending in the same literal expand; assignments expand after = and each : (also name= arguments); ${x:-~} operands expand unquoted, pattern operands always. ~root and the sandbox user resolve; other users stay literal. ${x/pat/rep} replacements are literal after quote removal; ${x////c} and ${x///} take / as the pattern. Bracket expressions honor \- and treat reversed ranges as empty; [bin; ends at the metacharacter. ((cmd) ...) and $((cmd) ...) parse as subshells; `(cmd)` is not $((. while break leaves the loop, (continue) in a loop only warns, continue 1 2 abandons the line. {{a,b} and zero-step ranges expand like bash. Unset ${v@Q}/@K/@A expand to nothing; set -u rejects them; ${!p@} lists arrays. Oils spec pass rate 77.3% to 78.8%. --- crates/bashkit/src/builtins/flow.rs | 9 + .../src/interpreter/brace_expansion.rs | 17 +- crates/bashkit/src/interpreter/expansion.rs | 387 +- crates/bashkit/src/interpreter/glob.rs | 107 +- crates/bashkit/src/interpreter/mod.rs | 39 +- crates/bashkit/src/parser/lexer.rs | 46 +- crates/bashkit/src/parser/mod.rs | 42 +- .../spec_cases/bash/brace-expansion.test.sh | 14 + .../tests/spec_cases/bash/globs.test.sh | 9 + .../spec_cases/bash/loop-control.test.sh | 32 + .../spec_cases/bash/paren-ambiguity.test.sh | 39 + .../bash/patsub-replacement.test.sh | 40 + .../spec_cases/bash/tilde-expansion.test.sh | 72 + .../tests/spec_cases/bash/variables.test.sh | 18 + knowledge/log.md | 3 + knowledge/operations/limitations.md | 1 + knowledge/operations/oils-spec.md | 15 +- ...ls-spec-linux-x86_64-20261008T132606Z.json | 5381 +++++++++++++++++ ...oils-spec-linux-x86_64-20261008T132606Z.md | 167 + 19 files changed, 6274 insertions(+), 164 deletions(-) create mode 100644 crates/bashkit/tests/spec_cases/bash/loop-control.test.sh create mode 100644 crates/bashkit/tests/spec_cases/bash/paren-ambiguity.test.sh create mode 100644 crates/bashkit/tests/spec_cases/bash/patsub-replacement.test.sh create mode 100644 crates/bashkit/tests/spec_cases/bash/tilde-expansion.test.sh create mode 100644 scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.json create mode 100644 scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.md diff --git a/crates/bashkit/src/builtins/flow.rs b/crates/bashkit/src/builtins/flow.rs index 499dd7246..4524beea2 100644 --- a/crates/bashkit/src/builtins/flow.rs +++ b/crates/bashkit/src/builtins/flow.rs @@ -99,6 +99,15 @@ fn loop_control(name: &str, ctx: &Context<'_>, make: fn(u32) -> ControlFlow) -> 0, ); } + if let Some(arg) = ctx.args.first() + && arg.trim().parse::().is_ok() + && ctx.args.len() > 1 + { + // Like `exit`: bash discards the rest of the line. + let mut result = ExecResult::err(format!("bash: {name}: too many arguments\n"), 1); + result.control_flow = ControlFlow::Abort; + return result; + } let levels = match ctx.args.first() { None => 1, Some(arg) => match arg.trim().parse::() { diff --git a/crates/bashkit/src/interpreter/brace_expansion.rs b/crates/bashkit/src/interpreter/brace_expansion.rs index 3b59829be..29639b1d0 100644 --- a/crates/bashkit/src/interpreter/brace_expansion.rs +++ b/crates/bashkit/src/interpreter/brace_expansion.rs @@ -81,6 +81,18 @@ impl Interpreter { // No valid brace pattern found let (start, end) = match (brace_start, brace_end) { (Some(s), Some(e)) => (s, e), + // An unclosed `{` is literal; a later group may still close + // (`{{a,b}` -> `{a {b`). + (Some(open), None) => { + return self.expand_after_literal_brace( + &chars, + open, + count, + bytes, + max, + recursion_depth, + ); + } _ => return vec![s.to_string()], }; @@ -267,7 +279,8 @@ impl Interpreter { // Parse optional step (default: 1 or -1 based on direction) let step: i64 = if parts.len() == 3 { match parts[2].parse::() { - Ok(0) => return None, // step=0 is invalid + // bash treats a zero step as 1. + Ok(0) => 1, Ok(s) => s, Err(_) => return None, } @@ -329,7 +342,7 @@ impl Interpreter { if start_char.is_ascii_alphabetic() && end_char.is_ascii_alphabetic() { let step: i64 = if parts.len() == 3 { match parts[2].parse::() { - Ok(0) => return None, + Ok(0) => 1, Ok(s) => s, Err(_) => return None, } diff --git a/crates/bashkit/src/interpreter/expansion.rs b/crates/bashkit/src/interpreter/expansion.rs index 160ba35a1..0a765b570 100644 --- a/crates/bashkit/src/interpreter/expansion.rs +++ b/crates/bashkit/src/interpreter/expansion.rs @@ -11,6 +11,12 @@ //! protected, only expansion results split, and a used `${v:-"..."}` //! operand contributes its own quoted/unquoted parts. A lone expansion //! keeps the cheaper whole-string `ifs_split` path. +//! +//! Important decision: tilde expansion is decided per unquoted literal part +//! at expansion time (`tilde_expand_word_literal`): a prefix that runs into +//! another part (`~"x"`, `~$v`) is not a tilde prefix. Assignment values +//! are pre-resolved by `tilde_assignment_value` (after `=` and each `:`); +//! `${...}` operands use the `operand_tilde` flag, set per operator. use super::*; @@ -180,13 +186,18 @@ impl Interpreter { } return out; } + let (is_set, value) = self.resolve_param_expansion_name(name); let value = if name.contains('[') { - self.resolve_param_expansion_name(name).1 + value } else { self.expand_variable(name) }; match operator { + // An unset variable quotes to nothing, not `''`. + 'Q' | 'K' | 'k' | 'A' if !is_set && value.is_empty() => String::new(), 'A' => format!("{}='{}'", name, value.replace('\'', "'\\''")), + // On a scalar, `@K`/`@k` quote like `@Q`. + 'K' | 'k' if !name.contains('[') => Self::transform_value(&value, 'Q'), _ => Self::transform_value(&value, operator), } } @@ -300,9 +311,8 @@ impl Interpreter { // Keep command-substitution bytes charged until the complete word has // been consumed; sibling substitutions otherwise evade live-byte caps. let mut substitution_leases = Vec::new(); - let mut is_first_part = true; - for part in &word.parts { + for (idx, part) in word.parts.iter().enumerate() { match part { WordPart::BadSubstitution(text) => { return Err(crate::error::Error::LineAbort( @@ -323,47 +333,14 @@ impl Interpreter { result.push_str(&text); } WordPart::Literal(s) => { - // Tilde expansion: ~ at start of word expands to $HOME - // A fully quoted word (`"~"`, `'~'`) keeps its tilde. - let tilde_ok = !word.quoted - || (word.has_unquoted_glob && word.part_quoted.first() == Some(&false)); - if is_first_part && tilde_ok && s.starts_with('~') { - // Tilde prefix runs to the first `/`: `~` is HOME, - // `~+` PWD, `~-` OLDPWD (literal when unset). - let (prefix, rest) = match s.find('/') { - Some(i) => (&s[1..i], &s[i..]), - None => (&s[1..], ""), - }; - let lookup = |name: &str| { - self.scoped - .variables - .get(name) - .or_else(|| self.env.get(name)) - .cloned() - }; - let dir = match prefix { - "" => Some( - self.env - .get("HOME") - .or_else(|| self.scoped.variables.get("HOME")) - .cloned() - .unwrap_or_else(|| "/home/user".to_string()), - ), - "+" => Some( - lookup("PWD").unwrap_or_else(|| self.cwd.display().to_string()), - ), - "-" => lookup("OLDPWD"), - _ => None, - }; - match dir { - Some(dir) => { - result.push_str(&dir); - result.push_str(rest); - } - None => result.push_str(s), - } - } else { - result.push_str(s); + // Tilde expansion (`~`, `~/x`, `x=~:~`); quoted literals + // keep their tilde. + match (!Self::part_is_quoted(word, idx) && s.contains('~')) + .then(|| self.tilde_expand_word_literal(word, idx, s)) + .flatten() + { + Some(expanded) => result.push_str(&expanded), + None => result.push_str(s), } } WordPart::Variable(name) => { @@ -499,6 +476,7 @@ impl Interpreter { // Delegate to sync helper to avoid bloating the async state // machine with Vec locals (causes stack overflow at // depth 32 in debug builds — see stack_overflow_regression_tests). + self.operand_outer_unquoted = !Self::part_is_quoted(word, idx); let expanded = self.apply_param_op_maybe_per_element( &value, name, @@ -507,6 +485,7 @@ impl Interpreter { *colon_variant, is_set, ); + self.operand_outer_unquoted = false; Self::append_expansion_for_word(&mut result, word, &expanded); } WordPart::ArrayAccess { name, index } => { @@ -644,6 +623,9 @@ impl Interpreter { Self::append_expansion_for_word(&mut result, word, &expanded); } WordPart::Transformation { name, operator } => { + if self.is_nounset() && !name.contains('[') && !self.is_variable_set(name) { + self.nounset_error = Some(self.unbound_variable_diag(name)); + } Self::append_expansion_for_word( &mut result, word, @@ -651,12 +633,161 @@ impl Interpreter { ); } } - is_first_part = false; } Ok(result) } + /// Whether part `idx` of `word` is quoted. Per-part flags count only + /// in a mixed word (some part quoted); otherwise the word's own + /// quoting applies to every part. + pub(super) fn part_is_quoted(word: &Word, idx: usize) -> bool { + if word.part_quoted.iter().any(|q| *q) { + word.part_quoted.get(idx).copied().unwrap_or(false) + } else { + word.quoted + } + } + + /// Directory for the tilde prefix `~name`: `~` is `$HOME`, `~+` `$PWD`, + /// `~-` `$OLDPWD`; `~root` and `~` are the fixed homes. + /// `None` keeps the text literal (unknown user, unset `OLDPWD`). + pub(super) fn tilde_dir(&self, prefix: &str) -> Option { + let lookup = |name: &str| { + self.scoped + .variables + .get(name) + .or_else(|| self.env.get(name)) + .cloned() + }; + match prefix { + "" => Some( + self.env + .get("HOME") + .or_else(|| self.scoped.variables.get("HOME")) + .cloned() + .unwrap_or_else(|| format!("/home/{}", self.tilde_user)), + ), + "+" => Some(lookup("PWD").unwrap_or_else(|| self.cwd.display().to_string())), + "-" => lookup("OLDPWD"), + "root" => Some("/root".to_string()), + user if user == &*self.tilde_user => Some(format!("/home/{user}")), + _ => None, + } + } + + /// Tilde-expand the eligible `~` positions of unquoted literal `s`: + /// `starts` lists the byte offsets where a tilde prefix may begin (word + /// start, after an assignment's `=`); with `after_colons`, every `:` at + /// or after the first start also begins one. A prefix ends at `/` or + /// `:`; one that runs to the end of `s` while more of the word follows + /// (`~"x"`, `~$v`) is not a tilde prefix. `None` when nothing changed. + fn tilde_expand_literal( + &self, + s: &str, + starts: &[usize], + after_colons: bool, + word_continues: bool, + ) -> Option { + let first = *starts.first()?; + let mut out = String::new(); + let mut copied = 0; + let mut pos = first; + let bytes = s.as_bytes(); + while pos < s.len() { + let eligible = + starts.contains(&pos) || (after_colons && pos > first && bytes[pos - 1] == b':'); + if eligible && bytes[pos] == b'~' { + let end = s[pos + 1..] + .find(['/', ':']) + .map(|i| pos + 1 + i) + .unwrap_or(s.len()); + if (end < s.len() || !word_continues) + && let Some(dir) = self.tilde_dir(&s[pos + 1..end]) + { + out.push_str(&s[copied..pos]); + out.push_str(&dir); + copied = end; + pos = end; + continue; + } + } + pos += 1; + } + if copied == 0 { + return None; + } + out.push_str(&s[copied..]); + Some(out) + } + + /// Tilde expansion for literal part `idx` of `word`: a leading `~`, and + /// in a word that looks like an assignment (`x=~:~`, as an argument to + /// `echo` or `local`) the `~` after its `=` and after each `:`. + fn tilde_expand_word_literal(&self, word: &Word, idx: usize, s: &str) -> Option { + let word_continues = idx + 1 < word.parts.len(); + let assign_eq = match word.parts.first() { + Some(WordPart::Literal(first)) if Self::is_assignment_prefix(first) => first.find('='), + _ => None, + }; + if idx == 0 { + let mut starts = vec![0]; + if let Some(eq) = assign_eq { + starts.push(eq + 1); + } + return self.tilde_expand_literal(s, &starts, assign_eq.is_some(), word_continues); + } + assign_eq?; + // Only `:` inside this part can start a prefix here. + let colon = s.find(':')?; + self.tilde_expand_literal(s, &[colon + 1], true, word_continues) + } + + /// An assignment value (`x=~`, `PATH=~/bin:~root/bin`, `a[0]=b:~`) with + /// its tildes resolved: a leading `~` and the `~` after each `:` of an + /// unquoted literal. Literals holding a `~` become quoted, so later + /// expansion leaves them alone. + pub(super) fn tilde_assignment_value<'w>(&self, word: &'w Word) -> std::borrow::Cow<'w, Word> { + use std::borrow::Cow; + let quoted_at = |i: usize| Self::part_is_quoted(word, i); + let has_tilde = + word.parts.iter().enumerate().any( + |(i, p)| matches!(p, WordPart::Literal(s) if !quoted_at(i) && s.contains('~')), + ); + if !has_tilde { + return Cow::Borrowed(word); + } + let mut out = word.clone(); + out.part_quoted = (0..word.parts.len()).map(quoted_at).collect(); + let n = word.parts.len(); + for (i, part) in word.parts.iter().enumerate() { + let WordPart::Literal(s) = part else { + continue; + }; + if quoted_at(i) || !s.contains('~') { + continue; + } + // A part after the first starts a prefix only after a `:` that + // ends the previous literal. + let mut starts = Vec::new(); + if i == 0 + || matches!(&word.parts[i - 1], WordPart::Literal(p) if !quoted_at(i - 1) && p.ends_with(':')) + { + starts.push(0); + } + if let Some(c) = s.find(':') { + starts.push(c + 1); + } + starts.sort_unstable(); + if let Some(expanded) = self.tilde_expand_literal(s, &starts, true, i + 1 < n) { + out.parts[i] = WordPart::Literal(expanded); + } + // Done with this part's tildes (`x=a=~` keeps its `~`). + out.part_quoted[i] = true; + } + Cow::Owned(out) + } + /// Expand a word to multiple fields (for array iteration and command args) /// Returns Vec where array expansions like "${arr[@]}" produce multiple fields. /// "${arr[*]}" in quoted context joins elements into a single field (bash behavior). @@ -864,6 +995,17 @@ impl Interpreter { .filter(|k| !Self::is_hidden_variable(k)) .cloned() .collect(); + // Array names count too, even an empty `hello=()`. + for k in self + .scoped + .arrays + .keys() + .chain(self.scoped.assoc_arrays.keys()) + { + if k.starts_with(prefix) && !names.contains(k) && !Self::is_hidden_variable(k) { + names.push(k.clone()); + } + } for k in self.env.keys() { if k.starts_with(prefix) && !names.contains(k) @@ -1011,10 +1153,12 @@ impl Interpreter { part_is_quoted: bool, ) -> Result<(String, bool, bool)> { let part_has_expansion = Self::is_field_split_expansion(part); - let value = if idx > 0 - && let WordPart::Literal(s) = part - { - s.clone() + let value = if let WordPart::Literal(s) = part { + // A leading `~` sees the whole word (`~$v` stays literal). + (idx == 0 && !part_is_quoted && s.contains('~')) + .then(|| self.tilde_expand_word_literal(word, 0, s)) + .flatten() + .unwrap_or_else(|| s.clone()) } else { let single = Word { parts: vec![part.clone()], @@ -1647,6 +1791,8 @@ impl Interpreter { } pub(super) fn expand_operand(&mut self, operand: &str) -> String { + let tilde = std::mem::take(&mut self.operand_tilde); + let repl = std::mem::take(&mut self.operand_replacement); if operand.is_empty() { return String::new(); } @@ -1663,24 +1809,52 @@ impl Interpreter { ); let mut result = String::new(); let mut in_marked = false; - for part in &word.parts { + for (idx, part) in word.parts.iter().enumerate() { match part { WordPart::Literal(s) => { - Self::push_marked_literal( - &mut result, - s, - quote_mark, - &mut in_marked, - force_quoted, - ); + // `${x:-~}`, `${x#~/}`: a leading unquoted `~`. + let tilde_expanded = (tilde && idx == 0 && !force_quoted) + .then(|| { + let seg_end = quote_mark.and_then(|m| s.find(m)).unwrap_or(s.len()); + let continues = seg_end < s.len() || word.parts.len() > 1; + self.tilde_expand_literal(&s[..seg_end], &[0], false, continues) + .map(|e| format!("{e}{}", &s[seg_end..])) + }) + .flatten(); + let text = tilde_expanded.as_deref().unwrap_or(s); + if repl { + Self::push_replacement_literal( + &mut result, + text, + quote_mark, + &mut in_marked, + force_quoted, + ); + } else { + Self::push_marked_literal( + &mut result, + text, + quote_mark, + &mut in_marked, + force_quoted, + ); + } } WordPart::Variable(name) => { let expanded = self.expand_variable(name); - Self::push_operand_expansion(&mut result, &expanded, in_marked || force_quoted); + Self::push_operand_expansion( + &mut result, + &expanded, + !repl && (in_marked || force_quoted), + ); } WordPart::ArithmeticExpansion(expr) => { let val = self.evaluate_arithmetic_with_assign(expr).to_string(); - Self::push_operand_expansion(&mut result, &val, in_marked || force_quoted); + Self::push_operand_expansion( + &mut result, + &val, + !repl && (in_marked || force_quoted), + ); } WordPart::ParameterExpansion { name, @@ -1697,17 +1871,29 @@ impl Interpreter { *colon_variant, is_set, ); - Self::push_operand_expansion(&mut result, &expanded, in_marked || force_quoted); + Self::push_operand_expansion( + &mut result, + &expanded, + !repl && (in_marked || force_quoted), + ); } WordPart::Length(name) => { let value = self.shell_length(&self.expand_variable(name)).to_string(); - Self::push_operand_expansion(&mut result, &value, in_marked || force_quoted); + Self::push_operand_expansion( + &mut result, + &value, + !repl && (in_marked || force_quoted), + ); } // Run ahead by `prefetch_operand_substs` (default-family // operators only); other operators leave the queue empty. WordPart::CommandSubstitution(_) => { if let Some(out) = self.operand_substs.pop_front() { - Self::push_operand_expansion(&mut result, &out, in_marked || force_quoted); + Self::push_operand_expansion( + &mut result, + &out, + !repl && (in_marked || force_quoted), + ); } } // TODO: process substitution in sync operand expansion @@ -1833,6 +2019,36 @@ impl Interpreter { } } + /// Literal text of a `${x/pat/rep}` replacement: it is not a pattern, + /// so quoted text stays as is and an unquoted `\c` is `c`. + fn push_replacement_literal( + out: &mut String, + s: &str, + quote_mark: Option, + in_marked: &mut bool, + force_quoted: bool, + ) { + let mut chars = s.chars(); + while let Some(ch) = chars.next() { + if Some(ch) == quote_mark { + *in_marked = !*in_marked; + continue; + } + if ch == '\\' && !*in_marked && !force_quoted { + match chars.next() { + Some(n) if Some(n) == quote_mark => { + out.push('\\'); + *in_marked = !*in_marked; + } + Some(n) => out.push(n), + None => out.push('\\'), + } + continue; + } + out.push(ch); + } + } + pub(super) fn push_operand_expansion(out: &mut String, s: &str, in_marked: bool) { for ch in s.chars() { Self::push_operand_char(out, ch, in_marked); @@ -1843,7 +2059,7 @@ impl Interpreter { if in_marked && matches!( ch, - '*' | '?' | '[' | ']' | '(' | ')' | '|' | '+' | '@' | '!' + '\\' | '*' | '?' | '[' | ']' | '(' | ')' | '|' | '+' | '@' | '!' ) { out.push('\\'); @@ -2119,6 +2335,7 @@ impl Interpreter { match operator { ParameterOp::UseDefault => { if use_default { + self.operand_tilde = self.operand_outer_unquoted; self.expand_operand(operand) } else { value.to_string() @@ -2126,6 +2343,7 @@ impl Interpreter { } ParameterOp::AssignDefault => { if use_default { + self.operand_tilde = self.operand_outer_unquoted; let expanded = self.expand_operand(operand); self.set_parameter_expansion_target(name, expanded.clone()); expanded @@ -2135,6 +2353,7 @@ impl Interpreter { } ParameterOp::UseReplacement => { if use_replacement { + self.operand_tilde = self.operand_outer_unquoted; self.expand_operand(operand) } else { String::new() @@ -2142,6 +2361,7 @@ impl Interpreter { } ParameterOp::Error => { if use_default { + self.operand_tilde = self.operand_outer_unquoted; let expanded = self.expand_operand(operand); let msg = if expanded.is_empty() { self.diag(format!("{}: parameter null or not set\n", name)) @@ -2241,6 +2461,8 @@ impl Interpreter { /// Expand a `#`/`%` pattern operand. Bash removes quotes there even in a /// double-quoted word, so `'...'` spans count as quoted (literal) text. pub(super) fn expand_pattern_operand(&mut self, operand: &str) -> String { + // A pattern tilde-expands even inside double quotes (`"${x#~}"`). + self.operand_tilde = true; self.expand_operand(&Self::single_quotes_as_quoted(operand)) } @@ -2248,6 +2470,8 @@ impl Interpreter { /// bash quote-removes `'...'` there even inside double quotes /// (`"${y/b/'}'}"` is `a}`). pub(super) fn expand_replacement_operand(&mut self, operand: &str) -> String { + self.operand_tilde = true; + self.operand_replacement = true; self.expand_operand(&Self::single_quotes_as_quoted(operand)) } @@ -2258,7 +2482,12 @@ impl Interpreter { Some(c @ ('#' | '%')) => (Some(c), &pattern[1..]), _ => (None, pattern), }; - let expanded = self.expand_pattern_operand(raw); + // After a `#`/`%` anchor the `~` is not at the pattern's start. + let expanded = if anchor.is_some() { + self.expand_operand(&Self::single_quotes_as_quoted(raw)) + } else { + self.expand_pattern_operand(raw) + }; match anchor { Some(c) => format!("{c}{expanded}"), None if expanded.starts_with(['#', '%']) => format!("\\{expanded}"), @@ -2607,19 +2836,37 @@ impl Interpreter { } else { c }; - if c == '-' && i > first && chars.get(i + 1).is_some_and(|n| *n != ']') { - out.push('-'); - } else { - if matches!(c, '\\' | ']' | '[' | '^' | '-' | '&' | '~') { - out.push('\\'); + // `c-e` range: only an unescaped `-` between two members. A + // reversed range (`[z-a]`) matches nothing, as in bash. + if chars.get(i + 1) == Some(&'-') && chars.get(i + 2).is_some_and(|n| *n != ']') { + let mut j = i + 2; + let end = if chars[j] == '\\' && j + 1 < chars.len() { + j += 1; + chars[j] + } else { + chars[j] + }; + if c <= end { + Self::push_regex_class_char(&mut out, c); + out.push('-'); + Self::push_regex_class_char(&mut out, end); } - out.push(c); + i = j + 1; + continue; } + Self::push_regex_class_char(&mut out, c); i += 1; } None } + fn push_regex_class_char(out: &mut String, c: char) { + if matches!(c, '\\' | ']' | '[' | '^' | '-' | '&' | '~') { + out.push('\\'); + } + out.push(c); + } + /// Remove prefix/suffix pattern from value pub(super) fn remove_pattern( &self, diff --git a/crates/bashkit/src/interpreter/glob.rs b/crates/bashkit/src/interpreter/glob.rs index 5b98cbd21..fb26c63a2 100644 --- a/crates/bashkit/src/interpreter/glob.rs +++ b/crates/bashkit/src/interpreter/glob.rs @@ -607,6 +607,9 @@ impl Interpreter { /// Match a bracket expression [abc], [a-z], [!abc], [^abc] /// Returns Some(true) if matched, Some(false) if not matched, None if invalid + /// + /// `\x` is a literal `x` (`[C\-D]` holds a literal `-`). A reversed + /// range (`[z-a]`) matches nothing, its endpoints included, as in bash. pub(crate) fn match_bracket_expr( &self, pattern_chars: &mut std::iter::Peekable>, @@ -614,9 +617,10 @@ impl Interpreter { nocase: bool, ) -> Option { // Security: stream range/class checks; avoid unbounded range materialization. + let fold = |c: char| if nocase { c.to_ascii_lowercase() } else { c }; + let probe = fold(value_char); let mut matched = false; - let mut saw_class_char = false; - let mut prev_char: Option = None; + let mut first = true; let mut negate = false; // Check for negation @@ -625,20 +629,10 @@ impl Interpreter { pattern_chars.next(); } - // Collect all characters in the bracket expression loop { - match pattern_chars.next() { - Some(']') if saw_class_char => break, - Some(']') if !saw_class_char => { - // ] as first char is literal - saw_class_char = true; - prev_char = Some(']'); - matched |= if nocase { - ']'.eq_ignore_ascii_case(&value_char) - } else { - value_char == ']' - }; - } + let c = match pattern_chars.next() { + // `]` first is a literal member. + Some(']') if !first => break, Some('[') if matches!(pattern_chars.peek(), Some(':')) => { // POSIX character class [:name:] pattern_chars.next(); // consume ':' @@ -655,67 +649,32 @@ impl Interpreter { } let mut class_chars = Vec::new(); expand_posix_class(&class_name, &mut class_chars); - if !class_chars.is_empty() { - saw_class_char = true; - prev_char = class_chars.last().copied(); - } - matched |= if nocase { - let lc = value_char.to_ascii_lowercase(); - class_chars.iter().any(|c| c.to_ascii_lowercase() == lc) - } else { - class_chars.contains(&value_char) - }; - } - Some('-') if saw_class_char => { - // Could be a range - if let Some(&next) = pattern_chars.peek() { - if next == ']' { - // - at end is literal - saw_class_char = true; - prev_char = Some('-'); - matched |= if nocase { - '-'.eq_ignore_ascii_case(&value_char) - } else { - value_char == '-' - }; - } else { - // Range: prev-next - pattern_chars.next(); - if let Some(prev) = prev_char { - let (start, end) = if prev <= next { - (prev, next) - } else { - (next, prev) - }; - let probe = if nocase { - value_char.to_ascii_lowercase() - } else { - value_char - }; - let (start_cmp, end_cmp) = if nocase { - (start.to_ascii_lowercase(), end.to_ascii_lowercase()) - } else { - (start, end) - }; - matched |= probe >= start_cmp && probe <= end_cmp; - saw_class_char = true; - prev_char = Some(next); - } - } - } else { - return None; // Unclosed bracket - } - } - Some(c) => { - saw_class_char = true; - prev_char = Some(c); - matched |= if nocase { - c.eq_ignore_ascii_case(&value_char) - } else { - c == value_char - }; + first = false; + matched |= class_chars.iter().any(|c| fold(*c) == probe); + continue; } + Some('\\') => pattern_chars.next()?, + Some(c) => c, None => return None, // Unclosed bracket + }; + first = false; + // `c-e` is a range unless the `-` is last (`[a-]`). + let mut ahead = pattern_chars.clone(); + let is_range = ahead.next() == Some('-') && !matches!(ahead.next(), Some(']') | None); + if !is_range { + matched |= fold(c) == probe; + continue; + } + pattern_chars.next(); // consume '-' + let end = match pattern_chars.next() { + Some('\\') => pattern_chars.next()?, + Some(e) => e, + None => return None, + }; + if c <= end { + let (start_cmp, end_cmp) = (fold(c), fold(end)); + matched |= (probe >= start_cmp && probe <= end_cmp) + || (value_char >= c && value_char <= end); } } Some(if negate { !matched } else { matched }) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 880e06866..f9ac9cc4d 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1661,6 +1661,18 @@ pub struct Interpreter { /// function (bash `loop_level`). A function call starts at 0, so /// `break`/`continue` never leave the caller's loop. loop_depth: usize, + /// Sandbox login name: `~name` expands to `/home/name` (and `~root` + /// to `/root`); other `~user` forms stay literal. + tilde_user: Arc, + /// The next `expand_operand` call tilde-expands a leading `~` + /// (consumed by it). + operand_tilde: bool, + /// The `${...}` being expanded is unquoted, so a `${x:-~}` operand + /// tilde-expands. + operand_outer_unquoted: bool, + /// The next `expand_operand` call expands a `${x/pat/rep}` replacement + /// (literal text, not a pattern; consumed by it). + operand_replacement: bool, /// Active function calls and `source`s: `return` is valid only when > 0. return_depth: usize, /// The last `[[ ]]` leaf was an invalid `=~` regex (status 2 if it @@ -2188,6 +2200,10 @@ impl Interpreter { script_depth: 0, child_shell_depth: 0, loop_depth: 0, + tilde_user: Arc::from(username_val.as_str()), + operand_tilde: false, + operand_outer_unquoted: false, + operand_replacement: false, return_depth: 0, cond_regex_error: false, cond_stderr: String::new(), @@ -2379,6 +2395,10 @@ impl Interpreter { // Forks are polled on this task's stack, so they inherit its depth. child_shell_depth: self.child_shell_depth, loop_depth: self.loop_depth, + tilde_user: self.tilde_user.clone(), + operand_tilde: false, + operand_outer_unquoted: false, + operand_replacement: false, return_depth: self.return_depth, cond_regex_error: false, cond_stderr: String::new(), @@ -4106,7 +4126,10 @@ impl Interpreter { self.output_stream_stdout_bytes, self.output_stream_stderr_bytes, ); + // A subshell is outside any loop: `(continue)` only warns. + let saved_loop_depth = std::mem::replace(&mut self.loop_depth, 0); let mut result = self.execute_command_sequence(commands).await; + self.loop_depth = saved_loop_depth; // Left here, before anything can return early. self.leave_nofork_scope(); // An `exec` redirect set inside the subshell applies to the @@ -5217,6 +5240,19 @@ impl Interpreter { ); acc.stdout.append(&condition_result.stdout); acc.stderr.append(&condition_result.stderr); + // `while break; do` leaves the loop from its condition. + if condition_result.control_flow != ControlFlow::None { + match acc.accumulate(ExecResult { + exit_code: condition_result.exit_code, + control_flow: condition_result.control_flow, + ..Default::default() + }) { + state::LoopAction::None => {} + state::LoopAction::Break => break, + state::LoopAction::Continue => continue, + state::LoopAction::Exit(r) => return Ok(*r), + } + } let should_break = if break_on_zero { condition_result.exit_code == 0 } else { @@ -7309,7 +7345,8 @@ impl Interpreter { for assignment in assignments { match &assignment.value { AssignmentValue::Scalar(word) => { - let value = self.expand_word(word).await?; + let word = self.tilde_assignment_value(word); + let value = self.expand_word(&word).await?; if self.is_xtrace_enabled() { // `+ v=x`, `+ a[1]='a b'`, `+ v+=y` let prefix = self.xtrace_prefix().await; diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index 5b5df01bf..c0afd217e 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -64,6 +64,7 @@ type Lookahead<'s, 'a> = std::iter::Peekable< >; /// Lexer for bash scripts. +#[derive(Clone)] pub struct Lexer<'a> { #[allow(dead_code)] // Stored for error reporting in future input: &'a str, @@ -118,6 +119,11 @@ impl<'a> Lexer<'a> { self.next_token_inner() } + /// Put `ch` back in front of the remaining input. + pub fn unread_char(&mut self, ch: char) { + self.reinject_buf.push_front(ch); + } + fn peek_char(&mut self) -> Option { if let Some(&ch) = self.reinject_buf.front() { Some(ch) @@ -563,6 +569,10 @@ impl<'a> Lexer<'a> { fn read_backtick_into(&mut self, word: &mut String) -> Result<(), String> { self.advance(); // consume opening ` word.push_str("$("); + // `` `(cmd)` `` must not read as `$((`. + if self.peek_char() == Some('(') { + word.push(' '); + } let body_start = word.len(); let mut closed = false; while let Some(c) = self.peek_char() { @@ -1027,10 +1037,12 @@ impl<'a> Lexer<'a> { // Check for $(( - arithmetic expansion if self.peek_char() == Some('(') { + let inner_open = word.len(); word.push('('); self.advance(); // Read until )) let mut depth = 2; + let mut subshell = false; while let Some(c) = self.peek_char() { word.push(c); self.advance(); @@ -1038,6 +1050,14 @@ impl<'a> Lexer<'a> { depth += 1; } else if c == ')' { depth -= 1; + if depth == 1 && !subshell && self.peek_char() != Some(')') { + // `$(( cmd ) )`: the inner `(` closes + // alone, so this is `$( (cmd) )`, a + // command substitution of a subshell + // (bash reads it the same way). + word.insert(inner_open, ' '); + subshell = true; + } if depth == 0 { break; } @@ -1106,7 +1126,8 @@ impl<'a> Lexer<'a> { // (quote removal — only the literal char survives). // `\$` and `\`` must stay literal through parse_word, so // they carry the NUL sentinel like quoted `\$` does. - if matches!(next, '$' | '`') { + // `\~` likewise must not tilde-expand. + if matches!(next, '$' | '`' | '~') { word.push('\x00'); } if Self::is_glob_escape_char(next) { @@ -1929,6 +1950,9 @@ impl<'a> Lexer<'a> { // Backtick command substitution inside double quotes self.advance(); // consume opening ` content.push_str("$("); + if self.peek_char() == Some('(') { + content.push(' '); + } let body_start = content.len(); while let Some(c) = self.peek_char() { if c == '`' { @@ -2304,8 +2328,12 @@ impl<'a> Lexer<'a> { fn read_bracket_word(&mut self) -> Option { let mut word = String::from("["); - // Read until we find the closing ] (handle nested correctly) + // Read until we find the closing ]. A metacharacter ends the word + // first: `[bin;` is the word `[bin`, not a bracket expression. while let Some(ch) = self.peek_char() { + if matches!(ch, ' ' | '\t' | '\n' | ';' | '&' | '|' | '<' | '>') { + break; + } word.push(ch); self.advance(); if ch == ']' { @@ -2338,6 +2366,13 @@ impl<'a> Lexer<'a> { /// way, so `<<` is a shift and spacing survives for `type`. Returns /// `None` when input ends first. pub fn read_dparen_body(&mut self) -> Option { + self.read_dparen_body_checked().ok() + } + + /// [`Self::read_dparen_body`], telling why it failed: `Err(true)` when + /// a `)` closed the inner `(` alone (`((cmd) )`), which bash reads as + /// nested subshells; `Err(false)` when input ran out. + pub fn read_dparen_body_checked(&mut self) -> Result { let mut body = String::new(); let mut depth = 0usize; while let Some(c) = self.advance() { @@ -2345,10 +2380,9 @@ impl<'a> Lexer<'a> { '(' => depth += 1, ')' if depth == 0 && self.peek_char() == Some(')') => { self.advance(); - return Some(body); + return Ok(body); } - // An unbalanced `)` stays text. - ')' if depth == 0 => {} + ')' if depth == 0 => return Err(true), ')' => depth -= 1, '\\' => { body.push(c); @@ -2377,7 +2411,7 @@ impl<'a> Lexer<'a> { } body.push(c); } - None + Err(false) } /// Read the raw source of a `[[ ... =~ REGEX ]]` operand, as bash does: diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 9fb2f083d..df3ee43a6 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -2090,10 +2090,22 @@ impl<'a> Parser<'a> { if self.peeked_token.is_none() { // Read `(( expr ))` as written (see read_dparen_body); the // command keeps that text, so `type` prints it unchanged. - let Some(body) = self.lexer.read_dparen_body() else { - return Err(Error::parse( - "unexpected end of input in arithmetic command".to_string(), - )); + let saved = self.lexer.clone(); + let body = match self.lexer.read_dparen_body_checked() { + Ok(body) => body, + Err(true) => { + // `((cmd) ...)`: two subshells, as bash reads it. Re-read + // from just after `((`, with the second `(` put back. + self.lexer = saved; + self.lexer.unread_char('('); + self.current_token = Some(tokens::Token::LeftParen); + return self.parse_subshell(); + } + Err(false) => { + return Err(Error::parse( + "unexpected end of input in arithmetic command".to_string(), + )); + } }; self.advance(); return Ok(CompoundCommand::Arithmetic(body)); @@ -3656,7 +3668,20 @@ impl<'a> Parser<'a> { if ch == '\x00' { // NUL sentinel from lexer: next char is a literal (escaped in source). if let Some(literal_ch) = chars.next() { - current.push(literal_ch); + if literal_ch == '~' + && !in_quoted_segment + && (current.is_empty() || current.ends_with([':', '='])) + { + // `\~` where a tilde prefix could start: a quoted + // part, so it never tilde-expands. + if !current.is_empty() { + push_part!(WordPart::Literal(std::mem::take(&mut current))); + } + parts.push(WordPart::Literal("~".to_string())); + part_quoted.push(true); + } else { + current.push(literal_ch); + } } } else if ch == '\u{1e}' || ch == '\u{1f}' { // A quote boundary ends the literal run, so `part_quoted` @@ -4243,6 +4268,13 @@ impl<'a> Parser<'a> { false }; let mut pattern = String::new(); + // `${x///}`, `${x////c}`: the pattern + // cannot be empty, so a `/` right after + // `//` is the pattern itself. + if replace_all && chars.peek() == Some(&'/') { + chars.next(); + pattern.push('/'); + } let mut in_dq = false; while let Some(&ch) = chars.peek() { if ch == '\x00' { diff --git a/crates/bashkit/tests/spec_cases/bash/brace-expansion.test.sh b/crates/bashkit/tests/spec_cases/bash/brace-expansion.test.sh index 891a7ab9d..a86f4cd2e 100644 --- a/crates/bashkit/tests/spec_cases/bash/brace-expansion.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/brace-expansion.test.sh @@ -157,3 +157,17 @@ for x in {only}; do echo $x; done ### expect {only} ### end + +### brace_unclosed_open_brace_is_literal +echo {{a,b} +echo x{{a,b}y}z +### expect +{a {b +x{ay}z x{by}z +### end + +### brace_zero_step_is_one +echo -{1..4..0}- {a..c..0} +### expect +-1- -2- -3- -4- a b c +### end diff --git a/crates/bashkit/tests/spec_cases/bash/globs.test.sh b/crates/bashkit/tests/spec_cases/bash/globs.test.sh index a57ee5020..c259066d8 100644 --- a/crates/bashkit/tests/spec_cases/bash/globs.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/globs.test.sh @@ -110,3 +110,12 @@ d1/ d2/ l/ d1/ d2/ f1/ d2/s/ ### end + +### glob_bracket_word_ends_at_metachar +echo [bin; echo [!bin +echo a[b +### expect +[bin +[!bin +a[b +### end diff --git a/crates/bashkit/tests/spec_cases/bash/loop-control.test.sh b/crates/bashkit/tests/spec_cases/bash/loop-control.test.sh new file mode 100644 index 000000000..71d3bcbea --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/loop-control.test.sh @@ -0,0 +1,32 @@ +# break/continue edge cases: in a loop condition, in a subshell (which is +# outside any loop), and with too many arguments (the line is abandoned). + +### break_in_while_condition +while break; do echo x; done +echo done +for i in 1 2; do while break; do echo x; done; echo i=$i; done +### expect +done +i=1 +i=2 +### end + +### continue_in_subshell_only_warns +for i in 1 2; do + ( continue; echo in-sub ) + echo st=$? +done 2>/dev/null +### expect +in-sub +st=0 +in-sub +st=0 +### end + +### continue_too_many_args_abandons_line +( for x in a b; do echo $x; continue 1 2; done; echo after ) +echo "status=$?" +### expect +a +status=1 +### end diff --git a/crates/bashkit/tests/spec_cases/bash/paren-ambiguity.test.sh b/crates/bashkit/tests/spec_cases/bash/paren-ambiguity.test.sh new file mode 100644 index 000000000..6b4eed823 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/paren-ambiguity.test.sh @@ -0,0 +1,39 @@ +# `((` that does not close with `))` is two nested subshells, and `$((` +# likewise a command substitution of a subshell, as bash reads them. + +### dparen_closed_by_separate_parens_is_subshell +((echo a) ; echo b) +echo st=$? +### expect +a +b +st=0 +### end + +### dparen_negated_test_subshells +i=-g +if ! ((test x"$i" = x-g) || (test x"$i" = x-O2)); then echo no; else echo yes; fi +### expect +yes +### end + +### dollar_dparen_closed_separately_is_command_subst +echo $((echo 1; echo 2) ) +### expect +1 2 +### end + +### backtick_starting_with_paren +x=`(echo a; echo b) | head -n 1` +echo $x +### expect +a +### end + +### arithmetic_still_arithmetic +echo $(( (1+2)*3 )) +(( (2+3) == 5 )) && echo ok +### expect +9 +ok +### end diff --git a/crates/bashkit/tests/spec_cases/bash/patsub-replacement.test.sh b/crates/bashkit/tests/spec_cases/bash/patsub-replacement.test.sh new file mode 100644 index 000000000..e13b7eb29 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/patsub-replacement.test.sh @@ -0,0 +1,40 @@ +# `${x/pat/rep}`: the replacement is literal text after quote removal (no +# glob escapes leak into it); `${x///}` and `${x////c}` take `/` as the +# pattern; a quoted expansion in the pattern matches literally. + +### patsub_replacement_quote_removal +mkdir -p /tmp/patsub_empty && cd /tmp/patsub_empty +v=xx +echo ${v/x/"?"} ${v/x/\?} ${v/x/\\} ${v/x/'\\'} ${v/x/"*"} ${v/x/a\/b} +echo "${v/x/"?"}" "${v/x/\?}" +### expect +?x ?x \x \\x *x a/bx +?x ?x +### end + +### patsub_slash_pattern +x=/_/ +echo ${x////c} ${x///} +H=/foo/bar +echo ${H////\\/} ${H//'/'/\\/} +### expect +c_c _ +\/foo\/bar \/foo\/bar +### end + +### patsub_quoted_backslash_pattern +v='[\f]' +x='\f' +echo ${v/"$x"/_} ${v/$x/_} ${v/\f/_} ${v/\\f/_} +### expect +[_] [\_] [\_] [_] +### end + +### pattern_reversed_range_and_escaped_dash +x=fooz +echo ${x//[z-a]} ${x//[a\-z]/Q} +[[ z == [z-a] ]] && echo bad || echo nomatch +### expect +fooz fooQ +nomatch +### end diff --git a/crates/bashkit/tests/spec_cases/bash/tilde-expansion.test.sh b/crates/bashkit/tests/spec_cases/bash/tilde-expansion.test.sh new file mode 100644 index 000000000..0a4252119 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/tilde-expansion.test.sh @@ -0,0 +1,72 @@ +# Tilde expansion: a leading unquoted `~`, the `~` after `=` and `:` in +# assignments (and in assignment-looking arguments), `${x:-~}` operands and +# pattern operators. Quoted or escaped tildes, and prefixes that run into +# quoted text or an expansion, stay literal. + +### tilde_quoted_forms_stay_literal +HOME=/home/tt +x=y +echo ~/"b" ~"/b" ~/$x ~$x "~" \~ ~:x +### expect +/home/tt/b ~/b /home/tt/y ~y ~ ~ /home/tt:x +### end + +### tilde_in_assignment_values +HOME=/home/tt +v=a:~:~root/x:"~":\~ +echo $v +v='~' +echo $v +v=\~ +echo $v +v=~/"b" +echo $v +PATHLIKE=~/bin:~/sbin +echo $PATHLIKE +### expect +a:/home/tt:/root/x:~:~ +~ +~ +/home/tt/b +/home/tt/bin:/home/tt/sbin +### end + +### tilde_in_assignment_like_arguments +HOME=/home/tt +echo x=~ x=~:~ a=b:~ 1x=~ foo:~ +readonly r=~:~ +echo $r +f() { local l=x:~; echo $l; } +f +### expect +x=/home/tt x=/home/tt:/home/tt a=b:/home/tt 1x=~ foo:~ +/home/tt:/home/tt +x:/home/tt +### end + +### tilde_in_prefix_and_indexed_assignment +HOME=/home/tt +xx=~root:~ env | grep '^xx=' +a[0]=foo:~ +echo ${a[0]} +### expect +xx=/root:/home/tt +foo:/home/tt +### end + +### tilde_in_parameter_operands +HOME=/home/tt +echo ${undef:-~} "${undef:-~}" ${HOME:+~/z} ${undef:-"~"} +x=/home/tt/a +echo ${x//~/z} "${x#~}" "${x/#~/Z}" +### expect +/home/tt ~ /home/tt/z ~ +z/a /a /home/tt/a +### end + +### tilde_brace_expansion_then_tilde +HOME=/home/tt +echo ~{/src,root} +### expect +/home/tt/src /root +### end diff --git a/crates/bashkit/tests/spec_cases/bash/variables.test.sh b/crates/bashkit/tests/spec_cases/bash/variables.test.sh index e3397b54f..128225dfc 100644 --- a/crates/bashkit/tests/spec_cases/bash/variables.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/variables.test.sh @@ -780,3 +780,21 @@ echo "after reset:${result}end" ### expect after reset:end ### end + +### transform_unset_quotes_to_nothing +empty='' +x=x +echo ${x@Q} ${empty@Q} ${undef@Q} ${x@K} ${undef@k} ${x@A} ${undef@A} +hello1=1 hello=() +echo ${!hello@} +### expect +'x' '' 'x' x='x' +hello hello1 +### end + +### transform_unset_with_nounset +set -u +(echo ${undef@Q}); echo "stat: $?" +### expect +stat: 1 +### end diff --git a/knowledge/log.md b/knowledge/log.md index 97f7bd174..ecbe49853 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -8,6 +8,9 @@ * **Fix**: `[[ ]]` is checked against bash's conditional grammar at parse time: `[[ a $op b ]]`, `[[ -z ]]`, `[[ '(' x ]]`, `[[ -f < ]]`, `[[ ]]`, `[[ && ]]`, `[[ a == b c ]]` are syntax errors (status 2) with bash's wording, newlines inside are blanks, quoted `'!'`/`'('` are operands (`[[ '!' == ! ]]` is true), `FOO=bar [[ ... ]]` runs a command named `[[` (127), and `$(...)` stderr in the operands goes through the command's redirects. `CommandSubstitution`/`ProcessSubstitution` print as shell text in `Word`'s `Display` (was `Debug`, TM-INF-022), which alias expansion re-parses. * **Fix**: `cd` takes `-L`/`-P`/`-e`/`-@`/`--`, reports `too many arguments`, `OLDPWD not set` and invalid options like bash, fails on `cd nope/..`, prints the directory for `cd -` and `CDPATH` hits, and resolves symlinks under `-P`; `pwd -P` prints the physical path. See limitations "`cd` / `pwd`". * **Fix**: aliases: `alias --`/`alias -p`/`unalias --`, trailing-blank chaining continues down the chain (`e_ one two` with `one='ONE '`), prefix assignments survive expansion (`FOO=2 al`), backquoted arguments re-parse, and a syntax error in the expansion exits 2. Parse-time alias semantics stay a gap (limitations "Aliases"). Oils spec pass rate 74.2% to 77.3% (`word-split` 16 misses to 0, `globignore` 16 to 1, `dbracket` 13 to 1, `builtin-cd` 14 to 5, `alias` 13 to 7). +* **Fix**: tilde expansion follows bash's eligibility rules. `~` expands only when unquoted and its prefix ends at `/` or `:` inside the same unquoted text (`~"/b"`, `~$v`, `\~`, `x='~'` stay literal; `~:x` expands); assignments expand after `=` and each `:` (`x=a:~:~root`, `xx=~ cmd`, `a[0]=b:~`, `readonly`/`local` arguments, `echo x=~`); `${x:-~}` expands unquoted and pattern operands (`${x//~/y}`, `"${x#~}"`) expand even quoted. `~root` is `/root` and `~` its home; other users stay literal (limitations "Tilde expansion"). +* **Fix**: `${x/pat/rep}` replacements are literal text after quote removal (`${v/x/"?"}` printed `\?x`, `${v/x/\\}` printed two backslashes); `${x////c}` and `${x///}` take `/` as the pattern; a quoted expansion holding `\` in a pattern matches it literally. Bracket expressions: `[C\-D]` holds a literal `-` in patterns, a reversed range `[z-a]` matches nothing (it matched both ends), and `[bin;` no longer reads past the `;` (`echo [bin; echo x` printed one word). +* **Fix**: `((cmd) ...)` and `$((cmd) ...)` whose inner `(` closes alone parse as nested subshells / a command substitution, as in bash (they were arithmetic syntax errors), and a backquoted `` `(cmd)` `` is no longer read as `$((`. `while break; do` leaves the loop from its condition (it looped until the command limit), `( continue )` inside a loop only warns (a subshell is outside any loop), and `continue 1 2` reports `too many arguments` and abandons the line. Brace expansion: an unclosed `{` is literal while a later group expands (`{{a,b}` is `{a {b`), and a zero step counts as 1 (`{1..4..0}`). `${undef@Q}`/`@K`/`@A` expand to nothing, `${x@K}` quotes a scalar, `set -u` rejects `${undef@Q}`, and `${!prefix@}` lists array names. Oils spec pass rate 77.3% to 78.8% (`tilde` 9 misses to 1, `var-op-patsub` 9 to 2, `var-op-bash` 8 to 3, `brace-expansion` 8 to 4, `glob` 5 to 3, `loop` and `paren-ambiguity` 5 to 0). * **Fix**: process substitution expands to bash's `/dev/fd/63`, counting down per command (`echo <(true) <(true)` is `/dev/fd/63 /dev/fd/62`), skipping fds still open (a caller's substitution, `exec 63>f`), and reusing 63 once the command, or the compound command whose words or redirects opened it, finishes. The fds live in a per-interpreter namespace (`ProcSubFs` wraps each shell's VFS view, TM-ISO-028) instead of `/dev/fd/proc_sub_` files in the shared VFS, so tenants on one filesystem each get their own `/dev/fd/63`; a closed one is "No such file or directory". `set -C` no longer refuses `> >(cmd)` (a FIFO is not clobbered). Remaining gaps in [Limitations](operations/limitations.md). bash-oracle match 103 to 104, strict 101 to 102. * **Fix**: coprocesses run concurrently. `coproc NAME cmd` is a background job wired to the shell through two bounded in-process pipes: writes to `${NAME[1]}` feed its stdin, `exec {NAME[1]}>&-` delivers EOF, `read <&${NAME[0]}` / `read -u` take its output line by line, so `coproc UPPER { tr a-z A-Z; }` and request/response loops (`echo q >&${C[1]}; read r <&${C[0]}`) work; the body used to run once at `coproc` with empty input. fds follow bash (63/60, then 62/58), `$NAME_PID`/`$!`/`wait $NAME_PID` give the job, a second coproc while one runs prints bash's `still exists` warning, NAME and NAME_PID are unset and the fds closed once `wait` reaps it, and `exec()` closes the shell's ends before waiting for jobs. `<&$empty` is now an error instead of reading the shell's stdin. See foundations/parallel-execution "Coprocesses", L-PROC-005. bash-oracle match 102 to 103, strict 100 to 101. * **Fix**: `$SHLVL` follows bash's fork suppression. bash execs some simple commands in place: the last one of a `bash -c` string or `$( )` body (also the right side of a trailing `&&`/`||`/`;`; no redirections, no EXIT/ERR or signal trap set), the last one of a `( )`/`<( )` body (a sole one even with redirections), and any simple command run with `&`; a shell started there lowers `$SHLVL` first and so keeps the level. `exec bash` lowers it too, except directly inside `( )`; a substitution inside a pipeline stage does not. `bash -c 'bash -c "echo \$SHLVL"'` printed 3 in a script started at 1, now 2. A `bash`/`sh` child no longer inherits trap handlers (an ignored signal stays ignored), so `bash -c 'trap "echo bye" EXIT; bash -c true'` prints `bye` once. L-PROC-003 narrowed. bash-oracle match 101 to 102, strict 99 to 100. diff --git a/knowledge/operations/limitations.md b/knowledge/operations/limitations.md index 1c00dae4e..8e0b487c2 100644 --- a/knowledge/operations/limitations.md +++ b/knowledge/operations/limitations.md @@ -117,6 +117,7 @@ Target: IEEE 1003.1-2024 Shell Command Language. | Diagnostic prefix | `$0: line N: ` on interpreter and shell-builtin messages, `$0` = sourced file inside `source`; `eval` text counts lines from the eval's own line; syntax errors read like bash (`near unexpected token `T'` plus the offending line, `unexpected end of file` on the line after the last, `unexpected EOF while looking for matching `"'`), named `bash: -c`, the script path, `$0: eval` or the sourced file | `(( ))`, `[[ ]]`, `{ }` and `( )` carry no span: `(( ))`/`[[ ]]` keep the line of the list or command before them, `{ }`/`( )` report line 1 at the top level when the construct itself (not a command inside it) fails; a function defined in a sourced file and called from the script names the caller's file (bash names the defining file); `${3}` under `set -u` reports `$3` (bash: `3`, only bare `$3` gets the `$`); `command not found` keeps the sandbox "Did you mean" / unavailable-command hint bash does not print; parse errors outside the grammar (`((` unterminated, `$(` unterminated, `[[ ]]` operand errors other than an unclosed `=~` group) keep bashkit's wording, and `x=(` with no `)` is accepted; under the CLI's `-c` a syntax error after commands that ran is named `bash: line N` (bash: `bash: -c: line N`); an `eval` command spanning several lines counts from its first line (bash: its last), and a function defined inside `eval` numbers `$LINENO` from the eval text, not the script | | Stderr routing | A command's own errors (`command not found`, a missing `./path`, `return`/`exit`/`source`/`getopts`/`builtin` usage errors) follow its redirects; stderr written inside `$(...)` reaches the outer stderr ahead of the expanding command's own, outside that command's redirects and inside a compound command's | stdout and stderr of one command are separate values, so `{ a; b; } 2>&1` puts the merged stderr after the group's stdout instead of in write order; `exit 1 2` drops the rest of the line and carries on like `bash SCRIPT` even under `bash -c` (bash -c exits 1) | | `set -x` | Simple commands with bash's word quoting (`''`, `'a b'`, `$'\001'`), assignments (`+ a=1`, `+ arr=(1 $(x))` as written), function calls, `[[ ]]` primaries; PS4 expanded (`$LINENO`, `$(...)`) with its first character repeated per `$(...)`/`eval`/`source`/trap level | `for`/`case`/`select`/`(( ))` headers are not traced; `export`/`declare` do not trace their own assignment (`+ Q=1`); a quoted `[[ == ]]` pattern shows only glob characters escaped (bash escapes each quoted character); `unset PS4` still prefixes `+ ` (PS4 is not seeded, so unset and never-set look alike); `declare -a a=(1 2)` traces as `declare -a a` without bash's `a=('1' '2')` line; a `<(...)` body is not traced | +| Tilde expansion | A leading unquoted `~`, `~+`, `~-`; in assignments (and `name=` arguments such as `local x=a:~`) after `=` and each `:`; `${x:-~}` operands when unquoted; pattern operands (`${x#~}`, `${x//~/y}`) even quoted; quoted, escaped or expansion-joined prefixes stay literal (`~"x"`, `\~`, `~$v`) | No user database: only `~root` (`/root`) and `~` (`/home/`) resolve, other `~name` stays literal; no `~N`/`~+N` directory-stack forms; a `${v-~:~}` operand inside an assignment gets only the leading tilde; `x=~` inside `a=( ... )` is expanded (bash leaves it) | | Brace expansion | Runs before other expansions on unquoted literal text; lists, nesting, ranges with step and zero padding, quoted/escaped braces and commas | Works on parsed words, not raw text: `$v{1,2}` expands `$v` then appends `1`/`2` (bash reads `$v1`, `$v2`); write `${v}` | ## Builtins diff --git a/knowledge/operations/oils-spec.md b/knowledge/operations/oils-spec.md index 081c4f3a0..65f278330 100644 --- a/knowledge/operations/oils-spec.md +++ b/knowledge/operations/oils-spec.md @@ -17,17 +17,20 @@ Implemented. Harness: `scripts/oils-spec/run.py` (`just oils-spec`). ## Headline -**77.3%**: bashkit passes 2080 of the 2692 Oils spec cases real bash 5.2.21 -passes (2759 cases in 132 spec files; bashkit passes 2086 of all cases, -75.6%). Oils `57d3f0d088c3`, 2026-10-08. Report: -`scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T115238Z.md`. +**78.8%**: bashkit passes 2120 of the 2692 Oils spec cases real bash 5.2.21 +passes (2759 cases in 132 spec files; bashkit passes 2127 of all cases, +77.1%). Oils `57d3f0d088c3`, 2026-10-08. Report: +`scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.md`. The first run (74.2%, `...T103235Z.md`) missed 16 cases each in `globignore` and `word-split`, 14 in `builtin-cd`, 13 in `alias` and -`dbracket`; those files now miss 1, 0, 5, 7 and 1. +`dbracket`; those files now miss 1, 0, 5, 7 and 1. The run before this one +(77.3%, `...T115238Z.md`) missed 9 in `tilde` and `var-op-patsub`, 8 in +`var-op-bash` and `brace-expansion`, 5 in `glob`, `loop` and +`paren-ambiguity`; they now miss 1, 2, 3, 4, 3, 0 and 0. Spec files with the most misses at the latest run: `builtin-completion` (34), `array` (25), `prompt` (25), `builtin-trap-bash` (19), `var-ref` (15). -24 of the 132 files pass completely. +28 of the 132 files pass completely. ## Why diff --git a/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.json b/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.json new file mode 100644 index 000000000..feb00dc60 --- /dev/null +++ b/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.json @@ -0,0 +1,5381 @@ +{ + "schema": 1, + "oils_repo": "https://github.com/oils-for-unix/oils", + "oils_rev": "57d3f0d088c36340bc4b3038208e58ea02acf15c", + "bashkit_commit": "5f04b4810ec2", + "bash_version": "5.2.21(1)-release", + "timestamp": "2026-10-08T13:26:06Z", + "platform": "linux-x86_64", + "jobs": 4, + "timeout_secs": 10, + "elapsed_secs": 34.8, + "files_run": 132, + "files_skipped": 91, + "total": { + "cases": 2759, + "bashkit_pass": 2127, + "bashkit_pass_pct": 77.1, + "bash_pass": 2692, + "bash_pass_pct": 97.6, + "bashkit_pass_of_bash": 2120, + "pass_rate": 78.8 + }, + "failure_classes": { + "behavior": 410, + "host": 24, + "interactive": 110, + "processes": 18, + "signals": 10 + }, + "files": { + "builtin-completion": { + "summary": { + "cases": 51, + "bashkit_pass": 17, + "bashkit_pass_pct": 33.3, + "bash_pass": 51, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 17, + "pass_rate": 33.3 + }, + "missed": [ + { + "case": 0, + "name": "complete with no args and complete -p both print completion spec", + "class": "interactive", + "on": "stdout" + }, + { + "case": 1, + "name": "complete -F f is usage error", + "class": "interactive", + "on": "stdout" + }, + { + "case": 2, + "name": "complete with nonexistent function", + "class": "interactive", + "on": "stdout" + }, + { + "case": 3, + "name": "complete with no action", + "class": "interactive", + "on": "stdout" + }, + { + "case": 5, + "name": "Invalid syntax", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "how compgen calls completion functions", + "class": "interactive", + "on": "stdout" + }, + { + "case": 7, + "name": "complete -o -F (git)", + "class": "interactive", + "on": "status" + }, + { + "case": 8, + "name": "compopt with invalid syntax", + "class": "interactive", + "on": "stdout" + }, + { + "case": 9, + "name": "compopt fails when not in completion function", + "class": "interactive", + "on": "status" + }, + { + "case": 14, + "name": "compgen -v P", + "class": "interactive", + "on": "stdout" + }, + { + "case": 15, + "name": "compgen -e with global/local exported vars", + "class": "interactive", + "on": "stdout" + }, + { + "case": 18, + "name": "compgen -e P", + "class": "interactive", + "on": "stdout" + }, + { + "case": 19, + "name": "compgen with actions: function / variable / file", + "class": "interactive", + "on": "stdout" + }, + { + "case": 20, + "name": "compgen with actions: alias, setopt", + "class": "interactive", + "on": "stdout" + }, + { + "case": 21, + "name": "compgen with actions: shopt", + "class": "interactive", + "on": "stdout" + }, + { + "case": 22, + "name": "compgen with action and suffix: helptopic", + "class": "interactive", + "on": "stdout" + }, + { + "case": 25, + "name": "compgen -A user", + "class": "interactive", + "on": "status" + }, + { + "case": 28, + "name": "compgen -A command completes builtins and keywords", + "class": "interactive", + "on": "stdout" + }, + { + "case": 29, + "name": "compgen -k shows the same keywords as bash", + "class": "processes", + "on": "stdout" + }, + { + "case": 31, + "name": "compgen -k completes reserved shell keywords", + "class": "interactive", + "on": "stdout" + }, + { + "case": 32, + "name": "-o filenames and -o nospace have no effect with compgen", + "class": "interactive", + "on": "stdout" + }, + { + "case": 33, + "name": "-o plusdirs and -o dirnames with compgen", + "class": "interactive", + "on": "stdout" + }, + { + "case": 34, + "name": "compgen -o default completes files and dirs", + "class": "interactive", + "on": "stdout" + }, + { + "case": 35, + "name": "compgen doesn't respect -X for user-defined functions", + "class": "interactive", + "on": "stdout" + }, + { + "case": 36, + "name": "compgen -W words -X filter", + "class": "interactive", + "on": "stdout" + }, + { + "case": 37, + "name": "compgen -f -X filter -- $cur", + "class": "interactive", + "on": "stdout" + }, + { + "case": 39, + "name": "compgen -W 'one two three'", + "class": "interactive", + "on": "stdout" + }, + { + "case": 40, + "name": "compgen -W evaluates code in $()", + "class": "interactive", + "on": "stdout" + }, + { + "case": 41, + "name": "compgen -W uses IFS, and delimiters are escaped with \\", + "class": "interactive", + "on": "stdout" + }, + { + "case": 42, + "name": "Parse errors for compgen -W and complete -W", + "class": "interactive", + "on": "stdout" + }, + { + "case": 43, + "name": "Runtime errors for compgen -W", + "class": "interactive", + "on": "stdout" + }, + { + "case": 46, + "name": "compgen -A builtin", + "class": "interactive", + "on": "stdout" + }, + { + "case": 47, + "name": "complete -C vs. compgen -C", + "class": "interactive", + "on": "stdout" + }, + { + "case": 50, + "name": "compgen -F with scalar COMPREPLY", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "array": { + "summary": { + "cases": 78, + "bashkit_pass": 53, + "bashkit_pass_pct": 67.9, + "bash_pass": 78, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 53, + "pass_rate": 67.9 + }, + "missed": [ + { + "case": 3, + "name": "space before ( in array initialization", + "class": "behavior", + "on": "status" + }, + { + "case": 5, + "name": "array with invalid token", + "class": "behavior", + "on": "status" + }, + { + "case": 9, + "name": "Negative index", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "Negative index and sparse array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Negative index and sparse array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "Length after unset", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "Retrieve index that is a command sub", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "${!a[1]} is named ref in bash", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "All elements unquoted", + "class": "behavior", + "on": "stdout" + }, + { + "case": 22, + "name": "$*", + "class": "behavior", + "on": "stdout" + }, + { + "case": 25, + "name": "Exporting array doesn't do anything, not even first element", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "Arrays can't be used as env bindings", + "class": "behavior", + "on": "stdout" + }, + { + "case": 28, + "name": "Associative arrays can't be used as env bindings either", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "Set array item to array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "Slice of array with [@]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 34, + "name": "Negative slice begin", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "Slice with arithmetic", + "class": "behavior", + "on": "stdout" + }, + { + "case": 59, + "name": "Slice of sparse array with [@]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 63, + "name": "Dynamic parsing of LHS a[$code]=value", + "class": "behavior", + "on": "stdout" + }, + { + "case": 65, + "name": "Is element set? test -v a[i]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 67, + "name": "test -v a[i] with arith expressions", + "class": "behavior", + "on": "stdout" + }, + { + "case": 71, + "name": "Regression: Negative out-of-range index in [[ -v a[index] ]]", + "class": "behavior", + "on": "stderr" + }, + { + "case": 73, + "name": "Regression: unset a[-2]: out-of-bound negative index should cause error", + "class": "behavior", + "on": "stderr" + }, + { + "case": 75, + "name": "Regression: Array length after unset", + "class": "behavior", + "on": "stdout" + }, + { + "case": 77, + "name": "Regression: silent out-of-bound negative index in ${a[-2]} and $((a[-2]))", + "class": "behavior", + "on": "stderr" + } + ], + "bash_fails": [] + }, + "prompt": { + "summary": { + "cases": 33, + "bashkit_pass": 4, + "bashkit_pass_pct": 12.1, + "bash_pass": 29, + "bash_pass_pct": 87.9, + "bashkit_pass_of_bash": 4, + "pass_rate": 13.8 + }, + "missed": [ + { + "case": 2, + "name": "literal escapes", + "class": "interactive", + "on": "stdout" + }, + { + "case": 4, + "name": "PS1 evaluation order", + "class": "interactive", + "on": "stdout" + }, + { + "case": 5, + "name": "PS1 evaluation order 2", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "\\1004", + "class": "interactive", + "on": "stdout" + }, + { + "case": 7, + "name": "\\001 octal literals are supported", + "class": "interactive", + "on": "stdout" + }, + { + "case": 8, + "name": "\\555 is beyond max octal byte of \\377 and wrapped to m", + "class": "interactive", + "on": "stdout" + }, + { + "case": 11, + "name": "Escaped backslash", + "class": "interactive", + "on": "stdout" + }, + { + "case": 12, + "name": "\\0001 octal literals are not supported", + "class": "interactive", + "on": "stdout" + }, + { + "case": 13, + "name": "\\u0001 unicode literals not supported", + "class": "interactive", + "on": "stdout" + }, + { + "case": 15, + "name": "hostname", + "class": "interactive", + "on": "stdout" + }, + { + "case": 16, + "name": "username", + "class": "interactive", + "on": "stdout" + }, + { + "case": 17, + "name": "current working dir", + "class": "interactive", + "on": "stdout" + }, + { + "case": 18, + "name": "\\W is basename of working dir", + "class": "interactive", + "on": "stdout" + }, + { + "case": 19, + "name": "\\t for 24h time (HH:MM:SS)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 20, + "name": "\\T for 12h time (HH:MM:SS)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 21, + "name": "\\@ for 12h time (HH:MM AM/PM)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 22, + "name": "\\A for 24h time (HH:MM)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 23, + "name": "\\d for date", + "class": "interactive", + "on": "stdout" + }, + { + "case": 24, + "name": "\\D{%H:%M} for strftime", + "class": "interactive", + "on": "stdout" + }, + { + "case": 25, + "name": "\\D{} for locale specific strftime", + "class": "interactive", + "on": "stdout" + }, + { + "case": 26, + "name": "\\s for shell, \\v for major.minor version, and \\V for full version", + "class": "interactive", + "on": "stdout" + }, + { + "case": 27, + "name": "\\j for number of jobs", + "class": "signals", + "on": "stdout" + }, + { + "case": 28, + "name": "\\l for TTY device basename", + "class": "interactive", + "on": "stdout" + }, + { + "case": 29, + "name": "\\! for history number", + "class": "interactive", + "on": "stdout" + }, + { + "case": 30, + "name": "\\# for command number", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [ + 0, + 1, + 3, + 32 + ] + }, + "builtin-trap-bash": { + "summary": { + "cases": 23, + "bashkit_pass": 4, + "bashkit_pass_pct": 17.4, + "bash_pass": 23, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 17.4 + }, + "missed": [ + { + "case": 3, + "name": "trap DEBUG ignores $?", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "but trap DEBUG respects errexit", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "trap DEBUG with 'return'", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "trap DEBUG with 'exit'", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "trap DEBUG with non-compound commands", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "trap DEBUG and control flow", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "trap DEBUG and command sub / subshell", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "trap DEBUG not run in forked interpreter for first pipeline part", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "One 'echo' in first pipeline part - why does bash behave differently from case above?", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "trap DEBUG and pipeline (lastpipe difference)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "trap DEBUG function call", + "class": "behavior", + "on": "stdout" + }, + { + "case": 14, + "name": "trap DEBUG case", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "trap DEBUG for each", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "trap DEBUG for expr", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "trap DEBUG if while", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "Compare trap DEBUG vs. trap ERR", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "Combine DEBUG trap and USR1 trap", + "class": "signals", + "on": "stdout" + }, + { + "case": 21, + "name": "Combine ERR trap and USR1 trap", + "class": "signals", + "on": "stdout" + }, + { + "case": 22, + "name": "Combine DEBUG trap and ERR trap", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-ref": { + "summary": { + "cases": 31, + "bashkit_pass": 15, + "bashkit_pass_pct": 48.4, + "bash_pass": 30, + "bash_pass_pct": 96.8, + "bashkit_pass_of_bash": 15, + "pass_rate": 50.0 + }, + "missed": [ + { + "case": 2, + "name": "${!undef:-}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "${!a[@]-'default'} is legal but fails with more than one element", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "var ref: 1, @, *", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "var ref TO array var", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "var ref TO array var, with subscripts", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "var ref TO assoc array a[key]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "var ref TO array with arbitrary subscripts", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "Indirect expansion TO fancy expansion features bash disallows", + "class": "behavior", + "on": "stdout" + }, + { + "case": 22, + "name": "Bad var ref", + "class": "behavior", + "on": "stdout" + }, + { + "case": 23, + "name": "Bad var ref 2", + "class": "behavior", + "on": "stdout" + }, + { + "case": 26, + "name": "Var Ref Code Injection $(tee PWNED)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "${!array_ref:-set} and ${!array_ref:=assign}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 28, + "name": "Array indirect expansion with suffix operators", + "class": "behavior", + "on": "stdout" + }, + { + "case": 29, + "name": "Array indirect expansion with replacements", + "class": "behavior", + "on": "stdout" + }, + { + "case": 30, + "name": "Array indirect expansion with @? conversion", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 12 + ] + }, + "builtin-fc": { + "summary": { + "cases": 14, + "bashkit_pass": 2, + "bashkit_pass_pct": 14.3, + "bash_pass": 14, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 14.3 + }, + "missed": [ + { + "case": 0, + "name": "fc -l lists history commands", + "class": "interactive", + "on": "stdout" + }, + { + "case": 1, + "name": "fc -ln lists history commands without numbers", + "class": "interactive", + "on": "stdout" + }, + { + "case": 2, + "name": "fc -lr lists history commands in reverse order", + "class": "interactive", + "on": "stdout" + }, + { + "case": 3, + "name": "fc -lnr lists history commands without numbers in reverse order", + "class": "interactive", + "on": "stdout" + }, + { + "case": 4, + "name": "fc -l lists history commands with default page size", + "class": "interactive", + "on": "stdout" + }, + { + "case": 5, + "name": "fc -l [first] where first is an index", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "fc -l [first] where first is an offset from current command", + "class": "interactive", + "on": "stdout" + }, + { + "case": 7, + "name": "fc -l [first] [last] where first and last are indexes", + "class": "interactive", + "on": "stdout" + }, + { + "case": 8, + "name": "fc -l [first] [last] where first and last are offsets from current command", + "class": "interactive", + "on": "stdout" + }, + { + "case": 9, + "name": "fc -l [first] [last] where first and last are reversed indexes", + "class": "interactive", + "on": "stdout" + }, + { + "case": 10, + "name": "fc -lr [first] [last] where first and last are reversed indexes does not undo reverse", + "class": "interactive", + "on": "stdout" + }, + { + "case": 12, + "name": "fc errors out on too many args with strict_arg_parse", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-history": { + "summary": { + "cases": 17, + "bashkit_pass": 5, + "bashkit_pass_pct": 29.4, + "bash_pass": 17, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 29.4 + }, + "missed": [ + { + "case": 0, + "name": "history -a", + "class": "interactive", + "on": "stdout" + }, + { + "case": 1, + "name": "history -w writes out the in-memory history to the history file", + "class": "interactive", + "on": "stdout" + }, + { + "case": 2, + "name": "history -r reads from the history file, and appends it to the current history", + "class": "interactive", + "on": "stdout" + }, + { + "case": 3, + "name": "history -n reads *new* commands from the history file, and appends them to the current history", + "class": "interactive", + "on": "stdout" + }, + { + "case": 4, + "name": "history -c clears in-memory history", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "history -d to delete history from end", + "class": "interactive", + "on": "stdout" + }, + { + "case": 9, + "name": "HISTFILE set to array", + "class": "interactive", + "on": "stdout" + }, + { + "case": 11, + "name": "history usage", + "class": "interactive", + "on": "stdout" + }, + { + "case": 12, + "name": "HISTSIZE shrinks the in-memory history when changed", + "class": "interactive", + "on": "stdout" + }, + { + "case": 13, + "name": "HISTFILESIZE shrinks the history file when changed", + "class": "behavior", + "on": "stdout" + }, + { + "case": 14, + "name": "recording history can be toggled with set -o/+o history", + "class": "interactive", + "on": "stdout" + }, + { + "case": 16, + "name": "shopt histappend - osh ignores shopt and appends, bash sometimes overwrites", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-type-bash": { + "summary": { + "cases": 31, + "bashkit_pass": 19, + "bashkit_pass_pct": 61.3, + "bash_pass": 31, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 19, + "pass_rate": 61.3 + }, + "missed": [ + { + "case": 1, + "name": "type -t -> alias", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "type -t doesn't find non-executable (like command -v)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "type -a -P gives multiple files", + "class": "host", + "on": "stdout" + }, + { + "case": 14, + "name": "type -P builtin -> not a file but file found", + "class": "host", + "on": "stdout" + }, + { + "case": 16, + "name": "type -f builtin -> function and file exists", + "class": "host", + "on": "stdout" + }, + { + "case": 19, + "name": "type -a -> alias; prints alias definition", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "type -ap -> alias", + "class": "behavior", + "on": "status" + }, + { + "case": 24, + "name": "type -a -> file", + "class": "host", + "on": "stdout" + }, + { + "case": 25, + "name": "type -ap -> file; abbreviated", + "class": "host", + "on": "stdout" + }, + { + "case": 26, + "name": "type -a -> builtin and file", + "class": "host", + "on": "stdout" + }, + { + "case": 27, + "name": "type -a -> builtin and file and shell function", + "class": "host", + "on": "stdout" + }, + { + "case": 28, + "name": "type -ap -> builtin and file; doesn't print builtin or function", + "class": "host", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "interactive": { + "summary": { + "cases": 18, + "bashkit_pass": 7, + "bashkit_pass_pct": 38.9, + "bash_pass": 18, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 38.9 + }, + "missed": [ + { + "case": 0, + "name": "'exit' in oshrc (regression)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 2, + "name": "interactive shell loads rcfile (when combined with -c)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 7, + "name": "interactive shell runs PROMPT_COMMAND after each command", + "class": "interactive", + "on": "stdout" + }, + { + "case": 10, + "name": "Error message with bad oshrc file (currently ignored)", + "class": "interactive", + "on": "stdout" + }, + { + "case": 11, + "name": "PROMPT_COMMAND can see $?, like bash", + "class": "interactive", + "on": "stdout" + }, + { + "case": 12, + "name": "PROMPT_COMMAND that writes to BASH_REMATCH", + "class": "interactive", + "on": "stdout" + }, + { + "case": 13, + "name": "NO ASSERTIONS: Are startup files sourced before or after job control?", + "class": "processes", + "on": "status" + }, + { + "case": 14, + "name": "HISTFILE is written in interactive shell", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "HISTFILE default value", + "class": "interactive", + "on": "stdout" + }, + { + "case": 16, + "name": "HISTFILE=my-history loads history from that file, and writes back to it", + "class": "interactive", + "on": "stdout" + }, + { + "case": 17, + "name": "HISTFILE=my-history with history -a", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "sh-options": { + "summary": { + "cases": 39, + "bashkit_pass": 28, + "bashkit_pass_pct": 71.8, + "bash_pass": 39, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 28, + "pass_rate": 71.8 + }, + "missed": [ + { + "case": 1, + "name": "$- with pipefail", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "$- with interactive shell", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "pass shopt options like sh -O nullglob", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "vi and emacs are mutually exclusive", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "interactive shell starts with emacs mode on", + "class": "interactive", + "on": "stdout" + }, + { + "case": 17, + "name": "shopt with no flags prints options", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "noclobber on <>", + "class": "behavior", + "on": "stdout" + }, + { + "case": 22, + "name": "noclobber on &> >", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "stubbed out bash options", + "class": "behavior", + "on": "stdout" + }, + { + "case": 35, + "name": "Unimplemented options - print, query, set, unset", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "Unimplemented options - OSH shopt -s ignore_shopt_not_impl", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "vars-special": { + "summary": { + "cases": 42, + "bashkit_pass": 29, + "bashkit_pass_pct": 69.0, + "bash_pass": 40, + "bash_pass_pct": 95.2, + "bashkit_pass_of_bash": 29, + "pass_rate": 72.5 + }, + "missed": [ + { + "case": 1, + "name": "$PWD is not only set, but exported", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "$PATH is set if unset at startup", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "$HOME is NOT set", + "class": "interactive", + "on": "stdout" + }, + { + "case": 4, + "name": "Vars set interactively only: $HISTFILE", + "class": "interactive", + "on": "stdout" + }, + { + "case": 5, + "name": "Some vars are set, even without startup file, or env: PATH, PWD", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "UID EUID PPID can't be changed", + "class": "processes", + "on": "stdout" + }, + { + "case": 13, + "name": "$BASHPID DOES change with subshell and command sub", + "class": "processes", + "on": "stdout" + }, + { + "case": 25, + "name": "$LINENO in ((", + "class": "behavior", + "on": "stdout" + }, + { + "case": 28, + "name": "$LINENO in for (( loop", + "class": "behavior", + "on": "stdout" + }, + { + "case": 37, + "name": "$_ with assignments, arrays, etc.", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "$_ is not undefined on first use", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 34, + 40 + ] + }, + "array-assign": { + "summary": { + "cases": 11, + "bashkit_pass": 1, + "bashkit_pass_pct": 9.1, + "bash_pass": 11, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 1, + "pass_rate": 9.1 + }, + "missed": [ + { + "case": 1, + "name": "Indexed LHS with spaces", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "Nested a[i[0]]=0", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "Multiple LHS array words", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "LHS array is protected with shopt -s eval_unsafe_arith, e.g. 'a[$(echo 2)]'", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "file named a[ is not executed", + "class": "host", + "on": "stdout" + }, + { + "case": 6, + "name": "More fragments like a[ a[5 a[5 + a[5 + 3]", + "class": "host", + "on": "stdout" + }, + { + "case": 7, + "name": "Are quotes allowed?", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "Tricky parsing - a[ a[0]=1 ]=X a[ a[0]+=1 ]+=X", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "argv.py a[1 + 2]=", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "declare builtin doesn't allow spaces", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "array-sparse": { + "summary": { + "cases": 40, + "bashkit_pass": 30, + "bashkit_pass_pct": 75.0, + "bash_pass": 40, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 30, + "pass_rate": 75.0 + }, + "missed": [ + { + "case": 11, + "name": "unset -v a[i] with out-of-bound negative index", + "class": "behavior", + "on": "stderr" + }, + { + "case": 12, + "name": "unset -v a[i] for max index", + "class": "behavior", + "on": "stdout" + }, + { + "case": 14, + "name": "[[ -v a[i] ]] with invalid negative index", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "((sp[i])) and ((sp[i]++)) with invalid negative index", + "class": "behavior", + "on": "stderr" + }, + { + "case": 18, + "name": "${sp[i]} with negative invalid index", + "class": "behavior", + "on": "stderr" + }, + { + "case": 19, + "name": "${a[@]:offset:length}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 25, + "name": "${a[@]@P}, ${a[@]@Q}, and ${a[@]@a}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "${a-}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 28, + "name": "${!a[0]}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 31, + "name": "compgen -F _set_COMPREPLY", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-bind": { + "summary": { + "cases": 9, + "bashkit_pass": 0, + "bashkit_pass_pct": 0.0, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 0, + "pass_rate": 0.0 + }, + "missed": [ + { + "case": 0, + "name": "bind -l should report readline functions", + "class": "interactive", + "on": "stdout" + }, + { + "case": 1, + "name": "bind -p -P to print function names and key bindings", + "class": "interactive", + "on": "stdout" + }, + { + "case": 2, + "name": "bind -s -S accepted", + "class": "interactive", + "on": "status" + }, + { + "case": 3, + "name": "bind -v -V accepted", + "class": "interactive", + "on": "stdout" + }, + { + "case": 4, + "name": "bind -q", + "class": "interactive", + "on": "stdout" + }, + { + "case": 5, + "name": "bind -X", + "class": "interactive", + "on": "stdout" + }, + { + "case": 6, + "name": "bind -m with bind -x/-X", + "class": "interactive", + "on": "stdout" + }, + { + "case": 7, + "name": "bind -r", + "class": "interactive", + "on": "stdout" + }, + { + "case": 8, + "name": "bind -r of bind -x commands", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-read": { + "summary": { + "cases": 64, + "bashkit_pass": 55, + "bashkit_pass_pct": 85.9, + "bash_pass": 64, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 55, + "pass_rate": 85.9 + }, + "missed": [ + { + "case": 33, + "name": "read -t 0 tests if input is available", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "read -u", + "class": "behavior", + "on": "stdout" + }, + { + "case": 38, + "name": "read -u -s", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "read -u 3 -d 5", + "class": "behavior", + "on": "stdout" + }, + { + "case": 40, + "name": "read -u 3 -d b -N 6", + "class": "behavior", + "on": "stdout" + }, + { + "case": 41, + "name": "read -N doesn't respect delimiter, while read -n does", + "class": "behavior", + "on": "stdout" + }, + { + "case": 48, + "name": "mapfile from directory (bash doesn't handle errors)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 54, + "name": "empty input and splitting", + "class": "behavior", + "on": "stdout" + }, + { + "case": 63, + "name": "read bash bug", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-trap": { + "summary": { + "cases": 33, + "bashkit_pass": 24, + "bashkit_pass_pct": 72.7, + "bash_pass": 33, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 24, + "pass_rate": 72.7 + }, + "missed": [ + { + "case": 0, + "name": "traps are not active inside subshells $() () trap | cat", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "trap EXIT calling exit", + "class": "behavior", + "on": "status" + }, + { + "case": 20, + "name": "trap EXIT with PARSE error and explicit exit", + "class": "behavior", + "on": "status" + }, + { + "case": 24, + "name": "exit codes for traps are isolated", + "class": "signals", + "on": "stdout" + }, + { + "case": 25, + "name": "traps are cleared in subshell (started with &)", + "class": "signals", + "on": "stdout" + }, + { + "case": 26, + "name": "trap USR1, sleep, SIGINT: non-interactively", + "class": "behavior", + "on": "stdout" + }, + { + "case": 28, + "name": "trap EXIT, sleep, SIGINT: non-interactively", + "class": "behavior", + "on": "stdout" + }, + { + "case": 29, + "name": "Remove trap with an unsigned integer", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "trap with command.NoOp - check internal invariant", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "introspect": { + "summary": { + "cases": 13, + "bashkit_pass": 3, + "bashkit_pass_pct": 23.1, + "bash_pass": 12, + "bash_pass_pct": 92.3, + "bashkit_pass_of_bash": 3, + "pass_rate": 25.0 + }, + "missed": [ + { + "case": 0, + "name": "${FUNCNAME[@]} array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 1, + "name": "FUNCNAME with source (scalar or array)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "BASH_SOURCE and BASH_LINENO scalar or array (e.g. for virtualenv)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "$((BASH_LINENO)) (scalar form in arith)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "${BASH_SOURCE[@]} with source and function name", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "${BASH_LINENO[@]} is a stack of line numbers for function calls", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "Locations with temp frame", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Locations when sourcing", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "Sourcing inside function grows the debug stack", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 8 + ] + }, + "nul-bytes": { + "summary": { + "cases": 16, + "bashkit_pass": 7, + "bashkit_pass_pct": 43.8, + "bash_pass": 16, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 43.8 + }, + "missed": [ + { + "case": 1, + "name": "printf - literal NUL in format string", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "printf - NUL byte in value (OSH and zsh agree)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "NUL bytes with echo $'\\0' (OSH and zsh agree)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "NUL bytes with test -n", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "NUL bytes with test -f", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "NUL bytes with ${#s} (OSH and zsh agree)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "Compare \\x00 byte versus \\x01 byte - read builtin", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Compare \\x00 byte versus \\x01 byte - read -n", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "Compare \\x00 byte versus \\x01 byte - mapfile builtin", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "redirect-multi": { + "summary": { + "cases": 13, + "bashkit_pass": 4, + "bashkit_pass_pct": 30.8, + "bash_pass": 13, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 30.8 + }, + "missed": [ + { + "case": 0, + "name": "File redirects with glob args (bash and zsh only)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "File redirect without matching any file, with failglob", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "File redirect to $var with glob char", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "File redirect that globs to more than one file (bash and zsh only)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "File redirect with extended glob", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "Extended glob that doesn't match anything", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "Non-file redirects don't respect glob args (we differe from bash)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Redirect with brace expansion isn't allowed", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "File redirects have word splitting too!", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "ysh-builtin-private": { + "summary": { + "cases": 23, + "bashkit_pass": 13, + "bashkit_pass_pct": 56.5, + "bash_pass": 22, + "bash_pass_pct": 95.7, + "bashkit_pass_of_bash": 13, + "pass_rate": 59.1 + }, + "missed": [ + { + "case": 9, + "name": "builtin sleep behaves like external sleep", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "builtin cat", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "builtin cat usage", + "class": "behavior", + "on": "stdout" + }, + { + "case": 14, + "name": "builtin cat non nonexistent file", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "builtin cat accept - for stdin", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "builtin rm: usage, removing files", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "builtin rm -f - ignores arguments that don't exist", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "builtin rm always fails on directories (regardless of -f)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 22, + "name": "compgen -A builtin doesn't find private builtins", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [ + 18 + ] + }, + "arith": { + "summary": { + "cases": 74, + "bashkit_pass": 66, + "bashkit_pass_pct": 89.2, + "bash_pass": 74, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 66, + "pass_rate": 89.2 + }, + "missed": [ + { + "case": 0, + "name": "Side Effect in Array Indexing", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "Arith sub within arith sub", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "Backticks within arith sub", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "Integer constant validation", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "Increment undefined variables with nounset", + "class": "behavior", + "on": "stdout" + }, + { + "case": 40, + "name": "nounset with arithmetic", + "class": "behavior", + "on": "stdout" + }, + { + "case": 50, + "name": "Comment not allowed in the middle of multiline arithmetic", + "class": "behavior", + "on": "stdout" + }, + { + "case": 70, + "name": "undef[0] with nounset", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "array-assoc": { + "summary": { + "cases": 42, + "bashkit_pass": 28, + "bashkit_pass_pct": 66.7, + "bash_pass": 36, + "bash_pass_pct": 85.7, + "bashkit_pass_of_bash": 28, + "pass_rate": 77.8 + }, + "missed": [ + { + "case": 15, + "name": "lookup with unquoted $key and quoted \"$i$i\"", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "Indexed array as key of associative array coerces to string (without shopt -s strict_array)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 30, + "name": "unset -v and assoc array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 31, + "name": "nameref and assoc array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "${!ref} and assoc array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "printf -v and assoc array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "test -v assoc[key]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 37, + "name": "test -v with dynamic parsing", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 1, + 2, + 21, + 26, + 34, + 41 + ] + }, + "assign-extended": { + "summary": { + "cases": 39, + "bashkit_pass": 27, + "bashkit_pass_pct": 69.2, + "bash_pass": 35, + "bash_pass_pct": 89.7, + "bashkit_pass_of_bash": 27, + "pass_rate": 77.1 + }, + "missed": [ + { + "case": 4, + "name": "declare -F with shopt -s extdebug prints more info", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "declare -F with shopt -s extdebug and main file", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "ble.sh: eval -- \"$(declare -p var arr)\"", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "eval -- \"$(declare -p arr)\" (restore arrays w/ unset elements)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "declare -p UNDEF (and typeset) -- prints something to stderr", + "class": "behavior", + "on": "stdout" + }, + { + "case": 29, + "name": "syntax error in array assignment", + "class": "behavior", + "on": "stdout" + }, + { + "case": 31, + "name": "myvar=typeset (another form of dynamic assignment)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 38, + "name": "unset and shell funcs", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 7, + 8, + 10, + 11 + ] + }, + "bugs": { + "summary": { + "cases": 29, + "bashkit_pass": 21, + "bashkit_pass_pct": 72.4, + "bash_pass": 29, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 21, + "pass_rate": 72.4 + }, + "missed": [ + { + "case": 5, + "name": "assign readonly -- multiple lines -- set -o posix", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "Function names", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "file with NUL byte", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "'echo' and printf fail on writing to full disk", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "other builtins fail on writing to full disk", + "class": "processes", + "on": "stdout" + }, + { + "case": 19, + "name": "command execution $(echo 42 | tee PWNED) not allowed", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "unset doesn't allow command execution", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "Crash after changing $[] to be alias of $(( ))", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-op-test": { + "summary": { + "cases": 37, + "bashkit_pass": 29, + "bashkit_pass_pct": 78.4, + "bash_pass": 37, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 29, + "pass_rate": 78.4 + }, + "missed": [ + { + "case": 15, + "name": "Nix idiom ${!hooksSlice+\"${!hooksSlice}\"} - was workaround for obsolete bash 4.3 bug", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "array and - and +", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "$@ (\"\" \"\") and - and +", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "$* (\"\" \"\") and - and + (IFS=)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "op-test for ${a} and ${a[0]}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 34, + "name": "op-test for ${!array} with array=\"a\" and array=\"a[0]\"", + "class": "behavior", + "on": "stdout" + }, + { + "case": 35, + "name": "op-test for ${!array} with array=\"a[@]\" or array=\"a[*]\"", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "op-test for unquoted ${a[*]:-empty} with IFS=", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "alias": { + "summary": { + "cases": 48, + "bashkit_pass": 41, + "bashkit_pass_pct": 85.4, + "bash_pass": 48, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 41, + "pass_rate": 85.4 + }, + "missed": [ + { + "case": 2, + "name": "define and use alias on a single line", + "class": "behavior", + "on": "stdout" + }, + { + "case": 23, + "name": "Loop split across alias in another way", + "class": "behavior", + "on": "stdout" + }, + { + "case": 24, + "name": "Loop split across both iterative and recursive aliases", + "class": "behavior", + "on": "stdout" + }, + { + "case": 30, + "name": "Alias can be defined and used on a single line", + "class": "behavior", + "on": "stdout" + }, + { + "case": 35, + "name": "alias for left brace", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "alias for left paren", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "here doc inside alias", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "array-literal": { + "summary": { + "cases": 19, + "bashkit_pass": 12, + "bashkit_pass_pct": 63.2, + "bash_pass": 19, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 12, + "pass_rate": 63.2 + }, + "missed": [ + { + "case": 0, + "name": "Tilde expansions in RHS of [k]=v (BashArray)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 1, + "name": "Tilde expansions in RHS of [k]=v (BashAssoc)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "append to element (BashAssoc)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "non-index forms of element (BashAssoc)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "Evaluation order (1)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "Evaluation order (2)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "Evaluation order (3)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "background": { + "summary": { + "cases": 27, + "bashkit_pass": 20, + "bashkit_pass_pct": 74.1, + "bash_pass": 27, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 20, + "pass_rate": 74.1 + }, + "missed": [ + { + "case": 1, + "name": "wait -n with arguments - arguments are respected", + "class": "processes", + "on": "stdout" + }, + { + "case": 5, + "name": "wait with invalid arg", + "class": "processes", + "on": "status" + }, + { + "case": 7, + "name": "wait for N parallel jobs and check failure", + "class": "processes", + "on": "stdout" + }, + { + "case": 11, + "name": "Start background pipeline, wait %job_spec", + "class": "processes", + "on": "stdout" + }, + { + "case": 12, + "name": "Wait for job and PIPESTATUS", + "class": "processes", + "on": "stdout" + }, + { + "case": 13, + "name": "Wait for job and PIPESTATUS - cat", + "class": "processes", + "on": "stdout" + }, + { + "case": 26, + "name": "Signal message for killed background job", + "class": "signals", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "ble-idioms": { + "summary": { + "cases": 26, + "bashkit_pass": 19, + "bashkit_pass_pct": 73.1, + "bash_pass": 26, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 19, + "pass_rate": 73.1 + }, + "missed": [ + { + "case": 5, + "name": "recursive arith: recursion", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "dynamic arith varname: copy/add", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "is-array with ${var@a}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "shift unshift reverse", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "shopt -u expand_aliases and eval", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "Issue #1069 [53] BUG: a[1 + 1]=2, etc. fails", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "Issue #1069 [53] - LHS array parsing a[1 + 2]=3 (see spec/array-assign for more)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-bracket": { + "summary": { + "cases": 52, + "bashkit_pass": 43, + "bashkit_pass_pct": 82.7, + "bash_pass": 50, + "bash_pass_pct": 96.2, + "bashkit_pass_of_bash": 43, + "pass_rate": 86.0 + }, + "missed": [ + { + "case": 27, + "name": "-k for sticky bit", + "class": "behavior", + "on": "stdout" + }, + { + "case": 36, + "name": "-G and -O for effective user ID and group ID", + "class": "behavior", + "on": "stdout" + }, + { + "case": 37, + "name": "-u for setuid, -g too", + "class": "host", + "on": "stdout" + }, + { + "case": 38, + "name": "-v to test variable (bash)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "test -o for options", + "class": "behavior", + "on": "stdout" + }, + { + "case": 41, + "name": "-ef", + "class": "behavior", + "on": "stdout" + }, + { + "case": 44, + "name": "test -c", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 25, + 26 + ] + }, + "command_": { + "summary": { + "cases": 16, + "bashkit_pass": 9, + "bashkit_pass_pct": 56.2, + "bash_pass": 16, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 9, + "pass_rate": 56.2 + }, + "missed": [ + { + "case": 3, + "name": "Name too long", + "class": "behavior", + "on": "status" + }, + { + "case": 8, + "name": "$PATH lookup", + "class": "host", + "on": "stdout" + }, + { + "case": 9, + "name": "filling $PATH cache, then insert the same command earlier in cache", + "class": "host", + "on": "stdout" + }, + { + "case": 10, + "name": "filling $PATH cache, then deleting command", + "class": "host", + "on": "stdout" + }, + { + "case": 11, + "name": "Non-executable on $PATH", + "class": "host", + "on": "stdout" + }, + { + "case": 12, + "name": "hash without args prints the cache", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "hash with args", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "redirect": { + "summary": { + "cases": 41, + "bashkit_pass": 34, + "bashkit_pass_pct": 82.9, + "bash_pass": 41, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 34, + "pass_rate": 82.9 + }, + "missed": [ + { + "case": 0, + "name": ">& and <& are the same", + "class": "behavior", + "on": "stderr" + }, + { + "case": 23, + "name": "&> redirects stdout and stderr", + "class": "processes", + "on": "stdout" + }, + { + "case": 24, + "name": ">&word redirects stdout and stderr when word is not a number or -", + "class": "processes", + "on": "stdout" + }, + { + "case": 26, + "name": "1>&2- to move file descriptor", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "1>&2- (Bash bug: fail to restore closed fd)", + "class": "processes", + "on": "stdout" + }, + { + "case": 29, + "name": "<> for read/write named pipes", + "class": "host", + "on": "stdout" + }, + { + "case": 30, + "name": "&>> appends stdout and stderr", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "sh-usage": { + "summary": { + "cases": 24, + "bashkit_pass": 17, + "bashkit_pass_pct": 70.8, + "bash_pass": 24, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 17, + "pass_rate": 70.8 + }, + "missed": [ + { + "case": 2, + "name": "sh +c is accepted", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "sh - and sh -- stop flag processing", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "Set LC_ALL LC_CTYPE LC_COLLATE LANG - affects glob ?", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "LC_CTYPE=invalid", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "sh -c -- 'echo hi' does not run anything (#2637)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 22, + "name": "'sh -c -z' does not try to run -z", + "class": "behavior", + "on": "stdout" + }, + { + "case": 23, + "name": "sh -c -x 'echo hi' - order is reversed", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-process": { + "summary": { + "cases": 26, + "bashkit_pass": 19, + "bashkit_pass_pct": 73.1, + "bash_pass": 24, + "bash_pass_pct": 92.3, + "bashkit_pass_of_bash": 18, + "pass_rate": 75.0 + }, + "missed": [ + { + "case": 3, + "name": "exec builtin accepts --", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "exec -- 2>&1", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "exec -a sets argv[0]", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "arg that would overflow 64 bits is detected", + "class": "processes", + "on": "stdout" + }, + { + "case": 21, + "name": "ulimit -f 1 prevents files larger 512 bytes", + "class": "processes", + "on": "stdout" + }, + { + "case": 25, + "name": "ulimit -n limits file descriptors", + "class": "processes", + "on": "stdout" + } + ], + "bash_fails": [ + 9, + 22 + ] + }, + "builtin-vars": { + "summary": { + "cases": 41, + "bashkit_pass": 33, + "bashkit_pass_pct": 80.5, + "bash_pass": 39, + "bash_pass_pct": 95.1, + "bashkit_pass_of_bash": 33, + "pass_rate": 84.6 + }, + "missed": [ + { + "case": 16, + "name": "Make an existing local variable readonly", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "assign to readonly variable - errexit", + "class": "behavior", + "on": "status" + }, + { + "case": 22, + "name": "Unset a function without -f", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "unset -v assoc (related to issue #661)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 35, + "name": "Unset array member with dynamic parsing", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "unset a[-1] (bf.bash regression)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 26, + 32 + ] + }, + "here-doc": { + "summary": { + "cases": 36, + "bashkit_pass": 30, + "bashkit_pass_pct": 83.3, + "bash_pass": 36, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 30, + "pass_rate": 83.3 + }, + "missed": [ + { + "case": 3, + "name": "Here doc from another input file descriptor", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "Multiple here docs with different descriptors", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Here doc line continuation", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "Here doc with line continuation, then pipe. Syntax error.", + "class": "behavior", + "on": "status" + }, + { + "case": 32, + "name": "Multiple here docs in pipeline", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "Multiple here docs in pipeline on multiple lines", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "sh-options-bash": { + "summary": { + "cases": 9, + "bashkit_pass": 3, + "bashkit_pass_pct": 33.3, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 3, + "pass_rate": 33.3 + }, + "missed": [ + { + "case": 0, + "name": "SHELLOPTS is updated when options are changed", + "class": "behavior", + "on": "stdout" + }, + { + "case": 1, + "name": "SHELLOPTS is readonly", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "SHELLOPTS and BASHOPTS are non-empty", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "SHELLOPTS reflects flags like sh -x", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "export SHELLOPTS does cross-process tracing", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "export SHELLOPTS does cross-process tracing with bash", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "xtrace": { + "summary": { + "cases": 19, + "bashkit_pass": 13, + "bashkit_pass_pct": 68.4, + "bash_pass": 19, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 13, + "pass_rate": 68.4 + }, + "missed": [ + { + "case": 0, + "name": "unset PS4", + "class": "interactive", + "on": "stderr" + }, + { + "case": 1, + "name": "set -o verbose prints unevaluated code", + "class": "behavior", + "on": "stderr" + }, + { + "case": 2, + "name": "xtrace with unprintable chars", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "Assignments and assign builtins", + "class": "behavior", + "on": "stderr" + }, + { + "case": 10, + "name": "[[ ]]", + "class": "behavior", + "on": "stderr" + }, + { + "case": 17, + "name": "Regression: xtrace for \"declare -a a+=(v)\"", + "class": "behavior", + "on": "stderr" + } + ], + "bash_fails": [] + }, + "arith-context": { + "summary": { + "cases": 16, + "bashkit_pass": 11, + "bashkit_pass_pct": 68.8, + "bash_pass": 16, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 11, + "pass_rate": 68.8 + }, + "missed": [ + { + "case": 6, + "name": "Ambiguous colon in slice", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "$[ is a synonym for $((", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "$[$var is a synonym for $(($var (#2426)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "$[$undefined] is a synonym for $(($undefined (#2566)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "Empty expression a[]", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "assign": { + "summary": { + "cases": 48, + "bashkit_pass": 40, + "bashkit_pass_pct": 83.3, + "bash_pass": 45, + "bash_pass_pct": 93.8, + "bashkit_pass_of_bash": 40, + "pass_rate": 88.9 + }, + "missed": [ + { + "case": 8, + "name": "Escaped = in command name", + "class": "behavior", + "on": "stdout" + }, + { + "case": 25, + "name": "Test above without 'local' (which is not POSIX)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 30, + "name": "assignment using dynamic keyword (splits in most shells, not in zsh/osh)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "assign and glob", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "declare and glob", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 24, + 27, + 46 + ] + }, + "blog1": { + "summary": { + "cases": 9, + "bashkit_pass": 4, + "bashkit_pass_pct": 44.4, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 44.4 + }, + "missed": [ + { + "case": 1, + "name": "${###}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "${####}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "${##2}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "${###2}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 7, + "name": "${#1#'###'}", + "class": "behavior", + "on": "status" + } + ], + "bash_fails": [] + }, + "builtin-cd": { + "summary": { + "cases": 30, + "bashkit_pass": 23, + "bashkit_pass_pct": 76.7, + "bash_pass": 28, + "bash_pass_pct": 93.3, + "bashkit_pass_of_bash": 23, + "pass_rate": 82.1 + }, + "missed": [ + { + "case": 5, + "name": "$OLDPWD", + "class": "behavior", + "on": "stdout" + }, + { + "case": 9, + "name": "setting $PWD doesn't affect the value of 'pwd' builtin", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "unset PWD; then pwd", + "class": "behavior", + "on": "stdout" + }, + { + "case": 14, + "name": "pwd in symlinked dir on shell initialization", + "class": "behavior", + "on": "stdout" + }, + { + "case": 26, + "name": "Survey of getcwd() syscall", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 24, + 25 + ] + }, + "builtin-eval-source": { + "summary": { + "cases": 23, + "bashkit_pass": 16, + "bashkit_pass_pct": 69.6, + "bash_pass": 21, + "bash_pass_pct": 91.3, + "bashkit_pass_of_bash": 16, + "pass_rate": 76.2 + }, + "missed": [ + { + "case": 1, + "name": "eval accepts/ignores --", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "eval usage", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "source accepts/ignores --", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "source looks in PATH for files", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "source finds files in PATH before current dir", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 13, + 14 + ] + }, + "builtin-meta": { + "summary": { + "cases": 18, + "bashkit_pass": 13, + "bashkit_pass_pct": 72.2, + "bash_pass": 18, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 13, + "pass_rate": 72.2 + }, + "missed": [ + { + "case": 3, + "name": "command -v doesn't find non-executable file", + "class": "host", + "on": "stdout" + }, + { + "case": 5, + "name": "command -V", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "command -V nonexistent", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "command -p (override existing program)", + "class": "host", + "on": "stdout" + }, + { + "case": 11, + "name": "command -p (hide tool in custom path)", + "class": "host", + "on": "status" + } + ], + "bash_fails": [] + }, + "builtin-meta-assign": { + "summary": { + "cases": 11, + "bashkit_pass": 6, + "bashkit_pass_pct": 54.5, + "bash_pass": 11, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 6, + "pass_rate": 54.5 + }, + "missed": [ + { + "case": 0, + "name": "builtin declare a=(x y) is allowed", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "export, builtin export", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "\\builtin declare - ble.sh relies on it", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "\\command readonly - similar issue", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "Dynamic $c readonly - bash and dash change behavior, mksh bug", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-printf": { + "summary": { + "cases": 63, + "bashkit_pass": 56, + "bashkit_pass_pct": 88.9, + "bash_pass": 58, + "bash_pass_pct": 92.1, + "bashkit_pass_of_bash": 53, + "pass_rate": 91.4 + }, + "missed": [ + { + "case": 16, + "name": "%06s is no-op", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "printf %6.0s and %0.0s", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "printf %6.s and %0.s", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "printf octal backslash escapes", + "class": "behavior", + "on": "stdout" + }, + { + "case": 51, + "name": "bash truncates long strftime string at 128", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 26, + 27, + 46, + 47, + 48 + ] + }, + "nameref": { + "summary": { + "cases": 32, + "bashkit_pass": 27, + "bashkit_pass_pct": 84.4, + "bash_pass": 32, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 27, + "pass_rate": 84.4 + }, + "missed": [ + { + "case": 15, + "name": "-n attribute on array is hard error, not a warning", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "exported nameref", + "class": "behavior", + "on": "stdout" + }, + { + "case": 26, + "name": "a[expr] in nameref", + "class": "behavior", + "on": "stdout" + }, + { + "case": 27, + "name": "a[@] in nameref", + "class": "behavior", + "on": "stdout" + }, + { + "case": 29, + "name": "bad mutation through nameref: ref[0]= where ref is array[0]", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "ble-features": { + "summary": { + "cases": 9, + "bashkit_pass": 5, + "bashkit_pass_pct": 55.6, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 55.6 + }, + "missed": [ + { + "case": 1, + "name": "[bash_unset] local-unset / dynamic-unset for localvar (mutated from tempenv)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "[bash_unset] local-unset / dynamic-unset for tempenv", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "[bash_unset] function call with tempenv vs tempenv-eval", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "[bash_unset] localvar-inherit from tempenv", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "ble-unset": { + "summary": { + "cases": 5, + "bashkit_pass": 1, + "bashkit_pass_pct": 20.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 1, + "pass_rate": 20.0 + }, + "missed": [ + { + "case": 1, + "name": "[bash_unset] local-unset / dynamic-unset for localvar on nested-context", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "[bash_unset] dynamic-unset for nested localvars", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "[bash_unset] dynamic-unset for nested tempenvs", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "[bash_unset] local-unset for nested tempenvs", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "brace-expansion": { + "summary": { + "cases": 55, + "bashkit_pass": 51, + "bashkit_pass_pct": 92.7, + "bash_pass": 55, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 51, + "pass_rate": 92.7 + }, + "missed": [ + { + "case": 11, + "name": "double expansion with simple var -- bash bug", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "double expansion with literal and simple var", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "Empty alternative with empty string suffix", + "class": "behavior", + "on": "stdout" + }, + { + "case": 42, + "name": "Mixed case char expansion is invalid", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-dirs": { + "summary": { + "cases": 18, + "bashkit_pass": 14, + "bashkit_pass_pct": 77.8, + "bash_pass": 18, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 14, + "pass_rate": 77.8 + }, + "missed": [ + { + "case": 1, + "name": "pushd usage", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "popd usage error", + "class": "behavior", + "on": "stdout" + }, + { + "case": 16, + "name": "pushd does not take more than one argument", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "dirs does not take arguments", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-kill": { + "summary": { + "cases": 20, + "bashkit_pass": 16, + "bashkit_pass_pct": 80.0, + "bash_pass": 19, + "bash_pass_pct": 95.0, + "bashkit_pass_of_bash": 15, + "pass_rate": 78.9 + }, + "missed": [ + { + "case": 8, + "name": "kill -l 10 TERM translates between names and numbers", + "class": "signals", + "on": "stdout" + }, + { + "case": 9, + "name": "kill -L checks for invalid input", + "class": "signals", + "on": "stdout" + }, + { + "case": 12, + "name": "kill -l 0 returns EXIT", + "class": "signals", + "on": "stdout" + }, + { + "case": 13, + "name": "kill -l 0 INT lists both signals", + "class": "signals", + "on": "stdout" + } + ], + "bash_fails": [ + 16 + ] + }, + "builtin-special": { + "summary": { + "cases": 12, + "bashkit_pass": 8, + "bashkit_pass_pct": 66.7, + "bash_pass": 12, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 8, + "pass_rate": 66.7 + }, + "missed": [ + { + "case": 1, + "name": "Prefix assignments persist after special builtins, like : (set -o posix)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "Prefix assignments persist after readonly, but NOT exported (set -o posix)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "Special builtins can't be redefined as shell functions (set -o posix)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "bash 'type' gets confused - says 'function', but runs builtin", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "parse-errors": { + "summary": { + "cases": 27, + "bashkit_pass": 21, + "bashkit_pass_pct": 77.8, + "bash_pass": 25, + "bash_pass_pct": 92.6, + "bashkit_pass_of_bash": 21, + "pass_rate": 84.0 + }, + "missed": [ + { + "case": 11, + "name": "} on the second line", + "class": "behavior", + "on": "status" + }, + { + "case": 16, + "name": "misplaced parentheses are not a subshell", + "class": "behavior", + "on": "status" + }, + { + "case": 22, + "name": "array literal inside array is a parse error", + "class": "behavior", + "on": "stdout" + }, + { + "case": 25, + "name": "%foo=() is parse error (regression)", + "class": "behavior", + "on": "status" + } + ], + "bash_fails": [ + 0, + 1 + ] + }, + "regex": { + "summary": { + "cases": 37, + "bashkit_pass": 33, + "bashkit_pass_pct": 89.2, + "bash_pass": 37, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 33, + "pass_rate": 89.2 + }, + "missed": [ + { + "case": 16, + "name": "Unquoted { is a regex parse error", + "class": "behavior", + "on": "stdout" + }, + { + "case": 18, + "name": "Quoted { and +", + "class": "behavior", + "on": "stdout" + }, + { + "case": 32, + "name": "Quotes '' \"\" $'' $\"\" in pattern", + "class": "behavior", + "on": "stdout" + }, + { + "case": 35, + "name": "make a lisp example", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-sub-quote": { + "summary": { + "cases": 41, + "bashkit_pass": 37, + "bashkit_pass_pct": 90.2, + "bash_pass": 41, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 37, + "pass_rate": 90.2 + }, + "missed": [ + { + "case": 18, + "name": "part_value tree with multiple words", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "part_value tree on RHS", + "class": "behavior", + "on": "stdout" + }, + { + "case": 38, + "name": "Right Brace as argument (similar to #702)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 39, + "name": "Var substitution with newlines (#2492)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "assign-dialects": { + "summary": { + "cases": 4, + "bashkit_pass": 1, + "bashkit_pass_pct": 25.0, + "bash_pass": 4, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 1, + "pass_rate": 25.0 + }, + "missed": [ + { + "case": 1, + "name": "test -v with strings", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "test -v with arrays", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "test -v with assoc arrays", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "blog2": { + "summary": { + "cases": 8, + "bashkit_pass": 5, + "bashkit_pass_pct": 62.5, + "bash_pass": 8, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 62.5 + }, + "missed": [ + { + "case": 4, + "name": "-a -a -a -a -a", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "-a -a -a -a -a -a", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "-a -a -a -a -a -a -a", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "bool-parse": { + "summary": { + "cases": 8, + "bashkit_pass": 5, + "bashkit_pass_pct": 62.5, + "bash_pass": 8, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 62.5 + }, + "missed": [ + { + "case": 0, + "name": "test builtin - Unexpected trailing word '--' (#2409)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 1, + "name": "test builtin: ( = ) is confusing: equality test or non-empty string test", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "test builtin: ( == ) is confusing: equality test or non-empty string test", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-bash": { + "summary": { + "cases": 13, + "bashkit_pass": 10, + "bashkit_pass_pct": 76.9, + "bash_pass": 13, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 10, + "pass_rate": 76.9 + }, + "missed": [ + { + "case": 0, + "name": "help", + "class": "behavior", + "on": "stderr" + }, + { + "case": 1, + "name": "bad help topic", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "mapfile (delimiter): -d '' (null-separated)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-misc": { + "summary": { + "cases": 7, + "bashkit_pass": 4, + "bashkit_pass_pct": 57.1, + "bash_pass": 7, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 57.1 + }, + "missed": [ + { + "case": 0, + "name": "history builtin usage", + "class": "interactive", + "on": "stdout" + }, + { + "case": 1, + "name": "Print shell strings with weird chars: set and printf %q and ${x@Q}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "time pipeline", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-set": { + "summary": { + "cases": 24, + "bashkit_pass": 21, + "bashkit_pass_pct": 87.5, + "bash_pass": 24, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 21, + "pass_rate": 87.5 + }, + "missed": [ + { + "case": 6, + "name": "set -u with undefined var in interactive shell does NOT exit the interpreter", + "class": "interactive", + "on": "stdout" + }, + { + "case": 16, + "name": "A single + is an ignored flag; not an argument", + "class": "behavior", + "on": "stdout" + }, + { + "case": 17, + "name": "set - + and + -", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-trap-err": { + "summary": { + "cases": 23, + "bashkit_pass": 19, + "bashkit_pass_pct": 82.6, + "bash_pass": 22, + "bash_pass_pct": 95.7, + "bashkit_pass_of_bash": 19, + "pass_rate": 86.4 + }, + "missed": [ + { + "case": 6, + "name": "trap ERR pipelines without simple commands", + "class": "behavior", + "on": "stdout" + }, + { + "case": 12, + "name": "set -o errtrace: trap ERR with &", + "class": "processes", + "on": "stdout" + }, + { + "case": 16, + "name": "trap ERR with \"atoms\": assignment (( [[", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 18 + ] + }, + "builtin-umask": { + "summary": { + "cases": 24, + "bashkit_pass": 21, + "bashkit_pass_pct": 87.5, + "bash_pass": 24, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 21, + "pass_rate": 87.5 + }, + "missed": [ + { + "case": 10, + "name": "'umask 0002' sets the umask", + "class": "host", + "on": "stdout" + }, + { + "case": 11, + "name": "set umask with symbolic mode: g-w,o-w", + "class": "host", + "on": "stdout" + }, + { + "case": 13, + "name": "umask with too many arguments (i.e. extra spaces)", + "class": "host", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "extglob-files": { + "summary": { + "cases": 23, + "bashkit_pass": 20, + "bashkit_pass_pct": 87.0, + "bash_pass": 23, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 20, + "pass_rate": 87.0 + }, + "missed": [ + { + "case": 15, + "name": "Extended glob in same word as array", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "no match", + "class": "behavior", + "on": "stdout" + }, + { + "case": 21, + "name": "noglob", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "fatal-errors": { + "summary": { + "cases": 5, + "bashkit_pass": 2, + "bashkit_pass_pct": 40.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 40.0 + }, + "missed": [ + { + "case": 1, + "name": "Unrecoverable: divide by zero in conditional word", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "Unrecoverable: ${undef?message}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "${undef} with nounset", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "func-parsing": { + "summary": { + "cases": 15, + "bashkit_pass": 12, + "bashkit_pass_pct": 80.0, + "bash_pass": 15, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 12, + "pass_rate": 80.0 + }, + "missed": [ + { + "case": 8, + "name": "= in function name", + "class": "behavior", + "on": "status" + }, + { + "case": 9, + "name": "Function name with $", + "class": "behavior", + "on": "status" + }, + { + "case": 10, + "name": "Function name with command sub", + "class": "behavior", + "on": "status" + } + ], + "bash_fails": [] + }, + "glob": { + "summary": { + "cases": 39, + "bashkit_pass": 34, + "bashkit_pass_pct": 87.2, + "bash_pass": 37, + "bash_pass_pct": 94.9, + "bashkit_pass_of_bash": 34, + "pass_rate": 91.9 + }, + "missed": [ + { + "case": 7, + "name": "glob can expand to command and arg", + "class": "behavior", + "on": "stdout" + }, + { + "case": 15, + "name": "glob with escaped - in char class", + "class": "behavior", + "on": "stdout" + }, + { + "case": 38, + "name": "shopt -u globskipdots shows . and ..", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 23, + 31 + ] + }, + "nix-idioms": { + "summary": { + "cases": 6, + "bashkit_pass": 3, + "bashkit_pass_pct": 50.0, + "bash_pass": 6, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 3, + "pass_rate": 50.0 + }, + "missed": [ + { + "case": 0, + "name": "var ref to array 'preHooks[@]'", + "class": "behavior", + "on": "stdout" + }, + { + "case": 1, + "name": "Similar to above with set -u", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "${!ref} to undefined string var is fatal, INCONSISTENT with array", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "quote": { + "summary": { + "cases": 35, + "bashkit_pass": 32, + "bashkit_pass_pct": 91.4, + "bash_pass": 35, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 32, + "pass_rate": 91.4 + }, + "missed": [ + { + "case": 27, + "name": "$'' octal escapes don't have leading 0", + "class": "behavior", + "on": "stdout" + }, + { + "case": 29, + "name": "OSH allows invalid backslashes", + "class": "behavior", + "on": "stdout" + }, + { + "case": 33, + "name": "$'' supports \\cA escape for Ctrl-A - mask with 0x1f", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "redirect-command": { + "summary": { + "cases": 23, + "bashkit_pass": 20, + "bashkit_pass_pct": 87.0, + "bash_pass": 23, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 20, + "pass_rate": 87.0 + }, + "missed": [ + { + "case": 8, + "name": "Redirect in the middle of two assignments", + "class": "behavior", + "on": "stdout" + }, + { + "case": 11, + "name": "Redirect in function body is evaluated multiple times", + "class": "behavior", + "on": "stdout" + }, + { + "case": 13, + "name": "redirect bash extensions: [[ (( for ((", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "serialize": { + "summary": { + "cases": 10, + "bashkit_pass": 7, + "bashkit_pass_pct": 70.0, + "bash_pass": 10, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 70.0 + }, + "missed": [ + { + "case": 3, + "name": "printf %q unprintable", + "class": "behavior", + "on": "stdout" + }, + { + "case": 5, + "name": "printf %q invalid unicode", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "${var@Q}", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-op-bash": { + "summary": { + "cases": 27, + "bashkit_pass": 23, + "bashkit_pass_pct": 85.2, + "bash_pass": 26, + "bash_pass_pct": 96.3, + "bashkit_pass_of_bash": 23, + "pass_rate": 88.5 + }, + "missed": [ + { + "case": 19, + "name": "${!A@a} and ${!A[@]@a}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 23, + "name": "${!r@a} with r='a[0]' (attribute for indirect expansion of an array element)", + "class": "behavior", + "on": "stdout" + }, + { + "case": 26, + "name": "Array expansion with nullary var op @a", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 3 + ] + }, + "var-op-len": { + "summary": { + "cases": 9, + "bashkit_pass": 6, + "bashkit_pass_pct": 66.7, + "bash_pass": 8, + "bash_pass_pct": 88.9, + "bashkit_pass_of_bash": 5, + "pass_rate": 62.5 + }, + "missed": [ + { + "case": 3, + "name": "String length with incomplete utf-8", + "class": "behavior", + "on": "stdout" + }, + { + "case": 4, + "name": "String length with invalid utf-8 continuation bytes", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "Length of undefined variable with nounset", + "class": "behavior", + "on": "status" + } + ], + "bash_fails": [ + 8 + ] + }, + "arg-parse": { + "summary": { + "cases": 3, + "bashkit_pass": 1, + "bashkit_pass_pct": 33.3, + "bash_pass": 3, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 1, + "pass_rate": 33.3 + }, + "missed": [ + { + "case": 1, + "name": "shift 1 extra", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "continue 1 extra, break, etc.", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "array-basic": { + "summary": { + "cases": 5, + "bashkit_pass": 3, + "bashkit_pass_pct": 60.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 3, + "pass_rate": 60.0 + }, + "missed": [ + { + "case": 1, + "name": "${a[@]} and ${a[*]}", + "class": "behavior", + "on": "stdout" + }, + { + "case": 2, + "name": "4 ways to interpolate empty array", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "assign-deferred": { + "summary": { + "cases": 9, + "bashkit_pass": 7, + "bashkit_pass_pct": 77.8, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 77.8 + }, + "missed": [ + { + "case": 7, + "name": "is 'builtin' prefix and array allowed? OSH is smarter", + "class": "behavior", + "on": "stdout" + }, + { + "case": 8, + "name": "is 'command' prefix and array allowed? OSH is smarter", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-getopts": { + "summary": { + "cases": 31, + "bashkit_pass": 29, + "bashkit_pass_pct": 93.5, + "bash_pass": 31, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 29, + "pass_rate": 93.5 + }, + "missed": [ + { + "case": 6, + "name": "getopts with invalid variable name", + "class": "behavior", + "on": "stdout" + }, + { + "case": 19, + "name": "flag and arg: -c10", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "builtin-type": { + "summary": { + "cases": 6, + "bashkit_pass": 4, + "bashkit_pass_pct": 66.7, + "bash_pass": 6, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 66.7 + }, + "missed": [ + { + "case": 1, + "name": "type -> alias external", + "class": "host", + "on": "stdout" + }, + { + "case": 2, + "name": "type of relative path", + "class": "host", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "divergence": { + "summary": { + "cases": 4, + "bashkit_pass": 2, + "bashkit_pass_pct": 50.0, + "bash_pass": 4, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 50.0 + }, + "missed": [ + { + "case": 1, + "name": "!( as negation and subshell versus extended glob - #2463", + "class": "behavior", + "on": "stdout" + }, + { + "case": 3, + "name": "builtin cat crashes a subshell (#2530)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "errexit": { + "summary": { + "cases": 35, + "bashkit_pass": 33, + "bashkit_pass_pct": 94.3, + "bash_pass": 35, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 33, + "pass_rate": 94.3 + }, + "missed": [ + { + "case": 33, + "name": "set -e in function #2", + "class": "behavior", + "on": "stdout" + }, + { + "case": 34, + "name": "Command sub exit code is lost", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "exit-status": { + "summary": { + "cases": 11, + "bashkit_pass": 9, + "bashkit_pass_pct": 81.8, + "bash_pass": 11, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 9, + "pass_rate": 81.8 + }, + "missed": [ + { + "case": 7, + "name": "If subshell true WITH ARGUMENT", + "class": "behavior", + "on": "stdout" + }, + { + "case": 10, + "name": "More test cases with empty argv", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "glob-bash": { + "summary": { + "cases": 8, + "bashkit_pass": 6, + "bashkit_pass_pct": 75.0, + "bash_pass": 8, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 6, + "pass_rate": 75.0 + }, + "missed": [ + { + "case": 3, + "name": "shopt -s failglob in array literal context", + "class": "behavior", + "on": "stdout" + }, + { + "case": 6, + "name": "shopt -s failglob behavior on single line with semicolon", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-op-patsub": { + "summary": { + "cases": 28, + "bashkit_pass": 26, + "bashkit_pass_pct": 92.9, + "bash_pass": 28, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 26, + "pass_rate": 92.9 + }, + "missed": [ + { + "case": 19, + "name": "When LC_ALL=C, pattern ? doesn't match multibyte character", + "class": "behavior", + "on": "stdout" + }, + { + "case": 23, + "name": "patsub with [^]]", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-op-slice": { + "summary": { + "cases": 22, + "bashkit_pass": 20, + "bashkit_pass_pct": 90.9, + "bash_pass": 22, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 20, + "pass_rate": 90.9 + }, + "missed": [ + { + "case": 9, + "name": "Slice undefined", + "class": "behavior", + "on": "stdout" + }, + { + "case": 20, + "name": "${array[@]:} vs ${array[@]: } - bash and zsh inconsistent", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "append": { + "summary": { + "cases": 20, + "bashkit_pass": 19, + "bashkit_pass_pct": 95.0, + "bash_pass": 20, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 19, + "pass_rate": 95.0 + }, + "missed": [ + { + "case": 11, + "name": "Try to append list to element", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "dbracket": { + "summary": { + "cases": 49, + "bashkit_pass": 48, + "bashkit_pass_pct": 98.0, + "bash_pass": 49, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 48, + "pass_rate": 98.0 + }, + "missed": [ + { + "case": 44, + "name": "tilde expansion with =~ (confusing)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "dparen": { + "summary": { + "cases": 15, + "bashkit_pass": 13, + "bashkit_pass_pct": 86.7, + "bash_pass": 14, + "bash_pass_pct": 93.3, + "bashkit_pass_of_bash": 13, + "pass_rate": 92.9 + }, + "missed": [ + { + "case": 11, + "name": "(( )) with redirect", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 10 + ] + }, + "errexit-osh": { + "summary": { + "cases": 35, + "bashkit_pass": 34, + "bashkit_pass_pct": 97.1, + "bash_pass": 35, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 34, + "pass_rate": 97.1 + }, + "missed": [ + { + "case": 30, + "name": "command_sub_errexit and command sub in array", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "extglob-match": { + "summary": { + "cases": 29, + "bashkit_pass": 28, + "bashkit_pass_pct": 96.6, + "bash_pass": 29, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 28, + "pass_rate": 96.6 + }, + "missed": [ + { + "case": 21, + "name": "Turning extglob on changes the meaning of [[ !(str) ]] in bash", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "for-expr": { + "summary": { + "cases": 9, + "bashkit_pass": 7, + "bashkit_pass_pct": 77.8, + "bash_pass": 8, + "bash_pass_pct": 88.9, + "bashkit_pass_of_bash": 7, + "pass_rate": 87.5 + }, + "missed": [ + { + "case": 2, + "name": "Accepts { } syntax too", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 6 + ] + }, + "globignore": { + "summary": { + "cases": 18, + "bashkit_pass": 17, + "bashkit_pass_pct": 94.4, + "bash_pass": 18, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 17, + "pass_rate": 94.4 + }, + "missed": [ + { + "case": 12, + "name": "Ignore .:..", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "history-expand": { + "summary": { + "cases": 1, + "bashkit_pass": 0, + "bashkit_pass_pct": 0.0, + "bash_pass": 1, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 0, + "pass_rate": 0.0 + }, + "missed": [ + { + "case": 0, + "name": "!! expansion", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "pipeline": { + "summary": { + "cases": 26, + "bashkit_pass": 24, + "bashkit_pass_pct": 92.3, + "bash_pass": 25, + "bash_pass_pct": 96.2, + "bashkit_pass_of_bash": 24, + "pass_rate": 96.0 + }, + "missed": [ + { + "case": 6, + "name": "Initial value of PIPESTATUS is empty string", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 10 + ] + }, + "process-sub": { + "summary": { + "cases": 9, + "bashkit_pass": 8, + "bashkit_pass_pct": 88.9, + "bash_pass": 8, + "bash_pass_pct": 88.9, + "bashkit_pass_of_bash": 7, + "pass_rate": 87.5 + }, + "missed": [ + { + "case": 2, + "name": "Process sub from shell to stdin", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 4 + ] + }, + "smoke": { + "summary": { + "cases": 18, + "bashkit_pass": 17, + "bashkit_pass_pct": 94.4, + "bash_pass": 18, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 17, + "pass_rate": 94.4 + }, + "missed": [ + { + "case": 11, + "name": "Here doc with redirect", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "tilde": { + "summary": { + "cases": 14, + "bashkit_pass": 13, + "bashkit_pass_pct": 92.9, + "bash_pass": 14, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 13, + "pass_rate": 92.9 + }, + "missed": [ + { + "case": 11, + "name": "x=${undef-~:~}", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "toysh": { + "summary": { + "cases": 8, + "bashkit_pass": 7, + "bashkit_pass_pct": 87.5, + "bash_pass": 8, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 87.5 + }, + "missed": [ + { + "case": 3, + "name": "char class / extglob", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "toysh-posix": { + "summary": { + "cases": 23, + "bashkit_pass": 22, + "bashkit_pass_pct": 95.7, + "bash_pass": 23, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 22, + "pass_rate": 95.7 + }, + "missed": [ + { + "case": 13, + "name": "for loop parsing - http://landley.net/notes.html#04-03-2020", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "unicode": { + "summary": { + "cases": 7, + "bashkit_pass": 1, + "bashkit_pass_pct": 14.3, + "bash_pass": 2, + "bash_pass_pct": 28.6, + "bashkit_pass_of_bash": 1, + "pass_rate": 50.0 + }, + "missed": [ + { + "case": 0, + "name": "OSH source code doesn't have to be valid Unicode (like other shells)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [ + 2, + 3, + 4, + 5, + 6 + ] + }, + "var-op-strip": { + "summary": { + "cases": 29, + "bashkit_pass": 28, + "bashkit_pass_pct": 96.6, + "bash_pass": 29, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 28, + "pass_rate": 96.6 + }, + "missed": [ + { + "case": 26, + "name": "Strip Right Brace (#702)", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "var-sub": { + "summary": { + "cases": 6, + "bashkit_pass": 5, + "bashkit_pass_pct": 83.3, + "bash_pass": 6, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 83.3 + }, + "missed": [ + { + "case": 4, + "name": "Descriptor redirect to bad \"$@\"", + "class": "behavior", + "on": "status" + } + ], + "bash_fails": [] + }, + "vars-bash": { + "summary": { + "cases": 1, + "bashkit_pass": 0, + "bashkit_pass_pct": 0.0, + "bash_pass": 1, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 0, + "pass_rate": 0.0 + }, + "missed": [ + { + "case": 0, + "name": "$SHELL is set to what is in /etc/passwd", + "class": "interactive", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "zsh-idioms": { + "summary": { + "cases": 3, + "bashkit_pass": 2, + "bashkit_pass_pct": 66.7, + "bash_pass": 3, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 66.7 + }, + "missed": [ + { + "case": 2, + "name": "zsh var sub is rejected at runtime", + "class": "behavior", + "on": "stdout" + } + ], + "bash_fails": [] + }, + "arith-dynamic": { + "summary": { + "cases": 4, + "bashkit_pass": 4, + "bashkit_pass_pct": 100.0, + "bash_pass": 4, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "array-compat": { + "summary": { + "cases": 12, + "bashkit_pass": 12, + "bashkit_pass_pct": 100.0, + "bash_pass": 12, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 12, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "builtin-echo": { + "summary": { + "cases": 27, + "bashkit_pass": 27, + "bashkit_pass_pct": 100.0, + "bash_pass": 27, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 27, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "builtin-times": { + "summary": { + "cases": 1, + "bashkit_pass": 1, + "bashkit_pass_pct": 100.0, + "bash_pass": 1, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 1, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "case_": { + "summary": { + "cases": 13, + "bashkit_pass": 13, + "bashkit_pass_pct": 100.0, + "bash_pass": 13, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 13, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "command-parsing": { + "summary": { + "cases": 5, + "bashkit_pass": 5, + "bashkit_pass_pct": 100.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "command-sub": { + "summary": { + "cases": 30, + "bashkit_pass": 28, + "bashkit_pass_pct": 93.3, + "bash_pass": 28, + "bash_pass_pct": 93.3, + "bashkit_pass_of_bash": 28, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [ + 11, + 12 + ] + }, + "comments": { + "summary": { + "cases": 2, + "bashkit_pass": 2, + "bashkit_pass_pct": 100.0, + "bash_pass": 2, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "empty-bodies": { + "summary": { + "cases": 3, + "bashkit_pass": 3, + "bashkit_pass_pct": 100.0, + "bash_pass": 3, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 3, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "explore-parsing": { + "summary": { + "cases": 5, + "bashkit_pass": 5, + "bashkit_pass_pct": 100.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "globstar": { + "summary": { + "cases": 5, + "bashkit_pass": 5, + "bashkit_pass_pct": 100.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "if_": { + "summary": { + "cases": 5, + "bashkit_pass": 5, + "bashkit_pass_pct": 100.0, + "bash_pass": 5, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 5, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "known-differences": { + "summary": { + "cases": 2, + "bashkit_pass": 2, + "bashkit_pass_pct": 100.0, + "bash_pass": 2, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "let": { + "summary": { + "cases": 2, + "bashkit_pass": 2, + "bashkit_pass_pct": 100.0, + "bash_pass": 2, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "loop": { + "summary": { + "cases": 29, + "bashkit_pass": 29, + "bashkit_pass_pct": 100.0, + "bash_pass": 29, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 29, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "nocasematch-match": { + "summary": { + "cases": 6, + "bashkit_pass": 6, + "bashkit_pass_pct": 100.0, + "bash_pass": 6, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 6, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "paren-ambiguity": { + "summary": { + "cases": 9, + "bashkit_pass": 9, + "bashkit_pass_pct": 100.0, + "bash_pass": 9, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 9, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "posix": { + "summary": { + "cases": 15, + "bashkit_pass": 15, + "bashkit_pass_pct": 100.0, + "bash_pass": 15, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 15, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "print-source-code": { + "summary": { + "cases": 4, + "bashkit_pass": 4, + "bashkit_pass_pct": 100.0, + "bash_pass": 4, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "sh-func": { + "summary": { + "cases": 12, + "bashkit_pass": 12, + "bashkit_pass_pct": 100.0, + "bash_pass": 12, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 12, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "strict-options": { + "summary": { + "cases": 17, + "bashkit_pass": 15, + "bashkit_pass_pct": 88.2, + "bash_pass": 15, + "bash_pass_pct": 88.2, + "bashkit_pass_of_bash": 15, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [ + 3, + 5 + ] + }, + "subshell": { + "summary": { + "cases": 2, + "bashkit_pass": 2, + "bashkit_pass_pct": 100.0, + "bash_pass": 2, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 2, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "temp-binding": { + "summary": { + "cases": 4, + "bashkit_pass": 4, + "bashkit_pass_pct": 100.0, + "bash_pass": 4, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 4, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "type-compat": { + "summary": { + "cases": 7, + "bashkit_pass": 5, + "bashkit_pass_pct": 71.4, + "bash_pass": 5, + "bash_pass_pct": 71.4, + "bashkit_pass_of_bash": 5, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [ + 3, + 5 + ] + }, + "var-num": { + "summary": { + "cases": 7, + "bashkit_pass": 7, + "bashkit_pass_pct": 100.0, + "bash_pass": 7, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 7, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "whitespace": { + "summary": { + "cases": 5, + "bashkit_pass": 0, + "bashkit_pass_pct": 0.0, + "bash_pass": 0, + "bash_pass_pct": 0.0, + "bashkit_pass_of_bash": 0, + "pass_rate": 0.0 + }, + "missed": [], + "bash_fails": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + "word-eval": { + "summary": { + "cases": 8, + "bashkit_pass": 8, + "bashkit_pass_pct": 100.0, + "bash_pass": 8, + "bash_pass_pct": 100.0, + "bashkit_pass_of_bash": 8, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [] + }, + "word-split": { + "summary": { + "cases": 55, + "bashkit_pass": 53, + "bashkit_pass_pct": 96.4, + "bash_pass": 53, + "bash_pass_pct": 96.4, + "bashkit_pass_of_bash": 53, + "pass_rate": 100.0 + }, + "missed": [], + "bash_fails": [ + 24, + 25 + ] + } + } +} diff --git a/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.md b/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.md new file mode 100644 index 000000000..e32e533e8 --- /dev/null +++ b/scripts/oils-spec/results/oils-spec-linux-x86_64-20261008T132606Z.md @@ -0,0 +1,167 @@ +# Oils spec pass rate + +- Oils revision: [`57d3f0d088c3`](https://github.com/oils-for-unix/oils/tree/57d3f0d088c36340bc4b3038208e58ea02acf15c) +- bashkit commit: `5f04b4810ec2` +- bash: 5.2.21(1)-release +- Spec files: 132 (bash column; 91 without one skipped) +- Runtime: 34.8 s, 4 jobs, 10 s per-case timeout + +## Headline + +**78.8%** — bashkit passes 2120 of the 2692 cases real bash passes. + +| Shell | Pass | Cases | % | +|-------|-----:|------:|--:| +| bash | 2692 | 2759 | 97.6 | +| bashkit | 2127 | 2759 | 77.1 | + +## bashkit failures by class + +Heuristic keyword classes; every case still counts in the rate. + +| Class | Failing cases | +|-------|--------------:| +| behavior | 410 | +| interactive | 110 | +| host | 24 | +| processes | 18 | +| signals | 10 | + +## Per spec file + +Sorted by cases bashkit misses among those bash passes. + +| Spec file | Cases | bash | bashkit | Missed | Rate % | +|-----------|------:|-----:|--------:|-------:|-------:| +| builtin-completion | 51 | 51 | 17 | 34 | 33.3 | +| array | 78 | 78 | 53 | 25 | 67.9 | +| prompt | 33 | 29 | 4 | 25 | 13.8 | +| builtin-trap-bash | 23 | 23 | 4 | 19 | 17.4 | +| var-ref | 31 | 30 | 15 | 15 | 50.0 | +| builtin-fc | 14 | 14 | 2 | 12 | 14.3 | +| builtin-history | 17 | 17 | 5 | 12 | 29.4 | +| builtin-type-bash | 31 | 31 | 19 | 12 | 61.3 | +| interactive | 18 | 18 | 7 | 11 | 38.9 | +| sh-options | 39 | 39 | 28 | 11 | 71.8 | +| vars-special | 42 | 40 | 29 | 11 | 72.5 | +| array-assign | 11 | 11 | 1 | 10 | 9.1 | +| array-sparse | 40 | 40 | 30 | 10 | 75.0 | +| builtin-bind | 9 | 9 | 0 | 9 | 0.0 | +| builtin-read | 64 | 64 | 55 | 9 | 85.9 | +| builtin-trap | 33 | 33 | 24 | 9 | 72.7 | +| introspect | 13 | 12 | 3 | 9 | 25.0 | +| nul-bytes | 16 | 16 | 7 | 9 | 43.8 | +| redirect-multi | 13 | 13 | 4 | 9 | 30.8 | +| ysh-builtin-private | 23 | 22 | 13 | 9 | 59.1 | +| arith | 74 | 74 | 66 | 8 | 89.2 | +| array-assoc | 42 | 36 | 28 | 8 | 77.8 | +| assign-extended | 39 | 35 | 27 | 8 | 77.1 | +| bugs | 29 | 29 | 21 | 8 | 72.4 | +| var-op-test | 37 | 37 | 29 | 8 | 78.4 | +| alias | 48 | 48 | 41 | 7 | 85.4 | +| array-literal | 19 | 19 | 12 | 7 | 63.2 | +| background | 27 | 27 | 20 | 7 | 74.1 | +| ble-idioms | 26 | 26 | 19 | 7 | 73.1 | +| builtin-bracket | 52 | 50 | 43 | 7 | 86.0 | +| command_ | 16 | 16 | 9 | 7 | 56.2 | +| redirect | 41 | 41 | 34 | 7 | 82.9 | +| sh-usage | 24 | 24 | 17 | 7 | 70.8 | +| builtin-process | 26 | 24 | 19 | 6 | 75.0 | +| builtin-vars | 41 | 39 | 33 | 6 | 84.6 | +| here-doc | 36 | 36 | 30 | 6 | 83.3 | +| sh-options-bash | 9 | 9 | 3 | 6 | 33.3 | +| xtrace | 19 | 19 | 13 | 6 | 68.4 | +| arith-context | 16 | 16 | 11 | 5 | 68.8 | +| assign | 48 | 45 | 40 | 5 | 88.9 | +| blog1 | 9 | 9 | 4 | 5 | 44.4 | +| builtin-cd | 30 | 28 | 23 | 5 | 82.1 | +| builtin-eval-source | 23 | 21 | 16 | 5 | 76.2 | +| builtin-meta | 18 | 18 | 13 | 5 | 72.2 | +| builtin-meta-assign | 11 | 11 | 6 | 5 | 54.5 | +| builtin-printf | 63 | 58 | 56 | 5 | 91.4 | +| nameref | 32 | 32 | 27 | 5 | 84.4 | +| ble-features | 9 | 9 | 5 | 4 | 55.6 | +| ble-unset | 5 | 5 | 1 | 4 | 20.0 | +| brace-expansion | 55 | 55 | 51 | 4 | 92.7 | +| builtin-dirs | 18 | 18 | 14 | 4 | 77.8 | +| builtin-kill | 20 | 19 | 16 | 4 | 78.9 | +| builtin-special | 12 | 12 | 8 | 4 | 66.7 | +| parse-errors | 27 | 25 | 21 | 4 | 84.0 | +| regex | 37 | 37 | 33 | 4 | 89.2 | +| var-sub-quote | 41 | 41 | 37 | 4 | 90.2 | +| assign-dialects | 4 | 4 | 1 | 3 | 25.0 | +| blog2 | 8 | 8 | 5 | 3 | 62.5 | +| bool-parse | 8 | 8 | 5 | 3 | 62.5 | +| builtin-bash | 13 | 13 | 10 | 3 | 76.9 | +| builtin-misc | 7 | 7 | 4 | 3 | 57.1 | +| builtin-set | 24 | 24 | 21 | 3 | 87.5 | +| builtin-trap-err | 23 | 22 | 19 | 3 | 86.4 | +| builtin-umask | 24 | 24 | 21 | 3 | 87.5 | +| extglob-files | 23 | 23 | 20 | 3 | 87.0 | +| fatal-errors | 5 | 5 | 2 | 3 | 40.0 | +| func-parsing | 15 | 15 | 12 | 3 | 80.0 | +| glob | 39 | 37 | 34 | 3 | 91.9 | +| nix-idioms | 6 | 6 | 3 | 3 | 50.0 | +| quote | 35 | 35 | 32 | 3 | 91.4 | +| redirect-command | 23 | 23 | 20 | 3 | 87.0 | +| serialize | 10 | 10 | 7 | 3 | 70.0 | +| var-op-bash | 27 | 26 | 23 | 3 | 88.5 | +| var-op-len | 9 | 8 | 6 | 3 | 62.5 | +| arg-parse | 3 | 3 | 1 | 2 | 33.3 | +| array-basic | 5 | 5 | 3 | 2 | 60.0 | +| assign-deferred | 9 | 9 | 7 | 2 | 77.8 | +| builtin-getopts | 31 | 31 | 29 | 2 | 93.5 | +| builtin-type | 6 | 6 | 4 | 2 | 66.7 | +| divergence | 4 | 4 | 2 | 2 | 50.0 | +| errexit | 35 | 35 | 33 | 2 | 94.3 | +| exit-status | 11 | 11 | 9 | 2 | 81.8 | +| glob-bash | 8 | 8 | 6 | 2 | 75.0 | +| var-op-patsub | 28 | 28 | 26 | 2 | 92.9 | +| var-op-slice | 22 | 22 | 20 | 2 | 90.9 | +| append | 20 | 20 | 19 | 1 | 95.0 | +| dbracket | 49 | 49 | 48 | 1 | 98.0 | +| dparen | 15 | 14 | 13 | 1 | 92.9 | +| errexit-osh | 35 | 35 | 34 | 1 | 97.1 | +| extglob-match | 29 | 29 | 28 | 1 | 96.6 | +| for-expr | 9 | 8 | 7 | 1 | 87.5 | +| globignore | 18 | 18 | 17 | 1 | 94.4 | +| history-expand | 1 | 1 | 0 | 1 | 0.0 | +| pipeline | 26 | 25 | 24 | 1 | 96.0 | +| process-sub | 9 | 8 | 8 | 1 | 87.5 | +| smoke | 18 | 18 | 17 | 1 | 94.4 | +| tilde | 14 | 14 | 13 | 1 | 92.9 | +| toysh | 8 | 8 | 7 | 1 | 87.5 | +| toysh-posix | 23 | 23 | 22 | 1 | 95.7 | +| unicode | 7 | 2 | 1 | 1 | 50.0 | +| var-op-strip | 29 | 29 | 28 | 1 | 96.6 | +| var-sub | 6 | 6 | 5 | 1 | 83.3 | +| vars-bash | 1 | 1 | 0 | 1 | 0.0 | +| zsh-idioms | 3 | 3 | 2 | 1 | 66.7 | +| arith-dynamic | 4 | 4 | 4 | 0 | 100.0 | +| array-compat | 12 | 12 | 12 | 0 | 100.0 | +| builtin-echo | 27 | 27 | 27 | 0 | 100.0 | +| builtin-times | 1 | 1 | 1 | 0 | 100.0 | +| case_ | 13 | 13 | 13 | 0 | 100.0 | +| command-parsing | 5 | 5 | 5 | 0 | 100.0 | +| command-sub | 30 | 28 | 28 | 0 | 100.0 | +| comments | 2 | 2 | 2 | 0 | 100.0 | +| empty-bodies | 3 | 3 | 3 | 0 | 100.0 | +| explore-parsing | 5 | 5 | 5 | 0 | 100.0 | +| globstar | 5 | 5 | 5 | 0 | 100.0 | +| if_ | 5 | 5 | 5 | 0 | 100.0 | +| known-differences | 2 | 2 | 2 | 0 | 100.0 | +| let | 2 | 2 | 2 | 0 | 100.0 | +| loop | 29 | 29 | 29 | 0 | 100.0 | +| nocasematch-match | 6 | 6 | 6 | 0 | 100.0 | +| paren-ambiguity | 9 | 9 | 9 | 0 | 100.0 | +| posix | 15 | 15 | 15 | 0 | 100.0 | +| print-source-code | 4 | 4 | 4 | 0 | 100.0 | +| sh-func | 12 | 12 | 12 | 0 | 100.0 | +| strict-options | 17 | 15 | 15 | 0 | 100.0 | +| subshell | 2 | 2 | 2 | 0 | 100.0 | +| temp-binding | 4 | 4 | 4 | 0 | 100.0 | +| type-compat | 7 | 5 | 5 | 0 | 100.0 | +| var-num | 7 | 7 | 7 | 0 | 100.0 | +| whitespace | 5 | 0 | 0 | 0 | 0.0 | +| word-eval | 8 | 8 | 8 | 0 | 100.0 | +| word-split | 55 | 53 | 53 | 0 | 100.0 |