mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use crate::ast::Program;
use crate::embed::{DiagnosticCategory, DiagnosticKind};
use crate::parser::Parser;
use crate::sys::Runtime;
use std::io;

use super::frontend::emit_diagnostic;
use super::{ControlFlow, OPT_NOEXEC, ShellState, shell_errln, shell_events};

pub(crate) struct RunStringOutcome {
    pub(crate) status: i32,
    pub(crate) parse_error: bool,
}

pub fn run_program<R: Runtime>(state: &mut ShellState, runtime: &mut R, program: &Program) -> i32 {
    run_program_with_context(state, runtime, program, None, None)
}

pub(crate) fn run_planned_program<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &Program,
    plan: &super::ShellExecProgram<'_>,
    raw_command: Option<&str>,
    command_id: Option<&str>,
) -> i32 {
    let mut emit_program_events = false;
    let mut owned_command_id = None;
    let previous_command_id = state.active_command_id.clone();
    if shell_events::observability_enabled(state) {
        let canonical = shell_events::canonical_program_text(program);
        emit_program_events = !canonical.trim().is_empty()
            || raw_command.is_some_and(|raw_command| !raw_command.trim().is_empty());
        if emit_program_events {
            let command_id = match command_id {
                Some(command_id) => command_id,
                None => owned_command_id.get_or_insert_with(shell_events::new_command_id),
            };
            shell_events::emit_program_start(state, command_id, raw_command, &canonical);
        }
    }
    state.set_active_command_id(
        command_id
            .map(ToString::to_string)
            .or_else(|| owned_command_id.clone())
            .or(previous_command_id.clone()),
    );
    let status = super::execute_program_plan(state, runtime, plan);
    if emit_program_events {
        let command_id = match command_id {
            Some(command_id) => command_id,
            None => owned_command_id.as_deref().unwrap_or_default(),
        };
        shell_events::emit_program_finish(state, command_id, status);
    }
    state.set_active_command_id(previous_command_id);
    status
}

fn run_program_with_context<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &Program,
    raw_command: Option<&str>,
    command_id: Option<&str>,
) -> i32 {
    let plan = super::build_program_execution(state, runtime, program);
    run_planned_program(state, runtime, program, &plan, raw_command, command_id)
}

/// Run a string of shell code through the parser and executor.
pub(crate) fn run_string<R: Runtime>(state: &mut ShellState, runtime: &mut R, text: &str) -> i32 {
    run_string_with_context(state, runtime, Some(text), None, None).status
}

pub(crate) fn run_string_with_outcome<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    text: &str,
) -> RunStringOutcome {
    run_string_with_context(state, runtime, Some(text), None, None)
}

#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
pub(super) fn run_string_with_source<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    text: &str,
    source_name: Option<&str>,
) -> i32 {
    run_string_with_context(state, runtime, Some(text), source_name, None).status
}

pub(crate) fn run_source_reader<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    source: impl io::Read + Send + 'static,
    source_name: Option<&str>,
) -> i32 {
    run_source_reader_with_outcome(state, runtime, source, source_name).status
}

pub(crate) fn run_source_reader_with_outcome<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    source: impl io::Read + Send + 'static,
    source_name: Option<&str>,
) -> RunStringOutcome {
    run_reader_with_context(state, runtime, Box::new(source), None, source_name, None)
}

fn run_string_with_context<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    text: Option<&str>,
    source_name: Option<&str>,
    command_id: Option<&str>,
) -> RunStringOutcome {
    let reader: Box<dyn io::Read + Send> = if let Some(text) = text {
        Box::new(std::io::Cursor::new(text.as_bytes().to_vec()))
    } else {
        Box::new(std::io::Cursor::new(Vec::new()))
    };
    run_reader_with_context(state, runtime, reader, text, source_name, command_id)
}

fn run_reader_with_context<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    source: Box<dyn io::Read + Send>,
    source_text: Option<&str>,
    source_name: Option<&str>,
    command_id: Option<&str>,
) -> RunStringOutcome {
    let old_source = state.current_source.clone();
    let active_source = source_name.unwrap_or(state.shell_name()).to_string();
    state.set_current_source(Some(active_source.clone()));
    let mut parser = Parser::new(source);
    crate::parser::configure_parser_for_language(&mut parser, &state.definition.language);
    parser.set_source_name(Some(active_source));
    let mut status = 0;
    let mut parse_error = false;
    loop {
        match parse_program_step(state, &mut parser, source_text, source_name) {
            ProgramParseStep::Program {
                program,
                raw_command,
            } => {
                status = execute_parsed_program_step(
                    state,
                    runtime,
                    &program,
                    Some(&raw_command),
                    command_id,
                );
                if state.exit_code >= 0 || state.control_flow != ControlFlow::None {
                    break;
                }
            }
            ProgramParseStep::Eof => break,
            ProgramParseStep::ParseError(parse_status) => {
                status = parse_status;
                parse_error = true;
                break;
            }
        }
    }
    state.set_current_source(old_source);
    RunStringOutcome {
        status,
        parse_error,
    }
}

