Skip to main content

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