Pipex is a C program that recreates the behavior of Unix pipes (|) in the shell. It allows you to chain multiple commands together, where the output of one command becomes the input of the next command, and finally writes the result to a file.
Pipex mimics the behavior of this shell command:
< infile cmd1 | cmd2 | cmd3 > outfileBut using our program:
./pipex infile "cmd1" "cmd2" "cmd3" outfileThink of pipes like a water pipe system:
- Input file β Command 1 β Command 2 β Command 3 β Output file
- Each command processes the data and passes it to the next command
- The final result is saved in the output file
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β infile βββββΆβ cmd1 βββββΆβ cmd2 βββββΆβ cmd3 β
β (input) β β (process) β β (process) β β (process) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ
β outfile β
β (output) β
βββββββββββββββ
Parent Process
β
ββββ fork() βββΆ Child Process 1 (cmd1)
β β
β ββββ pipe() βββΆ [read][write]
β β
β ββββ dup2() βββΆ Redirect stdin/stdout
β β
β ββββ execve() βββΆ Execute cmd1
β
ββββ fork() βββΆ Child Process 2 (cmd2)
β β
β ββββ pipe() βββΆ [read][write]
β β
β ββββ dup2() βββΆ Redirect stdin/stdout
β β
β ββββ execve() βββΆ Execute cmd2
β
ββββ fork() βββΆ Child Process 3 (cmd3)
β β
β ββββ open() βββΆ Open outfile
β β
β ββββ dup2() βββΆ Redirect stdout to file
β β
β ββββ execve() βββΆ Execute cmd3
β
ββββ wait() βββΆ Wait for all children to finish
./pipex infile "command1" "command2" outfile-
Simple text processing:
./pipex input.txt "cat" "grep hello" output.txt
This reads from
input.txt, pipes throughcat, thengrep hello, and saves tooutput.txt -
Multiple commands:
./pipex input.txt "ls -la" "grep .txt" "wc -l" output.txt
Lists files, filters for .txt files, counts lines, saves result
-
With here_doc (heredoc mode):
./pipex here_doc "EOF" "cat" "sort" output.txt
Allows interactive input until "EOF" is typed, then processes it
- GCC compiler
- Make utility
- Unix-like system (Linux, macOS)
# Clone or download the project
cd pipex
# Compile the program
make
# The executable 'pipex' will be createdmake- Compiles the programmake clean- Removes object filesmake fclean- Removes object files and executablemake re- Recompiles everything from scratch
pipex/
βββ inc/ # Header files
β βββ pipex.h # Main header with function declarations
β βββ get_next_line.h # Get next line utility header
βββ srcs/ # Source files
β βββ pipex.c # Main program logic
β βββ manage_command.c # Command execution management
β βββ create_command.c # Command path resolution
β βββ manage_heredoc.c # Heredoc functionality
βββ utils/ # Utility functions
β βββ ft_printf/ # Printf implementation
β βββ get_next_line.c # Line reading utility
β βββ ft_split.c # String splitting
β βββ libft_functions.c # Basic string operations
βββ Makefile # Build configuration
βββ README.md # This file
-
main()- Program entry point- Validates command line arguments
- Handles heredoc mode
- Orchestrates the entire pipe process
-
manage_infile()- Input file handling- Opens the input file
- Redirects stdin to the file
- Handles file opening errors gracefully
-
manage_command()- Command execution- Creates pipes between commands
- Forks child processes
- Redirects input/output between processes
-
create_command()- Command path resolution- Finds the full path of commands
- Searches in PATH environment variable
- Returns executable path (e.g., "/bin/ls" for "ls")
pipe()- Creates a pipe for inter-process communicationfork()- Creates a new child processexecve()- Replaces current process with a new programdup2()- Redirects file descriptorswait()- Waits for child processes to finishopen()- Opens files for reading/writing
Purpose: Creates a pipe, which is a unidirectional data channel for communication between processes.
Function Signature:
int pipe(int pipefd[2]);Arguments:
pipefd[2]: Array of two integers where:pipefd[0]= Read end (where you read data from)pipefd[1]= Write end (where you write data to)
Return Value:
0on success-1on failure (setserrno)
Example Usage:
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe failed");
exit(1);
}
// Now pipe_fd[0] is for reading, pipe_fd[1] is for writingHow it works:
Process A Process B
β β
β writes to pipe_fd[1] β reads from pipe_fd[0]
βΌ βΌ
βββββββββββββββ βββββββββββββββ
β PIPE βββββββββββββΆβ PIPE β
β (buffer) β β (buffer) β
βββββββββββββββ βββββββββββββββ
Purpose: Creates a new child process by duplicating the current process.
Function Signature:
pid_t fork(void);Arguments: None
Return Value:
- In parent process: Returns the PID (Process ID) of the child
- In child process: Returns
0 - On failure: Returns
-1(setserrno)
Example Usage:
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// Child process code
printf("I am the child, PID: %d\n", getpid());
} else {
// Parent process code
printf("I am the parent, child PID: %d\n", pid);
}What happens after fork():
Before fork(): After fork():
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Parent β β Parent β β Child β
β Process βββββΆβ Process β β Process β
β β β β β β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
(returns child (returns 0)
PID)
Purpose: Replaces the current process with a new program. The current process "dies" and is replaced by the new program.
Function Signature:
int execve(const char *pathname, char *const argv[], char *const envp[]);Arguments:
pathname: Full path to the executable (e.g., "/bin/ls")argv[]: Array of command-line arguments (argv[0] is usually the program name)envp[]: Array of environment variables
Return Value:
- On success: Never returns (process is replaced)
- On failure: Returns
-1(setserrno)
Example Usage:
char *args[] = {"ls", "-la", NULL};
char *env[] = {"PATH=/bin:/usr/bin", NULL};
execve("/bin/ls", args, env);
// If execve succeeds, this line is never reached
perror("execve failed");Important: After execve() succeeds, the current process is completely replaced. Any code after execve() in the same process will not execute.
Purpose: Duplicates a file descriptor, making the new descriptor point to the same file/pipe as the old one.
Function Signature:
int dup2(int oldfd, int newfd);Arguments:
oldfd: The file descriptor to duplicatenewfd: The new file descriptor number
Return Value:
- On success: Returns the new file descriptor (
newfd) - On failure: Returns
-1(setserrno)
Example Usage:
// Redirect stdout to a file
int file_fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
dup2(file_fd, STDOUT_FILENO); // Now stdout writes to the file
close(file_fd); // Close the original file descriptor
printf("This goes to output.txt\n"); // This writes to the fileHow it works:
Before dup2(): After dup2(fd, 1):
βββββββββββββββ βββββββββββββββ
β fd=3 β β fd=3 β
β (file) β β (file) β
βββββββββββββββ βββββββββββββββ
βββββββββββββββ βββββββββββββββ
β fd=1 β β fd=1 β
β (stdout) β β (stdout) β
βββββββββββββββ βββββββββββββββ
(now points to same file)
Purpose: Waits for a child process to change state (terminate, stop, or continue).
Function Signature:
pid_t wait(int *wstatus);Arguments:
wstatus: Pointer to store the exit status of the child process (can be NULL)
Return Value:
- On success: Returns the PID of the terminated child
- On failure: Returns
-1(setserrno)
Example Usage:
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("Child process\n");
exit(42);
} else {
// Parent process
int status;
pid_t child_pid = wait(&status);
printf("Child %d finished with status: %d\n", child_pid, WEXITSTATUS(status));
}Why wait() is important:
- Prevents "zombie processes" (terminated processes that haven't been cleaned up)
- Allows parent to know when child has finished
- Synchronizes parent and child processes
Purpose: Opens a file and returns a file descriptor for reading/writing.
Function Signature:
int open(const char *pathname, int flags, mode_t mode);Arguments:
pathname: Path to the fileflags: How to open the file (O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, etc.)mode: File permissions (used when O_CREAT is specified)
Common Flags:
O_RDONLY: Read onlyO_WRONLY: Write onlyO_RDWR: Read and writeO_CREAT: Create file if it doesn't existO_TRUNC: Truncate file to zero lengthO_APPEND: Append to file instead of overwriting
Return Value:
- On success: Returns the file descriptor (non-negative integer)
- On failure: Returns
-1(setserrno)
Example Usage:
// Open for reading
int fd_read = open("input.txt", O_RDONLY);
if (fd_read == -1) {
perror("Failed to open input.txt");
exit(1);
}
// Open for writing (create if doesn't exist, truncate if exists)
int fd_write = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_write == -1) {
perror("Failed to open output.txt");
exit(1);
}
// Open for appending
int fd_append = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);File Descriptor Numbers:
0: Standard input (stdin)1: Standard output (stdout)2: Standard error (stderr)3+: Other files/pipes
1. pipe(pipe_fd) β Creates communication channel
2. fork() β Creates child process
3. dup2() β Redirects stdin/stdout to pipe
4. execve() β Replaces process with new command
5. wait() β Parent waits for child to finish
6. open() β Opens input/output files
This combination allows pipex to create a pipeline where data flows from one command to the next through pipes, just like the Unix shell does.
# Create a test input file
echo "Hello World\nThis is a test\nHello again" > test_input.txt
# Test basic functionality
./pipex test_input.txt "cat" "grep Hello" test_output.txt
# Check the result
cat test_output.txt# Test with multiple commands
./pipex test_input.txt "cat" "grep Hello" "wc -l" result.txt
# Test heredoc
./pipex here_doc "END" "cat" "sort" sorted.txt
# Then type some lines and "END" to finishThe program handles various error scenarios:
- Invalid number of arguments
- File opening failures
- Command not found
- Pipe creation failures
- Process creation failures
This project teaches:
- Process management with
fork()andwait() - Inter-process communication with pipes
- File descriptor manipulation with
dup2() - Command execution with
execve() - Error handling in system programming
- Memory management in C
- Unix Philosophy: "Do one thing and do it well"
- Pipeline Pattern: Chain of processing steps
- Process Communication: How processes share data
- File Descriptors: How Unix handles I/O
Note: This project is part of the 42 school curriculum and demonstrates fundamental Unix system programming concepts.