C - how to handle user input during a loop

I am new to C and I have a simple program that requires some user input inside a while loop and exits 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 it right. However, if I press '2', the program first prints “Hello” (as it should), but then continues to print the “Please select an option” prompt twice. If I enter N characters and hit enter, the query will print N times.

The same behavior happens when I use fgets () with a limit of 2.

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

EDIT

This way, using fgets () with a large buffer works and stops the reoccurring request issue:

fgets(choice, 80, stdin);

      

This helped: How do I clear the input buffer in C?

+3


source to share


3 answers


When you getc

type, it is important to note that the user has put more than one character: at least stdin

contains 2 characters:

2\n

      

when it getc

receives the user-entered "2", the trailing character \n

is still in the buffer, so you need to clear it. The easiest way to do it is to add this:

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

      

This should fix it



For completeness:
Note that it getc

returns an int, not a char

. Make sure to compile the flags -Wall -pedantic

and always check the return type of the functions you are using.

It is tempting to flush the input buffer with fflush(stdin);

, and on some systems this will work. However: This behavior is undefined: the standard clearly states that it is fflush

intended to be used in update / output buffers and not in input buffers
:

C11 7.21.5.2 Function fflush, fflush only works on the output / update stream, not the input stream

However, some implementations (for example Microsoft ) support fflush(stdin);

as an extension. Relying on this, however, goes against the philosophy behind C. C was supposed to be portable, and by adhering to the standard, you make sure your code is portable. By relying on a specific extension, it removes this advantage.

+3


source


What seems like a very simple problem is actually quite complex. The root of the problem is that the terminals operate in two different modes: raw and cooked. Cooking mode, which is the default, means that the terminal does not read characters, it reads lines. This way, your program never receives any input at all unless an entire line is entered (or the end of a file character is received). The way the terminal recognizes the end of the line is to receive a newline character (0x0A), which can be invoked by pressing the Enter key. To make it even more confusing, on a Windows machine, pressing Enter will generate two characters (0x0D and 0x0A).

So, your main problem is that you want a one-character interface, but your terminal is running in line-oriented (cooked) mode.

The correct solution is to switch the terminal to raw mode so that your program can receive characters as the user types them. Also, I would recommend using getchar()

and not getc()

in this use. The difference is that getc () takes a file descriptor as an argument, so it can read from any stream. The function getchar()

only reads the standard input you want. Therefore, this is a more specific choice. After your program finishes, it should switch the terminal back to the way it was, so you need to save the current state of the terminal before modifying it.

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



Here is a complete program that does the following:

#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 like a fairly simple problem is quite difficult to get it right.

A book that I can highly recommend for navigating these topics is Understanding Unix / Linux Programming by Bruce Mole. Chapter 6 details all of the above.

+2


source


The reason this is happening is because stdin is buffered.

When you hit the line of code * choice = getc (stdin); no matter how many characters you type, getc (stdin) will only fetch the first character. So if you type "foo" it gets "f" and sets * the selection to "f". The oo characters are still in the input buffer. In addition, the carriage return that was caused by you hitting the return key is also in the input buffer. Therefore, since the buffer is not empty, the next time the loop starts, and not while waiting for input, getc (stdin); will immediately return the next character to the buffer. Getc (stdin) will continue to immediately return the next character in the buffer until the buffer is empty. Therefore, in general, you will specify N the number of times you enter a string of length N.

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

EDIT: Apparently someone is saying that he is not using fflush (stdin); Go with what he says.

+1


source







All Articles