mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
//! Host builtin registration and execution APIs.

use std::collections::HashMap;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::policy::{ShellIdentity, VariableAttributes};
use crate::shell;

pub type BuiltinCallback =
    dyn for<'a> Fn(&mut BuiltinHost<'a>, &[String]) -> i32 + Send + Sync + 'static;

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct BuiltinProperties {
    special: bool,
}

impl BuiltinProperties {
    pub const fn regular() -> Self {
        Self { special: false }
    }

    pub const fn special() -> Self {
        Self { special: true }
    }

    pub const fn is_special(self) -> bool {
        self.special
    }
}

impl Default for BuiltinProperties {
    fn default() -> Self {
        Self::regular()
    }
}

#[derive(Clone)]
pub(crate) enum RegisteredBuiltin {
    Standard {
        kind: crate::shell::StandardBuiltin,
        properties: BuiltinProperties,
    },
    Custom {
        properties: BuiltinProperties,
        handler: Arc<BuiltinCallback>,
    },
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PortableBuiltinEntry {
    name: String,
    kind: crate::shell::StandardBuiltin,
    properties: BuiltinProperties,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PortableBuiltinRegistry {
    entries: Vec<PortableBuiltinEntry>,
}

impl RegisteredBuiltin {
    pub(crate) fn standard(
        kind: crate::shell::StandardBuiltin,
        properties: BuiltinProperties,
    ) -> Self {
        Self::Standard { kind, properties }
    }

    pub(crate) fn custom(properties: BuiltinProperties, handler: Arc<BuiltinCallback>) -> Self {
        Self::Custom {
            properties,
            handler,
        }
    }

    pub(crate) fn properties(&self) -> BuiltinProperties {
        match self {
            Self::Standard { properties, .. } | Self::Custom { properties, .. } => *properties,
        }
    }
}

#[derive(Clone)]
pub(crate) struct BuiltinRegistry {
    entries: HashMap<String, RegisteredBuiltin>,
}

impl BuiltinRegistry {
    pub fn empty() -> Self {
        Self {
            entries: HashMap::new(),
        }
    }

    pub fn standard() -> Self {
        crate::shell::standard_builtin_registry()
    }

    pub(crate) fn contains(&self, name: &str) -> bool {
        self.entries.contains_key(name)
    }

    pub(crate) fn lookup(&self, name: &str) -> Option<RegisteredBuiltin> {
        self.entries.get(name).cloned()
    }

    pub(crate) fn remove(&mut self, name: &str) -> Option<RegisteredBuiltin> {
        self.entries.remove(name)
    }

    pub(crate) fn insert_standard(
        &mut self,
        name: impl Into<String>,
        kind: crate::shell::StandardBuiltin,
        properties: BuiltinProperties,
    ) -> Option<RegisteredBuiltin> {
        self.entries
            .insert(name.into(), RegisteredBuiltin::standard(kind, properties))
    }

    pub(crate) fn insert_custom(
        &mut self,
        name: impl Into<String>,
        properties: BuiltinProperties,
        handler: Arc<BuiltinCallback>,
    ) -> Option<RegisteredBuiltin> {
        self.entries
            .insert(name.into(), RegisteredBuiltin::custom(properties, handler))
    }

    pub(crate) fn names(&self) -> Vec<String> {
        let mut names = self.entries.keys().cloned().collect::<Vec<_>>();
        names.sort();
        names
    }

    pub(crate) fn portable_background_checkpoint(&self) -> (PortableBuiltinRegistry, Vec<String>) {
        let mut entries = Vec::with_capacity(self.entries.len());
        let mut non_portable_names = Vec::new();
        for (name, registered) in &self.entries {
            let RegisteredBuiltin::Standard { kind, properties } = registered else {
                non_portable_names.push(name.clone());
                continue;
            };
            entries.push(PortableBuiltinEntry {
                name: name.clone(),
                kind: *kind,
                properties: *properties,
            });
        }
        entries.sort_by(|left, right| left.name.cmp(&right.name));
        non_portable_names.sort();
        (PortableBuiltinRegistry { entries }, non_portable_names)
    }

    #[cfg(any(
        feature = "frontend",
        all(test, feature = "test-support", feature = "unix-runtime")
    ))]
    pub(crate) fn from_portable_background_checkpoint(checkpoint: PortableBuiltinRegistry) -> Self {
        let mut registry = Self::empty();
        for entry in checkpoint.entries {
            let _ = registry.insert_standard(entry.name, entry.kind, entry.properties);
        }
        registry
    }
}

impl Default for BuiltinRegistry {
    fn default() -> Self {
        Self::standard()
    }
}

impl fmt::Debug for BuiltinRegistry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut names = self.entries.keys().collect::<Vec<_>>();
        names.sort();
        f.debug_struct("BuiltinRegistry")
            .field("builtin_names", &names)
            .finish()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildError {
    BuiltinConflict(String),
}

impl fmt::Display for BuildError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BuiltinConflict(name) => {
                write!(
                    f,
                    "builtin registration conflicts with existing builtin: {name}"
                )
            }
        }
    }
}

impl std::error::Error for BuildError {}

pub struct BuiltinHost<'a> {
    state: &'a mut shell::ShellState,
}

impl<'a> BuiltinHost<'a> {
    pub(crate) fn new(state: &'a mut shell::ShellState) -> Self {
        Self { state }
    }

    pub fn env_get(&self, key: &str) -> Option<&str> {
        self.state.env_get(key)
    }

    pub fn identity(&self) -> &ShellIdentity {
        &self.state.definition.identity
    }

    pub fn shell_name(&self) -> &str {
        self.identity().name()
    }

    pub fn env_set(
        &mut self,
        key: &str,
        value: impl Into<String>,
        attrib: VariableAttributes,
    ) -> bool {
        self.state.env_set(key, value.into(), attrib.bits())
    }

    pub fn current_dir(&self) -> &Path {
        &self.state.path_state.cwd
    }

    pub fn set_current_dir<P: Into<PathBuf>>(&mut self, cwd: P) {
        self.state.path_state.cwd = cwd.into();
        self.state.env_set_internal(
            "PWD",
            self.state.path_state.cwd.display().to_string(),
            shell::VAR_EXPORT,
        );
    }

    pub fn write_stdout(&self, message: &str) -> io::Result<()> {
        self.state.stdout_fd.write_str(message)
    }

    pub fn write_stdout_bytes(&self, message: &[u8]) -> io::Result<()> {
        self.state.stdout_fd.write_all(message)
    }

    pub fn write_stdout_line(&self, message: &str) -> io::Result<()> {
        self.state.stdout_fd.write_line(message)
    }

    pub fn write_stderr(&self, message: &str) -> io::Result<()> {
        self.state.stderr_fd.write_line(message)
    }
}