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
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.
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.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;
}
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.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
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);
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.
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.
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
$ ./no_echoEnter text: <– user types “hello” but nothing appears on screenRead: hello <– program still received the text correctlyThe 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;
}
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.Two helper functions often used alongside tcgetattr/tcsetattr:
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
*/
/* 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);
/* 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);
/* 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);
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.Continue Learning
Next: Understand Canonical Mode in depth — line buffering, line editing, and special characters.
