use crate::builtins::{handle_builtin, BuiltinResult};
use crate::colors::red;
use crate::functions::Functions;
use crate::history::HistoryManager;
use crate::parser::Command;
use crate::ui;
use crate::variables::Variables;
pub enum ReadlineEvent {
Line(String),
Interrupted,
Eof,
Other,
}
pub trait LineEditor {
fn readline(&mut self, prompt: &str) -> ReadlineEvent;
fn add_history_entry(&mut self, entry: &str);
}
pub trait ExecutorTrait {
fn execute(
&self,
cmd: &Command,
vars: &mut Variables,
functions: &mut Functions,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
oldpwd: &mut Option<String>,
) -> Result<(), String>;
fn execute_pipeline(
&self,
pipeline: &[Command],
vars: &mut Variables,
functions: &mut Functions,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
oldpwd: &mut Option<String>,
) -> Result<(), String>;
}
pub struct RealExecutor;
impl ExecutorTrait for RealExecutor {
fn execute(
&self,
cmd: &Command,
vars: &mut Variables,
functions: &mut Functions,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
oldpwd: &mut Option<String>,
) -> Result<(), String> {
crate::executor::Executor::execute(
cmd,
vars,
functions,
history_mgr,
command_history,
oldpwd,
)
}
fn execute_pipeline(
&self,
pipeline: &[Command],
vars: &mut Variables,
functions: &mut Functions,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
oldpwd: &mut Option<String>,
) -> Result<(), String> {
crate::executor::Executor::execute_pipeline(
pipeline,
vars,
functions,
history_mgr,
command_history,
oldpwd,
)
}
}
#[allow(dead_code)]
pub struct NoOpEditor;
impl LineEditor for NoOpEditor {
fn readline(&mut self, _prompt: &str) -> ReadlineEvent {
ReadlineEvent::Eof
}
fn add_history_entry(&mut self, _entry: &str) {}
}
#[allow(clippy::too_many_arguments)]
pub fn execute_line<E: ExecutorTrait, L: LineEditor>(
line: &str,
editor: &mut L,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
executor: &E,
oldpwd: &mut Option<String>,
vars: &mut Variables,
functions: &mut Functions,
) -> bool {
editor.add_history_entry(line);
if let Some(pipeline) = Command::parse_pipeline(line) {
return execute_pipeline_struct(
&pipeline,
history_mgr,
command_history,
executor,
oldpwd,
vars,
functions,
);
}
true
}
pub fn execute_pipeline_struct<E: ExecutorTrait>(
pipeline: &[Command],
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
executor: &E,
oldpwd: &mut Option<String>,
vars: &mut Variables,
functions: &mut Functions,
) -> bool {
if pipeline.len() == 1 {
let cmd = &pipeline[0];
let builtin_res = if let Command::Simple(simple) = cmd {
handle_builtin(simple, history_mgr, command_history, oldpwd, vars)
} else {
Ok(BuiltinResult::NotHandled)
};
match builtin_res {
Ok(BuiltinResult::HandledExit(code)) => std::process::exit(code),
Ok(BuiltinResult::HandledContinue) => return true,
Ok(BuiltinResult::SourceFile(path)) => {
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("pmsh: source: {}: {}", path, e);
return true;
}
};
match Command::parse_script(&contents) {
Ok(pipelines) => {
for pipeline in pipelines {
if !execute_pipeline_struct(
&pipeline,
history_mgr,
command_history,
executor,
oldpwd,
vars,
functions,
) {
return false;
}
}
}
Err(e) => {
eprintln!("pmsh: source: error parsing script: {}", e);
}
}
return true;
}
Ok(BuiltinResult::NotHandled) => {
match executor.execute(cmd, vars, functions, history_mgr, command_history, oldpwd) {
Ok(()) => {
}
Err(e) => eprintln!("pmsh: {}", red(&e.to_string())),
}
}
Err(e) => eprintln!("Builtin error: {}", red(&e.to_string())),
}
} else {
match executor.execute_pipeline(
pipeline,
vars,
functions,
history_mgr,
command_history,
oldpwd,
) {
Ok(()) => {
}
Err(e) => eprintln!("pmsh: {}", red(&e.to_string())),
}
}
true
}
pub fn run_repl_with_state<E: ExecutorTrait, L: LineEditor>(
editor: &mut L,
history_mgr: &HistoryManager,
command_history: &mut Vec<String>,
executor: &E,
mut oldpwd: Option<String>,
mut vars: Variables,
mut functions: Functions,
) {
loop {
let event = editor.readline(&ui::format_prompt());
match event {
ReadlineEvent::Line(line) => {
if !execute_line(
&line,
editor,
history_mgr,
command_history,
executor,
&mut oldpwd,
&mut vars,
&mut functions,
) {
break;
}
}
ReadlineEvent::Interrupted => {
println!("^C");
continue;
}
ReadlineEvent::Eof => {
if let Err(e) = history_mgr.save(command_history) {
eprintln!("Warning: Could not save history: {}", e);
}
println!("^D");
break;
}
ReadlineEvent::Other => {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockEditor {
events: std::collections::VecDeque<ReadlineEvent>,
history: Vec<String>,
}
impl MockEditor {
fn new(events: Vec<ReadlineEvent>) -> Self {
Self {
events: events.into(),
history: Vec::new(),
}
}
}
impl LineEditor for MockEditor {
fn readline(&mut self, _prompt: &str) -> ReadlineEvent {
self.events.pop_front().unwrap_or(ReadlineEvent::Eof)
}
fn add_history_entry(&mut self, entry: &str) {
self.history.push(entry.to_string());
}
}
struct MockExecutor {
calls: std::cell::RefCell<Vec<Command>>,
}
impl MockExecutor {
fn new() -> Self {
Self {
calls: Default::default(),
}
}
}
impl ExecutorTrait for MockExecutor {
fn execute(
&self,
cmd: &Command,
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
self.calls.borrow_mut().push(cmd.clone());
Ok(())
}
fn execute_pipeline(
&self,
pipeline: &[Command],
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
for cmd in pipeline {
self.calls.borrow_mut().push(cmd.clone());
}
Ok(())
}
}
#[test]
fn test_repl_executes_command_and_exits_on_eof() {
let events = vec![
ReadlineEvent::Line("echo hello".to_string()),
ReadlineEvent::Eof,
];
let mut editor = MockEditor::new(events);
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&executor,
None,
Variables::new(),
Functions::new(),
);
let calls = executor.calls.borrow();
assert_eq!(calls.len(), 1);
if let Command::Simple(cmd) = &calls[0] {
assert_eq!(cmd.name, "echo");
assert_eq!(cmd.args, vec!["hello".to_string()]);
} else {
panic!("Expected Simple command");
}
}
#[test]
fn test_repl_executes_pipeline() {
let events = vec![
ReadlineEvent::Line("echo hello | wc -w".to_string()),
ReadlineEvent::Eof,
];
let mut editor = MockEditor::new(events);
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&executor,
None,
Variables::new(),
Functions::new(),
);
let calls = executor.calls.borrow();
assert_eq!(calls.len(), 2);
if let Command::Simple(cmd) = &calls[0] {
assert_eq!(cmd.name, "echo");
assert_eq!(cmd.args, vec!["hello".to_string()]);
} else {
panic!("Expected Simple command");
}
if let Command::Simple(cmd) = &calls[1] {
assert_eq!(cmd.name, "wc");
assert_eq!(cmd.args, vec!["-w".to_string()]);
} else {
panic!("Expected Simple command");
}
}
#[test]
#[serial_test::serial]
fn test_repl_builtins_flow() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp_path = tmp.path().to_string_lossy().to_string();
let events = vec![
ReadlineEvent::Line(format!("cd {}", tmp_path)),
ReadlineEvent::Line("history".to_string()),
ReadlineEvent::Line("exit".to_string()),
];
let mut editor = MockEditor::new(events);
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let orig = std::env::current_dir().unwrap();
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&executor,
None,
Variables::new(),
Functions::new(),
);
assert!(history.iter().any(|h| h.starts_with("cd ")));
let _ = std::env::set_current_dir(orig);
}
#[test]
#[serial_test::serial]
fn test_repl_executor_error_does_not_save_history() {
struct FailingExecutor;
impl ExecutorTrait for FailingExecutor {
fn execute(
&self,
_cmd: &Command,
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
Err("execution failed".to_string())
}
fn execute_pipeline(
&self,
_pipeline: &[Command],
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
Err("pipeline failed".to_string())
}
}
let events = vec![
ReadlineEvent::Line("nonexistent arg".to_string()),
ReadlineEvent::Eof,
];
let mut editor = MockEditor::new(events);
let tmp_home = tempfile::TempDir::new().unwrap();
let original = std::env::var("HOME").ok();
std::env::set_var("HOME", tmp_home.path().to_string_lossy().as_ref());
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let exec = FailingExecutor;
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&exec,
None,
Variables::new(),
Functions::new(),
);
assert!(history.is_empty());
match original {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
}
#[test]
fn test_repl_interrupted_event() {
let events = vec![
ReadlineEvent::Interrupted,
ReadlineEvent::Line("echo fine".to_string()),
ReadlineEvent::Eof,
];
let mut editor = MockEditor::new(events);
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&executor,
None,
Variables::new(),
Functions::new(),
);
let calls = executor.calls.borrow();
assert_eq!(calls.len(), 1);
if let Command::Simple(cmd) = &calls[0] {
assert_eq!(cmd.name, "echo");
} else {
panic!("Expected Simple command");
}
}
#[test]
fn test_repl_other_event() {
let events = vec![
ReadlineEvent::Other,
ReadlineEvent::Line("echo never_reached".to_string()),
];
let mut editor = MockEditor::new(events);
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
run_repl_with_state(
&mut editor,
&mgr,
&mut history,
&executor,
None,
Variables::new(),
Functions::new(),
);
let calls = executor.calls.borrow();
assert!(calls.is_empty()); }
#[test]
fn test_execute_line_empty() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let mut editor = MockEditor::new(vec![]);
let result = execute_line(
"",
&mut editor,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
assert!(executor.calls.borrow().is_empty());
}
#[test]
#[serial_test::serial]
fn test_execute_line_source_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let tmp_path = tmp.path().to_string_lossy().to_string();
std::fs::write(&tmp_path, "echo from_source\n").unwrap();
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let mut editor = MockEditor::new(vec![]);
let line = format!("source {}", tmp_path);
let result = execute_line(
&line,
&mut editor,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
let calls = executor.calls.borrow();
assert_eq!(calls.len(), 1);
if let Command::Simple(c) = &calls[0] {
assert_eq!(c.name, "echo");
} else {
panic!("Expected simple command");
}
}
#[test]
fn test_execute_line_source_file_not_found() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let mut editor = MockEditor::new(vec![]);
let result = execute_line(
"source /nonexistent/file.sh",
&mut editor,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
assert!(executor.calls.borrow().is_empty());
}
#[test]
fn test_execute_pipeline_struct_non_simple() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let pipeline = vec![Command::Subshell(vec![vec![Command::Simple(
crate::parser::SimpleCommand {
name: "echo".into(),
args: vec!["subshell_test".into()],
assignments: vec![],
},
)]])];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
}
#[test]
#[serial_test::serial]
fn test_execute_pipeline_struct_source() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let tmp_path = tmp.path().to_string_lossy().to_string();
std::fs::write(&tmp_path, "echo sourced_cmd\n").unwrap();
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "source".into(),
args: vec![tmp_path],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
let calls = executor.calls.borrow();
assert_eq!(calls.len(), 1);
if let Command::Simple(c) = &calls[0] {
assert_eq!(c.name, "echo");
} else {
panic!("Expected simple command from sourced file");
}
}
#[test]
fn test_execute_pipeline_struct_source_not_found() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "source".into(),
args: vec!["/nonexistent/path.sh".to_string()],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
assert!(executor.calls.borrow().is_empty());
}
#[test]
fn test_execute_pipeline_struct_executor_error() {
struct FailingExecutor;
impl ExecutorTrait for FailingExecutor {
fn execute(
&self,
_cmd: &Command,
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
Err("command failed".to_string())
}
fn execute_pipeline(
&self,
_pipeline: &[Command],
_vars: &mut Variables,
_functions: &mut Functions,
_history_mgr: &HistoryManager,
_command_history: &mut Vec<String>,
_oldpwd: &mut Option<String>,
) -> Result<(), String> {
Err("pipeline failed".to_string())
}
}
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = FailingExecutor;
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "some_cmd".into(),
args: vec![],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
let multi_pipeline = vec![
Command::Simple(crate::parser::SimpleCommand {
name: "cmd1".into(),
args: vec![],
assignments: vec![],
}),
Command::Simple(crate::parser::SimpleCommand {
name: "cmd2".into(),
args: vec![],
assignments: vec![],
}),
];
let result = execute_pipeline_struct(
&multi_pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
}
#[test]
fn test_execute_pipeline_struct_builtins_through_pipeline() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "complete".into(),
args: vec!["-W".into(), "foo bar".into(), "mycmd".into()],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "compgen".into(),
args: vec!["-W".into(), "hello world".into(), "--".into(), "hel".into()],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
let pipeline = vec![Command::Simple(crate::parser::SimpleCommand {
name: "version".into(),
args: vec![],
assignments: vec![],
})];
let result = execute_pipeline_struct(
&pipeline,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
}
#[test]
fn test_execute_line_with_builtin_handled_continue() {
let mgr = HistoryManager::new().unwrap_or_else(|_| HistoryManager::default());
let mut history: Vec<String> = Vec::new();
let executor = MockExecutor::new();
let mut oldpwd = None;
let mut vars = Variables::new();
let mut functions = Functions::new();
let mut editor = MockEditor::new(vec![]);
let result = execute_line(
"complete -W \"start stop\" myservice",
&mut editor,
&mgr,
&mut history,
&executor,
&mut oldpwd,
&mut vars,
&mut functions,
);
assert!(result);
assert!(executor.calls.borrow().is_empty());
}
}