-1

I'm trying to write to a program that takes in an integer from the terminal and safely checks if it actually is an int and if it is equal to or in between 0 and 5. The code should also have retry logic.

This is the code that I have written:

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


int main(){
    int input;
    char i[3];
    char *p;

    while(fgets(i, (sizeof(i)),stdin)){
        input=strtol(i,&p,10);
        if(input<0 || input>5 || p==i || (*p)!='\n'){
            printf("please enter a integer larger or equal to 0 or smaller or equal to 5\n");   
        }
        else{
            printf("%d",input);
            break;
        }
    }
}

The problem I have encountered is that if I input more characters than fgets() reads such as "aaaaa", I get the output:

please enter a integer larger or equal to 0 or smaller or equal to 5
please enter a integer larger or equal to 0 or smaller or equal to 5
please enter a integer larger or equal to 0 or smaller or equal to 5 

I'm assuming the issue is that the characters that fgets does not read get left in the buffer and get read on subsequent iterations which trigger the if statement and therefore the repeated output. I'm assuming the solution is the somehow clear the buffer but from what I've read fflush() isn't allowed. Is there a simple way to clear the buffer? And how would you go about doing it, if that actually is the issue to the problem?

Daniel Walker
  • 6,380
  • 5
  • 22
  • 45
  • "Is there a simple way to clear the buffer?" --> Yes, `scanf("%*[^\n]"); scanf("%1[\n]");` or the usual `fgetc()` `while` loop. The tricky part is to knowing if the buffer needs clearing as it depends how pedantic you need code to be. e.g. `fgets()` has trouble when input includes _null characters_ or input errors. – chux - Reinstate Monica Dec 17 '21 at 22:54
  • If the input string does not contain a newline, there is more waiting to be read. `fgets` reads up to *and including* the next newline subject to buffer size and EOF. – Weather Vane Dec 17 '21 at 23:09
  • If you're only taking a single digit, then you don't really need `strtol` or `fgets` at all. Use `fgetc` to read one char and compare it to `'0'`. (eg, `c = getchar(); input = c - '0';` Then consume the remainder of the line with `while( (c = getchar()) != EOF && c != '\n' );` – William Pursell Dec 17 '21 at 23:31
  • You may want to take a look at my function `get_int_from_user`, which I wrote at the end of [this answer to another question](https://stackoverflow.com/a/69636446/12149471). I believe this function does everything you need, except for checking whether the input is between `0` and `5`, which you would have to do yourself. – Andreas Wenzel Dec 18 '21 at 01:59
  • Never be stingy with your input buffers. Especially if you're trying to be robust in the face of input errors. You're trying to handle the case that the user can type *anything*. So you have to assume that the user might type more than a couple of characters. So make your input buffer, `i[]`, pretty big, bugger than whatever reasonably long line you expect your user to type. I would declare `char i[512];` at the very smallest, or these days maybe `char i[1024];`. Memory is cheap! – Steve Summit Dec 18 '21 at 04:34

2 Answers2

1

A simplistic approach starts with a bigger buffer.

#define BUF_N 100
char buf[BUF_N];

Then a not so simple approach that tests for all sort of troubles. I suspect simplifications exists, but at least this one handles many corners and is illustrative.

For simplicity, if the input line is excessively long, let us just say the input line is bad, regardless of text read.

  #define BUF_N 100
  char buf[BUF_N];

  while (fgets(buf, sizeof buf, stdin)) {
    // If entire buffer filled, the line may be longer, read it.
    if (strlen(buf) == BUF_N - 1 && buf[BUF_N - 2] != '\n') {
      printf("Excessive input\n");
      // Consume rest of the line 
      int ch;
      while ((ch = fgetc(stdin)) != '\n') {
        if (ch == EOF) {
          return EOF;
        }
      }
      continue;
    }

    char *p;
    long input = strtol(buf, &p, 10); // use long

    if (buf == p) {
      printf("Non-numeric input\n");
      continue;
    }

    if (input < 0 || input > 5) {
      printf("Out of range %ld\n", input);
      continue;
    }

    // Look for trailing junk
    while (isspace(*(unsigned char* )p)) {
      p++;
    }
    if (*p) {
      printf("Trailing junk\n");
      continue;
    }

    return (int) input;
  }
  return EOF;
}

The above is not so tolerant of reading null characters.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • I like it that you are checking for "trailing junk". This is the first time I have seen someone else do that. Most code I see simply accepts input such as `6sdfj23jlj` as valid input for the number `6`, although it should probably be rejected. – Andreas Wenzel Dec 18 '21 at 04:00
  • @AndreasWenzel Perhaps a simpler test would be `if (sscanf(buf, "%4d %c", &int_input, &ch) == 1 && int_input >= 0 && int_input <= 5)`? – chux - Reinstate Monica Dec 18 '21 at 04:10
  • @AndreasWenzel For me, a pedantic test would also handle reading _null characters_, something `fgets()` is ill-equipped to report. – chux - Reinstate Monica Dec 18 '21 at 04:12
  • Yes, that `sscanf` `if` condition would probably work nicely, except for the fact that the behavior is undefined if the result of the conversion is not representable as an `int`. That is why I prefer the function `strtol`, as the behavior of that function is well-defined in all cases. – Andreas Wenzel Dec 18 '21 at 04:25
  • @AndreasWenzel All 4 character numeric inputs representable as an `int`, hence the `"%4d"`. True it won't handle `"000001"`. – chux - Reinstate Monica Dec 18 '21 at 04:28
  • Ah, yes, good thinking. As far as I can tell, that is guaranteed to be representable as an `int`. – Andreas Wenzel Dec 18 '21 at 04:31
  • @AndreasWenzel Yes, a bit of CYA code. – chux - Reinstate Monica Dec 18 '21 at 04:33
1

Calling the function fgets with a buffer size of 3 will read at most one character, in addition to the newline character and the terminating null character. If the input is supposed to be a number between 0 and 5, then this behavior is acceptable, but you should be aware that your program will be unable to read input such as 05, even though it fulfills the range requirement.

If you want to allow input such as 05, then you should increase the buffer size.

You can easily detect whether the user entered more characters into the line than fit into the buffer. Since the function fgets also reads and stores the newline character into the buffer, you can check for the existance of this newline character, for example using the function strchr. If you determine that fgets did not read a newline character, then this means that the input was too long to fit into the buffer. In that case, you probably want to reject the input and discard the remainder of the input, so that it does not get read by the next call to fgets. This is what was happening in your program, which caused the same line to be processed multiple times.

One way of discarding the remainder of the input is to call getchar, which reads one character at a time. You should repeat this until you encounter the newline character, which means that the end of the line has been reached.

This is how the code could look:

while( fgets( i, sizeof i, stdin ) != NULL )
{
    //verify that entire line of input was read
    if ( strchr( i, '\n' ) == NULL )
    {
        int c;

        //verify that stream state is as expected
        if ( feof(stdin) || ferror(stdin) )
        {
            fprintf( stderr, "unexpected input error!\n" );
            exit( EXIT_FAILURE );
        }

        //print error message
        printf( "Line input was too long to fit into buffer!\n" );

        //discard remainder of line
        do
        {
            c = getchar();

            if ( c == EOF )
            {
                fprintf( stderr, "unexpected input error!\n" );
                exit( EXIT_FAILURE );
            }

        } while ( c != '\n' );

        continue;
    }

    //input was successful, so we can now process it with strtol
}

Note that you must additionally #include <string.h> in order to use the function strchr.

Andreas Wenzel
  • 22,760
  • 4
  • 24
  • 39
  • `if ( strchr( i, '\n' ) == NULL )` is an issue when the last line of input was like `"123"` with no `'\n'`as "then this means that the input was too long to fit into the buffer." is not true. Last lines without a `'\n'` are contentious corners. – chux - Reinstate Monica Dec 18 '21 at 04:16
  • @chux: OP seems to be dealing with user input. Therefore, introducing checks for end-of-file seem counter-intuitive and will probably confuse OP, even if it is possible to enter end-of-file in a terminal. However, I do agree that when dealing with input that could have originated from some other source, that it is important to check for end-of-file without a preceding newline character, even if [POSIX defines a line as always ending with a newline character](https://unix.stackexchange.com/a/404727). – Andreas Wenzel Dec 18 '21 at 04:42
  • Yes, many issues reading a _line_. – chux - Reinstate Monica Dec 18 '21 at 04:55
  • @chux: Actually, I was being a bit inconsistent in my code by checking for `EOF` in the `do` loop, but not checking for an error or end-of-file before concluding that the input was too long to fit into the buffer. Therefore, I have now decided to add these checks and have modified my code accordingly. – Andreas Wenzel Dec 18 '21 at 15:53