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