1use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
4
5use brush_parser::ast;
6use itertools::Itertools;
7use sys::commands::{CommandExt, CommandFdInjectionExt, CommandFgControlExt};
8
9use crate::{
10 ErrorKind, ExecutionControlFlow, ExecutionParameters, ExecutionResult, Shell, ShellFd,
11 builtins, env, error, escape,
12 interp::{self, Execute, ProcessGroupPolicy},
13 openfiles::{self, OpenFile, OpenFiles},
14 pathsearch, processes,
15 results::ExecutionSpawnResult,
16 sys, trace_categories, traps, variables,
17};
18
19pub enum CommandWaitResult {
21 CommandCompleted(ExecutionResult),
23 CommandStopped(ExecutionResult, processes::ChildProcess),
25}
26
27pub struct ExecutionContext<'a> {
29 pub shell: &'a mut Shell,
31 pub command_name: String,
33 pub params: ExecutionParameters,
35}
36
37impl ExecutionContext<'_> {
38 pub fn stdin(&self) -> impl std::io::Read + 'static {
40 self.params.stdin(self.shell)
41 }
42
43 pub fn stdout(&self) -> impl std::io::Write + 'static {
45 self.params.stdout(self.shell)
46 }
47
48 pub fn stderr(&self) -> impl std::io::Write + 'static {
50 self.params.stderr(self.shell)
51 }
52
53 pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
60 self.params.try_fd(self.shell, fd)
61 }
62
63 pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
65 self.params.iter_fds(self.shell)
66 }
67
68 pub(crate) const fn should_cmd_lead_own_process_group(&self) -> bool {
69 self.shell.options.interactive
70 && matches!(
71 self.params.process_group_policy,
72 ProcessGroupPolicy::NewProcessGroup
73 )
74 }
75}
76
77#[derive(Clone, Debug)]
79pub enum CommandArg {
80 String(String),
82 Assignment(ast::Assignment),
85}
86
87impl Display for CommandArg {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Self::String(s) => f.write_str(s),
91 Self::Assignment(a) => write!(f, "{a}"),
92 }
93 }
94}
95
96impl From<String> for CommandArg {
97 fn from(s: String) -> Self {
98 Self::String(s)
99 }
100}
101
102impl From<&String> for CommandArg {
103 fn from(value: &String) -> Self {
104 Self::String(value.clone())
105 }
106}
107
108impl CommandArg {
109 pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
110 match self {
111 Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
112 Self::Assignment(a) => {
113 let mut s = a.name.to_string();
114 let op = if a.append { "+=" } else { "=" };
115 s.push_str(op);
116 s.push_str(&escape::quote_if_needed(
117 a.value.to_string().as_str(),
118 escape::QuoteMode::SingleQuote,
119 ));
120 s.into()
121 }
122 }
123 }
124}
125
126#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
140pub fn compose_std_command<S: AsRef<OsStr>>(
141 context: &ExecutionContext<'_>,
142 command_name: &str,
143 argv0: &str,
144 args: &[S],
145 empty_env: bool,
146) -> Result<std::process::Command, error::Error> {
147 let mut cmd = std::process::Command::new(command_name);
148
149 cmd.arg0(argv0);
152
153 cmd.args(args);
155
156 cmd.current_dir(context.shell.working_dir());
158
159 cmd.env_clear();
161
162 if !empty_env {
164 for (k, v) in context.shell.env.iter_exported() {
165 if v.value().is_set() {
169 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
170 }
171 }
172 }
173
174 if !empty_env {
176 for (func_name, registration) in context.shell.funcs().iter() {
177 if registration.is_exported() {
178 let var_name = std::format!("BASH_FUNC_{func_name}%%");
179 let value = std::format!("() {}", registration.definition.body);
180 cmd.env(var_name, value);
181 }
182 }
183 }
184
185 match context.try_fd(OpenFiles::STDIN_FD) {
187 Some(OpenFile::Stdin(_)) | None => (),
188 Some(stdin_file) => {
189 let as_stdio: Stdio = stdin_file.into();
190 cmd.stdin(as_stdio);
191 }
192 }
193
194 match context.try_fd(OpenFiles::STDOUT_FD) {
196 Some(OpenFile::Stdout(_)) | None => (),
197 Some(stdout_file) => {
198 let as_stdio: Stdio = stdout_file.into();
199 cmd.stdout(as_stdio);
200 }
201 }
202
203 match context.try_fd(OpenFiles::STDERR_FD) {
205 Some(OpenFile::Stderr(_)) | None => {}
206 Some(stderr_file) => {
207 let as_stdio: Stdio = stderr_file.into();
208 cmd.stderr(as_stdio);
209 }
210 }
211
212 let other_files = context.iter_fds().filter(|(fd, _)| {
214 *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
215 });
216 cmd.inject_fds(other_files)?;
217
218 Ok(cmd)
219}
220
221pub(crate) async fn on_preexecute(
222 context: &mut ExecutionContext<'_>,
223 args: &[CommandArg],
224) -> Result<(), error::Error> {
225 invoke_debug_trap_handler_if_registered(context, args).await?;
227
228 Ok(())
229}
230
231async fn invoke_debug_trap_handler_if_registered(
232 context: &mut ExecutionContext<'_>,
233 args: &[CommandArg],
234) -> Result<(), error::Error> {
235 if context.shell.traps.handler_depth == 0 {
236 let debug_trap_handler = context
237 .shell
238 .traps
239 .handlers
240 .get(&traps::TrapSignal::Debug)
241 .cloned();
242 if let Some(debug_trap_handler) = debug_trap_handler {
243 let mut handler_params = context.params.clone();
245 handler_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
246
247 let full_cmd = args.iter().map(|arg| arg.to_string()).join(" ");
248
249 context.shell.env.update_or_add(
251 "BASH_COMMAND",
252 variables::ShellValueLiteral::Scalar(full_cmd),
253 |_| Ok(()),
254 env::EnvironmentLookup::Anywhere,
255 env::EnvironmentScope::Global,
256 )?;
257
258 context.shell.traps.handler_depth += 1;
259
260 let _ = context
262 .shell
263 .run_string(debug_trap_handler, &handler_params)
264 .await;
265
266 context.shell.traps.handler_depth -= 1;
267 }
268 }
269
270 Ok(())
271}
272
273pub async fn execute(
292 cmd_context: ExecutionContext<'_>,
293 process_group_id: &mut Option<i32>,
294 args: Vec<CommandArg>,
295 use_functions: bool,
296 path_dirs: Option<Vec<String>>,
297) -> Result<ExecutionSpawnResult, error::Error> {
298 let builtin = cmd_context
300 .shell
301 .builtins()
302 .get(&cmd_context.command_name)
303 .cloned();
304
305 if builtin
307 .as_ref()
308 .is_some_and(|r| !r.disabled && r.special_builtin)
309 {
310 return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
311 }
312
313 if use_functions {
316 if let Some(func_reg) = cmd_context
317 .shell
318 .funcs()
319 .get(cmd_context.command_name.as_str())
320 {
321 return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
323 .await;
324 }
325 }
326
327 if let Some(builtin) = builtin {
329 if !builtin.disabled {
330 return execute_builtin_command(&builtin, cmd_context, args).await;
331 }
332 }
333
334 if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
336 let path = if let Some(path_dirs) = path_dirs {
339 pathsearch::search_for_executable(
340 path_dirs.iter().map(String::as_str),
341 cmd_context.command_name.as_str(),
342 )
343 .next()
344 } else {
345 cmd_context
346 .shell
347 .find_first_executable_in_path_using_cache(&cmd_context.command_name)
348 };
349
350 if let Some(path) = path {
351 let resolved_path = path.to_string_lossy();
352 execute_external_command(
353 cmd_context,
354 resolved_path.as_ref(),
355 process_group_id,
356 &args[1..],
357 )
358 } else {
359 Err(ErrorKind::CommandNotFound(cmd_context.command_name).into())
360 }
361 } else {
362 let resolved_path = cmd_context.command_name.clone();
363
364 execute_external_command(
366 cmd_context,
367 resolved_path.as_str(),
368 process_group_id,
369 &args[1..],
370 )
371 }
372}
373
374pub(crate) fn execute_external_command(
375 context: ExecutionContext<'_>,
376 executable_path: &str,
377 process_group_id: &mut Option<i32>,
378 args: &[CommandArg],
379) -> Result<ExecutionSpawnResult, error::Error> {
380 let mut cmd_args = vec![];
382 for arg in args {
383 if let CommandArg::String(s) = arg {
384 cmd_args.push(s);
385 }
386 }
387
388 let child_stdin_is_terminal = context
390 .try_fd(openfiles::OpenFiles::STDIN_FD)
391 .is_some_and(|f| f.is_term());
392
393 let new_pg = context.should_cmd_lead_own_process_group();
395
396 #[allow(unused_mut, reason = "only mutated on unix platforms")]
398 let mut cmd = compose_std_command(
399 &context,
400 executable_path,
401 context.command_name.as_str(),
402 cmd_args.as_slice(),
403 false, )?;
405
406 if new_pg {
408 cmd.process_group(0);
410 } else {
411 if let Some(pgid) = process_group_id {
413 cmd.process_group(*pgid);
414 }
415 }
416
417 if new_pg && child_stdin_is_terminal {
421 cmd.take_foreground();
422 }
423
424 tracing::debug!(
426 target: trace_categories::COMMANDS,
427 "Spawning: cmd='{} {}'",
428 cmd.get_program().to_string_lossy().to_string(),
429 cmd.get_args()
430 .map(|a| a.to_string_lossy().to_string())
431 .join(" ")
432 );
433
434 match sys::process::spawn(cmd) {
435 Ok(child) => {
436 #[expect(clippy::cast_possible_wrap)]
438 let pid = child.id().map(|id| id as i32);
439 if let Some(pid) = &pid {
440 if new_pg {
441 *process_group_id = Some(*pid);
442 }
443 } else {
444 tracing::warn!("could not retrieve pid for child process");
445 }
446
447 Ok(ExecutionSpawnResult::StartedProcess(
448 processes::ChildProcess::new(pid, child),
449 ))
450 }
451 Err(spawn_err) => {
452 if context.shell.options.interactive {
453 sys::terminal::move_self_to_foreground()?;
454 }
455
456 if spawn_err.kind() == std::io::ErrorKind::NotFound {
457 if !context.shell.working_dir().exists() {
458 Err(
459 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
460 .into(),
461 )
462 } else {
463 Err(error::ErrorKind::CommandNotFound(context.command_name).into())
464 }
465 } else {
466 Err(
467 error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
468 .into(),
469 )
470 }
471 }
472 }
473}
474
475async fn execute_builtin_command(
476 builtin: &builtins::Registration,
477 context: ExecutionContext<'_>,
478 args: Vec<CommandArg>,
479) -> Result<ExecutionSpawnResult, error::Error> {
480 let result = (builtin.execute_func)(context, args).await?;
481 Ok(result.into())
482}
483
484pub(crate) async fn invoke_shell_function(
485 function_definition: Arc<ast::FunctionDefinition>,
486 mut context: ExecutionContext<'_>,
487 args: &[CommandArg],
488) -> Result<ExecutionSpawnResult, error::Error> {
489 let ast::FunctionBody(body, redirects) = &function_definition.body;
490
491 if let Some(redirects) = redirects {
493 for redirect in &redirects.0 {
494 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
495 }
496 }
497
498 let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
500 context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
501
502 let params = context.params.clone();
504
505 context
508 .shell
509 .enter_function(context.command_name.as_str(), &function_definition)?;
510
511 let result = body.execute(context.shell, ¶ms).await;
513
514 drop(params);
516
517 context.shell.leave_function()?;
519
520 context.shell.positional_parameters = prior_positional_params;
522
523 let mut result = result?;
525
526 match result.next_control_flow {
528 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
529 return error::unimp("break or continue returned from function invocation");
530 }
531 ExecutionControlFlow::ReturnFromFunctionOrScript => {
532 result.next_control_flow = ExecutionControlFlow::Normal;
534 }
535 _ => {}
536 }
537
538 Ok(result.into())
539}
540
541pub(crate) async fn invoke_command_in_subshell_and_get_output(
542 shell: &mut Shell,
543 params: &ExecutionParameters,
544 s: String,
545) -> Result<String, error::Error> {
546 let subshell = shell.clone();
548
549 let mut params = params.clone();
551 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
552
553 let (reader, writer) = std::io::pipe()?;
555 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
556
557 let cmd_join_handle = tokio::task::spawn_blocking(move || {
565 let rt = tokio::runtime::Handle::current();
566 rt.block_on(run_substitution_command(subshell, params, s))
567 });
568
569 let output_str = std::io::read_to_string(reader)?;
571
572 let run_result = cmd_join_handle.await?;
574 let cmd_result = run_result?;
575
576 *shell.last_exit_status_mut() = cmd_result.exit_code.into();
578
579 Ok(output_str)
580}
581
582async fn run_substitution_command(
583 mut shell: Shell,
584 mut params: ExecutionParameters,
585 command: String,
586) -> Result<ExecutionResult, error::Error> {
587 let parse_result = shell.parse_string(command);
589
590 if let Ok(program) = &parse_result {
594 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
595 interp::setup_redirect(&mut shell, &mut params, redir).await?;
596 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
597 return Ok(ExecutionResult::new(0));
598 }
599 }
600
601 let source_info = brush_parser::SourceInfo {
602 source: String::from("main"),
603 };
604
605 shell
607 .run_parsed_result(parse_result, &source_info, ¶ms)
608 .await
609}
610
611fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
614 let [complete] = program.complete_commands.as_slice() else {
616 return None;
617 };
618
619 let ast::CompoundList(items) = complete;
621 let [item] = items.as_slice() else {
622 return None;
623 };
624
625 let and_or = &item.0;
627 if !and_or.additional.is_empty() {
628 return None;
629 }
630
631 let pipeline = &and_or.first;
633 if pipeline.bang {
634 return None;
635 }
636
637 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
639 return None;
640 };
641
642 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
644 return None;
645 }
646
647 let prefix = simple_cmd.prefix.as_ref()?;
649 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
650 return None;
651 };
652
653 match redir {
655 ast::IoRedirect::File(
656 fd,
657 ast::IoFileRedirectKind::Read,
658 ast::IoFileRedirectTarget::Filename(..),
659 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
660 _ => None,
661 }
662}