yash_semantics/command/simple_command/
external.rs1use 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
95pub 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#[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 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 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
260 });
261 }
262
263 #[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 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 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}