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