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