use crate::ast::Program;
use crate::embed::{DiagnosticCategory, DiagnosticKind};
use crate::parser::Parser;
use crate::sys::Runtime;
use super::frontend::emit_diagnostic;
use super::{OPT_NOEXEC, ShellState, shell_errln, shell_events, shell_outln};
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)
}
pub(crate) fn run_string<R: Runtime>(state: &mut ShellState, runtime: &mut R, text: &str) -> i32 {
run_string_with_context(state, runtime, text, None, None)
}
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, text, source_name, None)
}
fn run_string_with_context<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
text: &str,
source_name: Option<&str>,
command_id: Option<&str>,
) -> i32 {
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::from_string(text);
crate::parser::configure_parser_for_language(&mut parser, &state.definition.language);
parser.set_source_name(Some(active_source));
let mut status = 0;
loop {
match parse_program_step(state, &mut parser, 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 {
break;
}
}
ProgramParseStep::Eof => break,
ProgramParseStep::ParseError(parse_status) => {
status = parse_status;
break;
}
}
}
state.set_current_source(old_source);
status
}
enum ProgramParseStep {
Program {
program: Program,
raw_command: String,
},
Eof,
ParseError(i32),
}
fn parse_program_step(
state: &ShellState,
parser: &mut Parser,
source_text: &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 = parser.current_pos().offset.min(source_text.len());
match parser.parse_line() {
Ok(Some(program)) => {
emit_parser_warnings(state, parser);
let end = parser.current_pos().offset.min(source_text.len());
ProgramParseStep::Program {
program,
raw_command: source_text[start..end].trim_end_matches('\n').to_string(),
}
}
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) {
shell_outln(state, &canonical);
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),
);
}