Terminally confused (part four)


In part three, I introduced terminal ioctls and terminal capabilities. This post will focus on the nitty-gritty, hopefully without devolving into a set of manual pages.

All terminal ioctls can be performed using the function ioctl(2). However, most of them can also be performed using the POSIX functions tcgetattr(3), tcsetattr(3), tcsendbreak(3), tcdrain(3), tcflush(3), tcflow(3), tcgetpgrp(3), tcsetpgrp(3), and tcgetsid(3). Notice that the number (3) indicates that these are library functions, whereas ioctl, with the number 2, is an actual system call; ultimately, each of these library functions must call ioctl on the given terminal descriptor.

I’m not going to talk about what tcsendbreak(3), tcdrain(3), tcflush(3), and tcflow(3) do; you can read the termios(3) manual page if you’re curious. However, as I promised, these map onto ioctl(2) calls, with request codes TCSBRK (with a zero argument), TCSBRK (with nonzero argument), TCFLSH, and TCXONC, respectively.

The functions tcgetattr(3) and tcsetattr(3) are defined in termios.h. Their prototypes are as follows:
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
These functions get and set the terminal attributes, also known as line settings. (The word “line” here refers not to rows of characters on the terminal, but rather something like the modem line connecting a physical terminal to the rest of a computer.) The former always succeeds, provided that fd is a valid file descriptor to a terminal device. The latter returns 0 on success and -1 on failure, but “success” only means that some of the desired changes to terminal attributes were successful; it might not necessarily mean that all changes succeeded.

The terminal attributes are stored in a structure of type struct termios, whose definition can be found in termios.h. The structure is divided into four sets of flags: input modes, which are found in the c_iflag member, output modes in c_oflag, control modes in c_cflag, and local modes in c_lflag. It also contains an array of characters named c_cc, the control characters for that terminal. POSIX specifies that certain flags must exist in each of the four sets, and also specifies certain control characters that must exist in c_cc, but does not specify that no further flags and control characters must exist.

Here are some examples of input mode flags. This information is taken from the termios(3) manual page.

  • INLCR: If this is set, every newline input is translated into a carriage return. You can test this by typing Ctrl+J in xxd(1); you should see that it is translated to ASCII 0x0D, that is, a carriage return.
  • ICRNL: If this is set, every carriage return input is translated into a newline. This is normally on, which is why pressing Enter (which sends a carriage return) results in programs receiving a newline character. If you turn it off, pressing Enter will echo “^M” instead of sending a newline. In either case, Ctrl+V Ctrl+M results in a literal carriage return, unless you have Ctrl+V quoting disabled (see below).
  • IGNCR: If this is set, carriage returns are simply ignored. (This overrides ICRNL.) Pressing Enter, then, will have no effect. Obviously, this is normally off. When it is on, you can still input a newline by pressing Ctrl+J.
  • IUCLC (non-POSIX): Normally off, this flag results in all uppercase characters being translated to lowercase characters.
  • IXON: Not the opposite of IXOFF. When on, you can suspend output at any time by pressing Ctrl+S (this can be reconfigured), which means attempts to write to the terminal will block. Output can be resumed with Ctrl+Q (again, reconfigurable).
  • IXOFF: Not the opposite of IXON. The manual page says that when this is on, you can suspend input using Ctrl+S and Ctrl+Q as in IXON. However, I tried this and it appears not to do anything.
  • IXANY: When this is on, and IXON is also on, the result is that typing any character (not just Ctrl+Q) will resume output on the terminal when suspended.

Here’s an example of how to use tcsetattr(3):

#include <stdio.h>
#include <termios.h>
int main()
{
    struct termios T_orig, T;
    tcgetattr(0, &T_orig); // this is bad because it doesn't check that stdin is actually a terminal
    T = T_orig;
    T.c_iflag |= IUCLC;
    tcsetattr(0, TCSANOW, &T);
    printf("Choose a hipster name (lowercase only): ");
    char buf[51];
    scanf("%50s", buf);
    printf("Hello %s, welcome to your new life as a hipster\n", buf);
    tcsetattr(0, TCSANOW, &T_orig);
    return 0;
}

