mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
//! Frontend adapters for interactive and CLI use.

use std::fs::OpenOptions;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use crate::advanced::SessionState;
use crate::args::process_args;
#[cfg(feature = "unix-runtime")]
use crate::embed::ShellBuilder;
use crate::embed::{RunOutcome, Shell, ShellOptions, capture_outcome_for_state};
use crate::policy::StartupPolicy;
use crate::runtime::Runtime;
#[cfg(feature = "unix-runtime")]
use crate::runtime::unix::UnixRuntime;
use crate::shell;

pub trait InteractiveFrontend {
    fn prompt(&mut self, shell: &SessionState, continuation: bool) -> io::Result<String>;
    fn read_line(&mut self, shell: &mut SessionState, prompt: &str) -> io::Result<Option<String>>;

    fn append_history(&mut self, _shell: &SessionState, _line: &str) -> io::Result<()> {
        Ok(())
    }

    fn on_unsupported_vi_mode(&mut self, _shell: &mut SessionState) -> io::Result<()> {
        Ok(())
    }
}

#[derive(Debug, Default)]
pub struct FdFrontend;

impl InteractiveFrontend for FdFrontend {
    fn prompt(&mut self, shell: &SessionState, continuation: bool) -> io::Result<String> {
        Ok(if continuation {
            shell.env_get("PS2").unwrap_or("> ").to_string()
        } else {
            shell.env_get("PS1").unwrap_or("$ ").to_string()
        })
    }

    fn read_line(&mut self, shell: &mut SessionState, prompt: &str) -> io::Result<Option<String>> {
        shell.write_stdout(prompt)?;
        shell.stdio().stdin.read_line()
    }

    fn append_history(&mut self, shell: &SessionState, line: &str) -> io::Result<()> {
        if line.trim().is_empty() || shell.has_option(ShellOptions::NOLOG) {
            return Ok(());
        }
        if let Some(history_appender) = shell.history_appender() {
            (history_appender.as_ref())(line);
            return Ok(());
        }
        let Some(path) = resolve_history_path(shell) else {
            return Ok(());
        };
        let mut file = OpenOptions::new().create(true).append(true).open(path)?;
        writeln!(file, "{line}")?;
        Ok(())
    }

    fn on_unsupported_vi_mode(&mut self, shell: &mut SessionState) -> io::Result<()> {
        shell.warn_vi_unsupported_once();
        Ok(())
    }
}

#[cfg(feature = "unix-runtime")]
pub fn run_cli(argv: &[String]) -> RunOutcome {
    let mut shell = ShellBuilder::new()
        .manage_signals(true)
        .build(UnixRuntime::new())
        .expect("default CLI shell should build");
    run_cli_with_shell(argv, &mut shell)
}

pub(crate) fn run_cli_with_shell<R: Runtime>(argv: &[String], shell: &mut Shell<R>) -> RunOutcome {
    let (session, runtime) = shell.parts_mut();
    run_cli_with_session(argv, session, runtime)
}

fn run_cli_with_session<R: Runtime>(
    argv: &[String],
    session: &mut SessionState,
    runtime: &mut R,
) -> RunOutcome {
    if session.security_policy().allow_ambient_fds() {
        session.state_mut().import_ambient_fds();
    }
    shell::clear_pending_traps(session.state());
    let init = match process_args(session.state_mut(), argv) {
        Ok(init) => init,
        Err(err) => {
            shell::report_arg_error(err, session.identity());
            return RunOutcome::empty_from_state(1, session.state());
        }
    };

    install_cli_adapters(session);
    if !session.has_explicit_startup_policy() {
        session.set_startup_policy(cli_startup_policy(
            argv,
            session.interactive(),
            session.has_option(ShellOptions::NOEXEC),
        ));
    }

    if init.machine_mode {
        let payload_text = match shell::load_machine_payload_text(&init, session.identity()) {
            Ok(payload_text) => payload_text,
            Err(err) => {
                eprintln!("{err}");
                return RunOutcome::empty_from_state(1, session.state());
            }
        };
        let mut outcome = capture_outcome_for_state(session.state_mut(), |state| {
            let status = shell::run_machine_payload(state, runtime, &payload_text);
            state.set_last_status(status);
            status
        });
        let final_status = shell::finalize_shell_run(session.state_mut(), runtime, outcome.status);
        outcome.status = final_status;
        outcome.set_exit_code(session.exit_code().map(|_| final_status));
        outcome.update_last_run_finished_status(final_status);
        return outcome;
    }

    let mut outcome = capture_outcome_for_state(session.state_mut(), |state| {
        shell::initialize_shell_session(state, runtime);
        let status = if state.interactive {
            shell::run_interactive(state, runtime).unwrap_or(1)
        } else {
            shell::run_non_interactive(state, runtime, &init)
        };
        state.set_last_status(status);
        status
    });
    let final_status = shell::finalize_shell_run(session.state_mut(), runtime, outcome.status);
    outcome.status = final_status;
    outcome.set_exit_code(session.exit_code().map(|_| final_status));
    outcome.update_last_run_finished_status(final_status);
    outcome
}

fn cli_startup_policy(argv: &[String], interactive: bool, noexec: bool) -> StartupPolicy {
    if !interactive || noexec {
        return StartupPolicy::None;
    }
    let login = shell::is_login_shell_argv(argv);
    match login {
        true => StartupPolicy::PosixLoginFilesAndInteractiveEnvHook,
        false => StartupPolicy::InteractiveEnvHook,
    }
}

fn install_cli_adapters(shell: &mut SessionState) {
    if let Some(path) = resolve_history_path(shell) {
        shell.set_history_path(Some(path));
    }
}

fn resolve_history_path(shell: &SessionState) -> Option<PathBuf> {
    if let Some(path) = shell.history_path() {
        return Some(path.to_path_buf());
    }
    if !shell.security_policy().allow_implicit_history_file() {
        return None;
    }
    if let Some(path) = shell
        .env_get(shell.identity().history_env_var())
        .filter(|path| !path.trim().is_empty())
    {
        return Some(PathBuf::from(path));
    }
    if let Some(path) = shell
        .env_get("HISTFILE")
        .filter(|path| !path.trim().is_empty())
    {
        return Some(PathBuf::from(path));
    }
    shell
        .env_get("HOME")
        .map(|home| Path::new(home).join(shell.identity().default_history_file()))
}