yash_semantics/command/simple_command/
external.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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//! Simple command semantics for external utilities
18
19use super::perform_assignments;
20use crate::Handle;
21use crate::command_search::search_path;
22use crate::job::add_job_if_suspended;
23use crate::redir::RedirGuard;
24use crate::xtrace::XTrace;
25use crate::xtrace::print;
26use crate::xtrace::trace_fields;
27use itertools::Itertools;
28use std::ffi::CString;
29use std::ops::ControlFlow::Continue;
30use yash_env::Env;
31use yash_env::System;
32use yash_env::io::print_error;
33use yash_env::semantics::ExitStatus;
34use yash_env::semantics::Field;
35use yash_env::semantics::Result;
36use yash_env::subshell::JobControl;
37use yash_env::subshell::Subshell;
38use yash_env::system::Errno;
39use yash_env::variable::Context;
40use yash_syntax::source::Location;
41use yash_syntax::syntax::Assign;
42use yash_syntax::syntax::Redir;
43
44pub async fn execute_external_utility(
45    env: &mut Env,
46    assigns: &[Assign],
47    fields: Vec<Field>,
48    redirs: &[Redir],
49) -> Result {
50    let mut xtrace = XTrace::from_options(&env.options);
51
52    let env = &mut RedirGuard::new(env);
53    if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
54        return e.handle(env).await;
55    };
56
57    let mut env = env.push_context(Context::Volatile);
58    perform_assignments(&mut env, assigns, true, xtrace.as_mut()).await?;
59
60    trace_fields(xtrace.as_mut(), &fields);
61    print(&mut env, xtrace).await;
62
63    let name = &fields[0];
64    let path = if name.value.contains('/') {
65        CString::new(&*name.value).ok()
66    } else {
67        search_path(&mut *env, &name.value)
68    };
69
70    if let Some(path) = path {
71        env.exit_status =
72            start_external_utility_in_subshell_and_wait(&mut env, path, fields).await?;
73    } else {
74        print_error(
75            &mut env,
76            format!("cannot execute external utility {:?}", name.value).into(),
77            "utility not found".into(),
78            &name.origin,
79        )
80        .await;
81        env.exit_status = ExitStatus::NOT_FOUND;
82    }
83
84    Continue(())
85}
86
87/// Starts an external utility in a subshell and waits for it to finish.
88///
89/// `path` is the path to the external utility. `fields` are the command line
90/// words of the utility. The first field must exist and be the name of the
91/// utility as it is used for error messages.
92///
93/// This function starts the utility in a subshell and waits for it to finish.
94/// The subshell is a foreground job if job control is enabled.
95///
96/// This function returns the exit status of the utility. In case of an error,
97/// it prints an error message to the standard error before returning an
98/// appropriate exit status.
99pub async fn start_external_utility_in_subshell_and_wait(
100    env: &mut Env,
101    path: CString,
102    fields: Vec<Field>,
103) -> Result<ExitStatus> {
104    let name = fields[0].clone();
105    let location = name.origin.clone();
106
107    let job_name = if env.controls_jobs() {
108        to_job_name(&fields)
109    } else {
110        String::new()
111    };
112    let args = to_c_strings(fields);
113    let subshell = Subshell::new(move |env, _job_control| {
114        Box::pin(replace_current_process(env, path, args, location))
115    })
116    .job_control(JobControl::Foreground);
117
118    match subshell.start_and_wait(env).await {
119        Ok((pid, result)) => add_job_if_suspended(env, pid, result, || job_name),
120        Err(errno) => {
121            print_error(
122                env,
123                format!("cannot execute external utility {:?}", name.value).into(),
124                errno.to_string().into(),
125                &name.origin,
126            )
127            .await;
128            Continue(ExitStatus::NOEXEC)
129        }
130    }
131}
132
133fn to_job_name(fields: &[Field]) -> String {
134    fields
135        .iter()
136        .format_with(" ", |field, f| f(&format_args!("{}", field.value)))
137        .to_string()
138}
139
140/// Converts fields to C strings.
141pub fn to_c_strings(s: Vec<Field>) -> Vec<CString> {
142    s.into_iter()
143        .filter_map(|f| {
144            let bytes = f.value.into_bytes();
145            // TODO Return NulError if the field contains a null byte
146            CString::new(bytes).ok()
147        })
148        .collect()
149}
150
151/// Substitutes the currently executing shell process with the external utility.
152///
153/// This function performs the very last step of the simple command execution.
154/// It disables the internal signal dispositions and calls the `execve` system
155/// call. If the call fails, it prints an error message to the standard error
156/// and updates `env.exit_status`, in which case the caller should immediately
157/// exit the current process with the exit status.
158///
159/// If the `execve` call fails with `ENOEXEC`, this function falls back on
160/// invoking the shell with the given arguments, so that the shell can interpret
161/// the script. The path to the shell executable is taken from
162/// [`System::shell_path`].
163pub async fn replace_current_process(
164    env: &mut Env,
165    path: CString,
166    args: Vec<CString>,
167    location: Location,
168) {
169    env.traps
170        .disable_internal_dispositions(&mut env.system)
171        .ok();
172
173    let envs = env.variables.env_c_strings();
174    let result = env.system.execve(path.as_c_str(), &args, &envs).await;
175    // TODO Prefer into_err to unwrap_err
176    let errno = result.unwrap_err();
177    match errno {
178        Errno::ENOEXEC => {
179            fall_back_on_sh(&mut env.system, path.clone(), args, envs).await;
180            env.exit_status = ExitStatus::NOEXEC;
181        }
182        Errno::ENOENT | Errno::ENOTDIR => {
183            env.exit_status = ExitStatus::NOT_FOUND;
184        }
185        _ => {
186            env.exit_status = ExitStatus::NOEXEC;
187        }
188    }
189    print_error(
190        env,
191        format!("cannot execute external utility {path:?}").into(),
192        errno.to_string().into(),
193        &location,
194    )
195    .await;
196}
197
198/// Invokes the shell with the given arguments.
199async fn fall_back_on_sh<S: System>(
200    system: &mut S,
201    mut script_path: CString,
202    mut args: Vec<CString>,
203    envs: Vec<CString>,
204) {
205    // Prevent the path to be regarded as an option
206    if script_path.as_bytes().starts_with("-".as_bytes()) {
207        let mut bytes = script_path.into_bytes();
208        bytes.splice(0..0, "./".bytes());
209        script_path = CString::new(bytes).unwrap();
210    }
211
212    args.insert(1, script_path);
213
214    // Some shells change their behavior depending on args[0].
215    // We set it to "sh" for the maximum portability.
216    c"sh".clone_into(&mut args[0]);
217
218    let sh_path = system.shell_path();
219    system.execve(&sh_path, &args, &envs).await.ok();
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::command::Command;
226    use assert_matches::assert_matches;
227    use futures_util::FutureExt;
228    use std::cell::RefCell;
229    use std::ops::ControlFlow::Continue;
230    use std::rc::Rc;
231    use std::str::from_utf8;
232    use yash_env::option::State::On;
233    use yash_env::system::Mode;
234    use yash_env::system::r#virtual::FileBody;
235    use yash_env::system::r#virtual::Inode;
236    use yash_env::variable::Scope;
237    use yash_env::variable::Value;
238    use yash_env_test_helper::assert_stderr;
239    use yash_env_test_helper::in_virtual_system;
240    use yash_env_test_helper::stub_tty;
241    use yash_syntax::syntax;
242
243    #[test]
244    fn simple_command_calls_execve_with_correct_arguments() {
245        in_virtual_system(|mut env, state| async move {
246            let mut content = Inode::default();
247            content.body = FileBody::Regular {
248                content: Vec::new(),
249                is_native_executable: true,
250            };
251            content.permissions.set(Mode::USER_EXEC, true);
252            let content = Rc::new(RefCell::new(content));
253            state
254                .borrow_mut()
255                .file_system
256                .save("/some/file", content)
257                .unwrap();
258
259            let mut var = env.variables.get_or_new("env", Scope::Global);
260            var.assign("scalar", None).unwrap();
261            var.export(true);
262            let mut var = env.variables.get_or_new("local", Scope::Global);
263            var.assign("ignored", None).unwrap();
264
265            let command: syntax::SimpleCommand = "var=123 /some/file foo bar".parse().unwrap();
266            let result = command.execute(&mut env).await;
267            assert_eq!(result, Continue(()));
268
269            let state = state.borrow();
270            let process = state.processes.values().last().unwrap();
271            let arguments = process.last_exec().as_ref().unwrap();
272            assert_eq!(arguments.0, c"/some/file".to_owned());
273            assert_eq!(
274                arguments.1,
275                [
276                    c"/some/file".to_owned(),
277                    c"foo".to_owned(),
278                    c"bar".to_owned()
279                ]
280            );
281            let mut envs = arguments.2.clone();
282            envs.sort();
283            assert_eq!(envs, [c"env=scalar".to_owned(), c"var=123".to_owned()]);
284        });
285    }
286
287    #[test]
288    fn simple_command_returns_exit_status_from_external_utility() {
289        in_virtual_system(|mut env, state| async move {
290            let mut content = Inode::default();
291            content.body = FileBody::Regular {
292                content: Vec::new(),
293                is_native_executable: true,
294            };
295            content.permissions.set(Mode::USER_EXEC, true);
296            let content = Rc::new(RefCell::new(content));
297            state
298                .borrow_mut()
299                .file_system
300                .save("/some/file", content)
301                .unwrap();
302
303            let command: syntax::SimpleCommand = "/some/file foo bar".parse().unwrap();
304            let result = command.execute(&mut env).await;
305            assert_eq!(result, Continue(()));
306            // In VirtualSystem, execve fails with ENOSYS.
307            assert_eq!(env.exit_status, ExitStatus::NOEXEC);
308        });
309    }
310
311    // TODO Test fall_back_on_sh
312
313    #[test]
314    fn simple_command_skips_running_external_utility_on_redirection_error() {
315        in_virtual_system(|mut env, state| async move {
316            let mut content = Inode::default();
317            content.body = FileBody::Regular {
318                content: Vec::new(),
319                is_native_executable: true,
320            };
321            content.permissions.set(Mode::USER_EXEC, true);
322            let content = Rc::new(RefCell::new(content));
323            state
324                .borrow_mut()
325                .file_system
326                .save("/some/file", content)
327                .unwrap();
328
329            let command: syntax::SimpleCommand = "/some/file </no/such/file".parse().unwrap();
330            let result = command.execute(&mut env).await;
331            assert_eq!(result, Continue(()));
332            assert_eq!(env.exit_status, ExitStatus::ERROR);
333        });
334    }
335
336    #[test]
337    fn simple_command_returns_127_for_non_existing_file() {
338        in_virtual_system(|mut env, _state| async move {
339            let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
340            let result = command.execute(&mut env).await;
341            assert_eq!(result, Continue(()));
342            assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
343        });
344    }
345
346    #[test]
347    fn simple_command_returns_126_on_exec_failure() {
348        in_virtual_system(|mut env, state| async move {
349            let mut content = Inode::default();
350            content.permissions.set(Mode::USER_EXEC, true);
351            let content = Rc::new(RefCell::new(content));
352            state
353                .borrow_mut()
354                .file_system
355                .save("/some/file", content)
356                .unwrap();
357
358            let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
359            let result = command.execute(&mut env).await;
360            assert_eq!(result, Continue(()));
361            assert_eq!(env.exit_status, ExitStatus::NOEXEC);
362        });
363    }
364
365    #[test]
366    fn simple_command_returns_126_on_fork_failure() {
367        let mut env = Env::new_virtual();
368        let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
369        let result = command.execute(&mut env).now_or_never().unwrap();
370        assert_eq!(result, Continue(()));
371        assert_eq!(env.exit_status, ExitStatus::NOEXEC);
372    }
373
374    #[test]
375    fn exit_status_is_127_on_command_not_found() {
376        let mut env = Env::new_virtual();
377        let command: syntax::SimpleCommand = "no_such_command".parse().unwrap();
378        let result = command.execute(&mut env).now_or_never().unwrap();
379        assert_eq!(result, Continue(()));
380        assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
381    }
382
383    #[test]
384    fn simple_command_assigns_variables_in_volatile_context_for_external_utility() {
385        in_virtual_system(|mut env, _state| async move {
386            let command: syntax::SimpleCommand = "a=123 /foo/bar".parse().unwrap();
387            _ = command.execute(&mut env).await;
388            assert_eq!(env.variables.get("a"), None);
389        });
390    }
391
392    #[test]
393    fn simple_command_performs_redirections_and_assignments_for_target_not_found() {
394        in_virtual_system(|mut env, state| async move {
395            let command: syntax::SimpleCommand =
396                "foo=${bar=baz} no_such_utility >/tmp/file".parse().unwrap();
397            _ = command.execute(&mut env).await;
398            assert_eq!(env.variables.get("foo"), None);
399            assert_eq!(
400                env.variables.get("bar").unwrap().value,
401                Some(Value::scalar("baz"))
402            );
403
404            let stdout = state.borrow().file_system.get("/tmp/file").unwrap();
405            let stdout = stdout.borrow();
406            assert_matches!(&stdout.body, FileBody::Regular { content, .. } => {
407                assert_eq!(from_utf8(content), Ok(""));
408            });
409        });
410    }
411
412    #[test]
413    fn simple_command_performs_command_search_after_assignment() {
414        in_virtual_system(|mut env, state| async move {
415            // Start with an unset PATH
416            let mut content = Inode::default();
417            content.body = FileBody::Regular {
418                content: Vec::new(),
419                is_native_executable: true,
420            };
421            content.permissions.set(Mode::USER_EXEC, true);
422            let content = Rc::new(RefCell::new(content));
423            state
424                .borrow_mut()
425                .file_system
426                .save("/foo/bar/tool", content)
427                .unwrap();
428
429            // In the simple command, PATH is set before command search is
430            // performed, so the utility is found.
431            let command: syntax::SimpleCommand = "PATH=/usr:/foo/bar:/tmp tool".parse().unwrap();
432
433            let result = command.execute(&mut env).await;
434            assert_eq!(result, Continue(()));
435
436            let state = state.borrow();
437            let process = state.processes.values().last().unwrap();
438            let arguments = process.last_exec().as_ref().unwrap();
439            assert_eq!(&*arguments.0, c"/foo/bar/tool");
440            assert_eq!(arguments.1, [c"tool".to_owned()]);
441        })
442    }
443
444    #[test]
445    fn job_control_for_external_utility() {
446        in_virtual_system(|mut env, state| async move {
447            env.options.set(yash_env::option::Monitor, On);
448            stub_tty(&state);
449
450            let mut content = Inode::default();
451            content.body = FileBody::Regular {
452                content: Vec::new(),
453                is_native_executable: true,
454            };
455            content.permissions.set(Mode::USER_EXEC, true);
456            let content = Rc::new(RefCell::new(content));
457            state
458                .borrow_mut()
459                .file_system
460                .save("/some/file", content)
461                .unwrap();
462
463            let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
464            let _ = command.execute(&mut env).await;
465
466            let state = state.borrow();
467            let (&pid, process) = state.processes.last_key_value().unwrap();
468            assert_ne!(pid, env.main_pid);
469            assert_ne!(process.pgid(), env.main_pgid);
470        })
471    }
472
473    #[test]
474    fn xtrace_for_external_utility() {
475        in_virtual_system(|mut env, state| async move {
476            env.options.set(yash_env::option::XTrace, On);
477
478            let mut content = Inode::default();
479            content.body = FileBody::Regular {
480                content: Vec::new(),
481                is_native_executable: true,
482            };
483            content.permissions.set(Mode::USER_EXEC, true);
484            let content = Rc::new(RefCell::new(content));
485            state
486                .borrow_mut()
487                .file_system
488                .save("/some/file", content)
489                .unwrap();
490
491            let command: syntax::SimpleCommand =
492                "VAR=123 /some/file foo bar >/dev/null".parse().unwrap();
493            let _ = command.execute(&mut env).await;
494
495            assert_stderr(&state, |stderr| {
496                assert!(
497                    stderr.starts_with("VAR=123 /some/file foo bar 1>/dev/null\n"),
498                    "stderr = {stderr:?}"
499                )
500            });
501        });
502    }
503}