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 builtins, error, escape,
13 interp::{self, Execute, ProcessGroupPolicy},
14 openfiles::{self, OpenFile, OpenFiles},
15 processes, sys, trace_categories, ExecutionParameters, ExecutionResult, Shell,
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 CommandSpawnResult::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 CommandSpawnResult::ImmediateExit(exit_code) => Ok(
63 CommandWaitResult::CommandCompleted(ExecutionResult::new(exit_code)),
64 ),
65 CommandSpawnResult::ExitShell(exit_code) => {
66 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
67 exit_code,
68 exit_shell: true,
69 ..ExecutionResult::default()
70 }))
71 }
72 CommandSpawnResult::ReturnFromFunctionOrScript(exit_code) => {
73 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
74 exit_code,
75 return_from_function_or_script: true,
76 ..ExecutionResult::default()
77 }))
78 }
79 CommandSpawnResult::BreakLoop(count) => {
80 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
81 exit_code: 0,
82 break_loop: Some(count),
83 ..ExecutionResult::default()
84 }))
85 }
86 CommandSpawnResult::ContinueLoop(count) => {
87 Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
88 exit_code: 0,
89 continue_loop: Some(count),
90 ..ExecutionResult::default()
91 }))
92 }
93 }
94 }
95}
96
97pub(crate) enum CommandWaitResult {
99 CommandCompleted(ExecutionResult),
101 CommandStopped(ExecutionResult, processes::ChildProcess),
103}
104
105pub struct ExecutionContext<'a> {
107 pub shell: &'a mut Shell,
109 pub command_name: String,
111 pub params: ExecutionParameters,
113}
114
115impl ExecutionContext<'_> {
116 pub fn stdin(&self) -> openfiles::OpenFile {
118 self.params.stdin()
119 }
120
121 pub fn stdout(&self) -> openfiles::OpenFile {
123 self.params.stdout()
124 }
125
126 pub fn stderr(&self) -> openfiles::OpenFile {
128 self.params.stderr()
129 }
130
131 pub(crate) fn should_cmd_lead_own_process_group(&self) -> bool {
132 self.shell.options.interactive
133 && matches!(
134 self.params.process_group_policy,
135 ProcessGroupPolicy::NewProcessGroup
136 )
137 }
138}
139
140#[derive(Clone, Debug)]
142pub enum CommandArg {
143 String(String),
145 Assignment(ast::Assignment),
148}
149
150impl Display for CommandArg {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 CommandArg::String(s) => f.write_str(s),
154 CommandArg::Assignment(a) => write!(f, "{a}"),
155 }
156 }
157}
158
159impl From<String> for CommandArg {
160 fn from(s: String) -> Self {
161 CommandArg::String(s)
162 }
163}
164
165impl From<&String> for CommandArg {
166 fn from(value: &String) -> Self {
167 CommandArg::String(value.clone())
168 }
169}
170
171impl CommandArg {
172 pub fn quote_for_tracing(&self) -> Cow<'_, str> {
173 match self {
174 CommandArg::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
175 CommandArg::Assignment(a) => {
176 let mut s = a.name.to_string();
177 let op = if a.append { "+=" } else { "=" };
178 s.push_str(op);
179 s.push_str(&escape::quote_if_needed(
180 a.value.to_string().as_str(),
181 escape::QuoteMode::SingleQuote,
182 ));
183 s.into()
184 }
185 }
186 }
187}
188
189#[allow(unused_variables)]
190pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
191 shell: &mut Shell,
192 command_name: &str,
193 argv0: &str,
194 args: &[S],
195 mut open_files: OpenFiles,
196 empty_env: bool,
197) -> Result<std::process::Command, error::Error> {
198 let mut cmd = std::process::Command::new(command_name);
199
200 #[cfg(unix)]
202 cmd.arg0(argv0);
203
204 cmd.args(args);
206
207 cmd.current_dir(shell.working_dir.as_path());
209
210 cmd.env_clear();
212
213 if !empty_env {
215 for (k, v) in shell.env.iter_exported() {
216 cmd.env(k.as_str(), v.value().to_cow_str(shell).as_ref());
217 }
218 }
219
220 match open_files.files.remove(&0) {
222 Some(OpenFile::Stdin) | None => (),
223 Some(stdin_file) => {
224 let as_stdio: Stdio = stdin_file.into();
225 cmd.stdin(as_stdio);
226 }
227 }
228
229 match open_files.files.remove(&1) {
231 Some(OpenFile::Stdout) | None => (),
232 Some(stdout_file) => {
233 let as_stdio: Stdio = stdout_file.into();
234 cmd.stdout(as_stdio);
235 }
236 }
237
238 match open_files.files.remove(&2) {
240 Some(OpenFile::Stderr) | None => {}
241 Some(stderr_file) => {
242 let as_stdio: Stdio = stderr_file.into();
243 cmd.stderr(as_stdio);
244 }
245 }
246
247 #[cfg(unix)]
249 {
250 let fd_mappings = open_files
251 .files
252 .into_iter()
253 .map(|(child_fd, open_file)| FdMapping {
254 child_fd: i32::try_from(child_fd).unwrap(),
255 parent_fd: open_file.into_owned_fd().unwrap(),
256 })
257 .collect();
258 cmd.fd_mappings(fd_mappings)
259 .map_err(|_e| error::Error::ChildCreationFailure)?;
260 }
261 #[cfg(not(unix))]
262 {
263 if !open_files.files.is_empty() {
264 return error::unimp("fd redirections on non-Unix platform");
265 }
266 }
267
268 Ok(cmd)
269}
270
271pub(crate) async fn execute(
272 cmd_context: ExecutionContext<'_>,
273 process_group_id: &mut Option<i32>,
274 args: Vec<CommandArg>,
275 use_functions: bool,
276 path_dirs: Option<Vec<String>>,
277) -> Result<CommandSpawnResult, error::Error> {
278 if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
279 let builtin = cmd_context
280 .shell
281 .builtins
282 .get(&cmd_context.command_name)
283 .cloned();
284
285 if builtin
287 .as_ref()
288 .is_some_and(|r| !r.disabled && r.special_builtin)
289 {
290 return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
291 }
292
293 if use_functions {
294 if let Some(func_reg) = cmd_context
295 .shell
296 .funcs
297 .get(cmd_context.command_name.as_str())
298 {
299 return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
301 .await;
302 }
303 }
304
305 if let Some(builtin) = builtin {
306 if !builtin.disabled {
307 return execute_builtin_command(&builtin, cmd_context, args).await;
308 }
309 }
310
311 let path = if let Some(path_dirs) = path_dirs {
312 cmd_context
313 .shell
314 .find_executables_in(path_dirs.iter(), &cmd_context.command_name)
315 .first()
316 .cloned()
317 } else {
318 cmd_context
319 .shell
320 .find_first_executable_in_path_using_cache(&cmd_context.command_name)
321 };
322
323 if let Some(path) = path {
324 let resolved_path = path.to_string_lossy();
325 execute_external_command(
326 cmd_context,
327 resolved_path.as_ref(),
328 process_group_id,
329 &args[1..],
330 )
331 } else {
332 writeln!(
333 cmd_context.stderr(),
334 "{}: command not found",
335 cmd_context.command_name
336 )?;
337 Ok(CommandSpawnResult::ImmediateExit(127))
338 }
339 } else {
340 let resolved_path = cmd_context.command_name.clone();
341
342 execute_external_command(
344 cmd_context,
345 resolved_path.as_str(),
346 process_group_id,
347 &args[1..],
348 )
349 }
350}
351
352#[allow(clippy::too_many_lines)]
353#[allow(unused_variables)]
354pub(crate) fn execute_external_command(
355 context: ExecutionContext<'_>,
356 executable_path: &str,
357 process_group_id: &mut Option<i32>,
358 args: &[CommandArg],
359) -> Result<CommandSpawnResult, error::Error> {
360 let mut cmd_args = vec![];
362 for arg in args {
363 if let CommandArg::String(s) = arg {
364 cmd_args.push(s);
365 }
366 }
367
368 #[allow(unused_variables)]
370 let child_stdin_is_terminal = context
371 .params
372 .open_files
373 .stdin()
374 .is_some_and(|f| f.is_term());
375
376 let new_pg = context.should_cmd_lead_own_process_group();
378
379 let mut stderr = context.stderr();
381
382 #[allow(unused_mut)]
384 let mut cmd = compose_std_command(
385 context.shell,
386 executable_path,
387 context.command_name.as_str(),
388 cmd_args.as_slice(),
389 context.params.open_files,
390 false, )?;
392
393 if new_pg {
395 #[cfg(unix)]
397 cmd.process_group(0);
398 } else if let Some(pgid) = process_group_id {
399 #[cfg(unix)]
401 cmd.process_group(*pgid);
402 }
403
404 #[cfg(unix)]
407 if new_pg && child_stdin_is_terminal {
408 unsafe {
409 cmd.pre_exec(setup_process_before_exec);
410 }
411 }
412
413 tracing::debug!(
415 target: trace_categories::COMMANDS,
416 "Spawning: cmd='{} {}'",
417 cmd.get_program().to_string_lossy().to_string(),
418 cmd.get_args()
419 .map(|a| a.to_string_lossy().to_string())
420 .join(" ")
421 );
422
423 match sys::process::spawn(cmd) {
424 Ok(child) => {
425 #[allow(clippy::cast_possible_wrap)]
427 let pid = child.id().map(|id| id as i32);
428 if let Some(pid) = &pid {
429 if new_pg {
430 *process_group_id = Some(*pid);
431 }
432 } else {
433 tracing::warn!("could not retrieve pid for child process");
434 }
435
436 Ok(CommandSpawnResult::SpawnedProcess(
437 processes::ChildProcess::new(pid, child),
438 ))
439 }
440 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
441 if context.shell.options.interactive {
442 sys::terminal::move_self_to_foreground()?;
443 }
444
445 if !context.shell.working_dir.exists() {
446 writeln!(
448 stderr,
449 "{}: working directory does not exist: {}",
450 context.shell.shell_name.as_ref().unwrap_or(&String::new()),
451 context.shell.working_dir.display()
452 )?;
453 } else if context.shell.options.sh_mode {
454 writeln!(
455 stderr,
456 "{}: {}: {}: not found",
457 context.shell.shell_name.as_ref().unwrap_or(&String::new()),
458 context.shell.get_current_input_line_number(),
459 context.command_name
460 )?;
461 } else {
462 writeln!(stderr, "{}: not found", context.command_name)?;
463 }
464 Ok(CommandSpawnResult::ImmediateExit(127))
465 }
466 Err(e) => {
467 if context.shell.options.interactive {
468 sys::terminal::move_self_to_foreground()?;
469 }
470
471 tracing::error!("error: {}", e);
472 Ok(CommandSpawnResult::ImmediateExit(126))
473 }
474 }
475}
476
477#[cfg(unix)]
478fn setup_process_before_exec() -> Result<(), std::io::Error> {
479 sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
480 Ok(())
481}
482
483async fn execute_builtin_command(
484 builtin: &builtins::Registration,
485 context: ExecutionContext<'_>,
486 args: Vec<CommandArg>,
487) -> Result<CommandSpawnResult, error::Error> {
488 let exit_code = match (builtin.execute_func)(context, args).await {
489 Ok(builtin_result) => match builtin_result.exit_code {
490 builtins::ExitCode::Success => 0,
491 builtins::ExitCode::InvalidUsage => 2,
492 builtins::ExitCode::Unimplemented => 99,
493 builtins::ExitCode::Custom(code) => code,
494 builtins::ExitCode::ExitShell(code) => return Ok(CommandSpawnResult::ExitShell(code)),
495 builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
496 return Ok(CommandSpawnResult::ReturnFromFunctionOrScript(code))
497 }
498 builtins::ExitCode::BreakLoop(count) => {
499 return Ok(CommandSpawnResult::BreakLoop(count))
500 }
501 builtins::ExitCode::ContinueLoop(count) => {
502 return Ok(CommandSpawnResult::ContinueLoop(count))
503 }
504 },
505 Err(e) => {
506 tracing::error!("error: {}", e);
507 1
508 }
509 };
510
511 Ok(CommandSpawnResult::ImmediateExit(exit_code))
512}
513
514pub(crate) async fn invoke_shell_function(
515 function_definition: Arc<ast::FunctionDefinition>,
516 mut context: ExecutionContext<'_>,
517 args: &[CommandArg],
518) -> Result<CommandSpawnResult, error::Error> {
519 let ast::FunctionBody(body, redirects) = &function_definition.body;
520
521 if let Some(redirects) = redirects {
523 for redirect in &redirects.0 {
524 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
525 }
526 }
527
528 let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
530 context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
531
532 let params = context.params.clone();
534
535 context
538 .shell
539 .enter_function(context.command_name.as_str(), &function_definition)?;
540
541 let result = body.execute(context.shell, ¶ms).await;
543
544 drop(params);
546
547 context.shell.leave_function()?;
549
550 context.shell.positional_parameters = prior_positional_params;
552
553 let result = result?;
555
556 Ok(if result.exit_shell {
558 CommandSpawnResult::ExitShell(result.exit_code)
559 } else {
560 CommandSpawnResult::ImmediateExit(result.exit_code)
561 })
562}
563
564pub(crate) async fn invoke_command_in_subshell_and_get_output(
565 shell: &mut Shell,
566 params: &ExecutionParameters,
567 s: String,
568) -> Result<String, error::Error> {
569 let mut subshell = shell.clone();
571
572 let mut params = params.clone();
574 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
575
576 let (reader, writer) = sys::pipes::pipe()?;
578 params
579 .open_files
580 .files
581 .insert(1, openfiles::OpenFile::PipeWriter(writer));
582
583 let result = subshell.run_string(s, ¶ms).await?;
585
586 drop(subshell);
589 drop(params);
590
591 shell.last_exit_status = result.exit_code;
593
594 let output_str = std::io::read_to_string(reader)?;
596
597 Ok(output_str)
598}