Implemented the exec builtin
Fixed readline and terminal interactions using stdin instead of /dev/tty
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ shell.nix
|
|||||||
*~
|
*~
|
||||||
TODO.md
|
TODO.md
|
||||||
AUDIT.md
|
AUDIT.md
|
||||||
|
KNOWN_ISSUES.md
|
||||||
rust-toolchain.toml
|
rust-toolchain.toml
|
||||||
/ref
|
/ref
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
let dirs = m.dirs_mut();
|
let dirs = m.dirs_mut();
|
||||||
dirs.push_front(cwd);
|
dirs.push_front(cwd);
|
||||||
match idx {
|
match idx {
|
||||||
StackIdx::FromTop(n) => dirs.rotate_left(n + 1),
|
StackIdx::FromTop(n) => dirs.rotate_left(n),
|
||||||
StackIdx::FromBottom(n) => dirs.rotate_right(n + 1),
|
StackIdx::FromBottom(n) => dirs.rotate_right(n + 1),
|
||||||
}
|
}
|
||||||
dirs.pop_front()
|
dirs.pop_front()
|
||||||
|
|||||||
50
src/builtin/exec.rs
Normal file
50
src/builtin/exec.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use nix::{errno::Errno, unistd::execvpe};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node, execute::ExecArgs},
|
||||||
|
procio::IoStack,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn exec_builtin(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 let Some(g) = guard {
|
||||||
|
// Persist redirections so they affect the entire shell,
|
||||||
|
// not just this command call
|
||||||
|
g.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
if expanded_argv.is_empty() {
|
||||||
|
state::set_status(0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = ExecArgs::from_expanded(expanded_argv)?;
|
||||||
|
|
||||||
|
let cmd = &args.cmd.0;
|
||||||
|
let span = args.cmd.1;
|
||||||
|
|
||||||
|
let Err(e) = execvpe(cmd, &args.argv, &args.envp);
|
||||||
|
|
||||||
|
// execvpe only returns on error
|
||||||
|
let cmd_str = cmd.to_str().unwrap().to_string();
|
||||||
|
match e {
|
||||||
|
Errno::ENOENT => {
|
||||||
|
Err(ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Err(ShErr::full(ShErrKind::Errno(e), format!("{e}"), span))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,12 @@ pub mod test; // [[ ]] thing
|
|||||||
pub mod trap;
|
pub mod trap;
|
||||||
pub mod zoltraak;
|
pub mod zoltraak;
|
||||||
pub mod dirstack;
|
pub mod dirstack;
|
||||||
|
pub mod exec;
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 24] = [
|
pub const BUILTINS: [&str; 25] = [
|
||||||
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||||
"pushd", "popd", "dirs"
|
"pushd", "popd", "dirs", "exec",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sets up a builtin command
|
/// Sets up a builtin command
|
||||||
|
|||||||
@@ -829,6 +829,11 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
|||||||
|
|
||||||
match unsafe { fork()? } {
|
match unsafe { fork()? } {
|
||||||
ForkResult::Child => {
|
ForkResult::Child => {
|
||||||
|
// Close the parent's pipe end so the grandchild doesn't inherit it.
|
||||||
|
// Without this, >(cmd) hangs because the command holds its own
|
||||||
|
// pipe's write end open and never sees EOF.
|
||||||
|
drop(register_fd);
|
||||||
|
|
||||||
let redir = Redir::new(proc_fd, redir_type);
|
let redir = Redir::new(proc_fd, redir_type);
|
||||||
let io_frame = IoFrame::from_redir(redir);
|
let io_frame = IoFrame::from_redir(redir);
|
||||||
let mut io_stack = IoStack::new();
|
let mut io_stack = IoStack::new();
|
||||||
@@ -842,7 +847,6 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
|||||||
}
|
}
|
||||||
ForkResult::Parent { child } => {
|
ForkResult::Parent { child } => {
|
||||||
write_jobs(|j| j.register_fd(child, register_fd));
|
write_jobs(|j| j.register_fd(child, register_fd));
|
||||||
let registered = read_jobs(|j| j.registered_fds().to_vec());
|
|
||||||
// Do not wait; process may run in background
|
// Do not wait; process may run in background
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ impl JobTab {
|
|||||||
let registered_fd = RegisteredFd { fd, owner_pid };
|
let registered_fd = RegisteredFd { fd, owner_pid };
|
||||||
self.fd_registry.push(registered_fd)
|
self.fd_registry.push(registered_fd)
|
||||||
}
|
}
|
||||||
|
/// Close all registered proc sub fds. Called after fork in exec_cmd
|
||||||
|
/// so the parent doesn't hold pipe ends that prevent EOF.
|
||||||
|
pub fn drain_registered_fds(&mut self) {
|
||||||
|
self.fd_registry.clear();
|
||||||
|
}
|
||||||
fn prune_jobs(&mut self) {
|
fn prune_jobs(&mut self) {
|
||||||
while let Some(job) = self.jobs.last() {
|
while let Some(job) = self.jobs.last() {
|
||||||
if job.is_none() {
|
if job.is_none() {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use termios::{LocalFlags, Termios};
|
use termios::{LocalFlags, Termios};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -31,6 +33,11 @@ use crate::prelude::*;
|
|||||||
/// lifecycle could lead to undefined behavior.
|
/// lifecycle could lead to undefined behavior.
|
||||||
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
||||||
|
|
||||||
|
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
|
||||||
|
open("/dev/tty", OFlag::O_RDWR, Mode::empty())
|
||||||
|
.expect("Failed to open /dev/tty")
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TermiosGuard {
|
pub struct TermiosGuard {
|
||||||
saved_termios: Option<Termios>,
|
saved_termios: Option<Termios>,
|
||||||
@@ -42,7 +49,7 @@ impl TermiosGuard {
|
|||||||
saved_termios: None,
|
saved_termios: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
if isatty(*TTY_FILENO).unwrap() {
|
||||||
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||||
new.saved_termios = Some(current_termios);
|
new.saved_termios = Some(current_termios);
|
||||||
|
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -29,6 +29,7 @@ use nix::unistd::read;
|
|||||||
|
|
||||||
use crate::builtin::trap::TrapTarget;
|
use crate::builtin::trap::TrapTarget;
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
|
use crate::libsh::sys::TTY_FILENO;
|
||||||
use crate::parse::execute::exec_input;
|
use crate::parse::execute::exec_input;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::prompt::get_prompt;
|
use crate::prompt::get_prompt;
|
||||||
@@ -51,6 +52,12 @@ struct FernArgs {
|
|||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
version: bool,
|
version: bool,
|
||||||
|
|
||||||
|
#[arg(short)]
|
||||||
|
interactive: bool,
|
||||||
|
|
||||||
|
#[arg(long,short)]
|
||||||
|
login_shell: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force evaluation of lazily-initialized values early in shell startup.
|
/// Force evaluation of lazily-initialized values early in shell startup.
|
||||||
@@ -70,7 +77,12 @@ fn kickstart_lazy_evals() {
|
|||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
kickstart_lazy_evals();
|
kickstart_lazy_evals();
|
||||||
let args = FernArgs::parse();
|
let mut args = FernArgs::parse();
|
||||||
|
if env::args().next().is_some_and(|a| a.starts_with('-')) {
|
||||||
|
// first arg is '-fern'
|
||||||
|
// meaning we are in a login shell
|
||||||
|
args.login_shell = true;
|
||||||
|
}
|
||||||
if args.version {
|
if args.version {
|
||||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
@@ -134,7 +146,7 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create readline instance with initial prompt
|
// Create readline instance with initial prompt
|
||||||
let mut readline = match FernVi::new(get_prompt().ok()) {
|
let mut readline = match FernVi::new(get_prompt().ok(), *TTY_FILENO) {
|
||||||
Ok(rl) => rl,
|
Ok(rl) => rl,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to initialize readline: {e}");
|
eprintln!("Failed to initialize readline: {e}");
|
||||||
@@ -169,7 +181,7 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
|
|
||||||
// Poll for stdin input
|
// Poll for stdin input
|
||||||
let mut fds = [PollFd::new(
|
let mut fds = [PollFd::new(
|
||||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
|
||||||
PollFlags::POLLIN,
|
PollFlags::POLLIN,
|
||||||
)];
|
)];
|
||||||
|
|
||||||
@@ -191,7 +203,7 @@ fn fern_interactive() -> ShResult<()> {
|
|||||||
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||||
{
|
{
|
||||||
let mut buffer = [0u8; 1024];
|
let mut buffer = [0u8; 1024];
|
||||||
match read(STDIN_FILENO, &mut buffer) {
|
match read(*TTY_FILENO, &mut buffer) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
// EOF
|
// EOF
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ use std::collections::{HashSet, VecDeque};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, 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, 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
|
||||||
},
|
},
|
||||||
expand::expand_aliases,
|
expand::expand_aliases,
|
||||||
jobs::{ChildProc, JobStack, dispatch_job},
|
jobs::{ChildProc, JobStack, dispatch_job},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoMode, IoStack},
|
procio::{IoMode, IoStack},
|
||||||
state::{self, ShFunc, VarFlags, read_logic, write_logic, write_vars},
|
state::{self, ShFunc, VarFlags, read_logic, write_jobs, write_logic, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -80,6 +80,10 @@ impl ExecArgs {
|
|||||||
pub fn new(argv: Vec<Tk>) -> ShResult<Self> {
|
pub fn new(argv: Vec<Tk>) -> ShResult<Self> {
|
||||||
assert!(!argv.is_empty());
|
assert!(!argv.is_empty());
|
||||||
let argv = prepare_argv(argv)?;
|
let argv = prepare_argv(argv)?;
|
||||||
|
Self::from_expanded(argv)
|
||||||
|
}
|
||||||
|
pub fn from_expanded(argv: Vec<(String, Span)>) -> ShResult<Self> {
|
||||||
|
assert!(!argv.is_empty());
|
||||||
let cmd = Self::get_cmd(&argv);
|
let cmd = Self::get_cmd(&argv);
|
||||||
let argv = Self::get_argv(argv);
|
let argv = Self::get_argv(argv);
|
||||||
let envp = Self::get_envp();
|
let envp = Self::get_envp();
|
||||||
@@ -603,6 +607,7 @@ impl Dispatcher {
|
|||||||
"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),
|
||||||
_ => 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()
|
||||||
@@ -663,6 +668,11 @@ impl Dispatcher {
|
|||||||
exit(e as i32)
|
exit(e as i32)
|
||||||
}
|
}
|
||||||
ForkResult::Parent { child } => {
|
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 cmd_name = exec_args.cmd.0.to_str().unwrap();
|
||||||
|
|
||||||
let child_pgid = if let Some(pgid) = job.pgid() {
|
let child_pgid = if let Some(pgid) = job.pgid() {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ pub enum IoMode {
|
|||||||
buf: String,
|
buf: String,
|
||||||
pipe: Arc<OwnedFd>,
|
pipe: Arc<OwnedFd>,
|
||||||
},
|
},
|
||||||
|
Close {
|
||||||
|
tgt_fd: RawFd,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IoMode {
|
impl IoMode {
|
||||||
@@ -152,6 +155,21 @@ impl<R: Read> IoBuf<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RedirGuard(IoFrame);
|
pub struct RedirGuard(IoFrame);
|
||||||
|
|
||||||
|
impl RedirGuard {
|
||||||
|
pub fn persist(mut self) {
|
||||||
|
if let Some(saved) = self.0.saved_io.take() {
|
||||||
|
close(saved.0).ok();
|
||||||
|
close(saved.1).ok();
|
||||||
|
close(saved.2).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the guard is dropped here
|
||||||
|
// but since we took the saved fds
|
||||||
|
// the drop does not restore them
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for RedirGuard {
|
impl Drop for RedirGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.0.restore().ok();
|
self.0.restore().ok();
|
||||||
|
|||||||
@@ -263,6 +263,9 @@ impl Completer {
|
|||||||
if let Some(eq_pos) = token_str.rfind('=') {
|
if let Some(eq_pos) = token_str.rfind('=') {
|
||||||
// Adjust span to only replace the part after '='
|
// Adjust span to only replace the part after '='
|
||||||
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
||||||
|
cur_token
|
||||||
|
.span
|
||||||
|
.set_range(self.token_span.0..self.token_span.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ impl LineBuf {
|
|||||||
dir: Direction,
|
dir: Direction,
|
||||||
) -> Box<dyn Iterator<Item = usize>> {
|
) -> Box<dyn Iterator<Item = usize>> {
|
||||||
self.update_graphemes_lazy();
|
self.update_graphemes_lazy();
|
||||||
let skip = if pos == 0 { 0 } else { pos + 1 };
|
let skip = pos + 1;
|
||||||
match dir {
|
match dir {
|
||||||
Direction::Forward => Box::new(self.grapheme_indices().to_vec().into_iter().skip(skip))
|
Direction::Forward => Box::new(self.grapheme_indices().to_vec().into_iter().skip(skip))
|
||||||
as Box<dyn Iterator<Item = usize>>,
|
as Box<dyn Iterator<Item = usize>>,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
|
|||||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
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::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::{
|
libsh::{
|
||||||
@@ -112,10 +113,10 @@ pub struct FernVi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FernVi {
|
impl FernVi {
|
||||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
pub fn new(prompt: Option<String>, tty: RawFd) -> ShResult<Self> {
|
||||||
let mut new = Self {
|
let mut new = Self {
|
||||||
reader: PollReader::new(),
|
reader: PollReader::new(),
|
||||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
writer: Box::new(TermWriter::new(tty)),
|
||||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||||
completer: Completer::new(),
|
completer: Completer::new(),
|
||||||
highlighter: Highlighter::new(),
|
highlighter: Highlighter::new(),
|
||||||
@@ -294,7 +295,7 @@ impl FernVi {
|
|||||||
|
|
||||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||||
let (cols, _) = get_win_size(STDIN_FILENO);
|
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||||
let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16;
|
let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16;
|
||||||
Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line)
|
Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|||||||
use vte::{Parser, Perform};
|
use vte::{Parser, Perform};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::{error::{ShErr, ShErrKind, ShResult}, sys::TTY_FILENO},
|
||||||
prompt::readline::keys::{KeyCode, ModKeys},
|
prompt::readline::keys::{KeyCode, ModKeys},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -30,7 +30,7 @@ use crate::{
|
|||||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||||
|
|
||||||
pub fn raw_mode() -> RawModeGuard {
|
pub fn raw_mode() -> RawModeGuard {
|
||||||
let orig = termios::tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) })
|
let orig = termios::tcgetattr(unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) })
|
||||||
.expect("Failed to get terminal attributes");
|
.expect("Failed to get terminal attributes");
|
||||||
let mut raw = orig.clone();
|
let mut raw = orig.clone();
|
||||||
termios::cfmakeraw(&mut raw);
|
termios::cfmakeraw(&mut raw);
|
||||||
@@ -39,17 +39,17 @@ pub fn raw_mode() -> RawModeGuard {
|
|||||||
// Keep OPOST enabled so \n is translated to \r\n on output
|
// Keep OPOST enabled so \n is translated to \r\n on output
|
||||||
raw.output_flags |= termios::OutputFlags::OPOST;
|
raw.output_flags |= termios::OutputFlags::OPOST;
|
||||||
termios::tcsetattr(
|
termios::tcsetattr(
|
||||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
|
||||||
termios::SetArg::TCSANOW,
|
termios::SetArg::TCSANOW,
|
||||||
&raw,
|
&raw,
|
||||||
)
|
)
|
||||||
.expect("Failed to set terminal to raw mode");
|
.expect("Failed to set terminal to raw mode");
|
||||||
|
|
||||||
let (cols, rows) = get_win_size(STDIN_FILENO);
|
let (cols, rows) = get_win_size(*TTY_FILENO);
|
||||||
|
|
||||||
RawModeGuard {
|
RawModeGuard {
|
||||||
orig,
|
orig,
|
||||||
fd: STDIN_FILENO,
|
fd: *TTY_FILENO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,14 +286,14 @@ impl RawModeGuard {
|
|||||||
where
|
where
|
||||||
F: FnOnce() -> R,
|
F: FnOnce() -> R,
|
||||||
{
|
{
|
||||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
let raw = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes");
|
||||||
let mut cooked = raw.clone();
|
let mut cooked = raw.clone();
|
||||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &cooked)
|
||||||
.expect("Failed to set cooked mode");
|
.expect("Failed to set cooked mode");
|
||||||
let res = f();
|
let res = f();
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &raw)
|
||||||
.expect("Failed to restore raw mode");
|
.expect("Failed to restore raw mode");
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -542,16 +542,10 @@ pub struct TermReader {
|
|||||||
buffer: BufReader<TermBuffer>,
|
buffer: BufReader<TermBuffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TermReader {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TermReader {
|
impl TermReader {
|
||||||
pub fn new() -> Self {
|
pub fn new(tty: RawFd) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer: BufReader::new(TermBuffer::new(1)),
|
buffer: BufReader::new(TermBuffer::new(tty)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ fn marker_priority_reset_placement() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn highlighter_produces_ansi_codes() {
|
fn highlighter_produces_ansi_codes() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("echo hello");
|
highlighter.load_input("echo hello", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ fn highlighter_produces_ansi_codes() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn highlighter_handles_empty_input() {
|
fn highlighter_handles_empty_input() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("");
|
highlighter.load_input("", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
@@ -397,12 +397,12 @@ fn highlighter_command_validation() {
|
|||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
|
|
||||||
// Valid command (echo exists)
|
// Valid command (echo exists)
|
||||||
highlighter.load_input("echo test");
|
highlighter.load_input("echo test", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let valid_output = highlighter.take();
|
let valid_output = highlighter.take();
|
||||||
|
|
||||||
// Invalid command (definitely doesn't exist)
|
// Invalid command (definitely doesn't exist)
|
||||||
highlighter.load_input("xyznotacommand123 test");
|
highlighter.load_input("xyznotacommand123 test", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let invalid_output = highlighter.take();
|
let invalid_output = highlighter.take();
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ fn highlighter_command_validation() {
|
|||||||
fn highlighter_preserves_text_content() {
|
fn highlighter_preserves_text_content() {
|
||||||
let input = "echo hello world";
|
let input = "echo hello world";
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input(input);
|
highlighter.load_input(input, 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ fn highlighter_preserves_text_content() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn highlighter_multiple_tokens() {
|
fn highlighter_multiple_tokens() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("ls -la | grep foo");
|
highlighter.load_input("ls -la | grep foo", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ fn highlighter_multiple_tokens() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn highlighter_string_with_variable() {
|
fn highlighter_string_with_variable() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input(r#"echo "hello $USER""#);
|
highlighter.load_input(r#"echo "hello $USER""#, 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
@@ -474,12 +474,12 @@ fn highlighter_reusable() {
|
|||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
|
|
||||||
// First input
|
// First input
|
||||||
highlighter.load_input("echo first");
|
highlighter.load_input("echo first", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output1 = highlighter.take();
|
let output1 = highlighter.take();
|
||||||
|
|
||||||
// Second input (reusing same highlighter)
|
// Second input (reusing same highlighter)
|
||||||
highlighter.load_input("echo second");
|
highlighter.load_input("echo second", 0);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output2 = highlighter.take();
|
let output2 = highlighter.take();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::state::{LogTab, ScopeStack, ShellParam, VarFlags, VarTab};
|
use std::path::PathBuf;
|
||||||
|
use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarTab};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ScopeStack Tests - Variable Scoping
|
// ScopeStack Tests - Variable Scoping
|
||||||
@@ -593,3 +594,239 @@ fn shellparam_display() {
|
|||||||
assert_eq!(ShellParam::Pos(1).to_string(), "1");
|
assert_eq!(ShellParam::Pos(1).to_string(), "1");
|
||||||
assert_eq!(ShellParam::Pos(99).to_string(), "99");
|
assert_eq!(ShellParam::Pos(99).to_string(), "99");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MetaTab Directory Stack Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_push_pop() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
|
||||||
|
// push_front means /var is on top, /tmp is below
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/var")));
|
||||||
|
|
||||||
|
let popped = meta.pop_dir();
|
||||||
|
assert_eq!(popped, Some(PathBuf::from("/var")));
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/tmp")));
|
||||||
|
|
||||||
|
let popped = meta.pop_dir();
|
||||||
|
assert_eq!(popped, Some(PathBuf::from("/tmp")));
|
||||||
|
assert_eq!(meta.pop_dir(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_empty() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
assert_eq!(meta.dir_stack_top(), None);
|
||||||
|
assert_eq!(meta.pop_dir(), None);
|
||||||
|
assert!(meta.dirs().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_rotate_fwd() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Build stack: front=[A, B, C, D]=back
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/a"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/b"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/c"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/d"));
|
||||||
|
|
||||||
|
// rotate_left(1): [B, C, D, A]
|
||||||
|
meta.rotate_dirs_fwd(1);
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/b")));
|
||||||
|
assert_eq!(meta.dirs().back(), Some(&PathBuf::from("/a")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_rotate_bkwd() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Build stack: front=[A, B, C, D]=back
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/a"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/b"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/c"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/d"));
|
||||||
|
|
||||||
|
// rotate_right(1): [D, A, B, C]
|
||||||
|
meta.rotate_dirs_bkwd(1);
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/d")));
|
||||||
|
assert_eq!(meta.dirs().back(), Some(&PathBuf::from("/c")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_rotate_zero_is_noop() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/a"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/b"));
|
||||||
|
meta.dirs_mut().push_back(PathBuf::from("/c"));
|
||||||
|
|
||||||
|
meta.rotate_dirs_fwd(0);
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/a")));
|
||||||
|
|
||||||
|
meta.rotate_dirs_bkwd(0);
|
||||||
|
assert_eq!(meta.dir_stack_top(), Some(&PathBuf::from("/a")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_pushd_rotation_with_cwd() {
|
||||||
|
// Simulates what pushd +N does: insert cwd, rotate, pop new top
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Stored stack: [/tmp, /var, /etc]
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// pushd +2 with cwd=/home:
|
||||||
|
// push_front(cwd): [/home, /tmp, /var, /etc]
|
||||||
|
// rotate_left(2): [/var, /etc, /home, /tmp]
|
||||||
|
// pop_front(): /var = new cwd
|
||||||
|
let cwd = PathBuf::from("/home");
|
||||||
|
let dirs = meta.dirs_mut();
|
||||||
|
dirs.push_front(cwd);
|
||||||
|
dirs.rotate_left(2);
|
||||||
|
let new_cwd = dirs.pop_front();
|
||||||
|
|
||||||
|
assert_eq!(new_cwd, Some(PathBuf::from("/var")));
|
||||||
|
let remaining: Vec<_> = meta.dirs().iter().collect();
|
||||||
|
assert_eq!(remaining, vec![
|
||||||
|
&PathBuf::from("/etc"),
|
||||||
|
&PathBuf::from("/home"),
|
||||||
|
&PathBuf::from("/tmp"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_pushd_minus_zero_with_cwd() {
|
||||||
|
// pushd -0: bring bottom to top
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Stored stack: [/tmp, /var, /etc]
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// pushd -0 with cwd=/home:
|
||||||
|
// push_front(cwd): [/home, /tmp, /var, /etc]
|
||||||
|
// rotate_right(0+1=1): [/etc, /home, /tmp, /var]
|
||||||
|
// pop_front(): /etc = new cwd
|
||||||
|
let cwd = PathBuf::from("/home");
|
||||||
|
let dirs = meta.dirs_mut();
|
||||||
|
dirs.push_front(cwd);
|
||||||
|
dirs.rotate_right(1);
|
||||||
|
let new_cwd = dirs.pop_front();
|
||||||
|
|
||||||
|
assert_eq!(new_cwd, Some(PathBuf::from("/etc")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_pushd_plus_zero_noop() {
|
||||||
|
// pushd +0: should be a no-op (cwd stays the same)
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// pushd +0 with cwd=/home:
|
||||||
|
// push_front(cwd): [/home, /tmp, /var, /etc]
|
||||||
|
// rotate_left(0): no-op
|
||||||
|
// pop_front(): /home = cwd unchanged
|
||||||
|
let cwd = PathBuf::from("/home");
|
||||||
|
let dirs = meta.dirs_mut();
|
||||||
|
dirs.push_front(cwd.clone());
|
||||||
|
dirs.rotate_left(0);
|
||||||
|
let new_cwd = dirs.pop_front();
|
||||||
|
|
||||||
|
assert_eq!(new_cwd, Some(PathBuf::from("/home")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_popd_removes_from_top() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// popd (no args) or popd +0: pop from front
|
||||||
|
let popped = meta.pop_dir();
|
||||||
|
assert_eq!(popped, Some(PathBuf::from("/tmp")));
|
||||||
|
assert_eq!(meta.dirs().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_popd_plus_n_offset() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Stored: [/tmp, /var, /etc] (front to back)
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// popd +2: full stack is [cwd, /tmp, /var, /etc]
|
||||||
|
// +2 = /var, which is stored index 1 (n-1 = 2-1 = 1)
|
||||||
|
let removed = meta.dirs_mut().remove(1); // n-1 for +N
|
||||||
|
assert_eq!(removed, Some(PathBuf::from("/var")));
|
||||||
|
|
||||||
|
let remaining: Vec<_> = meta.dirs().iter().collect();
|
||||||
|
assert_eq!(remaining, vec![
|
||||||
|
&PathBuf::from("/tmp"),
|
||||||
|
&PathBuf::from("/etc"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_popd_minus_zero() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Stored: [/tmp, /var, /etc]
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// popd -0: remove bottom (back)
|
||||||
|
// actual = len - 1 - 0 = 2, via checked_sub(0+1) = checked_sub(1) = 2
|
||||||
|
let len = meta.dirs().len();
|
||||||
|
let actual = len.checked_sub(1).unwrap();
|
||||||
|
let removed = meta.dirs_mut().remove(actual);
|
||||||
|
assert_eq!(removed, Some(PathBuf::from("/etc")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_popd_minus_n() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
// Stored: [/tmp, /var, /etc, /usr]
|
||||||
|
meta.push_dir(PathBuf::from("/usr"));
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
|
||||||
|
// popd -1: second from bottom = /etc
|
||||||
|
// actual = len - (1+1) = 4 - 2 = 2
|
||||||
|
let len = meta.dirs().len();
|
||||||
|
let actual = len.checked_sub(2).unwrap(); // n+1 = 2
|
||||||
|
let removed = meta.dirs_mut().remove(actual);
|
||||||
|
assert_eq!(removed, Some(PathBuf::from("/etc")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dirstack_clear() {
|
||||||
|
let mut meta = MetaTab::new();
|
||||||
|
|
||||||
|
meta.push_dir(PathBuf::from("/tmp"));
|
||||||
|
meta.push_dir(PathBuf::from("/var"));
|
||||||
|
meta.push_dir(PathBuf::from("/etc"));
|
||||||
|
|
||||||
|
meta.dirs_mut().clear();
|
||||||
|
assert!(meta.dirs().is_empty());
|
||||||
|
assert_eq!(meta.dir_stack_top(), None);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user