Skip to content

Latest commit

 

History

109 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pipex

pipex badge

Pipex mimics the functionality of the shell pipe command | by executing ./pipex infile cmd1 cmd2 outfile, which emulates the behavior of < infile cmd1 | cmd2 > outfile. It facilitates the connection of the standard output of one command to the standard input of another, creating a pipeline for data flow between commands executed within separate processes.

  • Command Execution: Utilizing the PATH environment variable to execute commands via execve()
  • Process Management: Creating child processes and establishing inter-process communication via fork(), waitpid(), pipe(), and dup2()
  • Error Handling: Robust error messaging using perror(), strerror(), and errno
  • Shell Behavior: Replicating the behavior of Z Shell (zsh) as closely as possible

Table of Contents


Getting Started

# Clone the repository
git clone https://github.com/alx-sch/42_pipex.git pipex
cd pipex

# Build
make

# Usage
./pipex infile cmd1 cmd2 outfile

This is equivalent to the shell command:

< infile cmd1 | cmd2 > outfile

Example:

./pipex infile.txt "grep hello" "wc -l" outfile.txt

is equivalent to:

< infile.txt grep hello | wc -l > outfile.txt

Run In Codespace

This repository includes a devcontainer setup so the project can compile and run immediately in Codespaces with no local setup.

  1. Open this repository in GitHub Codespaces.
  2. Wait for the container to build.
  3. The post-create step automatically runs make and creates a sample infile.
  4. Run the program:
./pipex infile "grep hello" "wc -l" outfile
cat outfile

Command Execution

The PATH Environment Variable

Environment variables are essential elements of the operating system's environment. They store information that various processes and applications utilize to configure their behavior and access system resources.

For example, common commands such as grep, ls, or cat are executable files stored within the system. To determine the exact path(s) to a specific command, you can use which in bash or where in zsh, followed by the command name, such as which grep or which ls.

When calling a command, the terminal shell checks the PATH environment variable. This variable contains a list of directories, delimited by colons, where the operating system searches to find the executable file corresponding to the given command.

To view a list of all environment variables and their values, you can execute the env command in the terminal. This command displays a list like this (excerpt):

[...]
LANGUAGE=en
USER=aschenk
SHELL=/bin/zsh
[...]
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

In a C program, you can access the list of environment variables by including char **envp as the third argument to the main function, e.g. int main(int argc, char **argv, char **envp). The envp parameter is structured as an array of strings in the format "VARIABLE=value", for example envp = {"LANGUAGE=en", "PATH=/usr/local/sbin:[...]", "[...]", NULL}.

To understand how Pipex retrieves the path to a specified command, please refer to the get_command_path() function here.

The Execve() System Call

So far so good — but why is it necessary to create multiple processes to execute multiple commands? Theoretically, you could save the output of a command in a variable and pass this as an input for another command, couldn't you? Such "command chaining" does work in shell scripting, e.g.:

output_of_command1=$(< infile.txt command1) 
command2 "$output_of_command1" > outfile.txt

However, in C, you would use a system call from the exec() family for this purpose (for more information, see here). As per project requirements, Pipex uses execve() (execute with vector of environment variables):

int execve(const char *path, char **const argv, char **const envp)
  • const char *path: Represents the path to the command executable, e.g. /usr/bin/ls.
  • char **const argv: Represents the command arguments in a NULL-terminated char array, e.g. {"ls", "-l", NULL}.
  • char **const envp: Represents the list of environment variables.

Members of the exec() family behave uniquely by loading and executing a new program (the command), effectively replacing the current process when called. They do not return to the original process after successful execution. This means that once execve() is called successfully (not returning -1), any following code is not executed.

So, to execute commands with input/output redirection, such as cmd1 < infile | cmd2 > outfile, each command execution requires a separate call to execve(). Since execve() replaces the current process, one process per command is necessary. The creation of additional processes is achieved through fork(). To enable communication between these processes, pipe() is used, which establishes a unidirectional communication channel.


Creating and Managing Multiple Processes

Understanding Fork()

Creating a new process is simply done by calling fork(), which creates two identical copies of the program's execution environment, with one being the parent (return value of fork() > 0) and the other being the child (return value of fork() = 0).

Let's look at a simple program using fork():

Note: The following code examples have been selected for relevance of explaining specific system calls — they do not directly relate to the pipex project.

// fork.c

#include <stdio.h> // prinft()
#include <unistd.h> // fork(), usleep()
#include <sys/types.h> // pid_t: int or long representing process ID's (PIDs)

int	main(void)
{
	pid_t	child_pid;

	printf("Before the fork!\n");
	child_pid = fork();
	printf("After the fork! Child PID: %d\n", child_pid);
	if (child_pid == 0) // Child process
		printf("Hello from the child! Child PID: %d\n", child_pid);
	else // Parent process
		printf("Hello from the parent! Child PID: %d\n", child_pid);

	return (0);
}

fork flow diagram
Parent process gets child PID > 0, child process gets PID = 0.

fork terminal output
Running the program twice; note the non-deterministic execution order.

"Before the fork!" is printed out once, before fork() is called. Then, "After the fork!" is printed out twice: Once by the parent process and once by the child process. This is because the fork() call creates a new process, resulting in two separate execution paths. In the parent process, it returns the process ID (PID) of the child process (> 0), while in the child process, it returns 0. This makes it possible to execute different tasks by distinguishing between the PIDs (if (pid == 0) for child process tasks and else for parent process tasks).

Introducing sleep()

Note that the parent and child processes run in parallel, meaning they execute independently and their execution order is somewhat random. While it's not straightforward to predict the exact order in which they will execute, introducing delays using functions like sleep() / usleep() can help synchronize their behavior to some extent:

// sleep_fork.c

#include <stdio.h> // prinft()
#include <unistd.h> // fork(), usleep()
#include <sys/types.h> // pid_t: int or long representing process ID's (PIDs)

int	main(void)
{
	pid_t	child_pid;

	printf("Before the fork!\n");
	child_pid = fork();
	printf("After the fork! Child PID: %d\n", child_pid);
	usleep(10); // Pause execution for 10 microseconds
	if (child_pid == 0) // Child process
		printf("Hello from the child! Child PID: %d\n", child_pid);
	else // Parent process
		printf("Hello from the parent! Child PID: %d\n", child_pid);

	return (0);
}

fork with sleep flow diagram
usleep(10) introduces a small delay, but execution order is still not guaranteed.

fork with sleep terminal output
The parent consistently prints first due to the timing, but this is not reliable.

Introducing waitpid()

A more controlled way for synchronizing the execution order can be achieved with waitpid(). It halts the execution until the passed process terminates, allowing the parent process to wait for the completion of a specific child process before continuing its execution. waitpid() can also be used to retrieve and propagate the exit status of a child process (learn more here).

// waitpid_fork.c

#include <stdio.h> // prinft()
#include <unistd.h> // fork(), usleep()
#include <sys/types.h> // pid_t: int or long representing process ID's (PIDs)
#include <sys/wait.h> // waitpid()

int	main(void)
{
	pid_t	child_pid;

	printf("Before the fork!\n");
	child_pid = fork();
	printf("After the fork! Child PID: %d\n", child_pid);
	usleep(10); // Pause execution for 10 microseconds
	if (child_pid == 0) // Child process
		printf("Hello from the child! Child PID: %d\n", child_pid);
	else // Parent process
	{
		waitpid(child_pid, NULL, 0); // waits for the child process to finish
		printf("Hello from the parent! Child PID: %d\n", child_pid);
	}
	return (0);
}

fork with waitpid flow diagram
waitpid() guarantees the parent waits for the child to finish before continuing.

fork with waitpid terminal output
The child always prints before the parent: deterministic execution order.

Pipe()

A pipe creates a unidirectional communication channel between two processes. Calling pipe(int fd[2]) populates an array of two file descriptors: fd[0] for reading and fd[1] for writing. Data written to fd[1] can be read from fd[0], allowing a parent and child process to exchange data after a fork().

#include <unistd.h> // pipe(), read(), write()
#include <stdio.h> // printf()
#include <string.h> // strlen()

