yash_semantics/command/simple_command/
external.rs1use super::perform_assignments;
20use crate::Handle;
21use crate::command::search::search_path;
22use crate::redir::RedirGuard;
23use crate::xtrace::XTrace;
24use crate::xtrace::print;
25use crate::xtrace::trace_fields;
26use std::ffi::CString;
27use std::ops::ControlFlow::Continue;
28use yash_env::Env;
29use yash_env::io::print_error;
30use yash_env::io::print_report;
31use yash_env::semantics::ExitStatus;
32use yash_env::semantics::Field;
33use yash_env::semantics::Result;
34use yash_env::semantics::command::ReplaceCurrentProcessError;
35use yash_env::semantics::command::run_external_utility_in_subshell;
36use yash_env::variable::Context;
37use yash_syntax::syntax::Assign;
38use yash_syntax::syntax::Redir;
39
40pub async fn execute_external_utility(
41 env: &mut Env,
42 assigns: &[Assign],
43 fields: Vec<Field>,
44 redirs: &[Redir],
45) -> Result {
46 let mut xtrace = XTrace::from_options(&env.options);
47
48 let env = &mut RedirGuard::new(env);
49 if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
50 return e.handle(env).await;
51 };
52
53 let mut env = env.push_context(Context::Volatile);
54 perform_assignments(&mut env, assigns, true, xtrace.as_mut()).await?;
55
56 trace_fields(xtrace.as_mut(), &fields);
57 print(&mut env, xtrace).await;
58
59 let name = &fields[0];
60 let path = if name.value.contains('/') {
61 CString::new(&*name.value).ok()
62 } else {
63 search_path(&mut *env, &name.value)
64 };
65
66 if let Some(path) = path {
67 env.exit_status =
68 start_external_utility_in_subshell_and_wait(&mut env, path, fields).await?;
69 } else {
70 print_error(
71 &mut env,
72 format!("cannot execute external utility {:?}", name.value).into(),
73 format!("utility {:?} not found", name.value).into(),
74 &name.origin,
75 )
76 .await;
77 env.exit_status = ExitStatus::NOT_FOUND;
78 }
79
80 Continue(())
81}
82
83pub async fn start_external_utility_in_subshell_and_wait(
96 env: &mut Env,
97 path: CString,
98 fields: Vec<Field>,
99) -> Result<ExitStatus> {
100 run_external_utility_in_subshell(
101 env,
102 path,
103 fields,
104 |env, error| Box::pin(async move { print_report(env, &(&error).into()).await }),
105 |env, ReplaceCurrentProcessError { path, errno }, location| {
106 Box::pin(async move {
107 print_error(
108 env,
109 format!("cannot execute external utility {:?}", path).into(),
110 format!("{:?}: {}", path, errno).into(),
111 &location,
112 )
113 .await;
114 })
115 },
116 )
117 .await
118}
119
120#[deprecated(since = "0.11.0")]
129pub fn to_c_strings(s: Vec<Field>) -> Vec<CString> {
130 s.into_iter()
131 .filter_map(|f| {
132 let bytes = f.value.into_bytes();
133 CString::new(bytes).ok()
135 })
136 .collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::command::Command;
143 use assert_matches::assert_matches;
144 use futures_util::FutureExt;
145 use std::cell::RefCell;
146 use std::ops::ControlFlow::Continue;
147 use std::rc::Rc;
148 use std::str::from_utf8;
149 use yash_env::option::State::On;
150 use yash_env::system::Mode;
151 use yash_env::system::r#virtual::FileBody;
152 use yash_env::system::r#virtual::Inode;
153 use yash_env::variable::Scope;
154 use yash_env::variable::Value;
155 use yash_env_test_helper::assert_stderr;
156 use yash_env_test_helper::in_virtual_system;
157 use yash_env_test_helper::stub_tty;
158 use yash_syntax::syntax;
159
160 #[test]
161 fn simple_command_calls_execve_with_correct_arguments() {
162 in_virtual_system(|mut env, state| async move {
163 let mut content = Inode::default();
164 content.body = FileBody::Regular {
165 content: Vec::new(),
166 is_native_executable: true,
167 };
168 content.permissions.set(Mode::USER_EXEC, true);
169 let content = Rc::new(RefCell::new(content));
170 state
171 .borrow_mut()
172 .file_system
173 .save("/some/file", content)
174 .unwrap();
175
176 let mut var = env.variables.get_or_new("env", Scope::Global);
177 var.assign("scalar", None).unwrap();
178 var.export(true);
179 let mut var = env.variables.get_or_new("local", Scope::Global);
180 var.assign("ignored", None).unwrap();
181
182 let command: syntax::SimpleCommand = "var=123 /some/file foo bar".parse().unwrap();
183 let result = command.execute(&mut env).await;
184 assert_eq!(result, Continue(()));
185
186 let state = state.borrow();
187 let process = state.processes.values().last().unwrap();
188 let arguments = process.last_exec().as_ref().unwrap();
189 assert_eq!(arguments.0, c"/some/file".to_owned());
190 assert_eq!(
191 arguments.1,
192 [
193 c"/some/file".to_owned(),
194 c"foo".to_owned(),
195 c"bar".to_owned()
196 ]
197 );
198 let mut envs = arguments.2.clone();
199 envs.sort();
200 assert_eq!(envs, [c"env=scalar".to_owned(), c"var=123".to_owned()]);
201 });
202 }
203
204 #[test]
205 fn simple_command_returns_exit_status_from_external_utility() {
206 in_virtual_system(|mut env, state| async move {
207 let mut content = Inode::default();
208 content.body = FileBody::Regular {
209 content: Vec::new(),
210 is_native_executable: true,
211 };
212 content.permissions.set(Mode::USER_EXEC, true);
213 let content = Rc::new(RefCell::new(content));
214 state
215 .borrow_mut()
216 .file_system
217 .save("/some/file", content)
218 .unwrap();
219
220 let command: syntax::SimpleCommand = "/some/file foo bar".parse().unwrap();
221 let result = command.execute(&mut env).await;
222 assert_eq!(result, Continue(()));
223 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
225 });
226 }
227
228 #[test]
231 fn simple_command_skips_running_external_utility_on_redirection_error() {
232 in_virtual_system(|mut env, state| async move {
233 let mut content = Inode::default();
234 content.body = FileBody::Regular {
235 content: Vec::new(),
236 is_native_executable: true,
237 };
238 content.permissions.set(Mode::USER_EXEC, true);
239 let content = Rc::new(RefCell::new(content));
240 state
241 .borrow_mut()
242 .file_system
243 .save("/some/file", content)
244 .unwrap();
245
246 let command: syntax::SimpleCommand = "/some/file </no/such/file".parse().unwrap();
247 let result = command.execute(&mut env).await;
248 assert_eq!(result, Continue(()));
249 assert_eq!(env.exit_status, ExitStatus::ERROR);
250 });
251 }
252
253 #[test]
254 fn simple_command_returns_127_for_non_existing_file() {
255 in_virtual_system(|mut env, _state| async move {
256 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
257 let result = command.execute(&mut env).await;
258 assert_eq!(result, Continue(()));
259 assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
260 });
261 }
262
263 #[test]
264 fn simple_command_returns_126_on_exec_failure() {
265 in_virtual_system(|mut env, state| async move {
266 let mut content = Inode::default();
267 content.permissions.set(Mode::USER_EXEC, true);
268 let content = Rc::new(RefCell::new(content));
269 state
270 .borrow_mut()
271 .file_system
272 .save("/some/file", content)
273 .unwrap();
274
275 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
276 let result = command.execute(&mut env).await;
277 assert_eq!(result, Continue(()));
278 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
279 });
280 }
281
282 #[test]
283 fn simple_command_returns_126_on_fork_failure() {
284 let mut env = Env::new_virtual();
285 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
286 let result = command.execute(&mut env).now_or_never().unwrap();
287 assert_eq!(result, Continue(()));
288 assert_eq!(env.exit_status, ExitStatus::NOEXEC);
289 }
290
291 #[test]
292 fn exit_status_is_127_on_command_not_found() {
293 let mut env = Env::new_virtual();
294 let command: syntax::SimpleCommand = "no_such_command".parse().unwrap();
295 let result = command.execute(&mut env).now_or_never().unwrap();
296 assert_eq!(result, Continue(()));
297 assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
298 }
299
300 #[test]
301 fn simple_command_assigns_variables_in_volatile_context_for_external_utility() {
302 in_virtual_system(|mut env, _state| async move {
303 let command: syntax::SimpleCommand = "a=123 /foo/bar".parse().unwrap();
304 _ = command.execute(&mut env).await;
305 assert_eq!(env.variables.get("a"), None);
306 });
307 }
308
309 #[test]
310 fn simple_command_performs_redirections_and_assignments_for_target_not_found() {
311 in_virtual_system(|mut env, state| async move {
312 let command: syntax::SimpleCommand =
313 "foo=${bar=baz} no_such_utility >/tmp/file".parse().unwrap();
314 _ = command.execute(&mut env).await;
315 assert_eq!(env.variables.get("foo"), None);
316 assert_eq!(
317 env.variables.get("bar").unwrap().value,
318 Some(Value::scalar("baz"))
319 );
320
321 let stdout = state.borrow().file_system.get("/tmp/file").unwrap();
322 let stdout = stdout.borrow();
323 assert_matches!(&stdout.body, FileBody::Regular { content, .. } => {
324 assert_eq!(from_utf8(content), Ok(""));
325 });
326 });
327 }
328
329 #[test]
330 fn simple_command_performs_command_search_after_assignment() {
331 in_virtual_system(|mut env, state| async move {
332 let mut content = Inode::default();
334 content.body = FileBody::Regular {
335 content: Vec::new(),
336 is_native_executable: true,
337 };
338 content.permissions.set(Mode::USER_EXEC, true);
339 let content = Rc::new(RefCell::new(content));
340 state
341 .borrow_mut()
342 .file_system
343 .save("/foo/bar/tool", content)
344 .unwrap();
345
346 let command: syntax::SimpleCommand = "PATH=/usr:/foo/bar:/tmp tool".parse().unwrap();
349
350 let result = command.execute(&mut env).await;
351 assert_eq!(result, Continue(()));
352
353 let state = state.borrow();
354 let process = state.processes.values().last().unwrap();
355 let arguments = process.last_exec().as_ref().unwrap();
356 assert_eq!(&*arguments.0, c"/foo/bar/tool");
357 assert_eq!(arguments.1, [c"tool".to_owned()]);
358 })
359 }
360
361 #[test]
362 fn job_control_for_external_utility() {
363 in_virtual_system(|mut env, state| async move {
364 env.options.set(yash_env::option::Monitor, On);
365 stub_tty(&state);
366
367 let mut content = Inode::default();
368 content.body = FileBody::Regular {
369 content: Vec::new(),
370 is_native_executable: true,
371 };
372 content.permissions.set(Mode::USER_EXEC, true);
373 let content = Rc::new(RefCell::new(content));
374 state
375 .borrow_mut()
376 .file_system
377 .save("/some/file", content)
378 .unwrap();
379
380 let command: syntax::SimpleCommand = "/some/file".parse().unwrap();
381 let _ = command.execute(&mut env).await;
382
383 let state = state.borrow();
384 let (&pid, process) = state.processes.last_key_value().unwrap();
385 assert_ne!(pid, env.main_pid);
386 assert_ne!(process.pgid(), env.main_pgid);
387 })
388 }
389
390 #[test]
391 fn xtrace_for_external_utility() {
392 in_virtual_system(|mut env, state| async move {
393 env.options.set(yash_env::option::XTrace, On);
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 =
409 "VAR=123 /some/file foo bar >/dev/null".parse().unwrap();
410 let _ = command.execute(&mut env).await;
411
412 assert_stderr(&state, |stderr| {
413 assert!(
414 stderr.starts_with("VAR=123 /some/file foo bar 1>/dev/null\n"),
415 "stderr = {stderr:?}"
416 )
417 });
418 });
419 }
420}