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