Direct Terminal Input

While raw terminal input allows standard I/O functions to capture uncooked text, another approach for reading the terminal may also capture a few uncooked morsels. This process involves using low-level file I/O commands. These functions are read() and write(), which are part of the POSIX standard.

As I explained in another post, the popular fopen() function reads formatted data, which is what the f stands for. On the other hand, the open() function reads data in the raw. But how raw?

The following code uses the read() and write() functions to process standard I/O. A prompt is output, input is gathered, then the text input is sent to standard output.

2026_01_24-Lesson.c

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

int main()
{
    char prompt[] = "Type away: ";
    char buffer[BUFSIZ];
    int ch,offset;

    /* output the prompt */
    write(fileno(stdout),prompt,sizeof(prompt));

    /* read standard input until the newline */
    offset = 0;
    do
    {
        read(fileno(stdin),&ch,1);
        buffer[offset] = ch;
        offset++;
        /* test for overflow */
        if( offset>BUFSIZ )
            break;
    }
    while( ch!='\n' );

    /* output the buffer */
    write(fileno(stdout),buffer,offset);

    /* clean-up */
    return 0;
}

The first use of the write() function is to send a prompt to standard output:

write(fileno(stdout),prompt,sizeof(prompt));

Here I use fileno(stdout) to obtain the file number for standard output. The unistd.h header file also defines the STDOUT_FILENO constant, which serves the same purpose.

The second argument is the address of the string to send, prompt[]. This argument is followed by the number of characters to write, obtained by using the sizeof operator. The value returned doesn’t include the terminating null character, though the null character isn’t necessary for writing a string to standard output in this manner.

A do-while loop reads standard input until the newline is fetched or the buffer is full. The read() function fetches one character at a time from the standard input device’s file descriptor:

read(fileno(stdin),&ch,1);

Again, I use the fileno() function where I could instead use the STDIN_FILENO defined constant. Next comes the address of the buffer to store input, a single integer variable ch in this example. The final argument is the number of characters to read, one.

The character input is stored in the buffer, then a check is made for overflow. The loop repeats until the newline is captured.

The program’s final task is to output the characters input. I use the write() function again, with the input buffer specified and variable output as the buffer’s size. This approach works even though no null character terminates the buffer as variable output holds the buffer’s size. (Technically, the buffer holds characters only, not a valid string.)

Here’s a sample run:

Type away: Hello, there!^H^H^H^H^H^H^H^H
Hello, there!

After I typed Hello, there!, I pressed Ctrl+H to backspace — but the program didn’t backspace! Instead, you see a series of ^H (control-H) characters appear. These characters don’t appear in the output, but they dwell in the buffer, captured by the read() function. Other control characters appear onscreen as well, though they’re not saved in the buffer. In fact, the Backspace key on a PC keyboard generates the ^? (Del or ASCII 127) character, which isn’t saved in the buffer.

The weirdness here is that reading standard input in the raw may seem like raw terminal input, but it’s not. It’s one step closer, with some characters (such as ^H) put into the buffer but others behaving as they normally do with cooked input. It’s important to know the difference between these modes to ensure that your program is fetching the data it needs.

Leave a Reply