1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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();
// Ensure shell has terminal
crate::env::jobs::take_terminal(executor.env.process.shell_pgid).ok();
// Snapshot the terminal's termios so we can restore it after every
// foreground job completes. Only meaningful in interactive + monitor
// mode (both flags were set above). capture_tty_termios returns
// Ok(None) silently if stdin is not a TTY.
//
// The `is_interactive && monitor` check is documentation-only at
// this site (both flags were set unconditionally above), but
// mirrors the symmetric guard inside `wait_for_foreground_job`'s
// `restore_shell_termios_if_interactive`, where the check IS
// load-bearing. Keep both in sync so a future "simplification"
// does not drop one and leave the other dangling.
if executor.env.mode.is_interactive && executor.env.mode.options.monitor {
if let Ok(Some(t)) = crate::exec::terminal_state::capture_tty_termios() {
executor.env.process.jobs.set_shell_tmodes(t);
}
}
// Set history variable defaults
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");
// Load history from file
executor.env.history.load(std::path::Path::new(&histfile));
// Load plugins
executor.load_plugins();
// Source ~/.yoshrc (yosh-specific startup file)
if !home.is_empty() {
let rc_path = std::path::PathBuf::from(&home).join(".yoshrc");
executor.source_file(&rc_path); // Silent skip if absent
}
// Source $ENV (POSIX: parameter-expanded path for interactive shells)
if let Some(env_val) = executor.env.vars.get("ENV").map(|s| s.to_string()) {
if !env_val.is_empty() {
// POSIX 2.6.1: tilde expansion occurs before parameter expansion
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);
// Parse as double-quoted word for parameter expansion
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 {
if 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(),
}
}
/// Run the interactive REPL loop. Returns the exit status.
pub fn run(&mut self) -> i32 {
let mut input_buffer = String::new();
loop {
// Reap zombies and display job notifications before prompt
self.executor.reap_zombies();
self.executor.display_job_notifications();
// Fire pre_prompt hook for PS1 (not PS2 continuation)
if input_buffer.is_empty() {
self.executor
.plugins
.call_pre_prompt(&mut self.executor.env);
}
// Choose PS1 or PS2
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);
// Display prompt on stderr
for line in &prompt_info.upper_lines {
eprint!("{}\r\n", line);
}
eprint!("{}", prompt_info.last_line);
io::stderr().flush().ok();
// Build completion context
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,
};
// Build checker env for syntax highlighting
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,
};
// Read a line
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) => {
// EOF (Ctrl+D)
if self.executor.env.mode.options.ignoreeof {
eprintln!("\r\nyosh: Use \"exit\" to leave the shell.");
input_buffer.clear();
continue;
}
// Exit the shell
eprintln!();
break;
}
Err(_) => {
break;
}
};
// Ctrl+C returns empty string — reset buffer and re-prompt
if line.is_empty() && !input_buffer.is_empty() {
input_buffer.clear();
continue;
}
// Skip empty lines at PS1
if line.is_empty() && input_buffer.is_empty() {
continue;
}
// Accumulate input
input_buffer.push_str(&line);
input_buffer.push('\n');
// Verbose mode: print the input
self.executor.verbose_print(&line);
// Try to parse
match classify_parse(&input_buffer, &self.executor.env.aliases) {
ParseStatus::Complete(commands) => {
// Add to history before executing
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 reading (PS2 will be shown next iteration)
continue;
}
ParseStatus::Empty => {
input_buffer.clear();
}
ParseStatus::Error(msg) => {
eprintln!("yosh: {}", msg);
input_buffer.clear();
}
}
// Process any pending signals
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();
}
// Save history to file
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() {
self.executor
.env
.history
.save(std::path::Path::new(&histfile), histfilesize);
}
self.executor.env.exec.last_exit_status
}
}