Skip to main content

yash_builtin/command/
identify.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Command identifying semantics
18
19use 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
40/// Result of [categorizing](categorize) a command
41///
42/// # Notes on equality
43///
44/// Although this type implements `PartialEq`, comparison between instances of
45/// this type may not always yield predictable results due to the presence of
46/// function pointers in [`Target`]. As a result, it is recommended to avoid
47/// relying on equality comparisons for values of this type. See
48/// <https://doc.rust-lang.org/std/ptr/fn.fn_addr_eq.html> for the
49/// characteristics of function pointer comparisons.
50pub enum Categorization<S> {
51    /// Shell reserved word
52    Keyword,
53    /// Alias
54    Alias(Rc<Alias>),
55    /// Target program that can be executed
56    Target(Target<S>),
57}
58
59// Not derived automatically because S may not implement Clone, PartialEq, or Debug.
60impl<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/// Error object for the command not found
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct NotFound<'a> {
114    /// Command name that was not found
115    pub name: &'a Field,
116}
117
118impl NotFound<'_> {
119    /// Converts this error to a [`Report`].
120    #[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
140/// Environment for [normalizing a target](normalize_target).
141trait 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
163/// Updates the target to make it suitable for the [description](describe_target).
164///
165/// This function makes sure any path contained in the target is absolute and
166/// names an executable file. If the path cannot be normalized, this function
167/// returns an error.
168///
169/// The error returned from this function does not contain any message because
170/// this function is used only by [`categorize`], which only need to report
171/// that the target is not found.
172fn 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
198/// Determines the category of the given command name.
199///
200/// This function requires an instance of [`IsKeyword`] to be present in the
201/// environment's [`any`](Env::any) storage to check for keywords. If no such
202/// instance is found, this function will **panic**.
203pub 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
232/// Appends the description of the given target to the result.
233///
234/// This function is a specialized helper for [`describe`]. It produces the
235/// description of the command search result that is to be printed to the
236/// standard output.
237pub 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
299/// Appends the description of the given categorization to the result.
300///
301/// This function produces the description of the command search result that is
302/// to be printed to the standard output.
303pub 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    /// Identifies the commands and returns the result.
344    ///
345    /// This function returns a string that should be printed to the standard
346    /// output, as well as a list of errors that should be printed to the
347    /// standard error.
348    ///
349    /// This function requires an instance of [`IsKeyword`] to be present in the
350    /// environment's [`any`](Env::any) storage to check for keywords. If no
351    /// such instance is found, this function will **panic**.
352    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    /// Performs the identifying semantics.
372    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}