Reading & Writing Terminal Attributes tcgetattr() and tcsetattr() explained simply

 

Reading & Writing Terminal Attributes
Chapter 62 โ€” tcgetattr() and tcsetattr() explained simply
๐Ÿ“˜ Theory
๐Ÿ’ป Code Examples
๐ŸŽฏ Interview Q&A

What Are Terminal Attributes?

When you open a terminal on Linux, the terminal driver (inside the kernel) controls how your input and output behave. For example: does pressing Enter send a newline? Does the terminal echo what you type? Is Ctrl+C treated as an interrupt signal?

All these behaviors are controlled through a structure called termios. The two key system calls to read and write this structure are tcgetattr() and tcsetattr().

Think of termios as a configuration panel for the terminal โ€” you can read its current settings, change some options, and write it back.

Key Terms in This Chapter
termios tcgetattr() tcsetattr() TCSANOW TCSADRAIN TCSAFLUSH c_lflag ECHO terminal driver SIGTTOU EIO

1. tcgetattr() โ€” Reading Terminal Attributes

tcgetattr() reads the current settings of a terminal into a struct termios variable. You give it a file descriptor pointing to the terminal (like STDIN_FILENO) and it fills in the structure.

Function signature:

#include <termios.h>

int tcgetattr(int fd, struct termios *termios_p);
/* Returns 0 on success, -1 on error */

fd โ€” file descriptor of the terminal (e.g., STDIN_FILENO, STDOUT_FILENO, or an open terminal file).
termios_p โ€” pointer to a struct termios that will be filled with the current settings.

2. tcsetattr() โ€” Writing Terminal Attributes

tcsetattr() writes new settings back to the terminal driver. It has an extra argument โ€” when โ€” that controls when the change takes effect.

Function signature:

#include <termios.h>

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
/* Returns 0 on success, -1 on error */

The optional_actions argument:

Value When the change takes effect Use case
TCSANOW Immediately, right now Quick changes that must take effect instantly
TCSADRAIN After all pending output is sent to terminal Changes that affect output โ€” wait for queued output to finish first
TCSAFLUSH After all pending output is sent, AND pending input is discarded Reading passwords โ€” discard any type-ahead the user may have typed

How tcsetattr() applies changes โ€” visual flow
Your code calls tcsetattr()
โ†’
TCSANOW?
Apply immediately
|
TCSADRAIN?
Wait for output queue
|
TCSAFLUSH?
Wait + discard input
โ†’
Terminal driver updated

3. The Recommended Pattern โ€” Read, Modify, Write

You should never build a termios structure from scratch and pass it to tcsetattr(). The reason: the structure has many fields and flags. If you build it from zero, you will accidentally reset things you didn’t mean to change.

The correct pattern is always:

  1. Read current attributes into a local copy using tcgetattr()
  2. Modify only the specific fields you want to change
  3. Write the modified copy back using tcsetattr()

This guarantees a fully initialized structure is always passed to tcsetattr().

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

void errExit(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void) {
    struct termios tp;

    /* Step 1: Read current terminal attributes */
    if (tcgetattr(STDIN_FILENO, &tp) == -1)
        errExit("tcgetattr");

    /* Step 2: Modify โ€” turn off ECHO flag */
    tp.c_lflag &= ~ECHO;

    /* Step 3: Write modified attributes back.
       TCSAFLUSH: wait for output to drain AND discard pending input.
       This is ideal when reading passwords. */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1)
        errExit("tcsetattr");

    printf("Echo is now OFF. Type something (it won't show):\n");

    char buf[128];
    fgets(buf, sizeof(buf), stdin);

    printf("You typed: %s\n", buf);

    /* Restore echo before exiting */
    tp.c_lflag |= ECHO;
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1)
        errExit("tcsetattr restore");

    return 0;
}

Note: We use bitwise AND with NOT (&= ~ECHO) to clear the ECHO bit. To turn echo back on, we use bitwise OR (|= ECHO) to set the bit.

4. Important Behaviors of tcsetattr()

Partial success:

tcsetattr() returns success (0) if any of the requested changes were applied โ€” even if not all were. It returns -1 only if none of the changes could be applied. So after calling tcsetattr(), if you want to confirm all changes were applied, call tcgetattr() again and compare.

Background process groups โ€” SIGTTOU:

