yash_semantics/command/simple_command/
external.rs1use 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
81pub 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
134pub 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 CString::new(bytes).ok()
141 })
142 .collect()
143}
144
145pub 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 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
192async 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 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 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 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
302 });
303 }
304
305 #[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}