use std::{
collections::VecDeque,
io::Write,
path::{Path, PathBuf},
sync::Arc,
};
use brush_parser::ast::{self, CommandPrefixOrSuffixItem};
use itertools::Itertools;
use tokio_util::sync::CancellationToken;
use crate::{
ShellFd,
arithmetic::{self, ExpandAndEvaluate},
commands::{self, CommandArg},
env::{EnvironmentLookup, EnvironmentScope, valid_variable_name},
error, expansion, extendedtests, extensions, ioutils, jobs, openfiles,
openfiles::{OpenFile, OpenFiles},
results::{ExecutionExitCode, ExecutionResult, ExecutionSpawnResult, ExecutionWaitResult},
shell::Shell,
sys, timing,
variables::{ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable},
};
struct PipelineExecutionContext<'a, SE: extensions::ShellExtensions> {
shell: commands::ShellForCommand<'a, SE>,
process_group_id: Option<i32>,
in_pipeline: bool,
}
pub struct ExternalCommandInfo<'a> {
pub command_name: &'a str,
pub executable_path: &'a str,
pub args: Vec<&'a str>,
}
#[derive(Clone)]
pub struct ExternalCommandOutputMarkers {
pub start_marker: String,
pub end_marker_prefix: String,
pub end_marker_suffix: String,
}
pub trait ExternalCommandOutputMarker: Send + Sync {
fn markers_for_external_command(
&self,
info: ExternalCommandInfo<'_>,
) -> Option<ExternalCommandOutputMarkers>;
}
#[derive(Clone, Default)]
pub struct ExecutionParameters {
open_files: openfiles::OpenFiles,
pub process_group_policy: ProcessGroupPolicy,
cancel_token: Option<CancellationToken>,
command_output_marker: Option<Arc<dyn ExternalCommandOutputMarker>>,
command_output_disabled: bool,
pub suppress_errexit: bool,
}
impl ExecutionParameters {
pub fn set_cancel_token(&mut self, token: CancellationToken) {
self.cancel_token = Some(token);
}
pub fn cancel_token(&self) -> Option<CancellationToken> {
self.cancel_token.clone()
}
pub fn is_cancelled(&self) -> bool {
self
.cancel_token
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
}
pub fn set_command_output_marker(&mut self, marker: Arc<dyn ExternalCommandOutputMarker>) {
self.command_output_marker = Some(marker);
self.command_output_disabled = false;
}
pub fn disable_command_output_marking(&mut self) {
self.command_output_disabled = true;
}
pub fn command_output_marker(&self) -> Option<&Arc<dyn ExternalCommandOutputMarker>> {
if self.command_output_disabled {
return None;
}
self.command_output_marker.as_ref()
}
pub fn stdin(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> impl std::io::Read + 'static {
self.try_stdin(shell).unwrap_or_else(|| {
ioutils::FailingReaderWriter::new("standard input not available").into()
})
}
pub fn try_stdin(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
self.try_fd(shell, openfiles::OpenFiles::STDIN_FD)
}
pub fn stdout(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> impl std::io::Write + 'static {
self.try_stdout(shell).unwrap_or_else(|| {
ioutils::FailingReaderWriter::new("standard output not available").into()
})
}
pub fn try_stdout(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
self.try_fd(shell, openfiles::OpenFiles::STDOUT_FD)
}
pub fn stderr(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> impl std::io::Write + 'static {
self.try_stderr(shell).unwrap_or_else(|| {
ioutils::FailingReaderWriter::new("standard error not available").into()
})
}
pub fn try_stderr(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
self.try_fd(shell, openfiles::OpenFiles::STDERR_FD)
}
pub fn try_fd(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
fd: ShellFd,
) -> Option<openfiles::OpenFile> {
match self.open_files.fd_entry(fd) {
openfiles::OpenFileEntry::Open(f) => Some(f.clone()),
openfiles::OpenFileEntry::NotPresent => None,
openfiles::OpenFileEntry::NotSpecified => {
shell.persistent_open_files().try_fd(fd).cloned()
},
}
}
pub fn set_fd(&mut self, fd: ShellFd, file: openfiles::OpenFile) {
self.open_files.set_fd(fd, file);
}
pub fn iter_fds(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
let our_fds = self.open_files.iter_fds();
let shell_fds = shell
.persistent_open_files()
.iter_fds()
.filter(|(fd, _)| !self.open_files.contains_fd(*fd));
#[allow(clippy::needless_collect)]
let all_fds: Vec<_> = our_fds
.chain(shell_fds)
.map(|(fd, file)| (fd, file.clone()))
.collect();
all_fds.into_iter()
}
}
fn ensure_not_cancelled(params: &ExecutionParameters) -> Result<(), error::Error> {
if params.is_cancelled() {
return Err(error::ErrorKind::Interrupted.into());
}
Ok(())
}
#[derive(Clone, Debug, Default)]
pub enum ProcessGroupPolicy {
#[default]
NewProcessGroup,
SameProcessGroup,
}
#[async_trait::async_trait]
pub trait Execute {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error>;
}
#[async_trait::async_trait]
trait ExecuteInPipeline<SE: extensions::ShellExtensions> {
async fn execute_in_pipeline(
&self,
context: PipelineExecutionContext<'_, SE>,
params: ExecutionParameters,
) -> Result<ExecutionSpawnResult, error::Error>;
}
#[async_trait::async_trait]
impl Execute for ast::Program {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut result = ExecutionResult::success();
for command in &self.complete_commands {
ensure_not_cancelled(params)?;
match command.execute(shell, params).await {
Ok(exec_result) => result = exec_result,
Err(err) => {
let _ = shell.display_error(&mut params.stderr(shell), &err);
result = err.into_result(shell);
},
}
shell.set_last_exit_status(result.exit_code.into());
if !result.is_normal_flow() {
break;
}
}
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::CompoundList {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut result = ExecutionResult::success();
for ast::CompoundListItem(ao_list, sep) in &self.0 {
ensure_not_cancelled(params)?;
let run_async = matches!(sep, ast::SeparatorOperator::Async);
if run_async {
let job = spawn_async_ao_list_as_job(ao_list, shell, params).await?;
let job_formatted = job.to_pid_style_string();
if shell.options().interactive && !shell.is_subshell() {
writeln!(params.stderr(shell), "{job_formatted}")?;
}
result = ExecutionResult::success();
} else {
result = ao_list.execute(shell, params).await?;
shell.set_last_exit_status(result.exit_code.into());
}
if !result.is_normal_flow() {
break;
}
}
Ok(result)
}
}
async fn spawn_async_ao_list_as_job<'a, SE: extensions::ShellExtensions>(
ao_list: &ast::AndOrList,
shell: &'a mut Shell<SE>,
params: &ExecutionParameters,
) -> Result<&'a jobs::Job, error::Error> {
let mut async_params = params.clone();
async_params.disable_command_output_marking();
if let Ok(null) = openfiles::null() {
async_params.set_fd(openfiles::OpenFiles::STDIN_FD, null);
}
let job = if should_try_spawn_pipeline_as_job(ao_list, shell, &async_params).await? {
match try_spawn_pipeline_as_job(&ao_list.first, ao_list.to_string(), shell, &async_params)
.await?
{
Some(job) => job,
None => spawn_async_ao_list_in_task(ao_list, shell, &async_params),
}
} else {
spawn_async_ao_list_in_task(ao_list, shell, &async_params)
};
Ok(shell.jobs_mut().add_as_current(job))
}
async fn should_try_spawn_pipeline_as_job<SE: extensions::ShellExtensions>(
ao_list: &ast::AndOrList,
shell: &mut Shell<SE>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
if !ao_list.additional.is_empty() {
return Ok(false);
}
let pipeline = &ao_list.first;
if pipeline.bang {
return Ok(false);
}
let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
return Ok(false);
};
let Some(command_word) = simple_cmd.word_or_name.as_ref() else {
return Ok(false);
};
let expanded = expansion::full_expand_and_split_word(shell, params, command_word).await?;
let [command_name] = expanded.as_slice() else {
return Ok(false);
};
if shell.aliases().contains_key(command_name) {
return Ok(false);
}
if shell
.builtins()
.get(command_name.as_str())
.is_some_and(|registration| !registration.disabled)
{
return Ok(false);
}
if shell.funcs().get(command_name.as_str()).is_some() {
return Ok(false);
}
Ok(true)
}
async fn try_spawn_pipeline_as_job<SE: extensions::ShellExtensions>(
pipeline: &ast::Pipeline,
command_line: String,
shell: &mut Shell<SE>,
params: &ExecutionParameters,
) -> Result<Option<jobs::Job>, error::Error> {
let mut subshell = shell.clone();
subshell.options_mut().interactive = false;
let spawn_results = spawn_pipeline_processes(pipeline, &mut subshell, params).await?;
let mut tasks = VecDeque::new();
for spawn_result in spawn_results {
if let ExecutionWaitResult::Stopped(child) = spawn_result.poll().await? {
tasks.push_back(jobs::JobTask::External(child));
}
}
if tasks.is_empty() {
return Ok(None);
}
Ok(Some(jobs::Job::new(tasks, command_line, jobs::JobState::Running)))
}
fn spawn_async_ao_list_in_task<SE: extensions::ShellExtensions>(
ao_list: &ast::AndOrList,
shell: &mut Shell<SE>,
params: &ExecutionParameters,
) -> jobs::Job {
let mut cloned_shell = shell.clone();
let cloned_params = params.clone();
let cloned_ao_list = ao_list.clone();
cloned_shell.options_mut().interactive = false;
let join_handle = tokio::spawn(async move {
cloned_ao_list
.execute(&mut cloned_shell, &cloned_params)
.await
});
jobs::Job::new(
[jobs::JobTask::Internal(join_handle)],
ao_list.to_string(),
jobs::JobState::Running,
)
}
#[async_trait::async_trait]
impl Execute for ast::AndOrList {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let has_operators = !self.additional.is_empty();
let mut first_params = params.clone();
if has_operators {
first_params.suppress_errexit = true;
}
let mut result = self.first.execute(shell, &first_params).await?;
for (index, next_ao) in self.additional.iter().enumerate() {
ensure_not_cancelled(params)?;
if !result.is_normal_flow() {
break;
}
let (is_and, pipeline) = match next_ao {
ast::AndOr::And(p) => (true, p),
ast::AndOr::Or(p) => (false, p),
};
if is_and {
if !result.is_success() {
continue;
}
} else if result.is_success() {
continue;
}
let mut params = params.clone();
let is_last = index == self.additional.len() - 1;
if !is_last {
params.suppress_errexit = true;
}
result = pipeline.execute(shell, ¶ms).await?;
}
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::Pipeline {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let stopwatch = self
.timed
.is_some()
.then(timing::start_timing)
.transpose()?;
let mut params = params.clone();
if self.bang {
params.suppress_errexit = true;
}
let spawn_results = spawn_pipeline_processes(self, shell, ¶ms).await?;
let mut result =
wait_for_pipeline_processes_and_update_status(self, spawn_results, shell, ¶ms).await?;
if self.bang {
result.exit_code = ExecutionExitCode::from(if result.is_success() { 1 } else { 0 });
}
shell.set_last_exit_status(result.exit_code.into());
if !result.is_success() && !params.suppress_errexit && !self.bang {
if shell.traps().handles(crate::traps::TrapSignal::Err) {
shell
.invoke_trap_handler(crate::traps::TrapSignal::Err, ¶ms)
.await?;
}
}
if !params.suppress_errexit && !self.bang {
shell.apply_errexit_if_enabled(&mut result);
}
if let (Some(timed), Some(stopwatch)) = (&self.timed, &stopwatch)
&& let Some(mut stderr) = params.try_fd(shell, openfiles::OpenFiles::STDERR_FD)
{
let timing = stopwatch.stop()?;
if timed.is_posix_output() {
std::write!(
stderr,
"real {}\nuser {}\nsys {}\n",
timing::format_duration_posixly(&timing.wall),
timing::format_duration_posixly(&timing.user),
timing::format_duration_posixly(&timing.system),
)?;
} else {
std::write!(
stderr,
"\nreal\t{}\nuser\t{}\nsys\t{}\n",
timing::format_duration_non_posixly(&timing.wall),
timing::format_duration_non_posixly(&timing.user),
timing::format_duration_non_posixly(&timing.system),
)?;
}
}
Ok(result)
}
}
async fn spawn_pipeline_processes(
pipeline: &ast::Pipeline,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<VecDeque<ExecutionSpawnResult>, error::Error> {
ensure_not_cancelled(params)?;
let pipeline_len = pipeline.seq.len();
let mut pipe_readers = vec![];
let mut pipe_writers = vec![];
let mut spawn_results = VecDeque::new();
let mut process_group_id: Option<i32> = None;
if pipeline_len > 1 {
for _ in 0..(pipeline_len - 1) {
let (reader, writer) = std::io::pipe()?;
pipe_readers.push(Some(reader.into()));
pipe_writers.push(Some(writer.into()));
}
pipe_readers.push(None);
}
for (current_pipeline_index, command) in pipeline.seq.iter().enumerate() {
ensure_not_cancelled(params)?;
let run_in_current_shell = pipeline_len == 1
|| (current_pipeline_index == pipeline_len - 1
&& shell.options().run_last_pipeline_cmd_in_current_shell
&& !shell.options().enable_job_control);
let mut cmd_params = params.clone();
if pipeline_len > 1 {
cmd_params.disable_command_output_marking();
}
if let Some(Some(reader)) = pipe_readers.pop() {
cmd_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
}
if let Some(Some(writer)) = pipe_writers.pop() {
cmd_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
}
let pipeline_context = if !run_in_current_shell {
if current_pipeline_index > 0 {
cmd_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
}
PipelineExecutionContext {
shell: commands::ShellForCommand::OwnedShell {
target: Box::new(shell.clone()),
parent: shell,
},
process_group_id,
in_pipeline: pipeline_len > 1,
}
} else {
PipelineExecutionContext {
shell: commands::ShellForCommand::ParentShell(shell),
process_group_id,
in_pipeline: pipeline_len > 1,
}
};
let spawn_result = command
.execute_in_pipeline(pipeline_context, cmd_params)
.await?;
if let ExecutionSpawnResult::StartedProcess(child) = &spawn_result {
if process_group_id.is_none() {
process_group_id = child.pgid();
}
}
spawn_results.push_back(spawn_result);
}
Ok(spawn_results)
}
async fn wait_for_pipeline_processes_and_update_status(
pipeline: &ast::Pipeline,
mut process_spawn_results: VecDeque<ExecutionSpawnResult>,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut result = ExecutionResult::success();
let mut stopped_children = vec![];
let mut last_failure_exit_code: Option<ExecutionExitCode> = None;
shell.last_pipeline_statuses_mut().clear();
while let Some(child) = process_spawn_results.pop_front() {
ensure_not_cancelled(params)?;
let wait_result = if !stopped_children.is_empty() {
child.poll().await?
} else {
child.wait_with_cancel(params.cancel_token()).await?
};
match wait_result {
ExecutionWaitResult::Completed(current_result) => {
result = current_result;
shell.set_last_exit_status(result.exit_code.into());
shell
.last_pipeline_statuses_mut()
.push(result.exit_code.into());
if !result.is_success() {
last_failure_exit_code = Some(result.exit_code);
}
},
ExecutionWaitResult::Stopped(child) => {
result = ExecutionResult::stopped();
shell.set_last_exit_status(result.exit_code.into());
shell
.last_pipeline_statuses_mut()
.push(result.exit_code.into());
stopped_children.push(jobs::JobTask::External(child));
},
}
}
if shell.options().return_last_failure_from_pipeline {
if let Some(failure_exit_code) = last_failure_exit_code {
result.exit_code = failure_exit_code;
}
}
if shell.options().interactive {
sys::terminal::move_self_to_foreground()?;
}
if !stopped_children.is_empty() {
let job = shell.jobs_mut().add_as_current(jobs::Job::new(
stopped_children,
pipeline.to_string(),
jobs::JobState::Stopped,
));
let formatted = job.to_string();
writeln!(params.stderr(shell), "\r{formatted}")?;
}
Ok(result)
}
#[async_trait::async_trait]
impl<SE: extensions::ShellExtensions> ExecuteInPipeline<SE> for ast::Command {
async fn execute_in_pipeline(
&self,
mut pipeline_context: PipelineExecutionContext<'_, SE>,
mut params: ExecutionParameters,
) -> Result<ExecutionSpawnResult, error::Error> {
ensure_not_cancelled(¶ms)?;
if pipeline_context.shell.options().do_not_execute_commands {
return Ok(ExecutionSpawnResult::Completed(ExecutionResult::success()));
}
pipeline_context.shell.set_current_cmd(self);
match self {
Self::Simple(simple) => simple.execute_in_pipeline(pipeline_context, params).await,
Self::Compound(compound, redirects) => {
params.disable_command_output_marking();
if let Some(redirects) = redirects {
for redirect in &redirects.0 {
setup_redirect(&mut pipeline_context.shell, &mut params, redirect).await?;
}
}
Ok(compound
.execute(&mut pipeline_context.shell, ¶ms)
.await?
.into())
},
Self::Function(func) => {
params.disable_command_output_marking();
Ok(func
.execute(&mut pipeline_context.shell, ¶ms)
.await?
.into())
},
Self::ExtendedTest(e, redirects) => {
if let Some(redirects) = redirects {
for redirect in &redirects.0 {
setup_redirect(&mut pipeline_context.shell, &mut params, redirect).await?;
}
}
let result = if extendedtests::eval_extended_test_expr(
&e.expr,
&mut pipeline_context.shell,
¶ms,
)
.await?
{
0
} else {
1
};
Ok(ExecutionResult::new(result).into())
},
}
}
}
enum WhileOrUntil {
While,
Until,
}
#[async_trait::async_trait]
impl Execute for ast::CompoundCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
match self {
Self::BraceGroup(ast::BraceGroupCommand { list, .. }) => list.execute(shell, params).await,
Self::Subshell(ast::SubshellCommand { list, .. }) => {
let mut subshell = shell.clone();
let subshell_result = match list.execute(&mut subshell, params).await {
Ok(result) => result,
Err(error) => {
let mut stderr = params.stderr(shell);
let _ = shell.display_error(&mut stderr, &error);
error.into_result(&subshell)
},
};
Ok(ExecutionResult::from(subshell_result.exit_code))
},
Self::ForClause(f) => f.execute(shell, params).await,
Self::CaseClause(c) => c.execute(shell, params).await,
Self::IfClause(i) => i.execute(shell, params).await,
Self::WhileClause(w) => (WhileOrUntil::While, w).execute(shell, params).await,
Self::UntilClause(u) => (WhileOrUntil::Until, u).execute(shell, params).await,
Self::Arithmetic(a) => a.execute(shell, params).await,
Self::ArithmeticForClause(a) => a.execute(shell, params).await,
Self::Coprocess(c) => c.execute(shell, params).await,
}
}
}
#[async_trait::async_trait]
impl Execute for ast::CoprocessCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
if shell.options().do_not_execute_commands {
return Ok(ExecutionResult::success());
}
let name = self
.name
.as_ref()
.map_or_else(|| "COPROC".to_string(), |w| w.to_string());
if !valid_variable_name(&name) {
writeln!(params.stderr(shell), "coproc {name}: not a valid identifier")?;
return Ok(ExecutionExitCode::GeneralError.into());
}
let (stdin_reader, stdin_writer) = std::io::pipe()?;
let (stdout_reader, stdout_writer) = std::io::pipe()?;
let stdout_fd = shell.open_files_mut().add(stdout_reader.into())?;
let stdin_fd = shell.open_files_mut().add(stdin_writer.into())?;
let mut child_shell = shell.clone();
child_shell.options_mut().interactive = false;
let mut child_params = params.clone();
child_params
.open_files
.set_fd(OpenFiles::STDIN_FD, stdin_reader.into());
child_params
.open_files
.set_fd(OpenFiles::STDOUT_FD, stdout_writer.into());
let body = self.body.clone();
let cancel_token = child_params.cancel_token();
let join_handle = tokio::spawn(async move {
let pipeline_context = PipelineExecutionContext {
shell: commands::ShellForCommand::ParentShell(&mut child_shell),
process_group_id: None,
in_pipeline: false,
};
let spawn_result = body
.execute_in_pipeline(pipeline_context, child_params)
.await?;
match spawn_result.wait_with_cancel(cancel_token).await? {
ExecutionWaitResult::Completed(result) => Ok(result),
ExecutionWaitResult::Stopped(_) => Ok(ExecutionResult::stopped()),
}
});
let job = shell.jobs_mut().add_as_current(jobs::Job::new(
[jobs::JobTask::Internal(join_handle)],
format!("coproc {name}"),
jobs::JobState::Running,
));
let job_id = job.id;
let arr_value = ShellValue::from(vec![stdout_fd.to_string(), stdin_fd.to_string()]);
shell
.env_mut()
.set_global(name.clone(), ShellVariable::new(arr_value))?;
let pid_name = format!("{name}_PID");
shell
.env_mut()
.set_global(pid_name, ShellVariable::new(job_id.to_string()))?;
Ok(ExecutionResult::success())
}
}
#[async_trait::async_trait]
impl Execute for ast::ForClauseCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut result = ExecutionResult::success();
let mut expanded_values = vec![];
if let Some(unexpanded_values) = &self.values {
for value in unexpanded_values {
let mut expanded = expansion::full_expand_and_split_word(shell, params, value).await?;
expanded_values.append(&mut expanded);
}
} else {
expanded_values.extend_from_slice(shell.current_shell_args());
}
for value in expanded_values {
ensure_not_cancelled(params)?;
if shell.options().print_commands_and_arguments {
if let Some(unexpanded_values) = &self.values {
shell
.trace_command(
params,
std::format!(
"for {} in {}",
self.variable_name,
unexpanded_values.iter().join(" ")
),
)
.await;
} else {
shell
.trace_command(params, std::format!("for {}", self.variable_name))
.await;
}
}
shell.env_mut().update_or_add(
&self.variable_name,
ShellValueLiteral::Scalar(value),
|_| Ok(()),
EnvironmentLookup::Anywhere,
EnvironmentScope::Global,
)?;
result = self.body.list.execute(shell, params).await?;
if result.is_return_or_exit() {
break;
}
let is_break = result.is_break();
result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
if is_break || result.is_continue() {
break;
}
}
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::CaseClauseCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("case {} in", &self.value))
.await;
}
let expanded_value = expansion::basic_expand_word(shell, params, &self.value).await?;
let mut result: ExecutionResult = ExecutionResult::success();
let mut force_execute_next_case = false;
for case in &self.cases {
ensure_not_cancelled(params)?;
if force_execute_next_case {
force_execute_next_case = false;
} else {
let mut matches = false;
for pattern in &case.patterns {
let expanded_pattern = expansion::basic_expand_pattern(shell, params, pattern)
.await?
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_conditionals);
if expanded_pattern.exactly_matches(expanded_value.as_str())? {
matches = true;
break;
}
}
if !matches {
continue;
}
}
result = if let Some(case_cmd) = &case.cmd {
case_cmd.execute(shell, params).await?
} else {
ExecutionResult::success()
};
if !result.is_normal_flow() {
break;
}
match case.post_action {
ast::CaseItemPostAction::ExitCase => break,
ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
force_execute_next_case = true;
},
ast::CaseItemPostAction::ContinueEvaluatingCases => (),
}
}
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::IfClauseCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut condition_params = params.clone();
condition_params.suppress_errexit = true;
let condition = self.condition.execute(shell, &condition_params).await?;
if !condition.is_normal_flow() {
return Ok(condition);
}
if condition.is_success() {
return self.then.execute(shell, params).await;
}
if let Some(elses) = &self.elses {
for else_clause in elses {
ensure_not_cancelled(params)?;
match &else_clause.condition {
Some(else_condition) => {
let else_condition_result =
else_condition.execute(shell, &condition_params).await?;
if !else_condition_result.is_normal_flow() {
return Ok(else_condition_result);
}
if else_condition_result.is_success() {
return else_clause.body.execute(shell, params).await;
}
},
None => {
return else_clause.body.execute(shell, params).await;
},
}
}
}
let result = ExecutionResult::success();
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for (WhileOrUntil, &ast::WhileOrUntilClauseCommand) {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let is_while = match self.0 {
WhileOrUntil::While => true,
WhileOrUntil::Until => false,
};
let test_condition = &self.1.0;
let body = &self.1.1;
let mut result = ExecutionResult::success();
let mut condition_params = params.clone();
condition_params.suppress_errexit = true;
loop {
ensure_not_cancelled(params)?;
let condition_result = test_condition.execute(shell, &condition_params).await?;
shell.set_last_exit_status(condition_result.exit_code.into());
if !condition_result.is_normal_flow() {
result = condition_result;
result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
break;
}
if condition_result.is_success() != is_while {
break;
}
result = body.list.execute(shell, params).await?;
if result.is_return_or_exit() {
break;
}
let is_break = result.is_break();
result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
if is_break || result.is_continue() {
break;
}
}
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::ArithmeticCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let value = self.expr.eval(shell, params, true).await?;
let result = if value != 0 {
ExecutionResult::success()
} else {
ExecutionResult::general_error()
};
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::ArithmeticForClauseCommand {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
ensure_not_cancelled(params)?;
let mut result = ExecutionResult::success();
if let Some(initializer) = &self.initializer {
initializer.eval(shell, params, true).await?;
}
loop {
ensure_not_cancelled(params)?;
if let Some(condition) = &self.condition {
if !condition.value.is_empty() && condition.eval(shell, params, true).await? == 0 {
break;
}
}
result = self.body.list.execute(shell, params).await?;
if result.is_return_or_exit() {
break;
}
let is_break = result.is_break();
result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
if is_break || result.is_continue() {
break;
}
if let Some(updater) = &self.updater {
updater.eval(shell, params, true).await?;
}
}
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
impl Execute for ast::FunctionDefinition {
async fn execute(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
_params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
let func_name = self.fname.value.clone();
if shell.options().posix_mode
&& shell
.builtins()
.get(&func_name)
.is_some_and(|r| r.special_builtin)
{
return Err(
error::Error::from(error::ErrorKind::FunctionNameShadowsSpecialBuiltin {
name: func_name,
})
.into_fatal(),
);
}
let source_info = shell
.call_stack()
.current_frame()
.map_or_else(crate::SourceInfo::default, |frame| frame.adjusted_source_info());
shell.define_func(func_name, self.clone(), &source_info);
let result = ExecutionResult::success();
shell.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
#[async_trait::async_trait]
#[allow(clippy::too_many_lines)]
impl<SE: extensions::ShellExtensions> ExecuteInPipeline<SE> for ast::SimpleCommand {
async fn execute_in_pipeline(
&self,
mut context: PipelineExecutionContext<'_, SE>,
mut params: ExecutionParameters,
) -> Result<ExecutionSpawnResult, error::Error> {
ensure_not_cancelled(¶ms)?;
let prefix_iter = self.prefix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
let suffix_iter = self.suffix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
let cmd_name_items = self
.word_or_name
.as_ref()
.map(|won| CommandPrefixOrSuffixItem::Word(won.clone()));
let mut assignments = vec![];
let mut args: Vec<CommandArg> = vec![];
let mut command_takes_assignments = false;
let status_change_count_before_expansion = context.shell.last_exit_status_change_count();
for item in prefix_iter.chain(cmd_name_items.iter()).chain(suffix_iter) {
ensure_not_cancelled(¶ms)?;
match item {
CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
if let Err(e) = setup_redirect(&mut context.shell, &mut params, redirect).await {
writeln!(params.stderr(&context.shell), "error: {e}")?;
return Ok(ExecutionResult::general_error().into());
}
},
CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell_command) => {
params.disable_command_output_marking();
let (installed_fd_num, substitution_file) =
setup_process_substitution(&context.shell, ¶ms, kind, subshell_command)?;
params
.open_files
.set_fd(installed_fd_num, substitution_file);
args.push(CommandArg::String(std::format!("/dev/fd/{installed_fd_num}")));
},
CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) => {
if args.is_empty() {
assignments.push(assignment);
} else {
if command_takes_assignments {
let expanded =
expand_assignment(&mut context.shell, ¶ms, assignment).await?;
args.push(CommandArg::Assignment(expanded));
} else {
let mut next_args =
expansion::full_expand_and_split_word(&mut context.shell, ¶ms, word)
.await?
.into_iter()
.map(CommandArg::String)
.collect();
args.append(&mut next_args);
}
}
},
CommandPrefixOrSuffixItem::Word(arg) => {
let mut next_args =
expansion::full_expand_and_split_word(&mut context.shell, ¶ms, arg).await?;
if args.is_empty() {
if let Some(cmd_name) = next_args.first() {
if let Some(alias_value) = context.shell.aliases().get(cmd_name.as_str()) {
let mut alias_pieces: Vec<_> = alias_value
.split_ascii_whitespace()
.map(|i| i.to_owned())
.collect();
next_args.remove(0);
alias_pieces.append(&mut next_args);
next_args = alias_pieces;
}
let first_arg = next_args[0].as_str();
if context
.shell
.builtins()
.get(first_arg)
.is_some_and(|r| !r.disabled && r.declaration_builtin)
{
command_takes_assignments = true;
}
}
}
let mut next_args = next_args.into_iter().map(CommandArg::String).collect();
args.append(&mut next_args);
},
}
}
if let Some(CommandArg::String(cmd_name)) = args.first().cloned() {
let mut stderr = params.stderr(&context.shell);
let (owned_shell, parent_shell) = match context.shell {
commands::ShellForCommand::ParentShell(shell) => (None, shell),
commands::ShellForCommand::OwnedShell { target, parent } => (Some(target), parent),
};
let shell = if let Some(owned_shell) = owned_shell {
commands::ShellForCommand::OwnedShell { target: owned_shell, parent: parent_shell }
} else {
commands::ShellForCommand::ParentShell(parent_shell)
};
let context =
PipelineExecutionContext {
shell,
process_group_id: context.process_group_id,
in_pipeline: context.in_pipeline,
};
match execute_command(context, params, cmd_name, assignments, args).await {
Ok(result) => Ok(result),
Err(err) => {
let _ = parent_shell.display_error(&mut stderr, &err);
let result = err.into_result(parent_shell);
Ok(result.into())
},
}
} else {
for assignment in assignments {
apply_assignment(
assignment,
&mut context.shell,
¶ms,
false,
None,
EnvironmentScope::Global,
)
.await?;
}
context.shell.update_last_arg_variable(None);
if status_change_count_before_expansion == context.shell.last_exit_status_change_count() {
context.shell.set_last_exit_status(0);
}
Ok(ExecutionResult::new(context.shell.last_exit_status()).into())
}
}
}
async fn execute_command(
mut context: PipelineExecutionContext<'_, impl extensions::ShellExtensions>,
params: ExecutionParameters,
cmd_name: String,
assignments: Vec<&ast::Assignment>,
args: Vec<CommandArg>,
) -> Result<ExecutionSpawnResult, error::Error> {
let mut guard = crate::env::ScopeGuard::new(&mut context.shell, EnvironmentScope::Command);
for assignment in &assignments {
apply_assignment(
assignment,
guard.shell(),
¶ms,
true,
Some(EnvironmentScope::Command),
EnvironmentScope::Command,
)
.await?;
}
if guard.shell().options().print_commands_and_arguments {
guard
.shell()
.trace_command(¶ms, args.iter().map(|arg| arg.quote_for_tracing()).join(" "))
.await;
}
guard.detach();
drop(guard);
let mut cmd = commands::SimpleCommand::new(context.shell, params, cmd_name, args);
cmd.process_group_id = context.process_group_id;
cmd.in_pipeline = context.in_pipeline;
cmd.post_execute = Some(|shell| shell.env_mut().pop_scope(EnvironmentScope::Command));
let _ = commands::on_preexecute(&mut cmd).await;
cmd.execute().await
}
async fn expand_assignment(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
assignment: &ast::Assignment,
) -> Result<ast::Assignment, error::Error> {
let value = expand_assignment_value(shell, params, &assignment.value).await?;
Ok(ast::Assignment {
name: basic_expand_assignment_name(shell, params, &assignment.name).await?,
value,
append: assignment.append,
loc: assignment.loc.clone(),
})
}
async fn basic_expand_assignment_name(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
name: &ast::AssignmentName,
) -> Result<ast::AssignmentName, error::Error> {
match name {
ast::AssignmentName::VariableName(name) => {
let expanded = expansion::basic_expand_word(shell, params, name).await?;
Ok(ast::AssignmentName::VariableName(expanded))
},
ast::AssignmentName::ArrayElementName(name, index) => {
let expanded_name = expansion::basic_expand_word(shell, params, name).await?;
let expanded_index = expansion::basic_expand_word(shell, params, index).await?;
Ok(ast::AssignmentName::ArrayElementName(expanded_name, expanded_index))
},
}
}
async fn expand_assignment_value(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
value: &ast::AssignmentValue,
) -> Result<ast::AssignmentValue, error::Error> {
let expanded = match value {
ast::AssignmentValue::Scalar(s) => {
let expanded_word = expansion::basic_expand_assignment_word(shell, params, s).await?;
ast::AssignmentValue::Scalar(ast::Word::from(expanded_word))
},
ast::AssignmentValue::Array(arr) => {
let mut expanded_values = vec![];
for (key, value) in arr {
if let Some(k) = key {
let expanded_key = expansion::basic_expand_assignment_word(shell, params, k)
.await?
.into();
let expanded_value = expansion::basic_expand_assignment_word(shell, params, value)
.await?
.into();
expanded_values.push((Some(expanded_key), expanded_value));
} else {
let split_expanded_value =
expansion::full_expand_and_split_word(shell, params, value).await?;
for expanded_value in split_expanded_value {
expanded_values.push((None, expanded_value.into()));
}
}
}
ast::AssignmentValue::Array(expanded_values)
},
};
Ok(expanded)
}
#[expect(clippy::too_many_lines)]
async fn apply_assignment(
assignment: &ast::Assignment,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
mut export: bool,
required_scope: Option<EnvironmentScope>,
creation_scope: EnvironmentScope,
) -> Result<(), error::Error> {
let mut array_index;
let variable_name = match &assignment.name {
ast::AssignmentName::VariableName(name) => {
array_index = None;
name
},
ast::AssignmentName::ArrayElementName(name, index) => {
let expanded = expansion::basic_expand_word(shell, params, index).await?;
array_index = Some(expanded);
name
},
};
let new_value = match &assignment.value {
ast::AssignmentValue::Scalar(unexpanded_value) => {
let value =
expansion::basic_expand_assignment_word(shell, params, unexpanded_value).await?;
ShellValueLiteral::Scalar(value)
},
ast::AssignmentValue::Array(unexpanded_values) => {
let mut elements = vec![];
for (unexpanded_key, unexpanded_value) in unexpanded_values {
let key = match unexpanded_key {
Some(unexpanded_key) => Some(
expansion::basic_expand_assignment_word(shell, params, unexpanded_key).await?,
),
None => None,
};
if key.is_some() {
let value =
expansion::basic_expand_assignment_word(shell, params, unexpanded_value).await?;
elements.push((key, value));
} else {
let values =
expansion::full_expand_and_split_word(shell, params, unexpanded_value).await?;
for value in values {
elements.push((None, value));
}
}
}
ShellValueLiteral::Array(ArrayLiteral(elements))
},
};
if shell.options().print_commands_and_arguments {
let op = if assignment.append { "+=" } else { "=" };
shell
.trace_command(params, std::format!("{}{op}{new_value}", assignment.name))
.await;
}
if let Some(idx) = &array_index {
let will_be_indexed_array = if let Some((_, existing_value)) = shell.env().get(variable_name)
{
matches!(
existing_value.value(),
ShellValue::IndexedArray(_) | ShellValue::Unset(ShellValueUnsetType::IndexedArray)
)
} else {
true
};
if will_be_indexed_array {
array_index = Some(
arithmetic::expand_and_eval(shell, params, idx.as_str(), false)
.await?
.to_string(),
);
}
}
let export_variables_on_modification = shell.options().export_variables_on_modification;
if let Some((existing_value_scope, existing_value)) =
shell.env_mut().get_mut(variable_name.as_str())
{
if required_scope.is_none() || Some(existing_value_scope) == required_scope {
if let Some(array_index) = array_index {
match new_value {
ShellValueLiteral::Scalar(s) => {
existing_value.assign_at_index(array_index, s, assignment.append)?;
},
ShellValueLiteral::Array(_) => {
return error::unimp("replacing an array item with an array");
},
}
} else {
if !export
&& export_variables_on_modification
&& !matches!(new_value, ShellValueLiteral::Array(_))
{
export = true;
}
existing_value.assign(new_value, assignment.append)?;
}
if export {
existing_value.export();
}
return Ok(());
}
}
let new_value = if let Some(array_index) = array_index {
match new_value {
ShellValueLiteral::Scalar(s) => {
ShellValue::indexed_array_from_literals(ArrayLiteral(vec![(Some(array_index), s)]))
},
ShellValueLiteral::Array(_) => {
return error::unimp("cannot assign list to array member");
},
}
} else {
match new_value {
ShellValueLiteral::Scalar(s) => {
export = export || shell.options().export_variables_on_modification;
ShellValue::String(s)
},
ShellValueLiteral::Array(values) => ShellValue::indexed_array_from_literals(values),
}
};
let mut new_var = ShellVariable::new(new_value);
if export {
new_var.export();
}
shell.env_mut().add(variable_name, new_var, creation_scope)
}
#[expect(clippy::too_many_lines)]
pub(crate) async fn setup_redirect(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &'_ mut ExecutionParameters,
redirect: &ast::IoRedirect,
) -> Result<(), error::Error> {
params.disable_command_output_marking();
match redirect {
ast::IoRedirect::OutputAndError(f, append) => {
let mut expanded_fields = expansion::full_expand_and_split_word(shell, params, f).await?;
if expanded_fields.len() != 1 {
return Err(error::ErrorKind::InvalidRedirection.into());
}
let expanded_file_path = expanded_fields.remove(0);
setup_redirect_output_and_error_to(shell, params, &expanded_file_path, *append)?;
},
ast::IoRedirect::File(specified_fd_num, kind, target) => {
match target {
ast::IoFileRedirectTarget::Filename(f) => {
let mut options = std::fs::File::options();
let mut expanded_fields =
expansion::full_expand_and_split_word(shell, params, f).await?;
if expanded_fields.len() != 1 {
return Err(error::ErrorKind::InvalidRedirection.into());
}
let expanded_file_path: PathBuf =
shell.absolute_path(Path::new(expanded_fields.remove(0).as_str()));
let default_fd_if_unspecified = get_default_fd_for_redirect_kind(kind);
match kind {
ast::IoFileRedirectKind::Read => {
options.read(true);
},
ast::IoFileRedirectKind::Write => {
if shell
.options()
.disallow_overwriting_regular_files_via_output_redirection
{
if !expanded_file_path.is_file() {
options.create(true);
} else {
options.create_new(true);
}
options.write(true);
} else {
options.create(true);
options.write(true);
options.truncate(true);
}
},
ast::IoFileRedirectKind::Append => {
options.create(true);
options.append(true);
},
ast::IoFileRedirectKind::ReadAndWrite => {
options.create(true);
options.read(true);
options.write(true);
},
ast::IoFileRedirectKind::Clobber => {
options.create(true);
options.write(true);
options.truncate(true);
},
ast::IoFileRedirectKind::DuplicateInput => {
options.read(true);
},
ast::IoFileRedirectKind::DuplicateOutput => {
options.create(true);
options.write(true);
},
}
let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
let opened_file = shell
.open_file(&options, &expanded_file_path, params)
.map_err(|err| {
error::ErrorKind::RedirectionFailure(
expanded_file_path.to_string_lossy().to_string(),
err.to_string(),
)
})?;
params.open_files.set_fd(fd_num, opened_file);
},
ast::IoFileRedirectTarget::Fd(fd) => {
let default_fd_if_unspecified = match kind {
ast::IoFileRedirectKind::DuplicateInput => 0,
ast::IoFileRedirectKind::DuplicateOutput => 1,
_ => {
return error::unimp("unexpected redirect kind");
},
};
let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
if let Some(f) = params.try_fd(shell, *fd) {
let target_file = f.try_clone()?;
params.open_files.set_fd(fd_num, target_file);
} else {
return Err(error::ErrorKind::BadFileDescriptor(*fd).into());
}
},
ast::IoFileRedirectTarget::Duplicate(word) => {
let default_fd_if_unspecified = match kind {
ast::IoFileRedirectKind::DuplicateInput => 0,
ast::IoFileRedirectKind::DuplicateOutput => 1,
_ => {
return error::unimp("unexpected redirect kind");
},
};
let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
let mut expanded_fields =
expansion::full_expand_and_split_word(shell, params, word).await?;
if expanded_fields.len() != 1 {
return Err(error::ErrorKind::InvalidRedirection.into());
}
let mut expanded = expanded_fields.remove(0);
let dash = if expanded.ends_with('-') {
expanded.pop();
true
} else {
false
};
if expanded.is_empty() {
} else if expanded.chars().all(|c: char| c.is_ascii_digit()) {
let source_fd_num = expanded
.parse::<ShellFd>()
.map_err(|_| error::ErrorKind::InvalidRedirection)?;
let target_file = if let Some(f) = params.try_fd(shell, source_fd_num) {
f.try_clone()?
} else {
return Err(error::ErrorKind::BadFileDescriptor(source_fd_num).into());
};
params.open_files.set_fd(fd_num, target_file);
} else if fd_num == 1 && !dash {
setup_redirect_output_and_error_to(
shell, params, &expanded, false,
)?;
} else {
return Err(error::ErrorKind::InvalidRedirection.into());
}
if dash {
params.open_files.remove_fd(fd_num);
}
},
ast::IoFileRedirectTarget::ProcessSubstitution(substitution_kind, subshell_cmd) => {
match kind {
ast::IoFileRedirectKind::Read
| ast::IoFileRedirectKind::Write
| ast::IoFileRedirectKind::Append
| ast::IoFileRedirectKind::ReadAndWrite
| ast::IoFileRedirectKind::Clobber => {
let (substitution_fd, substitution_file) =
setup_process_substitution(shell, params, substitution_kind, subshell_cmd)?;
let target_file = substitution_file.try_clone()?;
params.open_files.set_fd(substitution_fd, substitution_file);
let fd_num =
specified_fd_num.unwrap_or_else(|| get_default_fd_for_redirect_kind(kind));
params.open_files.set_fd(fd_num, target_file);
},
_ => return error::unimp("invalid process substitution"),
}
},
}
},
ast::IoRedirect::HereDocument(fd_num, io_here) => {
let fd_num = fd_num.unwrap_or(0);
let io_here_doc = if io_here.requires_expansion {
expansion::basic_expand_heredoc_word(shell, params, &io_here.doc).await?
} else {
io_here.doc.flatten()
};
let f = setup_open_file_with_contents(io_here_doc.as_str())?;
params.open_files.set_fd(fd_num, f);
},
ast::IoRedirect::HereString(fd_num, word) => {
let fd_num = fd_num.unwrap_or(0);
let mut expanded_word = expansion::basic_expand_word(shell, params, word).await?;
expanded_word.push('\n');
let f = setup_open_file_with_contents(expanded_word.as_str())?;
params.open_files.set_fd(fd_num, f);
},
}
Ok(())
}
fn setup_redirect_output_and_error_to(
shell: &Shell<impl extensions::ShellExtensions>,
params: &mut ExecutionParameters,
file_path: &str,
append: bool,
) -> Result<(), error::Error> {
let abs_file_path: PathBuf = shell.absolute_path(Path::new(file_path));
let mut file_options = std::fs::File::options();
file_options
.create(true)
.write(true)
.truncate(!append)
.append(append);
let stdout_file = shell
.open_file(&file_options, &abs_file_path, params)
.map_err(|err| {
error::ErrorKind::RedirectionFailure(
abs_file_path.to_string_lossy().to_string(),
err.to_string(),
)
})?;
let stderr_file = stdout_file.try_clone()?;
params.open_files.set_fd(OpenFiles::STDOUT_FD, stdout_file);
params.open_files.set_fd(OpenFiles::STDERR_FD, stderr_file);
Ok(())
}
const fn get_default_fd_for_redirect_kind(kind: &ast::IoFileRedirectKind) -> ShellFd {
match kind {
ast::IoFileRedirectKind::Read => 0,
ast::IoFileRedirectKind::Write => 1,
ast::IoFileRedirectKind::Append => 1,
ast::IoFileRedirectKind::ReadAndWrite => 0,
ast::IoFileRedirectKind::Clobber => 1,
ast::IoFileRedirectKind::DuplicateInput => 0,
ast::IoFileRedirectKind::DuplicateOutput => 1,
}
}
fn setup_process_substitution(
shell: &Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
kind: &ast::ProcessSubstitutionKind,
subshell_cmd: &ast::SubshellCommand,
) -> Result<(ShellFd, OpenFile), error::Error> {
let mut subshell = shell.clone();
let mut child_params = params.clone();
child_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
let (reader, writer) = std::io::pipe()?;
let (reader, writer) = (reader.into(), writer.into());
let target_file = match kind {
ast::ProcessSubstitutionKind::Read => {
child_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
reader
},
ast::ProcessSubstitutionKind::Write => {
child_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
writer
},
};
let subshell_cmd = subshell_cmd.to_owned();
tokio::spawn(async move {
let _ = subshell_cmd
.list
.execute(&mut subshell, &child_params)
.await;
});
let mut candidate_fd_num = 63;
while params.open_files.contains_fd(candidate_fd_num) {
candidate_fd_num -= 1;
if candidate_fd_num == 0 {
return error::unimp("no available file descriptors");
}
}
Ok((candidate_fd_num, target_file))
}
fn setup_open_file_with_contents(contents: &str) -> Result<OpenFile, error::Error> {
let (reader, mut writer) = std::io::pipe()?;
let bytes = contents.as_bytes();
#[cfg(any(target_os = "linux", target_os = "android"))]
{
use std::os::fd::AsFd as _;
if let Ok(len) = i32::try_from(bytes.len())
&& nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len)).is_ok()
{
writer.write_all(bytes)?;
drop(writer);
return Ok(reader.into());
}
}
#[cfg(target_family = "wasm")]
{
writer.write_all(bytes)?;
drop(writer);
return Ok(reader.into());
}
#[cfg(not(target_family = "wasm"))]
{
let payload = bytes.to_vec();
std::thread::Builder::new()
.name("brush-heredoc-writer".into())
.spawn(move || {
let _ = writer.write_all(&payload);
})?;
}
Ok(reader.into())
}