use crate::repl::{
completion::ReplCompleter,
help::show_help,
loader::LoadResult,
logic::{
get_history_path, process_command_by_mode, process_functions_command,
process_history_command, process_lint_command, process_load_command, process_mode_command,
process_parse_command, process_purify_command, process_reload_command,
process_source_command, process_vars_command,
},
multiline::is_incomplete,
variables::parse_assignment,
ReplConfig, ReplState,
};
use anyhow::Result;
use rustyline::config::Config;
use rustyline::history::DefaultHistory;
use rustyline::Editor;
use std::path::PathBuf;
pub fn run_repl(config: ReplConfig) -> Result<()> {
config.validate().map_err(|e| anyhow::anyhow!(e))?;
let rustyline_config = Config::builder()
.history_ignore_dups(config.history_ignore_dups)?
.history_ignore_space(config.history_ignore_space)
.max_history_size(config.max_history)?
.auto_add_history(true)
.build();
let completer = ReplCompleter::new();
let mut editor = Editor::with_config(rustyline_config)?;
editor.set_helper(Some(completer));
let mut state = ReplState::new();
let history_path = config
.history_path
.clone()
.unwrap_or_else(|| get_history_path().unwrap_or_else(|_| PathBuf::from(".bashrs_history")));
if history_path.exists() {
let _ = editor.load_history(&history_path);
}
println!("bashrs REPL v{}", env!("CARGO_PKG_VERSION"));
println!("Type 'quit' or 'exit' to exit, 'help' for commands");
println!("Tip: Use Up/Down arrows for history, Ctrl-R for reverse search");
println!(
"Current mode: {} - {}",
state.mode(),
state.mode().description()
);
let mut multiline_buffer = String::new();
loop {
let prompt = if multiline_buffer.is_empty() {
format!("bashrs [{}]> ", state.mode())
} else {
"... > ".to_string()
};
match editor.readline(&prompt) {
Ok(line) => {
match process_repl_line(&line, &mut multiline_buffer, &mut state, &mut editor) {
LineAction::Continue => continue,
LineAction::Break => break,
LineAction::Next => {}
}
}
Err(rustyline::error::ReadlineError::Interrupted) => {
handle_interrupt(&mut multiline_buffer);
continue;
}
Err(rustyline::error::ReadlineError::Eof) => {
println!("EOF");
break;
}
Err(err) => {
return Err(anyhow::anyhow!("REPL error: {}", err));
}
}
}
let _ = editor.save_history(&history_path);
Ok(())
}
enum LineAction {
Continue,
Break,
Next,
}
fn process_repl_line(
line: &str,
multiline_buffer: &mut String,
state: &mut ReplState,
editor: &mut Editor<ReplCompleter, DefaultHistory>,
) -> LineAction {
let trimmed_line = line.trim();
if trimmed_line.is_empty() {
if !multiline_buffer.is_empty() {
multiline_buffer.push('\n');
}
return LineAction::Continue;
}
if !multiline_buffer.is_empty() {
multiline_buffer.push('\n');
}
multiline_buffer.push_str(line);
if is_incomplete(multiline_buffer) {
return LineAction::Continue;
}
let complete_input = multiline_buffer.clone();
multiline_buffer.clear();
let _ = editor.add_history_entry(&complete_input);
state.add_history(complete_input.clone());
let trimmed = complete_input.trim();
if let Some((name, value)) = parse_assignment(trimmed) {
state.set_variable(name.clone(), value.clone());
println!("\u{2713} Variable set: {} = {}", name, value);
return LineAction::Continue;
}
if dispatch_repl_command(trimmed, state) {
return LineAction::Break;
}
LineAction::Next
}
fn handle_interrupt(multiline_buffer: &mut String) {
if !multiline_buffer.is_empty() {
println!("^C (multi-line input cancelled)");
multiline_buffer.clear();
} else {
println!("^C");
}
}
fn dispatch_repl_command(line: &str, state: &mut ReplState) -> bool {
if line.starts_with(':') {
dispatch_colon_command(line, state);
return false;
}
if line == "quit" || line == "exit" {
println!("Goodbye!");
return true;
}
if line == "help" || line.starts_with("help ") || line.starts_with(":help") {
print!("{}", show_help(extract_help_topic(line)));
return false;
}
handle_command_by_mode(line, state);
false
}
fn dispatch_colon_command(line: &str, state: &mut ReplState) {
let cmd = line.split_whitespace().next().unwrap_or("");
match cmd {
":mode" => handle_mode_command(line, state),
":parse" => handle_parse_command(line),
":purify" => handle_purify_command(line),
":lint" => handle_lint_command(line),
":load" => handle_load_command(line, state),
":source" => handle_source_command(line, state),
":functions" => handle_functions_command(state),
":reload" => handle_reload_command(state),
":history" => handle_history_command(state),
":vars" => handle_vars_command(state),
":clear" => handle_clear_command(),
":help" => print!("{}", show_help(extract_help_topic(line))),
_ => println!(
"Unknown command: {}. Type 'help' for available commands.",
cmd
),
}
}
fn extract_help_topic(line: &str) -> Option<&str> {
if line.contains(' ') {
let parts: Vec<&str> = line.split_whitespace().collect();
parts.get(1).copied()
} else {
None
}
}
fn handle_mode_command(line: &str, state: &mut ReplState) {
let (result, new_mode) = process_mode_command(line, state);
println!("{}", result.format());
if let Some(mode) = new_mode {
state.set_mode(mode);
}
}
fn handle_parse_command(line: &str) {
let result = process_parse_command(line);
println!("{}", result.format());
}
fn handle_purify_command(line: &str) {
let result = process_purify_command(line);
println!("{}", result.format());
}
fn handle_lint_command(line: &str) {
let result = process_lint_command(line);
println!("{}", result.format());
}
fn handle_command_by_mode(line: &str, state: &ReplState) {
let result = process_command_by_mode(line, state);
let output = result.format();
if !output.is_empty() {
print!("{}", output);
if !output.ends_with('\n') {
println!();
}
}
}
fn handle_history_command(state: &ReplState) {
let result = process_history_command(state);
println!("{}", result.format());
}
fn handle_vars_command(state: &ReplState) {
let result = process_vars_command(state);
println!("{}", result.format());
}
fn handle_clear_command() {
print!("\x1B[2J\x1B[H");
}
fn handle_load_command(line: &str, state: &mut ReplState) {
let (result, load_result) = process_load_command(line);
println!("{}", result.format());
if let Some(LoadResult::Success(script)) = load_result {
state.set_last_loaded_script(script.path.clone());
state.clear_functions();
for func in &script.functions {
state.add_function(func.clone());
}
}
}
fn handle_source_command(line: &str, state: &mut ReplState) {
let (result, load_result) = process_source_command(line);
println!("{}", result.format());
if let Some(LoadResult::Success(script)) = load_result {
state.set_last_loaded_script(script.path.clone());
for func in &script.functions {
state.add_function(func.clone());
}
}
}
fn handle_functions_command(state: &ReplState) {
let result = process_functions_command(state);
println!("{}", result.format());
}
fn handle_reload_command(state: &mut ReplState) {
let (result, load_result) = process_reload_command(state);
println!("{}", result.format());
if let Some(LoadResult::Success(script)) = load_result {
state.clear_functions();
for func in &script.functions {
state.add_function(func.clone());
}
}
}
#[cfg(test)]
#[path = "loop_tests_repl_003.rs"]
mod tests_extracted;