Skip to main content

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::Runtime;
22use crate::command::search::search_path;
23use crate::redir::RedirGuard;
24use crate::xtrace::XTrace;
25use crate::xtrace::print;
26use crate::xtrace::trace_fields;
27use std::ffi::CString;
28use std::ops::ControlFlow::Continue;
29use yash_env::Env;
30use yash_env::io::print_error;
31use yash_env::io::print_report;
32use yash_env::semantics::ExitStatus;
33use yash_env::semantics::Field;
34use yash_env::semantics::Result;
35use yash_env::semantics::command::ReplaceCurrentProcessError;
36use yash_env::semantics::command::run_external_utility_in_subshell;
37use yash_env::system::resource::SetRlimit;
38use yash_env::system::{
39    Close, Dup, Exec, Exit, Fcntl, Fork, GetPid, Isatty, Open, SendSignal, SetPgid, ShellPath,
40    Sigaction, Sigmask, Signals, TcSetPgrp, Wait, Write,
41};
42use yash_env::variable::Context;
43use yash_syntax::syntax::Assign;
44use yash_syntax::syntax::Redir;
45
46pub async fn execute_external_utility<S: Runtime + 'static>(
47    env: &mut Env<S>,
48    assigns: &[Assign],
49    fields: Vec<Field>,
50    redirs: &[Redir],
51) -> Result {
52    let mut xtrace = XTrace::from_options(&env.options);
53
54    let env = &mut RedirGuard::new(env);
55    if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
56        return e.handle(env).await;
57    };
58
59    let mut env = env.push_context(Context::Volatile);
60    perform_assignments(&mut env, assigns, true, xtrace.as_mut()).await?;
61
62    trace_fields(xtrace.as_mut(), &fields);
63    print(&mut env, xtrace).await;
64
65    let name = &fields[0];
66    let path = if name.value.contains('/') {
67        CString::new(&*name.value).ok()
68    } else {
69        search_path(&mut *env, &name.value)
70    };
71
72    if let Some(path) = path {
73        env.exit_status =
74            start_external_utility_in_subshell_and_wait(&mut env, path, fields).await?;
75    } else {
76        print_error(
77            &mut env,
78            format!("cannot execute external utility {:?}", name.value).into(),
79            format!("utility {:?} not found", name.value).into(),
80            &name.origin,
81        )
82        .await;
83        env.exit_status = ExitStatus::NOT_FOUND;
84    }
85
86    Continue(())
87}
88
89/// Starts an external utility in a subshell and waits for it to finish.
90///
91/// `path` is the path to the external utility. `fields` are the command line
92/// words of the utility. The first field must exist and be the name of the
93/// utility as it is used for error messages.
94///
95/// This function starts the utility in a subshell and waits for it to finish.
96/// The subshell is a foreground job if job control is enabled.
97///
98/// This function returns the exit status of the utility. In case of an error,
99/// it prints an error message to the standard error before returning an
100/// appropriate exit status.
101pub async fn start_external_utility_in_subshell_and_wait<S>(
102    env: &mut Env<S>,
103    path: CString,
104    fields: Vec<Field>,
105) -> Result<ExitStatus>
106where
107    S: Close
108        + Dup
109        + Exec
110        + Exit
111        + Fcntl
112        + Fork
113        + GetPid
114        + Isatty
115        + Open
116        + SendSignal
117        + SetPgid
118        + SetRlimit
119        + ShellPath
120        + Sigaction
121        + Sigmask
122        + Signals
123        + TcSetPgrp
124        + Wait
125        + Write
126        + 'static,
127{
128    run_external_utility_in_subshell(
129        env,
130        path,
131        fields,
132        |env, error| Box::pin(async move { print_report(env, &(&error).into()).await }),
133        |env, ReplaceCurrentProcessError { path, errno }, location| {
134            Box::pin(async move {
135                print_error(
136                    env,
137                    format!("cannot execute external utility {:?}", path).into(),
138                    format!("{:?}: {}", path, errno).into(),
139                    &location,
140                )
141                .await;
142            })
143        },
144    )
145    .await
146}
147
148/// Converts fields to C strings.
149///
150/// # Deprecated
151///
152/// This function is deprecated because it does not handle null bytes in field
153/// values. If a field contains a null byte, the field is simply skipped, which
154/// may lead to unexpected behavior. Users are encouraged to implement their own
155/// conversion that handles null bytes appropriately.
156#[deprecated(since = "0.11.0")]
157pub fn to_c_strings(s: Vec<Field>) -> Vec<CString> {
158    s.into_iter()
159        .filter_map(|f| {
160            let bytes = f.value.into_bytes();
161            // TODO Return NulError if the field contains a null byte
162            CString::new(bytes).ok()
163        })
164        .collect()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::command::Command;
171    use assert_matches::assert_matches;
172    use futures_util::FutureExt;
173    use std::cell::RefCell;
174    use std::ops::ControlFlow::Continue;
175    use std::rc::Rc;
176    use std::str::from_utf8;
177    use yash_env::option::State::On;
178    use yash_env::system::Mode;
179    use yash_env::system::r#virtual::FileBody;
180    use yash_env::system::r#virtual::Inode;
181    use yash_env::variable::Scope;
182    use yash_env::variable::Value;
183    use yash_env_test_helper::assert_stderr;
184    use yash_env_test_helper::in_virtual_system;
185    use yash_env_test_helper::stub_tty;
186    use yash_syntax::syntax;
187
188    #[test]
189    fn simple_command_calls_execve_with_correct_arguments() {
190        in_virtual_system(|mut env, state| async move {
191            let mut content = Inode::default();
192            content.body = FileBody::Regular {
193                content: Vec::new(),
194                is_native_executable: true,
195            };
196            content.permissions.set(Mode::USER_EXEC, true);
197            let content = Rc::new(RefCell::new(content));
198            state
199                .borrow_mut()
200                .file_system
201                .save("/some/file", content)
202                .unwrap();
203
204            let mut var = env.variables.get_or_new("env", Scope::Global);
205            var.assign("scalar", None).unwrap();
206            var.export(true);
207            let mut var = env.variables.get_or_new("local", Scope::Global);
208            var.assign("ignored", None).unwrap();
209
210            let command: syntax::SimpleCommand = "var=123 /some/file foo bar".parse().unwrap();
211            let result = command.execute(&mut env).await;
212            assert_eq!(result, Continue(()));
213
214            let state = state.borrow();
215            let process = state.processes.values().last().unwrap();
216            let arguments = process.last_exec().as_ref().unwrap();
217            assert_eq!(arguments.0, c"/some/file".to_owned());
218            assert_eq!(
219                arguments.1,
220                [
221                    c"/some/file".to_owned(),
222                    c"foo".to_owned(),
223                    c"bar".to_owned()
224                ]
225            );
226            let mut envs = arguments.2.clone();
227            envs.sort();
228            assert_eq!(envs, [c"env=scalar".to_owned(), c"var=123".to_owned()]);
229        });
230    }
231
232    #[test]
233    fn simple_command_returns_exit_status_from_external_utility() {
234        in_virtual_system(|mut env, state| async move {
235            let mut content = Inode::default();
236            content.body = FileBody::Regular {
237                content: Vec::new(),
238                is_native_executable: true,
239            };
240            content.permissions.set(Mode::USER_EXEC, true);
241            let content = Rc::new(RefCell::new(content));
242            state
243                .borrow_mut()
244                .file_system
245                .save("/some/file", content)
246                .unwrap();
247
248            let command: syntax::SimpleCommand = "/some/file foo bar".parse().unwrap();
249            let result = command.execute(&mut env).await;
250            assert_eq!(result, Continue(()));
251            // In VirtualSystem, execve fails with ENOSYS.
252            assert_eq!(env.exit_status, ExitStatus::NOEXEC);
253        });
254    }
255
256    // TODO Test fall_back_on_sh
257
258    #[test]
259    fn simple_command_skips_running_external_utility_on_redirection_error() {
260        in_virtual_system(|mut env, state| async move {
261            let mut content = Inode::default();
262            content.body = FileBody::Regular {
263                content: Vec::new(),
264                is_native_executable: true,
265            };
266            content.permissions.set(Mode::USER_EXEC, true);
267            let content = Rc::new(RefCell::new(content));
268            state
269                .borrow_mut()
270                .file_system
271                .save("/some/file", content)
272                .unwrap();
273
274            let command: syntax::SimpleCommand = "/some/file </no/such/file".parse().unwrap();
275            let result = command.execute(&mut env).await;
276            assert_eq!(result, Continue(()));
277            assert_eq!(env.exit_status, ExitStatus::ERROR);
278        });
279    }
280
281    #[test]
282    fn simple_command_returns_127_for_non_existing_file() {
283        in_virtual_system(|mut env, _state| async move {
284            let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
285            let result = command.execute(&mut env).await;
286            assert_eq!(result, Continue(()));
287            assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
288        });
289    }
290
291    #[test]
292    fn simple_command_returns_126_on_exec_failure() {
293        in_virtual_system(|mut env, state| async move {
294            let mut content = Inode::default();
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".parse().unwrap();
304            let result = command.execute(&mut env).await;
305            assert_eq!(result, Continue(()));
306            assert_eq!(env.exit_status, ExitStatus::NOEXEC);
307        });
308    }
309
310    #[test]
311    fn simple_command_returns_126_on_fork_failure() {
312        let mut env = Env::new_virtual();
313        let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
314        let result = command.execute(&mut env).now_or_never().unwrap();
315        assert_eq!(result, Continue(()));
316        assert_eq!(env.exit_status, ExitStatus::NOEXEC);
317    }
318
319    #[test]
320    fn exit_status_is_127_on_command_not_found() {
321        let mut env = Env::new_virtual();
322        let command: syntax::SimpleCommand = "no_such_command".parse().unwrap();
323        let result = command.execute(&mut env).now_or_never().unwrap();
324        assert_eq!(result, Continue(()));
325        assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
326    }
327
328    #[test]
329    fn simple_command_assigns_variables_in_volatile_context_for_external_utility() {
330        in_virtual_system(|mut env, _state| async move {
331            let command: syntax::SimpleCommand = "a=123 /foo/bar".parse().unwrap();
332            _ = command.execute(&mut env).await;
333            assert_eq!(env.variables.get("a"), None);
334        });
335    }
336
337    #[test]
338    fn simple_command_performs_redirections_and_assignments_for_target_not_found() {
339        in_virtual_system(|mut env, state| async move {
340            let command: syntax::SimpleCommand =
341                "foo=${bar=baz} no_such_utility >/tmp/file".parse().unwrap();
342            _ = command.execute(&mut env).await;
343            assert_eq!(env.variables.get("foo"), None);
344            assert_eq!(
345                env.variables.get("bar").unwrap().value,
346                Some(Value::scalar("baz"))
347            );
348
349            let stdout = state.borrow().file_system.get("/tmp/file").unwrap();
350            let stdout = stdout.borrow();
351            assert_matches!(&stdout.body, FileBody::Regular { content, .. } => {
352                assert_eq!(from_utf8(content), Ok(""));
353            });
354        });
355    }
356
357    #[test]
358    fn simple_command_performs_command_search_after_assignment() {
359        in_virtual_system(|mut env, state| async move {
360            // Start with an unset PATH
361            let mut content = Inode::default();
362            content.body = FileBody::Regular {
363                content: Vec::new(),
364                is_native_executable: true,
365            };
366            content.permissions.set(Mode::USER_EXEC, true);
367            let content = Rc::new(RefCell::new(content));
368            state
369                .borrow_mut()
370                .file_system
371                .save("/foo/bar/tool", content)
372                .unwrap();
373
374            // In the simple command, PATH is set before command search is
375            // performed, so the utility is found.
376            let command: syntax::SimpleCommand = "PATH=/usr:/foo/bar:/tmp tool".parse().unwrap();
377
378            let result = command.execute(&mut env).await;
379            assert_eq!(result, Continue(()));
380
381            let state = state.borrow();
382            let process = state.processes.values().last().unwrap();
383            let arguments = process.last_exec().as_ref().unwrap();
384            assert_eq!(&*arguments.0, c"/foo/bar/tool");
385            assert_eq!(arguments.1, [c"tool".to_owned()]);
386        })
387    }
388
389    #[test]
390    fn job_control_for_external_utility() {
391        in_virtual_system(|mut env, state| async move {
392            env.options.set(yash_env::option::Monitor, On);
393            stub_tty(&state);
394
395            let mut content = Inode::default();
396            content.body = FileBody::Regular {
397                content: Vec::new(),
398                is_native_executable: true,
399            };
400            content.permissions.set(Mode::USER_EXEC, true);
401            let content = Rc::new(RefCell::new(content));
402            state
403                .borrow_mut()
404                .file_system
405                .save("/some/file", content)
406                .unwrap();
407
408            let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
409            let _ = command.execute(&mut env).await;
410
411            let state = state.borrow();
412            let (&pid, process) = state.processes.last_key_value().unwrap();
413            assert_ne!(pid, env.main_pid);
414            assert_ne!(process.pgid(), env.main_pgid);
415        })
416    }
417
418    #[test]
419    fn xtrace_for_external_utility() {
420        in_virtual_system(|mut env, state| async move {
421            env.options.set(yash_env::option::XTrace, On);
422
423            let mut content = Inode::default();
424            content.body = FileBody::Regular {
425                content: Vec::new(),
426                is_native_executable: true,
427            };
428            content.permissions.set(Mode::USER_EXEC, true);
429            let content = Rc::new(RefCell::new(content));
430            state
431                .borrow_mut()
432                .file_system
433                .save("/some/file", content)
434                .unwrap();
435
436            let command: syntax::SimpleCommand =
437                "VAR=123 /some/file foo bar >/dev/null".parse().unwrap();
438            let _ = command.execute(&mut env).await;
439
440            assert_stderr(&state, |stderr| {
441                assert!(
442                    stderr.starts_with("VAR=123 /some/file foo bar 1>/dev/null\n"),
443                    "stderr = {stderr:?}"
444                )
445            });
446        });
447    }
448}