mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
use super::compound::{
    complete_command_list_step, run_case_clause_with, run_for_clause_with, run_if_clause_with,
    run_loop_clause_iteration, run_loop_core,
};
use super::simple_command::{run_planned_simple_command, run_simple_command};
use super::*;

fn duplicate_shell_fd(fd: sys::FileDescriptor) -> Result<sys::FileDescriptor, io::Error> {
    if fd.is_valid() {
        fd.dup()
    } else {
        Ok(sys::FileDescriptor::INVALID)
    }
}

fn run_with_owned_stdio<R: Runtime>(
    state: &mut ShellState,
    run: impl FnOnce(&mut ShellState, &mut R) -> i32,
    runtime: &mut R,
    stdin_fd: sys::FileDescriptor,
    stdout_fd: sys::FileDescriptor,
    stderr_fd: sys::FileDescriptor,
) -> i32 {
    let old_stdin = state.stdin_fd;
    let old_stdout = state.stdout_fd;
    let old_stderr = state.stderr_fd;

    let owned_stdin = match duplicate_shell_fd(stdin_fd) {
        Ok(fd) => fd,
        Err(err) => {
            report_stdio_dup_error(state, "stdin", &err);
            return 1;
        }
    };
    let owned_stdout = match duplicate_shell_fd(stdout_fd) {
        Ok(fd) => fd,
        Err(err) => {
            owned_stdin.close();
            report_stdio_dup_error(state, "stdout", &err);
            return 1;
        }
    };
    let owned_stderr = match duplicate_shell_fd(stderr_fd) {
        Ok(fd) => fd,
        Err(err) => {
            owned_stdin.close();
            owned_stdout.close();
            report_stdio_dup_error(state, "stderr", &err);
            return 1;
        }
    };

    state.stdin_fd = owned_stdin;
    state.stdout_fd = owned_stdout;
    state.stderr_fd = owned_stderr;
    let status = run(state, runtime);
    for fd in [state.stdin_fd, state.stdout_fd, state.stderr_fd] {
        fd.close();
    }
    state.stdin_fd = old_stdin;
    state.stdout_fd = old_stdout;
    state.stderr_fd = old_stderr;
    status
}

pub(super) fn run_command_with_stdio<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    cmd: &AstCommand,
    stdin_fd: sys::FileDescriptor,
    stdout_fd: sys::FileDescriptor,
    stderr_fd: sys::FileDescriptor,
) -> i32 {
    run_with_owned_stdio(
        state,
        |state, runtime| run_command(state, runtime, cmd),
        runtime,
        stdin_fd,
        stdout_fd,
        stderr_fd,
    )
}

pub(super) fn run_planned_simple_command_with_stdio<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    plan: &PlannedSimpleCommand<'_>,
    stdin_fd: sys::FileDescriptor,
    stdout_fd: sys::FileDescriptor,
    stderr_fd: sys::FileDescriptor,
) -> i32 {
    run_with_owned_stdio(
        state,
        |state, runtime| run_planned_simple_command(state, runtime, plan),
        runtime,
        stdin_fd,
        stdout_fd,
        stderr_fd,
    )
}

fn duplicate_subshell_stdio(
    state: &ShellState,
) -> Result<
    (
        sys::FileDescriptor,
        sys::FileDescriptor,
        sys::FileDescriptor,
    ),
    io::Error,
> {
    Ok((
        duplicate_shell_fd(state.stdin_fd)?,
        duplicate_shell_fd(state.stdout_fd)?,
        duplicate_shell_fd(state.stderr_fd)?,
    ))
}

fn close_subshell_stdio(state: &ShellState) {
    for fd in [state.stdin_fd, state.stdout_fd, state.stderr_fd] {
        if fd.is_valid() {
            fd.close();
        }
    }
}

fn report_stdio_dup_error(state: &ShellState, stream: &str, err: &io::Error) {
    shell_errln(
        state,
        &state.prefixed_message(format!("failed to duplicate {stream}: {err}")),
    );
}

fn run_redirected_body<R: Runtime, T: ?Sized>(
    state: &mut ShellState,
    runtime: &mut R,
    io_redirects: &[IoRedirect],
    body: &T,
    run_body: impl FnOnce(&mut ShellState, &mut R, &T) -> i32,
) -> i32 {
    let Ok(redirects) = RedirectGuard::apply(state, runtime, io_redirects) else {
        return 1;
    };
    let status = run_body(state, runtime, body);
    redirects.restore(state);
    status
}

