diff --git a/nix/hm-module.nix b/nix/hm-module.nix index bd7334e..a18cf38 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -50,7 +50,7 @@ in }; interactiveComments = lib.mkOption { type = lib.types.bool; - default = false; + default = true; description = "Whether to allow comments in interactive mode"; }; autoHistory = lib.mkOption { @@ -84,6 +84,11 @@ in default = true; description = "Whether to enable syntax highlighting in the shell"; }; + linebreakOnIncomplete = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to automatically insert a newline when the input is incomplete"; + }; extraPostConfig = lib.mkOption { type = lib.types.str; default = ""; @@ -117,6 +122,7 @@ in "shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}" "shopt prompt.comp_limit=${toString cfg.settings.completionLimit}" "shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}" + "shopt prompt.linebreak_on_incomplete=${boolToString cfg.settings.linebreakOnIncomplete}" ]) cfg.settings.extraPostConfig ]; diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 0121bc4..dacb8ff 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -32,6 +32,7 @@ pub const ECHO_OPTS: [OptSpec; 4] = [ ]; bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EchoFlags: u32 { const NO_NEWLINE = 0b000001; const USE_STDERR = 0b000010; @@ -60,6 +61,7 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( borrow_fd(STDOUT_FILENO) }; + let mut echo_output = prepare_echo_args( argv .into_iter() @@ -197,6 +199,7 @@ pub fn prepare_echo_args( prepared_args.push(prepared_arg); } + Ok(prepared_args) } diff --git a/src/builtin/local.rs b/src/builtin/local.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/builtin/local.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/builtin/read.rs b/src/builtin/read.rs index fbcc52c..bbbd2a1 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -113,7 +113,13 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S input.push(buf[0]); } } - Err(Errno::EINTR) => continue, + Err(Errno::EINTR) => { + if crate::signal::sigint_pending() { + state::set_status(130); + return Ok(String::new()); + } + continue; + } Err(e) => { return Err(ShErr::simple( ShErrKind::ExecFail, @@ -137,19 +143,32 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S let mut input: Vec = vec![]; loop { let mut buf = [0u8; 1]; + log::info!("read: about to call read()"); match read(STDIN_FILENO, &mut buf) { Ok(0) => { + log::info!("read: got EOF"); state::set_status(1); break; // EOF } - Ok(_) => { + Ok(n) => { + log::info!("read: got {} bytes: {:?}", n, &buf[..1]); if buf[0] == read_opts.delim { + state::set_status(0); break; // Delimiter reached, stop reading } input.push(buf[0]); } - Err(Errno::EINTR) => continue, + Err(Errno::EINTR) => { + let pending = crate::signal::sigint_pending(); + log::info!("read: got EINTR, sigint_pending={}", pending); + if pending { + state::set_status(130); + break; + } + continue; + } Err(e) => { + log::info!("read: got error: {}", e); return Err(ShErr::simple( ShErrKind::ExecFail, format!("read: Failed to read from stdin: {e}"), @@ -202,7 +221,6 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S } } - state::set_status(0); Ok(()) } diff --git a/src/expand.rs b/src/expand.rs index 22279c1..93cc4f3 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1576,7 +1576,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } } -fn glob_to_regex(glob: &str, anchored: bool) -> Regex { +pub fn glob_to_regex(glob: &str, anchored: bool) -> Regex { let mut regex = String::new(); if anchored { regex.push('^'); diff --git a/src/parse/execute.rs b/src/parse/execute.rs index bd88ddd..63ff588 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -4,7 +4,7 @@ use crate::{ builtin::{ alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, export::{export, local}, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak }, - expand::expand_aliases, + expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, dispatch_job}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, prelude::*, @@ -376,20 +376,37 @@ impl Dispatcher { result } fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { + let blame = brc_grp.get_span().clone(); let NdRule::BraceGrp { body } = brc_grp.class else { unreachable!() }; + 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 _guard = self.io_stack.pop_frame().redirect()?; + let brc_grp_logic = |s: &mut Self| -> ShResult<()> { - for node in body { - let blame = node.get_span(); - self.dispatch_node(node).try_blame(blame)?; - } + for node in body { + let blame = node.get_span(); + s.dispatch_node(node).try_blame(blame)?; + } - Ok(()) + Ok(()) + }; + + if fork_builtins { + log::trace!("Forking brace group"); + self.run_fork("brace group", |s| { + if let Err(e) = brc_grp_logic(s) { + eprintln!("{e}"); + } + }) + } else { + brc_grp_logic(self).try_blame(blame) + } } fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { + let blame = case_stmt.get_span().clone(); let NdRule::CaseNode { pattern, case_blocks, @@ -398,35 +415,52 @@ impl Dispatcher { unreachable!() }; - self.io_stack.append_to_frame(case_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); - let exp_pattern = pattern.clone().expand()?; - let pattern_raw = exp_pattern - .get_words() - .first() - .map(|s| s.to_string()) - .unwrap_or_default(); + self.io_stack.append_to_frame(case_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - 'outer: for block in case_blocks { - let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); - // Split at '|' to allow for multiple patterns like `foo|bar)` - let block_patterns = block_pattern_raw.split('|'); + 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(); - for pattern in block_patterns { - if pattern_raw == pattern || pattern == "*" { - for node in &body { - self.dispatch_node(node.clone())?; - } - break 'outer; - } - } - } + 'outer: for block in case_blocks { + let CaseNode { pattern, body } = block; + let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); + // Split at '|' to allow for multiple patterns like `foo|bar)` + let block_patterns = block_pattern_raw.split('|'); - Ok(()) + for pattern in block_patterns { + let pattern_regex = glob_to_regex(pattern, 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) { + eprintln!("{e}"); + } + }) + } else { + case_logic(self).try_blame(blame) + } } 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!(); }; @@ -437,47 +471,65 @@ impl Dispatcher { } }; - self.io_stack.append_to_frame(loop_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); - let CondNode { cond, body } = cond_node; - 'outer: loop { - if let Err(e) = self.dispatch_node(*cond.clone()) { - state::set_status(1); - return Err(e); - } + self.io_stack.append_to_frame(loop_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let status = state::get_status(); - if keep_going(kind, status) { - for node in &body { - if let Err(e) = self.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 { - break; - } - } + 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); + } - Ok(()) + 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 { + break; + } + } + + Ok(()) + }; + + if fork_builtins { + log::trace!("Forking builtin: loop"); + self.run_fork("loop", |s| { + if let Err(e) = loop_logic(s) { + eprintln!("{e}"); + } + }) + } else { + loop_logic(self).try_blame(blame) + } } 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 @@ -490,46 +542,60 @@ impl Dispatcher { ) }; - // Expand all array variables - let arr: Vec = to_expanded_strings(arr)?; - let vars: Vec = to_expanded_strings(vars)?; + self.io_stack.append_to_frame(for_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); + 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)?; - self.io_stack.append_to_frame(for_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let mut for_guard = VarCtxGuard::new(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))); + '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(), &val.to_string(), VarFlags::NONE)); - for_guard.vars.insert(var.to_string()); - } + for (var, val) in chunk_iter { + write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); + for_guard.vars.insert(var.to_string()); + } - for node in body.clone() { - if let Err(e) = self.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), - } - } - } - } + 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(()) + Ok(()) + }; + + if fork_builtins { + log::trace!("Forking builtin: for"); + self.run_fork("for", |s| { + if let Err(e) = for_logic(s) { + eprintln!("{e}"); + } + }) + } else { + for_logic(self).try_blame(blame) + } } fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { + let blame = if_stmt.get_span().clone(); let NdRule::IfNode { cond_nodes, else_block, @@ -537,54 +603,75 @@ impl Dispatcher { 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 _guard = self.io_stack.pop_frame().redirect()?; - let mut matched = false; - for node in cond_nodes { - let CondNode { cond, body } = node; + 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) = self.dispatch_node(*cond) { - state::set_status(1); - return Err(e); - } + 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 { - self.dispatch_node(body_node)?; - } - break; // Don't check remaining elif conditions - } - _ => continue, - } - } + 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 && !else_block.is_empty() { - for node in else_block { - self.dispatch_node(node)?; - } - } + if !matched && !else_block.is_empty() { + for node in else_block { + s.dispatch_node(node)?; + } + } - Ok(()) + Ok(()) + }; + + if fork_builtins { + log::trace!("Forking builtin: if"); + self.run_fork("if", |s| { + if let Err(e) = if_logic(s) { + eprintln!("{e}"); + state::set_status(1); + } + }) + } else { + if_logic(self).try_blame(blame) + } } fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { unreachable!() }; self.job_stack.new_job(); + let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins + // Zip the commands and their respective pipes into an iterator let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); - for ((rpipe, wpipe), cmd) in pipes_and_cmds { + for ((rpipe, wpipe), mut cmd) in pipes_and_cmds { if let Some(pipe) = rpipe { self.io_stack.push_to_frame(pipe); } if let Some(pipe) = wpipe { self.io_stack.push_to_frame(pipe); } + + if fork_builtin { + cmd.flags |= NdFlags::FORK_BUILTINS; + } self.dispatch_node(cmd)?; } let job = self.job_stack.finalize_job().unwrap(); @@ -592,17 +679,41 @@ impl Dispatcher { dispatch_job(job, is_bg)?; Ok(()) } - fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { + fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> { + let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS); + let cmd_raw = cmd.get_command().unwrap().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) { + eprintln!("{e}"); + } + }) + } 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 NdRule::Command { assignments, argv } = &mut cmd.class else { unreachable!() }; let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); - let cmd_raw = argv.first().unwrap(); let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); let io_stack_mut = &mut self.io_stack; - if cmd_raw.as_str() == "builtin" { *argv = argv .iter_mut() @@ -616,48 +727,44 @@ impl Dispatcher { .skip(1) .map(|tk| tk.clone()) .collect::>(); + if cmd.flags.contains(NdFlags::FORK_BUILTINS) { + cmd.flags |= NdFlags::NO_FORK; + } return self.dispatch_cmd(cmd); } - - let result = match cmd_raw.span.as_str() { - "echo" => echo(cmd, io_stack_mut, curr_job_mut), - "cd" => cd(cmd, curr_job_mut), - "export" => export(cmd, io_stack_mut, curr_job_mut), + match cmd_raw.as_str() { + "echo" => echo(cmd, io_stack_mut, curr_job_mut), + "cd" => cd(cmd, curr_job_mut), + "export" => export(cmd, io_stack_mut, curr_job_mut), "local" => local(cmd, io_stack_mut, curr_job_mut), - "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), - "source" => source(cmd, curr_job_mut), - "shift" => shift(cmd, curr_job_mut), - "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), - "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), + "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), + "source" => source(cmd, curr_job_mut), + "shift" => shift(cmd, curr_job_mut), + "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), + "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), "disown" => disown(cmd, io_stack_mut, curr_job_mut), - "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), - "alias" => alias(cmd, io_stack_mut, curr_job_mut), - "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), - "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), - "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), - "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), - "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), - "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), - "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), - "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), - "trap" => trap(cmd, io_stack_mut, curr_job_mut), + "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), + "alias" => alias(cmd, io_stack_mut, curr_job_mut), + "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), + "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), + "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), + "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), + "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), + "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), + "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), + "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), + "trap" => trap(cmd, io_stack_mut, curr_job_mut), "pushd" => pushd(cmd, io_stack_mut, curr_job_mut), "popd" => popd(cmd, io_stack_mut, curr_job_mut), "dirs" => dirs(cmd, io_stack_mut, curr_job_mut), "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), - _ => unimplemented!( - "Have not yet added support for builtin '{}'", - cmd_raw.span.as_str() - ), - }; - - if let Err(e) = result { - state::set_status(1); - return Err(e); - } - Ok(()) - } + _ => unimplemented!( + "Have not yet added support for builtin '{}'", + cmd_raw + ), + } + } fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() @@ -672,6 +779,8 @@ impl Dispatcher { 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(()); } @@ -679,32 +788,36 @@ impl Dispatcher { self.io_stack.append_to_frame(cmd.redirs); let exec_args = ExecArgs::new(argv)?; - let _guard = self.io_stack.pop_frame().redirect()?; - let job = self.job_stack.curr_job_mut().unwrap(); + let child_logic = || -> ! { + 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 => { + let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); + eprintln!("{err}"); + } + _ => { + let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); + eprintln!("{err}"); + } + } + exit(e as i32) + }; + + if no_fork { + child_logic(); + } + match unsafe { fork()? } { - ForkResult::Child => { - 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 => { - let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); - eprintln!("{err}"); - } - _ => { - let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); - eprintln!("{err}"); - } - } - exit(e as i32) - } + ForkResult::Child => child_logic(), ForkResult::Parent { child } => { // Close proc sub pipe fds - the child has inherited them // and will access them via /proc/self/fd/N. Keeping them @@ -730,6 +843,27 @@ impl Dispatcher { Ok(()) } + fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { + match unsafe { fork()? } { + ForkResult::Child => { + 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) = job.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![]; match behavior { diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 7455641..3823464 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -152,6 +152,7 @@ pub struct LexStream { source: Arc, pub cursor: usize, in_quote: bool, + brc_grp_start: Option, flags: LexFlags, } @@ -186,6 +187,7 @@ impl LexStream { source, cursor: 0, in_quote: false, + brc_grp_start: None, flags, } } @@ -220,8 +222,10 @@ impl LexStream { pub fn set_in_brc_grp(&mut self, is: bool) { if is { self.flags |= LexFlags::IN_BRC_GRP; + self.brc_grp_start = Some(self.cursor); } else { self.flags &= !LexFlags::IN_BRC_GRP; + self.brc_grp_start = None; } } pub fn next_is_cmd(&self) -> bool { @@ -698,6 +702,15 @@ impl Iterator for LexStream { return None; } else { // Return the EOI token + if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); + self.flags |= LexFlags::STALE; + return Err(ShErr::full( + ShErrKind::ParseErr, + "Unclosed brace group", + Span::new(start..self.cursor, self.source.clone()), + )).into(); + } let token = self.get_token(self.cursor..self.cursor, TkRule::EOI); self.flags |= LexFlags::STALE; return Some(Ok(token)); @@ -728,6 +741,14 @@ impl Iterator for LexStream { } if self.cursor == self.source.len() { + if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); + return Err(ShErr::full( + ShErrKind::ParseErr, + "Unclosed brace group", + Span::new(start..self.cursor, self.source.clone()), + )).into(); + } return None; } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 7162a2a..0431411 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -143,6 +143,8 @@ bitflags! { #[derive(Clone,Copy,Debug)] pub struct NdFlags: u32 { const BACKGROUND = 0b000001; + const FORK_BUILTINS = 0b000010; + const NO_FORK = 0b000100; } } @@ -1378,6 +1380,7 @@ impl ParseStream { redirs.push(redir); } } + TkRule::Comment => { /* Skip comments in command position */ } _ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class), } } diff --git a/src/procio.rs b/src/procio.rs index 28e4f33..55a43b9 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -342,6 +342,12 @@ impl DerefMut for IoStack { } } +impl From> for IoStack { + fn from(frames: Vec) -> Self { + Self { stack: frames } + } +} + pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { unsafe { BorrowedFd::borrow_raw(fd) } } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 69e7717..e2b9e72 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -2790,6 +2790,12 @@ impl LineBuf { } } } + Verb::AcceptLineOrNewline => { + // If this verb has reached this function, it means we have incomplete input + // and therefore must insert a newline instead of accepting the input + self.push('\n'); + self.cursor.add(1); + } Verb::Complete | Verb::EndOfFile @@ -2800,7 +2806,6 @@ impl LineBuf { | Verb::VisualModeLine | Verb::VisualModeBlock | Verb::CompleteBackward - | Verb::AcceptLineOrNewline | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these } Ok(()) diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 51f333c..9baad55 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -7,6 +7,7 @@ use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use crate::libsh::sys::TTY_FILENO; +use crate::parse::lex::LexStream; use crate::prelude::*; use crate::state::read_shopts; use crate::{ @@ -170,6 +171,31 @@ impl FernVi { self.history.reset(); } + fn should_submit(&mut self) -> ShResult { + let input = Arc::new(self.editor.buffer.clone()); + self.editor.calc_indent_level(); + let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::>>(); + let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::>>(); + let is_top_level = self.editor.auto_indent_level == 0; + + let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { + (true, true) => { + return Err(lex_result2.unwrap_err()); + } + (true, false) => { + return Err(lex_result1.unwrap_err()); + } + (false, true) => { + false + } + (false, false) => { + true + } + }; + + Ok(is_complete && is_top_level) + } + /// Process any available input and return readline event /// This is non-blocking - returns Pending if no complete line yet pub fn process_input(&mut self) -> ShResult { @@ -247,7 +273,7 @@ impl FernVi { continue; } - if cmd.should_submit() { + if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) { self.editor.set_hint(None); self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end self.print_line()?; // Redraw @@ -686,9 +712,8 @@ pub fn marker_for(class: &TkRule) -> Option { } TkRule::Sep => Some(markers::CMD_SEP), TkRule::Redir => Some(markers::REDIRECT), - TkRule::CasePattern => Some(markers::CASE_PAT), TkRule::Comment => Some(markers::COMMENT), - TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None, + TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None, } } @@ -782,7 +807,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } insertions.push((token.span.start, markers::SUBSH)); return insertions; - } + } else if token.class == TkRule::CasePattern { + insertions.push((token.span.end, markers::RESET)); + insertions.push((token.span.end - 1, markers::CASE_PAT)); + insertions.push((token.span.start, markers::OPERATOR)); + return insertions; + } let token_raw = token.span.as_str(); let mut token_chars = token_raw.char_indices().peekable(); diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index e6658a4..606ca57 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -131,7 +131,7 @@ impl ViCmd { .as_ref() .is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) } - pub fn should_submit(&self) -> bool { + pub fn is_submit_action(&self) -> bool { self .verb .as_ref() diff --git a/src/shopt.rs b/src/shopt.rs index b399d58..7d087c6 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -370,6 +370,7 @@ pub struct ShOptPrompt { pub comp_limit: usize, pub highlight: bool, pub auto_indent: bool, + pub linebreak_on_incomplete: bool, } impl ShOptPrompt { @@ -420,6 +421,15 @@ impl ShOptPrompt { }; self.auto_indent = val; } + "linebreak_on_incomplete" => { + let Ok(val) = val.parse::() else { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for linebreak_on_incomplete value", + )); + }; + self.linebreak_on_incomplete = val; + } "custom" => { todo!() } @@ -439,6 +449,7 @@ impl ShOptPrompt { "comp_limit", "highlight", "auto_indent", + "linebreak_on_incomplete", "custom", ]), ), @@ -484,6 +495,12 @@ impl ShOptPrompt { output.push_str(&format!("{}", self.auto_indent)); Ok(Some(output)) } + "linebreak_on_incomplete" => { + let mut output = + String::from("Whether to automatically insert a newline when the input is incomplete\n"); + output.push_str(&format!("{}", self.linebreak_on_incomplete)); + Ok(Some(output)) + } _ => Err( ShErr::simple( ShErrKind::SyntaxErr, @@ -499,6 +516,7 @@ impl ShOptPrompt { "comp_limit", "highlight", "auto_indent", + "linebreak_on_incomplete", ]), ), ), @@ -515,6 +533,7 @@ impl Display for ShOptPrompt { output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("highlight = {}", self.highlight)); output.push(format!("auto_indent = {}", self.auto_indent)); + output.push(format!("linebreak_on_incomplete = {}", self.linebreak_on_incomplete)); let final_output = output.join("\n"); @@ -530,6 +549,7 @@ impl Default for ShOptPrompt { comp_limit: 100, highlight: true, auto_indent: true, + linebreak_on_incomplete: true, } } } diff --git a/src/signal.rs b/src/signal.rs index 734247e..50718a1 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -46,6 +46,10 @@ pub fn signals_pending() -> bool { SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst) } +pub fn sigint_pending() -> bool { + SIGNALS.load(Ordering::SeqCst) & (1 << Signal::SIGINT as u64) != 0 +} + pub fn check_signals() -> ShResult<()> { let pending = SIGNALS.swap(0, Ordering::SeqCst); let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };