Execution Overview
The execution engine transforms parsed command nodes into running processes. It handles single commands, pipelines, redirections, and distinguishes between built-in and external commands.
Node Preparation
Before execution, the shell creates execution nodes from the tokenized commands using ft_prepare_nodes():
int ft_prepare_nodes (t_mini * data )
{
if ( ! check_wrong_redir ( data -> commands ))
return ( printf ( "syntax error: redirection \n " ), 1 );
else if ( ! check_wrong_pipes ( data -> commands ))
return ( printf ( "syntax error: pipes \n " ), 1 );
else
data -> nodes = ft_create_nodes (data);
return ( 0 );
}
Node Creation
Each node in a pipeline is created independently:
t_node * ft_create_nodes_aux ( char ** commands , t_mini * data )
{
t_node * new;
new = malloc ( sizeof (t_node));
if ( ! new)
return ( NULL );
new -> infile = STDIN_FILENO;
new -> outfile = STDOUT_FILENO;
new -> full_cmd = set_full_cmd (commands, 0 , 0 );
if ( new -> full_cmd != NULL )
new -> full_path = set_full_path (new, data -> bin_path );
if ( ! set_infile_outfile (new, commands, STDOUT_FILENO, STDIN_FILENO))
new -> is_set = 0 ;
else
new -> is_set = 1 ;
if ( new -> full_cmd == NULL )
new -> is_set = 0 ;
new -> n_pid = - 1 ;
free (commands);
return (new);
}
The is_set flag tracks whether the node is valid. Invalid redirections or missing commands set this to 0, causing the node to be skipped during execution.
Command Array Construction
The set_full_cmd() function extracts command arguments from tokens, filtering out redirection operators:
char ** set_full_cmd ( char ** commands , int i , int cmd )
{
char ** full_cmd;
cmd = count_cmd (commands);
i = 0 ;
if (cmd > 0 )
full_cmd = malloc ( sizeof ( char * ) * (cmd + 1 ));
else
return ( NULL );
if ( ! full_cmd)
return ( NULL );
cmd = 0 ;
while ( commands [i] != NULL )
{
if ( is_redirection ( commands [i]))
i ++ ; // Skip redirection operator and its target
else
full_cmd [cmd ++ ] = commands [i];
i ++ ;
}
full_cmd [cmd] = NULL ;
return (full_cmd);
}
Path Resolution
Minishell resolves command paths by searching the PATH environment variable.
The set_bin_path() function splits the PATH variable into directories:
void set_bin_path (t_mini * data )
{
char ** temp;
char * path;
path = find_env_var (( data -> env ), "PATH=" );
if (path != NULL )
{
temp = ft_split (path, '=' );
path = temp [ 1 ];
if (temp)
data -> bin_path = ft_split (path, ':' );
free ( temp [ 0 ]);
free ( temp [ 1 ]);
free (temp);
}
else
data -> bin_path = NULL ;
}
Finding Executables
The set_full_path() function searches PATH directories for the command:
char * set_full_path (t_node * node , char ** bin_path )
{
char * path;
int i;
char * temp;
i = 0 ;
if ( access ( node -> full_cmd [ 0 ], X_OK) == 0 && ! is_builtin ( node -> full_cmd [ 0 ]))
return ( ft_strdup ( node -> full_cmd [ 0 ]));
if (bin_path && node -> full_cmd && ! is_builtin ( node -> full_cmd [ 0 ]))
{
while ( bin_path [i] != NULL )
{
temp = ft_strjoin ( bin_path [i], "/" );
path = ft_strjoin (temp, node -> full_cmd [ 0 ]);
free (temp);
if ( access (path, X_OK) == 0 )
return (path);
free (path);
i ++ ;
}
return ( ft_strdup ( node -> full_cmd [ 0 ]));
}
return ( NULL );
}
Check Direct Path
If the command starts with / or ./, check if it’s executable
Skip Built-ins
Built-in commands don’t need path resolution
Search PATH
Iterate through PATH directories, testing each with access()
Return Result
Return the first executable match, or the original command if not found
Execution Dispatcher
The ft_execute_commands() function determines the execution strategy:
void ft_execute_commands (t_mini * data )
{
pid_t pid;
int pipefd [ 2 ];
int i;
pid = 0 ;
i = 0 ;
if ( data -> nbr_nodes == 1 && data -> nodes [ 0 ]-> full_cmd != NULL )
{
while ( data -> nodes [ 0 ]-> full_cmd [i])
remove_quotes ( data -> nodes [ 0 ]-> full_cmd [i ++ ], 0 );
if ( is_builtin ( data -> nodes [ 0 ]-> full_cmd [ 0 ])
&& data -> nodes [ 0 ]-> is_set )
prepare_builtin (data, data -> nodes [ 0 ]);
else
{
if ( data -> nodes [ 0 ]-> is_set == 1 )
execute_simple_command (data, data -> nodes [ 0 ], pid);
}
}
else if ( data -> nbr_nodes > 1 )
excecute_pipe_sequence (data, pipefd);
if (TEMP_FILE)
ft_clean (data);
}
Built-in commands in single-command scenarios run in the parent process to allow them to modify the shell’s environment (e.g., cd, export).
Single Command Execution
External commands are executed in a child process:
ft_execute_one_command.c:15
void execute_simple_command (t_mini * data , t_node * node , pid_t pid )
{
pid = fork ();
if (pid == - 1 )
return ;
else if (pid == 0 )
{
if ( node -> infile != STDIN_FILENO)
{
if ( dup2 ( node -> infile , STDIN_FILENO) == - 1 )
printf ( "Error with input file \n " );
close ( node -> infile );
}
if ( node -> outfile != STDOUT_FILENO)
{
if ( dup2 ( node -> outfile , STDOUT_FILENO) == - 1 )
printf ( "Error with output file \n " );
close ( node -> outfile );
}
if ( execve ( node -> full_path , node -> full_cmd , data -> execute_envp ) == - 1 )
{
printf ( " %s : command not found \n " , node -> full_cmd [ 0 ]);
exit ( 127 );
}
}
else
waitpid (pid, & g_status, 0 );
}
Built-in Execution
Built-ins run in the parent but with redirected file descriptors:
ft_execute_one_command.c:44
void prepare_builtin (t_mini * data , t_node * node )
{
int original_in;
int original_out;
original_in = dup (STDIN_FILENO);
original_out = dup (STDOUT_FILENO);
if ( node -> infile != STDIN_FILENO)
{
if ( dup2 ( node -> infile , STDIN_FILENO) == - 1 )
printf ( "Error with input file \n " );
close ( node -> infile );
}
if ( node -> outfile != STDOUT_FILENO)
{
if ( dup2 ( node -> outfile , STDOUT_FILENO) == - 1 )
printf ( "Error with output file \n " );
close ( node -> outfile );
}
execute_builtin ( data -> nodes [ 0 ]-> full_cmd , data -> env , & data);
dup2 (original_in, STDIN_FILENO);
dup2 (original_out, STDOUT_FILENO);
close (original_in);
close (original_out);
}
The original file descriptors are saved and restored after the built-in executes, ensuring the parent shell’s I/O remains unchanged.
Pipeline Execution
Pipelines are executed with multiple processes connected by pipes:
void excecute_pipe_sequence (t_mini * data , int pipefd [ 2 ])
{
int aux [ 2 ];
aux [ 0 ] = - 1 ;
aux [ 1 ] = - 1 ;
while ( data -> nodes [ ++ aux [ 0 ]])
{
if ( pipe (pipefd) == - 1 )
return ;
data -> nodes [ aux [ 0 ]]-> n_pid = fork ();
if ( data -> nodes [ aux [ 0 ]]-> n_pid == - 1 )
return ;
else if ( data -> nodes [ aux [ 0 ]]-> n_pid == 0
&& data -> nodes [ aux [ 0 ]]-> is_set == 1 )
child_process (data, data -> nodes [ aux [ 0 ]], aux, pipefd);
else
{
close ( pipefd [ 1 ]);
if ( aux [ 1 ] != - 1 )
close ( aux [ 1 ]);
aux [ 1 ] = pipefd [ 0 ];
}
}
aux [ 0 ] = - 1 ;
while ( ++ aux [ 0 ] < data -> nbr_nodes )
waitpid ( data -> nodes [ aux [ 0 ]]-> n_pid , & g_status, 0 );
}
Pipeline Mechanics
A new pipe is created for each command in the pipeline: if ( pipe (pipefd) == - 1 )
return ;
pipefd[0] is the read end, pipefd[1] is the write end.
Each child redirects its input/output: void child_process (t_mini * data , t_node * node , int aux [ 2 ], int pipefd [ 2 ])
{
if ( node -> infile != STDIN_FILENO)
{
if ( dup2 ( node -> infile , STDIN_FILENO) == - 1 )
printf ( "Error with input file or pipe output \n " );
close ( node -> infile );
}
else if ( aux [ 1 ] != - 1 )
{
if ( dup2 ( aux [ 1 ], STDIN_FILENO) == - 1 )
printf ( "Error with input file or pipe output \n " );
close ( aux [ 1 ]);
}
if ( node -> outfile != STDOUT_FILENO)
{
if ( dup2 ( node -> outfile , STDOUT_FILENO) == - 1 )
printf ( "Error withoutput file \n " );
close ( node -> outfile );
}
else if ( aux [ 0 ] < data -> nbr_nodes - 1 )
dup2 ( pipefd [ 1 ], STDOUT_FILENO);
close ( pipefd [ 0 ]);
close ( pipefd [ 1 ]);
child_execution (data, node);
}
The parent closes the write end and saves the read end for the next command: close ( pipefd [ 1 ]);
if ( aux [ 1 ] != - 1 )
close ( aux [ 1 ]);
aux [ 1 ] = pipefd [ 0 ];
aux[1] holds the read end of the previous pipe, which becomes the input for the next command.
After all processes are forked, the parent waits for them: aux [ 0 ] = - 1 ;
while ( ++ aux [ 0 ] < data -> nbr_nodes)
waitpid (data -> nodes [ aux [ 0 ]] -> n_pid , & g_status , 0 );
The exit status of the last command is stored in g_status.
File Descriptor Management
File descriptor lifecycle in pipelines:
Create Pipe
pipe(pipefd) creates a unidirectional channel
Fork Child
Child inherits all parent file descriptors
Redirect Child
dup2() redirects stdin/stdout to pipe ends
Close Unused FDs
Both ends of the pipe are closed in the child after duplication
Parent Cleanup
Parent closes write end, keeps read end for next child
Built-in Detection
The shell identifies built-in commands to execute them without forking:
int is_builtin ( char * str )
{
if ( ! ft_strncmp (str, "echo" , 4 ) || ! ft_strncmp (str, "cd" , 2 ) || \
! ft_strncmp (str, "pwd" , 3 ) || ! ft_strncmp (str, "export" , 6 ) || \
! ft_strncmp (str, "unset" , 5 ) || ! ft_strncmp (str, "env" , 3 ) || \
! ft_strncmp (str, "exit" , 4 ))
return ( 1 );
return ( 0 );
}
In pipelines, even built-ins are executed in child processes to maintain proper pipeline semantics. Only single-command built-ins run in the parent.
Exit Status Handling
The global status variable is updated after command execution:
if (g_status != 2 && g_status != 1 )
g_status = (g_status >> 8 ) & 0x FF ;
This extracts the actual exit code from the waitpid() status value, which encodes the exit code in the high byte.