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 inxxd
(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 overridesICRNL
.) 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 ofIXOFF
. 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 ofIXON
. The manual page says that when this is on, you can suspend input using Ctrl+S and Ctrl+Q as inIXON
. However, I tried this and it appears not to do anything.IXANY
: When this is on, andIXON
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) whenlogin
(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 pressFOO<C-u>BAR
, you will seeFOO^UBAR
, but the program reading will only seeBAR
. When the flag is on, you will also only seeBAR
.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. IfISIG
is off (inc_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 theIEXTEN
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. WhenIEXTEN
is off, Ctrl+V sends the literal ASCII 0x16.VQUIT
: The character that sends SIGQUIT when pressed, normally ASCII 0x1C or Ctrl+\. Only works whenISIG
is on; otherwise the literal character is sent.VSUSP
: The character that sends SIGTSTP when pressed, normally ASCII 0x1A or Ctrl+Z. Only works whenISIG
is on; otherwise the literal character is sent.VSTOP
: WhenIXON
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
: WhenIXON
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 ifIXANY
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 typestruct winsize*
and retrieves the window size into it. This struct contains the fieldsws_row
andws_col
; these areshort int
s that respectively indicate the number of rows and number of columns in the terminal window.TIOCSWINSZ
: takes a single argument of typeconst 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 aconst 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 useTIOCSTI
. This could be useful for remote assistance-type software, or it could be used for malicious purposes.TIOCSCTTY
andTIOCNOTTY
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
andKDSETLED
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
andKDSKBLED
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 along
as an argument, eitherKD_TEXT
orKD_GRAPHICS
, and sets either text mode or graphics mode, respectively.KDGETMODE
takes along*
as an argument and retrieves the mode. I will not discuss graphics mode here.KDGKBMODE
andKDSKBMODE
have the same prototypes asKDGETMODE
andKDSETMODE
, and allowable values areK_RAW
,K_XLATE
,K_MEDIUMRAW
, andK_UNICODE
,. By default the keyboard is in theK_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 byK_XLATE
. These applications useK_RAW
.K_MEDIUMRAW
is similar toK_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 howK_UNICODE
works, either.KDGKBMETA
andKDSKBMETA
again have the same prototype; this time the allowable values areK_METABIT
andK_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!