1use std::io::Write;
2#[cfg(unix)]
3use std::os::unix::process::CommandExt;
4use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
5
6use brush_parser::ast;
7#[cfg(unix)]
8use command_fds::{CommandFdExt, FdMapping};
9use itertools::Itertools;
10
11use crate::{
12 ExecutionParameters, ExecutionResult, Shell, builtins, error, escape,
13 interp::{self, Execute, ProcessGroupPolicy},
14 openfiles::{self, OpenFile, OpenFiles},
15 pathsearch, processes, sys, trace_categories,
16};
17
18pub(crate) enum CommandSpawnResult {
20 SpawnedProcess(processes::ChildProcess),
22 ImmediateExit(u8),
24 ExitShell(u8),
26 ReturnFromFunctionOrScript(u8),
29 BreakLoop(u8),
31 ContinueLoop(u8),
33}
34
35impl CommandSpawnResult {
36 #[allow(clippy::too_many_lines)]
38 pub async fn wait(self, no_wait: bool) -> Result<CommandWaitResult, error::Error> {
39 #[allow(clippy::ignored_unit_patterns)]
40 match self {
41 Self::SpawnedProcess(mut child) => {
42 let process_wait_result = if !no_wait {
43 child.wait().await?
46 } else {
47 processes::ProcessWaitResult::Stopped
48 };
49
50 let command_wait_result = match process_wait_result {
51 processes::ProcessWaitResult::Completed(output) => {
52 CommandWaitResult::CommandCompleted(ExecutionResult::from(output))
53 }
54 processes::ProcessWaitResult::Stopped => CommandWaitResult::CommandStopped(
55 ExecutionResult::from(processes::ProcessWaitResult::Stopped),
56 child,
57 ),
58 };
59
60 Ok(command_wait_result)
61 }
62 Self::ImmediateExit(exit_code) => Ok(CommandWaitResult::CommandCompleted(
63 ExecutionResult::new(exit_code),
64 )),
65 Self::ExitShell(exit_code) => {
66 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
67 exit_code,
68 exit_shell: true,
69 ..ExecutionResult::default()
70 }))
71 }
72 Self::ReturnFromFunctionOrScript(exit_code) => {
73 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
74 exit_code,
75 return_from_function_or_script: true,
76 ..ExecutionResult::default()
77 }))
78 }
79 Self::BreakLoop(count) => Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
80 exit_code: 0,
81 break_loop: Some(count),
82 ..ExecutionResult::default()
83 })),
84 Self::ContinueLoop(count) => Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
85 exit_code: 0,
86 continue_loop: Some(count),
87 ..ExecutionResult::default()
88 })),
89 }
90 }
91}
92
93pub(crate) enum CommandWaitResult {
95 CommandCompleted(ExecutionResult),
97 CommandStopped(ExecutionResult, processes::ChildProcess),
99}
100
101pub struct ExecutionContext<'a> {
103 pub shell: &'a mut Shell,
105 pub command_name: String,
107 pub params: ExecutionParameters,
109}
110
111impl ExecutionContext<'_> {
112 pub fn stdin(&self) -> impl std::io::Read + 'static {
114 self.params.stdin()
115 }
116
117 pub fn stdout(&self) -> impl std::io::Write + 'static {
119 self.params.stdout()
120 }
121
122 pub fn stderr(&self) -> impl std::io::Write + 'static {
124 self.params.stderr()
125 }
126
127 pub(crate) const fn should_cmd_lead_own_process_group(&self) -> bool {
128 self.shell.options.interactive
129 && matches!(
130 self.params.process_group_policy,
131 ProcessGroupPolicy::NewProcessGroup
132 )
133 }
134}
135
136#[derive(Clone, Debug)]
138pub enum CommandArg {
139 String(String),
141 Assignment(ast::Assignment),
144}
145
146impl Display for CommandArg {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 Self::String(s) => f.write_str(s),
150 Self::Assignment(a) => write!(f, "{a}"),
151 }
152 }
153}
154
155impl From<String> for CommandArg {
156 fn from(s: String) -> Self {
157 Self::String(s)
158 }
159}
160
161impl From<&String> for CommandArg {
162 fn from(value: &String) -> Self {
163 Self::String(value.clone())
164 }
165}
166
167impl CommandArg {
168 pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
169 match self {
170 Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
171 Self::Assignment(a) => {
172 let mut s = a.name.to_string();
173 let op = if a.append { "+=" } else { "=" };
174 s.push_str(op);
175 s.push_str(&escape::quote_if_needed(
176 a.value.to_string().as_str(),
177 escape::QuoteMode::SingleQuote,
178 ));
179 s.into()
180 }
181 }
182 }
183}
184
185#[allow(unused_variables)]
186pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
187 shell: &Shell,
188 command_name: &str,
189 argv0: &str,
190 args: &[S],
191 mut open_files: OpenFiles,
192 empty_env: bool,
193) -> Result<std::process::Command, error::Error> {
194 let mut cmd = std::process::Command::new(command_name);
195
196 #[cfg(unix)]
198 cmd.arg0(argv0);
199
200 cmd.args(args);
202
203 cmd.current_dir(shell.working_dir.as_path());
205
206 cmd.env_clear();
208
209 if !empty_env {
211 for (k, v) in shell.env.iter_exported() {
212 cmd.env(k.as_str(), v.value().to_cow_str(shell).as_ref());
213 }
214 }
215
216 if !empty_env {
218 for (func_name, registration) in shell.funcs.iter() {
219 if registration.is_exported() {
220 let var_name = std::format!("BASH_FUNC_{func_name}%%");
221 let value = std::format!("() {}", registration.definition.body);
222 cmd.env(var_name, value);
223 }
224 }
225 }
226
227 match open_files.files.remove(&0) {
229 Some(OpenFile::Stdin) | None => (),
230 Some(stdin_file) => {
231 let as_stdio: Stdio = stdin_file.into();
232 cmd.stdin(as_stdio);
233 }
234 }
235
236 match open_files.files.remove(&1) {
238 Some(OpenFile::Stdout) | None => (),
239 Some(stdout_file) => {
240 let as_stdio: Stdio = stdout_file.into();
241 cmd.stdout(as_stdio);
242 }
243 }
244
245 match open_files.files.remove(&2) {
247 Some(OpenFile::Stderr) | None => {}
248 Some(stderr_file) => {
249 let as_stdio: Stdio = stderr_file.into();
250 cmd.stderr(as_stdio);
251 }
252 }
253
254 #[cfg(unix)]
256 {
257 let fd_mappings = open_files
258 .files
259 .into_iter()
260 .map(|(child_fd, open_file)| FdMapping {
261 child_fd: i32::try_from(child_fd).unwrap(),
262 parent_fd: open_file.into_owned_fd().unwrap(),
263 })
264 .collect();
265 cmd.fd_mappings(fd_mappings)
266 .map_err(|_e| error::Error::ChildCreationFailure)?;
267 }
268 #[cfg(not(unix))]
269 {
270 if !open_files.files.is_empty() {
271 return error::unimp("fd redirections on non-Unix platform");
272 }
273 }
274
275 Ok(cmd)
276}
277
278pub(crate) async fn execute(
279 cmd_context: ExecutionContext<'_>,
280 process_group_id: &mut Option<i32>,
281 args: Vec<CommandArg>,
282 use_functions: bool,
283 path_dirs: Option<Vec<String>>,
284) -> Result<CommandSpawnResult, error::Error> {
285 let builtin = cmd_context
287 .shell
288 .builtins
289 .get(&cmd_context.command_name)
290 .cloned();
291
292 if builtin
294 .as_ref()
295 .is_some_and(|r| !r.disabled && r.special_builtin)
296 {
297 return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
298 }
299
300 if use_functions {
303 if let Some(func_reg) = cmd_context
304 .shell
305 .funcs
306 .get(cmd_context.command_name.as_str())
307 {
308 return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
310 .await;
311 }
312 }
313
314 if let Some(builtin) = builtin {
316 if !builtin.disabled {
317 return execute_builtin_command(&builtin, cmd_context, args).await;
318 }
319 }
320
321 if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
323 let path = if let Some(path_dirs) = path_dirs {
326 pathsearch::search_for_executable(
327 path_dirs.iter().map(String::as_str),
328 cmd_context.command_name.as_str(),
329 )
330 .next()
331 } else {
332 cmd_context
333 .shell
334 .find_first_executable_in_path_using_cache(&cmd_context.command_name)
335 };
336
337 if let Some(path) = path {
338 let resolved_path = path.to_string_lossy();
339 execute_external_command(
340 cmd_context,
341 resolved_path.as_ref(),
342 process_group_id,
343 &args[1..],
344 )
345 } else {
346 writeln!(
347 cmd_context.stderr(),
348 "{}: command not found",
349 cmd_context.command_name
350 )?;
351 Ok(CommandSpawnResult::ImmediateExit(127))
352 }
353 } else {
354 let resolved_path = cmd_context.command_name.clone();
355
356 execute_external_command(
358 cmd_context,
359 resolved_path.as_str(),
360 process_group_id,
361 &args[1..],
362 )
363 }
364}
365
366#[allow(clippy::too_many_lines)]
367#[allow(unused_variables)]
368pub(crate) fn execute_external_command(
369 context: ExecutionContext<'_>,
370 executable_path: &str,
371 process_group_id: &mut Option<i32>,
372 args: &[CommandArg],
373) -> Result<CommandSpawnResult, error::Error> {
374 let mut cmd_args = vec![];
376 for arg in args {
377 if let CommandArg::String(s) = arg {
378 cmd_args.push(s);
379 }
380 }
381
382 #[allow(unused_variables)]
384 let child_stdin_is_terminal = context
385 .params
386 .open_files
387 .stdin()
388 .is_some_and(|f| f.is_term());
389
390 let new_pg = context.should_cmd_lead_own_process_group();
392
393 let mut stderr = context.stderr();
395
396 #[allow(unused_mut)]
398 let mut cmd = compose_std_command(
399 context.shell,
400 executable_path,
401 context.command_name.as_str(),
402 cmd_args.as_slice(),
403 context.params.open_files,
404 false, )?;
406
407 if new_pg {
409 #[cfg(unix)]
411 cmd.process_group(0);
412 } else if let Some(pgid) = process_group_id {
413 #[cfg(unix)]
415 cmd.process_group(*pgid);
416 }
417
418 #[cfg(unix)]
421 if new_pg && child_stdin_is_terminal {
422 unsafe {
423 cmd.pre_exec(setup_process_before_exec);
424 }
425 }
426
427 tracing::debug!(
429 target: trace_categories::COMMANDS,
430 "Spawning: cmd='{} {}'",
431 cmd.get_program().to_string_lossy().to_string(),
432 cmd.get_args()
433 .map(|a| a.to_string_lossy().to_string())
434 .join(" ")
435 );
436
437 match sys::process::spawn(cmd) {
438 Ok(child) => {
439 #[allow(clippy::cast_possible_wrap)]
441 let pid = child.id().map(|id| id as i32);
442 if let Some(pid) = &pid {
443 if new_pg {
444 *process_group_id = Some(*pid);
445 }
446 } else {
447 tracing::warn!("could not retrieve pid for child process");
448 }
449
450 Ok(CommandSpawnResult::SpawnedProcess(
451 processes::ChildProcess::new(pid, child),
452 ))
453 }
454 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
455 if context.shell.options.interactive {
456 sys::terminal::move_self_to_foreground()?;
457 }
458
459 if !context.shell.working_dir.exists() {
460 writeln!(
462 stderr,
463 "{}: working directory does not exist: {}",
464 context.shell.shell_name.as_ref().unwrap_or(&String::new()),
465 context.shell.working_dir.display()
466 )?;
467 } else if context.shell.options.sh_mode {
468 writeln!(
469 stderr,
470 "{}: {}: {}: not found",
471 context.shell.shell_name.as_ref().unwrap_or(&String::new()),
472 context.shell.get_current_input_line_number(),
473 context.command_name
474 )?;
475 } else {
476 writeln!(stderr, "{}: not found", context.command_name)?;
477 }
478 Ok(CommandSpawnResult::ImmediateExit(127))
479 }
480 Err(e) => {
481 if context.shell.options.interactive {
482 sys::terminal::move_self_to_foreground()?;
483 }
484
485 tracing::error!("{e}");
486 Ok(CommandSpawnResult::ImmediateExit(126))
487 }
488 }
489}
490
491#[cfg(unix)]
492fn setup_process_before_exec() -> Result<(), std::io::Error> {
493 sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
494 Ok(())
495}
496
497async fn execute_builtin_command(
498 builtin: &builtins::Registration,
499 context: ExecutionContext<'_>,
500 args: Vec<CommandArg>,
501) -> Result<CommandSpawnResult, error::Error> {
502 let exit_code = match (builtin.execute_func)(context, args).await {
503 Ok(builtin_result) => match builtin_result.exit_code {
504 builtins::ExitCode::Success => 0,
505 builtins::ExitCode::InvalidUsage => 2,
506 builtins::ExitCode::Unimplemented => 99,
507 builtins::ExitCode::Custom(code) => code,
508 builtins::ExitCode::ExitShell(code) => return Ok(CommandSpawnResult::ExitShell(code)),
509 builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
510 return Ok(CommandSpawnResult::ReturnFromFunctionOrScript(code));
511 }
512 builtins::ExitCode::BreakLoop(count) => {
513 return Ok(CommandSpawnResult::BreakLoop(count));
514 }
515 builtins::ExitCode::ContinueLoop(count) => {
516 return Ok(CommandSpawnResult::ContinueLoop(count));
517 }
518 },
519 Err(e @ error::Error::Unimplemented(..)) => {
520 tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
521 1
522 }
523 Err(e @ error::Error::UnimplementedAndTracked(..)) => {
524 tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
525 1
526 }
527 Err(e) => {
528 tracing::error!("{e}");
529 1
530 }
531 };
532
533 Ok(CommandSpawnResult::ImmediateExit(exit_code))
534}
535
536pub(crate) async fn invoke_shell_function(
537 function_definition: Arc<ast::FunctionDefinition>,
538 mut context: ExecutionContext<'_>,
539 args: &[CommandArg],
540) -> Result<CommandSpawnResult, error::Error> {
541 let ast::FunctionBody(body, redirects) = &function_definition.body;
542
543 if let Some(redirects) = redirects {
545 for redirect in &redirects.0 {
546 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
547 }
548 }
549
550 let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
552 context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
553
554 let params = context.params.clone();
556
557 context
560 .shell
561 .enter_function(context.command_name.as_str(), &function_definition)?;
562
563 let result = body.execute(context.shell, ¶ms).await;
565
566 drop(params);
568
569 context.shell.leave_function()?;
571
572 context.shell.positional_parameters = prior_positional_params;
574
575 let result = result?;
577
578 Ok(if result.exit_shell {
580 CommandSpawnResult::ExitShell(result.exit_code)
581 } else {
582 CommandSpawnResult::ImmediateExit(result.exit_code)
583 })
584}
585
586pub(crate) async fn invoke_command_in_subshell_and_get_output(
587 shell: &mut Shell,
588 params: &ExecutionParameters,
589 s: String,
590) -> Result<String, error::Error> {
591 let mut subshell = shell.clone();
593
594 let mut params = params.clone();
596 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
597
598 let (reader, writer) = sys::pipes::pipe()?;
600 params
601 .open_files
602 .files
603 .insert(1, openfiles::OpenFile::PipeWriter(writer));
604
605 let result = subshell.run_string(s, ¶ms).await?;
607
608 drop(subshell);
611 drop(params);
612
613 shell.last_exit_status = result.exit_code;
615
616 let output_str = std::io::read_to_string(reader)?;
618
619 Ok(output_str)
620}