use std::{ cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt, }; use ariadne::Fmt; use crate::{ builtin::{ alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset} }, expand::{expand_aliases, expand_case_pattern, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, libsh::{ error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, guards::{scope_guard, var_ctx_guard}, utils::RedirVecUtils, }, prelude::*, procio::{IoMode, IoStack, PipeGenerator}, state::{ self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, }, }; use super::{ AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType, lex::{KEYWORDS, Span, Tk, TkFlags}, }; thread_local! { static RECURSE_DEPTH: Cell = const { Cell::new(0) }; } pub fn is_in_path(name: &str) -> bool { if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') { let path = Path::new(name); if path.exists() && path.is_file() && !path.is_dir() { let meta = match path.metadata() { Ok(m) => m, Err(_) => return false, }; if meta.permissions().mode() & 0o111 != 0 { return true; } } false } else { let Ok(path) = env::var("PATH") else { return false; }; let paths = path.split(':'); for path in paths { let full_path = Path::new(path).join(name); if full_path.exists() && full_path.is_file() && !full_path.is_dir() { let meta = match full_path.metadata() { Ok(m) => m, Err(_) => continue, }; if meta.permissions().mode() & 0o111 != 0 { return true; } } } false } } pub enum AssignBehavior { Export, Set, } /// Arguments to the execvpe function pub struct ExecArgs { pub cmd: (CString, Span), pub argv: Vec, pub envp: Vec, } impl ExecArgs { pub fn new(argv: Vec) -> ShResult { assert!(!argv.is_empty()); let argv = prepare_argv(argv)?; Self::from_expanded(argv) } pub fn from_expanded(argv: Vec<(String, Span)>) -> ShResult { assert!(!argv.is_empty()); let cmd = Self::get_cmd(&argv); let argv = Self::get_argv(argv); let envp = Self::get_envp(); Ok(Self { cmd, argv, envp }) } pub fn get_cmd(argv: &[(String, Span)]) -> (CString, Span) { let cmd = argv[0].0.as_str(); let span = argv[0].1.clone(); (CString::new(cmd).unwrap(), span) } pub fn get_argv(argv: Vec<(String, Span)>) -> Vec { argv .into_iter() .map(|s| CString::new(s.0).unwrap()) .collect() } pub fn get_envp() -> Vec { std::env::vars() .map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap()) .collect() } } /// Execute a `-c` command string, optimizing single simple commands to exec /// directly without forking. This avoids process group issues where grandchild /// processes (e.g. nvim spawning opencode) lose their controlling terminal. pub fn exec_dash_c(input: String) -> ShResult<()> { let log_tab = read_logic(|l| l.clone()); let expanded = expand_aliases(input, HashSet::new(), &log_tab); let source_name = "".to_string(); let mut parser = ParsedSrc::new(Arc::new(expanded)) .with_lex_flags(super::lex::LexFlags::empty()) .with_name(source_name.clone()); if let Err(errors) = parser.parse_src() { for error in errors { error.print_error(); } return Ok(()); } let mut nodes = parser.extract_nodes(); // Single simple command: exec directly without forking. // The parser wraps single commands as Conjunction → Pipeline → Command. // Unwrap all layers to check, then set NO_FORK on the inner Command. if nodes.len() == 1 { let is_single_cmd = match &nodes[0].class { NdRule::Command { .. } => true, NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), NdRule::Conjunction { elements } => { elements.len() == 1 && match &elements[0].cmd.class { NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }), NdRule::Command { .. } => true, _ => false, } } _ => false, }; if is_single_cmd { // Unwrap to the inner Command node let mut node = nodes.remove(0); loop { match node.class { NdRule::Conjunction { mut elements } => { node = *elements.remove(0).cmd; } NdRule::Pipeline { mut cmds } => { node = cmds.remove(0); } NdRule::Command { .. } => break, _ => break, } } node.flags |= NdFlags::NO_FORK; nodes.push(node); } } let mut dispatcher = Dispatcher::new(nodes, false, source_name); // exec_cmd expects a job on the stack (normally set up by exec_pipeline). // For the NO_FORK exec-in-place path, create one so it doesn't panic. dispatcher.job_stack.new_job(); dispatcher.begin_dispatch() } pub fn exec_input( input: String, io_stack: Option, interactive: bool, source_name: Option, ) -> ShResult<()> { let log_tab = read_logic(|l| l.clone()); let input = expand_aliases(input, HashSet::new(), &log_tab); let lex_flags = if interactive { super::lex::LexFlags::INTERACTIVE } else { super::lex::LexFlags::empty() }; let source_name = source_name.unwrap_or("".into()); let mut parser = ParsedSrc::new(Arc::new(input)) .with_lex_flags(lex_flags) .with_name(source_name.clone()); if let Err(errors) = parser.parse_src() { for error in errors { error.print_error(); } return Ok(()); } let nodes = parser.extract_nodes(); let mut dispatcher = Dispatcher::new(nodes, interactive, source_name.clone()); if let Some(mut stack) = io_stack { dispatcher.io_stack.extend(stack.drain(..)); } let result = dispatcher.begin_dispatch(); if state::get_status() != 0 && let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) { let saved_status = state::get_status(); exec_input(trap, None, false, Some(source_name))?; state::set_status(saved_status); } result } pub struct Dispatcher { nodes: VecDeque, interactive: bool, source_name: String, pub io_stack: IoStack, pub job_stack: JobStack, fg_job: bool, } impl Dispatcher { pub fn new(nodes: Vec, interactive: bool, source_name: String) -> Self { let nodes = VecDeque::from(nodes); Self { nodes, interactive, source_name, io_stack: IoStack::new(), job_stack: JobStack::new(), fg_job: true, } } pub fn begin_dispatch(&mut self) -> ShResult<()> { while let Some(node) = self.nodes.pop_front() { let blame = node.get_span(); self.dispatch_node(node).try_blame(blame)?; } Ok(()) } pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { match node.class { NdRule::Conjunction { .. } => self.exec_conjunction(node)?, NdRule::Pipeline { .. } => self.exec_pipeline(node)?, NdRule::IfNode { .. } => self.exec_if(node)?, NdRule::LoopNode { .. } => self.exec_loop(node)?, NdRule::ForNode { .. } => self.exec_for(node)?, NdRule::CaseNode { .. } => self.exec_case(node)?, NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?, NdRule::FuncDef { .. } => self.exec_func_def(node)?, NdRule::Negate { .. } => self.exec_negated(node)?, NdRule::Command { .. } => self.dispatch_cmd(node)?, NdRule::Test { .. } => self.exec_test(node)?, _ => unreachable!(), } Ok(()) } pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { let Some(cmd) = node.get_command() else { return self.exec_cmd(node); // Argv is empty, probably an assignment }; if is_func(node.get_command().cloned()) { self.exec_func(node) } else if cmd.flags.contains(TkFlags::BUILTIN) { self.exec_builtin(node) } else if is_subsh(node.get_command().cloned()) { self.exec_subsh(node) } else if read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() && !is_in_path(cmd.span.as_str()) { let dir = cmd.span.as_str().to_string(); let stack = IoStack { stack: self.io_stack.clone(), }; exec_input( format!("cd {dir}"), Some(stack), self.interactive, Some(self.source_name.clone()), ) } else { self.exec_cmd(node) } } pub fn exec_negated(&mut self, node: Node) -> ShResult<()> { let NdRule::Negate { cmd } = node.class else { unreachable!() }; self.dispatch_node(*cmd)?; let status = state::get_status(); state::set_status(if status == 0 { 1 } else { 0 }); Ok(()) } pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> { let NdRule::Conjunction { elements } = conjunction.class else { unreachable!() }; let mut elem_iter = elements.into_iter(); while let Some(element) = elem_iter.next() { let ConjunctNode { cmd, operator } = element; self.dispatch_node(*cmd)?; let status = state::get_status(); match operator { ConjunctOp::And => { if status != 0 { break; } } ConjunctOp::Or => { if status == 0 { break; } } ConjunctOp::Null => break, } } Ok(()) } pub fn exec_test(&mut self, node: Node) -> ShResult<()> { let test_result = double_bracket_test(node)?; match test_result { true => state::set_status(0), false => state::set_status(1), } Ok(()) } pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { let blame = func_def.get_span(); let ctx = func_def.context.clone(); let NdRule::FuncDef { name, body } = func_def.class else { unreachable!() }; let body_span = body.get_span(); let body = body_span.as_str().to_string(); let name = name.span.as_str().strip_suffix("()").unwrap(); if KEYWORDS.contains(&name) { return Err(ShErr::at( ShErrKind::SyntaxErr, blame, format!("function: Forbidden function name `{name}`"), )); } let mut func_parser = ParsedSrc::new(Arc::new(body)).with_context(ctx); if let Err(errors) = func_parser.parse_src() { for error in errors { error.print_error(); } return Ok(()); } let func = ShFunc::new(func_parser, blame); write_logic(|l| l.insert_func(name, func)); // Store the AST Ok(()) } fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { let _blame = subsh.get_span().clone(); let NdRule::Command { assignments, argv } = subsh.class else { unreachable!() }; let name = self.source_name.clone(); self.io_stack.append_to_frame(subsh.redirs); let _guard = self.io_stack.pop_frame().redirect()?; self.run_fork("anonymous_subshell", |s| { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { e.print_error(); return; }; let subsh_raw = argv[0].span.as_str(); let subsh_body = subsh_raw[1..subsh_raw.len() - 1].to_string(); // Remove surrounding parentheses if let Err(e) = exec_input(subsh_body, None, s.interactive, Some(name)) { e.print_error(); }; }) } fn exec_func(&mut self, func: Node) -> ShResult<()> { let mut blame = func.get_span().clone(); let func_name = func.get_command().unwrap().to_string(); let func_ctx = func.get_context(format!( "in call to function '{}'", func_name.fg(next_color()) )); let NdRule::Command { assignments, mut argv, } = func.class else { unreachable!() }; let max_depth = read_shopts(|s| s.core.max_recurse_depth); let depth = RECURSE_DEPTH.with(|d| { let cur = d.get(); d.set(cur + 1); cur + 1 }); if depth > max_depth { RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); return Err(ShErr::at( ShErrKind::InternalErr, blame, format!("maximum recursion depth ({max_depth}) exceeded"), )); } let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; let func_name = argv.remove(0).to_string(); let _var_guard = var_ctx_guard(env_vars.into_iter().collect()); self.io_stack.append_to_frame(func.redirs); blame.rename(func_name.clone()); let mut argv = prepare_argv(argv).try_blame(blame.clone())?; argv.insert(0, (func_name.clone(), blame.clone())); let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let _guard = scope_guard(Some(argv)); func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().flags = func.flags; if let Err(e) = self.exec_pipeline(func_body.body().clone()) { match e.kind() { ShErrKind::FuncReturn(code) => { state::set_status(*code); Ok(()) } _ => Err(e), } } else { Ok(()) } } else { Err(ShErr::at( ShErrKind::InternalErr, blame, format!("Failed to find function '{}'", func_name), )) }; RECURSE_DEPTH.with(|d| d.set(d.get() - 1)); result } fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { let NdRule::BraceGrp { body } = brc_grp.class else { unreachable!("expected BraceGrp node, got {:?}", brc_grp.class) }; if self.interactive { log::debug!("Executing brace group, body: {:?}", body); } let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(brc_grp.redirs); let guard = self.io_stack.pop_frame().redirect()?; let brc_grp_logic = |s: &mut Self| -> ShResult<()> { for node in body { let blame = node.get_span(); s.dispatch_node(node).try_blame(blame)?; } Ok(()) }; if fork_builtins { log::trace!("Forking brace group"); self.run_fork("brace group", |s| { if let Err(e) = brc_grp_logic(s) { e.print_error(); } }) } else { brc_grp_logic(self).map_err(|e| e.with_redirs(guard)) } } fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { let blame = case_stmt.get_span().clone(); let NdRule::CaseNode { pattern, case_blocks, } = case_stmt.class else { unreachable!() }; let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(case_stmt.redirs); let guard = self.io_stack.pop_frame().redirect()?; let case_logic = |s: &mut Self| -> ShResult<()> { let exp_pattern = pattern.clone().expand()?; let pattern_raw = exp_pattern .get_words() .first() .map(|s| s.to_string()) .unwrap_or_default(); 'outer: for block in case_blocks { let CaseNode { pattern, body } = block; let block_pattern_raw = pattern .span .as_str() .strip_suffix(')') .unwrap_or(pattern.span.as_str()) .trim(); // Split at '|' to allow for multiple patterns like `foo|bar)` let block_patterns = block_pattern_raw.split('|'); for pattern in block_patterns { let pattern_exp = expand_case_pattern(pattern)?; let pattern_regex = glob_to_regex(&pattern_exp, false); if pattern_regex.is_match(&pattern_raw) { for node in &body { s.dispatch_node(node.clone())?; } break 'outer; } } } Ok(()) }; if fork_builtins { log::trace!("Forking builtin: case"); self.run_fork("case", |s| { if let Err(e) = case_logic(s) { e.print_error(); } }) } else { case_logic(self) .try_blame(blame) .map_err(|e| e.with_redirs(guard)) } } fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { let blame = loop_stmt.get_span().clone(); let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { unreachable!(); }; let keep_going = |kind: LoopKind, status: i32| -> bool { match kind { LoopKind::While => status == 0, LoopKind::Until => status != 0, } }; let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(loop_stmt.redirs); let guard = self.io_stack.pop_frame().redirect()?; let loop_logic = |s: &mut Self| -> ShResult<()> { let CondNode { cond, body } = cond_node; 'outer: loop { if let Err(e) = s.dispatch_node(*cond.clone()) { state::set_status(1); return Err(e); } let status = state::get_status(); if keep_going(kind, status) { for node in &body { if let Err(e) = s.dispatch_node(node.clone()) { match e.kind() { ShErrKind::LoopBreak(code) => { state::set_status(*code); break 'outer; } ShErrKind::LoopContinue(code) => { state::set_status(*code); continue 'outer; } _ => { return Err(e); } } } } } else { state::set_status(0); break; } } Ok(()) }; if fork_builtins { log::trace!("Forking builtin: loop"); self.run_fork("loop", |s| { if let Err(e) = loop_logic(s) { e.print_error(); } }) } else { loop_logic(self) .try_blame(blame) .map_err(|e| e.with_redirs(guard)) } } fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { let blame = for_stmt.get_span().clone(); let NdRule::ForNode { vars, arr, body } = for_stmt.class else { unreachable!(); }; let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS); let to_expanded_strings = |tks: Vec| -> ShResult> { Ok( tks .into_iter() .map(|tk| tk.expand().map(|tk| tk.get_words())) .collect::>>>()? .into_iter() .flatten() .collect::>(), ) }; self.io_stack.append_to_frame(for_stmt.redirs); let guard = self.io_stack.pop_frame().redirect()?; let for_logic = |s: &mut Self| -> ShResult<()> { // Expand all array variables let arr: Vec = to_expanded_strings(arr)?; let vars: Vec = to_expanded_strings(vars)?; let mut for_guard = var_ctx_guard(vars.iter().map(|v| v.to_string()).collect()); 'outer: for chunk in arr.chunks(vars.len()) { let empty = String::new(); let chunk_iter = vars .iter() .zip(chunk.iter().chain(std::iter::repeat(&empty))); for (var, val) in chunk_iter { write_vars(|v| { v.set_var( &var.to_string(), VarKind::Str(val.to_string()), VarFlags::NONE, ) })?; for_guard.insert(var.to_string()); } for node in body.clone() { if let Err(e) = s.dispatch_node(node) { match e.kind() { ShErrKind::LoopBreak(code) => { state::set_status(*code); break 'outer; } ShErrKind::LoopContinue(code) => { state::set_status(*code); continue 'outer; } _ => return Err(e), } } } } Ok(()) }; if fork_builtins { log::trace!("Forking builtin: for"); self.run_fork("for", |s| { if let Err(e) = for_logic(s) { e.print_error(); } }) } else { for_logic(self) .try_blame(blame) .map_err(|e| e.with_redirs(guard)) } } fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { let blame = if_stmt.get_span().clone(); let NdRule::IfNode { cond_nodes, else_block, } = if_stmt.class else { unreachable!(); }; let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(if_stmt.redirs); let guard = self.io_stack.pop_frame().redirect()?; let if_logic = |s: &mut Self| -> ShResult<()> { let mut matched = false; for node in cond_nodes { let CondNode { cond, body } = node; if let Err(e) = s.dispatch_node(*cond) { state::set_status(1); return Err(e); } match state::get_status() { 0 => { matched = true; for body_node in body { s.dispatch_node(body_node)?; } break; // Don't check remaining elif conditions } _ => continue, } } if !matched { if !else_block.is_empty() { for node in else_block { s.dispatch_node(node)?; } } else { state::set_status(0); } } Ok(()) }; if fork_builtins { log::trace!("Forking builtin: if"); self.run_fork("if", |s| { if let Err(e) = if_logic(s) { e.print_error(); state::set_status(1); } }) } else { if_logic(self) .try_blame(blame) .map_err(|e| e.with_redirs(guard)) } } fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { let NdRule::Pipeline { cmds } = pipeline.class else { unreachable!() }; if self.interactive { log::debug!("Executing pipeline, cmds: {:#?}", cmds); } let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); self.job_stack.new_job(); if cmds.len() == 1 { self.fg_job = !is_bg && self.interactive; let cmd = cmds.into_iter().next().unwrap(); self.dispatch_node(cmd)?; // Give the pipeline terminal control as soon as the first child // establishes the PGID, so later children (e.g. nvim) don't get // SIGTTOU when they try to modify terminal attributes. // Only for interactive (top-level) pipelines — command substitution // and other non-interactive contexts must not steal the terminal. if !is_bg && self.interactive && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { attach_tty(pgid).ok(); } } else { let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); let mut pipes = PipeGenerator::new(cmds.len()).as_io_frames(); self.fg_job = !is_bg && self.interactive; let mut tty_attached = false; let last_cmd = cmds.len() - 1; for (i, mut cmd) in cmds.into_iter().enumerate() { let mut frame = pipes.next().ok_or_else(|| { ShErr::at( ShErrKind::InternalErr, cmd.get_span(), "failed to set up pipeline redirections".to_string(), ) })?; if i == 0 { for redir in std::mem::take(&mut in_redirs) { frame.push(redir); } } else if i == last_cmd { for redir in std::mem::take(&mut out_redirs) { frame.push(redir); } } let _guard = frame.redirect()?; cmd.flags |= NdFlags::FORK_BUILTINS; // multiple cmds means builtins must fork self.dispatch_node(cmd)?; // Give the pipeline terminal control as soon as the first child // establishes the PGID, so later children (e.g. nvim) don't get // SIGTTOU when they try to modify terminal attributes. // Only for interactive (top-level) pipelines — command substitution // and other non-interactive contexts must not steal the terminal. if !tty_attached && !is_bg && self.interactive && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { attach_tty(pgid).ok(); tty_attached = true; } } } let job = self.job_stack.finalize_job().unwrap(); dispatch_job(job, is_bg, self.interactive)?; Ok(()) } fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> { let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS); let cmd_raw = cmd .get_command() .unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class)) .to_string(); if fork_builtins { log::trace!("Forking builtin: {}", cmd_raw); let _guard = self.io_stack.pop_frame().redirect()?; self.run_fork(&cmd_raw, |s| { if let Err(e) = s.dispatch_builtin(cmd) { e.print_error(); } }) } else { let result = self.dispatch_builtin(cmd); if let Err(e) = result { let code = state::get_status(); if code == 0 { state::set_status(1); } return Err(e); } Ok(()) } } fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { let cmd_raw = cmd.get_command().unwrap().to_string(); let context = cmd.context.clone(); let NdRule::Command { assignments, argv } = &mut cmd.class else { unreachable!() }; let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; let _var_guard = var_ctx_guard(env_vars.into_iter().collect()); // Handle builtin/command recursion before redirect/job setup if cmd_raw.as_str() == "builtin" { *argv = argv .iter_mut() .skip(1) .map(|tk| tk.clone()) .collect::>(); return self.exec_builtin(cmd); } else if cmd_raw.as_str() == "command" { *argv = argv .iter_mut() .skip(1) .map(|tk| tk.clone()) .collect::>(); if cmd.flags.contains(NdFlags::FORK_BUILTINS) { cmd.flags |= NdFlags::NO_FORK; } return self.exec_cmd(cmd); } // Set up redirections here so we can attach the guard to propagated errors. self.io_stack.append_to_frame(mem::take(&mut cmd.redirs)); let frame = self.io_stack.pop_frame(); if self.interactive { log::debug!( "popped frame for builtin '{}', frame: {:#?}", cmd_raw, frame ); } let redir_guard = frame.redirect()?; // Register ChildProc in current job let job = self.job_stack.curr_job_mut().unwrap(); let child_pgid = if let Some(pgid) = job.pgid() { pgid } else { job.set_pgid(Pid::this()); Pid::this() }; let child = ChildProc::new(Pid::this(), Some(&cmd_raw), Some(child_pgid))?; job.push_child(child); // Handle exec specially — persist redirections before dispatch if cmd_raw.as_str() == "exec" { redir_guard.persist(); let result = exec::exec_builtin(cmd); return if let Err(e) = result { Err(e.with_context(context)) } else { Ok(()) }; } let result = match cmd_raw.as_str() { "echo" => echo(cmd), "cd" => cd(cmd), "export" => export(cmd), "local" => local(cmd), "pwd" => pwd(cmd), "source" | "." => source(cmd), "shift" => shift(cmd), "fg" => continue_job(cmd, JobBehavior::Foregound), "bg" => continue_job(cmd, JobBehavior::Background), "disown" => disown(cmd), "jobs" => jobs(cmd), "alias" => alias(cmd), "unalias" => unalias(cmd), "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "shopt" => shopt(cmd), "read" => read_builtin(cmd), "trap" => trap(cmd), "pushd" => pushd(cmd), "popd" => popd(cmd), "dirs" => dirs(cmd), "eval" => eval::eval(cmd), "readonly" => readonly(cmd), "unset" => unset(cmd), "complete" => complete_builtin(cmd), "compgen" => compgen_builtin(cmd), "map" => map::map(cmd), "pop" => arr_pop(cmd), "fpop" => arr_fpop(cmd), "push" => arr_push(cmd), "fpush" => arr_fpush(cmd), "rotate" => arr_rotate(cmd), "wait" => jobctl::wait(cmd), "type" => intro::type_builtin(cmd), "getopts" => getopts(cmd), "keymap" => keymap::keymap(cmd), "read_key" => read::read_key(cmd), "autocmd" => autocmd(cmd), "ulimit" => ulimit(cmd), "umask" => umask_builtin(cmd), "true" | ":" => { state::set_status(0); Ok(()) } "false" => { state::set_status(1); Ok(()) } _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw), }; if let Err(e) = result { if !e.is_flow_control() { state::set_status(1); } Err(e.with_context(context).with_redirs(redir_guard)) } else { Ok(()) } } fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { let blame = cmd.get_span().clone(); let context = cmd.context.clone(); let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() }; let mut env_vars_to_unset = vec![]; if !assignments.is_empty() { let assign_behavior = if argv.is_empty() { AssignBehavior::Set } else { AssignBehavior::Export }; env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; } let no_fork = cmd.flags.contains(NdFlags::NO_FORK); if argv.is_empty() { return Ok(()); } self.io_stack.append_to_frame(cmd.redirs); let exec_args = ExecArgs::new(argv).blame(blame)?; let _guard = self.io_stack.pop_frame().redirect()?; let job = self.job_stack.curr_job_mut().unwrap(); let existing_pgid = job.pgid(); let fg_job = self.fg_job; let interactive = self.interactive; let child_logic = |pgid: Option| -> ! { // For non-interactive exec-in-place (e.g. shed -c), skip process group // and terminal setup — just transparently replace the current process. if interactive || !no_fork { // Put ourselves in the correct process group before exec. // For the first child in a pipeline pgid is None, so we // become our own group leader (setpgid(0,0)). For later // children we join the leader's group. let our_pgid = pgid.unwrap_or(Pid::from_raw(0)); let _ = setpgid(Pid::from_raw(0), our_pgid); if fg_job { let tty_pgid = if our_pgid == Pid::from_raw(0) { nix::unistd::getpid() } else { our_pgid }; let _ = tcsetpgrp( unsafe { BorrowedFd::borrow_raw(*crate::libsh::sys::TTY_FILENO) }, tty_pgid, ); } } if interactive || !no_fork { crate::signal::reset_signals(fg_job); } let cmd = &exec_args.cmd.0; let span = exec_args.cmd.1; let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); // execvpe only returns on error let cmd_str = cmd.to_str().unwrap().to_string(); match e { Errno::ENOENT => { ShErr::new(ShErrKind::NotFound, span.clone()) .labeled(span, format!("{cmd_str}: command not found")) .with_context(context) .print_error(); } _ => { ShErr::at(ShErrKind::Errno(e), span, format!("{e}")) .with_context(context) .print_error(); } } exit(e as i32) }; if no_fork { child_logic(existing_pgid); } match unsafe { fork()? } { ForkResult::Child => child_logic(existing_pgid), ForkResult::Parent { child } => { // Close proc sub pipe fds - the child has inherited them // and will access them via /proc/self/fd/N. Keeping them // open here would prevent EOF on the pipe. write_jobs(|j| j.drain_registered_fds()); let cmd_name = exec_args.cmd.0.to_str().unwrap(); let child_pgid = if let Some(pgid) = existing_pgid { pgid } else { job.set_pgid(child); child }; let child_proc = ChildProc::new(child, Some(cmd_name), Some(child_pgid))?; job.push_child(child_proc); } } for var in env_vars_to_unset { unsafe { std::env::set_var(&var, "") }; } Ok(()) } fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { let existing_pgid = self.job_stack.curr_job_mut().unwrap().pgid(); match unsafe { fork()? } { ForkResult::Child => { let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0))); f(self); exit(state::get_status()) } ForkResult::Parent { child } => { write_jobs(|j| j.drain_registered_fds()); let job = self.job_stack.curr_job_mut().unwrap(); let child_pgid = if let Some(pgid) = existing_pgid { pgid } else { job.set_pgid(child); child }; let child_proc = ChildProc::new(child, Some(name), Some(child_pgid))?; job.push_child(child_proc); Ok(()) } } } fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { let mut new_env_vars = vec![]; let flags = match behavior { AssignBehavior::Export => VarFlags::EXPORT, AssignBehavior::Set => VarFlags::NONE, }; for assign in assigns { let is_arr = assign.flags.contains(NdFlags::ARR_ASSIGN); let NdRule::Assignment { kind, var, val } = assign.class else { unreachable!() }; let var = var.span.as_str(); let val = if is_arr { VarKind::arr_from_tk(val)? } else { VarKind::Str(val.expand()?.get_words().join(" ")) }; // Parse and expand array index BEFORE entering write_vars borrow let indexed = state::parse_arr_bracket(var) .map(|(name, idx_raw)| state::expand_arr_index(&idx_raw).map(|idx| (name, idx))) .transpose()?; match kind { AssignKind::Eq => { if let Some((name, idx)) = indexed { write_vars(|v| v.set_var_indexed(&name, idx, val.to_string(), flags))?; } else { write_vars(|v| v.set_var(var, val, flags))?; } } AssignKind::PlusEq => todo!(), AssignKind::MinusEq => todo!(), AssignKind::MultEq => todo!(), AssignKind::DivEq => todo!(), } if matches!(behavior, AssignBehavior::Export) { new_env_vars.push(var.to_string()); } } Ok(new_env_vars) } } pub fn prepare_argv(argv: Vec) -> ShResult> { let mut args = vec![]; for arg in argv { let span = arg.span.clone(); let expanded = arg.expand()?; for exp in expanded.get_words() { args.push((exp, span.clone())) } } Ok(args) } /// Initialize the pipes for a pipeline /// The first command gets `(None, WPipe)` /// The last command gets `(RPipe, None)` /// Commands inbetween get `(RPipe, WPipe)` /// If there is only one command, it gets `(None, None)` pub fn get_pipe_stack(num_cmds: usize) -> Vec<(Option, Option)> { let mut stack = Vec::with_capacity(num_cmds); let mut prev_read: Option = None; for i in 0..num_cmds { if i == num_cmds - 1 { stack.push((prev_read.take(), None)); } else { let (rpipe, wpipe) = IoMode::get_pipes(); let r_redir = Redir::new(rpipe, RedirType::Input); let w_redir = Redir::new(wpipe, RedirType::Output); // Push (prev_read, Some(w_redir)) and set prev_read to r_redir stack.push((prev_read.take(), Some(w_redir))); prev_read = Some(r_redir); } } stack } pub fn is_func(tk: Option) -> bool { let Some(tk) = tk else { return false }; read_logic(|l| l.get_func(&tk.to_string())).is_some() } pub fn is_subsh(tk: Option) -> bool { tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) } #[cfg(test)] mod tests { use crate::state; use crate::testutil::{TestGuard, test_input}; // ===================== while/until status ===================== #[test] fn while_loop_status_zero_after_completion() { let _g = TestGuard::new(); test_input("while false; do :; done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn while_loop_status_zero_after_iterations() { let _g = TestGuard::new(); test_input("X=0; while [[ $X -lt 3 ]]; do X=$((X+1)); done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn until_loop_status_zero_after_completion() { let _g = TestGuard::new(); test_input("until true; do :; done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn until_loop_status_zero_after_iterations() { let _g = TestGuard::new(); test_input("X=3; until [[ $X -le 0 ]]; do X=$((X-1)); done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn while_break_preserves_status() { let _g = TestGuard::new(); test_input("while true; do break; done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn while_body_status_propagates() { let _g = TestGuard::new(); test_input("X=0; while [[ $X -lt 1 ]]; do X=$((X+1)); false; done").unwrap(); // Loop body ended with `false` (status 1), but the loop itself // completed normally when the condition failed, so status should be 0 assert_eq!(state::get_status(), 0); } // ===================== if/elif/else status ===================== #[test] fn if_true_body_status() { let _g = TestGuard::new(); test_input("if true; then echo ok; fi").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn if_false_no_else_status() { let _g = TestGuard::new(); test_input("if false; then echo ok; fi").unwrap(); // No branch taken, POSIX says status is 0 assert_eq!(state::get_status(), 0); } #[test] fn if_else_branch_status() { let _g = TestGuard::new(); test_input("if false; then true; else false; fi").unwrap(); assert_eq!(state::get_status(), 1); } // ===================== for loop status ===================== #[test] fn for_loop_empty_list_status() { let _g = TestGuard::new(); test_input("for x in; do echo $x; done").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn for_loop_body_status() { let _g = TestGuard::new(); test_input("for x in a b c; do true; done").unwrap(); assert_eq!(state::get_status(), 0); } // ===================== case status ===================== #[test] fn case_match_status() { let _g = TestGuard::new(); test_input("case foo in foo) true;; esac").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn case_no_match_status() { let _g = TestGuard::new(); test_input("case foo in bar) true;; esac").unwrap(); assert_eq!(state::get_status(), 0); } // ===================== other stuff ===================== #[test] fn for_loop_var_zip() { let g = TestGuard::new(); test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap(); let out = g.read_output(); assert_eq!(out, "1 2\n3 4\n5 6\n"); } #[test] fn for_loop_unsets_zipped() { let g = TestGuard::new(); test_input("for a b c d in 1 2 3 4 5 6; do echo $a $b $c $d; done").unwrap(); let out = g.read_output(); assert_eq!(out, "1 2 3 4\n5 6\n"); } // ===================== negation (!) status ===================== #[test] fn negate_true() { let _g = TestGuard::new(); test_input("! true").unwrap(); assert_eq!(state::get_status(), 1); } #[test] fn negate_false() { let _g = TestGuard::new(); test_input("! false").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn double_negate_true() { let _g = TestGuard::new(); test_input("! ! true").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn double_negate_false() { let _g = TestGuard::new(); test_input("! ! false").unwrap(); assert_eq!(state::get_status(), 1); } #[test] fn negate_pipeline_last_cmd() { let _g = TestGuard::new(); // pipeline status = last cmd (false) = 1, negated → 0 test_input("! true | false").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn negate_pipeline_last_cmd_true() { let _g = TestGuard::new(); // pipeline status = last cmd (true) = 0, negated → 1 test_input("! false | true").unwrap(); assert_eq!(state::get_status(), 1); } #[test] fn negate_in_conjunction() { let _g = TestGuard::new(); // ! binds to pipeline, not conjunction: (! (true && false)) && true test_input("! (true && false) && true").unwrap(); assert_eq!(state::get_status(), 0); } #[test] fn negate_in_if_condition() { let g = TestGuard::new(); test_input("if ! false; then echo yes; fi").unwrap(); assert_eq!(state::get_status(), 0); assert_eq!(g.read_output(), "yes\n"); } }