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 futures_util::FutureExt as _;
179 use std::assert_matches;
180 use std::cell::RefCell;
181 use std::ops::ControlFlow::Continue;
182 use std::rc::Rc;
183 use yash_env::option::State::On;
184 use yash_env::system::Mode;
185 use yash_env::system::r#virtual::FileBody;
186 use yash_env::system::r#virtual::Inode;
187 use yash_env::test_helper::assert_stderr;
188 use yash_env::test_helper::in_virtual_system;
189 use yash_env::test_helper::stub_tty;
190 use yash_env::variable::Scope;
191 use yash_env::variable::Value;
192 use yash_syntax::syntax;
193
194 #[test]
195 fn simple_command_calls_execve_with_correct_arguments() {
196 in_virtual_system(|mut env, state| async move {
197 let mut content = Inode::default();
198 content.body = FileBody::Regular {
199 content: Vec::new(),
200 is_native_executable: true,
201 };
202 content.permissions.set(Mode::USER_EXEC, true);
203 let content = Rc::new(RefCell::new(content));
204 state
205 .borrow_mut()
206 .file_system
207 .save("/some/file", content)
208 .unwrap();
209
210 let mut var = env.variables.get_or_new("env", Scope::Global);
211 var.assign("scalar", None).unwrap();
212 var.export(true);
213 let mut var = env.variables.get_or_new("local", Scope::Global);
214 var.assign("ignored", None).unwrap();
215
216 let command: syntax::SimpleCommand = "var=123 /some/file foo bar".parse().unwrap();
217 let result = command.execute(&mut env).await;
218 assert_eq!(result, Continue(()));
219
220 let state = state.borrow();
221 let process = state.processes.values().last().unwrap();
222 let arguments = process.last_exec().as_ref().unwrap();
223 assert_eq!(arguments.0, c"/some/file".to_owned());
224 assert_eq!(
225 arguments.1,
226 [
227 c"/some/file".to_owned(),
228 c"foo".to_owned(),
229 c"bar".to_owned()
230 ]
231 );
232 let mut envs = arguments.2.clone();
233 envs.sort();
234 assert_eq!(envs, [c"env=scalar".to_owned(), c"var=123".to_owned()]);
235 });
236 }
237
238 #[test]
239 fn simple_command_returns_exit_status_from_external_utility() {
240 in_virtual_system(|mut env, state| async move {
241 let mut content = Inode::default();
242 content.body = FileBody::Regular {
243 content: Vec::new(),
244 is_native_executable: true,
245 };
246 content.permissions.set(Mode::USER_EXEC, true);
247 let content = Rc::new(RefCell::new(content));
248 state
249 .borrow_mut()
250 .file_system
251 .save("/some/file", content)
252 .unwrap();
253
254 let command: syntax::SimpleCommand = "/some/file foo bar".parse().unwrap();
255 let result = command.execute(&mut env).await;
256 assert_eq!(result, Continue(()));
257 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
259 });
260 }
261
262 #[test]
265 fn simple_command_skips_running_external_utility_on_redirection_error() {
266 in_virtual_system(|mut env, state| async move {
267 let mut content = Inode::default();
268 content.body = FileBody::Regular {
269 content: Vec::new(),
270 is_native_executable: true,
271 };
272 content.permissions.set(Mode::USER_EXEC, true);
273 let content = Rc::new(RefCell::new(content));
274 state
275 .borrow_mut()
276 .file_system
277 .save("/some/file", content)
278 .unwrap();
279
280 let command: syntax::SimpleCommand = "/some/file </no/such/file".parse().unwrap();
281 let result = command.execute(&mut env).await;
282 assert_eq!(result, Continue(()));
283 assert_eq!(env.exit_status, ExitStatus::ERROR);
284 });
285 }
286
287 #[test]
288 fn simple_command_returns_127_for_non_existing_file() {
289 in_virtual_system(|mut env, _state| async move {
290 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
291 let result = command.execute(&mut env).await;
292 assert_eq!(result, Continue(()));
293 assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
294 });
295 }
296
297 #[test]
298 fn simple_command_returns_126_on_exec_failure() {
299 in_virtual_system(|mut env, state| async move {
300 let mut content = Inode::default();
301 content.permissions.set(Mode::USER_EXEC, true);
302 let content = Rc::new(RefCell::new(content));
303 state
304 .borrow_mut()
305 .file_system
306 .save("/some/file", content)
307 .unwrap();
308
309 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
310 let result = command.execute(&mut env).await;
311 assert_eq!(result, Continue(()));
312 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
313 });
314 }
315
316 #[test]
317 fn simple_command_returns_126_on_fork_failure() {
318 let mut env = Env::new_virtual();
319 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
320 let result = command.execute(&mut env).now_or_never().unwrap();
321 assert_eq!(result, Continue(()));
322 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
323 }
324
325 #[test]
326 fn exit_status_is_127_on_command_not_found() {
327 let mut env = Env::new_virtual();
328 let command: syntax::SimpleCommand = "no_such_command".parse().unwrap();
329 let result = command.execute(&mut env).now_or_never().unwrap();
330 assert_eq!(result, Continue(()));
331 assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
332 }
333
334 #[test]
335 fn simple_command_assigns_variables_in_volatile_context_for_external_utility() {
336 in_virtual_system(|mut env, _state| async move {
337 let command: syntax::SimpleCommand = "a=123 /foo/bar".parse().unwrap();
338 _ = command.execute(&mut env).await;
339 assert_eq!(env.variables.get("a"), None);
340 });
341 }
342
343 #[test]
344 fn simple_command_performs_redirections_and_assignments_for_target_not_found() {
345 in_virtual_system(|mut env, state| async move {
346 let command: syntax::SimpleCommand =
347 "foo=${bar=baz} no_such_utility >/tmp/file".parse().unwrap();
348 _ = command.execute(&mut env).await;
349 assert_eq!(env.variables.get("foo"), None);
350 assert_eq!(
351 env.variables.get("bar").unwrap().value,
352 Some(Value::scalar("baz"))
353 );
354
355 let stdout = state.borrow().file_system.get("/tmp/file").unwrap();
356 let stdout = stdout.borrow();
357 assert_matches!(&stdout.body, FileBody::Regular { content, .. } if content.is_empty());
358 });
359 }
360
361 #[test]
362 fn simple_command_performs_command_search_after_assignment() {
363 in_virtual_system(|mut env, state| async move {
364 let mut content = Inode::default();
366 content.body = FileBody::Regular {
367 content: Vec::new(),
368 is_native_executable: true,
369 };
370 content.permissions.set(Mode::USER_EXEC, true);
371 let content = Rc::new(RefCell::new(content));
372 state
373 .borrow_mut()
374 .file_system
375 .save("/foo/bar/tool", content)
376 .unwrap();
377
378 let command: syntax::SimpleCommand = "PATH=/usr:/foo/bar:/tmp tool".parse().unwrap();
381
382 let result = command.execute(&mut env).await;
383 assert_eq!(result, Continue(()));
384
385 let state = state.borrow();
386 let process = state.processes.values().last().unwrap();
387 let arguments = process.last_exec().as_ref().unwrap();
388 assert_eq!(&*arguments.0, c"/foo/bar/tool");
389 assert_eq!(arguments.1, [c"tool".to_owned()]);
390 })
391 }
392
393 #[test]
394 fn job_control_for_external_utility() {
395 in_virtual_system(|mut env, state| async move {
396 env.options.set(yash_env::option::Monitor, On);
397 stub_tty(&state);
398
399 let mut content = Inode::default();
400 content.body = FileBody::Regular {
401 content: Vec::new(),
402 is_native_executable: true,
403 };
404 content.permissions.set(Mode::USER_EXEC, true);
405 let content = Rc::new(RefCell::new(content));
406 state
407 .borrow_mut()
408 .file_system
409 .save("/some/file", content)
410 .unwrap();
411
412 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
413 let _ = command.execute(&mut env).await;
414
415 let state = state.borrow();
416 let (&pid, process) = state.processes.last_key_value().unwrap();
417 assert_ne!(pid, env.main_pid);
418 assert_ne!(process.pgid(), env.main_pgid);
419 })
420 }
421
422 #[test]
423 fn xtrace_for_external_utility() {
424 in_virtual_system(|mut env, state| async move {
425 env.options.set(yash_env::option::XTrace, On);
426
427 let mut content = Inode::default();
428 content.body = FileBody::Regular {
429 content: Vec::new(),
430 is_native_executable: true,
431 };
432 content.permissions.set(Mode::USER_EXEC, true);
433 let content = Rc::new(RefCell::new(content));
434 state
435 .borrow_mut()
436 .file_system
437 .save("/some/file", content)
438 .unwrap();
439
440 let command: syntax::SimpleCommand =
441 "VAR=123 /some/file foo bar >/dev/null".parse().unwrap();
442 let _ = command.execute(&mut env).await;
443
444 assert_stderr(&state, |stderr| {
445 assert!(
446 stderr.starts_with("VAR=123 /some/file foo bar 1>/dev/null\n"),
447 "stderr = {stderr:?}"
448 )
449 });
450 });
451 }
452}