2

Good afternoon, my question is conceptual. How can I make it generate a "fancy" error when the user incorrectly enters some data that does not correspond to the scanf() function? So as to only allow integers to be entered in the example below (not characters or array of characters or an inappropriate data).

For example:

#include <stdio.h>

int a;
printf("Enter a number\n");
scanf("%d", &a); //the user is supposed to enter a number
printf("Your number is %d ", a);

//but if the user enters something inappropriate, like a character, the program leads to 
//undetermined behavior (which as I understand it interprets said character according to its 
//value in the ASCII code).

From already thank you very much

Juan598
  • 29
  • 3
  • 2
    The best solution is not to use `scanf` to read the input buffer. You should probably read the input buffer using `fgets` or a similar function, and then try to parse the line afterwards. If you then choose to use `sscanf`, always check the return value and deal with it properly. – Cheatah Sep 24 '22 at 20:32
  • 4
    `scanf` isn't as easy as it would seem. Forget that it exists, use e.g. [`fgets`](https://en.cppreference.com/w/c/io/fgets) to read whole lines of text, then possibly use `sscanf` to parse the string. And always check what `sscanf` [*returns*](https://en.cppreference.com/w/c/io/fscanf#Return_value). – Some programmer dude Sep 24 '22 at 20:33
  • 1
    My advice is contained in [this answer](https://stackoverflow.com/questions/73836358#73837424) to an unrelated question coincidentally posted just this morning. – Steve Summit Sep 24 '22 at 20:34
  • Also your understanding of the `scanf` family of function is not entirely correct. If `scanf` fails it doesn't extract any characters at all from the input buffer. What's in the buffer will stay in the buffer. – Some programmer dude Sep 24 '22 at 20:36
  • 4
    My opinion is that if you want to do the "fancy" input you're suggesting, it is simply *not possible* to do it using `scanf`. If you work really hard, you might be able to get, say, 80% of the robust error checking you're looking for, but you'll do three to five times as much work as if you just used `fgets`+`strtol`, and with `fgets`+`strtol` you can get 100%. (Don't get me wrong: trying to do "fancy", "robust" user input is a worthy and noble goal. But do yourself a favor and pursue an avenue that's not based on the cursed `scanf`.) – Steve Summit Sep 24 '22 at 20:37
  • @SteveSummit Alas, I think that dealing with input properly is one of those things that attracts a lot of karmic entropy. This question never seems to get old. Cheers! – Dúthomhas Sep 24 '22 at 21:00
  • @SteveSummit I don't think it's a good idea to call `scanf` cursed, but rather misunderstood, as people use it in the wrong way for the wrong reasons. It's like calling a shoe cursed when trying to use it to hammer a nail. If you are using the wrong tool, then no wonder you're going to have an unpleasant time with it. But I understand what you are trying to say. – Pablo Sep 25 '22 at 00:35
  • It's worse than that. Even if you do everything right with fgets, EOF checking, strtol, checking errno, and making sure there are no extra characters, you still can't be sure whether the input is correct until you echo the input back and ask the user whether or not the input was parsed correctly, as sometimes users make typos, and occasionally they will still make a secondary typo. – Dmytro Sep 25 '22 at 10:48
  • Does this answer your question? [How can I validate scanf numeric input](https://stackoverflow.com/questions/64260139/how-can-i-validate-scanf-numeric-input) – anastaciu Sep 27 '22 at 11:19

2 Answers2

0

In order to determine whether scanf was able to successfully convert the input to an integer, you should check the return value of scanf:

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

int main( void )
{
    int num;

    printf( "Enter a number: " );
    if ( scanf( "%d", &num ) != 1 )
    {
        printf( "Failed to convert input!\n" );
        exit( EXIT_FAILURE );
    }

    printf( "Conversion successful! The number is %d.\n", num );
}

However, using scanf for line-based user input is generally not recommended, because scanf does not behave in an intuitive manner when dealing with that kind of input. For example, scanf will generally not consume an entire line of input at once. Instead, it will generally only consume the input that matches the argument, but will leave the rest of the line on the input stream, including the newline character.

Leaving the newline character on the input stream can already cause a lot of trouble. For example, see this question.

Also, if the user enters for example 6abc, then scanf will successfully match the 6 and report success, but leave abc on the input stream, so that the next call to scanf will probably immediately fail.

For this reason, it is generally better to always read one line of input at a time, using the function fgets. After successfully reading one line of input as a string, you can use the function strtol to attempt to convert the string to an integer:

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

int main( void )
{
    char line[200], *p;
    int num;

    //prompt user for input
    printf( "Enter a number: " );

    //attempt to read one line of input
    if ( fgets( line, sizeof line, stdin ) == NULL )
    {
        printf( "Input failure!\n" );
        exit( EXIT_FAILURE );
    }

    //attempt to convert string to integer
    num = strtol( line, &p, 10 );
    if ( p == line )
    {
        printf( "Unable to convert to integer!\n" );
        exit( EXIT_FAILURE );
    }

    //print result
    printf( "Conversion successful! The number is %d.\n", num );
}

However, this code has the following issues:

  1. It does not check whether the input line was too long to fit into the buffer.

  2. It does not check whether the converted number is representable as an int, for example whether the number is too large to be stored in an int.

  3. It will accept 6abc as valid input for the number 6. This is not as bad as scanf, because scanf will leave abc on the input stream, whereas fgets will not. However, it would probably still be better to reject the input instead of accepting it.

All of these issues can be solved by doing the following:

Issue #1 can be solved by checking

  • whether the input buffer contains a newline character, or
  • whether end-of-file has been reached, which can be treated as equivalent to a newline character, because it also indicates the end of the line.

Issue #2 can be solved by checking whether the function strtol set errno to the value of the macro constant ERANGE, to determine whether the converted value is representable as a long. In order to determine whether this value is also representable as an int, the value returned by strtol should be compared against INT_MIN and INT_MAX.

Issue #3 can be solved by checking all remaining characters on the line. Since strtol accepts leading whitespace characters, it would probably also be appropriate to accept trailing whitespace characters. However, if the input contains any other trailing characters, the input should probably be rejected.

Here is an improved version of the code, which solves all of the issues mentioned above and also puts everything into a function named get_int_from_user. This function will automatically reprompt the user for input, until the input is valid.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>

int get_int_from_user( const char *prompt )
{
    //loop forever until user enters a valid number
    for (;;)
    {
        char buffer[1024], *p;
        long l;

        //prompt user for input
        fputs( prompt, stdout );

        //get one line of input from input stream
        if ( fgets( buffer, sizeof buffer, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //make sure that entire line was read in (i.e. that
        //the buffer was not too small)
        if ( strchr( buffer, '\n' ) == NULL && !feof( stdin ) )
        {
            int c;

            printf( "Line input was too long!\n" );

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

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

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

            continue;
        }

        //attempt to convert string to number
        errno = 0;
        l = strtol( buffer, &p, 10 );
        if ( p == buffer )
        {
            printf( "Error converting string to number!\n" );
            continue;
        }

        //make sure that number is representable as an "int"
        if ( errno == ERANGE || l < INT_MIN || l > INT_MAX )
        {
            printf( "Number out of range error!\n" );
            continue;
        }

        //make sure that remainder of line contains only whitespace,
        //so that input such as "6abc" gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Unexpected input encountered!\n" );

                //cannot use `continue` here, because that would go to
                //the next iteration of the innermost loop, but we
                //want to go to the next iteration of the outer loop
                goto continue_outer_loop;
            }
        }

        return l;

    continue_outer_loop:
        continue;
    }
}

int main( void )
{
    int number;

    number = get_int_from_user( "Enter a number: " );

    printf( "Input was valid.\n" );
    printf( "The number is: %d\n", number );

    return 0;
}

This program has the following behavior:

Enter a number: abc
Error converting string to number!
Enter a number: 6000000000
Number out of range error!
Enter a number: 6 7 8
Unexpected input encountered!
Enter a number: 6abc
Unexpected input encountered!
Enter a number: 6
Input was valid.
The number is: 6
Andreas Wenzel
  • 22,760
  • 4
  • 24
  • 39
-1

How to get verified user input of a specific type

#1 Get user input as a string

char s[100];
if (!fgets( s, sizeof(s), stdin )) *s = '\0';
char * p = strptok( s, "\r\n" );
if (!p) complain_and_quit();
*p = '\0';

...

Alternately:

#define __STDC_WANT_LIB_EXT2__ 1
#include <stdio.h>

 

char * s = NULL;
size_t n = 0;
if (getline( &s, &n, stdin ) < 0)
{
  free( s );
  complain_and_quit();
}

...

free( s );

#2 Get rid of any trailing whitespace

This could easily be put in a trim() function, but here we’ll spell it out:
Can’t believe I forgot this step. Sorry.

p = strchr( s, '\0' );
while (p-- != s) if (!isspace( *p )) break;
p[1] = '\0';

#3 Try to convert that string to the type of thing you want.

char * p;
int user_input = strtol( s, &p, 10 );
if (*p)
{
  // Input was not JUST an integer.
  // It could be something like "123 xyz", or "not-an-integer".
  // Look at the value of p to figure out where the conversion went wrong.
  complain();
}

do_something_with_an_integer( user_input );

That’s it!

Dúthomhas
  • 8,200
  • 2
  • 17
  • 39