Skip to main content

yash_semantics/command/
pipeline.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//! Implementation of pipeline semantics.
18
19use super::Command;
20use crate::Runtime;
21use crate::trap::run_exit_trap;
22use enumset::EnumSet;
23use itertools::Itertools as _;
24use std::ops::ControlFlow::{Break, Continue};
25use std::rc::Rc;
26use yash_env::Env;
27use yash_env::io::Fd;
28use yash_env::job::{Pid, handle_job_status};
29use yash_env::option::Option::{Exec, Interactive, PipeFail};
30use yash_env::option::State::{Off, On};
31use yash_env::semantics::Divert;
32use yash_env::semantics::ExitStatus;
33use yash_env::semantics::Result;
34use yash_env::stack::Frame;
35use yash_env::subshell::Config;
36use yash_env::subshell::JobControl;
37use yash_env::system::concurrency::WriteAll;
38use yash_env::system::{Close, Dup, Errno, Isatty, Pipe};
39use yash_syntax::syntax;
40
41/// Executes the pipeline.
42///
43/// # Executing commands
44///
45/// If this pipeline contains one command, it is executed in the current shell
46/// execution environment.
47///
48/// If the pipeline has more than one command, all the commands are executed
49/// concurrently. Every command is executed in a new subshell. The standard
50/// output of a command is connected to the standard input of the next command
51/// via a pipe, except for the standard output of the last command and the
52/// standard input of the first command, which are not modified.
53///
54/// If the pipeline has no command, it is a no-op.
55///
56/// # Exit status
57///
58/// The exit status of the pipeline is that of the last command (or zero if no
59/// command). If the pipeline starts with an `!`, the exit status is inverted:
60/// zero becomes one, and non-zero becomes zero.
61///
62/// In POSIX, the expected exit status is unclear when an inverted pipeline
63/// performs a jump as in `! return 42`. The behavior disagrees among existing
64/// shells. This implementation does not invert the exit status when the return
65/// value is `Err(Divert::...)`.
66///
67/// # `noexec` option
68///
69/// If the [`Exec`] and [`Interactive`] options are [`Off`] in `env.options`,
70/// the entire execution of the pipeline is skipped. (The `noexec` option is
71/// ignored if the shell is interactive, otherwise you cannot exit the shell
72/// in any way if the `ignoreeof` option is set.)
73///
74/// # Stack
75///
76/// if `self.negation` is true, [`Frame::Condition`] is pushed to the
77/// environment's stack while the pipeline is executed.
78impl<S: Runtime + 'static> Command<S> for syntax::Pipeline {
79    async fn execute(&self, env: &mut Env<S>) -> Result {
80        if env.options.get(Exec) == Off && env.options.get(Interactive) == Off {
81            return Continue(());
82        }
83
84        if !self.negation {
85            return execute_commands_in_pipeline(env, &self.commands).await;
86        }
87
88        let mut env = env.push_frame(Frame::Condition);
89        execute_commands_in_pipeline(&mut env, &self.commands).await?;
90        env.exit_status = if env.exit_status.is_successful() {
91            ExitStatus::FAILURE
92        } else {
93            ExitStatus::SUCCESS
94        };
95        Continue(())
96    }
97}
98
99async fn execute_commands_in_pipeline<S: Runtime + 'static>(
100    env: &mut Env<S>,
101    commands: &[Rc<syntax::Command>],
102) -> Result {
103    match commands.len() {
104        0 => {
105            env.exit_status = ExitStatus::SUCCESS;
106            Continue(())
107        }
108
109        1 => commands[0].execute(env).await,
110
111        _ => {
112            if env.controls_jobs() {
113                execute_job_controlled_pipeline(env, commands).await?
114            } else {
115                execute_multi_command_pipeline(env, commands).await?
116            }
117            env.apply_errexit()
118        }
119    }
120}
121
122async fn execute_job_controlled_pipeline<S: Runtime + 'static>(
123    env: &mut Env<S>,
124    commands: &[Rc<syntax::Command>],
125) -> Result {
126    let commands_2 = commands.to_vec();
127    let subshell = Config::foreground().start_and_wait(env, async move |sub_env, _job_control| {
128        let result = execute_multi_command_pipeline(sub_env, &commands_2).await;
129        sub_env.apply_result(result);
130        run_exit_trap(sub_env).await;
131    });
132    match subshell.await {
133        Ok((pid, result)) => {
134            env.exit_status = handle_job_status(env, pid, result, || to_job_name(commands))?;
135            Continue(())
136        }
137        Err(errno) => {
138            // TODO print error location using yash_env::io::print_error
139            let message = format!("cannot start a subshell in the pipeline: {errno}\n");
140            env.system.print_error(&message).await;
141            Break(Divert::Interrupt(Some(ExitStatus::NOEXEC)))
142        }
143    }
144}
145
146fn to_job_name(commands: &[Rc<syntax::Command>]) -> String {
147    commands
148        .iter()
149        .format_with(" | ", |cmd, f| f(&format_args!("{cmd}")))
150        .to_string()
151}
152
153async fn execute_multi_command_pipeline<S: Runtime + 'static>(
154    env: &mut Env<S>,
155    commands: &[Rc<syntax::Command>],
156) -> Result {
157    // Start commands
158    let mut commands = commands.iter().cloned();
159    let mut pipes = PipeSet::new();
160    let mut pids = Vec::new();
161    while let Some(command) = commands.next() {
162        let has_next = commands.len() > 0; // TODO ExactSizeIterator::is_empty
163        shift_or_fail(env, &mut pipes, has_next).await?;
164
165        let pipes = pipes;
166        let start_result = Config::new()
167            .start(env, async move |env, _job_control| {
168                let result = connect_pipe_and_execute_command(env, pipes, command).await;
169                env.apply_result(result);
170                run_exit_trap(env).await;
171            })
172            .await;
173        pids.push(pid_or_fail(env, start_result).await?);
174    }
175
176    shift_or_fail(env, &mut pipes, false).await?;
177
178    // Wait for all commands to finish, collecting the exit statuses
179    let mut final_exit_status = ExitStatus::SUCCESS;
180    let pipefail = env.options.get(PipeFail) == On;
181    for pid in pids {
182        let exit_status = env
183            .wait_for_subshell_to_finish(pid)
184            .await
185            .expect("cannot receive exit status of child process")
186            .1;
187        if !exit_status.is_successful() || !pipefail {
188            final_exit_status = exit_status;
189        }
190    }
191    env.exit_status = final_exit_status;
192
193    Continue(())
194}
195
196async fn shift_or_fail<S>(env: &mut Env<S>, pipes: &mut PipeSet, has_next: bool) -> Result
197where
198    S: Close + Isatty + Pipe + WriteAll,
199{
200    match pipes.shift(env, has_next) {
201        Ok(()) => Continue(()),
202        Err(errno) => {
203            // TODO print error location using yash_env::io::print_error
204            let message = format!("cannot connect pipes in the pipeline: {errno}\n");
205            env.system.print_error(&message).await;
206            Break(Divert::Interrupt(Some(ExitStatus::NOEXEC)))
207        }
208    }
209}
210
211async fn connect_pipe_and_execute_command<S: Runtime + 'static>(
212    env: &mut Env<S>,
213    pipes: PipeSet,
214    command: Rc<syntax::Command>,
215) -> Result {
216    match pipes.move_to_stdin_stdout(env) {
217        Ok(()) => (),
218        Err(errno) => {
219            // TODO print error location using yash_env::io::print_error
220            let message = format!("cannot connect pipes in the pipeline: {errno}\n");
221            env.system.print_error(&message).await;
222            return Break(Divert::Interrupt(Some(ExitStatus::NOEXEC)));
223        }
224    }
225
226    command.execute(env).await
227}
228
229async fn pid_or_fail<S>(
230    env: &mut Env<S>,
231    start_result: std::result::Result<(Pid, Option<JobControl>), Errno>,
232) -> Result<Pid>
233where
234    S: Isatty + WriteAll,
235{
236    match start_result {
237        Ok((pid, job_control)) => {
238            debug_assert_eq!(job_control, None);
239            Continue(pid)
240        }
241        Err(errno) => {
242            // TODO print error location using yash_env::io::print_error
243            env.system
244                .print_error(&format!(
245                    "cannot start a subshell in the pipeline: {errno}\n"
246                ))
247                .await;
248            Break(Divert::Interrupt(Some(ExitStatus::NOEXEC)))
249        }
250    }
251}
252
253/// Set of pipe file descriptors that connect commands.
254#[derive(Clone, Copy, Default)]
255struct PipeSet {
256    read_previous: Option<Fd>,
257    /// Reader and writer to the next command.
258    next: Option<(Fd, Fd)>,
259}
260
261impl PipeSet {
262    fn new() -> Self {
263        Self::default()
264    }
265
266    /// Updates the pipe set for the next command.
267    ///
268    /// Closes FDs that are no longer necessary and opens a new pipe if there is
269    /// a next command.
270    fn shift<S: Pipe + Close>(
271        &mut self,
272        env: &mut Env<S>,
273        has_next: bool,
274    ) -> std::result::Result<(), Errno> {
275        if let Some(fd) = self.read_previous {
276            let _ = env.system.close(fd);
277        }
278
279        if let Some((reader, writer)) = self.next {
280            let _ = env.system.close(writer);
281            self.read_previous = Some(reader);
282        } else {
283            self.read_previous = None;
284        }
285
286        self.next = None;
287        if has_next {
288            self.next = Some(env.system.pipe()?);
289        }
290
291        Ok(())
292    }
293
294    /// Moves the pipe FDs to stdin/stdout and closes the FDs that are no longer
295    /// necessary.
296    fn move_to_stdin_stdout<S: Dup + Close>(
297        mut self,
298        env: &mut Env<S>,
299    ) -> std::result::Result<(), Errno> {
300        if let Some((reader, writer)) = self.next {
301            assert_ne!(reader, writer);
302            assert_ne!(self.read_previous, Some(reader));
303            assert_ne!(self.read_previous, Some(writer));
304
305            env.system.close(reader)?;
306            if writer != Fd::STDOUT {
307                if self.read_previous == Some(Fd::STDOUT) {
308                    self.read_previous =
309                        Some(env.system.dup(Fd::STDOUT, Fd(0), EnumSet::empty())?);
310                }
311                env.system.dup2(writer, Fd::STDOUT)?;
312                env.system.close(writer)?;
313            }
314        }
315        if let Some(reader) = self.read_previous
316            && reader != Fd::STDIN
317        {
318            env.system.dup2(reader, Fd::STDIN)?;
319            env.system.close(reader)?;
320        }
321        Ok(())
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::tests::cat_builtin;
329    use crate::tests::return_builtin;
330    use crate::tests::suspend_builtin;
331    use futures_util::FutureExt as _;
332    use std::assert_matches;
333    use std::pin::Pin;
334    use std::rc::Rc;
335    use yash_env::VirtualSystem;
336    use yash_env::builtin::Builtin;
337    use yash_env::builtin::Type::Special;
338    use yash_env::job::ProcessResult;
339    use yash_env::job::ProcessState;
340    use yash_env::option::Option::{ErrExit, Monitor};
341    use yash_env::semantics::Field;
342    use yash_env::system::Concurrent;
343    use yash_env::system::GetPid as _;
344    use yash_env::system::r#virtual::FileBody;
345    use yash_env::system::r#virtual::SIGSTOP;
346    use yash_env::test_helper::assert_stdout;
347    use yash_env::test_helper::in_virtual_system;
348    use yash_env::test_helper::stub_tty;
349
350    #[test]
351    fn empty_pipeline() {
352        let mut env = Env::new_virtual();
353        let pipeline = syntax::Pipeline {
354            commands: vec![],
355            negation: false,
356        };
357        let result = pipeline.execute(&mut env).now_or_never().unwrap();
358        assert_eq!(result, Continue(()));
359        assert_eq!(env.exit_status, ExitStatus(0));
360    }
361
362    #[test]
363    fn single_command_pipeline_returns_exit_status_intact_without_divert() {
364        let mut env = Env::new_virtual();
365        env.builtins.insert("return", return_builtin());
366        let pipeline: syntax::Pipeline = "return -n 93".parse().unwrap();
367        let result = pipeline.execute(&mut env).now_or_never().unwrap();
368        assert_eq!(result, Continue(()));
369        assert_eq!(env.exit_status, ExitStatus(93));
370    }
371
372    #[test]
373    fn single_command_pipeline_returns_exit_status_intact_with_divert() {
374        let mut env = Env::new_virtual();
375        env.builtins.insert("return", return_builtin());
376        env.exit_status = ExitStatus(17);
377        let pipeline: syntax::Pipeline = "return 37".parse().unwrap();
378        let result = pipeline.execute(&mut env).now_or_never().unwrap();
379        assert_eq!(result, Break(Divert::Return(Some(ExitStatus(37)))));
380        assert_eq!(env.exit_status, ExitStatus(17));
381    }
382
383    #[test]
384    fn multi_command_pipeline_without_pipefail_returns_last_command_exit_status() {
385        in_virtual_system(|mut env, _state| async move {
386            env.builtins.insert("return", return_builtin());
387            env.options.set(PipeFail, Off);
388
389            let pipeline: syntax::Pipeline = "return -n 0 | return -n 0".parse().unwrap();
390            let result = pipeline.execute(&mut env).await;
391            assert_eq!(result, Continue(()));
392            assert_eq!(env.exit_status, ExitStatus(0));
393
394            let pipeline: syntax::Pipeline = "return -n 10 | return -n 20".parse().unwrap();
395            let result = pipeline.execute(&mut env).await;
396            assert_eq!(result, Continue(()));
397            assert_eq!(env.exit_status, ExitStatus(20));
398
399            let pipeline: syntax::Pipeline = "return -n 0 | return -n 20 | return -n 0 |\
400                return -n 30 | return -n 0 | return -n 0"
401                .parse()
402                .unwrap();
403            let result = pipeline.execute(&mut env).await;
404            assert_eq!(result, Continue(()));
405            assert_eq!(env.exit_status, ExitStatus(0));
406        });
407    }
408
409    #[test]
410    fn multi_command_pipeline_with_pipefail_returns_last_failed_command_exit_status() {
411        in_virtual_system(|mut env, _state| async move {
412            env.builtins.insert("return", return_builtin());
413            env.options.set(PipeFail, On);
414
415            let pipeline: syntax::Pipeline = "return -n 0 | return -n 0".parse().unwrap();
416            let result = pipeline.execute(&mut env).await;
417            assert_eq!(result, Continue(()));
418            assert_eq!(env.exit_status, ExitStatus(0));
419
420            let pipeline: syntax::Pipeline = "return -n 10 | return -n 20".parse().unwrap();
421            let result = pipeline.execute(&mut env).await;
422            assert_eq!(result, Continue(()));
423            assert_eq!(env.exit_status, ExitStatus(20));
424
425            let pipeline: syntax::Pipeline = "return -n 0 | return -n 20 | return -n 0 |\
426                return -n 30 | return -n 0 | return -n 0"
427                .parse()
428                .unwrap();
429            let result = pipeline.execute(&mut env).await;
430            assert_eq!(result, Continue(()));
431            assert_eq!(env.exit_status, ExitStatus(30));
432        });
433    }
434
435    #[test]
436    fn multi_command_pipeline_waits_for_all_child_commands() {
437        in_virtual_system(|mut env, state| async move {
438            env.builtins.insert("return", return_builtin());
439            let pipeline: syntax::Pipeline =
440                "return -n 1 | return -n 2 | return -n 3".parse().unwrap();
441            _ = pipeline.execute(&mut env).await;
442
443            // Only the original process remains.
444            for (pid, process) in &state.borrow().processes {
445                if *pid == env.main_pid {
446                    assert_eq!(process.state(), ProcessState::Running);
447                } else {
448                    assert_matches!(
449                        process.state(),
450                        ProcessState::Halted(ProcessResult::Exited(_))
451                    );
452                }
453            }
454        });
455    }
456
457    #[test]
458    fn multi_command_pipeline_does_not_wait_for_unrelated_child() {
459        in_virtual_system(|mut env, state| async move {
460            env.builtins.insert("return", return_builtin());
461
462            let list: syntax::List = "return -n 7&".parse().unwrap();
463            _ = list.execute(&mut env).await;
464            let async_pid = {
465                let state = state.borrow();
466                let mut iter = state.processes.keys();
467                assert_eq!(iter.next(), Some(&env.main_pid));
468                let async_pid = *iter.next().unwrap();
469                assert_eq!(iter.next(), None);
470                async_pid
471            };
472
473            let pipeline: syntax::Pipeline =
474                "return -n 1 | return -n 2 | return -n 3".parse().unwrap();
475            _ = pipeline.execute(&mut env).await;
476
477            let state = state.borrow();
478            let process = &state.processes[&async_pid];
479            assert_eq!(process.state(), ProcessState::exited(7));
480            assert!(process.state_has_changed());
481        });
482    }
483
484    #[test]
485    fn pipe_connects_commands_in_pipeline() {
486        in_virtual_system(|mut env, state| async move {
487            {
488                let file = state.borrow().file_system.get("/dev/stdin").unwrap();
489                let mut file = file.borrow_mut();
490                file.body = FileBody::new(*b"ok\n");
491            }
492
493            env.builtins.insert("cat", cat_builtin());
494
495            let pipeline: syntax::Pipeline = "cat | cat | cat".parse().unwrap();
496            let result = pipeline.execute(&mut env).await;
497            assert_eq!(result, Continue(()));
498            assert_eq!(env.exit_status, ExitStatus::SUCCESS);
499            assert_stdout(&state, |stdout| assert_eq!(stdout, "ok\n"));
500        });
501    }
502
503    #[test]
504    fn pipeline_leaves_no_pipe_fds_leftover() {
505        in_virtual_system(|mut env, state| async move {
506            env.builtins.insert("cat", cat_builtin());
507            let pipeline: syntax::Pipeline = "cat | cat".parse().unwrap();
508            let _ = pipeline.execute(&mut env).await;
509
510            let state = state.borrow();
511            let fds = state.processes[&env.main_pid].fds();
512            for fd in 3..10 {
513                assert!(!fds.contains_key(&Fd(fd)), "fd={fd}");
514            }
515        });
516    }
517
518    #[test]
519    fn inverting_exit_status_to_0_without_divert() {
520        let mut env = Env::new_virtual();
521        env.builtins.insert("return", return_builtin());
522        let pipeline: syntax::Pipeline = "! return -n 42".parse().unwrap();
523        let result = pipeline.execute(&mut env).now_or_never().unwrap();
524        assert_eq!(result, Continue(()));
525        assert_eq!(env.exit_status, ExitStatus(0));
526    }
527
528    #[test]
529    fn inverting_exit_status_to_1_without_divert() {
530        let mut env = Env::new_virtual();
531        env.builtins.insert("return", return_builtin());
532        let pipeline: syntax::Pipeline = "! return -n 0".parse().unwrap();
533        let result = pipeline.execute(&mut env).now_or_never().unwrap();
534        assert_eq!(result, Continue(()));
535        assert_eq!(env.exit_status, ExitStatus(1));
536    }
537
538    #[test]
539    fn not_inverting_exit_status_with_divert() {
540        let mut env = Env::new_virtual();
541        env.builtins.insert("return", return_builtin());
542        env.exit_status = ExitStatus(3);
543        let pipeline: syntax::Pipeline = "! return 15".parse().unwrap();
544        let result = pipeline.execute(&mut env).now_or_never().unwrap();
545        assert_eq!(result, Break(Divert::Return(Some(ExitStatus(15)))));
546        assert_eq!(env.exit_status, ExitStatus(3));
547    }
548
549    #[test]
550    fn noexec_option() {
551        let mut env = Env::new_virtual();
552        env.builtins.insert("return", return_builtin());
553        env.options.set(Exec, Off);
554        let pipeline: syntax::Pipeline = "return -n 93".parse().unwrap();
555        let result = pipeline.execute(&mut env).now_or_never().unwrap();
556        assert_eq!(result, Continue(()));
557        assert_eq!(env.exit_status, ExitStatus::SUCCESS);
558    }
559
560    #[test]
561    fn noexec_option_interactive() {
562        let mut env = Env::new_virtual();
563        env.builtins.insert("return", return_builtin());
564        env.options.set(Exec, Off);
565        env.options.set(Interactive, On);
566        let pipeline: syntax::Pipeline = "return -n 93".parse().unwrap();
567        let result = pipeline.execute(&mut env).now_or_never().unwrap();
568        assert_eq!(result, Continue(()));
569        assert_eq!(env.exit_status, ExitStatus(93));
570    }
571
572    #[test]
573    fn errexit_option() {
574        in_virtual_system(|mut env, _state| async move {
575            env.builtins.insert("return", return_builtin());
576            env.options.set(ErrExit, On);
577
578            let pipeline: syntax::Pipeline = "return -n 0 | return -n 93".parse().unwrap();
579            let result = pipeline.execute(&mut env).await;
580
581            assert_eq!(result, Break(Divert::Exit(None)));
582            assert_eq!(env.exit_status, ExitStatus(93));
583        });
584    }
585
586    #[test]
587    fn stack_without_inversion() {
588        fn stub_builtin(
589            env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
590            _args: Vec<Field>,
591        ) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
592            Box::pin(async move {
593                assert!(!env.stack.contains(&Frame::Condition), "{:?}", env.stack);
594                Default::default()
595            })
596        }
597
598        let mut env = Env::new_virtual();
599        env.builtins
600            .insert("foo", Builtin::new(Special, stub_builtin));
601        let pipeline: syntax::Pipeline = "foo".parse().unwrap();
602        let result = pipeline.execute(&mut env).now_or_never().unwrap();
603        assert_eq!(result, Continue(()));
604    }
605
606    #[test]
607    fn stack_with_inversion() {
608        fn stub_builtin(
609            env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
610            _args: Vec<Field>,
611        ) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
612            Box::pin(async move {
613                assert_matches!(
614                    env.stack.as_slice(),
615                    [Frame::Condition, Frame::Builtin { .. }]
616                );
617                Default::default()
618            })
619        }
620
621        let mut env = Env::new_virtual();
622        env.builtins
623            .insert("foo", Builtin::new(Special, stub_builtin));
624        let pipeline: syntax::Pipeline = "! foo".parse().unwrap();
625        let result = pipeline.execute(&mut env).now_or_never().unwrap();
626        assert_eq!(result, Continue(()));
627    }
628
629    #[test]
630    fn process_group_id_of_job_controlled_pipeline() {
631        fn stub_builtin(
632            env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
633            _args: Vec<Field>,
634        ) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
635            let pgid = env.system.getpgrp().0 as _;
636            Box::pin(async move { yash_env::builtin::Result::new(ExitStatus(pgid)) })
637        }
638
639        in_virtual_system(|mut env, state| async move {
640            env.builtins
641                .insert("foo", Builtin::new(Special, stub_builtin));
642            env.options.set(Monitor, On);
643            stub_tty(&state);
644
645            // TODO Better test all pipeline component exit statuses
646            let pipeline: syntax::Pipeline = "foo | foo".parse().unwrap();
647            let result = pipeline.execute(&mut env).await;
648            assert_eq!(result, Continue(()));
649            assert_ne!(env.exit_status, ExitStatus(env.main_pgid.0 as _));
650
651            // The shell should come back to the foreground after running the pipeline
652            assert_eq!(state.borrow().foreground, Some(env.main_pgid));
653        })
654    }
655
656    #[test]
657    fn job_controlled_suspended_pipeline_in_job_list() {
658        in_virtual_system(|mut env, state| async move {
659            env.builtins.insert("return", return_builtin());
660            env.builtins.insert("suspend", suspend_builtin());
661            env.options.set(Monitor, On);
662            stub_tty(&state);
663
664            let pipeline: syntax::Pipeline = "return -n 0 | suspend x".parse().unwrap();
665            let result = pipeline.execute(&mut env).await;
666            assert_eq!(result, Continue(()));
667            assert_eq!(env.exit_status, ExitStatus::from(SIGSTOP));
668
669            assert_eq!(env.jobs.len(), 1);
670            let job = env.jobs.iter().next().unwrap().1;
671            assert!(job.job_controlled);
672            assert_eq!(job.state, ProcessState::stopped(SIGSTOP));
673            assert!(job.state_changed);
674            assert_eq!(job.name, "return -n 0 | suspend x");
675        })
676    }
677
678    #[test]
679    fn pipe_set_shift_to_first_command() {
680        let system = VirtualSystem::new();
681        let process_id = system.process_id;
682        let state = Rc::clone(&system.state);
683        let mut env = Env::with_system(system);
684        let mut pipes = PipeSet::new();
685
686        let result = pipes.shift(&mut env, true);
687        assert_eq!(result, Ok(()));
688        assert_eq!(pipes.read_previous, None);
689        assert_eq!(pipes.next, Some((Fd(3), Fd(4))));
690        let state = state.borrow();
691        let process = &state.processes[&process_id];
692        assert_eq!(process.fds().get(&Fd(3)).unwrap().flags, EnumSet::empty());
693        assert_eq!(process.fds().get(&Fd(4)).unwrap().flags, EnumSet::empty());
694    }
695
696    #[test]
697    fn pipe_set_shift_to_middle_command() {
698        let system = VirtualSystem::new();
699        let process_id = system.process_id;
700        let state = Rc::clone(&system.state);
701        let mut env = Env::with_system(system);
702        let mut pipes = PipeSet::new();
703
704        let _ = pipes.shift(&mut env, true);
705        let result = pipes.shift(&mut env, true);
706        assert_eq!(result, Ok(()));
707        assert_eq!(pipes.read_previous, Some(Fd(3)));
708        assert_eq!(pipes.next, Some((Fd(4), Fd(5))));
709        let state = state.borrow();
710        let process = &state.processes[&process_id];
711        assert_eq!(process.fds().get(&Fd(3)).unwrap().flags, EnumSet::empty());
712        assert_eq!(process.fds().get(&Fd(4)).unwrap().flags, EnumSet::empty());
713        assert_eq!(process.fds().get(&Fd(5)).unwrap().flags, EnumSet::empty());
714    }
715
716    #[test]
717    fn pipe_set_shift_to_last_command() {
718        let system = VirtualSystem::new();
719        let process_id = system.process_id;
720        let state = Rc::clone(&system.state);
721        let mut env = Env::with_system(system);
722        let mut pipes = PipeSet::new();
723
724        let _ = pipes.shift(&mut env, true);
725        let result = pipes.shift(&mut env, false);
726        assert_eq!(result, Ok(()));
727        assert_eq!(pipes.read_previous, Some(Fd(3)));
728        assert_eq!(pipes.next, None);
729        let state = state.borrow();
730        let process = &state.processes[&process_id];
731        assert_eq!(process.fds().get(&Fd(3)).unwrap().flags, EnumSet::empty());
732    }
733
734    // TODO test PipeSet::move_to_stdin_stdout
735}