use super::heredoc::strip_for_analysis;
use super::lexer::{Expansion, lex};
use super::{ShellError, effective_allowlist, extract_all_commands, extract_base_from_segment};
pub(crate) fn check_substitution_in_args(command: &str) -> Result<(), ShellError> {
if !has_expanding_substitution_in_args(command) {
return Ok(());
}
let mut inner_cmds = extract_substitution_commands(command)?;
if let Ok(rewritten) = super::rewrite_case_constructs(command)
&& rewritten != command
{
inner_cmds
.retain(|inner| !matches!(extract_base_from_segment(inner).as_str(), "case" | "esac"));
inner_cmds.extend(extract_substitution_commands(&rewritten)?);
}
if inner_cmds.is_empty() {
return Ok(());
}
for inner in &inner_cmds {
super::enforcement::check_unconditional_blocked_only(inner)?;
}
let allowlist = effective_allowlist();
if allowlist.is_empty() {
return Ok(());
}
let scoping = crate::core::config::shell_policy::config().shell_allowlist_subcommand_scoping;
let allowlist = super::enforcement::Allowlist::new(allowlist, scoping);
for inner in &inner_cmds {
super::enforcement::check_all_segments(inner, &allowlist).map_err(|err| {
ShellError::from(format!(
"[BLOCKED — DO NOT RETRY] Command substitution runs a command that is gated \
like one at the top level: {}\n\
Command: {command}",
err.message()
.trim_start_matches("[BLOCKED — DO NOT RETRY] ")
))
})?;
}
Ok(())
}
fn extract_substitution_commands(command: &str) -> Result<Vec<String>, ShellError> {
let mut results = Vec::new();
for Expansion { body, arithmetic } in lex(command).expansions {
let bodies = if arithmetic {
super::compound::arithmetic_substitutions(body, command)?
} else {
vec![body]
};
for body in bodies {
let trimmed = body.trim();
if trimmed.is_empty() {
continue;
}
results.extend(extract_all_commands(&strip_for_analysis(
trimmed, false, true,
)));
results.extend(extract_substitution_commands(trimmed)?);
}
}
Ok(results)
}
pub(crate) fn has_expanding_substitution_in_args(command: &str) -> bool {
!lex(command).expansions.is_empty()
}