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//! Note the difference between a built-in being *ignored* and *rejected*. An
41//! ignored built-in is treated as if it did not exist, so the search falls
42//! through to an external utility. A rejected built-in is still found, but it
43//! cannot be executed; the search fails with [`Unusable`] rather than falling
44//! through. See [`Availability`].
45//!
46//! [command search]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_09_01_04
47//! [simple command]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_09_01
48
49use crate::Env;
50use crate::builtin::Builtin;
51use crate::builtin::Type;
52use crate::builtin::Type::{Elective, Extension, Mandatory, Special, Substitutive};
53use crate::builtin::is_posix_special_builtin_name;
54use crate::function::Function;
55use crate::option::{On, Portable, PosixlyCorrect};
56use crate::path::PathBuf;
57use crate::semantics::ExitStatus;
58use crate::system::IsExecutableFile;
59use crate::variable::Expansion;
60use crate::variable::PATH;
61use std::ffi::CStr;
62use std::ffi::CString;
63use std::rc::Rc;
64
65/// Whether a built-in found in the command search may be executed
66///
67/// The command search may find a built-in that the current shell options do not
68/// allow to execute. Such a built-in is not ignored: the search does not fall
69/// through to an external utility. Instead, it is reported as unavailable so
70/// that the caller can tell the user why the built-in cannot be used.
71#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
72#[non_exhaustive]
73pub enum Availability {
74    /// The built-in may be executed.
75    Available,
76
77    /// The built-in, or the name under which it was found, is not defined in
78    /// POSIX, and the [`Portable`] option rejects it.
79    NotPortable,
80}
81
82/// Target of a simple command execution
83///
84/// This is the result of the [command search](search).
85///
86/// # Notes on equality
87///
88/// Although this type implements `PartialEq`, comparison between instances of
89/// this type may not always yield predictable results due to the presence of
90/// function pointers in [`Builtin`]. As a result, it is recommended to avoid
91/// relying on equality comparisons for values of this type. See
92/// <https://doc.rust-lang.org/std/ptr/fn.fn_addr_eq.html> for the
93/// characteristics of function pointer comparisons.
94pub enum Target<S> {
95    /// Built-in utility
96    Builtin {
97        /// Definition of the built-in
98        builtin: Builtin<S>,
99
100        /// Whether the built-in may be executed
101        ///
102        /// [`search`] fails with [`Unusable::NotPortable`] instead of returning
103        /// an unavailable built-in, so this value is always
104        /// [`Available`](Availability::Available) in a target obtained from
105        /// `search`. A target obtained from [`classify`] may have any value
106        /// because `classify` does not reject anything by itself.
107        availability: Availability,
108
109        /// Path to the external utility that is shadowed by the substitutive
110        /// built-in
111        ///
112        /// This value is only used for substitutive built-ins. For other types
113        /// of built-ins, this value is always empty.
114        ///
115        /// The path may not necessarily be absolute. If the `PATH` variable
116        /// contains a relative directory name and the external utility is found
117        /// in that directory, the path will be relative.
118        path: CString,
119    },
120
121    /// Function
122    Function(Rc<Function<S>>),
123
124    /// External utility
125    External {
126        /// Path to the external utility
127        ///
128        /// The path may not necessarily be absolute. If the `PATH` variable
129        /// contains a relative directory name and the external utility is found
130        /// in that directory, the path will be relative.
131        ///
132        /// The path may not name an existing executable file, either. If the
133        /// command name contains a slash, the name is immediately regarded as a
134        /// path to an external utility, regardless of whether the named
135        /// external utility actually exists.
136        path: CString,
137    },
138}
139
140// Not derived automatically because S may not implement Clone, PartialEq, or Debug.
141impl<S> Clone for Target<S> {
142    fn clone(&self) -> Self {
143        match self {
144            Self::Builtin {
145                builtin,
146                availability,
147                path,
148            } => Self::Builtin {
149                builtin: *builtin,
150                availability: *availability,
151                path: path.clone(),
152            },
153            Self::Function(f) => Self::Function(f.clone()),
154            Self::External { path } => Self::External { path: path.clone() },
155        }
156    }
157}
158
159impl<S> PartialEq for Target<S> {
160    fn eq(&self, other: &Self) -> bool {
161        match (self, other) {
162            (
163                Self::Builtin {
164                    builtin: l_builtin,
165                    availability: l_availability,
166                    path: l_path,
167                },
168                Self::Builtin {
169                    builtin: r_builtin,
170                    availability: r_availability,
171                    path: r_path,
172                },
173            ) => l_builtin == r_builtin && l_availability == r_availability && l_path == r_path,
174            (Self::Function(l), Self::Function(r)) => l == r,
175            (Self::External { path: l_path }, Self::External { path: r_path }) => l_path == r_path,
176            _ => false,
177        }
178    }
179}
180
181impl<S> Eq for Target<S> {}
182
183impl<S> std::fmt::Debug for Target<S> {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        match self {
186            Self::Builtin {
187                builtin,
188                availability,
189                path,
190            } => f
191                .debug_struct("Builtin")
192                .field("builtin", builtin)
193                .field("availability", availability)
194                .field("path", path)
195                .finish(),
196            Self::Function(func) => f.debug_tuple("Function").field(func).finish(),
197            Self::External { path } => f.debug_struct("External").field("path", path).finish(),
198        }
199    }
200}
201
202impl<S> From<Rc<Function<S>>> for Target<S> {
203    #[inline]
204    fn from(function: Rc<Function<S>>) -> Target<S> {
205        Target::Function(function)
206    }
207}
208
209impl<S> From<Function<S>> for Target<S> {
210    #[inline]
211    fn from(function: Function<S>) -> Target<S> {
212        Target::Function(function.into())
213    }
214}
215
216// impl From<CString> for Target
217// not implemented because of ambiguity between substitutive built-ins and
218// external utilities
219
220/// Collection of data used in [classifying](classify) command names
221pub trait ClassifyEnv<S> {
222    /// Retrieves the built-in by name.
223    ///
224    /// This function returns `None` if there is no built-in with the given name
225    /// or if the current shell options make the shell ignore it. An ignored
226    /// built-in is treated as if it did not exist, so the command search falls
227    /// through to an external utility.
228    ///
229    /// If a built-in is found, this function also returns its
230    /// [`Availability`], which tells whether the shell options allow executing
231    /// it. Unlike an ignored built-in, an unavailable built-in still hides an
232    /// external utility of the same name.
233    #[must_use]
234    fn builtin(&self, name: &str) -> Option<(Builtin<S>, Availability)>;
235
236    /// Retrieves the function by name.
237    #[must_use]
238    fn function(&self, name: &str) -> Option<&Rc<Function<S>>>;
239}
240
241/// Part of the shell execution environment command path search depends on
242pub trait PathEnv {
243    /// Accesses the `$PATH` variable in the environment.
244    ///
245    /// This function returns an `Expansion` rather than a reference to a
246    /// variable value because the path may be dynamically computed in the
247    /// function.
248    #[must_use]
249    fn path(&self) -> Expansion<'_>;
250
251    /// Whether there is an executable file at the specified path.
252    #[must_use]
253    fn is_executable_file(&self, path: &CStr) -> bool;
254    // TODO Cache the results of external utility search
255}
256
257impl<S: IsExecutableFile> PathEnv for Env<S> {
258    /// Returns the value of the `$PATH` variable.
259    ///
260    /// This function assumes that the `$PATH` variable has no quirks. If the
261    /// variable has a quirk, the function panics.
262    fn path(&self) -> Expansion<'_> {
263        self.variables
264            .get(PATH)
265            .and_then(|var| {
266                assert_eq!(var.quirk, None, "PATH does not support quirks");
267                var.value.as_ref()
268            })
269            .into()
270    }
271
272    fn is_executable_file(&self, path: &CStr) -> bool {
273        self.system.is_executable_file(path)
274    }
275}
276
277impl<S> ClassifyEnv<S> for Env<S> {
278    fn builtin(&self, name: &str) -> Option<(Builtin<S>, Availability)> {
279        let builtin = self.builtins.get(name).copied()?;
280
281        // An extension built-in is ignored altogether when the shell must
282        // behave POSIXly, so the search falls through to an external utility.
283        let found = builtin.r#type != Extension || self.options.get(PosixlyCorrect) != On;
284        if !found {
285            return None;
286        }
287
288        // A built-in that POSIX does not define is found but rejected when the
289        // shell must reject non-portable behavior. Unlike an ignored built-in,
290        // it still hides an external utility of the same name. This also
291        // applies to a special built-in found under a non-POSIX alias name
292        // (e.g. `source`, an alias for `.`).
293        let availability = match builtin.r#type {
294            Elective | Extension if self.options.get(Portable) == On => Availability::NotPortable,
295            Special if self.options.get(Portable) == On && !is_posix_special_builtin_name(name) => {
296                Availability::NotPortable
297            }
298            Special | Mandatory | Elective | Extension | Substitutive => Availability::Available,
299        };
300
301        Some((builtin, availability))
302    }
303
304    #[inline]
305    fn function(&self, name: &str) -> Option<&Rc<Function<S>>> {
306        self.functions.get(name)
307    }
308}
309
310/// Reason why a built-in found in the command search cannot be executed
311#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
312#[non_exhaustive]
313pub enum Unusable {
314    /// The built-in is [substitutive](Substitutive), but no corresponding
315    /// external utility was found in `$PATH`.
316    NotInPath,
317
318    /// The built-in, or the name under which it was found, is not defined in
319    /// POSIX, and the [`Portable`] option rejects it.
320    NotPortable,
321}
322
323impl Unusable {
324    /// Returns the exit status the shell should produce for this reason.
325    ///
326    /// This is 126 if the built-in was rejected, and 127 if it effectively does
327    /// not exist as a runnable command.
328    #[must_use]
329    pub const fn exit_status(self) -> ExitStatus {
330        match self {
331            Self::NotInPath => ExitStatus::NOT_FOUND,
332            Self::NotPortable => ExitStatus::NOEXEC,
333        }
334    }
335}
336
337/// Describes the reason without naming the built-in.
338///
339/// The result is a sentence fragment meant to be embedded in an error message
340/// that identifies the built-in, so it does not mention the name itself. It
341/// states what the built-in is and why that makes it unusable, so the message
342/// stands on its own without a title telling the two reasons apart.
343impl std::fmt::Display for Unusable {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        match self {
346            Self::NotInPath => "a substitutive built-in, so it cannot be used unless $PATH has \
347                                an external utility of the same name"
348                .fmt(f),
349            Self::NotPortable => {
350                "not a POSIX built-in, so it cannot be used while the `portable` option is on"
351                    .fmt(f)
352            }
353        }
354    }
355}
356
357/// Reason why the [command search](search) did not yield a target
358#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
359#[non_exhaustive]
360pub enum Error {
361    /// No command was found with the given name.
362    NotFound,
363
364    /// A built-in was found, but it cannot be executed.
365    ///
366    /// Note that the shell must not fall back to an external utility in this
367    /// case: the built-in was found, so the command search is over.
368    Unusable(Unusable),
369}
370
371impl Error {
372    /// Returns the exit status the shell should produce for this error.
373    ///
374    /// This is 127 if no command was found, and the
375    /// [`Unusable::exit_status`] if a built-in was found but cannot be
376    /// executed.
377    #[must_use]
378    pub const fn exit_status(self) -> ExitStatus {
379        match self {
380            Self::NotFound => ExitStatus::NOT_FOUND,
381            Self::Unusable(cause) => cause.exit_status(),
382        }
383    }
384}
385
386/// Describes the reason without naming the command.
387///
388/// The result is a sentence fragment meant to be embedded in an error message
389/// that identifies the command, so it does not mention the name itself.
390impl std::fmt::Display for Error {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392        match self {
393            Self::NotFound => "no command with this name".fmt(f),
394            Self::Unusable(cause) => cause.fmt(f),
395        }
396    }
397}
398
399impl From<Unusable> for Error {
400    #[inline]
401    fn from(unusable: Unusable) -> Self {
402        Self::Unusable(unusable)
403    }
404}
405
406/// Completes the command search for a built-in.
407///
408/// This function decides whether a built-in found by [`classify`] can actually
409/// be executed. On success, it returns the value to be stored in the `path`
410/// field of [`Target::Builtin`], that is, the path to the external utility
411/// shadowed by a [substitutive](Substitutive) built-in, or an empty string for
412/// other types of built-ins.
413///
414/// The `r#type` and `availability` arguments should be the properties of the
415/// built-in that was found for `name`.
416///
417/// [`search`] applies this function to a built-in target, but the caller may
418/// need to apply it separately if it obtained a target from [`classify`]. That
419/// is typically the case when the caller wants to postpone the check until
420/// immediately before executing the built-in.
421///
422/// See [`search_path`] for why this function requires a mutable reference to
423/// the environment.
424pub fn resolve_builtin<E: PathEnv>(
425    env: &mut E,
426    name: &str,
427    r#type: Type,
428    availability: Availability,
429) -> Result<CString, Unusable> {
430    match availability {
431        Availability::NotPortable => Err(Unusable::NotPortable),
432
433        // A substitutive built-in is executed only if its external counterpart
434        // is present in `$PATH`.
435        Availability::Available if r#type == Substitutive => {
436            search_path(env, name).ok_or(Unusable::NotInPath)
437        }
438
439        Availability::Available => Ok(CString::default()),
440    }
441}
442
443/// Performs command search.
444///
445/// This function effectively combines the [`classify`], [`resolve_builtin`],
446/// and [`search_path`] functions into a single operation performing full
447/// command search.
448///
449/// See [`search_path`] for why this function requires a mutable reference to
450/// the environment.
451///
452/// See the [module documentation](self) for details of the command search
453/// process.
454pub fn search<S, E: ClassifyEnv<S> + PathEnv>(env: &mut E, name: &str) -> Result<Target<S>, Error> {
455    let mut target = classify(env, name);
456
457    match &mut target {
458        Target::Builtin {
459            builtin,
460            availability,
461            path,
462        } => *path = resolve_builtin(env, name, builtin.r#type, *availability)?,
463
464        Target::External { path } => {
465            *path = if name.contains('/') {
466                // Just access the given path.
467                CString::new(name).map_err(|_| Error::NotFound)?
468            } else {
469                // Need to actually find it in PATH.
470                search_path(env, name).ok_or(Error::NotFound)?
471            };
472        }
473
474        Target::Function(_) => {
475            // Nothing to do.
476        }
477    }
478
479    Ok(target)
480}
481
482/// Determines the type of command target without performing a full search.
483///
484/// This function is a simplified version of [`search`] that only classifies the
485/// command name into one of the target types. It does not return the actual
486/// target path, so it is more efficient than `search` if the caller only needs
487/// to know the type of target. However, since the function does not search for
488/// external utilities, it cannot determine whether a substitutive built-in or
489/// an external utility is the actual target. This function always assumes that
490/// searching for an external utility would succeed and returns a target with
491/// an empty path in such cases.
492///
493/// This function does not reject a built-in that the shell options disallow,
494/// either. It reports the [`Availability`] as part of the target so that the
495/// caller can reject it later with [`resolve_builtin`].
496#[must_use]
497pub fn classify<S, E: ClassifyEnv<S>>(env: &E, name: &str) -> Target<S> {
498    if name.contains('/') {
499        return Target::External {
500            path: CString::default(),
501        };
502    }
503
504    let builtin = env.builtin(name);
505    if let Some((builtin, availability)) = builtin
506        && builtin.r#type == Special
507    {
508        let path = CString::default();
509        return Target::Builtin {
510            builtin,
511            availability,
512            path,
513        };
514    }
515
516    if let Some(function) = env.function(name) {
517        return Rc::clone(function).into();
518    }
519
520    if let Some((builtin, availability)) = builtin {
521        let path = CString::default();
522        return Target::Builtin {
523            builtin,
524            availability,
525            path,
526        };
527    }
528
529    Target::External {
530        path: CString::default(),
531    }
532}
533
534/// Searches the `$PATH` for an executable file.
535///
536/// Returns the path to the executable if found. Note that the returned path may
537/// not be absolute if the `$PATH` contains a relative path.
538///
539/// This function requires a mutable reference to the environment because it may
540/// need to update a cache of the results of external utility search (TODO:
541/// which is not yet implemented). The function does not otherwise modify the
542/// environment.
543#[must_use]
544pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
545    env.path()
546        .split()
547        .filter_map(|dir| {
548            let candidate = PathBuf::from_iter([dir, name])
549                .into_unix_string()
550                .into_vec();
551            CString::new(candidate).ok()
552        })
553        .find(|path| env.is_executable_file(path))
554}
555
556#[allow(clippy::field_reassign_with_default, reason = "for readability")]
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::function::{FunctionBody, FunctionBodyObject, FunctionSet};
561    use crate::option::Off;
562    use crate::source::Location;
563    use crate::variable::Value;
564    use assert_matches::assert_matches;
565    use std::collections::HashMap;
566    use std::collections::HashSet;
567
568    #[test]
569    fn env_builtin_returns_special_builtin() {
570        let mut env = Env::new_virtual();
571        let builtin = Builtin::new(Special, |_, _| unreachable!());
572        env.builtins.insert("foo", builtin);
573        assert_eq!(env.builtin("foo"), Some((builtin, Availability::Available)));
574    }
575
576    #[test]
577    fn env_builtin_returns_mandatory_builtin() {
578        let mut env = Env::new_virtual();
579        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
580        env.builtins.insert("foo", builtin);
581        assert_eq!(env.builtin("foo"), Some((builtin, Availability::Available)));
582    }
583
584    #[test]
585    fn env_builtin_returns_elective_builtin() {
586        let mut env = Env::new_virtual();
587        let builtin = Builtin::new(Elective, |_, _| unreachable!());
588        env.builtins.insert("foo", builtin);
589        assert_eq!(env.builtin("foo"), Some((builtin, Availability::Available)));
590    }
591
592    #[test]
593    fn env_builtin_returns_extension_builtin_if_not_posixly_correct() {
594        let mut env = Env::new_virtual();
595        let builtin = Builtin::new(Extension, |_, _| unreachable!());
596        env.builtins.insert("foo", builtin);
597        assert_eq!(env.options.get(PosixlyCorrect), Off);
598        assert_eq!(env.builtin("foo"), Some((builtin, Availability::Available)));
599    }
600
601    #[test]
602    fn env_builtin_does_not_return_extension_builtin_if_posixly_correct() {
603        let mut env = Env::new_virtual();
604        env.options.set(PosixlyCorrect, On);
605        let builtin = Builtin::new(Extension, |_, _| unreachable!());
606        env.builtins.insert("foo", builtin);
607        assert_eq!(env.builtin("foo"), None);
608    }
609
610    #[test]
611    fn env_builtin_rejects_elective_builtin_if_portable() {
612        let mut env = Env::new_virtual();
613        env.options.set(Portable, On);
614        let builtin = Builtin::new(Elective, |_, _| unreachable!());
615        env.builtins.insert("foo", builtin);
616        assert_eq!(
617            env.builtin("foo"),
618            Some((builtin, Availability::NotPortable))
619        );
620    }
621
622    #[test]
623    fn env_builtin_rejects_extension_builtin_if_portable() {
624        let mut env = Env::new_virtual();
625        env.options.set(Portable, On);
626        let builtin = Builtin::new(Extension, |_, _| unreachable!());
627        env.builtins.insert("foo", builtin);
628        assert_eq!(
629            env.builtin("foo"),
630            Some((builtin, Availability::NotPortable))
631        );
632    }
633
634    #[test]
635    fn env_builtin_accepts_posix_builtins_if_portable() {
636        for r#type in [Mandatory, Substitutive] {
637            let mut env = Env::new_virtual();
638            env.options.set(Portable, On);
639            let builtin = Builtin::new(r#type, |_, _| unreachable!());
640            env.builtins.insert("foo", builtin);
641            assert_eq!(
642                env.builtin("foo"),
643                Some((builtin, Availability::Available)),
644                "type={type:?}"
645            );
646        }
647    }
648
649    #[test]
650    fn env_builtin_accepts_special_builtin_with_posix_name_if_portable() {
651        let mut env = Env::new_virtual();
652        env.options.set(Portable, On);
653        let builtin = Builtin::new(Special, |_, _| unreachable!());
654        env.builtins.insert(".", builtin);
655        assert_eq!(env.builtin("."), Some((builtin, Availability::Available)));
656    }
657
658    #[test]
659    fn env_builtin_rejects_special_builtin_with_non_posix_name_if_portable() {
660        let mut env = Env::new_virtual();
661        env.options.set(Portable, On);
662        let builtin = Builtin::new(Special, |_, _| unreachable!());
663        env.builtins.insert("source", builtin);
664        assert_eq!(
665            env.builtin("source"),
666            Some((builtin, Availability::NotPortable))
667        );
668    }
669
670    #[test]
671    fn env_builtin_returns_substitutive_builtin() {
672        // The `$PATH` check for substitutive built-ins is part of `search`, not
673        // `builtin`, so `builtin` returns the built-in regardless of `$PATH`.
674        let mut env = Env::new_virtual();
675        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
676        env.builtins.insert("foo", builtin);
677        assert_eq!(env.builtin("foo"), Some((builtin, Availability::Available)));
678    }
679
680    #[test]
681    fn unusable_exit_status() {
682        assert_eq!(Unusable::NotInPath.exit_status(), ExitStatus(127));
683        assert_eq!(Unusable::NotPortable.exit_status(), ExitStatus(126));
684    }
685
686    #[test]
687    fn unusable_display() {
688        assert_eq!(
689            Unusable::NotInPath.to_string(),
690            "a substitutive built-in, so it cannot be used unless $PATH has an external utility \
691             of the same name",
692        );
693        assert_eq!(
694            Unusable::NotPortable.to_string(),
695            "not a POSIX built-in, so it cannot be used while the `portable` option is on",
696        );
697    }
698
699    #[test]
700    fn error_exit_status() {
701        assert_eq!(Error::NotFound.exit_status(), ExitStatus(127));
702        assert_eq!(
703            Error::Unusable(Unusable::NotInPath).exit_status(),
704            ExitStatus(127),
705        );
706        assert_eq!(
707            Error::Unusable(Unusable::NotPortable).exit_status(),
708            ExitStatus(126),
709        );
710    }
711
712    #[test]
713    fn error_display() {
714        assert_eq!(Error::NotFound.to_string(), "no command with this name");
715        assert_eq!(
716            Error::Unusable(Unusable::NotPortable).to_string(),
717            "not a POSIX built-in, so it cannot be used while the `portable` option is on",
718        );
719    }
720
721    #[derive(Default)]
722    struct DummyEnv {
723        builtins: HashMap<&'static str, (Builtin<()>, Availability)>,
724        functions: FunctionSet<()>,
725        path: Expansion<'static>,
726        executables: HashSet<String>,
727    }
728
729    impl PathEnv for DummyEnv {
730        fn path(&self) -> Expansion<'_> {
731            self.path.as_ref()
732        }
733        fn is_executable_file(&self, path: &CStr) -> bool {
734            if let Ok(path) = path.to_str() {
735                self.executables.contains(path)
736            } else {
737                false
738            }
739        }
740    }
741
742    impl ClassifyEnv<()> for DummyEnv {
743        fn builtin(&self, name: &str) -> Option<(Builtin<()>, Availability)> {
744            self.builtins.get(name).copied()
745        }
746        fn function(&self, name: &str) -> Option<&Rc<Function<()>>> {
747            self.functions.get(name)
748        }
749    }
750
751    #[derive(Clone, Debug)]
752    struct FunctionBodyStub;
753
754    impl std::fmt::Display for FunctionBodyStub {
755        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756            unreachable!()
757        }
758    }
759    impl<S> FunctionBody<S> for FunctionBodyStub {
760        async fn execute(&self, _: &mut Env<S>) -> crate::semantics::Result {
761            unreachable!()
762        }
763    }
764
765    fn function_body_stub<S>() -> Rc<dyn FunctionBodyObject<S>> {
766        Rc::new(FunctionBodyStub)
767    }
768
769    #[test]
770    fn resolve_builtin_returns_empty_path_for_non_substitutive_builtin() {
771        let mut env = DummyEnv::default();
772
773        let result = resolve_builtin(&mut env, "foo", Mandatory, Availability::Available);
774
775        assert_eq!(result, Ok(CString::default()));
776    }
777
778    #[test]
779    fn resolve_builtin_returns_external_path_for_substitutive_builtin() {
780        let mut env = DummyEnv::default();
781        env.path = Expansion::from("/bin");
782        env.executables.insert("/bin/foo".to_string());
783
784        let result = resolve_builtin(&mut env, "foo", Substitutive, Availability::Available);
785
786        assert_eq!(result, Ok(c"/bin/foo".to_owned()));
787    }
788
789    #[test]
790    fn resolve_builtin_rejects_substitutive_builtin_missing_in_path() {
791        let mut env = DummyEnv::default();
792
793        let result = resolve_builtin(&mut env, "foo", Substitutive, Availability::Available);
794
795        assert_eq!(result, Err(Unusable::NotInPath));
796    }
797
798    #[test]
799    fn resolve_builtin_rejects_unavailable_builtin() {
800        // The rejection takes precedence over the `$PATH` search, so a
801        // substitutive built-in present in `$PATH` is still rejected.
802        let mut env = DummyEnv::default();
803        env.path = Expansion::from("/bin");
804        env.executables.insert("/bin/foo".to_string());
805
806        let result = resolve_builtin(&mut env, "foo", Substitutive, Availability::NotPortable);
807
808        assert_eq!(result, Err(Unusable::NotPortable));
809    }
810
811    #[test]
812    fn nothing_is_found_in_empty_env() {
813        let mut env = DummyEnv::default();
814        let target = search(&mut env, "foo");
815        assert_eq!(target, Err(Error::NotFound));
816    }
817
818    #[test]
819    fn nothing_is_found_with_name_unmatched() {
820        let mut env = DummyEnv::default();
821        env.builtins.insert(
822            "foo",
823            (
824                Builtin::new(Special, |_, _| unreachable!()),
825                Availability::Available,
826            ),
827        );
828        let function = Function::new("foo", function_body_stub(), Location::dummy(""));
829        env.functions.define(function).unwrap();
830
831        let target = search(&mut env, "bar");
832        assert_eq!(target, Err(Error::NotFound));
833    }
834
835    #[test]
836    fn classify_defaults_to_external() {
837        // In an empty environment, any name is not a built-in or function, so it
838        // is classified as an external utility.
839        let env = DummyEnv::default();
840        let target = classify(&env, "foo");
841        assert_eq!(
842            target,
843            Target::External {
844                path: CString::default()
845            }
846        );
847    }
848
849    #[test]
850    fn special_builtin_is_found() {
851        let mut env = DummyEnv::default();
852        let builtin = Builtin::new(Special, |_, _| unreachable!());
853        env.builtins
854            .insert("foo", (builtin, Availability::Available));
855
856        assert_matches!(
857            search(&mut env, "foo"),
858            Ok(Target::Builtin { builtin: result, availability, path }) => {
859                assert_eq!(result.r#type, builtin.r#type);
860                assert_eq!(availability, Availability::Available);
861                assert_eq!(*path, *c"");
862            }
863        );
864        assert_matches!(
865            classify(&env, "foo"),
866            Target::Builtin { builtin: result, availability, path } => {
867                assert_eq!(result.r#type, builtin.r#type);
868                assert_eq!(availability, Availability::Available);
869                assert_eq!(*path, *c"");
870            }
871        );
872    }
873
874    #[test]
875    fn function_is_found_if_not_hidden_by_special_builtin() {
876        let mut env = DummyEnv::default();
877        let function = Rc::new(Function::new(
878            "foo",
879            function_body_stub(),
880            Location::dummy("location"),
881        ));
882        env.functions.define(function.clone()).unwrap();
883
884        assert_matches!(search(&mut env, "foo"), Ok(Target::Function(result)) => {
885            assert_eq!(result, function);
886        });
887        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
888            assert_eq!(result, function);
889        });
890    }
891
892    #[test]
893    fn special_builtin_takes_priority_over_function() {
894        let mut env = DummyEnv::default();
895        let builtin = Builtin::new(Special, |_, _| unreachable!());
896        env.builtins
897            .insert("foo", (builtin, Availability::Available));
898        let function = Function::new("foo", function_body_stub(), Location::dummy("location"));
899        env.functions.define(function).unwrap();
900
901        assert_matches!(
902            search(&mut env, "foo"),
903            Ok(Target::Builtin { builtin: result, availability, path }) => {
904                assert_eq!(result.r#type, builtin.r#type);
905                assert_eq!(availability, Availability::Available);
906                assert_eq!(*path, *c"");
907            }
908        );
909        assert_matches!(
910            classify(&env, "foo"),
911            Target::Builtin { builtin: result, availability, path } => {
912                assert_eq!(result.r#type, builtin.r#type);
913                assert_eq!(availability, Availability::Available);
914                assert_eq!(*path, *c"");
915            }
916        );
917    }
918
919    #[test]
920    fn mandatory_builtin_is_found_if_not_hidden_by_function() {
921        let mut env = DummyEnv::default();
922        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
923        env.builtins
924            .insert("foo", (builtin, Availability::Available));
925
926        assert_matches!(
927            search(&mut env, "foo"),
928            Ok(Target::Builtin { builtin: result, availability, path }) => {
929                assert_eq!(result.r#type, builtin.r#type);
930                assert_eq!(availability, Availability::Available);
931                assert_eq!(*path, *c"");
932            }
933        );
934        assert_matches!(
935            classify(&env, "foo"),
936            Target::Builtin { builtin: result, availability, path } => {
937                assert_eq!(result.r#type, builtin.r#type);
938                assert_eq!(availability, Availability::Available);
939                assert_eq!(*path, *c"");
940            }
941        );
942    }
943
944    #[test]
945    fn elective_builtin_is_found_if_not_hidden_by_function() {
946        let mut env = DummyEnv::default();
947        let builtin = Builtin::new(Elective, |_, _| unreachable!());
948        env.builtins
949            .insert("foo", (builtin, Availability::Available));
950
951        assert_matches!(
952            search(&mut env, "foo"),
953            Ok(Target::Builtin { builtin: result, availability, path }) => {
954                assert_eq!(result.r#type, builtin.r#type);
955                assert_eq!(availability, Availability::Available);
956                assert_eq!(*path, *c"");
957            }
958        );
959        assert_matches!(
960            classify(&env, "foo"),
961            Target::Builtin { builtin: result, availability, path } => {
962                assert_eq!(result.r#type, builtin.r#type);
963                assert_eq!(availability, Availability::Available);
964                assert_eq!(*path, *c"");
965            }
966        );
967    }
968
969    #[test]
970    fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
971        let mut env = DummyEnv::default();
972        let builtin = Builtin::new(Extension, |_, _| unreachable!());
973        env.builtins
974            .insert("foo", (builtin, Availability::Available));
975
976        assert_matches!(
977            search(&mut env, "foo"),
978            Ok(Target::Builtin { builtin: result, availability, path }) => {
979                assert_eq!(result.r#type, builtin.r#type);
980                assert_eq!(availability, Availability::Available);
981                assert_eq!(*path, *c"");
982            }
983        );
984        assert_matches!(
985            classify(&env, "foo"),
986            Target::Builtin { builtin: result, availability, path } => {
987                assert_eq!(result.r#type, builtin.r#type);
988                assert_eq!(availability, Availability::Available);
989                assert_eq!(*path, *c"");
990            }
991        );
992    }
993
994    #[test]
995    fn function_takes_priority_over_mandatory_builtin() {
996        let mut env = DummyEnv::default();
997        env.builtins.insert(
998            "foo",
999            (
1000                Builtin::new(Mandatory, |_, _| unreachable!()),
1001                Availability::Available,
1002            ),
1003        );
1004
1005        let function = Rc::new(Function::new(
1006            "foo",
1007            function_body_stub(),
1008            Location::dummy("location"),
1009        ));
1010        env.functions.define(function.clone()).unwrap();
1011
1012        assert_matches!(search(&mut env, "foo"), Ok(Target::Function(result)) => {
1013            assert_eq!(result, function);
1014        });
1015        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
1016            assert_eq!(result, function);
1017        });
1018    }
1019
1020    #[test]
1021    fn function_takes_priority_over_elective_builtin() {
1022        let mut env = DummyEnv::default();
1023        env.builtins.insert(
1024            "foo",
1025            (
1026                Builtin::new(Elective, |_, _| unreachable!()),
1027                Availability::Available,
1028            ),
1029        );
1030
1031        let function = Rc::new(Function::new(
1032            "foo",
1033            function_body_stub(),
1034            Location::dummy("location"),
1035        ));
1036        env.functions.define(function.clone()).unwrap();
1037
1038        assert_matches!(search(&mut env, "foo"), Ok(Target::Function(result)) => {
1039            assert_eq!(result, function);
1040        });
1041        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
1042            assert_eq!(result, function);
1043        });
1044    }
1045
1046    #[test]
1047    fn function_takes_priority_over_extension_builtin() {
1048        let mut env = DummyEnv::default();
1049        env.builtins.insert(
1050            "foo",
1051            (
1052                Builtin::new(Extension, |_, _| unreachable!()),
1053                Availability::Available,
1054            ),
1055        );
1056
1057        let function = Rc::new(Function::new(
1058            "foo",
1059            function_body_stub(),
1060            Location::dummy("location"),
1061        ));
1062        env.functions.define(function.clone()).unwrap();
1063
1064        assert_matches!(search(&mut env, "foo"), Ok(Target::Function(result)) => {
1065            assert_eq!(result, function);
1066        });
1067        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
1068            assert_eq!(result, function);
1069        });
1070    }
1071
1072    #[test]
1073    fn substitutive_builtin_is_found_if_external_executable_exists() {
1074        let mut env = DummyEnv::default();
1075        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
1076        env.builtins
1077            .insert("foo", (builtin, Availability::Available));
1078        env.path = Expansion::from("/bin");
1079        env.executables.insert("/bin/foo".to_string());
1080
1081        assert_matches!(
1082            search(&mut env, "foo"),
1083            Ok(Target::Builtin { builtin: result, availability, path }) => {
1084                assert_eq!(result.r#type, builtin.r#type);
1085                assert_eq!(availability, Availability::Available);
1086                assert_eq!(*path, *c"/bin/foo");
1087            }
1088        );
1089        assert_matches!(
1090            classify(&env, "foo"),
1091            Target::Builtin { builtin: result, availability, path } => {
1092                assert_eq!(result.r#type, builtin.r#type);
1093                assert_eq!(availability, Availability::Available);
1094                assert_eq!(*path, *c"");
1095            }
1096        );
1097    }
1098
1099    #[test]
1100    fn substitutive_builtin_is_unusable_without_external_executable() {
1101        let mut env = DummyEnv::default();
1102        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
1103        env.builtins
1104            .insert("foo", (builtin, Availability::Available));
1105
1106        let target = search(&mut env, "foo");
1107        assert_eq!(target, Err(Error::Unusable(Unusable::NotInPath)));
1108    }
1109
1110    #[test]
1111    fn builtin_rejected_by_options_is_unusable() {
1112        let mut env = DummyEnv::default();
1113        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
1114        env.builtins
1115            .insert("foo", (builtin, Availability::NotPortable));
1116
1117        let target = search(&mut env, "foo");
1118        assert_eq!(target, Err(Error::Unusable(Unusable::NotPortable)));
1119    }
1120
1121    #[test]
1122    fn builtin_rejected_by_options_is_still_classified() {
1123        // `classify` does not reject anything by itself, so the caller can
1124        // postpone the rejection until the built-in is about to be executed.
1125        let mut env = DummyEnv::default();
1126        let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
1127        env.builtins
1128            .insert("foo", (builtin, Availability::NotPortable));
1129
1130        assert_matches!(
1131            classify(&env, "foo"),
1132            Target::Builtin { builtin: result, availability, path } => {
1133                assert_eq!(result.r#type, builtin.r#type);
1134                assert_eq!(availability, Availability::NotPortable);
1135                assert_eq!(*path, *c"");
1136            }
1137        );
1138    }
1139
1140    #[test]
1141    fn substitutive_builtin_is_classified_even_without_external_executable() {
1142        let mut env = DummyEnv::default();
1143        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
1144        env.builtins
1145            .insert("foo", (builtin, Availability::Available));
1146
1147        assert_matches!(
1148            classify(&env, "foo"),
1149            Target::Builtin { builtin: result, availability, path } => {
1150                assert_eq!(result.r#type, builtin.r#type);
1151                assert_eq!(availability, Availability::Available);
1152                assert_eq!(*path, *c"");
1153            }
1154        );
1155    }
1156
1157    #[test]
1158    fn function_takes_priority_over_substitutive_builtin() {
1159        let mut env = DummyEnv::default();
1160        let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
1161        env.builtins
1162            .insert("foo", (builtin, Availability::Available));
1163        env.path = Expansion::from("/bin");
1164        env.executables.insert("/bin/foo".to_string());
1165
1166        let function = Rc::new(Function::new(
1167            "foo",
1168            function_body_stub(),
1169            Location::dummy("location"),
1170        ));
1171        env.functions.define(function.clone()).unwrap();
1172
1173        assert_matches!(search(&mut env, "foo"), Ok(Target::Function(result)) => {
1174            assert_eq!(result, function);
1175        });
1176        assert_matches!(classify(&env, "foo"), Target::Function(result) => {
1177            assert_eq!(result, function);
1178        });
1179    }
1180
1181    #[test]
1182    fn external_utility_is_found_if_external_executable_exists() {
1183        let mut env = DummyEnv::default();
1184        env.path = Expansion::from("/bin");
1185        env.executables.insert("/bin/foo".to_string());
1186
1187        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1188            assert_eq!(*path, *c"/bin/foo");
1189        });
1190        assert_matches!(classify(&env, "foo"), Target::External { path } => {
1191            assert_eq!(*path, *c"");
1192        });
1193    }
1194
1195    #[test]
1196    fn returns_external_utility_if_name_contains_slash() {
1197        // In this case, the external utility file does not have to exist.
1198        let mut env = DummyEnv::default();
1199        // The special built-in should be ignored because the command name
1200        // contains a slash.
1201        let builtin = Builtin::new(Special, |_, _| unreachable!());
1202        env.builtins
1203            .insert("bar/baz", (builtin, Availability::Available));
1204
1205        assert_matches!(search(&mut env, "bar/baz"), Ok(Target::External { path }) => {
1206            assert_eq!(*path, *c"bar/baz");
1207        });
1208        assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
1209            assert_eq!(*path, *c"");
1210        });
1211    }
1212
1213    #[test]
1214    fn external_target_is_first_executable_found_in_path_scalar() {
1215        let mut env = DummyEnv::default();
1216        env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
1217        env.executables.insert("/usr/bin/foo".to_string());
1218        env.executables.insert("/bin/foo".to_string());
1219
1220        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1221            assert_eq!(*path, *c"/usr/bin/foo");
1222        });
1223
1224        env.executables.insert("/usr/local/bin/foo".to_string());
1225
1226        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1227            assert_eq!(*path, *c"/usr/local/bin/foo");
1228        });
1229    }
1230
1231    #[test]
1232    fn external_target_is_first_executable_found_in_path_array() {
1233        let mut env = DummyEnv::default();
1234        env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
1235        env.executables.insert("/usr/bin/foo".to_string());
1236        env.executables.insert("/bin/foo".to_string());
1237
1238        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1239            assert_eq!(*path, *c"/usr/bin/foo");
1240        });
1241
1242        env.executables.insert("/usr/local/bin/foo".to_string());
1243
1244        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1245            assert_eq!(*path, *c"/usr/local/bin/foo");
1246        });
1247    }
1248
1249    #[test]
1250    fn empty_string_in_path_names_current_directory() {
1251        let mut env = DummyEnv::default();
1252        env.path = Expansion::from("/x::/y");
1253        env.executables.insert("foo".to_string());
1254
1255        assert_matches!(search(&mut env, "foo"), Ok(Target::External { path }) => {
1256            assert_eq!(*path, *c"foo");
1257        });
1258    }
1259}