Linux Terminals – Part 3
Chapter 62 | Canonical Mode – Line Buffering, Delimiters & Line Editing
What is Canonical Mode?
When you open a terminal and type something, the text goes through the terminal driver before reaching your program. In canonical mode (also called “cooked mode”), the terminal driver collects your keystrokes and holds them in a line buffer. Your program does not receive any of those characters until you press Enter (or another line delimiter). This is the default mode for most terminals.
This mode is called “canonical” because it is the standard, well-defined way of handling input — and it supports full line editing before you commit the line by pressing Enter.
Keywords in This Part
ICANON canonical mode line buffer line delimiter NL / EOL / EOL2 EOF (Ctrl+D) ERASE (Backspace) KILL (Ctrl+U) WERASE (Ctrl+W) REPRINT (Ctrl+R) LNEXT (Ctrl+V) IEXTEN NOFLSH
How Canonical Mode Works – The Big Picture
Canonical mode is enabled by setting the ICANON flag in c_lflag. When active, here is what happens between you pressing a key and the program receiving it:
Canonical Mode Data Flow
You press a key
→
Terminal Driver
→
Line Buffer
on delimiter →
Your Program (read())
The terminal driver holds all characters in the line buffer. Your program’s read() call blocks until a complete line is available (i.e., until a line delimiter is received).
Visualizing the Line Buffer
User types: h e l l o (not yet pressed Enter)
← program’s read() is still blocked, waiting for delimiter
User presses Enter (NL delimiter)
← read() unblocks, returns “hello\n” to program
Partial reads: If the program calls read() requesting fewer bytes than the line contains, only the requested bytes are returned. The remaining bytes stay in the buffer and are returned by the next read() call. The line buffer is not discarded until all bytes have been read.
Line Delimiters – What Ends a Canonical Line?
A “line” in canonical mode ends when one of five possible delimiters is received. Understanding each is important because they behave slightly differently:
NL
The standard newline character (\n), generated by pressing Enter. This is the most common line terminator. The NL character is included in the data returned to the program.
EOL
An additional end-of-line character (not set by default). Configured via c_cc[VEOL]. Like NL, it is included in the returned data. Rarely used in practice.
EOL2
A second additional end-of-line character. Only active when both ICANON and IEXTEN are set. Also included in the returned data. Configured via c_cc[VEOL2].
EOF
End-of-file character — default is Ctrl+D. Unlike other delimiters, EOF is not included in the returned data. Special rule: EOF only works as a delimiter when it is not at the very start of the line. If Ctrl+D is pressed at an empty line start, read() returns 0 (indicating end of file).
CR
Carriage return (\r). Only treated as a line delimiter when the ICRNL flag is set (which maps CR to NL on input — the default). With ICRNL, pressing Enter sends CR which gets mapped to NL.
EOF (Ctrl+D) Special Behavior
Case 1: Ctrl+D in the middle of a line
User presses Ctrl+D →
read() returns “hel” (3 bytes, no EOF char appended). Next read() returns 0.
Case 2: Ctrl+D on an empty line
Line buffer is empty, user presses Ctrl+D →
read() returns 0 immediately (signals EOF to the program, like end of file).
/* Demonstrating EOF behavior in canonical mode */
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char buf[100];
ssize_t n;
while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
printf("Read %zd bytes: ", n);
write(STDOUT_FILENO, buf, n);
}
if (n == 0)
printf("EOF received (Ctrl+D on empty line)\n");
return 0;
}
/* Run: ./prog
Type: hello<Enter> → Read 6 bytes: hello\n
Type: <Ctrl+D> → EOF received
*/
Line Editing in Canonical Mode
One of the most useful features of canonical mode is that the user can edit the current line before pressing Enter. The terminal driver handles these editing operations, so your program never sees the deleted or corrected characters — it only gets the final, edited line.
These editing characters are stored in the c_cc[] array and are enabled by the ICANON flag (with some also requiring IEXTEN):
ERASE
Backspace or Ctrl+H
Deletes the last character typed. Like pressing Backspace in a text editor. Configured via c_cc[VERASE].
KILL
Ctrl+U
Deletes the entire current line — all characters typed since the last line delimiter. Configured via c_cc[VKILL].
WERASE
Ctrl+W
Deletes the last word typed (like Alt+Backspace in many editors). Requires IEXTEN. Configured via c_cc[VWERASE].
REPRINT
Ctrl+R
Reprints the current input line on a new line — useful after output has mixed with your typed input. Requires IEXTEN. Configured via c_cc[VREPRINT].
LNEXT
Ctrl+V
Escapes the next character — makes the next character literal (not interpreted as a special character). Requires IEXTEN. Example: Ctrl+V then Ctrl+C sends a literal ^C to the program. Configured via c_cc[VLNEXT].
ERASE in Action – Buffer Before and After
Buffer after typing “hello”:
User presses Backspace (ERASE):
Terminal driver deletes ‘o’. Program never saw it.
User presses Ctrl+U (KILL) — entire line erased:
Line buffer is now empty. Program never saw any of it.
IEXTEN requirement: WERASE, REPRINT, LNEXT, and EOL2 only work when both ICANON and IEXTEN flags are set. If IEXTEN is off, pressing Ctrl+W or Ctrl+V sends the character literally to the line buffer without any special treatment.
/* Customize the ERASE character to Delete key (0x7F)
and KILL character to Ctrl+X (0x18) */
#include <termios.h>
#include <unistd.h>
struct termios tp;
tcgetattr(STDIN_FILENO, &tp);
tp.c_cc[VERASE] = 0x7F; /* Delete key */
tp.c_cc[VKILL] = 0x18; /* Ctrl+X */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);
How read() Behaves in Canonical Mode
In canonical mode, a read() call on a terminal blocks until a complete line is available in the line buffer. Once a line delimiter is received, the line becomes available and read() returns.
read() Lifecycle in Canonical Mode
Program calls read()
→
Blocks (sleeping)
→ delimiter arrives
read() returns with data
There are three ways a canonical-mode read() can return:
1. A complete line is available
read() returns with up to the requested number of bytes from the line buffer. If the line is longer than requested, remaining bytes stay in the buffer for the next read().
2. EOF is detected
If a line has been partially typed and EOF (Ctrl+D) arrives, read() returns the partial line (without the EOF character). A subsequent read() will return 0.
3. A signal interrupts the read()
If a signal handler is invoked while read() is blocked, read() is interrupted. If SA_RESTART is not set for the signal, read() returns -1 with errno = EINTR.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(void)
{
char buf[20]; /* Deliberately small to show partial reads */
ssize_t n;
/* Read up to 5 bytes at a time */
while ((n = read(STDIN_FILENO, buf, 5)) > 0) {
printf("read() returned %zd bytes: [", n);
for (ssize_t i = 0; i < n; i++) {
if (buf[i] == '\n')
printf("\\n");
else
printf("%c", buf[i]);
}
printf("]\n");
}
return 0;
}
/* Example:
Type: "hello world" then Enter
Output:
read() returned 5 bytes: [hello]
read() returned 5 bytes: [ worl]
read() returned 2 bytes: [d\n]
The 12-byte line was split across three 5-byte read() calls.
*/
Key insight: In canonical mode, the unit of data availability is the line, not the byte. Even if you request 1 byte, read() blocks until a full line arrives, then returns 1 byte from that line. The rest stays buffered.
Signals and Queue Flushing in Canonical Mode
Some key combinations in canonical mode generate signals. The terminal driver also flushes queues when these signals are generated — unless NOFLSH is set (covered in Part 1).
| Key |
Character |
Signal Sent |
Default Queue Behavior |
| Ctrl+C |
INTR (c_cc[VINTR]) |
SIGINT |
Input + Output queues flushed |
| Ctrl+\ |
QUIT (c_cc[VQUIT]) |
SIGQUIT |
Input + Output queues flushed |
| Ctrl+Z |
SUSP (c_cc[VSUSP]) |
SIGTSTP |
Input + Output queues flushed |
Flushing happens regardless of signal disposition. Even if your program has set SIGINT to be ignored with signal(SIGINT, SIG_IGN), the queue flush still happens when Ctrl+C is pressed (unless NOFLSH is set). This can surprise programmers who think ignoring SIGINT means nothing happens.
/* Demonstrate: even with SIGINT ignored, Ctrl+C flushes queues
unless NOFLSH is set */
#include <termios.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
struct termios tp;
/* Ignore SIGINT */
signal(SIGINT, SIG_IGN);
/* Without NOFLSH: Ctrl+C still flushes queues (default) */
tcgetattr(STDIN_FILENO, &tp);
/* Option A: allow flush (default - don't set NOFLSH) */
/* Option B: prevent flush */
tp.c_lflag |= NOFLSH;
tcsetattr(STDIN_FILENO, TCSANOW, &tp);
printf("Type something, then Ctrl+C, then Enter:\n");
char buf[100];
if (fgets(buf, sizeof(buf), stdin))
printf("Got: %s\n", buf); /* With NOFLSH: pre-Ctrl+C text survives */
return 0;
}
Practical Example – Custom Line Reader in Canonical Mode
This example reads lines from the terminal in canonical mode, counts them, and shows how partial reads accumulate into complete lines:
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define MAX_LINE 1024
/* Reads exactly one full line from terminal in canonical mode.
Returns number of bytes read (including \n), or -1 on error/EOF. */
int read_canonical_line(int fd, char *buf, int maxlen)
{
int total = 0;
ssize_t n;
/*
* In canonical mode, once a line is available, read() will return
* the complete line (or up to maxlen-1 bytes of it).
* We read in a loop to handle partial reads (when maxlen is small).
*/
while (total < maxlen - 1) {
n = read(fd, buf + total, 1); /* Read one byte at a time */
if (n <= 0) {
if (total == 0) return -1; /* EOF or error before any data */
break;
}
total += n;
if (buf[total - 1] == '\n') /* Got newline = end of canonical line */
break;
}
buf[total] = '\0';
return total;
}
int main(void)
{
char line[MAX_LINE];
int linenum = 0;
int n;
printf("Type lines (Ctrl+D to stop):\n");
while ((n = read_canonical_line(STDIN_FILENO, line, MAX_LINE)) > 0) {
linenum++;
/* Strip trailing newline for display */
int len = strlen(line);
if (len > 0 && line[len-1] == '\n')
line[len-1] = '\0';
printf("Line %d (%d chars): \"%s\"\n", linenum, len - 1, line);
}
printf("Total lines read: %d\n", linenum);
return 0;
}
Key observation: Even though we call read() one byte at a time, it only returns after a complete line is available (because ICANON is on). The one-byte read loop simply walks through the already-available line.
Terminology: Canonical Mode vs “Cooked” Mode
You may see two terms used for the same thing — here is how they relate:
Canonical Mode
POSIX term. Defined by the ICANON flag. Means line-at-a-time input with line editing.
“Cooked” Mode
Historical UNIX term. Means the terminal driver “cooks” (processes) the raw keystrokes. Essentially the same as canonical mode with all standard processing enabled.
“Raw” Mode
Opposite extreme. ICANON off, ECHO off, signals disabled. Every byte goes straight to the program — no processing at all. Covered in Part 4.
Why “cooked”? The metaphor is that raw input from the keyboard is “cooked” (processed, transformed) by the terminal driver before being served to the application. Raw mode gives you the unprocessed bytes directly.
Interview Questions – Canonical Mode
Q1. What is canonical mode in Linux terminal handling and how is it enabled?
Canonical mode is the standard terminal input mode where the terminal driver collects keystrokes into a line buffer and only makes them available to the reading process once a line delimiter is received. It is enabled by setting the ICANON flag in the c_lflag field of struct termios. In this mode, line editing characters (backspace, word erase, line kill) are processed by the driver before the data reaches the application.
Q2. What are the five line delimiters in canonical mode and how do they differ?
The five delimiters are: NL (newline \n from pressing Enter — included in returned data), EOL (additional end-of-line char, seldom used — included), EOL2 (second additional delimiter, requires IEXTEN — included), EOF (Ctrl+D — not included in data; on empty line causes read() to return 0), and CR (carriage return, used as delimiter when ICRNL maps it to NL). EOF is special because it only terminates a line if typed at a non-initial position; at the start of a line it signals end-of-file.
Q3. If read() requests 3 bytes but the canonical line has 10 bytes, what happens?
read() blocks until the complete 10-byte line is available in the buffer. Once the line delimiter arrives and the full line is ready, read() returns 3 bytes (the amount requested). The remaining 7 bytes stay in the line buffer and will be returned by subsequent read() calls. The line buffer is not discarded until all bytes have been read.
Q4. What is the difference between ERASE, KILL, and WERASE characters?
ERASE (default: Backspace) deletes the single character immediately before the cursor in the line buffer. KILL (default: Ctrl+U) deletes the entire current line — all characters typed since the last line delimiter. WERASE (default: Ctrl+W) deletes the last word typed — it removes characters back to the preceding whitespace boundary. WERASE requires both ICANON and IEXTEN to be set.
Q5. What is the LNEXT character and why is it useful?
LNEXT (default: Ctrl+V) causes the next character typed to be treated as a literal character rather than as a special terminal control character. For example, typing Ctrl+V followed by Ctrl+C sends a literal ^C character to the program instead of generating SIGINT. This is useful when you want to include a special character in the input. LNEXT requires both ICANON and IEXTEN to be set.
Q6. Can a canonical-mode read() be interrupted before a line delimiter arrives?
Yes. If a signal is delivered to the process while read() is blocked waiting for a line, the read() call is interrupted. If the signal handler was established without SA_RESTART, read() returns -1 with errno set to EINTR. If SA_RESTART was specified, the kernel automatically restarts the read() after the handler returns. Programs need to handle EINTR correctly by either retrying the read() or propagating the error.
Q7. Why do text editors like vi disable canonical mode?
Canonical mode requires a line delimiter before delivering input to the program. Text editors need to respond to every single keystroke immediately — pressing ‘j’ should move the cursor down, pressing ‘d’ twice should delete a line, etc. — without requiring the user to press Enter after each command. To achieve this, they disable ICANON (noncanonical mode) and set MIN=1, TIME=0, so each keypress is delivered to the program immediately as it is typed.