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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
use std::io::IsTerminal as _;
use std::io::Write as _;
use crate::InputBackend;
use crate::InteractivePrompt;
use crate::ReadResult;
use crate::ShellError;
/// Result of an interactive execution.
pub enum InteractiveExecutionResult {
/// The command was executed and returned the given result.
Executed(brush_core::ExecutionResult),
/// The command failed to execute.
Failed(brush_core::Error),
/// End of input was reached.
Eof,
}
impl From<&InteractiveExecutionResult> for i32 {
/// Converts an `InteractiveExecutionResult` into a signed, 32-bit exit code.
fn from(value: &InteractiveExecutionResult) -> Self {
match value {
InteractiveExecutionResult::Executed(result) => u8::from(result.exit_code).into(),
InteractiveExecutionResult::Failed(_) => 1,
InteractiveExecutionResult::Eof => 0,
}
}
}
/// Options for interactive shells.
#[derive(Clone)]
pub struct InteractiveOptions {
/// Whether terminal shell integration is enabled.
pub terminal_shell_integration: bool,
/// Whether or not to run `PROMPT_COMMAND` before each prompt.
pub run_prompt_command: bool,
/// Whether or not to run zsh-style exec/cmd functions (e.g., `preexec_functions`,
/// `precmd_functions`).
pub run_cmd_exec_funcs: bool,
}
impl Default for InteractiveOptions {
fn default() -> Self {
Self {
terminal_shell_integration: false,
run_prompt_command: true,
run_cmd_exec_funcs: false,
}
}
}
/// Represents an interactive shell that displays prompts, interactively reads user input, etc.
pub struct InteractiveShell<'a, IB: InputBackend, SE: brush_core::ShellExtensions> {
/// The underlying shell instance.
shell: crate::ShellRef<SE>,
/// The input backend to use.
input: &'a mut IB,
/// Terminal integration utility, if any.
terminal_integration: Option<crate::term_integration::TerminalIntegration>,
/// Options.
options: InteractiveOptions,
}
impl<'a, IB: InputBackend, SE: brush_core::ShellExtensions> InteractiveShell<'a, IB, SE> {
/// Creates a new `InteractiveShell` wrapping the given shell instance.
///
/// # Arguments
///
/// * `shell` - The shell instance to wrap.
/// * `input` - The input backend to use.
/// * `options` - The user interface options to use.
pub fn new(
shell: &crate::ShellRef<SE>,
input: &'a mut IB,
options: &InteractiveOptions,
) -> Result<Self, ShellError> {
let stdin_is_terminal = std::io::stdin().is_terminal();
// Acquire terminal control if stdin is a terminal.
if stdin_is_terminal {
brush_core::terminal::TerminalControl::acquire()?;
}
// Set up terminal integration if enabled *and* if stdin is a terminal.
let terminal_integration = if options.terminal_shell_integration && stdin_is_terminal {
let terminfo = crate::term_detection::get_terminal_info(&HostEnvironment);
let terminal_integration = crate::term_integration::TerminalIntegration::new(terminfo);
print!("{}", terminal_integration.initialize().as_ref());
std::io::stdout().flush()?;
Some(terminal_integration)
} else {
None
};
Ok(Self {
shell: shell.clone(),
input,
terminal_integration,
options: options.clone(),
})
}
/// Runs the interactive shell loop, reading commands from standard input and writing
/// results to standard output and standard error. Continues until the shell
/// normally exits or until a fatal error occurs.
pub async fn run_interactively(&mut self) -> Result<(), ShellError> {
let mut shell = self.shell.lock().await;
let mut announce_exit = shell.options().interactive;
shell.start_interactive_session()?;
drop(shell);
loop {
let result = self.run_interactively_once().await?;
match result {
InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
next_control_flow: brush_core::results::ExecutionControlFlow::ExitShell,
..
}) => {
break;
}
InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
next_control_flow:
brush_core::results::ExecutionControlFlow::ReturnFromFunctionOrScript,
..
}) => {
tracing::error!("return from non-function/script");
}
InteractiveExecutionResult::Executed(_) => {}
InteractiveExecutionResult::Failed(err) => {
// Report the error, but continue to execute.
let shell = self.shell.lock().await;
let mut stderr = shell.stderr();
let _ = shell.display_error(&mut stderr, &err);
drop(shell);
}
InteractiveExecutionResult::Eof => {
break;
}
}
if self.shell.lock().await.options().exit_after_one_command {
announce_exit = false;
break;
}
}
let mut shell = self.shell.lock().await;
shell.end_interactive_session()?;
if announce_exit {
writeln!(shell.stderr(), "exit")?;
}
if let Err(e) = shell.save_history() {
// N.B. This seems like the sort of thing that's worth being noisy about,
// but bash doesn't do that -- and probably for a reason.
tracing::debug!("couldn't save history: {e}");
}
// Give the shell an opportunity to perform any on-exit operations.
shell.on_exit().await?;
drop(shell);
Ok(())
}
/// Runs the interactive shell loop once, reading a single command from standard input.
async fn run_interactively_once(&mut self) -> Result<InteractiveExecutionResult, ShellError> {
let mut shell = self.shell.lock().await;
// Run any pre-prompt actions.
Self::run_pre_prompt_actions(&mut shell, &self.options).await?;
// Compose the prompt.
let prompt = Self::compose_prompt(&mut shell, self.terminal_integration.as_ref()).await?;
drop(shell);
// Read input.
match self.input.read_line(&self.shell, prompt)? {
ReadResult::Input(read_result) => {
// We got a line of input -- execute it.
self.execute_line(read_result, true /* user input */).await
}
ReadResult::BoundCommand(read_result) => {
// We got a line that was bound to keybindings; execute it.
self.execute_line(read_result, false /* user input */).await
}
ReadResult::Eof => {
// We're done!
Ok(InteractiveExecutionResult::Eof)
}
ReadResult::Interrupted => {
// We were interrupted; report that appropriately.
let result: brush_core::ExecutionResult =
brush_core::ExecutionExitCode::Interrupted.into();
self.shell
.lock()
.await
.set_last_exit_status(result.exit_code.into());
Ok(InteractiveExecutionResult::Executed(result))
}
}
}
async fn compose_prompt(
shell: &mut brush_core::Shell<SE>,
terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
) -> Result<InteractivePrompt, ShellError> {
// Now that we've done that, compose the prompt.
let mut prompt = InteractivePrompt {
prompt: shell.compose_prompt().await?,
alt_side_prompt: shell.compose_alt_side_prompt().await?,
continuation_prompt: shell.compose_continuation_prompt().await?,
};
if let Some(terminal_integration) = terminal_integration {
let pre_prompt = terminal_integration.pre_prompt();
let working_dir = terminal_integration.report_cwd(shell.working_dir());
let post_prompt = terminal_integration.post_prompt();
prompt.prompt = [
pre_prompt.as_ref(),
working_dir.as_ref(),
prompt.prompt.as_str(),
post_prompt.as_ref(),
]
.concat();
}
Ok(prompt)
}
/// Executes the given line of input.
///
/// # Arguments
///
/// * `read_result` - The line of input to execute.
/// * `user_input` - Whether the line came from direct user input (as opposed to a key binding,
/// say).
async fn execute_line(
&mut self,
read_result: String,
user_input: bool,
) -> Result<InteractiveExecutionResult, ShellError> {
let mut shell = self.shell.lock().await;
// See if the the user interface has a non-empty read buffer.
let buffer_info = self.input.get_read_buffer();
// If the user interface did, in fact, have a non-empty read buffer,
// then reflect it to the shell in case any shell code wants to
// process and/or transform the buffer.
let nonempty_buffer = if let Some((buffer, cursor)) = buffer_info {
if !buffer.is_empty() {
shell.set_edit_buffer(buffer, cursor)?;
true
} else {
false
}
} else {
false
};
// If the line came from direct user input (as opposed to a key binding, say), then we
// need to do a few more things before executing it.
if user_input {
Self::run_pre_exec_actions(
&mut shell,
read_result.as_str(),
&self.options,
self.terminal_integration.as_ref(),
)
.await?;
}
// Count the command's lines.
let line_count = read_result.lines().count().max(1);
// Execute the command.
let params = shell.default_exec_params();
let source_info = brush_core::SourceInfo::from("main");
let result = match shell.run_string(read_result, &source_info, ¶ms).await {
Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
};
// Update cumulative line counter based on actual lines in the command.
shell.increment_interactive_line_offset(line_count);
// See if the shell has input buffer state that we need to reflect back to
// the user interface. It may be state that originally came from the user
// interface, or it may be state that was programmatically generated by
// the command we just executed.
let mut buffer_and_cursor = shell.pop_edit_buffer()?;
drop(shell);
if buffer_and_cursor.is_none() && nonempty_buffer {
buffer_and_cursor = Some((String::new(), 0));
}
if let Some((updated_buffer, updated_cursor)) = buffer_and_cursor {
self.input.set_read_buffer(updated_buffer, updated_cursor);
}
// Invoke terminal integration.
if let Some(terminal_integration) = &self.terminal_integration {
let exit_code = result.as_ref().map_or(1, i32::from);
print!(
"{}",
terminal_integration.post_exec_command(exit_code).as_ref()
);
std::io::stdout().flush()?;
}
result
}
async fn run_pre_prompt_actions(
shell: &mut brush_core::Shell<SE>,
options: &InteractiveOptions,
) -> Result<(), ShellError> {
// Check for any completed jobs.
shell.check_for_completed_jobs()?;
// If there's a variable called PROMPT_COMMAND, then run it first.
if options.run_prompt_command {
if let Some(prompt_cmd_var) = shell.env_var("PROMPT_COMMAND") {
match prompt_cmd_var.value() {
brush_core::ShellValue::String(cmd_str) => {
Self::run_pre_prompt_command(shell, cmd_str.to_owned()).await?;
}
brush_core::ShellValue::IndexedArray(values) => {
let owned_values: Vec<_> = values.values().cloned().collect();
for cmd_str in owned_values {
Self::run_pre_prompt_command(shell, cmd_str).await?;
}
}
// Other types are ignored.
_ => (),
}
}
}
// Next, run any zsh-style `precmd_functions`.
// TODO(precmd_functions): verify if we need to save/restore exit results.
if options.run_cmd_exec_funcs {
// If there's a variable called precmd_functions, then call them.
if let Some(brush_core::ShellValue::IndexedArray(precmd_funcs)) = shell
.env_var("precmd_functions")
.map(|var| var.value())
.cloned()
{
for func_name in precmd_funcs.values() {
let _ = shell
.invoke_function(
func_name,
std::iter::empty::<&str>(),
&shell.default_exec_params(),
)
.await;
}
}
}
Ok(())
}
async fn run_pre_exec_actions(
shell: &mut brush_core::Shell<SE>,
command_line: &str,
options: &InteractiveOptions,
terminal_integration: Option<&crate::term_integration::TerminalIntegration>,
) -> Result<(), ShellError> {
// Display the pre-command prompt (if there is one).
let precmd_prompt = shell.compose_precmd_prompt().await?;
if !precmd_prompt.is_empty() {
print!("{precmd_prompt}");
}
// Update history (if applicable).
shell.add_to_history(command_line.trim_end_matches('\n'))?;
// Next, run any zsh-style `preexec_functions`.
// TODO(preexec_functions): verify if we need to save/restore exit results.
if options.run_cmd_exec_funcs {
// If there's a variable called preexec_functions, then call them.
if let Some(brush_core::ShellValue::IndexedArray(preexec_funcs)) = shell
.env_var("preexec_functions")
.map(|var| var.value())
.cloned()
{
for func_name in preexec_funcs.values() {
let _ = shell
.invoke_function(func_name, &[command_line], &shell.default_exec_params())
.await;
}
}
}
// Invoke terminal integration.
if let Some(terminal_integration) = terminal_integration {
print!(
"{}",
terminal_integration.pre_exec_command(command_line).as_ref()
);
std::io::stdout().flush()?;
}
Ok(())
}
async fn run_pre_prompt_command(
shell: &mut brush_core::Shell<SE>,
prompt_cmd: String,
) -> Result<(), ShellError> {
// Save (and later restore) the last exit status.
let prev_last_result = shell.last_exit_status();
let prev_last_pipeline_statuses = shell.last_pipeline_statuses().to_vec();
// Run the command.
let params = shell.default_exec_params();
let source_info = brush_core::SourceInfo::from("PROMPT_COMMAND");
shell.run_string(prompt_cmd, &source_info, ¶ms).await?;
// Restore the last exit status.
*shell.last_pipeline_statuses_mut() = prev_last_pipeline_statuses;
shell.set_last_exit_status(prev_last_result);
Ok(())
}
}
/// Represents the host environment; used for terminal detection in conjunction
/// with the `TerminalEnvironment` trait.
struct HostEnvironment;
impl crate::term_detection::TerminalEnvironment for HostEnvironment {
/// Gets the value of the given environment variable from the host process's
/// OS environment variables. Returns `None` if the variable is not set.
///
/// # Arguments
///
/// * `name` - The name of the environment variable to get.
fn get_env_var(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
}