0
char buf[1];
if (argc == 1) {
    while (read(STDIN_FILENO, buf, 1) > 0) {
        write(1, buf, sizeof(buf));
    }
}

I have a few things I'd like to clarify about this snippet. We run it, ./exec_file Let's say we just press Enter. We move to the next line and read 1 byte '\n' then write it to stdout bringing us down one more line... simple enough. Now lets say we type h, then Enter. The program spits out h on the next line with an invisible '\n'.

Looking at the code after we type h it reads it into the buffer then writes it to stdout but somehow the program waits to spit it out on the next line till after I've pressed Enter..how?

Lastly, when we first hit the while loop wouldn't read initially return 0 since we haven't typed anything in initially??

Klas Lindbäck
  • 33,105
  • 5
  • 57
  • 82
ajanakos
  • 99
  • 1
  • 9

3 Answers3

3

stdin behaves a bit different than most other streams.

First, input is line buffered. That means that input isn't available until you press enter. this explains while the h won't appear until you press enter.

Since it is a stream it doesn't really have an end. Instead of failing when there is no data to read, the call will block until some data is available (or until the program receives a signal). A socket works the same way.

The blocking behaviour can be turned off using fcntl :

int fd = STDIN_FILENO;
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
Community
  • 1
  • 1
Klas Lindbäck
  • 33,105
  • 5
  • 57
  • 82
1

The terminal is by default line buffered, because it is in canonical mode. From Linux manuals tcgetattr(3):

Canonical and noncanonical mode

The setting of the ICANON canon flag in c_lflag determines whether the terminal is operating in canonical mode (ICANON set) or noncanonical mode (ICANON unset). By default, ICANON set.

In canonical mode:

  • Input is made available line by line. An input line is available when one of the line delimiters is typed (NL, EOL, EOL2; or EOF at the start of line). Except in the case of EOF, the line delimiter is included in the buffer returned by read(2).

  • Line editing is enabled (ERASE, KILL; and if the IEXTEN flag is set: WERASE, REPRINT, LNEXT). A read(2) returns at most one line of input; if the read(2) requested fewer bytes than are available in the current line of input, then only as many bytes as requested are read, and the remaining characters will be available for a future read(2).

You can switch off canonical mode on the terminal by calling tcgetattr with proper flags. First of all disable the canonical mode; then set the timeout to 0; set minimum read to 1 for blocking reads or 0 for non-blocking reads. Usually it is customary to also disable local echo, otherwise everything you type would still be automatically visible (and displayed twice in your program):

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

int main() {
    struct termios old_settings, new_settings;
    int is_terminal;

    // check whether the stdin is a terminal to begin with
    if (is_terminal = isatty(STDIN_FILENO)) {     
        // get the old settings
        tcgetattr(STDIN_FILENO, &old_settings);

        new_settings = old_settings;

        // disable canonical mode and echo
        new_settings.c_lflag &= (~ICANON & ~ECHO);

        // at least one character must be written before read returns
        new_settings.c_cc[VMIN] = 1;

        // no timeout
        new_settings.c_cc[VTIME] = 0;

        tcsetattr(STDIN_FILENO, TCSANOW, &new_settings);
    }

    while (read(STDIN_FILENO, buf, 1) > 0) {
        // add this here so that you can verify that it is character by character,
        // and not the local echo from the terminal
        write(STDOUT_FILENO, ">", 1);
        write(STDOUT_FILENO, buf, sizeof(buf));
    }

    // finally restore the old settings if it was a terminal
    if (is_terminal) {
        tcsetattr(STDIN_FILENO, TCSANOW, &old_settings);
    }
    return 0;
}
0

If you still want the blocking to happen, but want to read character by character, you can use termios to configure how the input will be given to your program. See the code below.

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

int main()
{
    char buf[1];
    struct termios term, term_orig;

    if (tcgetattr(0, &term_orig)) {
        printf("tcgetattr failed\n");
        exit(-1);
    }

    term = term_orig;

    term.c_lflag &= ~ICANON;
    term.c_lflag |= ECHO;
    term.c_cc[VMIN] = 1;
    term.c_cc[VTIME] = 0;

    if (tcsetattr(0, TCSANOW, &term)) {
        printf("tcsetattr failed\n");
        exit(-1);
    }

    while (read(0, buf, 1) > 0) {
        write(1, buf, sizeof(buf));
    }
    return 0;
}
rools
  • 1,539
  • 12
  • 21
  • Use [`isatty(STDIN_FILENO)`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/isatty.html) to see whether this approach works, because standard input is not necessarily a terminal. –  Apr 01 '16 at 07:58