Linux Terminals tcgetattr(), tcsetattr() and the no_echo Example

 

Linux Terminals – Part 2
Chapter 62 | tcgetattr(), tcsetattr() and the no_echo Example
2
Key System Calls
3
When-to-Apply Options
Part 2/4
Series

Changing Terminal Settings at Runtime

All the flags we studied in Part 1 live inside a struct termios. But how do you actually read the current terminal settings, change them, and restore them safely? That is exactly what tcgetattr() and tcsetattr() are for.

This part teaches you how to use these two functions correctly, explains the three “when to apply” options for tcsetattr(), and walks through a real-world example: hiding password input by disabling terminal echo.

Keywords in This Part

tcgetattr() tcsetattr() struct termios TCSANOW TCSADRAIN TCSAFLUSH ECHO flag c_lflag STDIN_FILENO save & restore

tcgetattr() – Read Current Terminal Settings
#include <termios.h>

int tcgetattr(int fd, struct termios *termios_p);

/* Returns 0 on success, -1 on error */

What it does: Reads the current terminal attributes for the terminal referred to by the file descriptor fd and fills in the struct termios that termios_p points to.

fd
File Descriptor – which terminal?
Usually STDIN_FILENO (0), STDOUT_FILENO (1), or a file descriptor returned by open("/dev/ttyS0", ...). The fd must refer to a terminal — passing a regular file will fail with ENOTTY.
tp
termios_p – where to put the result
Pointer to a struct termios that will be filled with the terminal’s current settings. All five fields (c_iflag, c_oflag, c_cflag, c_lflag, c_cc[]) are populated.
#include <termios.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    struct termios tp;

    /* Read current settings of the terminal on stdin */
    if (tcgetattr(STDIN_FILENO, &tp) == -1) {
        perror("tcgetattr");
        return 1;
    }

    /* Print whether ICANON is currently set */
    if (tp.c_lflag & ICANON)
        printf("ICANON is ON  (canonical/line mode)\n");
    else
        printf("ICANON is OFF (noncanonical mode)\n");

    /* Print whether ECHO is currently set */
    if (tp.c_lflag & ECHO)
        printf("ECHO is ON  (typed chars are shown)\n");
    else
        printf("ECHO is OFF (typed chars are hidden)\n");

    return 0;
}
Important: tcgetattr() only works on file descriptors that refer to a terminal device. If you call it on a pipe or regular file, it returns -1 with errno set to ENOTTY (“Not a typewriter”). You can use isatty(fd) to check first.

tcsetattr() – Apply New Terminal Settings
#include <termios.h>

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

/* Returns 0 on success, -1 on error */

What it does: Applies the settings in the given struct termios to the terminal referred to by fd. The optional_actions argument controls when the change takes effect.

The Three When-to-Apply Options

TCSANOW
Change takes effect immediately, right now, without waiting for anything.
TCSADRAIN
Wait until all output already written has been transmitted, then change. Safe when changing output settings.
TCSAFLUSH
Wait for output to drain AND discard any unread input before changing. Best for changing input settings.
When to Use Which Option
Need change NOW? No pending I/O?
Use TCSANOW
Changing output settings?
Use TCSADRAIN (wait for output to finish)
Changing input settings? (most common)
Use TCSAFLUSH (drain output + discard queued input)
Why TCSAFLUSH for input? When you change input settings (e.g., turning off ECHO), you usually don’t want the user’s already-buffered keystrokes to be processed under the old settings. TCSAFLUSH discards those stale bytes before applying the new settings.
struct termios tp, saved;

/* Step 1: Read current settings */
tcgetattr(STDIN_FILENO, &tp);

/* Step 2: Save a copy to restore later */
saved = tp;

/* Step 3: Modify what you need */
tp.c_lflag &= ~ECHO;   /* Turn off echo */

/* Step 4: Apply - discard pending input first */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);

/* ... do your work ... */

/* Step 5: Restore original settings */
tcsetattr(STDIN_FILENO, TCSANOW, &saved);

The Golden Pattern – Save, Modify, Restore

