Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion crates/bashkit/src/builtins/introspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl Builtin for Type {
}

let mut output = String::new();
let mut errors = String::new();
let mut all_found = true;

for name in &names {
Expand Down Expand Up @@ -105,6 +106,10 @@ impl Builtin for Type {
let mut found_any = false;
if is_func {
output.push_str(&format!("{} is a function\n", name));
if let Some(text) = shell.function_text(name) {
output.push_str(&text);
output.push('\n');
}
found_any = true;
}
if is_kw && (show_all || !found_any) {
Expand All @@ -127,7 +132,7 @@ impl Builtin for Type {
found_any = true;
}
if !found_any {
output.push_str(&format!("bash: type: {}: not found\n", name));
errors.push_str(&format!("bash: type: {}: not found\n", name));
all_found = false;
}
}
Expand All @@ -136,6 +141,7 @@ impl Builtin for Type {
let exit_code = if all_found { 0 } else { 1 };
Ok(ExecResult {
stdout: output.into(),
stderr: errors.into(),
exit_code,
..Default::default()
})
Expand Down
37 changes: 37 additions & 0 deletions crates/bashkit/src/interpreter/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,34 @@ impl Interpreter {
/// Try to parse a binary operator at the current precedence level.
/// Scans `chars`/`bo` for operators, splitting and recursing.
/// Returns `Some(value)` if an operator was found, `None` to try next level.
/// Whether the `+`/`-` at `i` is a sign rather than a binary operator:
/// nothing but another operator precedes it (`3 - -1`, `2 * -x`). A
/// postfix `x++ - 1` still leaves the `-` binary.
fn arith_sign_is_unary(chars: &[char], i: usize) -> bool {
let operand_end = |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | ')' | ']');
let mut j = i;
while j > 0 && chars[j - 1].is_ascii_whitespace() {
j -= 1;
}
if j == 0 {
return true;
}
let p = chars[j - 1];
if operand_end(p) {
return false;
}
if matches!(p, '+' | '-') && j >= 2 && chars[j - 2] == p {
let mut k = j - 2;
while k > 0 && chars[k - 1].is_ascii_whitespace() {
k -= 1;
}
if k > 0 && operand_end(chars[k - 1]) {
return false;
}
}
true
}

pub(super) fn try_parse_arith_addmul(
&self,
expr: &str,
Expand Down Expand Up @@ -708,6 +736,10 @@ impl Interpreter {
if chars[i] == '-' && i > 0 && chars[i - 1] == '-' {
continue;
}
// `3 - -1`: a sign after another operator is unary.
if Self::arith_sign_is_unary(chars, i) {
continue;
}
let left = self.parse_arithmetic_impl(&expr[..bo[i]], arith_depth + 1);
let right = self.parse_arithmetic_impl(&expr[bo[i] + 1..], arith_depth + 1);
return Some(if chars[i] == '+' {
Expand Down Expand Up @@ -759,6 +791,11 @@ impl Interpreter {
'*' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '*' => {
let left = self.parse_arithmetic_impl(&expr[..bo[i]], arith_depth + 1);
let right = self.parse_arithmetic_impl(&expr[bo[i] + 2..], arith_depth + 1);
// Negative exponent: bash errors; this evaluator's
// error value is 0 (TM-DOS-029), not `x ** 0`.
if right < 0 {
return Some(0);
}
let exp = right.clamp(0, 63) as u32;
return Some(left.wrapping_pow(exp));
}
Expand Down
1 change: 1 addition & 0 deletions crates/bashkit/src/interpreter/brace_expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ impl Interpreter {
quoted: word.quoted,
has_unquoted_glob: word.has_unquoted_glob,
part_quoted,
raw: None,
});
}
Some(words)
Expand Down
82 changes: 79 additions & 3 deletions crates/bashkit/src/interpreter/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,20 @@ impl Interpreter {
self.nounset_error = Some(format!("bash: {}: unbound variable\n", name));
}

if operand.contains("$(") {
// Boxed: keeps this frame small for deep `$(...)` nesting
// (TM-DOS-089).
Box::pin(self.prefetch_operand_substs(
operator,
operand,
*colon_variant,
is_set,
&value,
))
.await?;
} else {
self.operand_substs.clear();
}
// Delegate to sync helper to avoid bloating the async state
// machine with Vec<String> locals (causes stack overflow at
// depth 32 in debug builds — see stack_overflow_regression_tests).
Expand Down Expand Up @@ -717,6 +731,7 @@ impl Interpreter {
quoted: part_is_quoted,
has_unquoted_glob: false,
part_quoted: vec![part_is_quoted],
raw: None,
};
self.expand_word(&single).await?
};
Expand Down Expand Up @@ -1108,6 +1123,52 @@ impl Interpreter {

/// Expand an operand string from a parameter expansion (sync, lazy).
/// Only called when the operand is actually needed, providing lazy evaluation.
/// `${x:-$(cmd)}`: run the operand's command substitutions before the
/// sync operand expansion, only when the operator uses the operand (so
/// an unused default never runs). Outputs queue in `operand_substs`.
async fn prefetch_operand_substs(
&mut self,
operator: &ParameterOp,
operand: &str,
colon_variant: bool,
is_set: bool,
value: &str,
) -> Result<()> {
self.operand_substs.clear();
if !operand.contains("$(") {
return Ok(());
}
let unset_or_null = !is_set || (colon_variant && value.is_empty());
let uses_operand = match operator {
ParameterOp::UseDefault | ParameterOp::AssignDefault | ParameterOp::Error => {
unset_or_null
}
ParameterOp::UseReplacement => !unset_or_null,
_ => false,
};
if !uses_operand {
return Ok(());
}
let (word, _, _) = Self::parse_marked_operand(
operand,
self.limits.max_ast_depth,
self.limits.max_parser_operations,
);
for part in &word.parts {
if let WordPart::CommandSubstitution(commands) = part {
// THREAT[TM-DOS-088]: same depth accounting as word `$(...)`.
if self.counters.push_subst(&self.limits).is_err() {
return Err(crate::error::Error::Execution(
"maximum command substitution depth exceeded".to_string(),
));
}
let out = self.execute_cmd_subst(commands).await?;
self.operand_substs.push_back(out);
}
}
Ok(())
}

pub(super) fn expand_operand(&mut self, operand: &str) -> String {
if operand.is_empty() {
return String::new();
Expand Down Expand Up @@ -1165,7 +1226,14 @@ impl Interpreter {
let value = self.expand_variable(name).len().to_string();
Self::push_operand_expansion(&mut result, &value, in_marked || force_quoted);
}
// TODO: handle CommandSubstitution etc. in sync operand expansion
// 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);
}
}
// TODO: process substitution in sync operand expansion
_ => {}
}
}
Expand Down Expand Up @@ -1594,7 +1662,7 @@ impl Interpreter {
replacement,
} => {
// ${var/pattern/replacement} - replace first occurrence
let expanded_rep = self.expand_operand(replacement);
let expanded_rep = self.expand_replacement_operand(replacement);
let expanded_pat = self.expand_replace_pattern(pattern);
self.replace_pattern(value, &expanded_pat, &expanded_rep, false)
}
Expand All @@ -1603,7 +1671,7 @@ impl Interpreter {
replacement,
} => {
// ${var//pattern/replacement} - replace all occurrences
let expanded_rep = self.expand_operand(replacement);
let expanded_rep = self.expand_replacement_operand(replacement);
let expanded_pat = self.expand_replace_pattern(pattern);
self.replace_pattern(value, &expanded_pat, &expanded_rep, true)
}
Expand Down Expand Up @@ -1659,6 +1727,13 @@ impl Interpreter {
self.expand_operand(&Self::single_quotes_as_quoted(operand))
}

/// Expand the replacement of `${x/pattern/rep}`. Like the pattern,
/// bash quote-removes `'...'` there even inside double quotes
/// (`"${y/b/'}'}"` is `a}`).
pub(super) fn expand_replacement_operand(&mut self, operand: &str) -> String {
self.expand_operand(&Self::single_quotes_as_quoted(operand))
}

/// Expand the pattern of `${x/pattern/rep}`. A leading `#`/`%` in the
/// source is the anchor; a `#`/`%` that comes from expansion is literal.
pub(super) fn expand_replace_pattern(&mut self, pattern: &str) -> String {
Expand Down Expand Up @@ -2216,6 +2291,7 @@ mod expansion_charge_tests {
quoted,
has_unquoted_glob,
part_quoted: Vec::new(),
raw: None,
}
}

Expand Down
Loading
Loading