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