When your program changes terminal settings, it must restore them when done. If your program crashes or exits without restoring, the terminal stays in the modified state — the user sees a broken shell where nothing echoes or Enter doesn’t work.

Safe Terminal Modification Flow
1
tcgetattr() – Snapshot the current state
Read and store the current termios into two variables: one to work with, one untouched backup.
2
Modify the working copy
Use bitwise AND/OR to set or clear specific flags. Never hard-code the whole struct — always start from the current values to preserve other settings.
3
tcsetattr() – Apply the changes
Use TCSAFLUSH for input changes, TCSADRAIN for output changes, TCSANOW when immediate effect is needed.
4
Do your work
The terminal now behaves as you configured.
5
tcsetattr() – Restore the saved copy
Apply the untouched backup. The terminal returns to exactly how it was before your program ran.
Always restore on error paths too! Use a cleanup function or signal handler to call tcsetattr() with the saved copy even if your program exits due to an error or signal. Otherwise the user’s terminal stays broken.

Real Example: Disabling Echo (Password Input)

The most common real-world use of tcgetattr/tcsetattr is hiding password input. When you type a password in Linux, the terminal normally echoes (shows) every character you type. To hide it, we turn off the ECHO flag.

What the ECHO Flag Does
ECHO = 1 (Default – characters shown)
User types ‘p’
Terminal driver
→ echoes ‘p’ back
Screen shows ‘p’
ECHO = 0 (Echo off – characters hidden)
User types ‘p’
Terminal driver
silently buffers
Screen shows nothing
Character is still received by the program — it just isn’t displayed.

Complete no_echo.c – Line by Line Walkthrough

#include <termios.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    struct termios tp, save;
    char buf[BUF_SIZE];

    /* ---- Step 1: Read current terminal settings ---- */
    if (tcgetattr(STDIN_FILENO, &tp) == -1) {
        perror("tcgetattr");
        exit(EXIT_FAILURE);
    }

    /* ---- Step 2: Save a copy for restoration later ---- */
    save = tp;              /* Struct copy - saves ALL fields */

    /* ---- Step 3: Clear only the ECHO bit ---- */
    tp.c_lflag &= ~ECHO;   /* &= ~ECHO clears bit, all others unchanged */

    /* ---- Step 4: Apply the change (discard pending input) ---- */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
        perror("tcsetattr");
        exit(EXIT_FAILURE);
    }

    /* ---- Step 5: Read input (user types but nothing shows) ---- */
    printf("Enter text: ");
    fflush(stdout);         /* Force prompt to appear before read */

    if (fgets(buf, BUF_SIZE, stdin) == NULL)
        printf("Got end-of-file/error on fgets()\n");
    else
        printf("\nRead: %s", buf);  /* \n because Enter was not echoed */

    /* ---- Step 6: Restore original terminal settings ---- */
    if (tcsetattr(STDIN_FILENO, TCSANOW, &save) == -1) {
        perror("tcsetattr restore");
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

Line-by-Line Explanation

struct termios tp, save;
Two copies of termios: tp = working copy we will modify, save = untouched backup for restoration.
tcgetattr(STDIN_FILENO, &tp)
Read ALL current terminal settings from stdin into tp. This captures the complete current state.
save = tp;
Struct assignment copies all fields. Now save holds the original values safe from any changes.
tp.c_lflag &= ~ECHO;
Bitwise AND with NOT ECHO: clears only the ECHO bit. All other bits in c_lflag stay exactly as they were.
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp)
Apply the modified tp. TCSAFLUSH: wait for output, discard pending input, then apply. Chosen because we are changing an input setting (ECHO).
fflush(stdout);
Forces the “Enter text: ” prompt to appear on screen before fgets() blocks. Without this, buffered output might not show up.
tcsetattr(STDIN_FILENO, TCSANOW, &save)
Restore original settings immediately. TCSANOW used here because we just need to restore quickly with no pending I/O concern at this point.
Example Run:
$ ./no_echo
Enter text: <– user types “hello” but nothing appears on screen
Read: hello <– program still received the text correctly

Extended Example – Read Password with Proper Cleanup

