Implemented the autocmd builtin, which allows you to register hooks for certain shell events.

This commit is contained in:
2026-03-04 12:55:50 -05:00
parent ba2301fd38
commit fbadbebf8c
17 changed files with 486 additions and 103 deletions

91
src/builtin/autocmd.rs Normal file
View File

@@ -0,0 +1,91 @@
use regex::Regex;
use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, state::{self, AutoCmd, AutoCmdKind, write_logic}
};
pub struct AutoCmdOpts {
pattern: Option<Regex>,
clear: bool
}
fn autocmd_optspec() -> [OptSpec;2] {
[
OptSpec {
opt: Opt::Short('p'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
}
]
}
pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult<AutoCmdOpts> {
let mut autocmd_opts = AutoCmdOpts {
pattern: None,
clear: false
};
let mut opts = opts.iter();
while let Some(arg) = opts.next() {
match arg {
Opt::ShortWithArg('p', arg) => {
autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e)))?);
}
Opt::Short('c') => {
autocmd_opts.clear = true;
}
_ => {
return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg)));
}
}
}
Ok(autocmd_opts)
}
pub fn autocmd(node: Node) -> ShResult<()> {
let span = node.get_span();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv,opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?;
let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?;
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
let mut args = argv.iter();
let Some(autocmd_kind) = args.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd kind".to_string()));
};
let Ok(autocmd_kind) = autocmd_kind.0.parse::<AutoCmdKind>() else {
return Err(ShErr::at(ShErrKind::ExecFail, autocmd_kind.1.clone(), format!("invalid autocmd kind: {}", autocmd_kind.0)));
};
if autocmd_opts.clear {
write_logic(|l| l.clear_autocmds(autocmd_kind));
state::set_status(0);
return Ok(());
}
let Some(autocmd_cmd) = args.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd command".to_string()));
};
let autocmd = AutoCmd {
pattern: autocmd_opts.pattern,
command: autocmd_cmd.0.clone(),
};
write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd));
state::set_status(0);
Ok(())
}

View File

@@ -44,7 +44,7 @@ pub fn cd(node: Node) -> ShResult<()> {
.labeled(cd_span.clone(), format!("cd: Not a directory '{}'", new_dir.display().fg(next_color()))));
}
if let Err(e) = env::set_current_dir(new_dir) {
if let Err(e) = state::change_dir(new_dir) {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red))));
}

View File

@@ -52,7 +52,7 @@ fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> {
);
}
if let Err(e) = env::set_current_dir(target) {
if let Err(e) = state::change_dir(target) {
return Err(
ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red)))
);

View File

@@ -1,5 +1,5 @@
use crate::{
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::{keys::KeyEvent, vimode::ModeReport}, state::{self, write_logic}
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::keys::KeyEvent, state::{self, write_logic}
};
bitflags! {

View File

@@ -26,13 +26,14 @@ pub mod arrops;
pub mod intro;
pub mod getopts;
pub mod keymap;
pub mod autocmd;
pub const BUILTINS: [&str; 46] = [
pub const BUILTINS: [&str; 47] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
"getopts", "keymap", "read_key"
"getopts", "keymap", "read_key", "autocmd"
];
pub fn true_builtin() -> ShResult<()> {

View File

@@ -4,7 +4,6 @@ use nix::{
libc::{STDIN_FILENO, STDOUT_FILENO},
unistd::{isatty, read, write},
};
use yansi::Paint;
use crate::{
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, sys::TTY_FILENO}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, readline::term::{KeyReader, PollReader, RawModeGuard}, state::{self, VarFlags, VarKind, read_vars, write_vars}