Skip to main content

shrub_core/shell/
prompts.rs

1//! Prompt handling for shell instances.
2
3use std::borrow::Cow;
4
5use crate::{Shell, error, extensions, prompt};
6
7impl<SE: extensions::ShellExtensions> Shell<SE> {
8    /// Returns the default prompt string for the shell.
9    const fn default_prompt(&self) -> &'static str {
10        if self.options.sh_mode {
11            "$ "
12        } else {
13            "shrub$ "
14        }
15    }
16
17    /// Composes the shell's post-input, pre-command prompt, applying all
18    /// appropriate expansions.
19    ///
20    /// # Errors
21    ///
22    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
23    /// `PS0` is not a valid prompt string, or the error from any expansion in
24    /// it, such as a failing command substitution.
25    pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
26        self.expand_prompt_var("PS0", "").await
27    }
28
29    /// Composes the shell's prompt, applying all appropriate expansions.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
34    /// `PS1` is not a valid prompt string, or the error from any expansion in
35    /// it, such as a failing command substitution.
36    pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
37        self.expand_prompt_var("PS1", self.default_prompt()).await
38    }
39
40    /// Composes the shell's alternate-side prompt, applying all appropriate
41    /// expansions.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
46    /// `SHRUB_PS_ALT` is not a valid prompt string, or the error from any
47    /// expansion in it, such as a failing command substitution.
48    pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
49        // This is a shrub extension.
50        self.expand_prompt_var("SHRUB_PS_ALT", "").await
51    }
52
53    /// Composes the shell's continuation prompt.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
58    /// `PS2` is not a valid prompt string, or the error from any expansion in
59    /// it, such as a failing command substitution.
60    pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
61        self.expand_prompt_var("PS2", "> ").await
62    }
63
64    pub(super) async fn expand_prompt_var(
65        &mut self,
66        var_name: &str,
67        default: &str,
68    ) -> Result<String, error::Error> {
69        //
70        // TODO(prompt): bash appears to do this in a subshell; we need to
71        // investigate if that's required.
72        //
73
74        // Retrieve the spec.
75        let prompt_spec = self.parameter_or_default(var_name, default);
76        if prompt_spec.is_empty() {
77            return Ok(String::new());
78        }
79
80        // Save (and later restore) the state the last command left behind, so
81        // that expanding here is invisible to it.
82        let saved_status = self.save_command_status();
83
84        // Expand it. We must own the spec here: it borrows `self` (via the
85        // returned `Cow`), and `expand_prompt` needs `&mut self`.
86        let prompt_spec = prompt_spec.into_owned();
87        let params = self.default_exec_params();
88        let result = prompt::expand_prompt(self, &params, &prompt_spec).await;
89
90        self.restore_command_status(saved_status);
91
92        // Strip out special characters that readline would typically drop:
93        // \001 and \002 (start and end of non-printing sequences).
94        let mut expanded = result?;
95        expanded.retain(|c| c != '\x01' && c != '\x02');
96
97        Ok(expanded)
98    }
99
100    fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
101        self.env_str(name).unwrap_or_else(|| default.into())
102    }
103}