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