enum ProgramParseStep {
    Program {
        program: Program,
        raw_command: String,
    },
    Eof,
    ParseError(i32),
}

fn parse_program_step(
    state: &ShellState,
    parser: &mut Parser,
    source_text: Option<&str>,
    source_name: Option<&str>,
) -> ProgramParseStep {
    let aliases = state.aliases_snapshot();
    parser.set_alias_func(crate::parser::AliasFn::new(move |name| {
        aliases.get(name).cloned()
    }));
    let start = source_text
        .map(|text| parser.current_pos().offset.min(text.len()))
        .unwrap_or(0);
    match parser.parse_line() {
        Ok(Some(program)) => {
            emit_parser_warnings(state, parser);
            let raw_command = if let Some(source_text) = source_text {
                let end = parser.current_pos().offset.min(source_text.len());
                source_text[start..end].trim_end_matches('\n').to_string()
            } else {
                program.to_canonical()
            };
            ProgramParseStep::Program {
                program,
                raw_command,
            }
        }
        Ok(None) => {
            emit_parser_warnings(state, parser);
            ProgramParseStep::Eof
        }
        Err(err) => {
            report_parser_error(state, source_name, &err);
            ProgramParseStep::ParseError(2)
        }
    }
}

fn execute_parsed_program_step<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &Program,
    raw_command: Option<&str>,
    command_id: Option<&str>,
) -> i32 {
    let status = run_parsed_program(state, runtime, program, raw_command, command_id);
    state.set_last_status(status);
    status
}

fn emit_parser_warnings(state: &ShellState, parser: &mut Parser) {
    for diagnostic in parser.take_diagnostics() {
        let _ = state.stderr_fd.write_line(&diagnostic.message);
        emit_diagnostic(
            state,
            DiagnosticKind::Warning,
            DiagnosticCategory::Input,
            diagnostic.code,
            &diagnostic.message,
            diagnostic.source_name.clone(),
            diagnostic.range,
        );
    }
}

fn run_parsed_program<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &Program,
    raw_command: Option<&str>,
    command_id: Option<&str>,
) -> i32 {
    let canonical = program.to_canonical();
    if state.has_option(OPT_NOEXEC) && !state.interactive {
        0
    } else {
        if state.has_option(super::OPT_VERBOSE) {
            shell_errln(state, &canonical);
        }
        run_program_with_context(state, runtime, program, raw_command, command_id)
    }
}

fn report_parser_error(
    state: &ShellState,
    source_name: Option<&str>,
    err: &crate::parser::ParseError,
) {
    let source = err
        .source_name
        .as_deref()
        .or(source_name)
        .unwrap_or(state.shell_name());
    if matches!(
        err.message.as_str(),
        "unterminated single quote" | "unterminated double quote" | "unterminated backquote"
    ) {
        let quote = if err.message == "unterminated single quote" {
            "'"
        } else if err.message == "unterminated double quote" {
            "\""
        } else {
            "`"
        };
        let start_line = err.pos.line.saturating_sub(1).max(1);
        let first = format!(
            "{source}: line {start_line}: unexpected EOF while looking for matching `{quote}'"
        );
        let second = format!(
            "{source}: line {}: syntax error: unexpected end of file",
            err.pos.line
        );
        let _ = state.stderr_fd.write_line(&first);
        let _ = state.stderr_fd.write_line(&second);
        emit_diagnostic(
            state,
            DiagnosticKind::Error,
            DiagnosticCategory::Input,
            err.code,
            &err.message,
            err.source_name.clone().or_else(|| Some(source.to_string())),
            Some(err.range),
        );
        return;
    }
    let message = if err.source_name.is_some() {
        err.to_string()
    } else {
        state.prefixed_message(err.to_string())
    };
    let _ = state.stderr_fd.write_line(&message);
    emit_diagnostic(
        state,
        DiagnosticKind::Error,
        DiagnosticCategory::Input,
        err.code,
        &err.message,
        err.source_name.clone().or_else(|| Some(source.to_string())),
        Some(err.range),
    );
}