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        *path = search_path(env, name)?;
272    }
273
274    Some(target)
275}
276
277/// Determines the type of command target without performing a full search.
278///
279/// This function is a simplified version of [`search`] that only classifies the
280/// command name into one of the target types. It does not return the actual
281/// target path, so it is more efficient than `search` if the caller only needs
282/// to know the type of target. However, since the function does not search for
283/// external utilities, it cannot determine whether a substitutive built-in or
284/// an external utility is the actual target. This function always assumes that
285/// searching for an external utility would succeed and returns a target with
286/// an empty path in such cases.
287#[must_use]
288pub fn classify<S, E: ClassifyEnv<S>>(env: &E, name: &str) -> Target<S> {
289    if name.contains('/') {
290        return Target::External {
291            path: CString::default(),
292        };
293    }
294
295    let builtin = env.builtin(name);
296    if let Some(builtin) = builtin
297        && builtin.r#type == Special
298    {
299        let path = CString::default();
300        return Target::Builtin { builtin, path };
301    }
302
303    if let Some(function) = env.function(name) {
304        return Rc::clone(function).into();
305    }
306
307    if let Some(builtin) = builtin {
308        let path = CString::default();
309        return Target::Builtin { builtin, path };
310    }
311
312    Target::External {
313        path: CString::default(),
314    }
315}
316
317/// Searches the `$PATH` for an executable file.
318///
319/// Returns the path to the executable if found. Note that the returned path may
320/// not be absolute if the `$PATH` contains a relative path.
321///
322/// This function requires a mutable reference to the environment because it may
323/// need to update a cache of the results of external utility search (TODO:
324/// which is not yet implemented). The function does not otherwise modify the
325/// environment.
326#[must_use]
327pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
328    env.path()
329        .split()
330        .filter_map(|dir| {
331            let candidate = PathBuf::from_iter([dir, name])
332                .into_unix_string()
333                .into_vec();
334            CString::new(candidate).ok()
335        })
336        .find(|path| env.is_executable_file(path))
337}
338
339#[allow(clippy::field_reassign_with_default, reason = "for readability")]
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::builtin::Type::{Elective, Extension, Mandatory};
344    use crate::function::{FunctionBody, FunctionBodyObject, FunctionSet};
345    use crate::option::Off;
346    use crate::source::Location;
347    use crate::variable::Value;
348    use assert_matches::assert_matches;
349    use std::collections::HashMap;
350    use std::collections::HashSet;
351
352    #[test]
353    fn env_builtin_returns_special_builtin() {
354        let mut env = Env::new_virtual();
355        let builtin = Builtin::new(Special, |_, _| unreachable!());
356        env.builtins.insert("foo", builtin);
357        assert_eq!(env.builtin("foo"), Some(builtin));
358    }
359
360    #[test]
361    fn env_builtin_returns_mandatory_builtin() {
362        let mut env = Env::new_virtual();
363        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
364        env.builtins.insert("foo", builtin);
365        assert_eq!(env.builtin("foo"), Some(builtin));
366    }
367
368    #[test]
369    fn env_builtin_returns_elective_builtin() {
370        let mut env = Env::new_virtual();
371        let builtin = Builtin::new(Elective, |_, _| unreachable!());
372        env.builtins.insert("foo", builtin);
373        assert_eq!(env.builtin("foo"), Some(builtin));
374    }
375
376    #[test]
377    fn env_builtin_returns_extension_builtin_if_not_posixly_correct() {
378        let mut env = Env::new_virtual();
379        let builtin = Builtin::new(Extension, |_, _| unreachable!());
380        env.builtins.insert("foo", builtin);
381        assert_eq!(env.options.get(PosixlyCorrect), Off);
382        assert_eq!(env.builtin("foo"), Some(builtin));
383    }
384
385    #[test]
386    fn env_builtin_does_not_return_extension_builtin_if_posixly_correct() {
387        let mut env = Env::new_virtual();
388        env.options.set(PosixlyCorrect, On);
389        let builtin = Builtin::new(Extension, |_, _| unreachable!());
390        env.builtins.insert("foo", builtin);
391        assert_eq!(env.builtin("foo"), None);
392    }
393
394    #[test]
395    fn env_builtin_returns_substitutive_builtin() {
396        // The `$PATH` check for substitutive built-ins is part of `search`, not
397        // `builtin`, so `builtin` returns the built-in regardless of `$PATH`.
398        let mut env = Env::new_virtual();
399        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
400        env.builtins.insert("foo", builtin);
401        assert_eq!(env.builtin("foo"), Some(builtin));
402    }
403
404    #[derive(Default)]
405    struct DummyEnv {
406        builtins: HashMap<&'static str, Builtin<()>>,
407        functions: FunctionSet<()>,
408        path: Expansion<'static>,
409        executables: HashSet<String>,
410    }
411
412    impl PathEnv for DummyEnv {
413        fn path(&self) -> Expansion<'_> {
414            self.path.as_ref()
415        }
416        fn is_executable_file(&self, path: &CStr) -> bool {
417            if let Ok(path) = path.to_str() {
418                self.executables.contains(path)
419            } else {
420                false
421            }
422        }
423    }
424
425    impl ClassifyEnv<()> for DummyEnv {
426        fn builtin(&self, name: &str) -> Option<Builtin<()>> {
427            self.builtins.get(name).copied()
428        }
429        fn function(&self, name: &str) -> Option<&Rc<Function<()>>> {
430            self.functions.get(name)
431        }
432    }
433
434    #[derive(Clone, Debug)]
435    struct FunctionBodyStub;
436
437    impl std::fmt::Display for FunctionBodyStub {
438        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439            unreachable!()
440        }
441    }
442    impl<S> FunctionBody<S> for FunctionBodyStub {
443        async fn execute(&self, _: &mut Env<S>) -> crate::semantics::Result {
444            unreachable!()
445        }
446    }
447
448    fn function_body_stub<S>() -> Rc<dyn FunctionBodyObject<S>> {
449        Rc::new(FunctionBodyStub)
450    }
451
452    #[test]
453    fn nothing_is_found_in_empty_env() {
454        let mut env = DummyEnv::default();
455        let target = search(&mut env, "foo");
456        assert!(target.is_none(), "target = {target:?}");
457    }
458
459    #[test]
460    fn nothing_is_found_with_name_unmatched() {
461        let mut env = DummyEnv::default();
462        env.builtins
463            .insert("foo", Builtin::new(Special, |_, _| unreachable!()));
464        let function = Function::new("foo", function_body_stub(), Location::dummy(""));
465        env.functions.define(function).unwrap();
466
467        let target = search(&mut env, "bar");
468        assert!(target.is_none(), "target = {target:?}");
469    }
470
471    #[test]
472    fn classify_defaults_to_external() {
473        // In an empty environment, any name is not a built-in or function, so it
474        // is classified as an external utility.
475        let env = DummyEnv::default();
476        let target = classify(&env, "foo");
477        assert_eq!(
478            target,
479            Target::External {
480                path: CString::default()
481            }
482        );
483    }
484
485    #[test]
486    fn special_builtin_is_found() {
487        let mut env = DummyEnv::default();
488        let builtin = Builtin::new(Special, |_, _| unreachable!());
489        env.builtins.insert("foo", builtin);
490
491        assert_matches!(
492            search(&mut env, "foo"),
493            Some(Target::Builtin { builtin: result, path }) => {
494                assert_eq!(result.r#type, builtin.r#type);
495                assert_eq!(*path, *c"");
496            }
497        );
498        assert_matches!(
499            classify(&env, "foo"),
500            Target::Builtin { builtin: result, path } => {
501                assert_eq!(result.r#type, builtin.r#type);
502                assert_eq!(*path, *c"");
503            }
504        );
505    }
506
507    #[test]
508    fn function_is_found_if_not_hidden_by_special_builtin() {
509        let mut env = DummyEnv::default();
510        let function = Rc::new(Function::new(
511            "foo",
512            function_body_stub(),
513            Location::dummy("location"),
514        ));
515        env.functions.define(function.clone()).unwrap();
516
517        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
518            assert_eq!(result, function);
519        });
520        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
521            assert_eq!(result, function);
522        });
523    }
524
525    #[test]
526    fn special_builtin_takes_priority_over_function() {
527        let mut env = DummyEnv::default();
528        let builtin = Builtin::new(Special, |_, _| unreachable!());
529        env.builtins.insert("foo", builtin);
530        let function = Function::new("foo", function_body_stub(), Location::dummy("location"));
531        env.functions.define(function).unwrap();
532
533        assert_matches!(
534            search(&mut env, "foo"),
535            Some(Target::Builtin { builtin: result, path }) => {
536                assert_eq!(result.r#type, builtin.r#type);
537                assert_eq!(*path, *c"");
538            }
539        );
540        assert_matches!(
541            classify(&env, "foo"),
542            Target::Builtin { builtin: result, path } => {
543                assert_eq!(result.r#type, builtin.r#type);
544                assert_eq!(*path, *c"");
545            }
546        );
547    }
548
549    #[test]
550    fn mandatory_builtin_is_found_if_not_hidden_by_function() {
551        let mut env = DummyEnv::default();
552        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
553        env.builtins.insert("foo", builtin);
554
555        assert_matches!(
556            search(&mut env, "foo"),
557            Some(Target::Builtin { builtin: result, path }) => {
558                assert_eq!(result.r#type, builtin.r#type);
559                assert_eq!(*path, *c"");
560            }
561        );
562        assert_matches!(
563            classify(&env, "foo"),
564            Target::Builtin { builtin: result, path } => {
565                assert_eq!(result.r#type, builtin.r#type);
566                assert_eq!(*path, *c"");
567            }
568        );
569    }
570
571    #[test]
572    fn elective_builtin_is_found_if_not_hidden_by_function() {
573        let mut env = DummyEnv::default();
574        let builtin = Builtin::new(Elective, |_, _| unreachable!());
575        env.builtins.insert("foo", builtin);
576
577        assert_matches!(
578            search(&mut env, "foo"),
579            Some(Target::Builtin { builtin: result, path }) => {
580                assert_eq!(result.r#type, builtin.r#type);
581                assert_eq!(*path, *c"");
582            }
583        );
584        assert_matches!(
585            classify(&env, "foo"),
586            Target::Builtin { builtin: result, path } => {
587                assert_eq!(result.r#type, builtin.r#type);
588                assert_eq!(*path, *c"");
589            }
590        );
591    }
592
593    #[test]
594    fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
595        let mut env = DummyEnv::default();
596        let builtin = Builtin::new(Extension, |_, _| unreachable!());
597        env.builtins.insert("foo", builtin);
598
599        assert_matches!(
600            search(&mut env, "foo"),
601            Some(Target::Builtin { builtin: result, path }) => {
602                assert_eq!(result.r#type, builtin.r#type);
603                assert_eq!(*path, *c"");
604            }
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_mandatory_builtin() {
617        let mut env = DummyEnv::default();
618        env.builtins
619            .insert("foo", Builtin::new(Mandatory, |_, _| unreachable!()));
620
621        let function = Rc::new(Function::new(
622            "foo",
623            function_body_stub(),
624            Location::dummy("location"),
625        ));
626        env.functions.define(function.clone()).unwrap();
627
628        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
629            assert_eq!(result, function);
630        });
631        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
632            assert_eq!(result, function);
633        });
634    }
635
636    #[test]
637    fn function_takes_priority_over_elective_builtin() {
638        let mut env = DummyEnv::default();
639        env.builtins
640            .insert("foo", Builtin::new(Elective, |_, _| unreachable!()));
641
642        let function = Rc::new(Function::new(
643            "foo",
644            function_body_stub(),
645            Location::dummy("location"),
646        ));
647        env.functions.define(function.clone()).unwrap();
648
649        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
650            assert_eq!(result, function);
651        });
652        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
653            assert_eq!(result, function);
654        });
655    }
656
657    #[test]
658    fn function_takes_priority_over_extension_builtin() {
659        let mut env = DummyEnv::default();
660        env.builtins
661            .insert("foo", Builtin::new(Extension, |_, _| unreachable!()));
662
663        let function = Rc::new(Function::new(
664            "foo",
665            function_body_stub(),
666            Location::dummy("location"),
667        ));
668        env.functions.define(function.clone()).unwrap();
669
670        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
671            assert_eq!(result, function);
672        });
673        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
674            assert_eq!(result, function);
675        });
676    }
677
678    #[test]
679    fn substitutive_builtin_is_found_if_external_executable_exists() {
680        let mut env = DummyEnv::default();
681        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
682        env.builtins.insert("foo", builtin);
683        env.path = Expansion::from("/bin");
684        env.executables.insert("/bin/foo".to_string());
685
686        assert_matches!(
687            search(&mut env, "foo"),
688            Some(Target::Builtin { builtin: result, path }) => {
689                assert_eq!(result.r#type, builtin.r#type);
690                assert_eq!(*path, *c"/bin/foo");
691            }
692        );
693        assert_matches!(
694            classify(&env, "foo"),
695            Target::Builtin { builtin: result, path } => {
696                assert_eq!(result.r#type, builtin.r#type);
697                assert_eq!(*path, *c"");
698            }
699        );
700    }
701
702    #[test]
703    fn substitutive_builtin_is_not_found_without_external_executable() {
704        let mut env = DummyEnv::default();
705        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
706        env.builtins.insert("foo", builtin);
707
708        let target = search(&mut env, "foo");
709        assert!(target.is_none(), "target = {target:?}");
710    }
711
712    #[test]
713    fn substitutive_builtin_is_classified_even_without_external_executable() {
714        let mut env = DummyEnv::default();
715        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
716        env.builtins.insert("foo", builtin);
717
718        assert_matches!(
719            classify(&env, "foo"),
720            Target::Builtin { builtin: result, path } => {
721                assert_eq!(result.r#type, builtin.r#type);
722                assert_eq!(*path, *c"");
723            }
724        );
725    }
726
727    #[test]
728    fn function_takes_priority_over_substitutive_builtin() {
729        let mut env = DummyEnv::default();
730        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
731        env.builtins.insert("foo", builtin);
732        env.path = Expansion::from("/bin");
733        env.executables.insert("/bin/foo".to_string());
734
735        let function = Rc::new(Function::new(
736            "foo",
737            function_body_stub(),
738            Location::dummy("location"),
739        ));
740        env.functions.define(function.clone()).unwrap();
741
742        assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
743            assert_eq!(result, function);
744        });
745        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
746            assert_eq!(result, function);
747        });
748    }
749
750    #[test]
751    fn external_utility_is_found_if_external_executable_exists() {
752        let mut env = DummyEnv::default();
753        env.path = Expansion::from("/bin");
754        env.executables.insert("/bin/foo".to_string());
755
756        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
757            assert_eq!(*path, *c"/bin/foo");
758        });
759        assert_matches!(classify(&env, "foo"), Target::External { path } => {
760            assert_eq!(*path, *c"");
761        });
762    }
763
764    #[test]
765    fn returns_external_utility_if_name_contains_slash() {
766        // In this case, the external utility file does not have to exist.
767        let mut env = DummyEnv::default();
768        // The special built-in should be ignored because the command name
769        // contains a slash.
770        let builtin = Builtin::new(Special, |_, _| unreachable!());
771        env.builtins.insert("bar/baz", builtin);
772
773        assert_matches!(search(&mut env, "bar/baz"), Some(Target::External { path }) => {
774            assert_eq!(*path, *c"bar/baz");
775        });
776        assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
777            assert_eq!(*path, *c"");
778        });
779    }
780
781    #[test]
782    fn external_target_is_first_executable_found_in_path_scalar() {
783        let mut env = DummyEnv::default();
784        env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
785        env.executables.insert("/usr/bin/foo".to_string());
786        env.executables.insert("/bin/foo".to_string());
787
788        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
789            assert_eq!(*path, *c"/usr/bin/foo");
790        });
791
792        env.executables.insert("/usr/local/bin/foo".to_string());
793
794        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
795            assert_eq!(*path, *c"/usr/local/bin/foo");
796        });
797    }
798
799    #[test]
800    fn external_target_is_first_executable_found_in_path_array() {
801        let mut env = DummyEnv::default();
802        env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
803        env.executables.insert("/usr/bin/foo".to_string());
804        env.executables.insert("/bin/foo".to_string());
805
806        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
807            assert_eq!(*path, *c"/usr/bin/foo");
808        });
809
810        env.executables.insert("/usr/local/bin/foo".to_string());
811
812        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
813            assert_eq!(*path, *c"/usr/local/bin/foo");
814        });
815    }
816
817    #[test]
818    fn empty_string_in_path_names_current_directory() {
819        let mut env = DummyEnv::default();
820        env.path = Expansion::from("/x::/y");
821        env.executables.insert("foo".to_string());
822
823        assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
824            assert_eq!(*path, *c"foo");
825        });
826    }
827}