shrub-core 0.0.4

Reusable core of a POSIX/bash shell (used by shrub-shell)
Documentation
//! Prompt handling for shell instances.

use std::borrow::Cow;

use crate::{Shell, error, extensions, prompt};

impl<SE: extensions::ShellExtensions> Shell<SE> {
    /// Returns the default prompt string for the shell.
    const fn default_prompt(&self) -> &'static str {
        if self.options.sh_mode {
            "$ "
        } else {
            "shrub$ "
        }
    }

    /// Composes the shell's post-input, pre-command prompt, applying all
    /// appropriate expansions.
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
    /// `PS0` is not a valid prompt string, or the error from any expansion in
    /// it, such as a failing command substitution.
    pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
        self.expand_prompt_var("PS0", "").await
    }

    /// Composes the shell's prompt, applying all appropriate expansions.
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
    /// `PS1` is not a valid prompt string, or the error from any expansion in
    /// it, such as a failing command substitution.
    pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
        self.expand_prompt_var("PS1", self.default_prompt()).await
    }

    /// Composes the shell's alternate-side prompt, applying all appropriate
    /// expansions.
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
    /// `SHRUB_PS_ALT` is not a valid prompt string, or the error from any
    /// expansion in it, such as a failing command substitution.
    pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
        // This is a shrub extension.
        self.expand_prompt_var("SHRUB_PS_ALT", "").await
    }

    /// Composes the shell's continuation prompt.
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::ErrorKind::WordParseError`] if the value of
    /// `PS2` is not a valid prompt string, or the error from any expansion in
    /// it, such as a failing command substitution.
    pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
        self.expand_prompt_var("PS2", "> ").await
    }

    pub(super) async fn expand_prompt_var(
        &mut self,
        var_name: &str,
        default: &str,
    ) -> Result<String, error::Error> {
        //
        // TODO(prompt): bash appears to do this in a subshell; we need to
        // investigate if that's required.
        //

        // Retrieve the spec.
        let prompt_spec = self.parameter_or_default(var_name, default);
        if prompt_spec.is_empty() {
            return Ok(String::new());
        }

        // Save (and later restore) the state the last command left behind, so
        // that expanding here is invisible to it.
        let saved_status = self.save_command_status();

        // Expand it. We must own the spec here: it borrows `self` (via the
        // returned `Cow`), and `expand_prompt` needs `&mut self`.
        let prompt_spec = prompt_spec.into_owned();
        let params = self.default_exec_params();
        let result = prompt::expand_prompt(self, &params, &prompt_spec).await;

        self.restore_command_status(saved_status);

        // Strip out special characters that readline would typically drop:
        // \001 and \002 (start and end of non-printing sequences).
        let mut expanded = result?;
        expanded.retain(|c| c != '\x01' && c != '\x02');

        Ok(expanded)
    }

    fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
        self.env_str(name).unwrap_or_else(|| default.into())
    }
}