Implemented the exec builtin

Fixed readline and terminal interactions using stdin instead of /dev/tty
This commit is contained in:
2026-02-20 12:17:48 -05:00
parent 9b8c8d704b
commit 88c0945e47
16 changed files with 384 additions and 41 deletions

View File

@@ -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();

View File

@@ -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);
}