thecodingidiot.com

The PipelineThe Pipeline

The Pipeline

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:

ProcessClosesKeeps open via dup2
cmd1 childfd[0], fd[1] (after dup2), infd (after dup2), outfdstdin (from infd), stdout (from fd[1])
cmd2 childfd[0] (after dup2), fd[1], infd, outfd (after dup2)stdin (from fd[0]), stdout (from outfd)
parentfd[0], fd[1], infd, outfdnothing

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 re

Test 1 — cat and wc:

echo -e "alpha\nbeta\ngamma\ndelta" > input.txt
./pipeline input.txt "cat" "wc -l" output.txt
cat output.txt

Expected: 4.

Shell equivalent:

< input.txt cat | wc -l > output.txt
cat output.txt

Same result.

Test 2 — sort and uniq:

echo -e "apple\nbanana\napple\ncherry\nbanana" > fruit.txt
./pipeline fruit.txt "sort" "uniq" unique.txt
cat unique.txt

Expected:

apple
banana
cherry

Shell 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.