3

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?

Andre Kampling
  • 5,476
  • 2
  • 20
  • 47
IllegalCactus
  • 53
  • 3
  • 7
  • 1
    `if (*choice == '2') printf("Hi\n"); else printf("Oops: %x\n", *choice & 0xff );` – wildplasser Oct 01 '14 at 14:57
  • 1
    I'd probably use `fgets` with a larger buffer, say 80 chars or so. Or stick another loop in there that reads until the next newline. – Fred Larson Oct 01 '14 at 14:58
  • 3
    instead of doing `while(1)` why not just say `while(choice != 'q')`. Then eliminate the first if statement all together – urnotsam Oct 01 '14 at 14:59
  • 1
    google clearing the input buffer (***don't use `fflush` though***), you can't `getc` reliably on a dirty buffer – Elias Van Ootegem Oct 01 '14 at 15:01
  • Thanks Fred, that does fix the repeating prompt. Could anyone explain why this works, and why it was repeating? I've tried reading the reference and function definitions online but I'm still a bit unclear on how this works. – IllegalCactus Oct 01 '14 at 15:02
  • 1
    @Dylan When you press `2`, the 2 and a newline are both queued for reading on `stdin`. So your `getc` loop consumes the 2, runs again to consume the newline. This produces the extra prompt, but since you don't have a switch case for `\n`, that's all it does. Then it finally prints the prompt again and waits for the next input. The main idea to take away is that `getc` requires you to manage the newlines in the input buffer yourself, which can be painful. The `fgets` approach is generally simpler. – Gene Oct 01 '14 at 15:13

3 Answers3

3

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.

Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
2

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.

Tyler Durden
  • 11,156
  • 9
  • 64
  • 126
  • Thanks! I saw switching the terminal mode being offered as a solution on some other question but wasn't sure how to go about it. Problem is fixed now but this is an interesting aspect I didn't know about before. – IllegalCactus Oct 01 '14 at 18:41
1

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.

  • 1
    I've heard fflush() is supposed to be avoided too, but thanks for the explanation of input buffers. Making a lot more sense now. – IllegalCactus Oct 01 '14 at 15:29
  • @Dylan: Added to my answer why `fflush(stdin);` should be avoided. Basically: its behavior is undefined, and undefined behavior is bad – Elias Van Ootegem Oct 01 '14 at 15:39
  • See [Using `fflush(stdin)`?](http://stackoverflow.com/questions/2979209/using-fflushstdin) for more information. Note that it is defined and supported on Windows, and (despite some evidence in manual pages to the contrary) is not supported elsewhere in practice. – Jonathan Leffler Oct 01 '14 at 15:51