use crate::common::report::report_error;
use crate::common::report::report_failure;
use crate::common::syntax::Mode;
use crate::common::syntax::parse_arguments;
use std::ffi::CString;
use std::ops::ControlFlow::Break;
use yash_env::Env;
use yash_env::builtin::Result;
use yash_env::semantics::command::search::search_path;
use yash_env::semantics::command::{ReplaceCurrentProcessError, replace_current_process};
use yash_env::semantics::{Divert::Abort, ExitStatus, Field};
use yash_env::source::Location;
use yash_env::source::pretty::{Report, ReportType, Snippet};
pub async fn main(env: &mut Env, args: Vec<Field>) -> Result {
let args = match parse_arguments(&[], Mode::with_env(env), args) {
Ok((_options, operands)) => operands,
Err(error) => return report_error(env, &error).await,
};
let mut result = Result::default();
result.retain_redirs();
if let Some(name) = args.first() {
if !env.is_interactive() {
result.set_divert(Break(Abort(None)));
}
let path = if name.value.contains('/') {
CString::new(name.value.clone()).ok()
} else {
search_path(env, name.value.as_str())
};
if let Some(path) = path {
let location = name.origin.clone();
let Err(e) = replace_current_process(env, path, args).await;
let report = ExecFailure { inner: e, location };
let _ = report_failure(env, &report).await;
result.set_exit_status(env.exit_status);
} else {
let _ = report_failure(env, NotFound(name)).await;
result.set_exit_status(ExitStatus::NOT_FOUND);
}
}
result
}
#[derive(Debug)]
pub(crate) struct ExecFailure {
pub inner: ReplaceCurrentProcessError,
pub location: Location,
}
impl<'a> From<&'a ExecFailure> for Report<'a> {
fn from(value: &'a ExecFailure) -> Self {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = format!("cannot execute external utility {:?}", value.inner.path).into();
report.snippets = Snippet::with_primary_span(
&value.location,
format!("{:?}: {}", value.inner.path, value.inner.errno).into(),
);
report
}
}
#[derive(Debug)]
struct NotFound<'a>(&'a Field);
impl<'a> From<NotFound<'a>> for Report<'a> {
fn from(value: NotFound<'a>) -> Self {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = format!("cannot execute external utility {:?}", value.0.value).into();
report.snippets = Snippet::with_primary_span(
&value.0.origin,
format!("utility {:?} not found", value.0.value).into(),
);
report
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::FutureExt;
use std::cell::RefCell;
use std::ops::ControlFlow::Continue;
use std::rc::Rc;
use yash_env::VirtualSystem;
use yash_env::option::Option::Interactive;
use yash_env::option::State::On;
use yash_env::system::Mode;
use yash_env::system::r#virtual::{FileBody, Inode};
use yash_env::variable::{PATH, Scope};
fn dummy_file(is_native_executable: bool) -> Inode {
let mut content = Inode::default();
content.body = FileBody::Regular {
content: Vec::new(),
is_native_executable,
};
content.permissions.set(Mode::USER_EXEC, true);
content
}
fn executable_file() -> Inode {
dummy_file( true)
}
fn non_executable_file() -> Inode {
dummy_file( false)
}
#[test]
fn retains_redirs_without_args() {
let mut env = Env::new_virtual();
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result.exit_status(), ExitStatus::SUCCESS);
assert!(result.should_retain_redirs());
}
#[test]
fn executes_external_utility_when_given_operand() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(executable_file())))
.unwrap();
let path = &mut env.variables.get_or_new(PATH, Scope::Global);
path.assign("/bin", None).unwrap();
path.export(true);
let args = Field::dummies(["echo"]);
main(&mut env, args).now_or_never();
let process = &system.current_process();
let arguments = process.last_exec().as_ref().unwrap();
assert_eq!(arguments.0, c"/bin/echo".to_owned());
assert_eq!(arguments.1, [c"echo".to_owned()]);
assert_eq!(arguments.2, [c"PATH=/bin".to_owned()]);
}
#[test]
fn accepts_double_hyphen_separator() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(executable_file())))
.unwrap();
let args = Field::dummies(["--", "/bin/echo"]);
main(&mut env, args).now_or_never();
let process = &system.current_process();
let arguments = process.last_exec().as_ref().unwrap();
assert_eq!(arguments.0, c"/bin/echo".to_owned());
assert_eq!(arguments.1, [c"/bin/echo".to_owned()]);
}
#[test]
fn passing_argument_to_external_utility() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
system
.state
.borrow_mut()
.file_system
.save("/usr/bin/ls", Rc::new(RefCell::new(executable_file())))
.unwrap();
let path = &mut env.variables.get_or_new(PATH, Scope::Global);
path.assign("/usr/bin", None).unwrap();
path.export(true);
let args = Field::dummies(["ls", "-l"]);
main(&mut env, args).now_or_never();
let process = &system.current_process();
let arguments = process.last_exec().as_ref().unwrap();
assert_eq!(arguments.0, c"/usr/bin/ls".to_owned());
assert_eq!(arguments.1, [c"ls".to_owned(), c"-l".to_owned()]);
assert_eq!(arguments.2, [c"PATH=/usr/bin".to_owned()]);
}
#[test]
fn utility_name_with_slash() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(executable_file())))
.unwrap();
let args = Field::dummies(["/bin/echo"]);
main(&mut env, args).now_or_never();
let process = &system.current_process();
let arguments = process.last_exec().as_ref().unwrap();
assert_eq!(arguments.0, c"/bin/echo".to_owned());
assert_eq!(arguments.1, [c"/bin/echo".to_owned()]);
assert_eq!(arguments.2, [] as [CString; 0]);
}
#[test]
fn utility_not_found() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(executable_file())))
.unwrap();
let args = Field::dummies(["echo"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result.exit_status(), ExitStatus::NOT_FOUND);
assert_eq!(result.divert(), Break(Abort(None)));
let process = &system.current_process();
assert_eq!(process.last_exec(), &None);
}
#[test]
fn utility_not_executable() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
let content = non_executable_file();
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(content)))
.unwrap();
let args = Field::dummies(["/bin/echo"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result.exit_status(), ExitStatus::NOEXEC);
assert_eq!(result.divert(), Break(Abort(None)));
}
#[test]
fn utility_not_executable_interactive_no_abort() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
env.options.set(Interactive, On);
let content = non_executable_file();
system
.state
.borrow_mut()
.file_system
.save("/bin/echo", Rc::new(RefCell::new(content)))
.unwrap();
let args = Field::dummies(["/bin/echo"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result.exit_status(), ExitStatus::NOEXEC);
assert_eq!(result.divert(), Continue(()));
}
}