use super::Identify;
use super::search::SearchEnv;
use crate::command::Category;
use crate::common::output;
use crate::common::report::{merge_reports, report_failure};
use std::ffi::CStr;
use std::ffi::CString;
use std::rc::Rc;
use yash_env::Env;
use yash_env::alias::Alias;
use yash_env::builtin::{Builtin, Type};
use yash_env::parser::IsKeyword;
use yash_env::path::PathBuf;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::semantics::command::search::{Target, search};
use yash_env::source::pretty::{Report, ReportType, Snippet};
use yash_env::str::UnixStr;
use yash_env::system::{Fcntl, Fstat, GetCwd, IsExecutableFile, Isatty, Sysconf, Write};
use yash_quote::quoted;
pub enum Categorization<S> {
Keyword,
Alias(Rc<Alias>),
Target(Target<S>),
}
impl<S> Clone for Categorization<S> {
fn clone(&self) -> Self {
match self {
Self::Keyword => Self::Keyword,
Self::Alias(alias) => Self::Alias(alias.clone()),
Self::Target(target) => Self::Target(target.clone()),
}
}
}
impl<S> PartialEq for Categorization<S> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Keyword, Self::Keyword) => true,
(Self::Alias(l), Self::Alias(r)) => l == r,
(Self::Target(l), Self::Target(r)) => l == r,
_ => false,
}
}
}
impl<S> Eq for Categorization<S> {}
impl<S> std::fmt::Debug for Categorization<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Keyword => write!(f, "Keyword"),
Self::Alias(alias) => f.debug_tuple("Alias").field(alias).finish(),
Self::Target(target) => f.debug_tuple("Target").field(target).finish(),
}
}
}
impl<S> From<Rc<Alias>> for Categorization<S> {
fn from(alias: Rc<Alias>) -> Self {
Self::Alias(alias)
}
}
impl<S> From<&Rc<Alias>> for Categorization<S> {
fn from(alias: &Rc<Alias>) -> Self {
Self::Alias(Rc::clone(alias))
}
}
impl<S> From<Target<S>> for Categorization<S> {
fn from(target: Target<S>) -> Self {
Self::Target(target)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotFound<'a> {
pub name: &'a Field,
}
impl NotFound<'_> {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = "command not found".into();
report.snippets = Snippet::with_primary_span(
&self.name.origin,
format!("{}: not found", self.name.value).into(),
);
report
}
}
impl<'a> From<&'a NotFound<'a>> for Report<'a> {
#[inline]
fn from(error: &'a NotFound<'a>) -> Self {
error.to_report()
}
}
trait NormalizeEnv {
fn is_executable_file(&self, path: &CStr) -> bool;
fn pwd(&self) -> Result<PathBuf, ()>;
}
impl<S> NormalizeEnv for Env<S>
where
S: Fstat + GetCwd + IsExecutableFile,
{
#[inline]
fn is_executable_file(&self, path: &CStr) -> bool {
self.system.is_executable_file(path)
}
fn pwd(&self) -> Result<PathBuf, ()> {
match self.get_pwd_if_correct() {
Some(pwd) => Ok(pwd.into()),
None => self.system.getcwd().map_err(|_| ()),
}
}
}
fn normalize_target<E: NormalizeEnv, S>(env: &E, target: &mut Target<S>) -> Result<(), ()> {
match target {
Target::External { path }
| Target::Builtin {
builtin:
Builtin {
r#type: Type::Substitutive,
..
},
path,
} => {
if !env.is_executable_file(path) {
return Err(());
}
if !path.as_bytes().starts_with(b"/") {
let mut absolute_path = env.pwd()?;
absolute_path.push(UnixStr::from_bytes(path.as_bytes()));
*path = CString::new(absolute_path.into_unix_string().into_vec()).map_err(drop)?;
}
Ok(())
}
Target::Function(_) | Target::Builtin { .. } => Ok(()),
}
}
pub fn categorize<'f, S>(
name: &'f Field,
env: &mut SearchEnv<S>,
) -> Result<Categorization<S>, NotFound<'f>>
where
S: Fstat + GetCwd + IsExecutableFile + Sysconf + 'static,
{
if env.params.categories.contains(Category::Keyword) {
let IsKeyword(is_keyword) = env.env.any.get().expect("IsKeyword not found in env.any");
if is_keyword(env.env, &name.value) {
return Ok(Categorization::Keyword);
}
}
if env.params.categories.contains(Category::Alias) {
if let Some(alias) = env.env.aliases.get(name.value.as_str()) {
return Ok((&alias.0).into());
}
}
let mut target = search(env, &name.value).ok_or(NotFound { name })?;
normalize_target(env.env, &mut target).map_err(|()| NotFound { name })?;
Ok(target.into())
}
pub fn describe_target<S, W>(
target: &Target<S>,
name: &Field,
verbose: bool,
result: &mut W,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
match target {
Target::Builtin { builtin, path } => {
let path = path.to_string_lossy();
if verbose {
let desc = match builtin.r#type {
Type::Special => "special built-in",
Type::Mandatory => "mandatory built-in",
Type::Elective => "elective built-in",
Type::Extension => "extension built-in",
Type::Substitutive => "substitutive built-in",
};
write!(result, "{}: {}", name.value, desc)?;
if !path.is_empty() {
write!(result, " at {}", quoted(&path))?;
}
writeln!(result)?;
} else {
let output = if path.is_empty() {
&*name.value
} else {
&*path
};
writeln!(result, "{output}")?;
}
Ok(())
}
Target::Function(_) => {
if verbose {
writeln!(result, "{}: function", name.value)?;
} else {
writeln!(result, "{}", name.value)?;
}
Ok(())
}
Target::External { path } => {
let path = path.to_string_lossy();
if verbose {
writeln!(
result,
"{}: external utility at {}",
name.value,
quoted(&path)
)?;
} else {
writeln!(result, "{path}")?;
}
Ok(())
}
}
}
pub fn describe<S, W>(
categorization: &Categorization<S>,
name: &Field,
verbose: bool,
result: &mut W,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
match categorization {
Categorization::Keyword => {
if verbose {
writeln!(result, "{}: keyword", name.value)
} else {
writeln!(result, "{}", name.value)
}
}
Categorization::Alias(alias) => {
if verbose {
writeln!(result, "{}: alias for `{}`", alias.name, alias.replacement)
} else {
write!(result, "alias ")?;
if alias.name.starts_with('-') {
write!(result, "-- ")?;
}
writeln!(
result,
"{}={}",
quoted(&alias.name),
quoted(&alias.replacement)
)
}
}
Categorization::Target(target) => describe_target(target, name, verbose, result),
}
}
impl Identify {
pub fn result<S>(&self, env: &mut Env<S>) -> (String, Vec<NotFound<'_>>)
where
S: Fstat + GetCwd + IsExecutableFile + Sysconf + 'static,
{
let params = &self.search;
let env = &mut SearchEnv { env, params };
let mut result = String::new();
let mut errors = Vec::new();
for name in &self.names {
match categorize(name, env) {
Ok(categorization) => {
describe(&categorization, name, self.verbose, &mut result).unwrap()
}
Err(error) => errors.push(error),
}
}
(result, errors)
}
pub async fn execute<S>(&self, env: &mut Env<S>) -> crate::Result
where
S: Fcntl + Fstat + GetCwd + IsExecutableFile + Isatty + Sysconf + Write + 'static,
{
let (result, errors) = self.result(env);
let output_result = output(env, &result).await;
let error_result = if let Some(report) = merge_reports(&errors) {
if self.verbose {
report_failure(env, report).await
} else {
crate::Result::from(ExitStatus::FAILURE)
}
} else {
crate::Result::default()
};
output_result.max(error_result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Search;
use yash_env::alias::HashEntry;
use yash_env::builtin::Builtin;
use yash_env::function::Function;
use yash_env::source::Location;
use yash_env::system::r#virtual::VirtualSystem;
use yash_env_test_helper::function::FunctionBodyStub;
#[test]
fn normalize_absolute_executable() {
struct TestEnv;
impl NormalizeEnv for TestEnv {
fn is_executable_file(&self, _path: &CStr) -> bool {
true
}
fn pwd(&self) -> Result<PathBuf, ()> {
unreachable!()
}
}
let mut external_target = Target::<TestEnv>::External {
path: c"/bin/sh".to_owned(),
};
let result = normalize_target(&TestEnv, &mut external_target);
assert_eq!(result, Ok(()));
assert_eq!(
external_target,
Target::External {
path: c"/bin/sh".to_owned(),
}
);
let builtin = Builtin::<TestEnv>::new(Type::Substitutive, |_, _| unreachable!());
let mut builtin_target = Target::Builtin {
builtin,
path: c"/usr/bin/echo".to_owned(),
};
let result = normalize_target(&TestEnv, &mut builtin_target);
assert_eq!(result, Ok(()));
assert_eq!(
builtin_target,
Target::Builtin {
builtin,
path: c"/usr/bin/echo".to_owned(),
}
);
}
#[test]
fn normalize_relative_executable() {
struct TestEnv;
impl NormalizeEnv for TestEnv {
fn is_executable_file(&self, _path: &CStr) -> bool {
true
}
fn pwd(&self) -> Result<PathBuf, ()> {
Ok(PathBuf::from("/bin"))
}
}
let mut external_target = Target::<TestEnv>::External {
path: c"foo/sh".to_owned(),
};
let result = normalize_target(&TestEnv, &mut external_target);
assert_eq!(result, Ok(()));
assert_eq!(
external_target,
Target::External {
path: c"/bin/foo/sh".to_owned(),
}
);
}
#[test]
fn normalize_non_executable() {
struct TestEnv;
impl NormalizeEnv for TestEnv {
fn is_executable_file(&self, _path: &CStr) -> bool {
false
}
fn pwd(&self) -> Result<PathBuf, ()> {
unreachable!()
}
}
let mut external_target = Target::<TestEnv>::External {
path: c"/bin/sh".to_owned(),
};
let result = normalize_target(&TestEnv, &mut external_target);
assert_eq!(result, Err(()));
}
#[test]
fn categorize_keyword() {
let name = &Field::dummy("if");
let env = &mut Env::new_virtual();
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_env, word| {
assert_eq!(word, "if");
true
})));
let params = &Search::default_for_identify();
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Ok(Categorization::Keyword));
}
#[test]
fn categorize_non_keyword() {
let name = &Field::dummy("foo");
let env = &mut Env::new_virtual();
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_env, word| {
assert_eq!(word, "foo");
false
})));
let params = &Search::default_for_identify();
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Err(NotFound { name }));
}
#[test]
fn excluding_keyword() {
let name = &Field::dummy("if");
let env = &mut Env::new_virtual();
let params = &mut Search::default_for_identify();
params.categories.remove(Category::Keyword);
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Err(NotFound { name }));
}
#[test]
fn categorize_alias() {
let name = &Field::dummy("a");
let env = &mut Env::new_virtual();
let entry = HashEntry::new(
"a".to_string(),
"A".to_string(),
false,
Location::dummy("a"),
);
let alias = entry.0.clone();
env.aliases.insert(entry);
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
let params = &Search::default_for_identify();
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Ok(Categorization::Alias(alias)));
}
#[test]
fn categorize_non_alias() {
let name = &Field::dummy("a");
let env = &mut Env::new_virtual();
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
let params = &Search::default_for_identify();
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Err(NotFound { name }));
}
#[test]
fn excluding_alias() {
let name = &Field::dummy("a");
let env = &mut Env::new_virtual();
env.aliases.insert(HashEntry::new(
"a".to_string(),
"A".to_string(),
false,
Location::dummy("a"),
));
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
let params = &mut Search::default_for_identify();
params.categories.remove(Category::Alias);
let env = &mut SearchEnv { env, params };
let result = categorize(name, env);
assert_eq!(result, Err(NotFound { name }));
}
#[test]
fn describe_builtin_without_path() {
let name = &Field::dummy(":");
let target = &Target::Builtin {
builtin: Builtin::<()>::new(Type::Special, |_, _| unreachable!()),
path: CString::default(),
};
let mut output = String::new();
describe_target(target, name, false, &mut output).unwrap();
assert_eq!(output, ":\n");
let mut output = String::new();
describe_target(target, name, true, &mut output).unwrap();
assert_eq!(output, ":: special built-in\n");
}
#[test]
fn describe_builtin_with_path() {
let name = &Field::dummy("echo");
let target = &Target::Builtin {
builtin: Builtin::<()>::new(Type::Substitutive, |_, _| unreachable!()),
path: c"/bin/echo".to_owned(),
};
let mut output = String::new();
describe_target(target, name, false, &mut output).unwrap();
assert_eq!(output, "/bin/echo\n");
let mut output = String::new();
describe_target(target, name, true, &mut output).unwrap();
assert_eq!(output, "echo: substitutive built-in at /bin/echo\n");
}
#[test]
fn describe_function() {
let name = &Field::dummy("f");
let location = Location::dummy("f");
let function = Function::<()>::new("f", FunctionBodyStub::rc_dyn(), location);
let target = &Target::Function(function.into());
let mut output = String::new();
describe_target(target, name, false, &mut output).unwrap();
assert_eq!(output, "f\n");
let mut output = String::new();
describe_target(target, name, true, &mut output).unwrap();
assert_eq!(output, "f: function\n");
}
#[test]
fn describe_external() {
let name = &Field::dummy("ls");
let target = &Target::<()>::External {
path: c"/bin/ls".to_owned(),
};
let mut output = String::new();
describe_target(target, name, false, &mut output).unwrap();
assert_eq!(output, "/bin/ls\n");
let mut output = String::new();
describe_target(target, name, true, &mut output).unwrap();
assert_eq!(output, "ls: external utility at /bin/ls\n");
}
#[test]
fn describe_keyword() {
let categorization = &Categorization::<()>::Keyword;
let name = &Field::dummy("if");
let mut output = String::new();
let result = describe(categorization, name, false, &mut output);
assert_eq!(result, Ok(()));
assert_eq!(output, "if\n");
let mut output = String::new();
let result = describe(categorization, name, true, &mut output);
assert_eq!(result, Ok(()));
assert_eq!(output, "if: keyword\n");
}
#[test]
fn describe_alias() {
let categorization = &Categorization::<()>::Alias(Rc::new(Alias {
name: "foo".to_string(),
replacement: "bar".to_string(),
global: false,
origin: Location::dummy("dummy location"),
}));
let name = &Field::dummy("foo");
let mut output = String::new();
let result = describe(categorization, name, false, &mut output);
assert_eq!(result, Ok(()));
assert_eq!(output, "alias foo=bar\n");
let mut output = String::new();
let result = describe(categorization, name, true, &mut output);
assert_eq!(result, Ok(()));
assert_eq!(output, "foo: alias for `bar`\n");
}
#[test]
fn describe_alias_starting_with_hyphen() {
let categorization = &Categorization::<()>::Alias(Rc::new(Alias {
name: "-foo".to_string(),
replacement: "bar".to_string(),
global: false,
origin: Location::dummy("dummy location"),
}));
let name = &Field::dummy("-foo");
let mut output = String::new();
let result = describe(categorization, name, false, &mut output);
assert_eq!(result, Ok(()));
assert_eq!(output, "alias -- -foo=bar\n");
}
#[test]
fn identify_result_without_error() {
let env = &mut Env::new_virtual();
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| true)));
let mut identify = Identify::default();
let (result, errors) = identify.result(env);
assert_eq!(result, "");
assert_eq!(errors, []);
identify.names.push(Field::dummy("if"));
let (result, errors) = identify.result(env);
assert_eq!(result, "if\n");
assert_eq!(errors, []);
identify.verbose = true;
let (result, errors) = identify.result(env);
assert_eq!(result, "if: keyword\n");
assert_eq!(errors, []);
identify.names.push(Field::dummy("fi"));
let (result, errors) = identify.result(env);
assert_eq!(result, "if: keyword\nfi: keyword\n");
assert_eq!(errors, []);
}
#[test]
fn identify_result_with_error() {
let env = &mut Env::new_virtual();
env.any
.insert(Box::new(IsKeyword::<VirtualSystem>(|_, word| {
word == "if" || word == "fi"
})));
let identify = Identify {
names: Field::dummies(["if", "oops", "fi", "bar"]),
..Identify::default()
};
let (result, errors) = identify.result(env);
assert_eq!(result, "if\nfi\n");
assert_eq!(
errors,
[
NotFound {
name: &Field::dummy("oops")
},
NotFound {
name: &Field::dummy("bar")
}
]
);
}
}