int	main(void)
{
	int	pipe_fd[2];
	pid_t	child_pid;
	pid_t	received_child_pid;
	char	message[] = "Hello from the child! PID:";
	char	buffer[42];

	pipe(pipe_fd); // Pipe initialization
	child_pid = fork();

	if (child_pid == 0) // Child process
	{
		close(pipe_fd[0]); // Close the read end of the pipe
		write(pipe_fd[1], message, strlen(message) + 1); // Write message to the pipe
		write(pipe_fd[1], &child_pid, sizeof(pid_t)); // Write child PID to the pipe
		close(pipe_fd[1]); // Close the write end of the pipe
	}
	else // Parent process
	{
		close(pipe_fd[1]); // Close the write end of the pipe
		printf("Here is the pareny! PID: %d\n", child_pid);
		read(pipe_fd[0], buffer, sizeof(buffer)); // Read message from pipe
		read(pipe_fd[0], &received_child_pid, sizeof(pid_t)); // Read child PID from pipe
		printf("The child says: '%s %d'\n", buffer, received_child_pid);
		close(pipe_fd[0]); // Close the read end of the pipe
	}

	return (0);
}

pipe flow diagram
The child writes to pipe_fd[1], the parent reads from pipe_fd[0]; unused ends are closed.

pipe terminal output
The parent successfully receives the message written by the child through the pipe.

Introducing dup2()

While pipe() establishes the communication channel, dup2() is the key to making it seamless. dup2(oldfd, newfd) duplicates a file descriptor, redirecting newfd to point to the same resource as oldfd. This allows us to redirect stdout (fd 1) to the pipe's write end, so that standard output functions like printf() automatically write into the pipe instead of the terminal:

#include <unistd.h> // pipe(), read()
#include <stdio.h> // printf()

int	main(void)
{
	int	pipe_fd[2];
	pid_t	child_pid;
	char	buffer[42];

	pipe(pipe_fd); // Pipe initialization
	child_pid = fork();

	if (child_pid == 0) // Child process
	{
		close(pipe_fd[0]); // Close the read end of the pipe
		dup2(pipe_fd[1], 1); // Redirect stdout (fd = 1) to the write end of the pipe
		// Now stdout is redirected to the pipe, so printf will write to the pipe
		printf("Hello from the child! PID: %d", child_pid);
		close(pipe_fd[1]); // Close the original write end of the pipe
	}
	else // Parent process
	{
		close(pipe_fd[1]); // Close the write end of the pipe
		printf("Here is the parent! PID: %d\n", child_pid);
		read(pipe_fd[0], buffer, sizeof(buffer)); // Read message from pipe
		printf("The child says: '%s'\n", buffer);
		close(pipe_fd[0]); // Close the read end of the pipe
	}

	return (0);
}

pipe with dup2 flow diagram
dup2() redirects stdout to the pipe — printf() output flows through the pipe to the parent.

pipe with dup2 terminal output
Same result as before, but the child uses printf() instead of write().


Pipex vs Shell

A comparison of pipex output against Z Shell (zsh) behavior, demonstrating that the program handles edge cases identically to the shell.

Invalid Input on Left Side

Single invalid inputs on the left side of the pipeline — non-existent infile, no access permissions, invalid command, and invalid command option:

invalid left side comparison 1

  • Exit status signals success (blue) — the left side is handled in a process that does NOT report its exit status to the parent. Only the right (last) process determines the overall exit status.
  • Multiple invalid inputs: Only the file-related issue is addressed, not the invalid command — the process exits after file access fails.
  • Outfile creation: An empty outfile.txt is created (rw-r--r-- permissions) even if the pipe call failed.

invalid left side comparison 2

Invalid Input on Right Side

Same behavior as the left side, but the exit status signals error (red) — the right side is handled in a process that reports its exit status to the parent:

invalid right side comparison

Invalid Input on Both Sides

Error messages for both sides are printed — processes handling each side run in parallel, so one process exiting does not prevent the other from executing:

invalid both sides comparison

Processes Run in Parallel

Using time to demonstrate that both sides of the pipeline execute concurrently. A sleep 1 | sleep 2 pipeline takes ~2 seconds total (not 3), proving parallel execution:

parallel execution proof 1

parallel execution proof 2

Outfile Overwrites Infile

When the same file is used as both input and output (e.g. < infile.txt wc -w | cat > infile.txt), the result is a file containing 0. This happens because the outfile is created first as an empty file (overwriting the actual infile), and THEN the processes are initiated.

Parent Waits for the Last Process to Finish

The parent process waits for the last (rightmost) child process to terminate before exiting, propagating its exit status:

parent waits for last process

< infile.txt yes | head > outfile.txt
./pipex infile.txt yes head outfile.txt

Acknowledgements

The project badge is from this repository by Ali Ogun.

About

Reproducing the shell pipe command. Managing processes and facilitating communication between them.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages