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()))
}