1use super::Identify;
20use super::search::SearchEnv;
21use crate::command::Category;
22use crate::common::output;
23use crate::common::report::{merge_reports, report_failure};
24use std::ffi::CStr;
25use std::ffi::CString;
26use std::rc::Rc;
27use yash_env::Env;
28use yash_env::alias::Alias;
29use yash_env::builtin::{Builtin, Type};
30use yash_env::parser::IsKeyword;
31use yash_env::path::PathBuf;
32use yash_env::semantics::ExitStatus;
33use yash_env::semantics::Field;
34use yash_env::semantics::command::search::{Target, search};
35use yash_env::source::pretty::{Report, ReportType, Snippet};
36use yash_env::str::UnixStr;
37use yash_env::system::{Fcntl, Fstat, GetCwd, IsExecutableFile, Isatty, Sysconf, Write};
38use yash_quote::quoted;
39
40pub enum Categorization<S> {
51 Keyword,
53 Alias(Rc<Alias>),
55 Target(Target<S>),
57}
58
59impl<S> Clone for Categorization<S> {
61 fn clone(&self) -> Self {
62 match self {
63 Self::Keyword => Self::Keyword,
64 Self::Alias(alias) => Self::Alias(alias.clone()),
65 Self::Target(target) => Self::Target(target.clone()),
66 }
67 }
68}
69
70impl<S> PartialEq for Categorization<S> {
71 fn eq(&self, other: &Self) -> bool {
72 match (self, other) {
73 (Self::Keyword, Self::Keyword) => true,
74 (Self::Alias(l), Self::Alias(r)) => l == r,
75 (Self::Target(l), Self::Target(r)) => l == r,
76 _ => false,
77 }
78 }
79}
80
81impl<S> Eq for Categorization<S> {}
82
83impl<S> std::fmt::Debug for Categorization<S> {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::Keyword => write!(f, "Keyword"),
87 Self::Alias(alias) => f.debug_tuple("Alias").field(alias).finish(),
88 Self::Target(target) => f.debug_tuple("Target").field(target).finish(),
89 }
90 }
91}
92
93impl<S> From<Rc<Alias>> for Categorization<S> {
94 fn from(alias: Rc<Alias>) -> Self {
95 Self::Alias(alias)
96 }
97}
98
99impl<S> From<&Rc<Alias>> for Categorization<S> {
100 fn from(alias: &Rc<Alias>) -> Self {
101 Self::Alias(Rc::clone(alias))
102 }
103}
104
105impl<S> From<Target<S>> for Categorization<S> {
106 fn from(target: Target<S>) -> Self {
107 Self::Target(target)
108 }
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct NotFound<'a> {
114 pub name: &'a Field,
116}
117
118impl NotFound<'_> {
119 #[must_use]
121 pub fn to_report(&self) -> Report<'_> {
122 let mut report = Report::new();
123 report.r#type = ReportType::Error;
124 report.title = "command not found".into();
125 report.snippets = Snippet::with_primary_span(
126 &self.name.origin,
127 format!("{}: not found", self.name.value).into(),
128 );
129 report
130 }
131}
132
133impl<'a> From<&'a NotFound<'a>> for Report<'a> {
134 #[inline]
135 fn from(error: &'a NotFound<'a>) -> Self {
136 error.to_report()
137 }
138}
139
140trait NormalizeEnv {
142 fn is_executable_file(&self, path: &CStr) -> bool;
143 fn pwd(&self) -> Result<PathBuf, ()>;
144}
145
146impl<S> NormalizeEnv for Env<S>
147where
148 S: Fstat + GetCwd + IsExecutableFile,
149{
150 #[inline]
151 fn is_executable_file(&self, path: &CStr) -> bool {
152 self.system.is_executable_file(path)
153 }
154
155 fn pwd(&self) -> Result<PathBuf, ()> {
156 match self.get_pwd_if_correct() {
157 Some(pwd) => Ok(pwd.into()),
158 None => self.system.getcwd().map_err(|_| ()),
159 }
160 }
161}
162
163fn normalize_target<E: NormalizeEnv, S>(env: &E, target: &mut Target<S>) -> Result<(), ()> {
173 match target {
174 Target::External { path }
175 | Target::Builtin {
176 builtin:
177 Builtin {
178 r#type: Type::Substitutive,
179 ..
180 },
181 path,
182 } => {
183 if !env.is_executable_file(path) {
184 return Err(());
185 }
186 if !path.as_bytes().starts_with(b"/") {
187 let mut absolute_path = env.pwd()?;
188 absolute_path.push(UnixStr::from_bytes(path.as_bytes()));
189 *path = CString::new(absolute_path.into_unix_string().into_vec()).map_err(drop)?;
190 }
191 Ok(())
192 }
193
194 Target::Function(_) | Target::Builtin { .. } => Ok(()),
195 }
196}
197
198pub fn categorize<'f, S>(
204 name: &'f Field,
205 env: &mut SearchEnv<S>,
206) -> Result<Categorization<S>, NotFound<'f>>
207where
208 S: Fstat + GetCwd + IsExecutableFile + Sysconf + 'static,
209{
210 if env.params.categories.contains(Category::Keyword) {
211 let IsKeyword(is_keyword) = env
212 .env
213 .any
214 .get()
215 .expect("`IsKeyword` should be in `env.any`");
216 if is_keyword(env.env, &name.value) {
217 return Ok(Categorization::Keyword);
218 }
219 }
220
221 if env.params.categories.contains(Category::Alias) {
222 if let Some(alias) = env.env.aliases.get(name.value.as_str()) {
223 return Ok((&alias.0).into());
224 }
225 }
226
227 let mut target = search(env, &name.value).ok_or(NotFound { name })?;
228 normalize_target(env.env, &mut target).map_err(|()| NotFound { name })?;
229 Ok(target.into())
230}
231
232pub fn describe_target<S, W>(
238 target: &Target<S>,
239 name: &Field,
240 verbose: bool,
241 result: &mut W,
242) -> std::fmt::Result
243where
244 W: std::fmt::Write,
245{
246 match target {
247 Target::Builtin { builtin, path } => {
248 let path = path.to_string_lossy();
249 if verbose {
250 let desc = match builtin.r#type {
251 Type::Special => "special built-in",
252 Type::Mandatory => "mandatory built-in",
253 Type::Elective => "elective built-in",
254 Type::Extension => "extension built-in",
255 Type::Substitutive => "substitutive built-in",
256 };
257 write!(result, "{}: {}", name.value, desc)?;
258 if !path.is_empty() {
259 write!(result, " at {}", quoted(&path))?;
260 }
261 writeln!(result)?;
262 } else {
263 let output = if path.is_empty() {
264 &*name.value
265 } else {
266 &*path
267 };
268 writeln!(result, "{output}")?;
269 }
270 Ok(())
271 }
272
273 Target::Function(_) => {
274 if verbose {
275 writeln!(result, "{}: function", name.value)?;
276 } else {
277 writeln!(result, "{}", name.value)?;
278 }
279 Ok(())
280 }
281
282 Target::External { path } => {
283 let path = path.to_string_lossy();
284 if verbose {
285 writeln!(
286 result,
287 "{}: external utility at {}",
288 name.value,
289 quoted(&path)
290 )?;
291 } else {
292 writeln!(result, "{path}")?;
293 }
294 Ok(())
295 }
296 }
297}
298
299pub fn describe<S, W>(
304 categorization: &Categorization<S>,
305 name: &Field,
306 verbose: bool,
307 result: &mut W,
308) -> std::fmt::Result
309where
310 W: std::fmt::Write,
311{
312 match categorization {
313 Categorization::Keyword => {
314 if verbose {
315 writeln!(result, "{}: keyword", name.value)
316 } else {
317 writeln!(result, "{}", name.value)
318 }
319 }
320
321 Categorization::Alias(alias) => {
322 if verbose {
323 writeln!(result, "{}: alias for `{}`", alias.name, alias.replacement)
324 } else {
325 write!(result, "alias ")?;
326 if alias.name.starts_with('-') {
327 write!(result, "-- ")?;
328 }
329 writeln!(
330 result,
331 "{}={}",
332 quoted(&alias.name),
333 quoted(&alias.replacement)
334 )
335 }
336 }
337
338 Categorization::Target(target) => describe_target(target, name, verbose, result),
339 }
340}
341
342impl Identify {
343 pub fn result<S>(&self, env: &mut Env<S>) -> (String, Vec<NotFound<'_>>)
353 where
354 S: Fstat + GetCwd + IsExecutableFile + Sysconf + 'static,
355 {
356 let params = &self.search;
357 let env = &mut SearchEnv { env, params };
358 let mut result = String::new();
359 let mut errors = Vec::new();
360 for name in &self.names {
361 match categorize(name, env) {
362 Ok(categorization) => {
363 describe(&categorization, name, self.verbose, &mut result).unwrap()
364 }
365 Err(error) => errors.push(error),
366 }
367 }
368 (result, errors)
369 }
370
371 pub async fn execute<S>(&self, env: &mut Env<S>) -> crate::Result
373 where
374 S: Fcntl + Fstat + GetCwd + IsExecutableFile + Isatty + Sysconf + Write + 'static,
375 {
376 let (result, errors) = self.result(env);
377
378 let output_result = output(env, &result).await;
379
380 let error_result = if let Some(report) = merge_reports(&errors) {
381 if self.verbose {
382 report_failure(env, report).await
383 } else {
384 crate::Result::from(ExitStatus::FAILURE)
385 }
386 } else {
387 crate::Result::default()
388 };
389
390 output_result.max(error_result)
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::command::Search;
398 use yash_env::alias::HashEntry;
399 use yash_env::builtin::Builtin;
400 use yash_env::function::Function;
401 use yash_env::source::Location;
402 use yash_env::system::r#virtual::VirtualSystem;
403 use yash_env::test_helper::function::FunctionBodyStub;
404
405 #[test]
406 fn normalize_absolute_executable() {
407 struct TestEnv;
408 impl NormalizeEnv for TestEnv {
409 fn is_executable_file(&self, _path: &CStr) -> bool {
410 true
411 }
412 fn pwd(&self) -> Result<PathBuf, ()> {
413 unreachable!()
414 }
415 }
416
417 let mut external_target = Target::<TestEnv>::External {
418 path: c"/bin/sh".to_owned(),
419 };
420 let result = normalize_target(&TestEnv, &mut external_target);
421 assert_eq!(result, Ok(()));
422 assert_eq!(
423 external_target,
424 Target::External {
425 path: c"/bin/sh".to_owned(),
426 }
427 );
428
429 let builtin = Builtin::<TestEnv>::new(Type::Substitutive, |_, _| unreachable!());
430 let mut builtin_target = Target::Builtin {
431 builtin,
432 path: c"/usr/bin/echo".to_owned(),
433 };
434 let result = normalize_target(&TestEnv, &mut builtin_target);
435 assert_eq!(result, Ok(()));
436 assert_eq!(
437 builtin_target,
438 Target::Builtin {
439 builtin,
440 path: c"/usr/bin/echo".to_owned(),
441 }
442 );
443 }
444
445 #[test]
446 fn normalize_relative_executable() {
447 struct TestEnv;
448 impl NormalizeEnv for TestEnv {
449 fn is_executable_file(&self, _path: &CStr) -> bool {
450 true
451 }
452 fn pwd(&self) -> Result<PathBuf, ()> {
453 Ok(PathBuf::from("/bin"))
454 }
455 }
456
457 let mut external_target = Target::<TestEnv>::External {
458 path: c"foo/sh".to_owned(),
459 };
460 let result = normalize_target(&TestEnv, &mut external_target);
461 assert_eq!(result, Ok(()));
462 assert_eq!(
463 external_target,
464 Target::External {
465 path: c"/bin/foo/sh".to_owned(),
466 }
467 );
468 }
469
470 #[test]
471 fn normalize_non_executable() {
472 struct TestEnv;
473 impl NormalizeEnv for TestEnv {
474 fn is_executable_file(&self, _path: &CStr) -> bool {
475 false
476 }
477 fn pwd(&self) -> Result<PathBuf, ()> {
478 unreachable!()
479 }
480 }
481
482 let mut external_target = Target::<TestEnv>::External {
483 path: c"/bin/sh".to_owned(),
484 };
485 let result = normalize_target(&TestEnv, &mut external_target);
486 assert_eq!(result, Err(()));
487 }
488
489 #[test]
490 fn categorize_keyword() {
491 let name = &Field::dummy("if");
492 let env = &mut Env::new_virtual();
493 env.any
494 .insert(Box::new(IsKeyword::<VirtualSystem>(|_env, word| {
495 assert_eq!(word, "if");
496 true
497 })));
498 let params = &Search::default_for_identify();
499 let env = &mut SearchEnv { env, params };
500
501 let result = categorize(name, env);
502 assert_eq!(result, Ok(Categorization::Keyword));
503 }
504
505 #[test]
506 fn categorize_non_keyword() {
507 let name = &Field::dummy("foo");
508 let env = &mut Env::new_virtual();
509 env.any
510 .insert(Box::new(IsKeyword::<VirtualSystem>(|_env, word| {
511 assert_eq!(word, "foo");
512 false
513 })));
514 let params = &Search::default_for_identify();
515 let env = &mut SearchEnv { env, params };
516
517 let result = categorize(name, env);
518 assert_eq!(result, Err(NotFound { name }));
519 }
520
521 #[test]
522 fn excluding_keyword() {
523 let name = &Field::dummy("if");
524 let env = &mut Env::new_virtual();
525 let params = &mut Search::default_for_identify();
526 params.categories.remove(Category::Keyword);
527 let env = &mut SearchEnv { env, params };
528
529 let result = categorize(name, env);
530 assert_eq!(result, Err(NotFound { name }));
531 }
532
533 #[test]
534 fn categorize_alias() {
535 let name = &Field::dummy("a");
536 let env = &mut Env::new_virtual();
537 let entry = HashEntry::new(
538 "a".to_string(),
539 "A".to_string(),
540 false,
541 Location::dummy("a"),
542 );
543 let alias = entry.0.clone();
544 env.aliases.insert(entry);
545 env.any
546 .insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
547 let params = &Search::default_for_identify();
548 let env = &mut SearchEnv { env, params };
549
550 let result = categorize(name, env);
551 assert_eq!(result, Ok(Categorization::Alias(alias)));
552 }
553
554 #[test]
555 fn categorize_non_alias() {
556 let name = &Field::dummy("a");
557 let env = &mut Env::new_virtual();
558 env.any
559 .insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
560 let params = &Search::default_for_identify();
561 let env = &mut SearchEnv { env, params };
562
563 let result = categorize(name, env);
564 assert_eq!(result, Err(NotFound { name }));
565 }
566
567 #[test]
568 fn excluding_alias() {
569 let name = &Field::dummy("a");
570 let env = &mut Env::new_virtual();
571 env.aliases.insert(HashEntry::new(
572 "a".to_string(),
573 "A".to_string(),
574 false,
575 Location::dummy("a"),
576 ));
577 env.any
578 .insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| false)));
579 let params = &mut Search::default_for_identify();
580 params.categories.remove(Category::Alias);
581 let env = &mut SearchEnv { env, params };
582
583 let result = categorize(name, env);
584 assert_eq!(result, Err(NotFound { name }));
585 }
586
587 #[test]
588 fn describe_builtin_without_path() {
589 let name = &Field::dummy(":");
590 let target = &Target::Builtin {
591 builtin: Builtin::<()>::new(Type::Special, |_, _| unreachable!()),
592 path: CString::default(),
593 };
594
595 let mut output = String::new();
596 describe_target(target, name, false, &mut output).unwrap();
597 assert_eq!(output, ":\n");
598
599 let mut output = String::new();
600 describe_target(target, name, true, &mut output).unwrap();
601 assert_eq!(output, ":: special built-in\n");
602 }
603
604 #[test]
605 fn describe_builtin_with_path() {
606 let name = &Field::dummy("echo");
607 let target = &Target::Builtin {
608 builtin: Builtin::<()>::new(Type::Substitutive, |_, _| unreachable!()),
609 path: c"/bin/echo".to_owned(),
610 };
611
612 let mut output = String::new();
613 describe_target(target, name, false, &mut output).unwrap();
614 assert_eq!(output, "/bin/echo\n");
615
616 let mut output = String::new();
617 describe_target(target, name, true, &mut output).unwrap();
618 assert_eq!(output, "echo: substitutive built-in at /bin/echo\n");
619 }
620
621 #[test]
622 fn describe_function() {
623 let name = &Field::dummy("f");
624 let location = Location::dummy("f");
625 let function = Function::<()>::new("f", FunctionBodyStub::rc_dyn(), location);
626 let target = &Target::Function(function.into());
627
628 let mut output = String::new();
629 describe_target(target, name, false, &mut output).unwrap();
630 assert_eq!(output, "f\n");
631
632 let mut output = String::new();
633 describe_target(target, name, true, &mut output).unwrap();
634 assert_eq!(output, "f: function\n");
635 }
636
637 #[test]
638 fn describe_external() {
639 let name = &Field::dummy("ls");
640 let target = &Target::<()>::External {
641 path: c"/bin/ls".to_owned(),
642 };
643
644 let mut output = String::new();
645 describe_target(target, name, false, &mut output).unwrap();
646 assert_eq!(output, "/bin/ls\n");
647
648 let mut output = String::new();
649 describe_target(target, name, true, &mut output).unwrap();
650 assert_eq!(output, "ls: external utility at /bin/ls\n");
651 }
652
653 #[test]
654 fn describe_keyword() {
655 let categorization = &Categorization::<()>::Keyword;
656 let name = &Field::dummy("if");
657
658 let mut output = String::new();
659 let result = describe(categorization, name, false, &mut output);
660 assert_eq!(result, Ok(()));
661 assert_eq!(output, "if\n");
662
663 let mut output = String::new();
664 let result = describe(categorization, name, true, &mut output);
665 assert_eq!(result, Ok(()));
666 assert_eq!(output, "if: keyword\n");
667 }
668
669 #[test]
670 fn describe_alias() {
671 let categorization = &Categorization::<()>::Alias(Rc::new(Alias {
672 name: "foo".to_string(),
673 replacement: "bar".to_string(),
674 global: false,
675 origin: Location::dummy("dummy location"),
676 }));
677 let name = &Field::dummy("foo");
678
679 let mut output = String::new();
680 let result = describe(categorization, name, false, &mut output);
681 assert_eq!(result, Ok(()));
682 assert_eq!(output, "alias foo=bar\n");
683
684 let mut output = String::new();
685 let result = describe(categorization, name, true, &mut output);
686 assert_eq!(result, Ok(()));
687 assert_eq!(output, "foo: alias for `bar`\n");
688 }
689
690 #[test]
691 fn describe_alias_starting_with_hyphen() {
692 let categorization = &Categorization::<()>::Alias(Rc::new(Alias {
693 name: "-foo".to_string(),
694 replacement: "bar".to_string(),
695 global: false,
696 origin: Location::dummy("dummy location"),
697 }));
698 let name = &Field::dummy("-foo");
699
700 let mut output = String::new();
701 let result = describe(categorization, name, false, &mut output);
702 assert_eq!(result, Ok(()));
703 assert_eq!(output, "alias -- -foo=bar\n");
704 }
705
706 #[test]
707 fn identify_result_without_error() {
708 let env = &mut Env::new_virtual();
709 env.any
710 .insert(Box::new(IsKeyword::<VirtualSystem>(|_, _| true)));
711
712 let mut identify = Identify::default();
713 let (result, errors) = identify.result(env);
714 assert_eq!(result, "");
715 assert_eq!(errors, []);
716
717 identify.names.push(Field::dummy("if"));
718 let (result, errors) = identify.result(env);
719 assert_eq!(result, "if\n");
720 assert_eq!(errors, []);
721
722 identify.verbose = true;
723 let (result, errors) = identify.result(env);
724 assert_eq!(result, "if: keyword\n");
725 assert_eq!(errors, []);
726
727 identify.names.push(Field::dummy("fi"));
728 let (result, errors) = identify.result(env);
729 assert_eq!(result, "if: keyword\nfi: keyword\n");
730 assert_eq!(errors, []);
731 }
732
733 #[test]
734 fn identify_result_with_error() {
735 let env = &mut Env::new_virtual();
736 env.any
737 .insert(Box::new(IsKeyword::<VirtualSystem>(|_, word| {
738 word == "if" || word == "fi"
739 })));
740 let identify = Identify {
741 names: Field::dummies(["if", "oops", "fi", "bar"]),
742 ..Identify::default()
743 };
744
745 let (result, errors) = identify.result(env);
746
747 assert_eq!(result, "if\nfi\n");
748 assert_eq!(
749 errors,
750 [
751 NotFound {
752 name: &Field::dummy("oops")
753 },
754 NotFound {
755 name: &Field::dummy("bar")
756 }
757 ]
758 );
759 }
760}