Pipex 42

zakaria guellouch

June 16, 2025

Welcome! We’re going to build our own pipex program in C. Before we write any code, we’ll see:

1-What is pipex

2-Play in the shell to experiment with <, |, >, >>, <<.

3-Learn the key system calls (access, open, pipe, fork, dup2, execve, wait)

4-Combine everything

1. What Is Pipex?

pipex is a small C program that mimics how your shell connects commands with pipes and redirects their input/output—when you run

./pipex infile "cmd1 args" "cmd2 args" outfile

it does exactly what

< infile cmd1 args | cmd2 args > outfile

does in Bash.

2-Bash Redirections & Pipes

Try these mini-experiments in your terminal!

2.1 Input Redirection <
  • Syntax: < infile command
  • Behavior: The command reads its stdin from infile, not from your keyboard.
echo "Hello Pipex" > hello.txt  # create a file hello.txt, and the text inside is "Life is good"
cat < hello.txt                 # 'cat' reads from hello.txt and prints "Life is good"
2.2 Pipe |
  • Syntax: cmd1 | cmd2
  • Behavior: Feeds output of cmd1 into input of cmd2.
echo "Life is good" | wc -w   # → 3 words
2.3 Output Redirection >
  • Syntax: command > outfile
  • Behavior: Writes output to outfile, replacing old content.
ls > list.txt   # creates a file list.txt if it doesn't exit and save file list in it
cat list.txt    # view it
echo "life is good" > list.txt # you're gonna notice that list of files is removed and it's replaced by "life is good", that what i meant in "replacing old content"
# to save the old content it's in the next section
2.4 Append Redirection >> (Bonus)
  • Syntax: command >> outfile
  • Behavior: Appends output to the end of outfile.
echo "Line 1" > notes.txt # creates a file notes.txt if it doesn't exit and save "Line 1" in it 
echo "Line 2" >> notes.txt   # adds to end of the file "Line 2" without losing the "Line 1"
cat notes.txt
2.5 Here-Document << (Bonus)
  • Syntax: cmd << LIMITER
  • Behavior: Reads from keyboard (stdin) until you type exactly LIMITER on its own line.
cat << STOP | grep good
>Line 1
>Line 2
>life is bad
>life is good
>STOP
life is good

Types until STOP, then grep filters “life is good”

2.6 Everything is a file

if you run “which ls” in bash, you’re gonna notice a path whether “/bin/ls” or “/usr/bin/ls” it doesn’t matter, try to run that absolute path

$> which ls
/usr/bin/ls
$> /usr/bin/ls
infile  libft  main.c  Makefile  minipipex  minipipex.h  outfile  utils.c
$> ls
infile  libft  main.c  Makefile  minipipex  minipipex.h  outfile  utils.c

you’ll notice that every command is just an executable file, try for other commands like echo, cat, it’s gonna be the same, why am i showing you this? you’ll see it in the next chapter

3-The C System Calls You’ll Use

1. access(pathname, mode)

What it does
Checks whether the file at pathname exists and/or whether your process has the requested permissions on it.

    • Returns:

      • 0 → you do have those permissions

      • -1 → you don’t (or the file doesn’t exist)

Common mode flags

    • F_OK → does the file exist?

    • R_OK → can I read it?

    • W_OK → can I write to it?

    • X_OK → can I execute it?

Examples

if (access("file.txt", F_OK) != 0)
   perror("file.txt does not exist");
if (access("file.txt", R_OK | W_OK) == 0)
   printf("file.txt is readable and writable\n");

2. open(pathname, flags, [mode])

What it does
Opens (or creates) a file and returns a file descriptor.

    • flags tell the kernel how you want to open it:

      • O_RDONLY → read only

      • O_WRONLY → write only

      • O_RDWR → read & write

      • O_CREAT → create if it doesn’t exist

      • O_TRUNC → if it exists, truncate to zero length

    • mode (0644, etc.) only matters when you use O_CREAT, and sets the new file’s permissions.

    • Returns:

      • >= 0 → a valid file descriptor

      • -1 → open failed to open the file

Example

int in_fd = open("infile.txt", O_RDONLY);
int out_fd = open("outfile.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (in_fd < 0)
     perror("open infile failed");
if (out_fd < 0)
     perror("open outfile failed")

3. pipe(int pipefd[2])

What it does
Creates a unidirectional data channel (a pipe) with two file descriptors:

    • pipefd[0] → read end

    • pipefd[1] → write end

Data written to pipefd[1] can be read from pipefd[0].

    • Returns:

      • 0 → success

      • -1 → error

Example

int pipefd[2];
if (pipe(pipefd) < 0)
     perror("pipe");
/* if it succeeded pipefd[1] is the writer, pipefd[0] is the reader */

4. fork()

What it does
Creates a child process. The child is a near-identical copy of the parent.

    • Returns:

      • > 0 → in the parent, the return value is the child’s PID

      • 0 → in the child

      • -1 → error (no child created)

Example

pid_t pid = fork();
if (pid < 0)
     perror("fork failed");
else if (pid == 0){
    /* child process code */
} else {
   /* parent process code */
}

5. dup2(oldfd, newfd)

What it does
Redirects one file descriptor (newfd) to the same open file/table entry as another (oldfd).

    1. Closes newfd if it’s already open.

    2. Makes newfd refer to the same file/pipe as oldfd.

After dup2: both descriptors share the same offset and flags.

    • Returns:

      • newfd on success

      • -1 on error

Why you do this in pipex

    • dup2(in_fd, STDIN_FILENO) makes stdin read from your input file.

    • dup2(pipefd[1], STDOUT_FILENO) makes stdout write into the pipe.

Important: Always close(oldfd) afterward to avoid leaking that descriptor.

dup2(in_fd, STDIN_FILENO);
close(in_fd);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);

6. execve(path, args, env)

What it does
Replaces the current process image with a new program.

    • path → path to the executable

    • args → argument vector (args[0] is the command name)

    • env → environment variables

    • Returns:

      • Never returns on success (the new program runs instead).

      • -1 on error (old program continues).

Example

char *args[] = {"/bin/ls", "-l", "-a", NULL};
if (execve("/bin/lsss", args, env) < 0) //path /bin/lsss doesn't exist which force execve to fail
    perror("execve failed");
          // if you want to find the path of any bash command all you need to type is "which ls" in bash

7. wait(NULL)

What it does
Suspends the calling (parent) process until one of its child processes terminates. When you pass `NULL`, you tell it you don’t care about the child’s exit status—only that it’s finished.

Returns

  • On success: the PID of the child that terminated (> 0)
  • On error: -1

Example
// After forking two children for cmd1 and cmd2:
if (wait(NULL) < 0)
perror(“wait failed”);
if (wait(NULL) < 0)
perror(“wait failed”);

we’re gonna start building pipex

4. Combine everything

PART1: MANDATORY