brush_interactive/
interactive_shell.rs

1use crate::ShellError;
2use std::io::Write;
3
4/// Result of a read operation.
5pub enum ReadResult {
6    /// The user entered a line of input.
7    Input(String),
8    /// End of input was reached.
9    Eof,
10    /// The user interrupted the input operation.
11    Interrupted,
12}
13
14/// Result of an interactive execution.
15pub enum InteractiveExecutionResult {
16    /// The command was executed and returned the given result.
17    Executed(brush_core::ExecutionResult),
18    /// The command failed to execute.
19    Failed(brush_core::Error),
20    /// End of input was reached.
21    Eof,
22}
23
24/// Represents an interactive prompt.
25pub struct InteractivePrompt {
26    /// Prompt to display.
27    pub prompt: String,
28    /// Alternate-side prompt (typically right) to display.
29    pub alt_side_prompt: String,
30    /// Prompt to display on a continuation line of input.
31    pub continuation_prompt: String,
32}
33
34/// Represents a shell capable of taking commands from standard input.
35pub trait InteractiveShell {
36    /// Returns an immutable reference to the inner shell object.
37    fn shell(&self) -> impl AsRef<brush_core::Shell> + Send;
38
39    /// Returns a mutable reference to the inner shell object.
40    fn shell_mut(&mut self) -> impl AsMut<brush_core::Shell> + Send;
41
42    /// Reads a line of input, using the given prompt.
43    ///
44    /// # Arguments
45    ///
46    /// * `prompt` - The prompt to display to the user.
47    fn read_line(&mut self, prompt: InteractivePrompt) -> Result<ReadResult, ShellError>;
48
49    /// Update history, if relevant.
50    fn update_history(&mut self) -> Result<(), ShellError>;
51
52    /// Runs the interactive shell loop, reading commands from standard input and writing
53    /// results to standard output and standard error. Continues until the shell
54    /// normally exits or until a fatal error occurs.
55    // NOTE: we use desugared async here because [async_fn_in_trait] "warning: use of `async fn` in
56    // public traits is discouraged as auto trait bounds cannot be specified"
57    fn run_interactively(&mut self) -> impl std::future::Future<Output = Result<(), ShellError>> {
58        async {
59            // TODO: Consider finding a better place for this.
60            let _ = brush_core::TerminalControl::acquire()?;
61
62            let mut announce_exit = self.shell().as_ref().options.interactive;
63
64            loop {
65                let result = self.run_interactively_once().await?;
66                match result {
67                    InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
68                        exit_shell,
69                        return_from_function_or_script,
70                        ..
71                    }) => {
72                        if exit_shell {
73                            break;
74                        }
75
76                        if return_from_function_or_script {
77                            tracing::error!("return from non-function/script");
78                        }
79                    }
80                    InteractiveExecutionResult::Failed(e) => {
81                        // Report the error, but continue to execute.
82                        tracing::error!("error: {:#}", e);
83                    }
84                    InteractiveExecutionResult::Eof => {
85                        break;
86                    }
87                }
88
89                if self.shell().as_ref().options.exit_after_one_command {
90                    announce_exit = false;
91                    break;
92                }
93            }
94
95            if announce_exit {
96                writeln!(self.shell().as_ref().stderr(), "exit")?;
97            }
98
99            if let Err(e) = self.update_history() {
100                // N.B. This seems like the sort of thing that's worth being noisy about,
101                // but bash doesn't do that -- and probably for a reason.
102                tracing::debug!("couldn't save history: {e}");
103            }
104
105            Ok(())
106        }
107    }
108
109    /// Runs the interactive shell loop once, reading a single command from standard input.
110    fn run_interactively_once(
111        &mut self,
112    ) -> impl std::future::Future<Output = Result<InteractiveExecutionResult, ShellError>> {
113        async {
114            let mut shell = self.shell_mut();
115            let shell_mut = shell.as_mut();
116
117            // Check for any completed jobs.
118            shell_mut.check_for_completed_jobs()?;
119
120            // If there's a variable called PROMPT_COMMAND, then run it first.
121            if let Some(prompt_cmd) = shell_mut.get_env_str("PROMPT_COMMAND") {
122                // Save (and later restore) the last exit status.
123                let prev_last_result = shell_mut.last_exit_status;
124                let prev_last_pipeline_statuses = shell_mut.last_pipeline_statuses.clone();
125
126                let params = shell_mut.default_exec_params();
127
128                shell_mut
129                    .run_string(prompt_cmd.into_owned(), &params)
130                    .await?;
131
132                shell_mut.last_pipeline_statuses = prev_last_pipeline_statuses;
133                shell_mut.last_exit_status = prev_last_result;
134            }
135
136            // Now that we've done that, compose the prompt.
137            let prompt = InteractivePrompt {
138                prompt: shell_mut.as_mut().compose_prompt().await?,
139                alt_side_prompt: shell_mut.as_mut().compose_alt_side_prompt().await?,
140                continuation_prompt: shell_mut.as_mut().compose_continuation_prompt().await?,
141            };
142
143            drop(shell);
144
145            match self.read_line(prompt)? {
146                ReadResult::Input(read_result) => {
147                    let mut shell_mut = self.shell_mut();
148
149                    let precmd_prompt = shell_mut.as_mut().compose_precmd_prompt().await?;
150                    if !precmd_prompt.is_empty() {
151                        print!("{precmd_prompt}");
152                    }
153
154                    let params = shell_mut.as_mut().default_exec_params();
155                    match shell_mut.as_mut().run_string(read_result, &params).await {
156                        Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
157                        Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
158                    }
159                }
160                ReadResult::Eof => Ok(InteractiveExecutionResult::Eof),
161                ReadResult::Interrupted => {
162                    let mut shell_mut = self.shell_mut();
163                    shell_mut.as_mut().last_exit_status = 130;
164                    Ok(InteractiveExecutionResult::Executed(
165                        brush_core::ExecutionResult::new(130),
166                    ))
167                }
168            }
169        }
170    }
171}