yash_semantics/
command_search.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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 search
18//!
19//! The [command search](search) is part of the execution of a [simple
20//! command](yash_syntax::syntax::SimpleCommand). It determines a command target
21//! that is to be invoked. A [target](Target) can be a built-in utility,
22//! function, or external utility.
23//!
24//! If the command name contains a slash, the target is always an external
25//! utility. Otherwise, the shell searches the following candidates for the
26//! target (in the order of priority):
27//!
28//! 1. [Special] built-ins
29//! 1. Functions
30//! 1. Other built-ins
31//! 1. External utilities
32//!
33//! For a [substitutive](Substitutive) built-in or external utility to be chosen
34//! as a target, a corresponding executable file must be present in a directory
35//! specified in the `$PATH` variable.
36
37use std::ffi::CStr;
38use std::ffi::CString;
39use std::rc::Rc;
40use yash_env::Env;
41use yash_env::System;
42use yash_env::builtin::Builtin;
43use yash_env::builtin::Type::{Special, Substitutive};
44use yash_env::function::Function;
45use yash_env::path::PathBuf;
46use yash_env::variable::Expansion;
47use yash_env::variable::PATH;
48
49/// Target of a simple command execution
50///
51/// This is the result of the [command search](search).
52///
53/// # Notes on equality
54///
55/// Although this type implements `PartialEq`, comparison between instances of
56/// this type may not always yield predictable results due to the presence of
57/// function pointers in [`Builtin`]. As a result, it is recommended to avoid
58/// relying on equality comparisons for values of this type. See
59/// <https://doc.rust-lang.org/std/ptr/fn.fn_addr_eq.html> for the
60/// characteristics of function pointer comparisons.
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub enum Target {
63    /// Built-in utility
64    Builtin {
65        /// Definition of the built-in
66        builtin: Builtin,
67
68        /// Path to the external utility that is shadowed by the substitutive
69        /// built-in
70        ///
71        /// This value is only used for substitutive built-ins. For other types
72        /// of built-ins, this value is always empty.
73        ///
74        /// The path may not necessarily be absolute. If the `PATH` variable
75        /// contains a relative directory name and the external utility is found
76        /// in that directory, the path will be relative.
77        path: CString,
78    },
79
80    /// Function
81    Function(Rc<Function>),
82
83    /// External utility
84    External {
85        /// Path to the external utility
86        ///
87        /// The path may not necessarily be absolute. If the `PATH` variable
88        /// contains a relative directory name and the external utility is found
89        /// in that directory, the path will be relative.
90        ///
91        /// The path may not name an existing executable file, either. If the
92        /// command name contains a slash, the name is immediately regarded as a
93        /// path to an external utility, regardless of whether the named
94        /// external utility actually exists.
95        path: CString,
96    },
97}
98
99impl From<Rc<Function>> for Target {
100    #[inline]
101    fn from(function: Rc<Function>) -> Target {
102        Target::Function(function)
103    }
104}
105
106impl From<Function> for Target {
107    #[inline]
108    fn from(function: Function) -> Target {
109        Target::Function(function.into())
110    }
111}
112
113// impl From<CString> for Target
114// not implemented because of ambiguity between substitutive built-ins and
115// external utilities
116
117/// Collection of data used in [classifying](classify) command names
118pub trait ClassifyEnv {
119    /// Retrieves the built-in by name.
120    #[must_use]
121    fn builtin(&self, name: &str) -> Option<Builtin>;
122
123    /// Retrieves the function by name.
124    #[must_use]
125    fn function(&self, name: &str) -> Option<&Rc<Function>>;
126}
127
128/// Part of the shell execution environment command path search depends on
129pub trait PathEnv {
130    /// Accesses the `$PATH` variable in the environment.
131    ///
132    /// This function returns an `Expansion` rather than a reference to a
133    /// variable value because the path may be dynamically computed in the
134    /// function.
135    #[must_use]
136    fn path(&self) -> Expansion<'_>;
137
138    /// Whether there is an executable file at the specified path.
139    #[must_use]
140    fn is_executable_file(&self, path: &CStr) -> bool;
141    // TODO Cache the results of external utility search
142}
143
144impl PathEnv for Env {
145    /// Returns the value of the `$PATH` variable.
146    ///
147    /// This function assumes that the `$PATH` variable has no quirks. If the
148    /// variable has a quirk, the function panics.
149    fn path(&self) -> Expansion<'_> {
150        self.variables
151            .get(PATH)
152            .and_then(|var| {
153                assert_eq!(var.quirk, None, "PATH does not support quirks");
154                var.value.as_ref()
155            })
156            .into()
157    }
158
159    fn is_executable_file(&self, path: &CStr) -> bool {
160        self.system.is_executable_file(path)
161    }
162}
163
164impl ClassifyEnv for Env {
165    fn builtin(&self, name: &str) -> Option<Builtin> {
166        self.builtins.get(name).copied()
167    }
168
169    #[inline]
170    fn function(&self, name: &str) -> Option<&Rc<Function>> {
171        self.functions.get(name)
172    }
173}
174
175/// Performs command search.
176///
177/// This function effectively combines the [`classify`] and [`search_path`]
178/// functions into a single operation performing full command search.
179///
180/// See [`search_path`] for why this function requires a mutable reference to
181/// the environment.
182///
183/// See the [module documentation](self) for details of the command search
184/// process.
185#[must_use]
186pub fn search<E: ClassifyEnv + PathEnv>(env: &mut E, name: &str) -> Option<Target> {
187    let mut target = classify(env, name);
188
189    'fill_path: {
190        let path = match &mut target {
191            Target::Builtin { builtin, path } if builtin.r#type == Substitutive => {
192                // Must verify the external counterpart exists.
193                path
194            }
195
196            Target::External { path } => {
197                if name.contains('/') {
198                    // Just access the given path.
199                    *path = CString::new(name).ok()?;
200                    break 'fill_path;
201                } else {
202                    // Need to actually find it in PATH.
203                    path
204                }
205            }
206
207            Target::Builtin { .. } | Target::Function(_) => {
208                // Nothing to do.
209                break 'fill_path;
210            }
211        };
212
213        if let Some(real_path) = search_path(env, name) {
214            *path = real_path;
215        } else {
216            return None;
217        }
218    }
219
220    Some(target)
221}
222
223/// Determines the type of command target without performing a full search.
224///
225/// This function is a simplified version of [`search`] that only classifies the
226/// command name into one of the target types. It does not return the actual
227/// target path, so it is more efficient than `search` if the caller only needs
228/// to know the type of target. However, since the function does not search for
229/// external utilities, it cannot determine whether a substitutive built-in or
230/// an external utility is the actual target. This function always assumes that
231/// searching for an external utility would succeed and returns a target with
232/// an empty path in such cases.
233#[must_use]
234pub fn classify<E: ClassifyEnv>(env: &E, name: &str) -> Target {
235    if name.contains('/') {
236        return Target::External {
237            path: CString::default(),
238        };
239    }
240
241    let builtin = env.builtin(name);
242    if let Some(builtin) = builtin {
243        if builtin.r#type == Special {
244            let path = CString::default();
245            return Target::Builtin { builtin, path };
246        }
247    }
248
249    if let Some(function) = env.function(name) {
250        return Rc::clone(function).into();
251    }
252
253    if let Some(builtin) = builtin {
254        let path = CString::default();
255        return Target::Builtin { builtin, path };
256    }
257
258    Target::External {
259        path: CString::default(),
260    }
261}
262
263/// Searches the `$PATH` for an executable file.
264///
265/// Returns the path to the executable if found. Note that the returned path may
266/// not be absolute if the `$PATH` contains a relative path.
267///
268/// This function requires a mutable reference to the environment because it may
269/// need to update a cache of the results of external utility search (TODO:
270/// which is not yet implemented). The function does not otherwise modify the
271/// environment.
272#[must_use]
273pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
274    env.path()
275        .split()
276        .filter_map(|dir| {
277            let candidate = PathBuf::from_iter([dir, name])
278                .into_unix_string()
279                .into_vec();
280            CString::new(candidate).ok()
281        })
282        .find(|path| env.is_executable_file(path))
283}
284
285#[allow(clippy::field_reassign_with_default)]
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use assert_matches::assert_matches;
290    use std::collections::HashMap;
291    use std::collections::HashSet;
292    use yash_env::builtin::Type::{Elective, Extension, Mandatory};
293    use yash_env::function::FunctionSet;
294    use yash_env::variable::Value;
295    use yash_syntax::source::Location;
296    use yash_syntax::syntax::CompoundCommand;
297    use yash_syntax::syntax::FullCompoundCommand;
298
299    #[derive(Default)]
300    struct DummyEnv {
301        builtins: HashMap<&'static str, Builtin>,
302        functions: FunctionSet,
303        path: Expansion<'static>,
304        executables: HashSet<String>,
305    }
306
307    impl PathEnv for DummyEnv {
308        fn path(&self) -> Expansion<'_> {
309            self.path.as_ref()
310        }
311        fn is_executable_file(&self, path: &CStr) -> bool {
312            if let Ok(path) = path.to_str() {
313                self.executables.contains(path)
314            } else {
315                false
316            }
317        }
318    }
319
320    impl ClassifyEnv for DummyEnv {
321        fn builtin(&self, name: &str) -> Option<Builtin> {
322            self.builtins.get(name).copied()
323        }
324        fn function(&self, name: &str) -> Option<&Rc<Function>> {
325            self.functions.get(name)
326        }
327    }
328
329    fn full_compound_command(s: &str) -> FullCompoundCommand {
330        FullCompoundCommand {
331            command: CompoundCommand::Grouping(s.parse().unwrap()),
332            redirs: vec![],
333        }
334    }
335
336    #[test]
337    fn nothing_is_found_in_empty_env() {
338        let mut env = DummyEnv::default();
339        let target = search(&mut env, "foo");
340        assert!(target.is_none(), "target = {target:?}");
341    }
342
343    #[test]
344    fn nothing_is_found_with_name_unmatched() {
345        let mut env = DummyEnv::default();
346        env.builtins
347            .insert("foo", Builtin::new(Special, |_, _| unreachable!()));
348        let function = Function::new("foo", full_compound_command(""), Location::dummy(""));
349        env.functions.define(function).unwrap();
350
351        let target = search(&mut env, "bar");
352        assert!(target.is_none(), "target = {target:?}");
353    }
354
355    #[test]
356    fn classify_defaults_to_external() {
357        // In an empty environment, any name is not a built-in or function, so it
358        // is classified as an external utility.
359        let env = DummyEnv::default();
360        let target = classify(&env, "foo");
361        assert_eq!(
362            target,
363            Target::External {
364                path: CString::default()
365            }
366        );
367    }
368
369    #[test]
370    fn special_builtin_is_found() {
371        let mut env = DummyEnv::default();
372        let builtin = Builtin::new(Special, |_, _| unreachable!());
373        env.builtins.insert("foo", builtin);
374
375        assert_matches!(
376            search(&mut env, "foo"),
377            Some(Target::Builtin { builtin: result, path }) => {
378                assert_eq!(result.r#type, builtin.r#type);
379                assert_eq!(*path, *c"");
380            }
381        );
382        assert_matches!(
383            classify(&env, "foo"),
384            Target::Builtin { builtin: result, path } => {
385                assert_eq!(result.r#type, builtin.r#type);
386                assert_eq!(*path, *c"");
387            }
388        );
389    }
390
391    #[test]
392    fn function_is_found_if_not_hidden_by_special_builtin() {
393        let mut env = DummyEnv::default();
394        let function = Rc::new(Function::new(
395            "foo",
396            full_compound_command("bar"),
397            Location::dummy("location"),
398        ));
399        env.functions.define(function.clone()).unwrap();
400
401        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
402            assert_eq!(result, function);
403        });
404        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
405            assert_eq!(result, function);
406        });
407    }
408
409    #[test]
410    fn special_builtin_takes_priority_over_function() {
411        let mut env = DummyEnv::default();
412        let builtin = Builtin::new(Special, |_, _| unreachable!());
413        env.builtins.insert("foo", builtin);
414        let function = Function::new(
415            "foo",
416            full_compound_command("bar"),
417            Location::dummy("location"),
418        );
419        env.functions.define(function).unwrap();
420
421        assert_matches!(
422            search(&mut env, "foo"),
423            Some(Target::Builtin { builtin: result, path }) => {
424                assert_eq!(result.r#type, builtin.r#type);
425                assert_eq!(*path, *c"");
426            }
427        );
428        assert_matches!(
429            classify(&env, "foo"),
430            Target::Builtin { builtin: result, path } => {
431                assert_eq!(result.r#type, builtin.r#type);
432                assert_eq!(*path, *c"");
433            }
434        );
435    }
436
437    #[test]
438    fn mandatory_builtin_is_found_if_not_hidden_by_function() {
439        let mut env = DummyEnv::default();
440        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
441        env.builtins.insert("foo", builtin);
442
443        assert_matches!(
444            search(&mut env, "foo"),
445            Some(Target::Builtin { builtin: result, path }) => {
446                assert_eq!(result.r#type, builtin.r#type);
447                assert_eq!(*path, *c"");
448            }
449        );
450        assert_matches!(
451            classify(&env, "foo"),
452            Target::Builtin { builtin: result, path } => {
453                assert_eq!(result.r#type, builtin.r#type);
454                assert_eq!(*path, *c"");
455            }
456        );
457    }
458
459    #[test]
460    fn elective_builtin_is_found_if_not_hidden_by_function() {
461        let mut env = DummyEnv::default();
462        let builtin = Builtin::new(Elective, |_, _| unreachable!());
463        env.builtins.insert("foo", builtin);
464
465        assert_matches!(
466            search(&mut env, "foo"),
467            Some(Target::Builtin { builtin: result, path }) => {
468                assert_eq!(result.r#type, builtin.r#type);
469                assert_eq!(*path, *c"");
470            }
471        );
472        assert_matches!(
473            classify(&env, "foo"),
474            Target::Builtin { builtin: result, path } => {
475                assert_eq!(result.r#type, builtin.r#type);
476                assert_eq!(*path, *c"");
477            }
478        );
479    }
480
481    #[test]
482    fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
483        let mut env = DummyEnv::default();
484        let builtin = Builtin::new(Extension, |_, _| unreachable!());
485        env.builtins.insert("foo", builtin);
486
487        assert_matches!(
488            search(&mut env, "foo"),
489            Some(Target::Builtin { builtin: result, path }) => {
490                assert_eq!(result.r#type, builtin.r#type);
491                assert_eq!(*path, *c"");
492            }
493        );
494        assert_matches!(
495            classify(&env, "foo"),
496            Target::Builtin { builtin: result, path } => {
497                assert_eq!(result.r#type, builtin.r#type);
498                assert_eq!(*path, *c"");
499            }
500        );
501    }
502
503    #[test]
504    fn function_takes_priority_over_mandatory_builtin() {
505        let mut env = DummyEnv::default();
506        env.builtins
507            .insert("foo", Builtin::new(Mandatory, |_, _| unreachable!()));
508
509        let function = Rc::new(Function::new(
510            "foo",
511            full_compound_command("bar"),
512            Location::dummy("location"),
513        ));
514        env.functions.define(function.clone()).unwrap();
515
516        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
517            assert_eq!(result, function);
518        });
519        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
520            assert_eq!(result, function);
521        });
522    }
523
524    #[test]
525    fn function_takes_priority_over_elective_builtin() {
526        let mut env = DummyEnv::default();
527        env.builtins
528            .insert("foo", Builtin::new(Elective, |_, _| unreachable!()));
529
530        let function = Rc::new(Function::new(
531            "foo",
532            full_compound_command("bar"),
533            Location::dummy("location"),
534        ));
535        env.functions.define(function.clone()).unwrap();
536
537        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
538            assert_eq!(result, function);
539        });
540        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
541            assert_eq!(result, function);
542        });
543    }
544
545    #[test]
546    fn function_takes_priority_over_extension_builtin() {
547        let mut env = DummyEnv::default();
548        env.builtins
549            .insert("foo", Builtin::new(Extension, |_, _| unreachable!()));
550
551        let function = Rc::new(Function::new(
552            "foo",
553            full_compound_command("bar"),
554            Location::dummy("location"),
555        ));
556        env.functions.define(function.clone()).unwrap();
557
558        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
559            assert_eq!(result, function);
560        });
561        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
562            assert_eq!(result, function);
563        });
564    }
565
566    #[test]
567    fn substitutive_builtin_is_found_if_external_executable_exists() {
568        let mut env = DummyEnv::default();
569        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
570        env.builtins.insert("foo", builtin);
571        env.path = Expansion::from("/bin");
572        env.executables.insert("/bin/foo".to_string());
573
574        assert_matches!(
575            search(&mut env, "foo"),
576            Some(Target::Builtin { builtin: result, path }) => {
577                assert_eq!(result.r#type, builtin.r#type);
578                assert_eq!(*path, *c"/bin/foo");
579            }
580        );
581        assert_matches!(
582            classify(&env, "foo"),
583            Target::Builtin { builtin: result, path } => {
584                assert_eq!(result.r#type, builtin.r#type);
585                assert_eq!(*path, *c"");
586            }
587        );
588    }
589
590    #[test]
591    fn substitutive_builtin_is_not_found_without_external_executable() {
592        let mut env = DummyEnv::default();
593        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
594        env.builtins.insert("foo", builtin);
595
596        let target = search(&mut env, "foo");
597        assert!(target.is_none(), "target = {target:?}");
598    }
599
600    #[test]
601    fn substitutive_builtin_is_classified_even_without_external_executable() {
602        let mut env = DummyEnv::default();
603        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
604        env.builtins.insert("foo", builtin);
605
606        assert_matches!(
607            classify(&env, "foo"),
608            Target::Builtin { builtin: result, path } => {
609                assert_eq!(result.r#type, builtin.r#type);
610                assert_eq!(*path, *c"");
611            }
612        );
613    }
614
615    #[test]
616    fn function_takes_priority_over_substitutive_builtin() {
617        let mut env = DummyEnv::default();
618        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
619        env.builtins.insert("foo", builtin);
620        env.path = Expansion::from("/bin");
621        env.executables.insert("/bin/foo".to_string());
622
623        let function = Rc::new(Function::new(
624            "foo",
625            full_compound_command("bar"),
626            Location::dummy("location"),
627        ));
628        env.functions.define(function.clone()).unwrap();
629
630        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
631            assert_eq!(result, function);
632        });
633        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
634            assert_eq!(result, function);
635        });
636    }
637
638    #[test]
639    fn external_utility_is_found_if_external_executable_exists() {
640        let mut env = DummyEnv::default();
641        env.path = Expansion::from("/bin");
642        env.executables.insert("/bin/foo".to_string());
643
644        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
645            assert_eq!(*path, *c"/bin/foo");
646        });
647        assert_matches!(classify(&env, "foo"), Target::External { path } => {
648            assert_eq!(*path, *c"");
649        });
650    }
651
652    #[test]
653    fn returns_external_utility_if_name_contains_slash() {
654        // In this case, the external utility file does not have to exist.
655        let mut env = DummyEnv::default();
656        // The special built-in should be ignored because the command name
657        // contains a slash.
658        let builtin = Builtin::new(Special, |_, _| unreachable!());
659        env.builtins.insert("bar/baz", builtin);
660
661        assert_matches!(search(&mut env, "bar/baz"), Some(Target::External { path }) => {
662            assert_eq!(*path, *c"bar/baz");
663        });
664        assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
665            assert_eq!(*path, *c"");
666        });
667    }
668
669    #[test]
670    fn external_target_is_first_executable_found_in_path_scalar() {
671        let mut env = DummyEnv::default();
672        env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
673        env.executables.insert("/usr/bin/foo".to_string());
674        env.executables.insert("/bin/foo".to_string());
675
676        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
677            assert_eq!(*path, *c"/usr/bin/foo");
678        });
679
680        env.executables.insert("/usr/local/bin/foo".to_string());
681
682        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
683            assert_eq!(*path, *c"/usr/local/bin/foo");
684        });
685    }
686
687    #[test]
688    fn external_target_is_first_executable_found_in_path_array() {
689        let mut env = DummyEnv::default();
690        env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
691        env.executables.insert("/usr/bin/foo".to_string());
692        env.executables.insert("/bin/foo".to_string());
693
694        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
695            assert_eq!(*path, *c"/usr/bin/foo");
696        });
697
698        env.executables.insert("/usr/local/bin/foo".to_string());
699
700        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
701            assert_eq!(*path, *c"/usr/local/bin/foo");
702        });
703    }
704
705    #[test]
706    fn empty_string_in_path_names_current_directory() {
707        let mut env = DummyEnv::default();
708        env.path = Expansion::from("/x::/y");
709        env.executables.insert("foo".to_string());
710
711        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
712            assert_eq!(*path, *c"foo");
713        });
714    }
715}