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