@@ -87,6 +87,164 @@ pub fn execute_external_with_redirects(
8787 }
8888}
8989
90+ /// Execute a pipeline of external commands with stdio chaining.
91+ ///
92+ /// Spawns each command in sequence, piping stdout of stage N to stdin of stage N+1.
93+ /// Final stage's output goes to redirections or inherits from shell.
94+ ///
95+ /// # Arguments
96+ /// * `stages` - Vector of (program, args) pairs for each pipeline stage
97+ /// * `redirects` - Final redirections (>, >>, etc.) applied to last stage
98+ /// * `state` - Shell state for undo tracking
99+ ///
100+ /// # Returns
101+ /// Exit code of the rightmost (last) command in the pipeline (POSIX behavior)
102+ ///
103+ /// # Errors
104+ /// Returns error if:
105+ /// - Pipeline is empty or has only one stage
106+ /// - Program not found in PATH
107+ /// - Failed to spawn child process
108+ /// - Failed to setup redirections
109+ ///
110+ /// # Examples
111+ /// ```no_run
112+ /// use vsh::external;
113+ /// use vsh::state::ShellState;
114+ ///
115+ /// let stages = vec![
116+ /// ("ls".to_string(), vec!["-la".to_string()]),
117+ /// ("grep".to_string(), vec!["test".to_string()]),
118+ /// ];
119+ /// let mut state = ShellState::new("/tmp")?;
120+ /// let exit_code = external::execute_pipeline(&stages, &[], &mut state)?;
121+ /// # Ok::<(), anyhow::Error>(())
122+ /// ```
123+ pub fn execute_pipeline (
124+ stages : & [ ( String , Vec < String > ) ] ,
125+ redirects : & [ Redirection ] ,
126+ state : & mut ShellState ,
127+ ) -> Result < i32 > {
128+ if stages. is_empty ( ) {
129+ anyhow:: bail!( "Pipeline must have at least one stage" ) ;
130+ }
131+
132+ if stages. len ( ) == 1 {
133+ // Single stage - just run normally with redirects
134+ return execute_external_with_redirects ( & stages[ 0 ] . 0 , & stages[ 0 ] . 1 , redirects, state) ;
135+ }
136+
137+ // Setup final redirection output files
138+ let redirect_setup = if !redirects. is_empty ( ) {
139+ Some ( RedirectSetup :: setup ( redirects, state) ?)
140+ } else {
141+ None
142+ } ;
143+
144+ // Configure stdio for stages and spawn children
145+ let mut children: Vec < std:: process:: Child > = Vec :: new ( ) ;
146+ let mut prev_stdout: Option < std:: process:: ChildStdout > = None ;
147+
148+ for ( idx, ( program, args) ) in stages. iter ( ) . enumerate ( ) {
149+ let executable = find_in_path ( program) ?;
150+
151+ let is_first = idx == 0 ;
152+ let is_last = idx == stages. len ( ) - 1 ;
153+
154+ // Configure stdin
155+ let stdin_cfg = if is_first {
156+ // First stage: inherit from shell
157+ Stdio :: inherit ( )
158+ } else {
159+ // Middle/last stages: read from previous stdout
160+ Stdio :: from ( prev_stdout. take ( ) . unwrap ( ) )
161+ } ;
162+
163+ // Configure stdout
164+ let stdout_cfg = if is_last {
165+ // Last stage: redirect to file or inherit
166+ if !redirects. is_empty ( ) {
167+ stdio_config_from_redirects ( redirects, redirect_setup. as_ref ( ) . unwrap ( ) , state) ?. 1
168+ } else {
169+ Stdio :: inherit ( )
170+ }
171+ } else {
172+ // First/middle stages: pipe to next
173+ Stdio :: piped ( )
174+ } ;
175+
176+ // Configure stderr (inherit unless redirected in last stage)
177+ let stderr_cfg = if is_last && !redirects. is_empty ( ) {
178+ stdio_config_from_redirects ( redirects, redirect_setup. as_ref ( ) . unwrap ( ) , state) ?. 2
179+ } else {
180+ Stdio :: inherit ( )
181+ } ;
182+
183+ // Spawn child
184+ let mut command = Command :: new ( & executable) ;
185+ command
186+ . args ( args)
187+ . stdin ( stdin_cfg)
188+ . stdout ( stdout_cfg)
189+ . stderr ( stderr_cfg) ;
190+
191+ #[ cfg( unix) ]
192+ {
193+ use std:: os:: unix:: process:: CommandExt ;
194+ command. process_group ( 0 ) ;
195+ }
196+
197+ let mut child = command
198+ . spawn ( )
199+ . context ( format ! ( "Failed to spawn: {}" , program) ) ?;
200+
201+ // Save stdout for next stage
202+ if !is_last {
203+ prev_stdout = child. stdout . take ( ) ;
204+ }
205+
206+ children. push ( child) ;
207+ }
208+
209+ // Wait for all children in order, checking for SIGINT
210+ let mut final_exit_code = 0 ;
211+
212+ for ( idx, mut child) in children. into_iter ( ) . enumerate ( ) {
213+ loop {
214+ match child. try_wait ( ) . context ( "Failed to wait for child" ) ? {
215+ Some ( status) => {
216+ let exit_code = handle_exit_status ( status) ?;
217+ if idx == stages. len ( ) - 1 {
218+ // Last stage's exit code is pipeline's exit code
219+ final_exit_code = exit_code;
220+ }
221+ break ;
222+ }
223+ None => {
224+ // Child still running - check for interrupt
225+ if crate :: INTERRUPT_REQUESTED . load ( Ordering :: Relaxed ) {
226+ // Kill all remaining children
227+ child. kill ( ) . context ( "Failed to kill child process" ) ?;
228+ crate :: INTERRUPT_REQUESTED . store ( false , Ordering :: Relaxed ) ;
229+ child. wait ( ) . context ( "Failed to wait for killed child" ) ?;
230+
231+ // Don't record modifications - operation interrupted
232+ return Ok ( 130 ) ; // 128 + SIGINT
233+ }
234+ std:: thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
235+ }
236+ }
237+ }
238+ }
239+
240+ // Record undo for final redirection (if any)
241+ if let Some ( setup) = redirect_setup {
242+ setup. record_for_undo ( state) ?;
243+ }
244+
245+ Ok ( final_exit_code)
246+ }
247+
90248/// Execute an external command without redirections (legacy interface).
91249///
92250/// Spawns a child process via execve(), polls for completion, and handles
0 commit comments