Skip to main content

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():
ft_prepare_nodes.c:88
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:
ft_prepare_nodes.c:41
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:
set_full_cmd_path.c:77
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.

Extracting PATH

The set_bin_path() function splits the PATH variable into directories:
set_bin_path.c:26
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:
set_full_cmd_path.c:25
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);
}
1

Check Direct Path

If the command starts with / or ./, check if it’s executable
2

Skip Built-ins

Built-in commands don’t need path resolution
3

Search PATH

Iterate through PATH directories, testing each with access()
4

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:
ft_execute_commands.c:97
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:
ft_execute_commands.c:68
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:
ft_execute_commands.c:41
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:
1

Create Pipe

pipe(pipefd) creates a unidirectional channel
2

Fork Child

Child inherits all parent file descriptors
3

Redirect Child

dup2() redirects stdin/stdout to pipe ends
4

Close Unused FDs

Both ends of the pipe are closed in the child after duplication
5

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:
set_full_cmd_path.c:15
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:
main.c:118
if (g_status != 2 && g_status != 1)
    g_status = (g_status >> 8) & 0xFF;
This extracts the actual exit code from the waitpid() status value, which encodes the exit code in the high byte.

Build docs developers (and LLMs) love