fn run_redirected_subshell<R: Runtime, T: ?Sized>(
    state: &mut ShellState,
    runtime: &mut R,
    io_redirects: &[IoRedirect],
    body: &T,
    run_body: impl FnOnce(&mut ShellState, &mut R, &T) -> i32,
) -> i32 {
    let Ok(redirects) = RedirectGuard::apply(state, runtime, io_redirects) else {
        return 1;
    };
    let mut child_runtime = match runtime.fork() {
        Ok(runtime) => runtime,
        Err(err) => {
            redirects.restore(state);
            shell_errln(
                state,
                &state.prefixed_message(format!("failed to fork subshell runtime: {err}")),
            );
            return 1;
        }
    };
    let mut child = state.fork_session();
    match duplicate_subshell_stdio(state) {
        Ok(stdio) => {
            (child.stdin_fd, child.stdout_fd, child.stderr_fd) = stdio;
        }
        Err(err) => {
            redirects.restore(state);
            report_stdio_dup_error(state, "subshell stdio", &err);
            return 1;
        }
    }
    let status = run_body(&mut child, &mut child_runtime, body);
    close_subshell_stdio(&child);
    redirects.restore(state);
    status
}

pub(super) fn run_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    cmd: &AstCommand,
) -> i32 {
    if state.has_option(OPT_NOEXEC) {
        return 0;
    }

    match cmd {
        AstCommand::Simple(sc) => run_simple_command(state, runtime, sc),
        AstCommand::BraceGroup(group) => run_redirected_body(
            state,
            runtime,
            group.io_redirects(),
            group.body(),
            |state, runtime, body| run_built_command_list_array_from_ast(state, runtime, body),
        ),
        AstCommand::Subshell(group) => run_redirected_subshell(
            state,
            runtime,
            group.io_redirects(),
            group.body(),
            |state, runtime, body| run_built_command_list_array_from_ast(state, runtime, body),
        ),
        AstCommand::If(if_clause) => run_if_clause_with(
            state,
            runtime,
            if_clause,
            run_built_command_list_array_from_ast,
        ),
        AstCommand::For(clause) => run_for_clause_with(
            state,
            runtime,
            clause,
            run_built_command_list_array_from_ast,
        ),
        AstCommand::Loop(clause) => run_loop_core(state, runtime, |state, runtime| {
            run_loop_clause_iteration(
                state,
                runtime,
                clause,
                run_built_command_list_array_from_ast,
            )
        }),
        AstCommand::Case(clause) => run_case_clause_with(
            state,
            runtime,
            clause,
            run_built_command_list_array_from_ast,
        ),
        AstCommand::FunctionDef(function_definition) => {
            run_function_def(state, function_definition)
        }
    }
}

fn execute_background<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    raw_and_or_list: &AndOrList,
) -> i32 {
    if !state.definition.security_policy.allow_background_jobs() {
        shell_errln(
            state,
            "background jobs are disabled by shell security policy",
        );
        return 1;
    }
    match shell_jobs::run_background(state, runtime, raw_and_or_list) {
        Ok(pid) => {
            state.set_last_background_pid(pid);
            0
        }
        Err(err) => {
            state.set_last_background_pid(None);
            err
        }
    }
}

struct CommandListOutcome {
    status: i32,
}

fn execute_built_command_list<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    cl: &CommandList,
) -> CommandListOutcome {
    if cl.ampersand {
        return CommandListOutcome {
            status: execute_background(state, runtime, &cl.and_or_list),
        };
    }
    let planned = exec::build_runtime_and_or_list(state, runtime, &cl.and_or_list);
    let status = exec::run_exec_and_or_list(state, runtime, &planned);
    CommandListOutcome { status }
}

fn execute_planned_command_list<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    cl: &ExecCommandList<'_>,
) -> CommandListOutcome {
    if cl.ampersand() {
        return CommandListOutcome {
            status: execute_background(state, runtime, cl.raw_and_or_list()),
        };
    }
    let planned = exec::realize_runtime_and_or_list(state, runtime, cl.and_or_list());
    let status = exec::run_exec_and_or_list(state, runtime, &planned);
    CommandListOutcome { status }
}

fn run_command_list_array_with<R: Runtime, T>(
    state: &mut ShellState,
    runtime: &mut R,
    lists: &[T],
    mut execute_list: impl FnMut(&mut ShellState, &mut R, &T) -> CommandListOutcome,
) -> i32 {
    let mut status = 0;
    for cl in lists {
        if state.exit_code >= 0 || state.branch != BranchControl::None {
            break;
        }
        let outcome = execute_list(state, runtime, cl);
        status = outcome.status;
        complete_command_list_step(state, runtime, status);
    }
    status
}

fn run_built_command_list_array_from_ast<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    lists: &[CommandList],
) -> i32 {
    run_command_list_array_with(state, runtime, lists, execute_built_command_list)
}

pub(super) fn run_exec_command_list_array<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    lists: &[ExecCommandList<'_>],
) -> i32 {
    run_command_list_array_with(state, runtime, lists, execute_planned_command_list)
}

fn run_function_def(state: &mut ShellState, fd: &FunctionDefinition) -> i32 {
    state.functions.insert(fd.name.clone(), *fd.body.clone());
    0
}