Skip to main content

Signal Overview

Minishell handles Unix signals to provide a user experience consistent with standard shells. The primary signals handled are SIGINT (Ctrl+C) and SIGQUIT (Ctrl+\).

Signal Setup

Signals are configured during shell initialization:
signals.c:25
void setup_signals(void)
{
    signal(SIGINT, handle_sigint);
    signal(SIGQUIT, SIG_IGN);
}
This function is called once in main() during shell startup, before entering the read-eval-print loop.

Signal Behavior

SIGINT (Ctrl+C)

Caught and handled with custom handler to interrupt current line

SIGQUIT (Ctrl+\)

Ignored in the parent shell process

SIGINT Handler

The handle_sigint() function provides graceful interrupt handling:
signals.c:15
void handle_sigint(int sig)
{
    (void)sig;
    printf("\n");
    rl_on_new_line();
    rl_replace_line("", 0);
    rl_redisplay();
    g_status = 130;
}

Handler Breakdown

1

Print Newline

printf("\n") moves the cursor to a new line, providing visual feedback
2

Reset Readline State

rl_on_new_line() tells readline that the cursor is on a new line
3

Clear Input Buffer

rl_replace_line("", 0) clears any partial input the user had typed
4

Redisplay Prompt

rl_redisplay() shows a fresh prompt, ready for new input
5

Set Exit Status

g_status = 130 follows the convention: 128 + signal number (SIGINT = 2)

Readline Integration

Minishell uses GNU Readline for input, which requires special handling with signals.

Readline Functions

Informs the readline library that the cursor has moved to a new line. This is essential after printing the newline in the signal handler to keep readline’s internal state consistent.When called: After printing output that moves the cursor
Replaces the current line buffer with a new string. The second argument controls the undo list behavior.Signature: void rl_replace_line(const char *text, int clear_undo)Usage in Minishell: rl_replace_line("", 0) clears the input buffer
Redraws the current line, including the prompt and any input. Called after modifying the line buffer to update the display.Effect: User sees a clean, new prompt without the interrupted input

Global Status Variable

The exit status is stored in a global variable accessible throughout the codebase:
main.c:15
int g_status = 0;
This variable serves multiple purposes:

Status Values

ValueMeaningSet By
0SuccessSuccessful command execution
1-255Command exit codeChild process exit
127Command not foundexecve() failure
130Interrupted by SIGINTSignal handler
2Syntax errorParser validation

Status in Variable Expansion

The special variable $? expands to the current status:
env_parsed2.c:57
else if (var_value[0] == '?')
    new_value = ft_itoa(g_status);
Example usage:
minishell$ false
minishell$ echo $?
1
minishell$ ls /nonexistent
ls: cannot access '/nonexistent': No such file or directory
minishell$ echo $?
2

Exit Status Extraction

After waiting for child processes, the raw status is converted to a standard exit code:
main.c:118
if (g_status != 2 && g_status != 1)
    g_status = (g_status >> 8) & 0xFF;
The waitpid() function stores the exit code in the high byte of the status integer. The bit shift >> 8 extracts the actual exit code (0-255).

Why the Check?

if (g_status != 2 && g_status != 1)
This condition prevents overwriting:
  • 2: Syntax errors set by the parser
  • 1: Special error conditions
These values are already in the correct format and shouldn’t be shifted.

Signal Behavior in Child Processes

Child processes inherit signal handlers from the parent. However, for proper job control, child processes should have default signal behavior.

Current Implementation

Minishell’s current implementation allows children to inherit the parent’s signal handlers. In a more complete implementation, child processes would reset signals:
// Hypothetical child signal reset (not in current code)
if (pid == 0)
{
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    // ... continue with execution
}
With SIG_IGN set for SIGQUIT in the parent, children inherit this behavior, meaning Ctrl+\ won’t generate a core dump in child processes as it would in bash.

Interactive vs Non-Interactive

The current signal setup assumes interactive mode. In a complete shell implementation, non-interactive mode (script execution) would handle signals differently:

Interactive Mode (Current)

  • SIGINT: Display new prompt
  • SIGQUIT: Ignored

Non-Interactive Mode (Hypothetical)

  • SIGINT: Terminate shell
  • SIGQUIT: Terminate shell

Signal Safety

The handle_sigint() function calls several functions that are not technically async-signal-safe:
printf("\n");          // Not async-signal-safe
rl_on_new_line();      // Readline function
rl_replace_line("", 0); // Readline function
rl_redisplay();        // Readline function
While this works in practice for readline-based applications, strictly conforming signal handlers should only call async-signal-safe functions. Readline provides its own signal handling facilities that are designed to work with signals.

Exit Status in Pipelines

In pipelines, the exit status is taken from the last command:
ft_execute_commands.c:92
aux[0] = -1;
while (++aux[0] < data->nbr_nodes)
    waitpid(data->nodes[aux[0]]->n_pid, &g_status, 0);
Each waitpid() overwrites g_status, so the final value reflects the last command in the pipeline:
minishell$ false | true | false
minishell$ echo $?
1
minishell$ false | true
minishell$ echo $?
0

Signal Flow Diagram

Testing Signal Handling

You can test signal behavior with these scenarios:
1

Interrupt During Input

Type a partial command and press Ctrl+C:
minishell$ echo hell^C
minishell$
The partial input should be cleared.
2

Interrupt Running Command

Run a long-running command and interrupt it:
minishell$ sleep 100
^C
minishell$ echo $?
130
3

SIGQUIT Should Be Ignored

Press Ctrl+\ at the prompt:
minishell$ ^\minishell$
Nothing should happen (no quit, no core dump).

Build docs developers (and LLMs) love