Note that IUCLC does not affect output, so “Choose” and “Hello” will be properly capitalized. On the other hand, it becomes quite impossible to type in “Brian”; instead, “brian” will appear on the terminal, cementing my transformation into a hipster. Remember always to leave the terminal in the same state you found it in, unless your program is supposed to change the terminal state (stty(1) and reset(1) are good examples). Note also that even though we are changing the input mode, we don’t have to use 0 as the file descriptor; any file descriptor to the terminal will work, even a write-only one (i.e., typically 1 and 2 would work.)

You’ll notice I didn’t talk about the second argument to tcsetattr(3) yet. This indicates an optional action to perform before the change. TCSANOW simply means: perform the change now without further delay. There are two other possible arguments: TCSADRAIN, which should be used when making changes that affect output, and TCSAFLUSH. I won’t go into the details, as they are not terribly interesting.

The following are some output mode flags:

  • OLCUC (non-POSIX): When set, translate outputted lowercase characters to uppercase. Obviously, this is normally off.
  • ONLCR: When set, outputted newlines are translated into the CR-NL sequence. This is normally on, which is why, when a program writes a newline character, the cursor returns to the first column (carriage return) and advances down one line (newline).
  • OCRNL: When set, outputted carriage returns are translated into newlines.

I’ll skip the control mode flags, because most of them deal with serial lines, and I don’t want to get into that.

The following are some local mode flags:

  • ISIG: When this is on, as it usually is, pressing Ctrl+C results in SIGINT, Ctrl+\ results in SIGQUIT, and Ctrl+Z results in SIGTSTP; furthermore, even if a program ignores or catches these signals, it won’t see the character, though you can enter these characters literally by pressing Ctrl+V first. All of these characters are reconfigurable.
  • ICANON: On for canonical mode, off for noncanonical mode. In canonical mode, input entered at the terminal does not become available for reading until the line is finished, usually by pressing Enter (or Ctrl+D is pressed to signal the end of the file), and the line may be edited before it has been sent. In particular, you can press Ctrl+U to erase the entire current line. In noncanonical mode, each character becomes available for reading as it is pressed, and you can’t edit lines; if you press Backspace or Ctrl+U, a literal character will be sent as input.
  • ECHO: This is normally on, and results in characters typed in being echoed. On a local terminal, such as one of the numbered ttys, this just means that when you press a key, you see a character appear on the screen. This is turned off (for example) when login(1) is asking you to enter your password, so that nobody will be able to see your password as it is being typed in.
  • ECHOK: This is normally on. When it is off, in canonical mode, Ctrl+U can still be used to erase the entire current line, but the screen won’t show the current line as being erased; instead, it echos the Ctrl+U. So if you press FOO<C-u>BAR, you will see FOO^UBAR, but the program reading will only see BAR. When the flag is on, you will also only see BAR.
  • ECHOCTL (non-POSIX): Normally on, which results in control characters being echoed by adding 64 (0x40) to the ASCII code and prepending “^”. (The exceptions are tabs, newlines, and suspend/resume characters.) For example, pressing Ctrl+A, which generates ASCII code 1, results in “^A” being echoed. This is simply the reverse of how Ctrl works in the first place: pressing Ctrl+key results in 0x40 being subtracted from the key’s ASCII value. For example, you can send a null character by pressing Ctrl+@, because “@” has ASCII value 0x40. When this is off, nothing will be shown at all, since these characters are unprintable.
  • TOSTOP: This is always off whenever I test it, but I’m not sure whether some systems or shells might set it to on by default. When it is on, background processes attempting to write to the terminal will receive a SIGTTOU, and, at any rate, the output won’t appear. When it is off, background processes can write to the terminal with no problem.

You can play around with all these settings using stty(1). For example, after entering stty igncr, you’ll be unable to enter any more commands at the shell by pressing Enter, until you’ve run stty -igncr. (Exercise: How do you enter this command, since Enter doesn’t work?)

Modifying individual attributes is a bit of a pain in C, though, which is presumably the explanation behind the appeal of terrible code like system("stty -echo"). Here’s a short program I wrote that shows how to modify terminal attributes with greater facility using some boilerplate:

#include <stddef.h>
#include <stdio.h>
#include <termios.h>
#define TCSETBIT(fd, opt, type, bit) \
    tc_bit_helper(fd, TCSA##opt, offsetof(struct termios, c_##type## flag),\
                  -1, bit)