The simple example above has a problem: if the user presses Ctrl+C while echo is off, the program exits without restoring the terminal. Here is a more robust version:

#include <termios.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

#define BUF_SIZE 256

static struct termios saved_tp;   /* Global so signal handler can use it */
static int terminal_modified = 0;

/* Signal handler to restore terminal before exit */
static void cleanup_handler(int sig)
{
    if (terminal_modified) {
        tcsetattr(STDIN_FILENO, TCSANOW, &saved_tp);
        terminal_modified = 0;
    }
    /* Re-raise the signal with default action */
    signal(sig, SIG_DFL);
    raise(sig);
}

int read_password(char *buf, int maxlen)
{
    struct termios tp;
    struct sigaction sa, old_sa;

    /* Install cleanup handler for Ctrl+C */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = cleanup_handler;
    sigaction(SIGINT, &sa, &old_sa);

    /* Save and modify terminal settings */
    if (tcgetattr(STDIN_FILENO, &tp) == -1)
        return -1;

    saved_tp = tp;           /* Save for restoration */
    terminal_modified = 1;

    tp.c_lflag &= ~ECHO;    /* Disable echo */
    tp.c_lflag &= ~ECHONL;  /* Also disable echoing newline */

    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
        terminal_modified = 0;
        return -1;
    }

    /* Read the password */
    printf("Password: ");
    fflush(stdout);

    int result = 0;
    if (fgets(buf, maxlen, stdin) == NULL) {
        result = -1;
    } else {
        /* Remove trailing newline */
        int len = strlen(buf);
        if (len > 0 && buf[len-1] == '\n')
            buf[len-1] = '\0';
        printf("\n");   /* Move to next line (Enter was not echoed) */
    }

    /* Restore original settings */
    tcsetattr(STDIN_FILENO, TCSANOW, &saved_tp);
    terminal_modified = 0;

    /* Restore original signal handler */
    sigaction(SIGINT, &old_sa, NULL);

    return result;
}

int main(void)
{
    char password[BUF_SIZE];

    if (read_password(password, sizeof(password)) == -1) {
        fprintf(stderr, "Failed to read password\n");
        return 1;
    }

    printf("Password length: %zu chars\n", strlen(password));

    /* Clear the password from memory when done */
    memset(password, 0, sizeof(password));

    return 0;
}
Real World: The C standard library provides getpass() which does exactly this, but it is marked obsolete. The above pattern (save, disable echo, read, restore, handle signals) is the recommended approach for modern programs.

Bonus: isatty() and ttyname() – Checking Terminal Identity

Two helper functions often used alongside tcgetattr/tcsetattr:

int isatty(int fd); /* Returns 1 if fd is a terminal, 0 otherwise */

char *ttyname(int fd); /* Returns name like “/dev/pts/3”, or NULL */

#include <unistd.h>
#include <stdio.h>

int main(void)
{
    /* Check stdin */
    if (isatty(STDIN_FILENO))
        printf("stdin IS a terminal: %s\n", ttyname(STDIN_FILENO));
    else
        printf("stdin is NOT a terminal (maybe a pipe or file)\n");

    /* Check stdout */
    if (isatty(STDOUT_FILENO))
        printf("stdout IS a terminal: %s\n", ttyname(STDOUT_FILENO));
    else
        printf("stdout is NOT a terminal (maybe redirected)\n");

    return 0;
}
/* Output when run normally:
   stdin IS a terminal: /dev/pts/0
   stdout IS a terminal: /dev/pts/0

   Output when run as: ./prog < /dev/null
   stdin is NOT a terminal (maybe a pipe or file)
   stdout IS a terminal: /dev/pts/0
*/
Why check isatty() first? If you call tcgetattr() on a non-terminal fd (like a pipe), it fails with ENOTTY. Many programs check isatty() first and skip terminal configuration when stdin/stdout is redirected.

