1use 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
41impl<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 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 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; 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 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 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 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 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#[derive(Clone, Copy, Default)]
255struct PipeSet {
256 read_previous: Option<Fd>,
257 next: Option<(Fd, Fd)>,
259}
260
261impl PipeSet {
262 fn new() -> Self {
263 Self::default()
264 }
265
266 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 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 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 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 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 }