#define TCCLRBIT(fd, opt, type, bit) \
    tc_bit_helper(fd, TCSA##opt, offsetof(struct termios, c_##type## flag),\
                  ~bit, 0)
struct termios T_orig;
int tc_bit_helper(int fd, int opt, size_t ofs, tcflag_t mask1, tcflag_t mask2)
{
    struct termios T;
    tcgetattr(fd, &T);
    tcflag_t* target = (tcflag_t*)((void*)(&T) + ofs);
    *target = *target & mask1 | mask2;
    return tcsetattr(fd, opt, &T);
}
int main()
{
    tcgetattr(0, &T_orig);
    TCCLRBIT(0, NOW, l, ECHO);
    printf("Password: ");
    scanf("%*s");
    TCSETBIT(0, NOW, l, ECHO);
    printf("\nPassword accepted. Enter 1 to launch the nukes, 0 to cancel: ");
    int option;
    scanf("%d", &option);
    if (option == 1)
        printf("Nukes launched. Have a nice day.\n");
    else if (option == 0)
        printf("Operation aborted.\n");
    else
        printf("Invalid code. Operation aborted\n");
    tcsetattr(0, TCSANOW, &T_orig);
    return 0;
}

In case you didn’t know this, the C preprocessor concatenates strings with the ## operator. So the statement TCCLRBIT(0, NOW, l, ECHO) becomes tc_bit_helper(fd, TCSANOW, offsetof(struct termios, c_lflag), -1, bit). The function tc_bit_helper uses the offsetof-computed argument to determine which member of struct termios to modify. Note that tcflag_t is the type of the members c_iflag and so on.

Note also that since echoing is off while the password is being entered, we have to manually output a newline after it is entered; otherwise, “Password accepted…” will appear on the same line as “Password: “.

Some of the characters in the c_cc have the following meanings:

  • VEOF: Set to ASCII 0x04 by default, that is, Ctrl+D. Signals the end of file on the terminal. This only works in canonical mode; in noncanonical mode, the process reading will receive the literal character.
  • VERASE: Set to ASCII 0x7F by default, the character generated by Backspace. Does what backspace normally does, that is, erases the character to the left of the cursor. This also only works in canonical mode.
  • VINTR: The character that sends SIGINT when pressed, normally ASCII 0x03 or Ctrl+C. If ISIG is off (in c_lflags), then the literal character is sent.
  • VKILL: The character that erases the current line, normally ASCII 0x15 or Ctrl+U. Only works in canonical mode; in noncanonical mode the literal character is sent.
  • VLNEXT: The character that quotes the next character, normally ASCII 0x16 or Ctrl+V. Only works when the IEXTEN flag is set (I didn’t talk about this). So, for example, Ctrl+V Ctrl+C sends a literal ASCII 0x03 instead of a SIGINT. When IEXTEN is off, Ctrl+V sends the literal ASCII 0x16.
  • VQUIT: The character that sends SIGQUIT when pressed, normally ASCII 0x1C or Ctrl+\. Only works when ISIG is on; otherwise the literal character is sent.
  • VSUSP: The character that sends SIGTSTP when pressed, normally ASCII 0x1A or Ctrl+Z. Only works when ISIG is on; otherwise the literal character is sent.
  • VSTOP: When IXON is on, this is the character that suspends output. When it’s off, the literal character is sent. The default is Ctrl+S, or ASCII 0x13.
  • VSTART: When IXON is on, this is the character that resumes output after it has been suspended. When it’s off, the literal character is sent. The default is Ctrl+Q, or ASCII 0x11. Note that if IXANY is on, this character loses its special status, as any key will resume suspended output.

The above characters are fully configurable, so that, for example, you can make it so that Ctrl+E ends the file instead of Ctrl+D, using stty eof ^E (where you enter the “^E” by typing Ctrl+V Ctrl+E). Each character can also be disabled by setting its value to _POSIX_VDISABLE. Here’s how you can do it in C:

