- fixed 'I' command in normal mode not moving to exact start of line

- Added disown builtin
- Fixed job table not hanging up child processes on exit
- Added target architecture and os to --version output
- Added local builtin for creating variables scoped to functions
This commit is contained in:
2026-02-23 16:10:49 -05:00
parent 723bfd8413
commit f8e02d31cd
10 changed files with 205 additions and 17 deletions

34
src/builtin/eval.rs Normal file
View File

@@ -0,0 +1,34 @@
use nix::{errno::Errno, unistd::execvpe};
use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::ShResult,
parse::{NdRule, Node, execute::exec_input},
procio::IoStack,
state,
};
pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (expanded_argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if expanded_argv.is_empty() {
state::set_status(0);
return Ok(());
}
let joined_argv = expanded_argv.into_iter()
.map(|(s, _)| s)
.collect::<Vec<_>>()
.join(" ");
exec_input(joined_argv, None, false)
}

View File

@@ -4,7 +4,7 @@ use crate::{
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{IoStack, borrow_fd},
state::{self, VarFlags, write_vars}, state::{self, VarFlags, read_vars, write_vars},
}; };
use super::setup_builtin; use super::setup_builtin;
@@ -45,3 +45,42 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the local variables
let vars_output = read_vars(|v| {
let mut vars = v.flatten_vars()
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>();
vars.sort();
let mut vars_joined = vars.join("\n");
vars_joined.push('\n');
vars_joined
});
let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it
} else {
for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, VarFlags::LOCAL));
} else {
write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL)); // Create an uninitialized local variable
}
}
}
state::set_status(0);
Ok(())
}

View File

@@ -179,3 +179,46 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(()) Ok(())
} }
pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id
} else {
return Err(ShErr::full(ShErrKind::ExecFail, "disown: No jobs to disown", blame));
};
let mut tabid = curr_job_id;
let mut nohup = false;
let mut disown_all = false;
while let Some((arg, span)) = argv.next() {
match arg.as_str() {
"-h" => nohup = true,
"-a" => disown_all = true,
_ => {
tabid = parse_job_id(&arg, span.clone())?;
}
}
}
if disown_all {
write_jobs(|j| j.disown_all(nohup))?;
} else {
write_jobs(|j| j.disown(JobID::TableID(tabid), nohup))?;
}
state::set_status(0);
Ok(())
}

1
src/builtin/local.rs Normal file
View File

@@ -0,0 +1 @@

View File

@@ -27,11 +27,12 @@ pub mod trap;
pub mod zoltraak; pub mod zoltraak;
pub mod dirstack; pub mod dirstack;
pub mod exec; pub mod exec;
pub mod eval;
pub const BUILTINS: [&str; 25] = [ pub const BUILTINS: [&str; 28] = [
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias",
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
"pushd", "popd", "dirs", "exec", "pushd", "popd", "dirs", "exec", "eval"
]; ];
/// Sets up a builtin command /// Sets up a builtin command

View File

