mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
//! Advanced parse and execution-planning APIs.

use std::path::{Path, PathBuf};

use crate::ast::{
    AndOrList, Assignment, BinOpType, Command as AstCommand, IoRedirect, ParameterOp, Pipeline,
    Program, Word,
};
use crate::policy::VariableAttributes;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandResolutionError {
    NotExecutable(PathBuf),
    NotFound,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeferredReason {
    /// Realize this node against the current shell state at execution time.
    NeedsCurrentShellState,
    /// Realize this node only if short-circuit evaluation reaches it.
    ShortCircuitRhs,
}

/// Identify the borrowed AST carried by a lazy execution-plan node.
#[derive(Debug, Clone, Copy)]
pub enum PlannedLazyAst<'ast> {
    /// A deferred `&&` or `||` right-hand side.
    AndOr(&'ast AndOrList),
    /// A deferred pipeline stage command.
    Command(&'ast AstCommand),
}

/// Preserve AST-backed work that must stay lazy until execution.
///
/// This node carries borrowed AST plus the reason that execution planning
/// cannot yet freeze it into a fully realized command.
#[derive(Debug, Clone)]
pub struct PlannedLazyNode<'ast> {
    pub(crate) ast: PlannedLazyAst<'ast>,
    pub(crate) reason: DeferredReason,
    pub(crate) work_id: Option<String>,
    pub(crate) work: Option<DeferredPipelineStageWork<'ast>>,
}

impl<'ast> PlannedLazyNode<'ast> {
    /// Return the borrowed AST this lazy node will realize.
    pub fn ast(&self) -> PlannedLazyAst<'ast> {
        self.ast
    }

    /// Return the deferred and-or AST when this node came from short-circuit planning.
    pub fn and_or_list(&self) -> Option<&'ast AndOrList> {
        match self.ast {
            PlannedLazyAst::AndOr(and_or) => Some(and_or),
            PlannedLazyAst::Command(_) => None,
        }
    }

    /// Return the deferred command AST when this node came from pipeline planning.
    pub fn command(&self) -> Option<&'ast AstCommand> {
        match self.ast {
            PlannedLazyAst::AndOr(_) => None,
            PlannedLazyAst::Command(command) => Some(command),
        }
    }

    /// Return why this node remains lazy.
    pub fn reason(&self) -> DeferredReason {
        self.reason
    }

    /// Return the work identifier for observable lazy command work.
    ///
    /// This is present for deferred pipeline-stage commands and absent for
    /// lazy short-circuit RHS nodes.
    pub fn work_id(&self) -> Option<&str> {
        self.work_id.as_deref()
    }

    /// Return the deferred command work classification, when this node carries a command.
    pub fn deferred_work(&self) -> Option<DeferredPipelineStageWork<'ast>> {
        self.work
    }
}

#[derive(Debug)]
pub struct PlannedProgram<'ast> {
    pub(crate) body: Vec<PlannedCommandList<'ast>>,
}

impl<'ast> PlannedProgram<'ast> {
    pub fn command_lists(&self) -> &[PlannedCommandList<'ast>] {
        &self.body
    }
}

#[derive(Debug)]
pub struct PlannedCommandList<'ast> {
    pub(crate) raw_and_or_list: &'ast AndOrList,
    pub(crate) and_or_list: PlannedAndOr<'ast>,
    pub(crate) ampersand: bool,
}

impl<'ast> PlannedCommandList<'ast> {
    pub fn raw_and_or_list(&self) -> &'ast AndOrList {
        self.raw_and_or_list
    }

    pub fn and_or_list(&self) -> &PlannedAndOr<'ast> {
        &self.and_or_list
    }

    pub fn ampersand(&self) -> bool {
        self.ampersand
    }
}

#[derive(Debug)]
pub enum PlannedAndOr<'ast> {
    Pipeline(PlannedPipeline<'ast>),
    BinOp {
        op: BinOpType,
        left: Box<PlannedAndOr<'ast>>,
        /// The lazy right-hand side of a short-circuit operator.
        right: PlannedLazyNode<'ast>,
    },
}

#[derive(Debug)]
pub struct PlannedPipeline<'ast> {
    pub(crate) bang: bool,
    pub(crate) stages: Vec<PlannedPipelineStage<'ast>>,
}

impl<'ast> PlannedPipeline<'ast> {
    pub fn bang(&self) -> bool {
        self.bang
    }

    pub fn stages(&self) -> &[PlannedPipelineStage<'ast>] {
        &self.stages
    }
}

#[derive(Debug, Clone)]
pub struct PreparedExternalStagePlan {
    pub(crate) program: String,
    pub(crate) argv: Vec<String>,
    pub(crate) env: Vec<(String, String)>,
    pub(crate) cwd: PathBuf,
}

impl PreparedExternalStagePlan {
    pub fn program(&self) -> &str {
        &self.program
    }

    pub fn argv(&self) -> &[String] {
        &self.argv
    }

