Refactored internals for builtins inside of pipelines

This commit is contained in:
2026-02-24 10:54:24 -05:00
parent cab7a0fea7
commit 622e9f4a1e
14 changed files with 440 additions and 191 deletions

View File

@@ -50,7 +50,7 @@ in
}; };
interactiveComments = lib.mkOption { interactiveComments = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = true;
description = "Whether to allow comments in interactive mode"; description = "Whether to allow comments in interactive mode";
}; };
autoHistory = lib.mkOption { autoHistory = lib.mkOption {
@@ -84,6 +84,11 @@ in
default = true; default = true;
description = "Whether to enable syntax highlighting in the shell"; 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 { extraPostConfig = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = ""; default = "";
@@ -117,6 +122,7 @@ in
"shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}" "shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}"
"shopt prompt.comp_limit=${toString cfg.settings.completionLimit}" "shopt prompt.comp_limit=${toString cfg.settings.completionLimit}"
"shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}" "shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}"
"shopt prompt.linebreak_on_incomplete=${boolToString cfg.settings.linebreakOnIncomplete}"
]) ])
cfg.settings.extraPostConfig cfg.settings.extraPostConfig
]; ];

View File

@@ -32,6 +32,7 @@ pub const ECHO_OPTS: [OptSpec; 4] = [
]; ];
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EchoFlags: u32 { pub struct EchoFlags: u32 {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010; 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) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = prepare_echo_args( let mut echo_output = prepare_echo_args(
argv argv
.into_iter() .into_iter()
@@ -197,6 +199,7 @@ pub fn prepare_echo_args(
prepared_args.push(prepared_arg); prepared_args.push(prepared_arg);
} }
Ok(prepared_args) Ok(prepared_args)
} }

View File

@@ -1 +0,0 @@

View File

