use crate::{callstack, env, error, functions, trace_categories};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
pub fn in_sourced_script(&self) -> bool {
self.call_stack.in_sourced_script()
}
pub fn in_function(&self) -> bool {
self.call_stack.in_function()
}
pub fn start_interactive_session(&mut self) -> Result<(), error::Error> {
self.call_stack.push_interactive_session();
Ok(())
}
pub fn end_interactive_session(&mut self) -> Result<(), error::Error> {
if self
.call_stack
.current_frame()
.is_none_or(|frame| !frame.frame_type.is_interactive_session())
{
return Err(error::ErrorKind::NotInInteractiveSession.into());
}
self.call_stack.pop();
Ok(())
}
pub fn start_command_string_mode(&mut self) {
self.call_stack.push_command_string();
}
pub fn end_command_string_mode(&mut self) -> Result<(), error::Error> {
if self
.call_stack
.current_frame()
.is_none_or(|frame| !frame.frame_type.is_command_string())
{
return Err(error::ErrorKind::NotExecutingCommandString.into());
}
self.call_stack.pop();
Ok(())
}
pub(crate) fn enter_trap_handler(
&mut self,
signal: crate::traps::TrapSignal,
handler: Option<&crate::traps::TrapHandler>,
) -> ShellGuard<'_, SE> {
let last_exit_status = self.last_exit_status;
self.call_stack.push_trap_handler(signal, handler);
let frames = self.call_stack.depth();
ShellGuard::new(
self,
Leave::TrapHandler {
frames,
last_exit_status,
},
)
}
pub(crate) fn enter_script(
&mut self,
call_type: callstack::ScriptCallType,
source_info: &crate::SourceInfo,
args: impl IntoIterator<Item = String>,
) -> ShellGuard<'_, SE> {
self.call_stack.push_script(call_type, source_info, args);
let frames = self.call_stack.depth();
ShellGuard::new(self, Leave::Script { frames })
}
pub(crate) fn enter_function(
&mut self,
name: &str,
function: &functions::Registration,
args: impl IntoIterator<Item = String>,
) -> Result<ShellGuard<'_, SE>, error::Error> {
if let Some(max_call_depth) = self.options.max_function_call_depth
&& self.call_stack.function_call_depth() >= max_call_depth
{
return Err(error::ErrorKind::MaxFunctionCallDepthExceeded.into());
}
if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
let depth = self.call_stack.function_call_depth();
let prefix = repeated_char_str(' ', depth);
tracing::debug!(target: trace_categories::FUNCTIONS, "Entering func [depth={depth}]: {prefix}{name}");
}
self.call_stack.push_function(name, function, args);
self.env.push_scope(env::EnvironmentScope::Local);
let leave = Leave::Function {
frames: self.call_stack.depth(),
scopes: self.env.scope_depth(),
};
Ok(ShellGuard::new(self, leave))
}
pub(crate) fn enter_scope(&mut self, scope: env::EnvironmentScope) -> ShellGuard<'_, SE> {
self.env.push_scope(scope);
let scopes = self.env.scope_depth();
ShellGuard::new(self, Leave::Scope { scopes, scope })
}
pub(crate) const fn block_trap_delivery(&mut self) -> ShellGuard<'_, SE> {
self.call_stack.acquire_trap_delivery_block();
ShellGuard::new(self, Leave::TrapBlock)
}
pub fn current_shell_args(&self) -> &[String] {
for frame in self.call_stack.iter() {
match frame.frame_type {
crate::callstack::FrameType::Function(..) => return &frame.args,
_ if frame.frame_type.is_run_script() => return &frame.args,
_ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
return &frame.args;
}
_ => (),
}
}
self.args.as_slice()
}
pub fn current_shell_args_mut(&mut self) -> &mut Vec<String> {
for frame in self.call_stack.iter_mut() {
match frame.frame_type {
crate::callstack::FrameType::Function(..) => return &mut frame.args,
_ if frame.frame_type.is_run_script() => return &mut frame.args,
_ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
return &mut frame.args;
}
_ => (),
}
}
&mut self.args
}
}
fn repeated_char_str(c: char, count: usize) -> String {
(0..count).map(|_| c).collect()
}
pub(crate) struct ShellGuard<'a, SE: crate::extensions::ShellExtensions> {
shell: &'a mut crate::Shell<SE>,
leave: Leave,
}
#[derive(Clone, Copy, Debug)]
enum Leave {
Function {
frames: usize,
scopes: usize,
},
Script {
frames: usize,
},
TrapHandler {
frames: usize,
last_exit_status: u8,
},
Scope {
scopes: usize,
scope: env::EnvironmentScope,
},
TrapBlock,
}
impl<'a, SE: crate::extensions::ShellExtensions> ShellGuard<'a, SE> {
const fn new(shell: &'a mut crate::Shell<SE>, leave: Leave) -> Self {
Self { shell, leave }
}
}
impl<SE: crate::extensions::ShellExtensions> std::ops::Deref for ShellGuard<'_, SE> {
type Target = crate::Shell<SE>;
fn deref(&self) -> &Self::Target {
self.shell
}
}
impl<SE: crate::extensions::ShellExtensions> std::ops::DerefMut for ShellGuard<'_, SE> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.shell
}
}
impl<SE: crate::extensions::ShellExtensions> ShellGuard<'_, SE> {
fn frame_on_top(&self, depth: usize, is_expected: fn(&callstack::FrameType) -> bool) -> bool {
let stack = &self.shell.call_stack;
stack.depth() == depth
&& stack
.current_frame()
.is_some_and(|frame| is_expected(&frame.frame_type))
}
fn pop_frame(&mut self, depth: usize, is_expected: fn(&callstack::FrameType) -> bool) -> bool {
let on_top = self.frame_on_top(depth, is_expected);
if on_top {
self.shell.call_stack.pop();
}
on_top
}
fn pop_scope(&mut self, depth: usize, scope_type: env::EnvironmentScope) -> bool {
self.shell.env.scope_depth() == depth && self.shell.env.pop_scope(scope_type).is_ok()
}
fn report_imbalance(&self) {
tracing::error!(
"shell state out of balance when leaving {:?}: call stack {} deep, {} variable scopes",
self.leave,
self.shell.call_stack.depth(),
self.shell.env.scope_depth()
);
}
}
impl<SE: crate::extensions::ShellExtensions> Drop for ShellGuard<'_, SE> {
fn drop(&mut self) {
let left = match self.leave {
Leave::Function { frames, scopes } => {
let left = self.frame_on_top(frames, callstack::FrameType::is_function)
&& self.pop_scope(scopes, env::EnvironmentScope::Local);
if left
&& let Some(frame) = self.shell.call_stack.pop()
&& let callstack::FrameType::Function(call) = frame.frame_type
&& tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG)
{
let depth = self.shell.call_stack.function_call_depth();
let prefix = repeated_char_str(' ', depth);
tracing::debug!(target: trace_categories::FUNCTIONS, "Exiting func [depth={depth}]: {prefix}{}", call.function_name);
}
left
}
Leave::Script { frames } => self.pop_frame(frames, callstack::FrameType::is_script),
Leave::TrapHandler {
frames,
last_exit_status,
} => {
self.shell.last_exit_status = last_exit_status;
self.pop_frame(frames, callstack::FrameType::is_trap_handler)
}
Leave::Scope { scopes, scope } => self.pop_scope(scopes, scope),
Leave::TrapBlock => {
let blocked = self.shell.call_stack.is_trap_delivery_suppressed();
self.shell.call_stack.release_trap_delivery_block();
blocked
}
};
if !left {
self.report_imbalance();
}
}
}
#[cfg(test)]
#[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")]
mod tests {
use super::*;
#[tokio::test]
async fn guards_pop_nothing_when_out_of_balance() -> Result<(), error::Error> {
let mut shell = crate::Shell::builder()
.profile(crate::ProfileLoadBehavior::Skip)
.rc(crate::RcLoadBehavior::Skip)
.build()
.await?;
let params = shell.default_exec_params();
shell
.run_string("f() { :; }", &crate::SourceInfo::default(), ¶ms)
.await?;
let f = shell
.funcs()
.get("f")
.cloned()
.ok_or_else(|| error::ErrorKind::FunctionNotFound("f".into()))?;
let frames = shell.call_stack.depth();
let scopes = shell.env.scope_depth();
let mut function = shell.enter_function("f", &f, std::iter::empty())?;
function.env_mut().push_scope(env::EnvironmentScope::Local);
drop(function);
assert_eq!(shell.call_stack.depth(), frames + 1);
assert_eq!(shell.env.scope_depth(), scopes + 2);
let mut script = shell.enter_script(
callstack::ScriptCallType::Source,
&crate::SourceInfo::default(),
std::iter::empty(),
);
script.call_stack.pop();
script.start_command_string_mode();
drop(script);
assert_eq!(shell.call_stack.depth(), frames + 2);
Ok(())
}
}