pub mod command_checker;
pub mod command_completion;
pub mod completion;
pub mod display_width;
pub mod edit_action;
pub mod fuzzy_search;
pub mod highlight;
pub mod highlight_scanner;
pub mod history;
pub mod keymap;
pub mod kill_ring;
pub mod line_editor;
pub mod parse_status;
pub mod prompt;
pub mod terminal;
pub mod undo;
use std::io::{self, Write};
use crate::exec::Executor;
use crate::signal;
use command_completion::{CommandCompleter, CommandCompletionContext};
use completion::CompletionContext;
use highlight::{CheckerEnv, HighlightScanner};
use line_editor::LineEditor;
use parse_status::{ParseStatus, classify_parse};
use prompt::{PromptInfo, expand_prompt};
use terminal::CrosstermTerminal;
pub struct Repl {
executor: Executor,
line_editor: LineEditor,
terminal: CrosstermTerminal,
scanner: HighlightScanner,
command_completer: CommandCompleter,
}
impl Repl {
pub fn new(shell_name: String) -> Self {
signal::init_signal_handling();
let mut executor = Executor::new(shell_name, vec![]);
crate::env::default_path::ensure_default_path(&mut executor.env);
executor.env.mode.is_interactive = true;
executor.env.mode.options.monitor = true;
signal::init_job_control_signals();
crate::env::jobs::take_terminal(executor.env.process.shell_pgid).ok();
if executor.env.mode.is_interactive
&& executor.env.mode.options.monitor
&& let Ok(Some(t)) = crate::exec::terminal_state::capture_tty_termios()
{
executor.env.process.jobs.set_shell_tmodes(t);
}
let home = executor.env.vars.get("HOME").unwrap_or("").to_string();
let histfile = format!("{}/.yosh_history", home);
let _ = executor.env.vars.set("HISTFILE", &histfile);
let _ = executor.env.vars.set("HISTSIZE", "500");
let _ = executor.env.vars.set("HISTFILESIZE", "500");
let _ = executor.env.vars.set("HISTCONTROL", "ignoreboth");
executor.env.history.load(std::path::Path::new(&histfile));
executor.load_plugins();
if !home.is_empty() {
let rc_path = std::path::PathBuf::from(&home).join(".yoshrc");
executor.source_file(&rc_path); }
if let Some(env_val) = executor.env.vars.get("ENV").map(|s| s.to_string())
&& !env_val.is_empty()
{
let home = executor.env.vars.get("HOME").map(|s| s.to_string());
let after_tilde = crate::expand::expand_tilde_prefix(home.as_deref(), &env_val);
let input = format!("\"{}\"", after_tilde);
let expanded = match crate::lexer::Lexer::new(&input).next_token() {
Ok(tok) => {
if let crate::lexer::token::Token::Word(word) = tok.token {
crate::expand::expand_word_to_string(&mut executor.env, &word)
.ok()
.or_else(|| Some(after_tilde.clone()))
} else {
Some(after_tilde.clone())
}
}
Err(_) => Some(after_tilde.clone()),
};
if let Some(path) = expanded
&& executor.source_file(std::path::Path::new(&path)).is_none()
{
eprintln!("yosh: {}: No such file or directory", path);
}
}
Self {
executor,
line_editor: LineEditor::new(),
terminal: CrosstermTerminal::new(),
scanner: HighlightScanner::new(),
command_completer: CommandCompleter::new(),
}
}
pub fn run(&mut self) -> i32 {
let mut input_buffer = String::new();
loop {
self.executor.reap_zombies();
self.executor.display_job_notifications();
if input_buffer.is_empty() {
self.executor
.plugins
.call_pre_prompt(&mut self.executor.env);
}
let prompt_var = if input_buffer.is_empty() {
"PS1"
} else {
"PS2"
};
let prompt = expand_prompt(&mut self.executor.env, prompt_var);
let prompt_info = PromptInfo::from_prompt(&prompt);
for line in &prompt_info.upper_lines {
eprint!("{}\r\n", line);
}
eprint!("{}", prompt_info.last_line);
io::stderr().flush().ok();
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| self.executor.env.vars.get("PWD").unwrap_or(".").to_string());
let home = self.executor.env.vars.get("HOME").unwrap_or("").to_string();
let show_dotfiles = self
.executor
.env
.vars
.get("YOSH_SHOW_DOTFILES")
.map(|v| v == "1")
.unwrap_or(false);
let comp_ctx = CompletionContext {
cwd,
home,
show_dotfiles,
};
let path_val = self.executor.env.vars.get("PATH").unwrap_or("").to_string();
let checker_env = CheckerEnv {
path: &path_val,
aliases: &self.executor.env.aliases,
};
let mut cmd_ctx = CommandCompletionContext {
completer: &mut self.command_completer,
path: &path_val,
builtins: crate::builtin::BUILTIN_NAMES,
aliases: &self.executor.env.aliases,
};
let line = match self.line_editor.read_line_with_completion(
&prompt_info.last_line,
&prompt_info.upper_lines,
&mut self.executor.env.history,
&mut self.terminal,
&comp_ctx,
&mut cmd_ctx,
&mut self.scanner,
&checker_env,
&input_buffer,
) {
Ok(Some(line)) => line,
Ok(None) => {
if self.executor.env.mode.options.ignoreeof {
eprintln!("\r\nyosh: Use \"exit\" to leave the shell.");
input_buffer.clear();
continue;
}
eprintln!();
break;
}
Err(_) => {
break;
}
};
if line.is_empty() && !input_buffer.is_empty() {
input_buffer.clear();
continue;
}
if line.is_empty() && input_buffer.is_empty() {
continue;
}
input_buffer.push_str(&line);
input_buffer.push('\n');
self.executor.verbose_print(&line);
match classify_parse(&input_buffer, &self.executor.env.aliases) {
ParseStatus::Complete(commands) => {
let histsize: usize = self
.executor
.env
.vars
.get("HISTSIZE")
.and_then(|s| s.parse().ok())
.unwrap_or(500);
let histcontrol = self
.executor
.env
.vars
.get("HISTCONTROL")
.unwrap_or("ignoreboth")
.to_string();
let cmd_text = input_buffer.trim_end().to_string();
self.executor
.env
.history
.add(&cmd_text, histsize, &histcontrol);
for cmd in &commands {
let status = self.executor.exec_complete_command(cmd);
self.executor.env.exec.last_exit_status = status;
if self.executor.exit_requested.is_some() {
break;
}
}
input_buffer.clear();
}
ParseStatus::Incomplete => {
continue;
}
ParseStatus::Empty => {
input_buffer.clear();
}
ParseStatus::Error(msg) => {
eprintln!("yosh: {}", msg);
input_buffer.clear();
}
}
self.executor.process_pending_signals();
if let Some(code) = self.executor.exit_requested {
self.executor.env.exec.last_exit_status = code;
break;
}
}
self.executor.process_pending_signals();
if self.executor.exit_requested.is_none() {
self.executor.execute_exit_trap();
}
let histfile = self
.executor
.env
.vars
.get("HISTFILE")
.unwrap_or("")
.to_string();
let histfilesize: usize = self
.executor
.env
.vars
.get("HISTFILESIZE")
.and_then(|s| s.parse().ok())
.unwrap_or(500);
if !histfile.is_empty()
&& let Err(e) = self
.executor
.env
.history
.save(std::path::Path::new(&histfile), histfilesize)
{
eprintln!("yosh: warning: cannot save history to {}: {}", histfile, e);
}
self.executor.env.exec.last_exit_status
}
}