Common Mistakes to Avoid
❌ Mistake 1: Hard-coding the entire termios struct
/* WRONG - overwrites all settings you don't know about */
struct termios tp;
memset(&tp, 0, sizeof(tp));
tp.c_lflag = ICANON | ECHO;   /* Lost all other settings! */
tcsetattr(STDIN_FILENO, TCSANOW, &tp);
/* RIGHT - start from current settings, change only what you need */
struct termios tp;
tcgetattr(STDIN_FILENO, &tp);
tp.c_lflag &= ~ECHO;          /* Only change ECHO, rest preserved */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);
❌ Mistake 2: Not restoring terminal on exit
/* WRONG - exits without restoring */
tcgetattr(STDIN_FILENO, &tp);
tp.c_lflag &= ~ECHO;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);
do_stuff();
exit(0);   /* Echo is still OFF in the shell! */
/* RIGHT - always restore */
tcgetattr(STDIN_FILENO, &tp);
save = tp;
tp.c_lflag &= ~ECHO;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);
do_stuff();
tcsetattr(STDIN_FILENO, TCSANOW, &save);  /* Restore! */
exit(0);
❌ Mistake 3: Forgetting fflush() before blocking read
/* WRONG - prompt might not appear before fgets blocks */
printf("Enter password: ");
fgets(buf, BUF_SIZE, stdin);

/* RIGHT - flush stdout so prompt is visible immediately */
printf("Enter password: ");
fflush(stdout);
fgets(buf, BUF_SIZE, stdin);

Interview Questions – tcgetattr / tcsetattr
Q1. What is the purpose of tcgetattr() and tcsetattr()?
tcgetattr() reads the current terminal attributes (all flags and special characters) of a terminal file descriptor into a struct termios. tcsetattr() writes a modified struct termios back to the terminal, changing how the terminal driver processes input and output. Together they form the standard POSIX API for terminal configuration.
Q2. What are the three optional_actions values for tcsetattr() and when should each be used?
TCSANOW applies the change immediately. TCSADRAIN waits for all pending output to be transmitted before applying — use this when changing output-related settings to avoid corruption. TCSAFLUSH also waits for output to drain but additionally discards any unread input bytes before applying — use this when changing input settings (like turning off ECHO) so stale keystrokes under the old settings are thrown away.
Q3. In the no_echo example, why is TCSAFLUSH used in tcsetattr() but TCSANOW used for restoration?
TCSAFLUSH is used when disabling ECHO because it discards any input that was typed (and echoed) before the setting change takes effect — ensuring no stale echoed characters remain. TCSANOW is used for restoration because at that point we just want to return the terminal to normal immediately; there is no concern about pending I/O corrupting anything.
Q4. What happens if tcgetattr() is called on a file descriptor that is not a terminal?
It returns -1 and sets errno to ENOTTY (“Not a typewriter”). This happens when the fd refers to a regular file, pipe, or socket. Programs that may run with redirected stdin/stdout should call isatty(fd) before calling tcgetattr() to avoid this error.
Q5. How do you correctly clear a single terminal flag without affecting other flags?
Use the bitwise AND with the bitwise NOT of the flag: tp.c_lflag &= ~FLAGNAME. This clears only the specified bit while leaving all other bits unchanged. To set a flag, use OR: tp.c_lflag |= FLAGNAME. Never assign a hard-coded value to the flag field because that destroys all the other settings you did not explicitly set.
Q6. Why must you call fflush(stdout) before a blocking read when the terminal is in modified state?
stdout is typically line-buffered when connected to a terminal, meaning output is only flushed when a newline is written or the buffer is full. A prompt like “Enter password: ” does not end with a newline, so it may stay in the buffer and never appear on screen while the program blocks in fgets(). Calling fflush(stdout) forces the buffered prompt to be written out immediately.
Q7. How would you make a terminal modification safe against Ctrl+C interruption?
Install a signal handler for SIGINT (and optionally SIGTERM, SIGQUIT) that calls tcsetattr() with the saved original struct termios before allowing the process to exit. Store the saved termios in a global variable and use a flag to track whether the terminal has been modified. In the handler, restore the terminal and then re-raise the signal with the default action so the program exits normally.

Continue Learning

Next: Understand Canonical Mode in depth — line buffering, line editing, and special characters.

Part 3: Canonical Mode → ← Part 1: Terminal Flags

Leave a Reply

Your email address will not be published. Required fields are marked *