@@ -113,7 +113,13 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
input.push(buf[0]); 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) => { Err(e) => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
@@ -137,19 +143,32 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
let mut input: Vec<u8> = vec![]; let mut input: Vec<u8> = vec![];
loop { loop {
let mut buf = [0u8; 1]; let mut buf = [0u8; 1];
log::info!("read: about to call read()");
match read(STDIN_FILENO, &mut buf) { match read(STDIN_FILENO, &mut buf) {
Ok(0) => { Ok(0) => {
log::info!("read: got EOF");
state::set_status(1); state::set_status(1);
break; // EOF break; // EOF
} }
Ok(_) => { Ok(n) => {
log::info!("read: got {} bytes: {:?}", n, &buf[..1]);
if buf[0] == read_opts.delim { if buf[0] == read_opts.delim {
state::set_status(0);
break; // Delimiter reached, stop reading break; // Delimiter reached, stop reading
} }
input.push(buf[0]); 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) => { Err(e) => {
log::info!("read: got error: {}", e);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("read: Failed to read from stdin: {e}"), 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(()) Ok(())
} }

View File

@@ -1576,7 +1576,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
} }
fn glob_to_regex(glob: &str, anchored: bool) -> Regex { pub fn glob_to_regex(glob: &str, anchored: bool) -> Regex {
let mut regex = String::new(); let mut regex = String::new();
if anchored { if anchored {
regex.push('^'); regex.push('^');

View File

@@ -4,7 +4,7 @@ use crate::{
builtin::{ 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 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}, jobs::{ChildProc, JobStack, dispatch_job},
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
prelude::*, prelude::*,
@@ -376,20 +376,37 @@ impl Dispatcher {
result result
} }
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { 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 { let NdRule::BraceGrp { body } = brc_grp.class else {
unreachable!() unreachable!()
}; };
let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(brc_grp.redirs); 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 { for node in body {
let blame = node.get_span(); let blame = node.get_span();
self.dispatch_node(node).try_blame(blame)?; 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<()> { fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> {
let blame = case_stmt.get_span().clone();
let NdRule::CaseNode { let NdRule::CaseNode {
pattern, pattern,
case_blocks, case_blocks,
@@ -398,35 +415,52 @@ impl Dispatcher {
unreachable!() unreachable!()
}; };
self.io_stack.append_to_frame(case_stmt.redirs); let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS);
let _guard = self.io_stack.pop_frame().redirect()?;
let exp_pattern = pattern.clone().expand()?; self.io_stack.append_to_frame(case_stmt.redirs);
let pattern_raw = exp_pattern let _guard = self.io_stack.pop_frame().redirect()?;
.get_words()
.first()
.map(|s| s.to_string())
.unwrap_or_default();
'outer: for block in case_blocks { let case_logic = |s: &mut Self| -> ShResult<()> {
let CaseNode { pattern, body } = block; let exp_pattern = pattern.clone().expand()?;
let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); let pattern_raw = exp_pattern
// Split at '|' to allow for multiple patterns like `foo|bar)` .get_words()
let block_patterns = block_pattern_raw.split('|'); .first()
.map(|s| s.to_string())
.unwrap_or_default();
for pattern in block_patterns { 'outer: for block in case_blocks {
if pattern_raw == pattern || pattern == "*" { let CaseNode { pattern, body } = block;
for node in &body { let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim();
self.dispatch_node(node.clone())?; // Split at '|' to allow for multiple patterns like `foo|bar)`
} let block_patterns = block_pattern_raw.split('|');
break 'outer;
}
}
}
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<()> { 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 { let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else {
unreachable!(); unreachable!();
}; };
@@ -437,47 +471,65 @@ impl Dispatcher {
} }
}; };
self.io_stack.append_to_frame(loop_stmt.redirs); let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS);
let _guard = self.io_stack.pop_frame().redirect()?;
let CondNode { cond, body } = cond_node; self.io_stack.append_to_frame(loop_stmt.redirs);
'outer: loop { let _guard = self.io_stack.pop_frame().redirect()?;
if let Err(e) = self.dispatch_node(*cond.clone()) {
state::set_status(1);
return Err(e);
}
let status = state::get_status(); let loop_logic = |s: &mut Self| -> ShResult<()> {
if keep_going(kind, status) { let CondNode { cond, body } = cond_node;
for node in &body { 'outer: loop {
if let Err(e) = self.dispatch_node(node.clone()) { if let Err(e) = s.dispatch_node(*cond.clone()) {
match e.kind() { state::set_status(1);
ShErrKind::LoopBreak(code) => { return Err(e);
state::set_status(*code); }
break 'outer;
}
ShErrKind::LoopContinue(code) => {
state::set_status(*code);
continue 'outer;
}
_ => {
return Err(e);
}
}
}
}
} else {
break;
}
}
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<()> { 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 { let NdRule::ForNode { vars, arr, body } = for_stmt.class else {
unreachable!(); unreachable!();
}; };
let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS);
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> { let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
Ok( Ok(
tks tks
@@ -490,46 +542,60 @@ impl Dispatcher {
) )
}; };
// Expand all array variables self.io_stack.append_to_frame(for_stmt.redirs);
let arr: Vec<String> = to_expanded_strings(arr)?; let _guard = self.io_stack.pop_frame().redirect()?;
let vars: Vec<String> = to_expanded_strings(vars)?;
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<String> = to_expanded_strings(arr)?;
let vars: Vec<String> = to_expanded_strings(vars)?;
self.io_stack.append_to_frame(for_stmt.redirs); let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect());
let _guard = self.io_stack.pop_frame().redirect()?;
'outer: for chunk in arr.chunks(vars.len()) { 'outer: for chunk in arr.chunks(vars.len()) {
let empty = String::new(); let empty = String::new();
let chunk_iter = vars let chunk_iter = vars
.iter() .iter()
.zip(chunk.iter().chain(std::iter::repeat(&empty))); .zip(chunk.iter().chain(std::iter::repeat(&empty)));
for (var, val) in chunk_iter { for (var, val) in chunk_iter {
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE)); write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE));
for_guard.vars.insert(var.to_string()); for_guard.vars.insert(var.to_string());
} }
for node in body.clone() { for node in body.clone() {
if let Err(e) = self.dispatch_node(node) { if let Err(e) = s.dispatch_node(node) {
match e.kind() { match e.kind() {
ShErrKind::LoopBreak(code) => { ShErrKind::LoopBreak(code) => {
state::set_status(*code); state::set_status(*code);
break 'outer; break 'outer;
} }
ShErrKind::LoopContinue(code) => { ShErrKind::LoopContinue(code) => {
state::set_status(*code); state::set_status(*code);
continue 'outer; continue 'outer;
} }
_ => return Err(e), _ => 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<()> { fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
let blame = if_stmt.get_span().clone();
let NdRule::IfNode { let NdRule::IfNode {
cond_nodes, cond_nodes,
else_block, else_block,
@@ -537,54 +603,75 @@ impl Dispatcher {
else { else {
unreachable!(); unreachable!();
}; };
let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(if_stmt.redirs); 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; let if_logic = |s: &mut Self| -> ShResult<()> {
for node in cond_nodes { let mut matched = false;
let CondNode { cond, body } = node; for node in cond_nodes {
let CondNode { cond, body } = node;
if let Err(e) = self.dispatch_node(*cond) { if let Err(e) = s.dispatch_node(*cond) {
state::set_status(1); state::set_status(1);
return Err(e); return Err(e);
} }
match state::get_status() { match state::get_status() {
0 => { 0 => {
matched = true; matched = true;
for body_node in body { for body_node in body {
self.dispatch_node(body_node)?; s.dispatch_node(body_node)?;
} }
break; // Don't check remaining elif conditions break; // Don't check remaining elif conditions
} }
_ => continue, _ => continue,
} }
} }
if !matched && !else_block.is_empty() { if !matched && !else_block.is_empty() {
for node in else_block { for node in else_block {
self.dispatch_node(node)?; 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<()> { fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else {
unreachable!() unreachable!()
}; };
self.job_stack.new_job(); 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 // Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); 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 { if let Some(pipe) = rpipe {
self.io_stack.push_to_frame(pipe); self.io_stack.push_to_frame(pipe);
} }
if let Some(pipe) = wpipe { if let Some(pipe) = wpipe {
self.io_stack.push_to_frame(pipe); self.io_stack.push_to_frame(pipe);
} }
if fork_builtin {
cmd.flags |= NdFlags::FORK_BUILTINS;
}
self.dispatch_node(cmd)?; self.dispatch_node(cmd)?;
} }
let job = self.job_stack.finalize_job().unwrap(); let job = self.job_stack.finalize_job().unwrap();
@@ -592,17 +679,41 @@ impl Dispatcher {
dispatch_job(job, is_bg)?; dispatch_job(job, is_bg)?;
Ok(()) 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 { let NdRule::Command { assignments, argv } = &mut cmd.class else {
unreachable!() unreachable!()
}; };
let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); 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 curr_job_mut = self.job_stack.curr_job_mut().unwrap();
let io_stack_mut = &mut self.io_stack; let io_stack_mut = &mut self.io_stack;
if cmd_raw.as_str() == "builtin" { if cmd_raw.as_str() == "builtin" {
*argv = argv *argv = argv
.iter_mut() .iter_mut()
@@ -616,48 +727,44 @@ impl Dispatcher {
.skip(1) .skip(1)
.map(|tk| tk.clone()) .map(|tk| tk.clone())
.collect::<Vec<Tk>>(); .collect::<Vec<Tk>>();
if cmd.flags.contains(NdFlags::FORK_BUILTINS) {
cmd.flags |= NdFlags::NO_FORK;
}
return self.dispatch_cmd(cmd); return self.dispatch_cmd(cmd);
} }
match cmd_raw.as_str() {
let result = match cmd_raw.span.as_str() { "echo" => echo(cmd, io_stack_mut, curr_job_mut),
"echo" => echo(cmd, io_stack_mut, curr_job_mut), "cd" => cd(cmd, curr_job_mut),
"cd" => cd(cmd, curr_job_mut), "export" => export(cmd, io_stack_mut, curr_job_mut),
"export" => export(cmd, io_stack_mut, curr_job_mut),
"local" => local(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), "pwd" => pwd(cmd, io_stack_mut, curr_job_mut),
"source" => source(cmd, curr_job_mut), "source" => source(cmd, curr_job_mut),
"shift" => shift(cmd, curr_job_mut), "shift" => shift(cmd, curr_job_mut),
"fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound),
"bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background),
"disown" => disown(cmd, io_stack_mut, curr_job_mut), "disown" => disown(cmd, io_stack_mut, curr_job_mut),
"jobs" => jobs(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), "alias" => alias(cmd, io_stack_mut, curr_job_mut),
"unalias" => unalias(cmd, io_stack_mut, curr_job_mut), "unalias" => unalias(cmd, io_stack_mut, curr_job_mut),
"return" => flowctl(cmd, ShErrKind::FuncReturn(0)), "return" => flowctl(cmd, ShErrKind::FuncReturn(0)),
"break" => flowctl(cmd, ShErrKind::LoopBreak(0)), "break" => flowctl(cmd, ShErrKind::LoopBreak(0)),
"continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)),
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
"zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut),
"shopt" => shopt(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), "read" => read_builtin(cmd, io_stack_mut, curr_job_mut),
"trap" => trap(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), "pushd" => pushd(cmd, io_stack_mut, curr_job_mut),
"popd" => popd(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), "dirs" => dirs(cmd, io_stack_mut, curr_job_mut),
"exec" => exec::exec_builtin(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), "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut),
_ => unimplemented!( _ => unimplemented!(
"Have not yet added support for builtin '{}'", "Have not yet added support for builtin '{}'",
cmd_raw.span.as_str() cmd_raw
), ),
}; }
}
if let Err(e) = result {
state::set_status(1);
return Err(e);
}
Ok(())
}
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
let NdRule::Command { assignments, argv } = cmd.class else { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
@@ -672,6 +779,8 @@ impl Dispatcher {
env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?;
} }
let no_fork = cmd.flags.contains(NdFlags::NO_FORK);
if argv.is_empty() { if argv.is_empty() {
return Ok(()); return Ok(());
} }
@@ -679,32 +788,36 @@ impl Dispatcher {
self.io_stack.append_to_frame(cmd.redirs); self.io_stack.append_to_frame(cmd.redirs);
let exec_args = ExecArgs::new(argv)?; let exec_args = ExecArgs::new(argv)?;
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let job = self.job_stack.curr_job_mut().unwrap(); 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()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => 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)
}
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
// Close proc sub pipe fds - the child has inherited them // Close proc sub pipe fds - the child has inherited them
// and will access them via /proc/self/fd/N. Keeping them // and will access them via /proc/self/fd/N. Keeping them
@@ -730,6 +843,27 @@ impl Dispatcher {
Ok(()) 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<Node>, behavior: AssignBehavior) -> ShResult<Vec<String>> { fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> ShResult<Vec<String>> {
let mut new_env_vars = vec![]; let mut new_env_vars = vec![];
match behavior { match behavior {

View File

@@ -152,6 +152,7 @@ pub struct LexStream {
source: Arc<String>, source: Arc<String>,
pub cursor: usize, pub cursor: usize,
in_quote: bool, in_quote: bool,
brc_grp_start: Option<usize>,
flags: LexFlags, flags: LexFlags,
} }
@@ -186,6 +187,7 @@ impl LexStream {
source, source,
cursor: 0, cursor: 0,
in_quote: false, in_quote: false,
brc_grp_start: None,
flags, flags,
} }
} }
@@ -220,8 +222,10 @@ impl LexStream {
pub fn set_in_brc_grp(&mut self, is: bool) { pub fn set_in_brc_grp(&mut self, is: bool) {
if is { if is {
self.flags |= LexFlags::IN_BRC_GRP; self.flags |= LexFlags::IN_BRC_GRP;
self.brc_grp_start = Some(self.cursor);
} else { } else {
self.flags &= !LexFlags::IN_BRC_GRP; self.flags &= !LexFlags::IN_BRC_GRP;
self.brc_grp_start = None;
} }
} }
pub fn next_is_cmd(&self) -> bool { pub fn next_is_cmd(&self) -> bool {
@@ -698,6 +702,15 @@ impl Iterator for LexStream {
return None; return None;
} else { } else {
// Return the EOI token // 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); let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
self.flags |= LexFlags::STALE; self.flags |= LexFlags::STALE;
return Some(Ok(token)); return Some(Ok(token));
@@ -728,6 +741,14 @@ impl Iterator for LexStream {
} }
if self.cursor == self.source.len() { 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; return None;
} }

View File

@@ -143,6 +143,8 @@ bitflags! {
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
pub struct NdFlags: u32 { pub struct NdFlags: u32 {
const BACKGROUND = 0b000001; const BACKGROUND = 0b000001;
const FORK_BUILTINS = 0b000010;
const NO_FORK = 0b000100;
} }
} }
@@ -1378,6 +1380,7 @@ impl ParseStream {
redirs.push(redir); redirs.push(redir);
} }
} }
TkRule::Comment => { /* Skip comments in command position */ }
_ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class), _ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class),
} }
} }

View File

@@ -342,6 +342,12 @@ impl DerefMut for IoStack {
} }
} }
impl From<Vec<IoFrame>> for IoStack {
fn from(frames: Vec<IoFrame>) -> Self {
Self { stack: frames }
}
}
pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
unsafe { BorrowedFd::borrow_raw(fd) } unsafe { BorrowedFd::borrow_raw(fd) }
} }