If a process in a background process group calls tcsetattr(), the terminal driver sends the signal SIGTTOU to the entire process group, suspending it. This is the kernel’s way of saying “background processes should not mess with the terminal.”

Orphaned process group โ€” EIO:

If tcsetattr() is called from an orphaned process group (no parent that is a session leader), it fails with errno = EIO.

Older UNIX โ€” ioctl():

In older UNIX systems, terminal attributes were controlled using ioctl() calls directly. The problem was that ioctl() cannot type-check its third argument. POSIX introduced tcgetattr()/tcsetattr() as cleaner, type-safe wrappers. On Linux, these functions internally call ioctl() anyway, but you get proper type checking at the C level.

5. Full Example โ€” Password Input Without Echo

A practical real-world use case: reading a password without showing it on screen.

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

#define MAX_PASS 64

int main(void) {
    struct termios old_tp, new_tp;
    char password[MAX_PASS];

    /* Save current terminal settings */
    if (tcgetattr(STDIN_FILENO, &old_tp) == -1) {
        perror("tcgetattr");
        exit(EXIT_FAILURE);
    }

    /* Copy and modify: disable echo and canonical mode */
    new_tp = old_tp;
    new_tp.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);

    /* Apply new settings โ€” TCSAFLUSH discards any type-ahead */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_tp) == -1) {
        perror("tcsetattr");
        exit(EXIT_FAILURE);
    }

    printf("Enter password: ");
    fflush(stdout);

    if (fgets(password, MAX_PASS, stdin) != NULL) {
        /* Remove trailing newline */
        password[strcspn(password, "\n")] = '\0';
    }

    printf("\n"); /* move to new line after user pressed Enter */

    /* Restore original terminal settings */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_tp) == -1) {
        perror("tcsetattr restore");
        exit(EXIT_FAILURE);
    }

    printf("Password entered (length=%zu). [Not shown]\n", strlen(password));

    /* In real code: verify password here, then clear it from memory */
    memset(password, 0, sizeof(password));

    return 0;
}

๐ŸŽฏ Interview Questions & Answers

Q1. What is the termios structure and what does it represent?

The termios structure is the kernel’s configuration block for a terminal device. It holds flags for input processing (c_iflag), output processing (c_oflag), control modes (c_cflag), local modes (c_lflag), line speed (c_ispeed/c_ospeed), and the special character array (c_cc). Modifying this structure controls all aspects of terminal behavior such as echo, canonical mode, and signal generation.

Q2. What is the difference between TCSANOW, TCSADRAIN, and TCSAFLUSH?

TCSANOW applies changes immediately. TCSADRAIN waits for all queued output to be transmitted first, making it safe when changing output-related settings. TCSAFLUSH does everything TCSADRAIN does, but additionally discards any pending (unread) input โ€” making it ideal when disabling echo before reading a password.

Q3. Why should you never construct a termios structure from scratch before calling tcsetattr()?

The termios structure has many fields beyond what you intend to change. Building from scratch means all other fields start at zero or garbage values, effectively resetting settings you did not intend to touch. The correct approach is to read the current settings with tcgetattr(), modify only the needed fields, and write back with tcsetattr().

Q4. tcsetattr() returned 0, but your change did not take effect. Why?

tcsetattr() returns 0 (success) if any of the requested changes were applied โ€” not necessarily all. If you need to verify, call tcgetattr() again after tcsetattr() and compare the result with what you intended to set.

Q5. What happens if a background process calls tcsetattr()?

The terminal driver sends the signal SIGTTOU to the entire background process group, causing it to stop. If the process is in an orphaned process group, the call fails with errno set to EIO instead.

Q6. How do you turn off terminal echo in C?

Read current attributes with tcgetattr(), then clear the ECHO bit in c_lflag using tp.c_lflag &= ~ECHO, then write back with tcsetattr() using TCSAFLUSH. Always restore the original settings before the program exits.

Q7. Why did POSIX introduce tcgetattr()/tcsetattr() when ioctl() already existed?

ioctl() takes a void * third argument, so the compiler cannot type-check it. This makes it easy to pass the wrong structure type without any warning. tcgetattr() and tcsetattr() are typed wrappers that let the compiler catch type mismatches at compile time. Internally on Linux, they still call ioctl() under the hood.

Continue Learning

Next: Learn to use the stty command to view and change terminal settings from the shell

Next: stty Command โ†’ Home

Leave a Reply

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