Skip to main content

yash_env/semantics/
command.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2025 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Command execution components
18//!
19//! This module provides functionality related to command execution semantics.
20
21pub mod search;
22
23use crate::Env;
24use crate::function::Function;
25use crate::job::{RunBlocking, RunUnblocking, handle_job_status};
26use crate::semantics::{ExitStatus, Field, Result};
27use crate::source::Location;
28use crate::source::pretty::{Report, ReportType, Snippet};
29use crate::subshell::{BlockSignals, Config};
30use crate::system::concurrency::WaitForSignals;
31use crate::system::resource::SetRlimit;
32use crate::system::{
33    Close, Dup, Errno, Exec, Exit, Fork, GetPid, Open, SendSignal, SetPgid, ShellPath, TcSetPgrp,
34    Wait,
35};
36use crate::trap::SignalSystem;
37use itertools::Itertools as _;
38use std::convert::Infallible;
39use std::ffi::CString;
40use std::ops::ControlFlow::Continue;
41use std::pin::Pin;
42use std::rc::Rc;
43use thiserror::Error;
44
45type PinFuture<'a, T = ()> = Pin<Box<dyn Future<Output = T> + 'a>>;
46type FutureResult<'a, T = ()> = PinFuture<'a, Result<T>>;
47
48type EnvPrepHook<S> = fn(&mut Env<S>) -> PinFuture<'_, ()>;
49
50/// Wrapper for a function that runs a shell function
51///
52/// This struct declares a function type that runs a shell function.
53/// It is used to inject command execution behavior into the shell environment.
54/// An instance of this struct can be stored in the shell environment
55/// ([`Env::any`]) and used by modules that need to run shell functions.
56///
57/// The wrapped function takes the following arguments:
58///
59/// 1. A mutable reference to the shell environment (`&'a mut Env`)
60/// 2. A reference-counted pointer to the shell function to be executed (`Rc<Function>`)
61/// 3. A vector of fields representing the arguments to be passed to the function (`Vec<Field>`)
62///     - This should not be empty; the first element is the function name and
63///       the rest are the actual arguments.
64/// 4. An optional environment preparation hook
65///    (`Option<fn(&mut Env) -> Pin<Box<dyn Future<Output = ()>>>>`)
66///     - This hook is called after setting up the local variable context. It can inject
67///       additional setup logic or modify the environment before the function is executed.
68///
69/// The function returns a future that resolves to a [`Result`] indicating the
70/// outcome of the function execution.
71///
72/// The most standard implementation of this type is provided in the
73/// [`yash-semantics` crate](https://crates.io/crates/yash-semantics):
74///
75/// ```
76/// # use yash_env::Env;
77/// # use yash_env::semantics::command::RunFunction;
78/// fn register_run_function<S: 'static>(env: &mut Env<S>) {
79///     env.any.insert(Box::new(RunFunction::<S>(|env, function, fields, env_prep_hook| {
80///         Box::pin(async move {
81///             yash_semantics::command::simple_command::execute_function_body(
82///                 env, function, fields, env_prep_hook
83///             ).await
84///         })
85///     })));
86/// }
87/// # register_run_function(&mut Env::new_virtual());
88/// ```
89pub struct RunFunction<S>(
90    #[allow(clippy::type_complexity, reason = "we can't make this simpler")]
91    pub  for<'a> fn(
92        &'a mut Env<S>,
93        Rc<Function<S>>,
94        Vec<Field>,
95        Option<EnvPrepHook<S>>,
96    ) -> FutureResult<'a>,
97);
98
99// Not derived automatically because S may not implement Clone, Copy or Debug.
100impl<S> Clone for RunFunction<S> {
101    fn clone(&self) -> Self {
102        *self
103    }
104}
105
106impl<S> Copy for RunFunction<S> {}
107
108impl<S> std::fmt::Debug for RunFunction<S> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_tuple("RunFunction").field(&self.0).finish()
111    }
112}
113
114/// Error returned when [replacing the current process](replace_current_process) fails
115#[derive(Clone, Debug, Error)]
116#[error("cannot execute external utility {path:?}: {errno}")]
117pub struct ReplaceCurrentProcessError {
118    /// Path of the external utility attempted to be executed
119    pub path: CString,
120    /// Error returned by the [`execve`](Exec::execve) system call
121    pub errno: Errno,
122}
123
124/// Substitutes the currently executing shell process with the external utility.
125///
126/// This function performs the very last step of the simple command execution.
127/// It disables the internal signal dispositions and calls the
128/// [`execve`](Exec::execve) system call. If the call fails, it updates
129/// `env.exit_status` and returns an error, in which case the caller should
130/// print an error message and terminate the current process with the exit
131/// status.
132///
133/// If the `execve` call fails with [`ENOEXEC`](Errno::ENOEXEC), this function
134/// falls back on invoking the shell with the given arguments, so that the shell
135/// can interpret the script. The path to the shell executable is taken from
136/// [`ShellPath::shell_path`].
137///
138/// If the `execve` call succeeds, the future returned by this function never
139/// resolves.
140///
141/// This function is for implementing the simple command execution semantics and
142/// the `exec` built-in utility.
143pub async fn replace_current_process<S: Exec + ShellPath + SignalSystem>(
144    env: &mut Env<S>,
145    path: CString,
146    args: Vec<Field>,
147) -> std::result::Result<Infallible, ReplaceCurrentProcessError> {
148    env.traps
149        .disable_internal_dispositions(&env.system)
150        .await
151        .ok();
152
153    let args = to_c_strings(args);
154    let envs = env.variables.env_c_strings();
155    let Err(errno) = env
156        .system
157        .execve(path.as_c_str(), args.as_slice(), envs.as_slice())
158        .await;
159    env.exit_status = match errno {
160        Errno::ENOEXEC => {
161            fall_back_on_sh(&env.system, path.clone(), args, envs).await;
162            ExitStatus::NOEXEC
163        }
164        Errno::ENOENT | Errno::ENOTDIR => ExitStatus::NOT_FOUND,
165        _ => ExitStatus::NOEXEC,
166    };
167    Err(ReplaceCurrentProcessError { path, errno })
168}
169
170/// Converts fields to C strings.
171fn to_c_strings(s: Vec<Field>) -> Vec<CString> {
172    s.into_iter()
173        .filter_map(|f| {
174            let bytes = f.value.into_bytes();
175            // TODO Handle interior null bytes more gracefully
176            CString::new(bytes).ok()
177        })
178        .collect()
179}
180
181/// Invokes the shell with the given arguments.
182async fn fall_back_on_sh<S: ShellPath + Exec>(
183    system: &S,
184    mut script_path: CString,
185    mut args: Vec<CString>,
186    envs: Vec<CString>,
187) {
188    // Prevent the path to be regarded as an option
189    if script_path.as_bytes().starts_with("-".as_bytes()) {
190        let mut bytes = script_path.into_bytes();
191        bytes.splice(0..0, "./".bytes());
192        script_path = CString::new(bytes).unwrap();
193    }
194
195    args.insert(1, script_path);
196
197    // Some shells change their behavior depending on args[0].
198    // We set it to "sh" for the maximum portability.
199    c"sh".clone_into(&mut args[0]);
200
201    let sh_path = system.shell_path();
202    system.execve(&sh_path, args, envs).await.ok();
203}
204
205/// Error returned when starting a subshell fails in [`run_external_utility_in_subshell`]
206#[derive(Clone, Debug, Error)]
207#[error("cannot start subshell for utility {utility:?}: {errno}")]
208pub struct StartSubshellError {
209    pub utility: Field,
210    pub errno: Errno,
211}
212
213impl<'a> From<&'a StartSubshellError> for Report<'a> {
214    fn from(error: &'a StartSubshellError) -> Self {
215        let mut report = Report::new();
216        report.r#type = ReportType::Error;
217        report.title = format!(
218            "cannot start subshell for utility {:?}",
219            error.utility.value
220        )
221        .into();
222        report.snippets = Snippet::with_primary_span(
223            &error.utility.origin,
224            format!("{:?}: {}", error.utility.value, error.errno).into(),
225        );
226        report
227    }
228}
229
230/// Starts an external utility in a subshell and waits for it to finish.
231///
232/// `path` is the path to the external utility. `args` are the command line
233/// words of the utility. The first field must exist and be the name of the
234/// utility as it may be used in error messages.
235///
236/// This function starts the utility in a subshell and waits for it to finish.
237/// The subshell will be a foreground job if job control is enabled.
238///
239/// This function returns the exit status of the utility. In case of an error,
240/// one of the error handling functions will be called before returning an
241/// appropriate exit status. `handle_start_subshell_error` is called in the
242/// parent shell if starting the subshell fails.
243/// `handle_replace_current_process_error` is called in the subshell if
244/// replacing the subshell process with the utility fails. Both functions
245/// should print appropriate error messages.
246///
247/// This function is for implementing the simple command execution semantics and
248/// the `command` built-in utility. This function internally uses
249/// [`replace_current_process`] to execute the utility in the subshell.
250pub async fn run_external_utility_in_subshell<S>(
251    env: &mut Env<S>,
252    path: CString,
253    args: Vec<Field>,
254    handle_start_subshell_error: fn(&mut Env<S>, StartSubshellError) -> PinFuture<'_>,
255    handle_replace_current_process_error: fn(
256        &mut Env<S>,
257        ReplaceCurrentProcessError,
258        Location,
259    ) -> PinFuture<'_>,
260) -> Result<ExitStatus>
261where
262    S: BlockSignals
263        + Close
264        + Dup
265        + Exec
266        + Exit
267        + Fork
268        + GetPid
269        + Open
270        + RunBlocking
271        + RunUnblocking
272        + SendSignal
273        + SetPgid
274        + SetRlimit
275        + ShellPath
276        + SignalSystem
277        + TcSetPgrp
278        + Wait
279        + WaitForSignals
280        + 'static,
281{
282    let utility = args[0].clone();
283
284    let job_name = if env.controls_jobs() {
285        to_job_name(&args)
286    } else {
287        String::new()
288    };
289    let subshell_result =
290        Config::foreground().start_and_wait(env, async move |env, _job_control| {
291            let location = args[0].origin.clone();
292            let Err(e) = replace_current_process(env, path, args).await;
293            handle_replace_current_process_error(env, e, location).await;
294        });
295
296    match subshell_result.await {
297        Ok((pid, result)) => handle_job_status(env, pid, result, || job_name),
298        Err(errno) => {
299            handle_start_subshell_error(env, StartSubshellError { utility, errno }).await;
300            Continue(ExitStatus::NOEXEC)
301        }
302    }
303}
304
305fn to_job_name(fields: &[Field]) -> String {
306    fields
307        .iter()
308        .format_with(" ", |field, f| f(&format_args!("{}", field.value)))
309        .to_string()
310}