Skip to main content

yash_builtin/command/
invoke.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 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 invoking semantics
18
19use super::Invoke;
20use super::identify::NotFound;
21use super::search::SearchEnv;
22use crate::common::report::report_failure;
23use crate::exec::ExecFailure;
24use std::ops::ControlFlow::{Break, Continue};
25use yash_env::Env;
26use yash_env::semantics::ExitStatus;
27use yash_env::semantics::Field;
28use yash_env::semantics::command::RunFunction;
29use yash_env::semantics::command::run_external_utility_in_subshell;
30use yash_env::semantics::command::search::{Target, search};
31use yash_env::system::resource::SetRlimit;
32use yash_env::system::{
33    Close, Dup, Exec, Exit, Fcntl, Fork, GetPid, IsExecutableFile, Isatty, Open, SendSignal,
34    SetPgid, ShellPath, Sigaction, Sigmask, Signals, Sysconf, TcSetPgrp, Wait, Write,
35};
36
37impl Invoke {
38    /// Execute the command
39    pub async fn execute<S>(self, env: &mut Env<S>) -> crate::Result
40    where
41        S: Close
42            + Dup
43            + Exec
44            + Exit
45            + Fcntl
46            + Fork
47            + GetPid
48            + IsExecutableFile
49            + Isatty
50            + Open
51            + SendSignal
52            + SetPgid
53            + SetRlimit
54            + ShellPath
55            + Sigaction
56            + Sigmask
57            + Signals
58            + Sysconf
59            + TcSetPgrp
60            + Wait
61            + Write
62            + 'static,
63    {
64        let Some(name) = self.fields.first() else {
65            return crate::Result::default();
66        };
67
68        let params = &self.search;
69        let search_env = &mut SearchEnv { env, params };
70        let Some(target) = search(search_env, &name.value) else {
71            let mut result = report_failure(env, &NotFound { name }).await;
72            result.set_exit_status(ExitStatus::NOT_FOUND);
73            return result;
74        };
75
76        invoke_target(env, target, self.fields).await
77    }
78}
79
80/// Invokes the target with the given fields.
81///
82/// This function is called after the command is found. The first field must be
83/// the command name that was searched for. The rest of the fields are the
84/// arguments to the command.
85///
86/// This function requires an instance of [`RunFunction`] to be present in
87/// [`env.any`](Env::any), which is used to invoke shell functions. If no such
88/// instance is found, this function will **panic**.
89async fn invoke_target<S>(
90    env: &mut Env<S>,
91    target: Target<S>,
92    mut fields: Vec<Field>,
93) -> crate::Result
94where
95    S: Close
96        + Dup
97        + Exec
98        + Exit
99        + Fcntl
100        + Fork
101        + GetPid
102        + Isatty
103        + Open
104        + SendSignal
105        + SetPgid
106        + SetRlimit
107        + ShellPath
108        + Sigaction
109        + Sigmask
110        + Signals
111        + TcSetPgrp
112        + Wait
113        + Write
114        + 'static,
115{
116    match target {
117        Target::Builtin { builtin, .. } => {
118            let frame = yash_env::stack::Builtin {
119                name: fields.remove(0),
120                // Any built-in is considered non-special in the command built-in.
121                is_special: false,
122            };
123            let mut env = env.push_frame(frame.into());
124            (builtin.execute)(&mut env, fields).await
125        }
126
127        Target::Function(function) => {
128            let RunFunction(run_function) =
129                env.any.get().expect("`RunFunction` should be in `env.any`");
130            let divert = run_function(env, function, fields, None).await;
131            crate::Result::with_exit_status_and_divert(env.exit_status, divert)
132        }
133
134        Target::External { path } => {
135            let result = run_external_utility_in_subshell(
136                env,
137                path,
138                fields,
139                |env, error| Box::pin(async move { _ = report_failure(env, &error).await }),
140                |env, inner, location| {
141                    Box::pin(async move {
142                        _ = report_failure(env, &ExecFailure { inner, location }).await
143                    })
144                },
145            )
146            .await;
147
148            match result {
149                Continue(exit_status) => exit_status.into(),
150                Break(divert) => {
151                    crate::Result::with_exit_status_and_divert(env.exit_status, Break(divert))
152                }
153            }
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::super::Search;
161    use super::*;
162    use assert_matches::assert_matches;
163    use enumset::EnumSet;
164    use futures_util::FutureExt as _;
165    use std::ffi::CString;
166    use std::rc::Rc;
167    use yash_env::VirtualSystem;
168    use yash_env::builtin::Builtin;
169    use yash_env::builtin::Type::Special;
170    use yash_env::function::{Function, FunctionBody, FunctionBodyObject};
171    use yash_env::semantics::Field;
172    use yash_env::source::Location;
173    use yash_env::test_helper::assert_stderr;
174    use yash_env::test_helper::assert_stdout;
175    use yash_semantics::Divert::Return;
176    use yash_semantics::Runtime;
177    use yash_syntax::syntax::FullCompoundCommand;
178
179    /// Test body wrapper that actually executes the command
180    #[derive(Clone, Debug)]
181    struct FunctionBodyImpl(FullCompoundCommand);
182
183    impl std::fmt::Display for FunctionBodyImpl {
184        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185            self.0.fmt(f)
186        }
187    }
188    impl<S: Runtime + 'static> FunctionBody<S> for FunctionBodyImpl {
189        async fn execute(&self, env: &mut Env<S>) -> yash_env::semantics::Result {
190            use yash_semantics::command::Command as _;
191            self.0.execute(env).await
192        }
193    }
194
195    fn function_body_impl<S: Runtime + 'static>(src: &str) -> Rc<dyn FunctionBodyObject<S>> {
196        Rc::new(FunctionBodyImpl(src.parse().unwrap()))
197    }
198
199    #[test]
200    fn empty_command_invocation() {
201        let mut env = Env::new_virtual();
202        let invoke = Invoke::default();
203        let result = invoke.execute(&mut env).now_or_never().unwrap();
204        assert_eq!(result, crate::Result::default());
205    }
206
207    #[test]
208    fn command_not_found() {
209        let system = VirtualSystem::new();
210        let state = Rc::clone(&system.state);
211        let mut env = Env::with_system(system);
212        env.builtins
213            .insert("foo", Builtin::new(Special, |_, _| unreachable!()));
214        let invoke = Invoke {
215            fields: Field::dummies(["foo"]),
216            search: Search {
217                standard_path: false,
218                categories: EnumSet::empty(),
219            },
220        };
221
222        let result = invoke.execute(&mut env).now_or_never().unwrap();
223        assert_eq!(result.exit_status(), ExitStatus::NOT_FOUND);
224        assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
225        assert_stderr(&state, |stderr| {
226            assert!(stderr.contains("not found"), "stderr: {stderr:?}");
227        });
228    }
229
230    #[test]
231    fn invoking_builtin() {
232        fn make_result() -> yash_env::builtin::Result {
233            let mut result = crate::Result::default();
234            result.set_exit_status(ExitStatus(79));
235            result.set_divert(Break(Return(None)));
236            result.retain_redirs();
237            result
238        }
239
240        let mut env = Env::new_virtual();
241        let target = Target::Builtin {
242            builtin: Builtin::new(Special, |_, args| {
243                Box::pin(async move {
244                    assert_eq!(args, Field::dummies(["bar", "baz"]));
245                    make_result()
246                })
247            }),
248            path: CString::default(),
249        };
250
251        let result = invoke_target(&mut env, target, Field::dummies(["foo", "bar", "baz"]))
252            .now_or_never()
253            .unwrap();
254        assert_eq!(result, make_result());
255    }
256
257    #[test]
258    fn invoking_function() {
259        let mut env = Env::new_virtual();
260        env.any.insert(Box::new(RunFunction::<VirtualSystem>(
261            |env, function, fields, env_prep_hook| {
262                Box::pin(async move {
263                    yash_semantics::command::simple_command::execute_function_body(
264                        env,
265                        function,
266                        fields,
267                        env_prep_hook,
268                    )
269                    .await
270                })
271            },
272        )));
273        env.builtins.insert(
274            ":",
275            Builtin::new(Special, |_, args| {
276                Box::pin(async move {
277                    assert_matches!(args.as_slice(), [bar, baz] => {
278                        assert_eq!(bar.value, "bar");
279                        assert_eq!(baz.value, "baz");
280                    });
281                    crate::Result::with_exit_status_and_divert(ExitStatus(42), Break(Return(None)))
282                })
283            }),
284        );
285        let origin = Location::dummy("some location");
286        let target = Target::Function(Rc::new(Function::new(
287            "foo",
288            function_body_impl(r#"{ : "$@"; }"#),
289            origin,
290        )));
291
292        let result = invoke_target(&mut env, target, Field::dummies(["foo", "bar", "baz"]))
293            .now_or_never()
294            .unwrap();
295        assert_eq!(result, crate::Result::from(ExitStatus(42)));
296    }
297}