2

I would like to implement a function that read an integer but this function should be:

  • Resilient to \n
  • Robust against ^D (EOF)
  • Compliant with printf "42 20 10" | ./a.out

For now I wrote this but I feel it is ugly and too complicated:

#include <stdio.h>
#include <stdbool.h>

int read_integer(char *text, int min, int max, int nom) {
    int n;
    bool failure = false;

    do {
        printf("%s [%d] ? : ", text, nom);

        // Slurp spaces    
        scanf("%*[\t ]");

        // Hack to capture default value
        char buf[2];
        if (scanf("%1[\n]", buf) == 1) {
            return nom;
        }

        if (failure = (scanf("%d", &n) == 0 || n < min || n > max)) {
            if (feof(stdin)) {
                printf("\n");
                return nom;
            }
            printf("Error: value should be between %d and %d!\n\n", min, max);
            scanf("%*[^\n]%*1[\n]");
        }     

    } while(failure);

    scanf("%*[^\n]%*1[\n]");

    return n;
}

int main(void) {
    do {
        printf("You said %d\n", read_integer("What's the answer", 10, 50, 42));        
    } while(!feof(stdin));
}

Is there a better way?

Currently it does not work, the line before the end captured 42 which was never entered, and a new line is not displayed:

$ gcc main.c && ./a.out
What's the answer [42] ? : oops
Error: value should be between 10 and 50!

What's the answer [42] ? : 100
Error: value should be between 10 and 50!

What's the answer [42] ? : You said 42
What's the answer [42] ? :

EDIT

From the comments, I tried to write the same using fgets, but still it is not perfect. Or at least very complicated...

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

/**
 * Read an integer from `stdin`.
 * @param min Minimum accepted value
 * @param max Maximum accepted value
 * @param nom Default value
 * @return captured integer
 */
int read_integer(char *text, int min, int max, int nom) {
    int n = nom;
    bool failure = false;

    do {
        printf("%s [%d] ? : ", text, nom);

        // Read user input
        char line[24];
        do {
            if (fgets(line, sizeof(line), stdin) != line || feof(stdin)) {
                exit(EXIT_FAILURE);
                break;
            }
        } while (strchr(line, '\n') == NULL);

        // Default value?
        {
            char *cursor = line;
            while ((*cursor == ' ' || *cursor == '\t') && *cursor != '\0') {
                cursor++;
            }        
            if (*cursor == '\n') {
                return n;
            }
        }

        // Not a number ?
        if (sscanf(line, "%d", &n) != 1) {
            printf("Error: this is not valid entry!\n\n");
            continue;
        } 

        // Not in the range ?
        if (n < min || n > max) {
            printf("Error: value should be between %d and %d!\n\n", min, max);
            continue;
        }

        return n;
    } while(true);
}

int main() {
    do {
        printf("You said %d\n", 
            read_integer("What's the answer", 10, 50, 42));        
    } while(!feof(stdin));
}
nowox
  • 25,978
  • 39
  • 143
  • 293
  • 7
    The simplest way is to read a line with `fgets()` and then parse it with `sscanf()`. – Barmar Nov 19 '19 at 22:08
  • 6
    Any time you're trying to do something complicated, the answer is "Don't use `scanf`." Anything complicated you try to do with `scanf` is almost invariably (a) more trouble than it's worth or (b) doomed to failure in the end. If you simply use `"%d"`, and check `scanf's` return value, you'll achieve all you can reasonably achieve with `scanf`. If you want something more than that, do yourself a favor and pursue a non-`scanf` solution. – Steve Summit Nov 19 '19 at 22:11
  • 1
    I agree, scanf is evil, and should rarely be used. But is this possible to do it with only the use of scanf ? – nowox Nov 19 '19 at 22:13
  • https://stackoverflow.com/q/5431941/6699433 – klutt Nov 19 '19 at 22:14
  • 2
    @nowox It's possible to drive nails with a screwdriver -- but it's a bad idea, and it's not even a fun challenge. No one will envy your macho hackerly studliness if you, somehow, manage to do this with only the use of `scanf` -- they'll just shake their heads at the waste of time, especially since it's *so much* easier to achieve the same result using, say, `fgets` and `strtol`. `scanf` is a cruel hoax which has wasted untold thousands of beginning programmers' hours and somehow continues to do so even though we all know it's evil. – Steve Summit Nov 19 '19 at 22:17
  • 2
    *Proper way to read integer with scanf?* **DON'T**. It's fragile and when it does break you have no reliable way to recover. Use `fgets()`/`getline()` to read a line, then use `strtol()` to parse the numbers. `strtol()` skips leading spaces and returns the address of the first character it couldn't convert - use it to "walk" the line until either `strtol()` fails or you run out of line to parse. – Andrew Henle Nov 19 '19 at 22:26
  • @Barmar, the problem with `fgets` and `sscanf` is that you will need to create a buffer. I would if possible avoid using a separated buffer. – nowox Nov 20 '19 at 07:30

1 Answers1

4

Use fgets and strtol, and don't forget to complain if strtol ignores extra characters (use the optional endptr to check it). Put the fgets and strtol into a function, and add your validation around the call to that function, so you don't repeat the same code every time you read an integer.

cliffordheath
  • 2,536
  • 15
  • 16