#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use std::io;
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
use crate::args::ArgError;
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use crate::args::process_args;
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use crate::parser::{ParseError, Parser};
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
use crate::policy::ShellIdentity;
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use crate::sys::Runtime;
#[cfg(all(test, feature = "test-support"))]
use super::driver as shell_driver;
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use super::shell_traps;
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use super::{OPT_IGNOREEOF, OPT_NOTIFY, append_history_line, run_string, shell_jobs, shell_out};
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use super::{OPT_NOEXEC, machine as shell_machine};
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use super::{ShellState, shell_errln};
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) fn is_login_shell_argv(argv: &[String]) -> bool {
argv.first().is_some_and(|arg0| arg0.starts_with('-'))
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(super) fn should_source_profile(state: &ShellState, argv: &[String]) -> bool {
(state.interactive && !state.has_option(OPT_NOEXEC) && is_login_shell_argv(argv))
|| (state.interactive
&& !state.has_option(OPT_NOEXEC)
&& state.definition.startup_policy.should_source_profile())
}
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
fn parse_error_needs_more_input(buffer: &str, err: &ParseError) -> bool {
err.range.end.offset >= buffer.len()
}
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
fn interactive_prompt(state: &ShellState, continuation: bool) -> String {
if continuation {
state.env_get("PS2").unwrap_or("> ").to_string()
} else {
state.env_get("PS1").unwrap_or("$ ").to_string()
}
}
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
fn interactive_buffer_needs_more_input(state: &ShellState, buffer: &str) -> bool {
let mut parser = Parser::from_string(buffer);
crate::parser::configure_parser_for_language(&mut parser, &state.definition.language);
let aliases = state.aliases_snapshot();
parser.set_alias_func(crate::parser::AliasFn::new(move |name| {
aliases.get(name).cloned()
}));
match parser.parse_program() {
Ok(_) => false,
Err(err) => parse_error_needs_more_input(buffer, &err),
}
}
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
pub fn run_interactive<R: Runtime>(state: &mut ShellState, runtime: &mut R) -> io::Result<i32> {
let mut pending = String::new();
loop {
if pending.is_empty() && state.has_option(OPT_NOTIFY) {
shell_jobs::maybe_notify_jobs(state, runtime);
}
let prompt = interactive_prompt(state, !pending.is_empty());
shell_out(state, &prompt)?;
let Some(input) = state.stdin_fd.read_line()? else {
if pending.is_empty() && state.has_option(OPT_IGNOREEOF) {
shell_errln(state, "Use \"exit\" to leave the shell.");
continue;
}
if !pending.is_empty() {
let code = run_string(state, runtime, &pending);
state.set_last_status(code);
}
break;
};
if !pending.is_empty() {
pending.push('\n');
}
pending.push_str(&input);
if interactive_buffer_needs_more_input(state, &pending) {
continue;
}
append_history_line(state, &pending);
let code = run_string(state, runtime, &pending);
state.set_last_status(code);
pending.clear();
if state.exit_code >= 0 {
break;
}
}
Ok(if state.exit_code >= 0 {
state.exit_code
} else {
state.last_status
})
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub fn run(argv: &[String]) -> i32 {
let mut runtime = crate::sys::UnixRuntime::new();
let mut state = ShellState::new();
run_with_state(argv, &mut state, &mut runtime)
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(super) fn run_with_state<R: Runtime>(
argv: &[String],
state: &mut ShellState,
runtime: &mut R,
) -> i32 {
state.manage_signals = true;
shell_traps::clear_pending_traps(state);
let init = match process_args(state, argv) {
Ok(init) => init,
Err(err) => {
report_arg_error(err, &ShellIdentity::default());
return 1;
}
};
if init.machine_mode {
let payload_text =
match shell_machine::load_machine_payload_text(&init, &ShellIdentity::default()) {
Ok(payload_text) => payload_text,
Err(err) => {
eprintln!("{err}");
return 1;
}
};
let status = super::exec::run_machine_payload(state, runtime, &payload_text);
return finalize_shell_run(state, runtime, status);
}
shell_driver::initialize_shell_session(state, runtime);
let status = if state.interactive {
run_interactive(state, runtime).unwrap_or(1)
} else {
shell_driver::run_non_interactive(state, runtime, &init)
};
finalize_shell_run(state, runtime, status)
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) fn report_arg_error(err: ArgError, identity: &ShellIdentity) {
match err {
ArgError::Usage => {
let shell_name = identity.name();
eprintln!("usage: {shell_name} [(-|+)abCefhmnuvx] [-o option] [args...]");
eprintln!(" {shell_name} [(-|+)abCefhmnuvx] [+o option] [args...]");
eprintln!(" {shell_name} --machine -c <payload>");
eprintln!(" {shell_name} -- [args...]");
}
other => eprintln!("{}: {other}", identity.name()),
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) fn finalize_shell_run<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
status: i32,
) -> i32 {
super::finalize_shell_state(state, runtime, status)
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::sys::{DeterministicRuntime, StringStdioIn, StringStdioOut};
use super::*;
fn temp_path(label: &str) -> PathBuf {
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
std::env::temp_dir().join(format!(
"mxsh-{label}-{}-{}",
std::process::id(),
NEXT_ID.fetch_add(1, Ordering::Relaxed)
))
}
#[test]
fn interactive_multiline_if_uses_continuation_prompt() {
let input = StringStdioIn::new("if true\nthen echo hi\nfi\nexit\n");
let output = StringStdioOut::new();
let mut state = ShellState::new();
state.populate_env();
state.interactive = true;
state.stdin_fd = input.fd();
state.stdout_fd = output.fd();
let mut runtime = DeterministicRuntime::new();
let status = run_interactive(&mut state, &mut runtime).expect("interactive run succeeds");
input.join();
let out = output.collect();
assert_eq!(status, 0);
assert!(out.contains("hi\n"), "expected command output, got {out:?}");
assert!(
out.contains("> "),
"expected continuation prompt in output, got {out:?}"
);
}
#[test]
fn interactive_notify_reports_completed_job_before_next_prompt() {
let input = StringStdioIn::new("sleep 1 &\nexit\n");
let output = StringStdioOut::new();
let mut state = ShellState::new();
state.populate_env();
state.interactive = true;
state.options |= OPT_NOTIFY;
state.stdin_fd = input.fd();
state.stdout_fd = output.fd();
state.stderr_fd = output.fd();
let mut runtime = DeterministicRuntime::new();
runtime.push_spawn(
Some(4242),
[
crate::sys::ProcessEvent::Running,
crate::sys::ProcessEvent::Exited(0),
],
[],
);
let status = run_interactive(&mut state, &mut runtime).expect("interactive run succeeds");
input.join();
let out = output.collect();
assert_eq!(status, 0);
let notify_idx = out
.find("[1] Done(0) 4242")
.expect("expected job notification");
let prompt_idx = out[notify_idx..]
.find("$ ")
.map(|idx| notify_idx + idx)
.expect("expected prompt after notification");
assert!(
notify_idx < prompt_idx,
"expected notification before next prompt, got {out:?}"
);
}
#[test]
fn interactive_env_file_is_sourced_and_can_define_helper_function() {
let env_path = temp_path("interactive-env");
fs::write(&env_path, "helper() { echo from_env; }\n").expect("write ENV file");
let input = StringStdioIn::new("helper\nexit\n");
let output = StringStdioOut::new();
let mut state = ShellState::new();
state.interactive = true;
std::sync::Arc::make_mut(&mut state.definition).startup_policy =
crate::policy::StartupPolicy::InteractiveEnvHook;
state.env_set("ENV", env_path.display().to_string(), 0);
state.stdin_fd = input.fd();
state.stdout_fd = output.fd();
let mut runtime = DeterministicRuntime::new();
shell_driver::initialize_shell_session(&mut state, &mut runtime);
let status = run_interactive(&mut state, &mut runtime).expect("interactive run succeeds");
input.join();
let out = output.collect();
assert_eq!(status, 0);
assert!(
out.contains("from_env\n"),
"expected helper output, got {out:?}"
);
let _ = fs::remove_file(env_path);
}
}