yash_semantics/command/simple_command/
external.rs1use 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
87pub 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
140pub 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 CString::new(bytes).ok()
147 })
148 .collect()
149}
150
151pub 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 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
198async 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 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 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 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
308 });
309 }
310
311 #[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 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 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}