The pieces are in place. This page assembles them into a complete two-command pipeline function and verifies it against the shell.
The complete pipeline.c
Replace the contents of pipeline.c:
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdlib.h>
#include "libtciutil.h"
void exec_cmd(char **argv);
void run_pipeline(t_list *cmds, int infd, int outfd)
{
int fd[2];
pid_t pid1;
pid_t pid2;
int status;
int last_status;
char **cmd1;
char **cmd2;
cmd1 = (char **)((t_list *)cmds)->content;
cmd2 = (char **)((t_list *)cmds->next)->content;
if (pipe(fd) < 0) {
perror("pipe");
exit(1);
}
pid1 = fork();
if (pid1 < 0) { perror("fork"); exit(1); }
if (pid1 == 0) {
dup2(infd, STDIN_FILENO); /* infile → stdin */
dup2(fd[1], STDOUT_FILENO); /* stdout → pipe write end */
close(fd[0]);
close(fd[1]);
if (infd != STDIN_FILENO)
close(infd);
if (outfd != STDOUT_FILENO)
close(outfd);
exec_cmd(cmd1);
}
pid2 = fork();
if (pid2 < 0) { perror("fork"); exit(1); }
if (pid2 == 0) {
dup2(fd[0], STDIN_FILENO); /* pipe read end → stdin */
dup2(outfd, STDOUT_FILENO); /* stdout → outfile */
close(fd[0]);
close(fd[1]);
if (infd != STDIN_FILENO)
close(infd);
if (outfd != STDOUT_FILENO)
close(outfd);
exec_cmd(cmd2);
}
close(fd[0]); /* parent owns neither pipe end */
close(fd[1]);
waitpid(pid1, &status, 0);
waitpid(pid2, &status, 0);
last_status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
(void)last_status; /* used in main */
}The close checklist
Every child closes every fd it will not use before calling exec_cmd.
For a two-command pipeline with infile and outfile:
| Process | Closes | Keeps open via dup2 |
|---|---|---|
| cmd1 child | fd[0], fd[1] (after dup2), infd (after dup2), outfd | stdin (from infd), stdout (from fd[1]) |
| cmd2 child | fd[0] (after dup2), fd[1], infd, outfd (after dup2) | stdin (from fd[0]), stdout (from outfd) |
| parent | fd[0], fd[1], infd, outfd | nothing |
The checks if (infd != STDIN_FILENO) and if (outfd != STDOUT_FILENO) guard
against closing the standard file descriptors when infile and outfile happen
to already be stdin/stdout — which cannot happen in normal use but is
a defensive pattern.
The complete main.c
Update main.c to parse the invocation format and call run_pipeline:
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdlib.h>
#include "libtciutil.h"
void run_pipeline(t_list *cmds, int infd, int outfd);
static char **parse_cmd(char *str)
{
return (tciu_split(str, ' '));
}
static void free_cmd(void *ptr)
{
char **argv;
int i;
argv = (char **)ptr;
i = 0;
while (argv[i])
free(argv[i++]);
free(argv);
}
int main(int argc, char **argv)
{
int infd;
int outfd;
t_list *cmds;
t_list *node;
int i;
if (argc < 4) {
tci_printf("usage: ./pipeline infile cmd1 cmd2 outfile\n");
return (1);
}
infd = open(argv[1], O_RDONLY);
if (infd < 0) { perror(argv[1]); return (1); }
outfd = open(argv[argc - 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd < 0) { perror(argv[argc - 1]); close(infd); return (1); }
cmds = NULL;
i = 2;
while (i < argc - 1) {
node = tciu_lstnew(parse_cmd(argv[i]));
if (!node) { close(infd); close(outfd); return (1); }
tciu_lstadd_back(&cmds, node);
i++;
}
run_pipeline(cmds, infd, outfd);
close(infd);
close(outfd);
tciu_lstclear(&cmds, free_cmd);
return (0);
}parse_cmd splits the command string on spaces using tciu_split,
producing the argv array that exec_cmd expects. free_cmd frees
both the strings in the array and the array itself — it is the del
function passed to tciu_lstclear.
Verify against the shell
make reTest 1 — cat and wc:
echo -e "alpha\nbeta\ngamma\ndelta" > input.txt
./pipeline input.txt "cat" "wc -l" output.txt
cat output.txtExpected: 4.
Shell equivalent:
< input.txt cat | wc -l > output.txt
cat output.txtSame result.
Test 2 — sort and uniq:
echo -e "apple\nbanana\napple\ncherry\nbanana" > fruit.txt
./pipeline fruit.txt "sort" "uniq" unique.txt
cat unique.txtExpected:
apple
banana
cherryShell equivalent: < fruit.txt sort | uniq > unique.txt.
Test 3 — command not found:
./pipeline input.txt "nosuchcmd" "wc -l" output.txt
echo $?Expected: exit code 127. The nosuchcmd child exits 127; wc -l reads
nothing and exits 0. run_pipeline returns wc's exit status (the
last command's status), which is 0. To match the shell's behaviour
exactly — where the exit status of a pipeline is the status of the last
command — this is correct.
The two-command pipeline works. The next page generalises it to chains of N commands.