Implemented the exec builtin
Fixed readline and terminal interactions using stdin instead of /dev/tty
This commit is contained in:
@@ -158,7 +158,7 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
||||
let dirs = m.dirs_mut();
|
||||
dirs.push_front(cwd);
|
||||
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),
|
||||
}
|
||||
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 zoltraak;
|
||||
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",
|
||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||
"pushd", "popd", "dirs"
|
||||
"pushd", "popd", "dirs", "exec",
|
||||
];
|
||||
|
||||
/// Sets up a builtin command
|
||||
|
||||
@@ -829,6 +829,11 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
||||
|
||||
match unsafe { fork()? } {
|
||||
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 io_frame = IoFrame::from_redir(redir);
|
||||
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 } => {
|
||||
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
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@@ -198,6 +198,11 @@ impl JobTab {
|
||||
let registered_fd = RegisteredFd { fd, owner_pid };
|
||||
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) {
|
||||
while let Some(job) = self.jobs.last() {
|
||||
if job.is_none() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use termios::{LocalFlags, Termios};
|
||||
|
||||
use crate::prelude::*;
|
||||
@@ -31,6 +33,11 @@ use crate::prelude::*;
|
||||
/// lifecycle could lead to undefined behavior.
|
||||
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)]
|
||||
pub struct TermiosGuard {
|
||||
saved_termios: Option<Termios>,
|
||||
@@ -42,7 +49,7 @@ impl TermiosGuard {
|
||||
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();
|
||||
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::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::prelude::*;
|
||||
use crate::prompt::get_prompt;
|
||||
@@ -51,6 +52,12 @@ struct FernArgs {
|
||||
|
||||
#[arg(long)]
|
||||
version: bool,
|
||||
|
||||
#[arg(short)]
|
||||
interactive: bool,
|
||||
|
||||
#[arg(long,short)]
|
||||
login_shell: bool,
|
||||
}
|
||||
|
||||
/// Force evaluation of lazily-initialized values early in shell startup.
|
||||
@@ -70,7 +77,12 @@ fn kickstart_lazy_evals() {
|
||||
fn main() -> ExitCode {
|
||||
env_logger::init();
|
||||
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 {
|
||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
||||
return ExitCode::SUCCESS;
|
||||
@@ -134,7 +146,7 @@ fn fern_interactive() -> ShResult<()> {
|
||||
}
|
||||
|
||||
// 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,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize readline: {e}");
|
||||
@@ -169,7 +181,7 @@ fn fern_interactive() -> ShResult<()> {
|
||||
|
||||
// Poll for stdin input
|
||||
let mut fds = [PollFd::new(
|
||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
||||
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
|
||||
PollFlags::POLLIN,
|
||||
)];
|
||||
|
||||
@@ -191,7 +203,7 @@ fn fern_interactive() -> ShResult<()> {
|
||||
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||
{
|
||||
let mut buffer = [0u8; 1024];
|
||||
match read(STDIN_FILENO, &mut buffer) {
|
||||
match read(*TTY_FILENO, &mut buffer) {
|
||||
Ok(0) => {
|
||||
// EOF
|
||||
break;
|
||||
|
||||
@@ -2,14 +2,14 @@ use std::collections::{HashSet, VecDeque};
|
||||
|
||||
use crate::{
|
||||
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,
|
||||
jobs::{ChildProc, JobStack, dispatch_job},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
prelude::*,
|
||||
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::{
|
||||
@@ -80,6 +80,10 @@ impl ExecArgs {
|
||||
pub fn new(argv: Vec<Tk>) -> ShResult<Self> {
|
||||
assert!(!argv.is_empty());
|
||||
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 argv = Self::get_argv(argv);
|
||||
let envp = Self::get_envp();
|
||||
@@ -603,6 +607,7 @@ impl Dispatcher {
|
||||
"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),
|
||||
_ => unimplemented!(
|
||||
"Have not yet added support for builtin '{}'",
|
||||
cmd_raw.span.as_str()
|
||||
@@ -663,6 +668,11 @@ impl Dispatcher {
|
||||
exit(e as i32)
|
||||
}
|
||||
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 child_pgid = if let Some(pgid) = job.pgid() {
|
||||
|
||||
@@ -39,6 +39,9 @@ pub enum IoMode {
|
||||
buf: String,
|
||||
pipe: Arc<OwnedFd>,
|
||||
},
|
||||
Close {
|
||||
tgt_fd: RawFd,
|
||||
}
|
||||
}
|
||||
|
||||
impl IoMode {
|
||||
@@ -152,6 +155,21 @@ impl<R: Read> IoBuf<R> {
|
||||
}
|
||||
|
||||
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 {
|
||||
fn drop(&mut self) {
|
||||
self.0.restore().ok();
|
||||
|
||||
@@ -263,6 +263,9 @@ impl Completer {
|
||||
if let Some(eq_pos) = token_str.rfind('=') {
|
||||
// Adjust span to only replace the part after '='
|
||||
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) {
|
||||
|
||||
@@ -824,7 +824,7 @@ impl LineBuf {
|
||||
dir: Direction,
|
||||
) -> Box<dyn Iterator<Item = usize>> {
|
||||
self.update_graphemes_lazy();
|
||||
let skip = if pos == 0 { 0 } else { pos + 1 };
|
||||
let skip = pos + 1;
|
||||
match dir {
|
||||
Direction::Forward => Box::new(self.grapheme_indices().to_vec().into_iter().skip(skip))
|
||||
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 vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
libsh::{
|
||||
@@ -112,10 +113,10 @@ pub struct 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 {
|
||||
reader: PollReader::new(),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
writer: Box::new(TermWriter::new(tty)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
completer: Completer::new(),
|
||||
highlighter: Highlighter::new(),
|
||||
@@ -294,7 +295,7 @@ impl FernVi {
|
||||
|
||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||
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;
|
||||
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 crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
libsh::{error::{ShErr, ShErrKind, ShResult}, sys::TTY_FILENO},
|
||||
prompt::readline::keys::{KeyCode, ModKeys},
|
||||
};
|
||||
use crate::{
|
||||
@@ -30,7 +30,7 @@ use crate::{
|
||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||
|
||||
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");
|
||||
let mut raw = orig.clone();
|
||||
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
|
||||
raw.output_flags |= termios::OutputFlags::OPOST;
|
||||
termios::tcsetattr(
|
||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
||||
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
|
||||
termios::SetArg::TCSANOW,
|
||||
&raw,
|
||||
)
|
||||
.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 {
|
||||
orig,
|
||||
fd: STDIN_FILENO,
|
||||
fd: *TTY_FILENO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,14 +286,14 @@ impl RawModeGuard {
|
||||
where
|
||||
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();
|
||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||
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");
|
||||
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");
|
||||
res
|
||||
}
|
||||
@@ -542,16 +542,10 @@ pub struct TermReader {
|
||||
buffer: BufReader<TermBuffer>,
|
||||
}
|
||||
|
||||
impl Default for TermReader {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TermReader {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(tty: RawFd) -> Self {
|
||||
Self {
|
||||
buffer: BufReader::new(TermBuffer::new(1)),
|
||||
buffer: BufReader::new(TermBuffer::new(tty)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ fn marker_priority_reset_placement() {
|
||||
#[test]
|
||||
fn highlighter_produces_ansi_codes() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
highlighter.load_input("echo hello");
|
||||
highlighter.load_input("echo hello", 0);
|
||||
highlighter.highlight();
|
||||
let output = highlighter.take();
|
||||
|
||||
@@ -384,7 +384,7 @@ fn highlighter_produces_ansi_codes() {
|
||||
#[test]
|
||||
fn highlighter_handles_empty_input() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
highlighter.load_input("");
|
||||
highlighter.load_input("", 0);
|
||||
highlighter.highlight();
|
||||
let output = highlighter.take();
|
||||
|
||||
@@ -397,12 +397,12 @@ fn highlighter_command_validation() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
// Valid command (echo exists)
|
||||
highlighter.load_input("echo test");
|
||||
highlighter.load_input("echo test", 0);
|
||||
highlighter.highlight();
|
||||
let valid_output = highlighter.take();
|
||||
|
||||
// Invalid command (definitely doesn't exist)
|
||||
highlighter.load_input("xyznotacommand123 test");
|
||||
highlighter.load_input("xyznotacommand123 test", 0);
|
||||
highlighter.highlight();
|
||||
let invalid_output = highlighter.take();
|
||||
|
||||
@@ -419,7 +419,7 @@ fn highlighter_command_validation() {
|
||||
fn highlighter_preserves_text_content() {
|
||||
let input = "echo hello world";
|
||||
let mut highlighter = Highlighter::new();
|
||||
highlighter.load_input(input);
|
||||
highlighter.load_input(input, 0);
|
||||
highlighter.highlight();
|
||||
let output = highlighter.take();
|
||||
|
||||
@@ -438,7 +438,7 @@ fn highlighter_preserves_text_content() {
|
||||
#[test]
|
||||
fn highlighter_multiple_tokens() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
highlighter.load_input("ls -la | grep foo");
|
||||
highlighter.load_input("ls -la | grep foo", 0);
|
||||
highlighter.highlight();
|
||||
let output = highlighter.take();
|
||||
|
||||
@@ -456,7 +456,7 @@ fn highlighter_multiple_tokens() {
|
||||
#[test]
|
||||
fn highlighter_string_with_variable() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
highlighter.load_input(r#"echo "hello $USER""#);
|
||||
highlighter.load_input(r#"echo "hello $USER""#, 0);
|
||||
highlighter.highlight();
|
||||
let output = highlighter.take();
|
||||
|
||||
@@ -474,12 +474,12 @@ fn highlighter_reusable() {
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
// First input
|
||||
highlighter.load_input("echo first");
|
||||
highlighter.load_input("echo first", 0);
|
||||
highlighter.highlight();
|
||||
let output1 = highlighter.take();
|
||||
|
||||
// Second input (reusing same highlighter)
|
||||
highlighter.load_input("echo second");
|
||||
highlighter.load_input("echo second", 0);
|
||||
highlighter.highlight();
|
||||
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
|
||||
@@ -593,3 +594,239 @@ fn shellparam_display() {
|
||||
assert_eq!(ShellParam::Pos(1).to_string(), "1");
|
||||
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