brush_interactive/
interactive_shell.rs

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
use crate::ShellError;
use std::io::Write;

/// Result of a read operation.
pub enum ReadResult {
    /// The user entered a line of input.
    Input(String),
    /// End of input was reached.
    Eof,
    /// The user interrupted the input operation.
    Interrupted,
}

/// 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,
}

/// Represents a shell capable of taking commands from standard input.
#[async_trait::async_trait]
pub trait InteractiveShell {
    /// Returns an immutable reference to the inner shell object.
    fn shell(&self) -> impl AsRef<brush_core::Shell> + Send;

    /// Returns a mutable reference to the inner shell object.
    fn shell_mut(&mut self) -> impl AsMut<brush_core::Shell> + Send;

    /// Reads a line of input, using the given prompt.
    ///
    /// # Arguments
    ///
    /// * `prompt` - The prompt to display to the user.
    fn read_line(&mut self, prompt: &str) -> Result<ReadResult, ShellError>;

    /// Update history, if relevant.
    fn update_history(&mut self) -> Result<(), ShellError>;

    /// 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.
    async fn run_interactively(&mut self) -> Result<(), ShellError> {
        // TODO: Consider finding a better place for this.
        let _ = brush_core::TerminalControl::acquire()?;

        loop {
            let result = self.run_interactively_once().await?;
            match result {
                InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
                    exit_shell,
                    return_from_function_or_script,
                    ..
                }) => {
                    if exit_shell {
                        break;
                    }

                    if return_from_function_or_script {
                        tracing::error!("return from non-function/script");
                    }
                }
                InteractiveExecutionResult::Failed(e) => {
                    // Report the error, but continue to execute.
                    tracing::error!("error: {:#}", e);
                }
                InteractiveExecutionResult::Eof => {
                    break;
                }
            }
        }

        if self.shell().as_ref().options.interactive {
            writeln!(self.shell().as_ref().stderr(), "exit")?;
        }

        if let Err(e) = self.update_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}");
        }

        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_mut = self.shell_mut();

        // Check for any completed jobs.
        shell_mut.as_mut().check_for_completed_jobs()?;

        // If there's a variable called PROMPT_COMMAND, then run it first.
        if let Some((_, prompt_cmd)) = shell_mut.as_mut().env.get("PROMPT_COMMAND") {
            let prompt_cmd = prompt_cmd.value().to_cow_string().to_string();

            // Save (and later restore) the last exit status.
            let prev_last_result = shell_mut.as_mut().last_exit_status;

            let params = shell_mut.as_mut().default_exec_params();

            shell_mut.as_mut().run_string(prompt_cmd, &params).await?;

            shell_mut.as_mut().last_exit_status = prev_last_result;
        }

        // Now that we've done that, compose the prompt.
        let prompt = shell_mut.as_mut().compose_prompt().await?;

        drop(shell_mut);

        match self.read_line(prompt.as_str())? {
            ReadResult::Input(read_result) => {
                let mut shell_mut = self.shell_mut();
                let params = shell_mut.as_mut().default_exec_params();
                match shell_mut.as_mut().run_string(read_result, &params).await {
                    Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
                    Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
                }
            }
            ReadResult::Eof => Ok(InteractiveExecutionResult::Eof),
            ReadResult::Interrupted => {
                let mut shell_mut = self.shell_mut();
                shell_mut.as_mut().last_exit_status = 130;
                Ok(InteractiveExecutionResult::Executed(
                    brush_core::ExecutionResult::new(130),
                ))
            }
        }
    }
}