#include <stdio.h>
#include <termios.h>
#include <bits/posix_opt.h> // needed for _POSIX_VDISABLE
#define TCSETCHR(fd, opt, idx, val) \
    tc_chr_helper(fd, TCSA##opt, idx, val)
struct termios T_orig;
int tc_chr_helper(int fd, int opt, size_t idx, cc_t val) // cc_t is the type of the elements of c_cc
{
    struct termios T;
    tcgetattr(fd, &T);
    T.c_cc[idx] = val;
    return tcsetattr(fd, opt, &T);
}
int main()
{
    tcgetattr(0, &T_orig);
    TCSETCHR(0, NOW, VINTR, _POSIX_VDISABLE);
    printf("Ctrl+C disabled! Try to interrupt me now, puny mortal!\n");
    scanf("%*s");
    TCSETCHR(0, NOW, VEOF, _POSIX_VDISABLE);
    printf("Hint: to close this program, type in an end-of-file.\n");
    char c;
    int taunted = 0;
    do
    {
        c = getchar();
        if (c == 4 && !taunted)
        {
            printf("Ctrl+D isn't EOF anymore! nyaa nyaa!\n");
            TCSETCHR(0, NOW, VEOF, 5);
            taunted = 1;
        }
    }
    while (c != EOF);
    tcsetattr(0, TCSANOW, &T_orig);
    return 0;
}

So that’s it for the terminal attributes—there are more, but you’ll have to see the man page for them. For reference, tcgetattr(3) corresponds to the ioctl(2) with TCGETS as the request code. tcsetattr(3) is more complicated: it maps to three different request codes, depending on whether the optional action is TCSANOW, TCSADRAIN, or TCSAFLUSH: TCSETS, TCSETSW, and TCSETSF, respectively.

Wait, I lied. (Okay, I did this on purpose.) POSIX also states that the terminal input and output speeds, in bits per second (baud rates) must be inside the struct termios somewhere, but doesn’t specify where. This parameter is important for serial lines, but appears to have no effect on local terminals. In Linux, the baud rate is stored in the 5-bit CBAUD field in c_cflags; other systems might do things differently. However, POSIX does specify that you can get and set these fields of struct termios using the functions cfgetispeed(3), cfsetispeed(3), cfgetospeed(3), and cfsetospeed(3); I won’t go over the details. These functions, unlike the ones starting with tc, do not perform any ioctl(2); they simply operate on struct termios variables.

The functions tcgetpgrp(3), tcsetpgrp(3), and tcgetsid(3) are defined in unistd.h, unlike the others, which are defined in termios.h. You won’t find them in the termios(3) man page, but rather in their own separate man pages. They are used to get and set the foreground process group of a terminal. This is part of job control, and I will discuss it in a future part. They correspond to the ioctl(2) request codes TIOCGPGRP, TIOCSPGRP, and TIOCGSID, respectively.

You’ll notice that the terminal attributes don’t include the number of rows and columns on the terminal. For whatever reason, this was never included in the struct termios, and is one of those things that can only be done by ioctl(2). In addition to the request codes mentioned in the foregoing discussion, here are some interesting ones:

  • TIOCGWINSZ: takes a single argument of type struct winsize* and retrieves the window size into it. This struct contains the fields ws_row and ws_col; these are short ints that respectively indicate the number of rows and number of columns in the terminal window.
  • TIOCSWINSZ: takes a single argument of type const struct winsize* and sets the window size. These values are passed to the terminal driver on the kernel side, but they are not used by the terminal itself; instead, the next time a program retrieves the terminal window size, it will receive the previously stored values. Furthermore, the foreground process group receives the window size change signal, SIGWINCH, when the terminal window size changes.
  • TIOCSTI: takes a const char* to a single character and adds it to the terminal input queue, as though it had been entered at the keyboard. You see, simply writing to a terminal device only causes characters to show on the screen; it does not cause processes reading from the terminal to receive those characters. If you want to “fake” input, you have to use TIOCSTI. This could be useful for remote assistance-type software, or it could be used for malicious purposes.
  • TIOCSCTTY and TIOCNOTTY are used to set and remove the controlling terminal for the calling process’s session, respectively. I will discuss these with job control.
  • TIOCGSID returns the ID of the session controlled by the given terminal. I will discuss this with job control.

The following ioctls are not only non-POSIX but also specific to the Linux system console and numbered virtual terminals, so that they are documented in console_ioctl(4) instead of tty_ioctl(4). The reason why they are restricted to these terminals is that they simply wouldn’t make sense for other kinds of terminals, like serial lines and pseudoterminals; many of them have something to do with the keyboard and the monitor, which are associated exclusively with those terminals.

  • KDGETLED and KDSETLED are used to get and set the statuses (on/off) of the keyboard LEDs corresponding to Caps Lock, Num Lock, and Scroll Lock. I won’t get into the details, as you’re not likely to ever have to do this.
  • KDGKBLED and KDSKBLED actually set the states of Caps Lock, Num Lock, and Scroll Lock. (I bet you didn’t know that this could be done separately from toggling the LEDs.)
  • KDSETMODE takes a long as an argument, either KD_TEXT or KD_GRAPHICS, and sets either text mode or graphics mode, respectively. KDGETMODE takes a long* as an argument and retrieves the mode. I will not discuss graphics mode here.
  • KDGKBMODE and KDSKBMODE have the same prototypes as KDGETMODE and KDSETMODE, and allowable values are K_RAW, K_XLATE, K_MEDIUMRAW, and K_UNICODE,. By default the keyboard is in the K_XLATE state, which means scan codes from the keyboard driver are converted into ASCII character codes. One consequence of this is that a program cannot know when a modifier key alone is pressed (such as Ctrl). For many reasons, some applications might prefer to receive raw keyboard events as scan codes; the X server, for example, which provides an abstraction for the keyboard that replaces the one provided by K_XLATE. These applications use K_RAW. K_MEDIUMRAW is similar to K_RAW but handles special keys differently (such as those annoying keys most modern keyboards have for pausing Windows Media Player and such); I don’t know the details because they are not documented (unless you consider the Linux source code to be documentation). I’m not sure how K_UNICODE works, either.
  • KDGKBMETA and KDSKBMETA again have the same prototype; this time the allowable values are K_METABIT and K_ESCPREFIX. When the former setting is active, holding down Alt (also known as the Meta key) with another key results in setting the high order bit (that is, adding 0x80 to the ASCII code); when the latter setting is active, the chord is instead translated into an escape sequence starting with ^[ (0x1B).
  • VT_ACTIVATE, as mentioned in part one, puts a given virtual terminal into the foreground. Its argument is the number of the terminal you want to switch to, starting from one.
  • VT_WAITACTIVE takes a terminal number as argument and waits for that terminal to become active (foreground).

As an example, here’s a rather lame, incomplete implementation of chvt(1):

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <linux/vt.h> // for VT_ACTIVATE
const char* tty_prefix = "/dev/tty";
int main(int argc, char** argv)
{
    if (argc == 1)
    {
        fprintf(stderr, "usage: %s ttynum\n", argv[0]);
        exit(1);
    }
    char* buf = malloc(strlen(tty_prefix) + strlen(argv[1]) + 1);
    strcpy(buf, tty_prefix);
    strcat(buf, argv[1]);
    int fd = open(buf, O_RDWR);
    if (fd == -1)
    {
        fprintf(stderr, "Couldn't get a file descriptor referring to the console\n");
        exit(1);
    }
    return -ioctl(fd, VT_ACTIVATE, atoi(argv[1]));
}

Note that the real chvt(1) is a bit more sophisticated, on my system at least. The code I gave above will fail if it can’t open the target terminal; on the other hand, the real chvt(1) tries a lot harder to obtain a file descriptor to some terminal that the VT_ACTIVATE call will accept, only giving up when it exhausts all its options.

If you ever find yourself wondering how a program does something involving terminals, and you suspect that an ioctl is involved, strace(1) is your friend. I usually do something like strace chvt 1 2>&1 | less and search for an ioctl. If the call is TCGETS, TCSETS, TCSETSW, or TCSETSF, then strace(1) will even be nice enough to print out the struct termios argument in human-readable form, like {B38400 opost isig -icanon -echo ...}.


Well, I said I was going to cover terminal ioctls and terminal capabilities, but it looks like I’m out of space for today. I’ll cover terminal capabilities in part five, I promise!

About Brian

Hi! I'm Brian Bi. As of November 2014 I live in Sunnyvale, California, USA and I'm a software engineer at Google. Besides code, I also like math, physics, chemistry, and some other miscellaneous things.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a comment