yash_semantics/command/simple_command/
external.rs1use 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
89pub 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#[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 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 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
253 });
254 }
255
256 #[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 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 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}