    pub fn env(&self) -> &[(String, String)] {
        &self.env
    }

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

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeferredExpansion<'ast> {
    CommandSubstitution { word: &'ast Word },
    ParameterExpansion { word: &'ast Word, op: ParameterOp },
    ArithmeticExpansion { word: &'ast Word },
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeferredPipelineStageWork<'ast> {
    CommandDispatch,
    CompoundCommand,
    Expansion(DeferredExpansion<'ast>),
}

#[derive(Debug)]
pub enum PlannedPipelineStage<'ast> {
    PreparedExternal(PreparedExternalStagePlan),
    Failure {
        status: i32,
    },
    SimpleCommand(PlannedSimpleCommand<'ast>),
    /// A pipeline stage that must remain lazy until execution.
    Lazy(PlannedLazyNode<'ast>),
}

impl<'ast> PlannedPipelineStage<'ast> {
    /// Return the lazy node when this stage must be realized at execution time.
    pub fn lazy(&self) -> Option<&PlannedLazyNode<'ast>> {
        match self {
            Self::Lazy(lazy) => Some(lazy),
            Self::PreparedExternal(_) | Self::Failure { .. } | Self::SimpleCommand(_) => None,
        }
    }

    pub fn work_id(&self) -> Option<&str> {
        match self {
            Self::Lazy(lazy) => lazy.work_id(),
            Self::PreparedExternal(_) | Self::Failure { .. } | Self::SimpleCommand(_) => None,
        }
    }

    pub fn deferred_work(&self) -> Option<DeferredPipelineStageWork<'ast>> {
        match self {
            Self::Lazy(lazy) => lazy.deferred_work(),
            Self::PreparedExternal(_) | Self::Failure { .. } | Self::SimpleCommand(_) => None,
        }
    }
}

#[derive(Debug, Clone)]
pub enum PlannedSimpleCommandKind {
    AssignmentsOnly {
        has_command_substitution: bool,
    },
    Function {
        command_name: String,
        argv: Vec<String>,
    },
    Builtin {
        argv: Vec<String>,
    },
    ShellOverride {
        argv: Vec<String>,
    },
    External {
        program: String,
        argv: Vec<String>,
    },
    UnspecifiedUtility {
        command_name: String,
    },
    ResolutionFailure {
        command_name: String,
        resolution: CommandResolutionError,
    },
    CommandNotFoundHandler {
        argv: Vec<String>,
    },
}

#[derive(Debug, Clone)]
pub struct PlannedSimpleCommand<'ast> {
    pub(crate) assignments: &'ast [Assignment],
    pub(crate) redirects: &'ast [IoRedirect],
    pub(crate) assignment_attrib: VariableAttributes,
    pub(crate) restore_assignments: bool,
    pub(crate) resolve_external_after_assignments: bool,
    pub(crate) kind: PlannedSimpleCommandKind,
    pub(crate) source_line: Option<u32>,
}

impl<'ast> PlannedSimpleCommand<'ast> {
    pub fn assignments(&self) -> &'ast [Assignment] {
        self.assignments
    }

    pub fn redirects(&self) -> &'ast [IoRedirect] {
        self.redirects
    }

    pub fn assignment_attributes(&self) -> VariableAttributes {
        self.assignment_attrib
    }

    pub fn restore_assignments(&self) -> bool {
        self.restore_assignments
    }

    pub fn kind(&self) -> &PlannedSimpleCommandKind {
        &self.kind
    }

    pub fn source_line(&self) -> Option<u32> {
        self.source_line
    }

    pub fn command_name(&self) -> Option<&str> {
        match &self.kind {
            PlannedSimpleCommandKind::AssignmentsOnly { .. } => None,
            PlannedSimpleCommandKind::Function { command_name, .. }
            | PlannedSimpleCommandKind::UnspecifiedUtility { command_name }
            | PlannedSimpleCommandKind::ResolutionFailure { command_name, .. } => {
                Some(command_name.as_str())
            }
            PlannedSimpleCommandKind::Builtin { argv }
            | PlannedSimpleCommandKind::ShellOverride { argv }
            | PlannedSimpleCommandKind::CommandNotFoundHandler { argv }
            | PlannedSimpleCommandKind::External { argv, .. } => argv.first().map(String::as_str),
        }
    }

    pub fn argv(&self) -> &[String] {
        match &self.kind {
            PlannedSimpleCommandKind::AssignmentsOnly { .. } => &[],
            PlannedSimpleCommandKind::Function { argv, .. }
            | PlannedSimpleCommandKind::Builtin { argv }
            | PlannedSimpleCommandKind::ShellOverride { argv }
            | PlannedSimpleCommandKind::CommandNotFoundHandler { argv }
            | PlannedSimpleCommandKind::External { argv, .. } => argv,
            PlannedSimpleCommandKind::UnspecifiedUtility { .. }
            | PlannedSimpleCommandKind::ResolutionFailure { .. } => &[],
        }
    }
}

/// A reusable execution plan for one parsed program.
///
/// Building this plan is session- and runtime-dependent because expansion,
/// command substitution, and command resolution can depend on shell state and
/// runtime lookups.
#[derive(Debug)]
pub struct ExecutionPlan<'ast> {
    pub(crate) program: &'ast Program,
    pub(crate) inner: PlannedProgram<'ast>,
}

impl<'ast> ExecutionPlan<'ast> {
    pub fn program(&self) -> &'ast Program {
        self.program
    }

    pub fn plan(&self) -> &PlannedProgram<'ast> {
        &self.inner
    }

    pub fn command_lists(&self) -> &[PlannedCommandList<'ast>] {
        self.inner.command_lists()
    }
}

/// A reusable execution plan for one parsed pipeline.
///
/// This is lower-level than [`ExecutionPlan`] and does not emit program-level
/// start/finish events. It is intended for embedders that need pipeline
/// planning as a first-class API.
#[derive(Debug)]
pub struct PipelineExecutionPlan<'ast> {
    pub(crate) pipeline: &'ast Pipeline,
    pub(crate) inner: PlannedPipeline<'ast>,
}

impl<'ast> PipelineExecutionPlan<'ast> {
    pub fn pipeline(&self) -> &'ast Pipeline {
        self.pipeline
    }

    pub fn plan(&self) -> &PlannedPipeline<'ast> {
        &self.inner
    }

    pub fn stages(&self) -> &[PlannedPipelineStage<'ast>] {
        self.inner.stages()
    }
}