@@ -373,7 +373,7 @@ impl JobTab {
if job if job
.get_stats() .get_stats()
.iter() .iter()
.all(|stat| matches!(stat, WtStat::Exited(_, _))) .all(|stat| matches!(stat, WtStat::Exited(_, _) | WtStat::Signaled(_, _, _)))
{ {
jobs_to_remove.push(JobID::TableID(id)); jobs_to_remove.push(JobID::TableID(id));
} }
@@ -383,6 +383,41 @@ impl JobTab {
} }
Ok(()) Ok(())
} }
pub fn hang_up(&mut self) {
for job in self.jobs_mut().iter_mut().flatten() {
if job.send_hup {
job.killpg(Signal::SIGHUP).ok();
}
}
}
pub fn disown(&mut self, id: JobID, nohup: bool) -> ShResult<()> {
if let Some(job) = self.query_mut(id.clone()) {
if nohup {
job.no_hup();
} else {
self.remove_job(id);
}
}
Ok(())
}
pub fn disown_all(&mut self, nohup: bool) -> ShResult<()> {
let mut ids_to_remove = vec![];
for job in self.jobs_mut().iter_mut().flatten() {
if nohup {
job.no_hup();
} else {
ids_to_remove.push(JobID::TableID(job.tabid().unwrap()));
}
}
for id in ids_to_remove {
self.remove_job(id);
}
Ok(())
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -390,6 +425,7 @@ pub struct JobBldr {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Option<Pid>, pgid: Option<Pid>,
children: Vec<ChildProc>, children: Vec<ChildProc>,
send_hup: bool,
} }
impl Default for JobBldr { impl Default for JobBldr {
@@ -404,6 +440,7 @@ impl JobBldr {
table_id: None, table_id: None,
pgid: None, pgid: None,
children: vec![], children: vec![],
send_hup: true,
} }
} }
pub fn with_id(self, id: usize) -> Self { pub fn with_id(self, id: usize) -> Self {
@@ -411,6 +448,7 @@ impl JobBldr {
table_id: Some(id), table_id: Some(id),
pgid: self.pgid, pgid: self.pgid,
children: self.children, children: self.children,
send_hup: self.send_hup,
} }
} }
pub fn with_pgid(self, pgid: Pid) -> Self { pub fn with_pgid(self, pgid: Pid) -> Self {
@@ -418,6 +456,7 @@ impl JobBldr {
table_id: self.table_id, table_id: self.table_id,
pgid: Some(pgid), pgid: Some(pgid),
children: self.children, children: self.children,
send_hup: self.send_hup,
} }
} }
pub fn set_pgid(&mut self, pgid: Pid) { pub fn set_pgid(&mut self, pgid: Pid) {
@@ -426,11 +465,16 @@ impl JobBldr {
pub fn pgid(&self) -> Option<Pid> { pub fn pgid(&self) -> Option<Pid> {
self.pgid self.pgid
} }
pub fn no_hup(mut self) -> Self {
self.send_hup = false;
self
}
pub fn with_children(self, children: Vec<ChildProc>) -> Self { pub fn with_children(self, children: Vec<ChildProc>) -> Self {
Self { Self {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid, pgid: self.pgid,
children, children,
send_hup: self.send_hup,
} }
} }
pub fn push_child(&mut self, child: ChildProc) { pub fn push_child(&mut self, child: ChildProc) {
@@ -441,6 +485,7 @@ impl JobBldr {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid.unwrap_or(Pid::from_raw(0)), pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
children: self.children, children: self.children,
send_hup: self.send_hup,
} }
} }
} }
@@ -469,12 +514,16 @@ pub struct Job {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Pid, pgid: Pid,
children: Vec<ChildProc>, children: Vec<ChildProc>,
send_hup: bool,
} }
impl Job { impl Job {
pub fn set_tabid(&mut self, id: usize) { pub fn set_tabid(&mut self, id: usize) {
self.table_id = Some(id) self.table_id = Some(id)
} }
pub fn no_hup(&mut self) {
self.send_hup = false;
}
pub fn running(&self) -> bool { pub fn running(&self) -> bool {
!self.children.iter().all(|chld| chld.exited()) !self.children.iter().all(|chld| chld.exited())
} }
@@ -520,8 +569,7 @@ impl Job {
let stat = match sig { let stat = match sig {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
Signal::SIGCONT => WtStat::Continued(self.pgid), Signal::SIGCONT => WtStat::Continued(self.pgid),
Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false), sig => WtStat::Signaled(self.pgid, sig, false),
_ => unimplemented!("{}", sig),
}; };
self.set_stats(stat); self.set_stats(stat);
Ok(killpg(self.pgid, sig)?) Ok(killpg(self.pgid, sig)?)

View File

@@ -36,7 +36,7 @@ use crate::prompt::get_prompt;
use crate::prompt::readline::term::raw_mode; use crate::prompt::readline::term::raw_mode;
use crate::prompt::readline::{FernVi, ReadlineEvent}; use crate::prompt::readline::{FernVi, ReadlineEvent};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{read_logic, source_rc, write_meta}; use crate::state::{read_logic, source_rc, write_jobs, write_meta};
use clap::Parser; use clap::Parser;
use state::{read_vars, write_vars}; use state::{read_vars, write_vars};
@@ -74,9 +74,25 @@ fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
/// We need to make sure that even if we panic, our child processes get sighup
fn setup_panic_handler() {
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = state::FERN.try_with(|fern| {
if let Ok(mut jobs) = fern.jobs.try_borrow_mut() {
jobs.hang_up();
}
});
default_panic_hook(info);
}));
}
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
setup_panic_handler();
let mut args = FernArgs::parse(); let mut args = FernArgs::parse();
if env::args().next().is_some_and(|a| a.starts_with('-')) { if env::args().next().is_some_and(|a| a.starts_with('-')) {
// first arg is '-fern' // first arg is '-fern'
@@ -84,7 +100,7 @@ fn main() -> ExitCode {
args.login_shell = true; args.login_shell = true;
} }
if args.version { if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION")); println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS);
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
@@ -104,6 +120,7 @@ fn main() -> ExitCode {
eprintln!("fern: error running EXIT trap: {e}"); eprintln!("fern: error running EXIT trap: {e}");
} }
write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }

View File

@@ -2,7 +2,7 @@ use std::collections::{HashSet, VecDeque};
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, exec, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, 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,
jobs::{ChildProc, JobStack, dispatch_job}, jobs::{ChildProc, JobStack, dispatch_job},
@@ -588,11 +588,13 @@ impl Dispatcher {
"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),
"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),
"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),
@@ -608,6 +610,7 @@ impl Dispatcher {
"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),
_ => unimplemented!( _ => unimplemented!(
"Have not yet added support for builtin '{}'", "Have not yet added support for builtin '{}'",
cmd_raw.span.as_str() cmd_raw.span.as_str()

View File

@@ -677,15 +677,19 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
/// - Unimplemented features (comments, brace groups) /// - Unimplemented features (comments, brace groups)
pub fn marker_for(class: &TkRule) -> Option<Marker> { pub fn marker_for(class: &TkRule) -> Option<Marker> {
match class { match class {
TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => { TkRule::Pipe |
TkRule::ErrPipe |
TkRule::And |
TkRule::Or |
TkRule::Bg |
TkRule::BraceGrpStart |
TkRule::BraceGrpEnd => {
Some(markers::OPERATOR) Some(markers::OPERATOR)
} }
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::CasePattern => Some(markers::CASE_PAT),
TkRule::BraceGrpStart => todo!(), TkRule::Comment => Some(markers::COMMENT),
TkRule::BraceGrpEnd => todo!(),
TkRule::Comment => todo!(),
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None, TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None,
} }
} }

View File

@@ -147,9 +147,7 @@ pub fn hang_up(_: libc::c_int) {
SHOULD_QUIT.store(true, Ordering::SeqCst); SHOULD_QUIT.store(true, Ordering::SeqCst);
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
write_jobs(|j| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { j.hang_up();
job.killpg(Signal::SIGTERM).ok();
}
}); });
} }