View File

@@ -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::Complete
| Verb::EndOfFile | Verb::EndOfFile
@@ -2800,7 +2806,6 @@ impl LineBuf {
| Verb::VisualModeLine | Verb::VisualModeLine
| Verb::VisualModeBlock | Verb::VisualModeBlock
| Verb::CompleteBackward | Verb::CompleteBackward
| Verb::AcceptLineOrNewline
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
} }
Ok(()) Ok(())

View File

@@ -7,6 +7,7 @@ use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::parse::lex::LexStream;
use crate::prelude::*; use crate::prelude::*;
use crate::state::read_shopts; use crate::state::read_shopts;
use crate::{ use crate::{
@@ -170,6 +171,31 @@ impl FernVi {
self.history.reset(); self.history.reset();
} }
fn should_submit(&mut self) -> ShResult<bool> {
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::<ShResult<Vec<_>>>();
let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
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 /// Process any available input and return readline event
/// This is non-blocking - returns Pending if no complete line yet /// This is non-blocking - returns Pending if no complete line yet
pub fn process_input(&mut self) -> ShResult<ReadlineEvent> { pub fn process_input(&mut self) -> ShResult<ReadlineEvent> {
@@ -247,7 +273,7 @@ impl FernVi {
continue; 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.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.print_line()?; // Redraw self.print_line()?; // Redraw
@@ -686,9 +712,8 @@ pub fn marker_for(class: &TkRule) -> Option<Marker> {
} }
TkRule::Sep => Some(markers::CMD_SEP), TkRule::Sep => Some(markers::CMD_SEP),
TkRule::Redir => Some(markers::REDIRECT), TkRule::Redir => Some(markers::REDIRECT),
TkRule::CasePattern => Some(markers::CASE_PAT),
TkRule::Comment => Some(markers::COMMENT), 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)); insertions.push((token.span.start, markers::SUBSH));
return insertions; 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 token_raw = token.span.as_str();
let mut token_chars = token_raw.char_indices().peekable(); let mut token_chars = token_raw.char_indices().peekable();

View File

@@ -131,7 +131,7 @@ impl ViCmd {
.as_ref() .as_ref()
.is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) .is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
} }
pub fn should_submit(&self) -> bool { pub fn is_submit_action(&self) -> bool {
self self
.verb .verb
.as_ref() .as_ref()

View File

@@ -370,6 +370,7 @@ pub struct ShOptPrompt {
pub comp_limit: usize, pub comp_limit: usize,
pub highlight: bool, pub highlight: bool,
pub auto_indent: bool, pub auto_indent: bool,
pub linebreak_on_incomplete: bool,
} }
impl ShOptPrompt { impl ShOptPrompt {
@@ -420,6 +421,15 @@ impl ShOptPrompt {
}; };
self.auto_indent = val; self.auto_indent = val;
} }
"linebreak_on_incomplete" => {
let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for linebreak_on_incomplete value",
));
};
self.linebreak_on_incomplete = val;
}
"custom" => { "custom" => {
todo!() todo!()
} }
@@ -439,6 +449,7 @@ impl ShOptPrompt {
"comp_limit", "comp_limit",
"highlight", "highlight",
"auto_indent", "auto_indent",
"linebreak_on_incomplete",
"custom", "custom",
]), ]),
), ),
@@ -484,6 +495,12 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.auto_indent)); output.push_str(&format!("{}", self.auto_indent));
Ok(Some(output)) 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( _ => Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
@@ -499,6 +516,7 @@ impl ShOptPrompt {
"comp_limit", "comp_limit",
"highlight", "highlight",
"auto_indent", "auto_indent",
"linebreak_on_incomplete",
]), ]),
), ),
), ),
@@ -515,6 +533,7 @@ impl Display for ShOptPrompt {
output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("comp_limit = {}", self.comp_limit));
output.push(format!("highlight = {}", self.highlight)); output.push(format!("highlight = {}", self.highlight));
output.push(format!("auto_indent = {}", self.auto_indent)); output.push(format!("auto_indent = {}", self.auto_indent));
output.push(format!("linebreak_on_incomplete = {}", self.linebreak_on_incomplete));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -530,6 +549,7 @@ impl Default for ShOptPrompt {
comp_limit: 100, comp_limit: 100,
highlight: true, highlight: true,
auto_indent: true, auto_indent: true,
linebreak_on_incomplete: true,
} }
} }
} }

View File

@@ -46,6 +46,10 @@ pub fn signals_pending() -> bool {
SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst) 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<()> { pub fn check_signals() -> ShResult<()> {
let pending = SIGNALS.swap(0, Ordering::SeqCst); let pending = SIGNALS.swap(0, Ordering::SeqCst);
let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };