Skip to content

Commit f246ec2

Browse files
author
Test
committed
feat(external): implement execute_pipeline with stdio chaining (Phase 6 M3 Component 4)
- Add execute_pipeline() function in external.rs - Chain stdio using Stdio::piped() for intermediate stages - First stage inherits stdin, middle stages use piped, last uses redirect - Wait for all children in order, checking for SIGINT - Return exit code from last stage (POSIX behavior) - Record undo for final redirections - Update Pipeline case in executable.rs to call execute_pipeline() Manual test successful: ls | head -3 works correctly Tests: 98/98 passing (52 unit + 27 integration + 19 property) Part of Phase 6 Milestone 3: Pipeline implementation
1 parent 2ad48f7 commit f246ec2

2 files changed

Lines changed: 166 additions & 3 deletions

File tree

impl/rust-cli/src/executable.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,14 @@ impl ExecutableCommand for Command {
310310

311311
// Pipeline commands (not reversible by default, but redirections are)
312312
Command::Pipeline { stages, redirects } => {
313-
// TODO: Implement execute_pipeline (task #20)
314-
let _ = (stages, redirects);
315-
Err(anyhow::anyhow!("Pipeline execution not yet implemented"))
313+
let exit_code = external::execute_pipeline(stages, redirects, state).unwrap_or_else(
314+
|e| {
315+
eprintln!("Pipeline error: {}", e);
316+
127
317+
},
318+
);
319+
320+
Ok(ExecutionResult::ExternalCommand { exit_code })
316321
}
317322
}
318323
}

impl/rust-cli/src/external.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)