Skip to main content

yash_env/
lib.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//! This crate defines the shell execution environment.
18//!
19//! A shell execution environment, [`Env`], is a collection of data that may
20//! affect or be affected by the execution of commands. The environment consists
21//! of application-managed parts and system-managed parts. Application-managed
22//! parts are implemented in pure Rust in this crate. Many application-managed
23//! parts like [function]s and [variable]s can be manipulated independently of
24//! interactions with the underlying system. System-managed parts, on the other
25//! hand, depend on the underlying system. Attributes like the working directory
26//! and umask are managed by the system to be accessed only by interaction with
27//! the system interface.
28//!
29//! Traits declared in the [`system`] module define the interface to the
30//! system-managed parts of the environment.
31//! [`RealSystem`] provides an implementation for them that interacts with
32//! the underlying system. [`VirtualSystem`] simulates a system for testing
33//! purposes.
34//!
35//! We assume that the shell process is single-threaded. This means that the
36//! shell process itself must not create threads, and all interactions with the
37//! system must be performed in the main thread. Using any items in this crate
38//! in a multi-threaded context is not supported and may cause undefined
39//! behavior. This prerequisite still applies even when a [`VirtualSystem`]
40//! simulates concurrent execution of multiple processes, which run as
41//! asynchronous tasks.
42
43use self::alias::AliasSet;
44use self::any::DataSet;
45use self::builtin::Builtin;
46use self::function::FunctionSet;
47use self::io::Fd;
48use self::job::JobList;
49use self::job::Pid;
50use self::job::ProcessResult;
51use self::job::ProcessState;
52use self::option::On;
53use self::option::OptionSet;
54use self::option::{AllExport, ErrExit, Interactive, Monitor};
55use self::semantics::Divert;
56use self::semantics::ExitStatus;
57use self::stack::Frame;
58use self::stack::Stack;
59use self::system::CaughtSignals;
60use self::system::Clock;
61use self::system::Close;
62use self::system::Concurrent;
63use self::system::Dup;
64use self::system::Errno;
65use self::system::Fstat;
66use self::system::GetCwd;
67use self::system::GetPid;
68use self::system::Isatty;
69use self::system::Mode;
70use self::system::OfdAccess;
71use self::system::Open;
72use self::system::OpenFlag;
73use self::system::Select;
74#[allow(deprecated)]
75pub use self::system::SharedSystem;
76use self::system::Sigaction;
77use self::system::Sigmask;
78use self::system::SignalList;
79use self::system::Signals;
80#[allow(deprecated)]
81pub use self::system::System;
82use self::system::TcSetPgrp;
83use self::system::Wait;
84#[cfg(unix)]
85pub use self::system::real::RealSystem;
86pub use self::system::r#virtual::VirtualSystem;
87use self::trap::TrapSet;
88use self::variable::PPID;
89use self::variable::Scope;
90use self::variable::VariableRefMut;
91use self::variable::VariableSet;
92use std::collections::HashMap;
93use std::fmt::Debug;
94use std::ops::ControlFlow::{self, Break, Continue};
95use std::rc::Rc;
96use std::task::Context;
97use std::task::Poll;
98use std::task::Waker;
99pub use unix_path as path;
100pub use unix_str as str;
101
102/// Whole shell execution environment.
103///
104/// The shell execution environment consists of application-managed parts and
105/// system-managed parts. Application-managed parts are directly implemented in
106/// the `Env` instance. System-managed parts are managed by a [`Concurrent`]
107/// wrapping a `System` instance.
108///
109/// # Cloning
110///
111/// `Env::clone` effectively clones the application-managed parts of the
112/// environment. Since [`Concurrent`] is reference-counted, you will not get a
113/// deep copy of the system-managed parts. See also
114/// [`clone_with_system`](Self::clone_with_system).
115#[derive(Clone, Debug)]
116#[non_exhaustive]
117pub struct Env<S> {
118    /// Aliases defined in the environment
119    pub aliases: AliasSet,
120
121    /// Name of the current shell executable or shell script
122    ///
123    /// Special parameter `0` expands to this value.
124    pub arg0: String,
125
126    /// Built-in utilities available in the environment
127    pub builtins: HashMap<&'static str, Builtin<S>>,
128
129    /// Exit status of the last executed command
130    pub exit_status: ExitStatus,
131
132    /// Functions defined in the environment
133    pub functions: FunctionSet<S>,
134
135    /// Jobs managed in the environment
136    pub jobs: JobList,
137
138    /// Process group ID of the main shell process
139    pub main_pgid: Pid,
140
141    /// Process ID of the main shell process
142    ///
143    /// This PID represents the value of the `$` special parameter.
144    pub main_pid: Pid,
145
146    /// Shell option settings
147    pub options: OptionSet,
148
149    /// Runtime execution context stack
150    pub stack: Stack,
151
152    /// Traps defined in the environment
153    pub traps: TrapSet,
154
155    /// File descriptor to the controlling terminal
156    ///
157    /// [`get_tty`](Self::get_tty) saves a file descriptor in this variable, so
158    /// you don't have to prepare it yourself.
159    pub tty: Option<Fd>,
160
161    /// Variables and positional parameters defined in the environment
162    pub variables: VariableSet,
163
164    /// Abstract container that can store any type-erased data
165    pub any: DataSet,
166
167    /// Interface to the system-managed parts of the environment
168    pub system: Rc<Concurrent<S>>,
169}
170
171impl<S> Env<S> {
172    /// Creates a new environment with the given system.
173    ///
174    /// Members of the new environments are default-constructed except that:
175    /// - `main_pgid` is initialized as `system.getpgrp()`
176    /// - `main_pid` is initialized as `system.getpid()`
177    /// - `system` is initialized as `Rc::new(Concurrent::new(system))`
178    #[must_use]
179    pub fn with_system(system: S) -> Self
180    where
181        S: GetPid,
182    {
183        Env {
184            aliases: Default::default(),
185            arg0: Default::default(),
186            builtins: Default::default(),
187            exit_status: Default::default(),
188            functions: Default::default(),
189            jobs: Default::default(),
190            main_pgid: system.getpgrp(),
191            main_pid: system.getpid(),
192            options: Default::default(),
193            stack: Default::default(),
194            traps: Default::default(),
195            tty: Default::default(),
196            variables: Default::default(),
197            any: Default::default(),
198            system: Rc::new(Concurrent::new(system)),
199        }
200    }
201
202    /// Clones this environment.
203    ///
204    /// The application-managed parts of the environment are cloned normally.
205    /// The system-managed parts are replaced with the provided `System`
206    /// instance.
207    #[must_use]
208    pub fn clone_with_system(&self, system: S) -> Self {
209        Env {
210            aliases: self.aliases.clone(),
211            arg0: self.arg0.clone(),
212            builtins: self.builtins.clone(),
213            exit_status: self.exit_status,
214            functions: self.functions.clone(),
215            jobs: self.jobs.clone(),
216            main_pgid: self.main_pgid,
217            main_pid: self.main_pid,
218            options: self.options,
219            stack: self.stack.clone(),
220            traps: self.traps.clone(),
221            tty: self.tty,
222            variables: self.variables.clone(),
223            any: self.any.clone(),
224            system: Rc::new(Concurrent::new(system)),
225        }
226    }
227}
228
229impl Env<VirtualSystem> {
230    /// Creates a new environment with a default-constructed [`VirtualSystem`].
231    #[must_use]
232    pub fn new_virtual() -> Self {
233        Env::with_system(VirtualSystem::default())
234    }
235}
236
237impl<S> Env<S> {
238    /// Initializes default variables.
239    ///
240    /// This function assigns the following variables to `self`:
241    ///
242    /// - `IFS=' \t\n'`
243    /// - `OPTIND=1`
244    /// - `PS1='$ '`
245    /// - `PS2='> '`
246    /// - `PS4='+ '`
247    /// - `PPID=(parent process ID)`
248    /// - `PWD=(current working directory)` (See [`Env::prepare_pwd`])
249    ///
250    /// This function ignores any errors that may occur.
251    ///
252    /// TODO: PS1 should be set to `"# "` for root users.
253    pub fn init_variables(&mut self)
254    where
255        S: Fstat + GetCwd + GetPid,
256    {
257        self.variables.init();
258
259        self.variables
260            .get_or_new(PPID, Scope::Global)
261            .assign(self.system.getppid().to_string(), None)
262            .ok();
263
264        self.prepare_pwd().ok();
265    }
266
267    /// Waits for some signals to be caught in the current process.
268    ///
269    /// Returns an array of signals caught.
270    ///
271    /// This function is a wrapper for [`Concurrent::wait_for_signals`].
272    /// Before the function returns, it passes the results to
273    /// [`TrapSet::catch_signal`] so the trap set can remember the signals
274    /// caught to be handled later.
275    pub async fn wait_for_signals(&mut self) -> Rc<SignalList> {
276        let result = self.system.wait_for_signals().await;
277        for signal in result.iter().copied() {
278            self.traps.catch_signal(signal);
279        }
280        result
281    }
282
283    /// Waits for a specific signal to be caught in the current process.
284    ///
285    /// This function calls [`wait_for_signals`](Self::wait_for_signals)
286    /// repeatedly until it returns results containing the specified `signal`.
287    pub async fn wait_for_signal(&mut self, signal: signal::Number) {
288        while !self.wait_for_signals().await.contains(&signal) {}
289    }
290
291    /// Returns signals that have been caught.
292    ///
293    /// This function is similar to
294    /// [`wait_for_signals`](Self::wait_for_signals) but does not wait for
295    /// signals to be caught. Instead, it only checks if any signals have been
296    /// caught but not yet consumed in the [`Concurrent`] instance. If no
297    /// signals have been caught, it returns `None`.
298    pub fn poll_signals(&mut self) -> Option<Rc<SignalList>>
299    where
300        S: Select + CaughtSignals + Clock,
301    {
302        let system = Rc::clone(&self.system);
303
304        let mut future = std::pin::pin!(self.wait_for_signals());
305        let mut context = Context::from_waker(Waker::noop());
306
307        // Check if the result is ready before peeking the system
308        if let Poll::Ready(signals) = future.as_mut().poll(&mut context) {
309            return Some(signals);
310        }
311
312        // Peek to handle any pending signals
313        system.peek();
314
315        // Check again if the result is ready after peeking
316        if let Poll::Ready(signals) = future.poll(&mut context) {
317            return Some(signals);
318        }
319
320        None
321    }
322
323    /// Whether error messages should be printed in color
324    ///
325    /// This function decides whether messages printed to the standard error
326    /// should contain ANSI color escape sequences. The result is true only if
327    /// the standard error is a terminal.
328    ///
329    /// The current implementation simply checks if the standard error is a
330    /// terminal. This will be changed in the future to support user
331    /// configuration.
332    #[must_use]
333    fn should_print_error_in_color(&self) -> bool
334    where
335        S: Isatty,
336    {
337        // TODO Enable color depending on user config (force/auto/never)
338        // TODO Check if the terminal really supports color (needs terminfo)
339        self.system.isatty(Fd::STDERR)
340    }
341
342    /// Returns a file descriptor to the controlling terminal.
343    ///
344    /// This function returns `self.tty` if it is `Some` FD. Otherwise, it
345    /// opens `/dev/tty` and saves the new FD to `self.tty` before returning it.
346    pub async fn get_tty(&mut self) -> Result<Fd, Errno>
347    where
348        S: Open + Dup + Close,
349    {
350        if let Some(fd) = self.tty {
351            return Ok(fd);
352        }
353
354        let first_fd = {
355            // POSIX.1-2024 Job control specifications are written in the
356            // assumption that a job-control shell may not have a control
357            // terminal. The shell should not make an arbitrary terminal its
358            // control terminal, so we open /dev/tty with NoCtty.
359            let mut result = self
360                .system
361                .open(
362                    c"/dev/tty",
363                    OfdAccess::ReadWrite,
364                    OpenFlag::CloseOnExec | OpenFlag::NoCtty,
365                    Mode::empty(),
366                )
367                .await;
368            if result == Err(Errno::EINVAL) {
369                // However, some systems do not support NoCtty. In that case,
370                // we open /dev/tty without NoCtty.
371                result = self
372                    .system
373                    .open(
374                        c"/dev/tty",
375                        OfdAccess::ReadWrite,
376                        OpenFlag::CloseOnExec.into(),
377                        Mode::empty(),
378                    )
379                    .await;
380            }
381            result?
382        };
383
384        let final_fd = io::move_fd_internal(&self.system, first_fd);
385        self.tty = final_fd.ok();
386        final_fd
387    }
388
389    /// Ensures the shell is in the foreground.
390    ///
391    /// If the current process belongs to the same process group as the session
392    /// leader, this function forces the current process to be in the foreground
393    /// by calling [`job::tcsetpgrp_with_block`]. Otherwise, this function
394    /// suspends the process until it is resumed in the foreground by another
395    /// job-controlling process (see [`job::tcsetpgrp_without_block`]).
396    ///
397    /// This function returns an error if the process does not have a controlling
398    /// terminal, that is, [`get_tty`](Self::get_tty) returns `Err(_)`.
399    ///
400    /// # Note on POSIX conformance
401    ///
402    /// This function implements part of the initialization of the job-control
403    /// shell. POSIX says that the shell should bring itself into the foreground
404    /// only if it is started as the controlling process (that is, the session
405    /// leader) for the terminal session. However, this function also brings the
406    /// shell into the foreground if the shell is in the same process group as
407    /// the session leader because it is unlikely that there is another
408    /// job-controlling process that can bring the shell into the foreground.
409    pub async fn ensure_foreground(&mut self) -> Result<(), Errno>
410    where
411        S: Open + Dup + Close + GetPid + Signals + Sigmask + Sigaction + TcSetPgrp,
412    {
413        let fd = self.get_tty().await?;
414
415        if self.system.getsid(Pid(0)) == Ok(self.main_pgid) {
416            job::tcsetpgrp_with_block(&self.system, fd, self.main_pgid).await
417        } else {
418            job::tcsetpgrp_without_block(&self.system, fd, self.main_pgid).await
419        }
420    }
421
422    /// Waits for a subshell to terminate, suspend, or resume.
423    ///
424    /// This function waits for a subshell to change its execution state. The
425    /// `target` parameter specifies which child to wait for:
426    ///
427    /// - `-1`: any child
428    /// - `0`: any child in the same process group as the current process
429    /// - `pid`: the child whose process ID is `pid`
430    /// - `-pgid`: any child in the process group whose process group ID is `pgid`
431    ///
432    /// When [`self.system.wait`](system::Wait::wait) returned a new state of the
433    /// target, it is sent to `self.jobs` ([`JobList::update_status`]) before
434    /// being returned from this function.
435    ///
436    /// If there is no matching target, this function returns
437    /// `Err(Errno::ECHILD)`.
438    ///
439    /// If the target subshell is not job-controlled, you may want to use
440    /// [`wait_for_subshell_to_finish`](Self::wait_for_subshell_to_finish)
441    /// instead.
442    pub async fn wait_for_subshell(&mut self, target: Pid) -> Result<(Pid, ProcessState), Errno>
443    where
444        S: Signals + Sigmask + Sigaction + Wait,
445    {
446        // We need to set the internal disposition before calling `wait` so we don't
447        // miss any `SIGCHLD` that may arrive between `wait` and `wait_for_signal`.
448        self.traps
449            .enable_internal_disposition_for_sigchld(&self.system)
450            .await?;
451
452        loop {
453            if let Some((pid, state)) = self.system.wait(target)? {
454                self.jobs.update_status(pid, state);
455                return Ok((pid, state));
456            }
457            self.wait_for_signal(S::SIGCHLD).await;
458        }
459    }
460
461    /// Wait for a subshell to terminate or suspend.
462    ///
463    /// This function is similar to
464    /// [`wait_for_subshell`](Self::wait_for_subshell), but returns only when
465    /// the target is finished (either exited or killed by a signal) or
466    /// suspended.
467    pub async fn wait_for_subshell_to_halt(
468        &mut self,
469        target: Pid,
470    ) -> Result<(Pid, ProcessResult), Errno>
471    where
472        S: Signals + Sigmask + Sigaction + Wait,
473    {
474        loop {
475            let (pid, state) = self.wait_for_subshell(target).await?;
476            if let ProcessState::Halted(result) = state {
477                return Ok((pid, result));
478            }
479        }
480    }
481
482    /// Wait for a subshell to terminate.
483    ///
484    /// This function is similar to
485    /// [`wait_for_subshell`](Self::wait_for_subshell), but returns only when
486    /// the target is finished (either exited or killed by a signal).
487    ///
488    /// Returns the process ID of the awaited process and its exit status.
489    pub async fn wait_for_subshell_to_finish(
490        &mut self,
491        target: Pid,
492    ) -> Result<(Pid, ExitStatus), Errno>
493    where
494        S: Signals + Sigmask + Sigaction + Wait,
495    {
496        loop {
497            let (pid, result) = self.wait_for_subshell_to_halt(target).await?;
498            if !result.is_stopped() {
499                return Ok((pid, result.into()));
500            }
501        }
502    }
503
504    /// Applies all job status updates to jobs in `self.jobs`.
505    ///
506    /// This function calls [`self.system.wait`](system::Wait::wait) repeatedly until
507    /// all status updates available are applied to `self.jobs`
508    /// ([`JobList::update_status`]).
509    ///
510    /// Note that updates of subshells that are not managed in `self.jobs` are
511    /// lost when you call this function.
512    pub fn update_all_subshell_statuses(&mut self)
513    where
514        S: Wait,
515    {
516        while let Ok(Some((pid, state))) = self.system.wait(Pid::ALL) {
517            self.jobs.update_status(pid, state);
518        }
519    }
520}
521
522impl<S> Env<S> {
523    /// Tests whether the current environment is an interactive shell.
524    ///
525    /// This function returns true if and only if:
526    ///
527    /// - the [`Interactive`] option is `On` in `self.options`, and
528    /// - the current context is not in a subshell (no `Frame::Subshell` in `self.stack`).
529    #[must_use]
530    pub fn is_interactive(&self) -> bool {
531        self.options.get(Interactive) == On && !self.stack.contains(&Frame::Subshell)
532    }
533
534    /// Tests whether the shell is performing job control.
535    ///
536    /// This function returns true if and only if:
537    ///
538    /// - the [`Monitor`] option is `On` in `self.options`, and
539    /// - the current context is not in a subshell (no `Frame::Subshell` in `self.stack`).
540    #[must_use]
541    pub fn controls_jobs(&self) -> bool {
542        self.options.get(Monitor) == On && !self.stack.contains(&Frame::Subshell)
543    }
544
545    /// Get an existing variable or create a new one.
546    ///
547    /// This method is a thin wrapper around [`VariableSet::get_or_new`].
548    /// If the [`AllExport`] option is on, the variable is
549    /// [exported](VariableRefMut::export) before being returned from the
550    /// method.
551    ///
552    /// You should prefer using this method over [`VariableSet::get_or_new`] to
553    /// make sure that the [`AllExport`] option is applied.
554    pub fn get_or_create_variable<N>(&mut self, name: N, scope: Scope) -> VariableRefMut<'_>
555    where
556        N: Into<String>,
557    {
558        let mut variable = self.variables.get_or_new(name, scope);
559        if self.options.get(AllExport) == On {
560            variable.export(true);
561        }
562        variable
563    }
564
565    /// Tests whether the [`ErrExit`] option is applicable in the current context.
566    ///
567    /// This function returns true if and only if:
568    /// - the [`ErrExit`] option is on, and
569    /// - the current stack has no [`Condition`] frame.
570    ///
571    /// [`Condition`]: Frame::Condition
572    pub fn errexit_is_applicable(&self) -> bool {
573        self.options.get(ErrExit) == On && !self.stack.contains(&Frame::Condition)
574    }
575
576    /// Returns a `Divert` if the shell should exit because of the [`ErrExit`]
577    /// shell option.
578    ///
579    /// The function returns `Break(Divert::Exit(None))` if the [`errexit`
580    /// option is applicable](Self::errexit_is_applicable) and the current
581    /// `self.exit_status` is non-zero. Otherwise, it returns `Continue(())`.
582    pub fn apply_errexit(&self) -> ControlFlow<Divert> {
583        if !self.exit_status.is_successful() && self.errexit_is_applicable() {
584            Break(Divert::Exit(None))
585        } else {
586            Continue(())
587        }
588    }
589
590    /// Updates the exit status from the given result.
591    ///
592    /// If `result` is a `Break(divert)` where `divert.exit_status()` is `Some`
593    /// exit status, this function sets `self.exit_status` to that exit status.
594    pub fn apply_result(&mut self, result: crate::semantics::Result) {
595        match result {
596            Continue(_) => {}
597            Break(divert) => {
598                if let Some(exit_status) = divert.exit_status() {
599                    self.exit_status = exit_status;
600                }
601            }
602        }
603    }
604}
605
606pub mod alias;
607pub mod any;
608pub mod builtin;
609pub mod decl_util;
610pub mod function;
611pub mod input;
612pub mod io;
613pub mod job;
614pub mod option;
615pub mod parser;
616pub mod prompt;
617pub mod pwd;
618pub mod semantics;
619pub mod signal;
620pub mod source;
621pub mod stack;
622pub mod subshell;
623pub mod system;
624pub mod trap;
625pub mod variable;
626pub mod waker;
627
628#[cfg(any(test, feature = "yash-executor"))]
629mod executor_helper;
630#[cfg(any(test, feature = "test-helper"))]
631pub mod test_helper;
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636    use crate::io::MIN_INTERNAL_FD;
637    use crate::job::Job;
638    use crate::source::Location;
639    use crate::subshell::Subshell;
640    use crate::system::r#virtual::Inode;
641    use crate::system::r#virtual::SIGCHLD;
642    use crate::test_helper::in_virtual_system;
643    use crate::trap::Action;
644    use futures_executor::LocalPool;
645    use futures_util::FutureExt as _;
646    use std::cell::RefCell;
647
648    #[test]
649    fn wait_for_signal_remembers_signal_in_trap_set() {
650        in_virtual_system(|mut env, state| async move {
651            env.traps
652                .set_action(
653                    &env.system,
654                    SIGCHLD,
655                    Action::Command("".into()),
656                    Location::dummy(""),
657                    false,
658                )
659                .await
660                .unwrap();
661            {
662                let mut state = state.borrow_mut();
663                let process = state.processes.get_mut(&env.main_pid).unwrap();
664                assert!(process.blocked_signals().contains(&SIGCHLD));
665                let _ = process.raise_signal(SIGCHLD);
666            }
667            env.wait_for_signal(SIGCHLD).await;
668
669            let trap_state = env.traps.get_state(SIGCHLD).0.unwrap();
670            assert!(trap_state.pending);
671        })
672    }
673
674    fn poll_signals_env() -> (Env<VirtualSystem>, VirtualSystem) {
675        let system = VirtualSystem::new();
676        let mut env = Env::with_system(system.clone());
677        env.traps
678            .set_action(
679                &env.system,
680                SIGCHLD,
681                Action::Command("".into()),
682                Location::dummy(""),
683                false,
684            )
685            .now_or_never()
686            .unwrap()
687            .unwrap();
688        (env, system)
689    }
690
691    #[test]
692    fn poll_signals_none() {
693        let mut env = poll_signals_env().0;
694        let result = env.poll_signals();
695        assert_eq!(result, None);
696    }
697
698    #[test]
699    fn poll_signals_some() {
700        let (mut env, system) = poll_signals_env();
701        {
702            let mut state = system.state.borrow_mut();
703            let process = state.processes.get_mut(&system.process_id).unwrap();
704            assert!(process.blocked_signals().contains(&SIGCHLD));
705            let _ = process.raise_signal(SIGCHLD);
706        }
707
708        let result = env.poll_signals().unwrap();
709        assert_eq!(result.as_slice(), [SIGCHLD]);
710    }
711
712    #[test]
713    fn get_tty_opens_tty() {
714        let system = VirtualSystem::new();
715        let tty = Rc::new(RefCell::new(Inode::new([])));
716        system
717            .state
718            .borrow_mut()
719            .file_system
720            .save("/dev/tty", Rc::clone(&tty))
721            .unwrap();
722        let mut env = Env::with_system(system.clone());
723
724        let fd = env.get_tty().now_or_never().unwrap().unwrap();
725        assert!(
726            fd >= MIN_INTERNAL_FD,
727            "get_tty returned {fd}, which should be >= {MIN_INTERNAL_FD}"
728        );
729        system
730            .with_open_file_description(fd, |ofd| {
731                assert!(Rc::ptr_eq(ofd.inode(), &tty));
732                Ok(())
733            })
734            .unwrap();
735
736        system.state.borrow_mut().file_system = Default::default();
737
738        // get_tty returns cached FD
739        let fd = env.get_tty().now_or_never().unwrap().unwrap();
740        system
741            .with_open_file_description(fd, |ofd| {
742                assert!(Rc::ptr_eq(ofd.inode(), &tty));
743                Ok(())
744            })
745            .unwrap();
746    }
747
748    #[test]
749    fn start_and_wait_for_subshell() {
750        in_virtual_system(|mut env, _state| async move {
751            let subshell = Subshell::new(|env, _job_control| {
752                Box::pin(async { env.exit_status = ExitStatus(42) })
753            });
754            let (pid, _) = subshell.start(&mut env).await.unwrap();
755            let result = env.wait_for_subshell(pid).await;
756            assert_eq!(result, Ok((pid, ProcessState::exited(42))));
757        });
758    }
759
760    #[test]
761    fn start_and_wait_for_subshell_with_job_list() {
762        in_virtual_system(|mut env, _state| async move {
763            let subshell = Subshell::new(|env, _job_control| {
764                Box::pin(async { env.exit_status = ExitStatus(42) })
765            });
766            let (pid, _) = subshell.start(&mut env).await.unwrap();
767            let mut job = Job::new(pid);
768            job.name = "my job".to_string();
769            let job_index = env.jobs.add(job.clone());
770            let result = env.wait_for_subshell(pid).await;
771            assert_eq!(result, Ok((pid, ProcessState::exited(42))));
772            job.state = ProcessState::exited(42);
773            assert_eq!(env.jobs[job_index], job);
774        });
775    }
776
777    #[test]
778    fn wait_for_subshell_no_subshell() {
779        let system = VirtualSystem::new();
780        let mut executor = LocalPool::new();
781        system.state.borrow_mut().executor = Some(Rc::new(executor.spawner()));
782        let mut env = Env::with_system(system);
783        executor.run_until(async move {
784            let result = env.wait_for_subshell(Pid::ALL).await;
785            assert_eq!(result, Err(Errno::ECHILD));
786        });
787    }
788
789    #[test]
790    fn update_all_subshell_statuses_without_subshells() {
791        let mut env = Env::new_virtual();
792        env.update_all_subshell_statuses();
793    }
794
795    #[test]
796    fn update_all_subshell_statuses_with_subshells() {
797        let system = VirtualSystem::new();
798        let mut executor = futures_executor::LocalPool::new();
799        system.state.borrow_mut().executor = Some(Rc::new(executor.spawner()));
800
801        let mut env = Env::with_system(system);
802
803        let [job_1, job_2, job_3] = executor.run_until(async {
804            // Run a subshell.
805            let subshell_1 = Subshell::new(|env, _job_control| {
806                Box::pin(async { env.exit_status = ExitStatus(12) })
807            });
808            let (pid_1, _) = subshell_1.start(&mut env).await.unwrap();
809
810            // Run another subshell.
811            let subshell_2 = Subshell::new(|env, _job_control| {
812                Box::pin(async { env.exit_status = ExitStatus(35) })
813            });
814            let (pid_2, _) = subshell_2.start(&mut env).await.unwrap();
815
816            // This one will never finish.
817            let subshell_3 =
818                Subshell::new(|_env, _job_control| Box::pin(futures_util::future::pending()));
819            let (pid_3, _) = subshell_3.start(&mut env).await.unwrap();
820
821            // Yet another subshell. We don't make this into a job.
822            let subshell_4 = Subshell::new(|env, _job_control| {
823                Box::pin(async { env.exit_status = ExitStatus(100) })
824            });
825            let (_pid_4, _) = subshell_4.start(&mut env).await.unwrap();
826
827            // Create jobs.
828            let job_1 = env.jobs.add(Job::new(pid_1));
829            let job_2 = env.jobs.add(Job::new(pid_2));
830            let job_3 = env.jobs.add(Job::new(pid_3));
831            [job_1, job_2, job_3]
832        });
833
834        // Let the jobs (except job_3) finish.
835        executor.run_until_stalled();
836
837        // We're not yet updated.
838        assert_eq!(env.jobs[job_1].state, ProcessState::Running);
839        assert_eq!(env.jobs[job_2].state, ProcessState::Running);
840        assert_eq!(env.jobs[job_3].state, ProcessState::Running);
841
842        env.update_all_subshell_statuses();
843
844        // Now we have the results.
845        // TODO assert_eq!(env.jobs[job_1].state, ProcessState::Exited(ExitStatus(12)));
846        // TODO assert_eq!(env.jobs[job_2].state, ProcessState::Exited(ExitStatus(35)));
847        assert_eq!(env.jobs[job_3].state, ProcessState::Running);
848    }
849
850    #[test]
851    fn get_or_create_variable_with_all_export_off() {
852        let mut env = Env::new_virtual();
853        let mut a = env.get_or_create_variable("a", Scope::Global);
854        assert!(!a.is_exported);
855        a.export(true);
856        let a = env.get_or_create_variable("a", Scope::Global);
857        assert!(a.is_exported);
858    }
859
860    #[test]
861    fn get_or_create_variable_with_all_export_on() {
862        let mut env = Env::new_virtual();
863        env.options.set(AllExport, On);
864        let mut a = env.get_or_create_variable("a", Scope::Global);
865        assert!(a.is_exported);
866        a.export(false);
867        let a = env.get_or_create_variable("a", Scope::Global);
868        assert!(a.is_exported);
869    }
870
871    #[test]
872    fn errexit_on() {
873        let mut env = Env::new_virtual();
874        env.exit_status = ExitStatus::FAILURE;
875        env.options.set(ErrExit, On);
876        assert_eq!(env.apply_errexit(), Break(Divert::Exit(None)));
877    }
878
879    #[test]
880    fn errexit_with_zero_exit_status() {
881        let mut env = Env::new_virtual();
882        env.options.set(ErrExit, On);
883        assert_eq!(env.apply_errexit(), Continue(()));
884    }
885
886    #[test]
887    fn errexit_in_condition() {
888        let mut env = Env::new_virtual();
889        env.exit_status = ExitStatus::FAILURE;
890        env.options.set(ErrExit, On);
891        let env = env.push_frame(Frame::Condition);
892        assert_eq!(env.apply_errexit(), Continue(()));
893    }
894
895    #[test]
896    fn errexit_off() {
897        let mut env = Env::new_virtual();
898        env.exit_status = ExitStatus::FAILURE;
899        assert_eq!(env.apply_errexit(), Continue(()));
900    }
901
902    #[test]
903    fn apply_result_with_continue() {
904        let mut env = Env::new_virtual();
905        env.apply_result(Continue(()));
906        assert_eq!(env.exit_status, ExitStatus::default());
907    }
908
909    #[test]
910    fn apply_result_with_divert_without_exit_status() {
911        let mut env = Env::new_virtual();
912        env.apply_result(Break(Divert::Exit(None)));
913        assert_eq!(env.exit_status, ExitStatus::default());
914    }
915
916    #[test]
917    fn apply_result_with_divert_with_exit_status() {
918        let mut env = Env::new_virtual();
919        env.apply_result(Break(Divert::Exit(Some(ExitStatus(67)))));
920        assert_eq!(env.exit_status, ExitStatus(67));
921    }
922}