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