IllegalCactus
IllegalCactus

Reputation: 53

C - how to handle user input in a while loop

I'm new to C and I have a simple program that takes some user input inside a while loop, and quits if the user presses 'q':

while(1)
{
   printf("Please enter a choice: \n1)quit\n2)Something");
   *choice = getc(stdin);

   // Actions.
   if (*choice == 'q') break;
   if (*choice == '2') printf("Hi\n");
}

When I run this and hit 'q', the program does quit correctly. However if I press '2' the program first prints out "Hi" (as it should) but then goes on to print the prompt "Please choose an option" twice. If I enter N characters and press enter, the prompt gets printed N times.

This same behaviour happens when I use fgets() with a limit of 2.

How do I get this loop working properly? It should only take the first character of input and then do something once according to what was entered.

EDIT

So using fgets() with a larger buffer works, and stops the repeated prompt issue:

fgets(choice, 80, stdin);

This kind of helped: How to clear input buffer in C?

Upvotes: 3

Views: 5996

Answers (3)

Tyler Durden
Tyler Durden

Reputation: 11562

What seems to be a very simple problem is actually pretty complicated. The root of the problem is that terminals operate in two different modes: raw and cooked. Cooked mode, which is the default, means that the terminal does not read characters, it reads lines. So, your program never receives any input at all unless a whole line is entered (or an end of file character is received). The way the terminal recognizes an end of line is by receiving a newline character (0x0A) which can be caused by pressing the Enter key. To make it even more confusing, on a Windows machine pressing Enter causes TWO characters to be generated, (0x0D and 0x0A).

So, your basic problem is that you want a single-character interface, but your terminal is operating in a line-oriented (cooked) mode.

The correct solution is to switch the terminal to raw mode so your program can receive characters as the user types them. Also, I would recommend the use of getchar() rather than getc() in this usage. The difference is that getc() takes a file descriptor as an argument, so it can read from any stream. The getchar() function only reads from standard input, which is what you want. Therefore, it is a more specific choice. After your program is done it should switch the terminal back to the way it was, so it needs to save the current terminal state before modifying it.

Also, you should handle the case that the EOF (0x04) is received by the terminal which the user can do by pressing CTRL-D.

Here is the complete program that does these things:

#include    <stdio.h>
#include    <termios.h>
main(){
    tty_mode(0);                /* save current terminal mode */
    set_terminal_raw();         /* set -icanon, -echo   */
    interact();                 /* interact with user */
    tty_mode(1);                /* restore terminal to the way it was */
    return 0;                   /* 0 means the program exited normally */
}

void interact(){
    while(1){
        printf( "\nPlease enter a choice: \n1)quit\n2)Something\n" );
        switch( getchar() ){
            case 'q': return;
            case '2': {
               printf( "Hi\n" );
               break;
            }
            case EOF: return;
        }
    }
}

/* put file descriptor 0 into chr-by-chr mode and noecho mode */
set_terminal_raw(){
    struct  termios ttystate;
    tcgetattr( 0, &ttystate);               /* read current setting */
    ttystate.c_lflag          &= ~ICANON;   /* no buffering     */
    ttystate.c_lflag          &= ~ECHO;     /* no echo either   */
    ttystate.c_cc[VMIN]        =  1;        /* get 1 char at a time */
    tcsetattr( 0 , TCSANOW, &ttystate);     /* install settings */
}

/* 0 => save current mode  1 => restore mode */
tty_mode( int operation ){
    static struct termios original_mode;
    if ( operation == 0 )
        tcgetattr( 0, &original_mode );
    else
        return tcsetattr( 0, TCSANOW, &original_mode ); 
}

As you can see, what seems to be a pretty simple problem is quite tricky to do properly.

A book I can highly recommend to navigate these matters is "Understanding Unix/Linux Programming" by Bruce Molay. Chapter 6 explains all the things above in detail.

Upvotes: 2

Elias Van Ootegem
Elias Van Ootegem

Reputation: 76408

When you getc the input, it's important to note that the user has put in more than one character: at the very least, the stdin contains 2 chars:

2\n

when getc gets the "2" the user has put in, the trailing \n character is still in the buffer, so you'll have to clear it. The simplest way here to do so would be to add this:

if (*choice == '2')
    puts("Hi");
while (*choice != '\n' && *choice != EOF)//EOF just in case
    *choice = getc(stdin);

That should fix it

For completeness:
Note that getc returns an int, not a char. Make sure to compile with -Wall -pedantic flags, and always check the return type of the functions you use.

It is tempting to clear the input buffer using fflush(stdin);, and on some systems, this will work. However: This behavior is undefined: the standard clearly states that fflush is meant to be used on update/output buffers, not input buffers:

C11 7.21.5.2 The fflush function, fflush works only with output/update stream, not input stream

However, some implementations (for example Microsoft) do support fflush(stdin); as an extension. Relying on it, though, goes against the philosophy behind C. C was meant to be portable, and by sticking to the standard, you are assured your code is portable. Relying on a specific extension takes away this advantage.

Upvotes: 3

Michael S. Miller
Michael S. Miller

Reputation: 437

The reason why this is happening is because stdin is buffered.

When you get to the line of code *choice = getc(stdin); no matter how many characters you type, getc(stdin) will only retrieve the first character. So if you type "foo" it will retrieve 'f' and set *choice to 'f'. The characters "oo" are still in the input buffer. Moreover, the carriage return character that resulted from you striking the return key is also in the input buffer. Therefore since the buffer isn't empty, the next time the loop executes, rather than waiting for you to enter something, getc(stdin); will immediately return the next character in the buffer. The function getc(stdin) will continue to immediately return the next character in the buffer until the buffer is empty. Therefore, in general it will prompt you N number of times when you enter a string of length N.

You can get around this by flushing the buffer with fflush(stdin); immediately after the line *choice = getc(stdin);

EDIT: Apparently someone else is saying not to use fflush(stdin); Go with what he says.

Upvotes: 1

Related Questions