2

I am having trouble with the stdin buffer, I'd appreciate any insight, I have this function which accepts user input for last name

void lastName(int *counter, User *pt) {

    for (int i = *counter; i < (*counter + 1); i++) {
        pt[i].lastName = calloc (MAX_LENGTH, sizeof(char));
        printf("Enter Last Name: ");
        fgets(pt[i].lastName, MAX_LENGTH, stdin);
        strtok(pt[i].lastName, "\n");
    }
}

I also have this function that accepts user input for ID

void id(int *counter, User *pt) {

    char num[MAX_LENGTH];
    long temp;
    for (int i = *counter; i < *counter + 1; i++) {
        printf("Enter the ID of %s: ", pt[i].firstName);
        fgets(num, MAX_LENGTH, stdin);
        strtok(num, "\n");
        temp = strtol(num, NULL, 10);
        pt[i].id = (int) temp;
    }
}

This is how I am calling them in main

lastName(&counter, pt);
id(&counter, pt);

If i enter a last name which is very long, it gets cut and displayed to MAX_LENGTH but the fgets for the ID gets skipped, otherwise it works fine. I am wondering as to how my fgets is working in this case? MAX_LENGTH is 10. I tried clearing my buffer with while(getchar() != '\n'); and it works but I have to press enter twice.

Enter First Name: Test
Enter Last Name: Williamsamsmases
Enter the ID of Test: Would you like to enter a User?(Y/N):N
Kevin H
  • 21
  • 3
  • 3
    It's not getting skipped. It is eating up whatever the previous call haven't finished. – Eugene Sh. Apr 16 '18 at 20:01
  • 1
    If you enter a line that is longer than what the destination buffer can hold, then `fgets` will read everything it can until the destination is full. The rest of the input remains in the input buffer and, so the next `fgets` call reads what has been left behind. – Pablo Apr 16 '18 at 20:03
  • Do you recommend something to clear the buffer with? fflush(stdin) doesn't seem to work for me, neither does fseek(stdin, 0, SEEK_END) – Kevin H Apr 16 '18 at 20:07
  • 2
    `fflush(stdin)` is undefined behavior. You can `getchar` until `\n` encountered. https://stackoverflow.com/questions/7898215/how-to-clear-input-buffer-in-c – Eugene Sh. Apr 16 '18 at 20:10
  • 1
    `stdin` is not a seek-able stream, you cannot use `fseek` with `stdin` – Pablo Apr 16 '18 at 20:18
  • @KevinH: Welcome to StackOverflow. Please accept the best answer (click the check mark) if one is acceptable, and up-vote any comments that you found helpful. (a) That's how reputation points are earned, and (b) when looking for unanswered questions to look at, we'll see you accepted an answer. – Jeff Learman Apr 16 '18 at 20:39
  • Short answer: change `MAX_LENGTH` to 1000 or so. You always want to give `fgets` a buffer comfortably larger than any line you expect to read. Although `fgets`'s behavior is well-defined when the buffer isn't big enough, and although there are things you can do to detect and deal with the problem, it's more trouble than it's worth. Just make the buffer bigger. – Steve Summit Apr 16 '18 at 21:07
  • Don't ask how to flush the input buffer! That usually indicates you've done something wrong. Well-written programs don't have to clear the input buffer. (Clearing the input buffer throws away the user's work, and you usually don't want to throw away the user's work.) Only throw away input if the user has made a mistake, and your input parsers need to resynchronize. – Steve Summit Apr 16 '18 at 21:10
  • Is it a "mistake" for the user to enter a name larger than 10? Perhaps (although that's what you might call "user-hostile behavior"). But if there's a limit on things like name length that you need to enforce, trying to get `fgets` to enforce that limit for you is usually not the right way of doing it. – Steve Summit Apr 16 '18 at 21:11
  • Oh, I see. It's not just `num` that might overflow when you read into it, it's `pt[i].lastName`, and that *is* limited. So do something like `char tmpbuf[1000]` and then `fgets(tmpbuf, sizeof(tmpbuf), stdin)` and then `if(strlen(tmpbuf) > MAX_LENGTH-1) fprintf(stderr, "name too long!\n")` and then `strcpy(pt[i].lastName, tmpbuf)`. – Steve Summit Apr 16 '18 at 21:14

2 Answers2

2

If you enter a line that is longer than what the destination buffer can hold, then fgets will read everything it can until the destination is full. The rest of the input remains in the input buffer and, so the next fgets call reads what has been left behind.

If a newline is not present in the destination buffer, you have 3 options:

  1. the entered line is longer than what the destination can hold. In that case you have to keep calling fgets until a newline is in the buffer.
  2. You can "clean" the input buffer with this function:

    void clean_stdin(void)
    {
        int c;
        while((c = getchar()) != '\n' && c != EOF);
    }
    

    and then you can call it when you realize that there is no newline in the destination buffer. But don't call it every time you call fgets, you can only call this function when you know that there are characters left in the input buffer (when there is no newline in the destination buffer), otherwise the function will wait for user input.

  3. If your system has support for getline, then you can use it to read the whole line and getline will take care of allocating the memory needed for it:

    char *line = NULL;
    size_t n = 0;
    
    if(getline(&line, &n, stdin) == -1)
    {
        // error
    }
    
    ...
    free(line);
    

    If getline is not available on your system, then you could write a function that emulates getline, one like this:

    char *fgets_long(FILE *fp)
    {
        size_t size = 0, currlen = 0;
        char line[1024];
        char *ret = NULL, *tmp;
    
        while(fgets(line, sizeof line, fp))
        {
            int wholeline = 0;
            size_t len = strlen(line);
    
            if(line[len - 1] == '\n')
            {
                line[len-- - 1] = 0;
                wholeline = 1;
            }
    
            if(currlen + len >= size)
            {
                // we need more space in the buffer
                size += (sizeof line) - (size ? 1 : 0);
                tmp = realloc(ret, size);
                if(tmp == NULL)
                    break; // return all we've got so far
                ret = tmp;
            }
    
            memcpy(ret + currlen, line, len + 1);
            currlen += len;
    
            if(wholeline)
                break;
        }
    
        if(ret)
        {
            tmp = realloc(ret, currlen + 1);
            if(tmp)
                ret = tmp;
        }
    
        return ret;
    }
    

    This function works like fgets but it guarantees that you read the whole line, the function takes care of allocation enough memory for the destination.

Pablo
  • 13,271
  • 4
  • 39
  • 59
1

When you call fgets, it'll read input from stdin until the variable being inserted into is full. Once it is, the rest of the input on the input buffer remains there. One solution is to clear the input buffer after reading each value.

int ch;

while ((ch = getchar()) != '\n' && ch != EOF);

You essentially read from the buffer and discard the values until a \n or EOF is reached.

Jimenemex
  • 3,104
  • 3
  • 24
  • 56